🧮

maplibre-gl-manual-geolocate: 任意の場所を「現在地」にするMapLibre GL JSライブラリ

に公開

maplibre-gl-manual-geolocate demo

https://maplibre-gl-manual-geolocate.mierune.dev/

ブラウザの位置情報APIを使わずに、Web地図ライブラリMapLibre GL JS上で、任意の場所をユーザーの「現在地」に設定し表示できるJavaScriptパッケージ maplibre-gl-manual-geolocate を作りました。

開発やデモで特定の位置を表示したいとき、プライバシー保護やオフライン環境で位置を扱いたい場合などに適しています。

MapLibre GL JS標準の現在地機能(GeolocateControl)と全く同じ見た目の操作感を持つボタンと地図マーカーを提供し、APIも大まかに互換性を保っています。

ライブラリ

使い方

以下のコマンドでインストールできます。

npm install @mierune/maplibre-gl-manual-geolocate

以下に簡単な例を示します。基本的には、MapLibre GL JS標準の現在地機能(GeolocateControl)と同様の使い方ができます。

import { ManualGeolocateControl } from "@mierune/maplibre-gl-manual-geolocate";
import { Map } from "maplibre-gl";

const map = new Map({
  container: "map",
  style: "https://demotiles.maplibre.org/style.json",
  center: [139.6917, 35.6895],
  zoom: 12,
});

// コントロールを作成
const manualGeolocateControl = new ManualGeolocateControl({
  position: { lng: 139.6917, lat: 35.6895 }, // 任意の場所
  accuracy: 50, // 精度範囲
});

// 地図に追加(デフォルトの GeolocateControl と同様)
map.addControl(manualGeolocateControl, "top-right");

API

コントロールの作成時に、以下のオプションを設定できます。

  • position: 任意の位置を指定。LngLatLikeとして、[lng, lat], {lng: number, lat: number} などいろいろな記述方法の値を受け付けます
  • accuracy?: 精度の範囲(メートル単位)。現在地マーカーの背後に描画される半透明の円で表現されます
  • showAccuracyCircle?: 精度範囲の円を描画するか
  • fitBoundsOptions?: 現在地に合わせて地図を移動する際の設定。別記事でも解説しています

提供しているメソッドは以下の通りです:

  • setPosition(coordinates: LngLatLike): void: 任意の位置を変更
  • setAccuracy(accuracy: number): void: 精度範囲を変更
  • setShowAccuracyCircle(show: boolean): void: 精度範囲円を描画するかを変更
  • setFitBoundsOptions(options: FitBoundsOptions): void: 現在地に合わせて地図を移動する際の設定の変更
  • trigger(): void: 現在地マーカーを表示し、そこへ地図の中心を移動。現在地ボタンを押した時と同じ動作

その他、発火されるイベント(geolocate, outofmaxbounds)や、利用例(ダイナミックな位置変更、イベントのハンドリング、オートズームの設定、開発と本番環境での切り替え)など、詳細はREADMEを参照してください。

標準のGeolocateControlとの違い

当ライブラリ ManualGeolocateControl は、MapLibre GL JSが標準で提供する GeolocateControl と、見た目や基本的な挙動は基本的に同じです。

ただし、 ManualGeolocateControlブラウザのGeolocation APIを使用していません。そのため、ユーザーに位置情報の利用許可を求める必要がなく、ブラウザの権限設定に依存しません。また、このAPIはセキュリティ要件上、HTTPS環境でしか動作しませんが、当ライブラリにはその制限がありません。

さらに、標準機能にあるウォッチモード(ユーザーの移動に合わせてマーカーを自動更新し、地図を追従する)は、このライブラリにはありません。このモードについては別記事で解説しましたので、そちらも参照ください。

以下は、オプションの違いをまとめた表です。

オプション名 GeolocateControl ManualGeolocateControl 説明
fitBoundsOptions 自動ズームの設定(同じ動作)
positionOptions Geolocation API 用の設定(手動版では不要)
showAccuracyCircle 精度範囲の円を表示するかどうか
showUserLocation 手動版では常に現在地を表示するため不要
trackUserLocation リアルタイム追従モード(手動版では非対応)
Manual 独自
position 必須。表示する座標を指定
accuracy 任意。精度(メートル単位)を指定

その他の詳細についてはREADMEの該当箇所を参照ください。


制作過程

背景

私たちMIERUNEは、歴史的な地図を現代の地図感覚で楽しめるウェブサービス「れきちず」を開発しています:

https://rekichizu.jp/

このれきちずへ先日(2025年8月)、新たに「比較機能」を追加しました:

https://x.com/chizutodesign/status/1953756317656789485

この機能は、内部的には2つの地図を生成し、それらを1ページ内で重ね合わせたり並べたりして表示することで実現しています。以下のSvelte MapLibre GLでの例と同じ方式です:

https://svelte-maplibre-gl.mierune.dev/examples/side-by-side

当初は、現在地機能を片方の地図だけで有効にしていたため、現在地マーカーが片方にしか表示されないという状態でした。そのため「両方の地図に”同期した現在地マーカー”を表示したい」と考えたのが、今回の出発点です。

れきちず - 比較モードで両方の地図に現在地マーカーが表示されている様子

図: れきちずの比較モードで、両方の地図に現在地マーカーが表示されている様子。従来は、片方(この場合、現在地ボタンがある右側)の地図のみに表示されていた

MapLibre GL JS標準の現在地機能を調べつつ実装を進める中で、「これは汎用的なパーツとして切り出せるのでは?」と思い、独立したライブラリとして公開しました。

ちなみに、一時的にブラウザの現在位置を変更したい場合は、ChromeのDevToolsで簡単に行うことができます。

https://zenn.dev/mierune/articles/c2ec78fb16bc64

IControl: MapLibre GL JSのコントロールのためのインターフェイス

MapLibre GL JSでは、地図上のインタラクティブなコントロール(例えば現在地機能とその操作ボタン)の仕様を定義するためのインターフェイスとして IControl が用意されています。

https://maplibre.org/maplibre-gl-js/docs/API/interfaces/IControl/

この IControl は、すべてのコントロールで共通して onAddonRemove メソッドを実装する必要があります。また、通常は div などのHTML要素を持ち、 onAdd 内で
this._container = document.createElement('div'); のように要素を生成・保持します。

interface IControl {
    // 必須: コントロールが地図へ追加されたときに呼ばれる
    onAdd(map: Map): HTMLElement;
    
    // 必須: コントロールが地図から削除されたときに呼ばれる
    onRemove(): void;
    
    // 任意: コントロールのデフォルト表示位置を返す
    getDefaultPosition?: () => ControlPosition;
}

本ライブラリ maplibre-gl-manual-geolocate では、このインターフェイスの実装が src/ManualGeolocateControl.ts でされています。

https://github.com/MIERUNE/maplibre-gl-manual-geolocate/blob/7f8f6d9bc5389cb6a8d7a7aeb2b2e01572897011/src/ManualGeolocateControl.ts#L29-L142

仕組み

とてもシンプルに、MapLibre GL JS標準の現在地機能に準拠しています。

「位置(経緯度)」と「精度(メートル)」の値を保持し、それらをもとに標準の現在地機能と同じ形でマーカーを描画しています。

具体的には、maplibregl-user-location-accuracy-circle, maplibregl-user-location-dot という要素を作成し、適切な位置へ配置します。

https://github.com/MIERUNE/maplibre-gl-manual-geolocate/blob/7f8f6d9bc5389cb6a8d7a7aeb2b2e01572897011/src/ManualGeolocateControl.ts#L148-L168

標準のスタイル定義がそのまま適用されるため、標準の現在地機能とまったく同じ見た目になります。

https://github.com/maplibre/maplibre-gl-js/blob/5e0f8f749eab9bb5e0ea7e0a34a14112b52f0a66/src/css/maplibre-gl.css#L765-L770

https://github.com/maplibre/maplibre-gl-js/blob/5e0f8f749eab9bb5e0ea7e0a34a14112b52f0a66/src/css/maplibre-gl.css#L809-L814

標準機能のコードリーディング

今回の実装を進めるにあたり、MapLibre GL JS標準の現在地機能のコードを読み、動作の理解を深めました。

普段何気なく使っている機能ですが、実際に中身を追ってみると「なるほど、こういう仕組みなのか」と発見が多く、背後で動いている ブラウザのGeolocation APIも含めて大変勉強になりました。学んだ内容は以下の記事にまとめています。

https://zenn.dev/mierune/articles/70a59fe7770d31

全体的な実装の仕組みや状態管理もそうですし、また、より細かい部分の実装も参考になりました。

たとえば、マーカーの背後に描画される「精度の円」(GPSなどから得られる“精度 50メートル”といった値をもとに、半透明の青い円で表現される)の「画面上での大きさ(ピクセル数)」をどう算出するか。

MapLibre GL JSの地図 - 精度範囲の円のサイズ

ナイーブには、「赤道の円周(約4万キロ)と緯度を元に算出」するという方法が考えられます。

_getMetersPerPixelAtLatitude(latitude: number, zoom: number): number {
  const earthCircumference = 40075017; // 赤道の円周
  const latitudeRadians = (latitude * Math.PI) / 180;
  return (
    (earthCircumference * Math.cos(latitudeRadians)) / Math.pow(2, zoom + 8)
  );
}

しかし、MapLibre GL JS では別のアプローチを採用しています。具体的には、

  • 現在地の「地理座標」を project() で「ピクセル座標」へ変換する
  • その座標に100ピクセル足して、それを unproject() で地理座標へ再変換する
  • これにより「ピクセル ↔ メートル」のスケールが求まる
  • その値をもとに精度の円を描画する

という仕組みになっており、なるほどな〜と思いました。

https://github.com/maplibre/maplibre-gl-js/blob/14f56b00e0f08784681ef98f0731c60f3923a4a9/src/ui/control/geolocate_control.ts#L476-L481

また、実装や検証を進める中でバグを発見し、コードをある程度読んでいたことから修正方法も検討することができました。そして実際にPRを出して、v5.8.0に含まれました!私にとって初めてのMapLibreへのコントリビューションで嬉しかったです。コードを読み理解を深めていったからこそできたことでしょう。

https://github.com/maplibre/maplibre-gl-js/pull/6464

AIコーディング

今回の実装は、2025年に入ってからの全てのプロジェクトと同様に、AIをメインに据えて進めました。

使用したのはClaude CodeとCodex(たまにGemini CLI)、開発環境としてはエージェント統合環境のWarpや次世代エディタのZedを好んで使いました。

まず実装前に、READMEを最初にすべて書き切ることから始めました。インターフェイスや挙動を文章として整理していく過程で、「ここはどう扱うべきか」「この設定も必要だな」と発想が広がり、曖昧だったゴールイメージを明確にできました。そしてこのドキュメントをもとに、AIエージェントによりひとつひとつ実装を重ねていきました。Kiroのスペック駆動開発と同様の流れでしょう。

エージェントによる各タスクのアウトプットは、そのまま使える場合もあれば、大きく手を入れる必要がある場合もありました。重要なのは、タスクを小さく区切り、人間が一つひとつの提案を丁寧にレビューし、理解してから進めることだと思います。そうすることで、品質が劣化したり、収拾がつかなくなることを防ぎ、全体を統制しながら開発を進めることができました。

今回は、最初に作成したREADMEが共通のリファレンスとして機能し、AIの出力が大きく外れたときも修正が容易でした。さらに、MapLibreの標準実装という比較対象が明確に存在したため、挙動の一致を確認したり、実装のヒントを得たりする際にも非常に助かりました。

また、コードリーディングの過程でもAIを活用し、理解の補助として強力に機能してくれました。

今回の開発ではAIは、単に開発速度を高めるだけでなく、自分ひとりではまだ理解や力量が足らず手を出しづらかった領域に踏み込み、理解を深める助けとなったと感じました。

今後、AIのアウトプットはさらに精緻になり、人間による介入の必要性はますます減っていくでしょう。それでも、人間による理解と制御は、程度の差こそあれ、当面は求められるのではないでしょうか。また、自身の理解が広がり深まっていくことは、単純に、楽しいことだなぁと私は思います。

Enjoy coding!

参考文献

https://zenn.dev/qazsato/articles/1cd38006185329
https://qiita.com/dayjournal/items/2200ce37472c3b4cd6f3
https://zenn.dev/mapbox_japan/articles/335915a261d6e9
https://zenn.dev/mierune/articles/70a59fe7770d31

MIERUNEのZennブログ

Discussion