オープンソースの地図ライブラリ MapLibre GL JS を Svelte のコンポーネントとして扱えるようにする Svelte MapLibre GL というライブラリを開発しています。すでにプロダクション利用にも耐えうる状態になっているので、ご紹介します。
(React の “React Map GL“ をご存知の方であれば、そのリアクティビティをもう一歩推し進めて、Svelte の特徴も活かしたものと捉えて頂くとよいと思います)
ドキュメンテーションのサイトにデモを色々と用意していますので、ぜひそちらもご覧ください(内容は拡充中):
- ドキュメンテーション: https://svelte-maplibre-gl.mierune.dev
- GitHub: https://github.com/MIERUNE/svelte-maplibre-gl
- npm: https://www.npmjs.com/package/svelte-maplibre-gl
Svelte 5 以降に最適化して実装しているため、Svelte 4では利用できません。
百聞は一見に如かず
ということでシンプルな例を示します。まず svelte-maplibre-gl
パッケージをプロジェクトに追加して......
$ npm install -D svelte-maplibre-gl
......そして、これだけのコードを書くだけで......
<script>
import { MapLibre, Marker, Popup } from "svelte-maplibre-gl";
let lnglat: [number, number] = $state([137, 36]);
let popupOpen = $state(false);
</script>
Marker: {`(${lnglat[1].toFixed(4)}, ${lnglat[0].toFixed(4)})`}
<label><input type="checkbox" bind:checked={popupOpen} /> Popup ({popupOpen ? "open" : "close"})</label>
<MapLibre
class="h-[300px] w-full"
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
zoom={5}
center={{ lng: 137, lat: 36 }}
>
<Marker bind:lnglat draggable>
<Popup offset={[0, -30]}>
{`(${lnglat[1].toFixed(4)}, ${lnglat[0].toFixed(4)})`}
</Popup>
</Marker>
</MapLibre>
...... 背景地図が表示されて、マーカーとポップアップがSvelteの変数と連動します:
SvelteとMapLibre GL JSの状態が “相互に” 連携していることがわかると思います。Svelteのリアクティブな変数の変化がMapLibreに反映され、逆にMapLibreの変化がSvelteに反映されます。
このようなライブラリは何がうれしいのでしょうか:
- リアクティビティ — SvelteのようなリアクティブなWeb UI構築のパラダイムを、シームレスに MapLibre に持ちこめます。Svelteのリアクティブな変数を MapLibre の世界にまで効かせることができます。
- ライフサイクル管理 — MapLibre の各種オブジェクトのライフサイクルが Svelte のコンポーネントツリーによって管理されるため、リソースの追加や削除、イベントハンドラの設定や解除、そしてそれらの適切なタイミング、といったことに気を配る必要がなくなります。
- デメリットがない — このライブラリを使ったからといって、Map インスタンスなどを直接操作する自由が奪われるわけではありません。命令的な操作や、従来通りに書きたい処理は原則いままで通りに書けます。
可能な限りリアクティブに
このライブラリは、MapLibre GL JSのAPIで動的に変更できる要素すべてを、Svelteのリアクティビティで操れることを目指しています。
各種レイヤの paint
や layout
プロパティをリアクティブな変数で操れることは当然必要なので......
3D Terrain の exaggerations もリアクティブに変化します:
<HillshadeLayer
paint={{ 'hillshade-exaggeration': hillshade[0], /* ... */ }}
demo: https://svelte-maplibre-gl.mierune.dev/examples/terrain
クラスタリング機能のパラメータもリアクティブな変数に反応します:
<GeoJSONSource
data="https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson"
{cluster}
clusterRadius={cluster ? clusterRadius[0] : undefined}
>
demo: https://svelte-maplibre-gl.mierune.dev/examples/clusters
以下は、2つのMapLibreインスタンスの表示を同期させるという少々テクニカルな例です。このようなデモを命令的なコードほぼゼロで簡単に作ることができます。
2つの地図の座標変換に関わる値をすべて同じリアクティブ変数にバインドしているだけです。
<MapLibre
style="https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"
bind:center bind:zoom bind:bearing bind:pitch bind:roll bind:elevation />
<MapLibre
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
bind:center bind:zoom bind:bearing bind:pitch bind:roll bind:elevation />
demo: https://svelte-maplibre-gl.mierune.dev/examples/side-by-side
ホバーエフェクトなどでよく使われる Map.setFeatureState
も宣言的に扱えます:
主にこれだけでホバーエフェクトが作れます:
<FillLayer
onmousemove={(ev) => (hoveredId = ev.features?.[0]?.id)}
onmouseleave={() => (hoveredId = undefined)}
paint={{ 'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 0.4, 0.1], 'fill-color': '#00ff55' }}
/>
{#if hoveredId}
<FeatureState id={hoveredId} state={{ hover: true }} />
{/if}
demo: https://svelte-maplibre-gl.mierune.dev/examples/hover-styles
開発の動機
せっかく Svelte、React、Vue といったリアクティブな Web UI フレームワークの恩恵をうけて開発しているのに、MapLibre GL JSを扱う部分にはそれが適用できず、コードも jQuery の時代のようになってしまうのは残念で、なにより面倒です。
これに対処するため、 React には古くから React Map GL という有名な Mapbox/MapLibre GL JS のラッパーがあります。しかし私や私が勤める会社は React ではなく Svelte (SvelteKit) というフレームワークを好んで使っています。
Svelte にも良いライブラリが欲しいので、一念発起して Svelte 上で同じようなことをするオープンソースのライブラリを整備することにしました。
実をいうとすでに Svelte MapLibre というライブラリを開発している先人がいらっしゃるのですが、基礎の作りを根本的に見直したいと感じたのと、最近公開された Svelte 5 を前提にして Svelte 4 の限界にとらわれずに設計しなおそうと考え、ゼロから作っています。
React Map GL よりもリアクティブに
Svelte はデータジャーナリズムやインタラクティブメディアを背景に生まれたこともあり、React とは異なる設計思想を持っています。我々のライブラリも Svelte に合わせて、React Map GL と比べてエルゴノミックさをより重視しています。
Svelte MapLibre GL は React Map GL よりもリアクティビティを強めに取り入れた作りになっています。具体例としては:
- 一部において、Svelte がもつ双方向バインディング (two-way binding) を活かして、MapLibreとSvelteの状態を相互に同期できるようにしています。(two-way binding の問題のないまっとうな使い方であり、利便性は大変良いです)。
- MapLibre GL JS の API から逸脱しない範囲で宣言的なプログラミングを強化しています:
- ホバー効果などでよく使われる
Map.setFeatureState
による地物状態を、宣言的なコンポーネントで表現できるようにしています。 - 例えば「ポイントのベクターデータにMarkerを打つ」などに使われる
Map.querySourceFeatures
やMap.queryRenderedFeatures
も宣言的に扱えるようにしています。 - 各レイヤのコンポーネントごとに
onmousemove
などのイベントを設定できるようにしています。
- ホバー効果などでよく使われる
-
逆に、MapLibre GL JS が提供していない概念を恣意的に導入することは可能な限り避けています。先駆者のライブラリにはしばしばそのような機能が見受けられます。
- 例: React Map GL は
interactiveLayerIds
といった独自の概念を導入して、Mapコンポーネントに対してマップレイヤイベントをセットします。 - 例: Svelte MapLibre(先人のほうのライブラリ)も、例えば MarkerLayer のような独自の概念やホバーの支援などを特別に用意してしまっています。
- 例: React Map GL は
- ベースレイヤ変更時のユーザ定義リソースの保持を自動で行います。(ベースレイヤ (
style.json
) の切り替えとユーザ定義の動的なソース/レイヤとの相性の悪さは、ご存知の方もいらっしゃるだろうと思います) - などなど
リアクティビティを広く活用する(マップの中心座標を変数にバインドしたりもできる)ことによるパフォーマンスへの影響は気になるところですが、今のところパフォーマンスまわりの問題は何も感じません。Svelte の特徴である、変数代入のリアクティビティとDOM操作がコンパイルタイムに用意されることが上手く効いていたら面白いのですが、現時点では何とも言えません。
余談: Svelte 5 のうれしさ
今回、当ライブラリを Svelte 5 専用 (Runes mode, Modern mode) で実装しました。ライブラリの実装を Svelte 5 で行ってうれしかったこと(Svelte 4だったら辛かっただろうこと)を記しておきます:
- TypeScript との親和性がよい:
- 例えば
<CircleLayer>
コンポーネントの内部実装の$props
への型付けとして以下のような記述ができています。MapLibre の型情報をひとつひとつ写経してくる必要がありません:interface Props extends Omit<maplibregl.CircleLayerSpecification, 'id' | 'source' | 'type' | 'source-layer'>, MapLayerEventProps { id?: string; sourceLayer?: maplibregl.CircleLayerSpecification['source-layer']; beforeId?: string; children?: Snippet; }
- 例えば
- どれが
$bindable
なプロパティであるかが明示される。 -
on:
ディレクティブとcreateEventDispatcher
の廃止。(なおSvelte 4 でも新しい Svelte 5 スタイルのイベントハインドラの渡し方は可能です)- どのイベントハンドラがユーザから提供されているかが容易に分かるので、MapLibreに対する無駄なイベント購読をせずに済みます。
などなど、他にもメリットが多々あるはずですが、Svelte 5で書くのが当然になってしまいSvelte 4のつらさを忘れてしまいました。
ぜひフィードバックをください
このライブラリにご興味をもって頂けましたら、ぜひ試してみてください。
リアクティビティに関する細かなバグや、足りない機能性はあるだろうと思います。フィードバックを頂けるとありがたいです。