20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MapLibreAdvent Calendar 2024

Day 9

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

Last updated at Posted at 2024-12-08

オープンソースの地図ライブラリ MapLibre GL JSSvelte のコンポーネントとして扱えるようにする Svelte MapLibre GL というライブラリを開発しています。すでにプロダクション利用にも耐えうる状態になっているので、ご紹介します。

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

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

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

8fc8a08664cc5fe86d11fb4c5af3453f.gif

SvelteとMapLibre GL JSの状態が “相互に” 連携していることがわかると思います。Svelteのリアクティブな変数の変化がMapLibreに反映され、逆にMapLibreの変化がSvelteに反映されます。

このようなライブラリは何がうれしいのでしょうか:

  • リアクティビティ — SvelteのようなリアクティブなWeb UI構築のパラダイムを、シームレスに MapLibre に持ちこめます。Svelteのリアクティブな変数を MapLibre の世界にまで効かせることができます。
  • ライフサイクル管理 — MapLibre の各種オブジェクトのライフサイクルが Svelte のコンポーネントツリーによって管理されるため、リソースの追加や削除、イベントハンドラの設定や解除、そしてそれらの適切なタイミング、といったことに気を配る必要がなくなります。
  • デメリットがない — このライブラリを使ったからといって、Map インスタンスなどを直接操作する自由が奪われるわけではありません。命令的な操作や、従来通りに書きたい処理は原則いままで通りに書けます。

可能な限りリアクティブに

このライブラリは、MapLibre GL JSのAPIで動的に変更できる要素すべてを、Svelteのリアクティビティで操れることを目指しています。

各種レイヤの paintlayout プロパティをリアクティブな変数で操れることは当然必要なので......

3D Terrain の exaggerations もリアクティブに変化します:

a5aff7152168936880c40b7e19bfbd95.gif

    <HillshadeLayer
      paint={{ 'hillshade-exaggeration': hillshade[0], /* ... */ }}

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


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

49d417bbb9766d5da36a40fdedc44655.gif

  <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インスタンスの表示を同期させるという少々テクニカルな例です。このようなデモを命令的なコードほぼゼロで簡単に作ることができます。

72d70302aae918fc6f2f136e8a935c64.gif

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

1d4528eb29e6695cec17c064c2d30abc.gif

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

    <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.querySourceFeaturesMap.queryRenderedFeatures も宣言的に扱えるようにしています。
    • 各レイヤのコンポーネントごとに onmousemove などのイベントを設定できるようにしています。
  • 逆に、MapLibre GL JS が提供していない概念を恣意的に導入することは可能な限り避けています。先駆者のライブラリにはしばしばそのような機能が見受けられます。
    • 例: React Map GL は interactiveLayerIds といった独自の概念を導入して、Mapコンポーネントに対してマップレイヤイベントをセットします。
    • 例: Svelte MapLibre(先人のほうのライブラリ)も、例えば MarkerLayer のような独自の概念やホバーの支援などを特別に用意してしまっています。
  • ベースレイヤ変更時のユーザ定義リソースの保持を自動で行います。(ベースレイヤ (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のつらさを忘れてしまいました。

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

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

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

20
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?