これは MIERUNE AdventCalendar 2022 4日目の記事です。
昨日は @yuskesuzki@github さんによる Django GEOS API で地図上にちゃんとした魔法陣を描く でした。
今回はMaplibre GL JSを使用して、地図上に木を植えたいと思います。
地図上に木を出現させる
まずは、Maplibreのポップアップを使用して地図上に木を表示します。マップを生成し、マップの中心にポップアップを生成します。このとき、ポップアップのoptionsを以下のようにします。
closeOnClick: false
地図をクリックした時にポップアップが消えないようにします。
closeButton: false
ポップアップの閉じるボタンを非表示にします。
anchor: "bottom"
ポップアップの位置が地図の移動に合わせて自動で動かないようにbottom
で固定します。
// 地図の生成
const map = new maplibregl.Map({
container: "map",
style: {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution:
"© <a href='https://www.openstreetmap.org/copyright/ja' target='_blank'>OpenStreetMap</a> contributions"
}
},
layers: [
{
id: "osm",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 24
}
]
},
center: [141.361, 43.064],
zoom: 12,
pitch: 60
});
// 地図が読み込まれた後の処理
map.on("load", () => {
// ポップアップの生成
const popup = new maplibregl.Popup({
closeOnClick: false,
closeButton: false,
anchor: "bottom"
});
// ポップアップを地図の中心に表示
popup.setLngLat(map.getCenter()).setHTML("").addTo(map);
});
setHTML()
の中は空文字なので、ポップアップの中には何も文字が表示されません。
そしたら、このポップアップをCSSでカスタマイズして木に変身させます。
MaplibreのポップアップはHTML要素なので、Chromeの開発者ツールを使用してポップアップの要素を検証することで、デフォルトのCSSスタイリングを確認することができます。
MaplibreのポップアップのCSSセレクターは親要素の.mapboxgl-popup
と子要素の.mapboxgl-popup-tip
と.mapboxgl-popup-content
が存在し、CSSによるスタイリングができます(もっと細いセレクターが存在しますが今回は省略)。
.mapboxgl
の部分を.maplibregl
に変えてもスタイリングは可能ですが、Mapbox GL JSでも使用できる.mapboxgl
で記述する方が個人的にはおすすめです。
今回はポップアップのデフォルトのCSSスタイリングを上書きするようにCSSを記述してるので、通常のdiv要素よりも余分にCSSを記述しています。疑似要素の::before
と::after
を使ってパーツを増やし、重ね合わせることで、すこし立体感のある木を表現しています。
/* ポップアップの三角形の部分 (樹幹に変える)*/
.mapboxgl-popup .mapboxgl-popup-tip {
z-index: 0;
order: 1;
transform: rotate(180deg);
border-top: 100px solid rgb(51, 157, 33);
border-right: 30px solid transparent;
border-left: 30px solid transparent;
filter: drop-shadow(0 1px 8px rgba(0, 0, 0, 0.241));
}
/* ポップアップの四角形の部分(幹に変える) */
.mapboxgl-popup .mapboxgl-popup-content {
z-index: 0;
pointer-events: none;
align-self: center;
padding: 0px;
border-radius: 0;
margin: 0px;
width: 20px;
height: 60px;
background: #e17b33;
box-shadow: 0px 0px 16px -6px rgba(0, 0, 0, 0.6);
}
.mapboxgl-popup .mapboxgl-popup-content::after {
content: "";
position: absolute;
width: 60px;
height: 20px;
display: block;
background-color: rgb(51, 157, 33);
border-radius: 45%;
transform: translateX(-20px) translateY(-8px);
filter: drop-shadow(0 10px 4px rgba(0, 0, 0, 0.217));
}
.mapboxgl-popup .mapboxgl-popup-content::before {
content: "";
position: absolute;
width: 20px;
height: 10px;
bottom: -5px;
background-color: #e17b33;
border-radius: 50%;
}
CSSアニメーションを追加する
地面から生えてきた感じを表現したいのでanimation
プロパティと@keyframes
を使ってCSSアニメーションを追加します。
.mapboxgl-popup .mapboxgl-popup-tip {
z-index: 0;
order: 1;
transform: rotate(180deg);
border-top: 100px solid rgb(51, 157, 33);
border-right: 30px solid transparent;
border-left: 30px solid transparent;
filter: drop-shadow(0 1px 8px rgba(0, 0, 0, 0.241));
+ animation: borderTip 1s ease;
}
+ @keyframes borderTip {
+ 0% {
+ border-top: 0px solid rgb(51, 157, 33);
+ border-right: 0px solid transparent;
+ border-left: 0px solid transparent;
+ border-filter: drop-shadow(0 0px 0px rgba(0, 0, 0, 0.241));
+ }
+ 100% {
+ border-top: 100px solid rgb(51, 157, 33);
+ border-right: 30px solid transparent;
+ border-left: 30px solid transparent;
+ }
+ }
.mapboxgl-popup .mapboxgl-popup-content {
z-index: 0;
pointer-events: none;
align-self: center;
padding: 0px;
border-radius: 0;
margin: 0px;
width: 20px;
height: 60px;
background: #e17b33;
box-shadow: 0px 0px 16px -6px rgba(0, 0, 0, 0.6);
+ animation: transformContent 1s ease;
}
+ @keyframes transformContent {
+ 0% {
+ width: 0px;
+ height: 0px;
+ transform: translateX(0px);
+ }
+ 100% {
+ width: 20px;
+ height: 60px;
+ }
+ }
.mapboxgl-popup .mapboxgl-popup-content::after {
content: "";
position: absolute;
width: 60px;
height: 20px;
display: block;
background-color: rgb(51, 157, 33);
border-radius: 45%;
transform: translateX(-20px) translateY(-8px);
filter: drop-shadow(0 10px 4px rgba(0, 0, 0, 0.217));
+ animation: transformContentAfter 1s ease;
}
+ @keyframes transformContentAfter {
+ 0% {
+ width: 0px;
+ height: 0px;
+ border-radius: 50%;
+ transform: translateX(0px) translateY(0px);
+ filter: drop-shadow(0 0px 0px rgba(0, 0, 0, 0.217));
+ }
+ 100% {
+ width: 60px;
+ height: 20px;
+ border-radius: 45%;
+ transform: translateX(-20px) translateY(-8px);
+ }
+ }
.mapboxgl-popup .mapboxgl-popup-content::before {
content: "";
position: absolute;
width: 20px;
height: 10px;
bottom: -5px;
background-color: #e17b33;
border-radius: 50%;
+ animation: transformContentBefore 1s ease;
}
+ @keyframes transformContentBefore {
+ 0% {
+ width: 0px;
+ height: 0px;
+ bottom: 0px;
+ transform: translateX(0px);
+ }
+ 100% {
+ width: 20px;
+ height: 10px;
+ bottom: -5px;
+ }
+ }
地面からニョキニョキ生えるようになりました。
クリックイベントの追加
もっとたくさん植林したいので、クリックしたところに木(ポップアップ)が生成されるようにマップのクリックイベントを追加します。
// 地図をクリックした時の処理
map.on("click", (e) => {
const popup = new maplibregl.Popup({
closeOnClick: false,
closeButton: false,
anchor: "bottom"
});
// クリックした地点に木を生やす
popup.setLngLat(e.lngLat).setHTML("").addTo(map);
});
これで地図上に植林ができるようになりました!。実際に地図をクリックして植林してみましょう!
See the Pen Maplibre click Tree Popup by satoshi7190 (@satoshi7190) on CodePen.
これで好きなだけ植林ができますね。
自動で植林する
しかし、1本1本手動で植林するのは面倒なので、自動化して一片にたくさん植林できるようにします。
map.on("load", ()
を追加し、Turf.jsのrandomPoint()
を使って複数のポイントをランダムに生成して、map()
のループ処理を使ってランダムに複数本の木が生えるようにします(とりあえず今回は50本)。ポイントを生成するには生成する範囲(bbox)の情報が必要なので、map.getBounds()
でマップ生成時の初期画面のbboxを取得します。
この時に地図が傾いていると、bboxが上方向にずれるので、地図の初期表示のpitch
を0にして、bboxを取得した後にmap.setPitch(60)
で地図を傾けるようにしています。
※個人的な好みなので、地図を傾ける必要がない場合は不要です。
// 地図の生成
const map = new maplibregl.Map({
container: "map",
style: {
version: 8,
sources: {
osm: {
type: "raster",
tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"],
tileSize: 256,
attribution:
"© <a href='https://www.openstreetmap.org/copyright/ja' target='_blank'>OpenStreetMap</a> contributions"
}
},
layers: [
{
id: "osm",
type: "raster",
source: "osm",
minzoom: 0,
maxzoom: 24
}
]
},
center: [141.361, 43.064],
zoom: 12,
- pitch: 60
+ pitch: 0
});
// 地図が読み込まれた後の処理
map.on("load", () => {
+ // 初期画面のbboxを取得
+ const bbox = [
+ map.getBounds()._sw.lng,
+ map.getBounds()._sw.lat,
+ map.getBounds()._ne.lng,
+ map.getBounds()._ne.lat
+ ];
+ // ポイントをランダムに生成
+ const point = turf.randomPoint(50, { bbox: bbox });
+ // 地図を傾ける
+ map.setPitch(60);
+ // ループ処理で植林
+ point.features.map((data) => {
const popup = new maplibregl.Popup({
closeOnClick: false,
closeButton: false,
anchor: "bottom"
});
- popup.setLngLat(map.getCenter()).setHTML("").addTo(map);
+ popup.setLngLat(data.geometry.coordinates).setHTML("").addTo(map);
+ });
});
ループ処理のタイミングをずらす
一度に複数の木(ポップアップ)に同じタイミングでCSSアニメーションを実行すると処理が重くなるので、map()
のループ処理を一定間隔でタイミングをずらして実行したいと思います。
map()
のループ処理の中にsetTimeout()
を記述して、非同期処理にします。map()
の第二引数(index
)で、配列のインデックス番号を取得しています。インデックス番号にsetTimeout()
の第二引数の実行タイミングの秒数100
を乗算することで、二番目の配列の処理は200ミリ秒後。三番目の配列の処理は300ミリ秒後・・・といった感じに非同期処理のタイミングがずらせるので、ループ処理の個々の操作を遅延させ、指定した秒数ごとに順番に処理させることができます。
// 地図が読み込まれた後の処理
map.on("load", () => {
// 初期画面のbboxを取得
const bbox = [
map.getBounds()._sw.lng,
map.getBounds()._sw.lat,
map.getBounds()._ne.lng,
map.getBounds()._ne.lat
];
// ポイントをランダムに生成
const point = turf.randomPoint(50, { bbox: bbox });
// 地図を傾ける
map.jumpTo({ pitch: 60 });
// ループ処理で植林
- point.features.map((data) => {
+ point.features.map((data, index) => {
+
+ //タイミングをずらして順番に処理
+ setTimeout(() => {
const popup = new maplibregl.Popup({
closeOnClick: false,
closeButton: false,
anchor: "bottom"
});
popup.setLngLat(data.geometry.coordinates).setHTML("").addTo(map);
+ }, 100 * index);
});
});
これで、植林が自動化されました。
See the Pen Maplibre Tree Popup by satoshi7190 (@satoshi7190) on CodePen.
こうしたループを遅延させるやりかたはアニメーションを一定間隔で順番に実行したい時に便利です。マップのカメラを順番に移動させたい時にも使えます。
Forest Rate in Japan.#MapLibre #MapBox pic.twitter.com/9ePDbvuA6s
— Satoshi Komatsu (@satoshi7190) November 5, 2022
まとめ
MaplibreのポップアップはCSSで好き勝手に魔改造できるので、さまざまな表現が可能になります。マーカーも同様にCSSで真改造できるので、18日目の記事ではMaplibreのマーカーについて紹介したいと思います
明日は@northprintさんによる地図で音を鳴らすです!お楽しみにー