MapLibre GL JS を Svelte の世界で快適に使うためのライブラリを作りました

オープンソースの地図ライブラリ [MapLibre GL JS](https://maplibre.org/maplibre-gl-js/docs/) を [Svelte](https://svelte.dev/) のコンポーネントとして扱えるようにする **[Svelte MapLibre GL](https://svelte-maplibre-gl.mierune.dev/examples/complex)** というライブラリを開発しています。すでにプロダクション利用にも耐えうる状態になっているので、ご紹介します。

（React の “React Map GL“ をご存知の方であれば、そのリアクティビティをもう一歩推し進めて、Svelte の特徴も活かしたものと捉えて頂くとよいと思います）

ドキュメンテーションのサイトにデモを色々と用意していますので、ぜひそちらもご覧ください（内容は拡充中）：

https://svelte-maplibre-gl.mierune.dev/examples/complex

- ドキュメンテーション： 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` パッケージをプロジェクトに追加して......

```console
$ npm install -D svelte-maplibre-gl
```

......そして、これだけのコードを書くだけで......

```svelte
<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の変数と連動します：

![8fc8a08664cc5fe86d11fb4c5af3453f.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95370/056e3ee3-f2e4-6065-7da1-f664f405061b.gif)

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 すらリアクティブに変化します：

![a5aff7152168936880c40b7e19bfbd95.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95370/29d2e430-d30e-bd0d-fea6-44624c4018bf.gif)

```svelte
    <HillshadeLayer
      paint={{ 'hillshade-exaggeration': hillshade, /* ... */ }}
```

demo: https://svelte-maplibre-gl.mierune.dev/examples/terrain

---

クラスタリング機能のパラメータもリアクティブな変数に反応します：

![49d417bbb9766d5da36a40fdedc44655.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95370/e5587c1c-b983-3056-4e88-59b051e12518.gif)

```svelte
  <GeoJSONSource
    data="https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson"
    {cluster}
    clusterRadius={cluster ? clusterRadius : undefined}
  >
```

demo: https://svelte-maplibre-gl.mierune.dev/examples/clusters

---

以下は、２つのMapLibreインスタンスの表示を同期させるという少々テクニカルな例です。このようなデモを命令的なコードほぼゼロで簡単に作ることができます。

![72d70302aae918fc6f2f136e8a935c64.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95370/ebc4f0a2-722c-982e-ce7d-1194022f55ed.gif)

2つの地図の座標変換に関わる値をすべて同じリアクティブ変数にバインドしているだけです。

```svelte
  <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` も宣言的に扱えます：

![1d4528eb29e6695cec17c064c2d30abc.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/95370/1915fe92-0b0a-5abc-d83a-f3e14b2c486e.gif)

主にこれだけでホバーエフェクトが作れます：

```svelte
    <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](https://visgl.github.io/react-map-gl/) という有名な Mapbox/MapLibre GL JS のラッパーがあります。しかし私や私が勤める会社は React ではなく Svelte (SvelteKit) というフレームワークを好んで使っています。

Svelte にも良いライブラリが欲しいので、一念発起して Svelte 上で同じようなことをするオープンソースのライブラリを整備することにしました。

実をいうとすでに [Svelte MapLibre](https://svelte-maplibre.vercel.app/) というライブラリを開発している先人がいらっしゃるのですが、基礎の作りを根本的に見直したいと感じたのと、最近公開された 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` による地物状態を、宣言的なコンポーネントで表現できるようにしています。
    - 例: https://svelte-maplibre-gl.mierune.dev/examples/hover-styles
  - 例えば「ポイントのベクターデータにMarkerを打つ」などに使われる `Map.querySourceFeatures` や `Map.queryRenderedFeatures` も宣言的に扱えるようにしています。
    - 例: https://svelte-maplibre-gl.mierune.dev/examples/query-features
  - 各レイヤのコンポーネントごとに `onmousemove` などのイベントを設定できるようにしています。
- **逆に、MapLibre GL JS が提供していない概念を恣意的に導入することは可能な限り避けています**。先駆者のライブラリにはしばしばそのような機能が見受けられます。
  - 例： React Map GL は `interactiveLayerIds` といった独自の概念を導入して、Mapコンポーネントに対してマップレイヤイベントをセットします。
  - 例： Svelte MapLibre（先人のほうのライブラリ）も、例えば MarkerLayer のような独自の概念やホバーの支援などを特別に用意してしまっています。
- ベースレイヤ変更時のユーザ定義リソースの保持を自動で行います。（ベースレイヤ (`style.json`) の切り替えとユーザ定義の動的なソース／レイヤとの相性の悪さは、Mapbox/MapLibreの経験者の方々はご存知かもしれません）
    - 例: https://svelte-maplibre-gl.mierune.dev/examples/basestyle
- などなど

リアクティビティを広く活用する（マップの中心座標を変数にバインドしたりもできる）ことによるパフォーマンスへの影響は気になるところですが、今のところパフォーマンスまわりの問題は何も感じません。Svelte の特徴である、変数代入のリアクティビティがコンパイルタイムで用意されることが上手く効いていたら面白いのですが、現時点では何とも言えません。

## 余談： Svelte 5 のうれしさ

今回、当ライブラリを Svelte 5 専用 (Runes mode, Modern mode) で実装しました。ライブラリの実装を Svelte 5 で行ってうれしかったこと（Svelte 4だったら辛かっただろうこと）を記しておきます：

- TypeScript との親和性がよい：
  - 例えば `<CircleLayer>` コンポーネントの内部実装の `$props` への型付けとして以下のような記述ができています。MapLibre の型情報をひとつひとつ写経して実装する必要がありませんでした：
      ```ts
	interface Props
		extends Omit<maplibregl.CircleLayerSpecification, 'id' | 'source' | 'type' | 'source-layer'>,
			MapLayerEventProps {
		id?: string;
		sourceLayer?: maplibregl.CircleLayerSpecification['source-layer'];
		beforeId?: string;
		children?: Snippet;
	}
      ```
- どれが `$bindable` なプロパティであるかが明示されるようになりました。Svelte 4 はすべての props が bindable というやんちゃな仕様でした。
- `on:` ディレクティブと `createEventDispatcher` の廃止。（なおSvelte 4 でも新しい Svelte 5 スタイルのイベントハインドラの渡し方は可能です）
  - どのイベントハンドラがユーザから提供されているかが容易に分かるので、MapLibreに対する無駄なイベント購読をせずに済みます。

などなど、他にもメリットが多々あるはずですが、Svelte 5で書くのが当然になってしまいSvelte 4のつらさを忘れてしまいました。


## ぜひフィードバックをください

このライブラリにご興味をもって頂けましたら、ぜひ試してみてください。

リアクティビティに関する細かなバグや、足りない機能性はあるだろうと思います。フィードバックを頂けるとありがたいです。

https://github.com/MIERUNE/svelte-maplibre-gl