パントレ開発部

【MapLibre】複数のテキストを表示し、クリックすると画像付きポップアップが開くようにする

Programming

 当サイトは海外旅ブログまとめサイトです。トップページをご覧になるとお分かりになるかと思いますが、地図上の地名をクリックすると画像付きポップアップが開くようになっています。このページでは、そのような地図の実装方法について説明したいと思います。
 MapLibre の基本的な使い方はこちら。

複数のテキストを記載する

 実装例


 テキストを配置する場合(タイル自体に文字があってすいません…)、一旦 GeoJSON で定義してあげて、map.addSource で ソースとして追加し、addLayer で追加したソースを参照するのが良いかなと思います。
 ちなみに MapLibre では、Web フォントを表示するための機能として、Glyphs 指定が出来るようになっています。(逆に書いておかないとエラーになります。)

https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf

 上の図のズームレベルを変えると分かるかと思いますが、文字が重なりそうになると自動的に優先度の低い文字が消えるようになっています。この優先度は、GeoJSON で定義する順番で決まっているみたいです(いわゆる z-index のような指定は出来ないみたいです)。OpenLayers みたいに、ズームレベルを検知するイベントをつけなくてもいいところはかなり便利ですね。

 全体のソースコードは以下のようになります。

<!DOCTYPE html>
<html>
<head>
<title>htmlMap</title>
<meta http-equiv='content-type' charset='utf-8'>
<meta name='viewport' content='width=device-width'>
</head>
<body>
<!-- 埋め込みマップのdivタグ。マップサイズはwidth(幅)とheight(高さ)で決まる -->
<div id='mapcontainer' style='width:100%; height:300px; z-index:0; border:1px solid #333;'></div>

<!-- 以下 MapLibre のJavaScriptとCSS -->
<link rel='stylesheet' href='https://unpkg.com/maplibre-gl/dist/maplibre-gl.css' />
<script src='https://unpkg.com/maplibre-gl/dist/maplibre-gl.js'></script>


<script>
function init_map() {

    const places = {
        'type': 'FeatureCollection',
        'features': [{
            'type': 'Feature',
            'properties': {
            'description': '札幌',
        },
        'geometry': {
            'type': 'Point',
            'coordinates': [141.3507794, 43.0686498]
        }},{
            'type': 'Feature',
            'properties': {
            'description': '函館',
        },
        'geometry': {
            'type': 'Point',
            'coordinates': [140.7257372, 41.7742072]
        }},{
            'type': 'Feature',
            'properties': {
            'description': '苫小牧',
        },
        'geometry': {
            'type': 'Point',
            'coordinates': [141.5968821, 42.6400714]
        }}
    ]};

    const map =  new maplibregl.Map({
        container: 'mapcontainer',
        style: {
            version: 8,
            glyphs: 'https://maps.gsi.go.jp/xyz/noto-jp/{fontstack}/{range}.pbf',
            sources: {
                rtile: {
                    type: 'raster',
                    tiles: [
                        'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
                    ],
                    tileSize: 256,
                    attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html">国土地理院</a>',
                },
            },
            layers: [{
                id: 'raster-tiles',
                type: 'raster',
                source: 'rtile',
                minzoom: 0,
                maxzoom: 18,
            }]
        },
        center: [142.14, 43.65], 
        zoom:  5, 
        pitch: 0 
    });

    map.on('load', () => {

        map.addSource('places', {
            'type': 'geojson',
            'data': places
        });

        map.addLayer({
            'id': 'places',
            'type': 'symbol',
            'source': 'places',
            'layout': {
                'text-field': ['get', 'description']
            }
        });
    });
}

//ダウンロード時に初期化する
window.addEventListener('DOMContentLoaded', init_map());

</script>
</body>
</html>

 

テキストにクリックイベントを付加する

 応用編です。テキストにクリックイベントを付加し、ポップアップが開くようにします。クリックイベントを付加する相手として、map.addSource で ソースとして追加した places を指定すると、ループを回さなくても複数のテキストに対してクリックイベントを付加できます。

map.on('click', 'places', (e) => {
    const coordinates = e.features[0].geometry.coordinates.slice();

    while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
        coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
    }

    new maplibregl.Popup().setLngLat(coordinates).setHTML('HTML').addTo(map);
});

 ついでに文字を見やすくするため、フォントサイズ、文字色と、文字の縁取りも以下のように指定してみます。文字の縁取りは text-halo から行えます。(あらかじめ、texsize を GeoJSON に入れておきます。)

map.addLayer({
    'id': 'places',
    'type': 'symbol',
    'source': 'places',
    'layout': {
        'text-field': ['get', 'description'],
        'text-size': ['get', 'textsize']
    },
    'paint': {
        'text-color': '#000',
        'text-halo-color': '#fff',
        'text-halo-width': 2
    }
});

 実装例と全体のソースコードは以下のようになります。地図内の文字をクリックすると、画像付きポップアップが開きます。

<html>
<head>
<title>htmlMap</title>
<meta http-equiv='content-type' charset='utf-8'>
<meta name='viewport' content='width=device-width'>

<!-- 以下 MapLibre のJavaScriptとCSS -->
<link rel='stylesheet' href='https://unpkg.com/maplibre-gl/dist/maplibre-gl.css'/>
<script src='https://unpkg.com/maplibre-gl/dist/maplibre-gl.js'></script>

<!-- 以下 ポップアップの見た目調整用 -->
<style>
.maplibregl-popup {
	box-sizing: content-box;
	margin: 0px; 
}
.maplibregl-popup-content {
	width: 234px;
	height: 136px;
	padding: 5px 5px !important;
}
.maplibregl-popup-close-button{
	background-color: #fff;
}
.maplibregl-popup-close-button:hover {
	background-color: #ddd;
}
.popup_comment{
	background-color:rgba(0,0,0,0.7);
	color:#fff;
	position: absolute;
	bottom: 0px;
	left: 0px; 
	box-sizing:content-box; 
	margin:0px; 
	padding: 5px; 
	max-width: 100%; 
	text-align:left; 
	font-size:12px;
}
.popup_container{
	position: relative; 
	width:100%; 
	height:100%;
}
.popup_image{
	width:100%;
	height:100%;
	border-radius: 0;
	border: none;
	object-fit: cover;
}

</style>
</head>
<body>

<!-- 埋め込みマップのdivタグ。マップサイズはwidth(幅)とheight(高さ)で決まる -->
<div id='mapcontainer' style='width:100%; height:300px; z-index:0;'></div>

<script>
function init_map() {

    //GeoJsonの定義
    const places = {
        'type': 'FeatureCollection',
        'features': [{
            'type':'Feature',
            'properties':{
                'description':'東京',
                'image':'https:\/\/pancake-trail.site\/wp-content\/uploads\/2023\/11\/すみだソラマチ_190505_0002-e1699772537354.jpg',
                'textsize':18
            },
            'geometry':{
                'type':'Point',
                'coordinates':[139.767136,35.681243]
            }
        },{
            'type':'Feature',
            'properties':{
                'description':'大阪',
                'image':'https:\/\/pancake-trail.site\/wp-content\/uploads\/2024\/03\/DSC03487-768x510-1-e1710138311576.jpg',
                'textsize':16
            },
            'geometry':{
                'type':'Point',
                'coordinates':[135.525753,34.687244]
            }
        },{
            'type':'Feature',
            'properties':{
                'description':'愛知',
                'image':'https:\/\/pancake-trail.site\/wp-content\/uploads\/2024\/08\/20240602134348-e1724331424816.jpg',
                'textsize':14
            },
            'geometry':{
                'type':'Point',
                'coordinates':[136.917151,35.150302]
            }
        },{
            'type':'Feature',
            'properties':{
                'description':'福岡',
                'image':'https:\/\/pancake-trail.site\/wp-content\/uploads\/2024\/05\/20240516103301-e1715949420726.jpg',
                'textsize':16
            },
            'geometry':{
                'type':'Point',
                'coordinates':[130.42071,33.589732]
            }
        }
    ]};

    //マップの定義
    const map =  new maplibregl.Map({
        container: 'mapcontainer',
        style: {
            version: 8,
            glyphs: 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf',
            sources: {
                rtile: {
                    type: 'raster',
                    tiles: [
                        'https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png',
                    ],
                    tileSize: 256,
                    attribution: '<a href="https://maps.gsi.go.jp/development/ichiran.html">国土地理院</a>',
                },
            },
            layers: [{
                id: 'raster-tiles',
                type: 'raster',
                source: 'rtile',
                minzoom: 0,
                maxzoom: 18,
            }]
        },
        center: [136, 39], 
        zoom:  3, 
        pitch: 0 
    });

    //ソースとレイヤーを追加
    map.on('load', () => {

        map.addSource('places', {
            'type': 'geojson',
            'data': places
        });

        map.addLayer({
            'id': 'places',
            'type': 'symbol',
            'source': 'places',
            'layout': {
                'text-field': ['get', 'description'],
                'text-size': ['get', 'textsize']
            },
            'paint': {
                'text-color': '#000',
                'text-halo-color': '#fff',
                'text-halo-width': 2
            }
        });
    });

    //クリックイベントを付加
    map.on('click', 'places', (e) => {
        const coordinates = e.features[0].geometry.coordinates.slice();
        const imageURL = e.features[0].properties.image;
        const description = e.features[0].properties.description;
        while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
            coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
        }
        new maplibregl.Popup()
        .setLngLat(coordinates)
        .setHTML('<div class="popup_container"><img src=' + imageURL + ' class="popup_image"/><div class="popup_comment">'+ description +'</div></div>')
        .addTo(map);
    });

    //マウスポインタの表示を制御
    map.on('mouseenter', 'places', () => {
        map.getCanvas().style.cursor = 'pointer';
    });
    map.on('mouseleave', 'places', () => {
        map.getCanvas().style.cursor = '';
    });
}

//ダウンロード時に初期化する
window.addEventListener('DOMContentLoaded', init_map());

</script>
</body>
</html>

 

少し宣伝

 当サイトは海外旅ブログまとめサイトとなっています。トップページに地図がありますが、地名を押すと画像が開き、画像を押すと関連記事一覧(クリック数順)が開きます。地名もその国で記事が多い都市の文字が大きくなるようにしています。ぜひ覗いてみてください。
 当サイトで海外旅ブログを執筆することも可能です(もちろん無料です)! また既にブログをお持ちの方も、当サイトからリンクを貼ることができるようになっています。パントレ開発部までお気軽にお問い合わせください。


 このページが皆様のプログラミングの一助となりますことをお祈りいたします

パントレ開発部