ドキュメント / 拡張レイヤー / 拡張モジュールを作る

拡張モジュールを作る

ヒートマップ・GeoJSON レイヤー・マーカークラスタリングは、どれも Core を書き換えずに外から足された別パッケージです。同じ口が公開されているので、第三者も同じやり方で自分の拡張を配布できます。このページはその手順です。

ANDROID
com.mapconductor:core
iOS
MapConductorCore
REACT
@mapconductor/js-sdk-core

00 · 拡張モジュールの形

拡張モジュールとは、コアモジュールだけに依存する独立したパッケージのことです。プロバイダのパッケージ(react-for-*, android-for-*, ios-for-*)には依存しません。これが守られていれば、利用者がどの地図プロバイダを選んでいてもあなたの拡張は動きます。

OK
コアの公開型(状態・コレクタ・コントローラ契約・タイルサーバ・サービスレジストリ)だけを使う。
NG
プロバイダのパッケージを import する。コントローラを別型にキャストして内部フィールドを覗く。

出荷済みの 3 つがそのまま参考実装です。作りたいものに近いものを読むのが近道です。

参考にする実装
heatmap          ラスタタイルを自前で描く      · タイルサーバ + RasterLayer
geojson-layer    大量のベクタをタイル化する    · タイルサーバ + RasterLayer
marker-clustering マーカーを加工して差し戻す   · MarkerState + サービスレジストリ
プラットフォーム

01 · パッケージを作る

依存はコアだけにします。プロバイダは利用者のアプリが選ぶものなので、あなたのパッケージからは見えません。

build.gradle.kts
dependencies {
    implementation("com.mapconductor:core")
    implementation("com.mapconductor:compose") // Composable を公開するなら
    // android-for-* には依存しない
}

02 · 状態を定義する

利用者が触るのは状態オブジェクトです。コアの状態型(MarkerState など)と同じ作法にしておくと、利用者が別のことを覚えずに済みます。値が変わったことを知らせる仕組み(フィンガープリント)を持たせておくと、コアのコレクタに載せて差分更新を受け取れます。

コアが用意しているもの
OverlayCollector       id → 状態 のマップ。追加/削除/差分と、値の変化通知をまとめる
ComponentState         id を持つ状態の契約(Android)
fingerPrint()          描画に効く値だけを集めた比較用の値

03 · 描画の出口を選ぶ

ここが一番大事な設計判断です。プロバイダごとに描画コードを書くのではなく、コアが既に持っている出口へ流し込みます。出口は 2 つあります。

A · RasterLayer

自分でタイルを描く

ローカルのタイルサーバに描画関数を登録し、その URL テンプレートを指す RasterLayer を 1 枚置きます。ヒートマップと GeoJSON レイヤーがこれです。件数が多いほど有利で、どのプロバイダでも見た目が完全に一致します。

B · 既存のオーバーレイ

マーカーや図形として返す

入力を加工して MarkerState / PolygonState などに変換し、コレクタへ書き込みます。そこから先はプロバイダ通常の描画経路が処理します。クラスタリングがこれです。クリックやドラッグがそのまま効きます。

A · タイルサーバ + RasterLayer(android-heatmap と同じ)
val tileServer = TileServerRegistry.get()
val renderer = MyTileRenderer(tileSize = 256)   // タイル1枚を描く関数を持つ

DisposableEffect(groupId) {
    tileServer.register(groupId, renderer)
    onDispose { tileServer.unregister(groupId) }
}

val layer = remember {
    RasterLayerState(
        id = "my-ext-$groupId",
        source = RasterLayerSource.UrlTemplate(
            template = tileServer.urlTemplate(groupId, renderer.tileSize),
            tileSize = renderer.tileSize,
            scheme = TileScheme.XYZ,
        ),
    )
}
RasterLayer(layer)
B を選ぶ場合、React ではコレクタへ直接書かず、プロバイダが登録した MarkerRenderingSupport をサービスレジストリから受け取ってレンダラを作ります(05 参照)。プロバイダ側が描画経路を差し替えられるようにするためです。

04 · カメラと後始末

ズームやパンに追従したい場合、地図コントローラのイベントを直接いじってはいけません。単一スロットのリスナーを奪い合うことになり、拡張を 2 つ載せた利用者の環境で壊れます。オーバーレイコントローラとして登録すれば、他のオーバーレイと同じ経路でカメラ変更が届きます。

HeatmapCameraController.kt と同じ形
class MyCameraController(
    private val renderer: MyTileRenderer,
) : OverlayControllerInterface<Unit, Unit>, OnCameraChangeReceiverInterface {
    override val zIndex: Int = 0
    override suspend fun add(data: List<Unit>) {}
    override suspend fun update(state: Unit) {}
    override suspend fun clear() {}
    override fun find(position: GeoPointInterface): Unit? = null

    override suspend fun onCameraChanged(mapCameraPosition: MapCameraPosition) {
        renderer.updateCameraZoom(mapCameraPosition.zoom)
    }

    override fun destroy() {}
}

// 登録
val mapController = LocalMapViewController.current
DisposableEffect(mapController, cameraController) {
    mapController.registerOverlayController(cameraController)
    onDispose { cameraController.destroy() }
}
登録したものは必ず解除してください。地図を破棄せずプロバイダだけ差し替える利用者がいるため、解除漏れは前のプロバイダのレンダラを掴んだままになります。

05 · 機能を受け渡す

ここまでの 4 つで足りない場合だけ使います。プロバイダにしか作れないものが必要なとき、MapServiceRegistry を通してそれを受け取ります。型付きキーで登録・取得するので、キャストは要りません。

実例はマーカークラスタリングです。ネイティブのマーカー型はプロバイダごとに違うため、クラスタリングは自分ではレンダラを作れません。そこでプロバイダが登録した MarkerRenderingSupport を引き当て、そこからレンダラを作ってもらいます。

解決する側(拡張モジュール)
// キーは singleton object として定義する
object MyCapabilityKey : MapServiceKey<MyCapability>

// 取り出す。未登録なら null なので、機能を落として続けるか諦めるかを決める
val services = LocalMapServiceRegistry.current
val capability = services.get(MyCapabilityKey) ?: return
登録する側(プロバイダ)
// マップ1つにつき1つのレジストリ。state が持っている(react / ios と同じ)
state.serviceRegistry.put(MyCapabilityKey, myCapability)

// 消えるときは clear() ではなく remove()。他の capability を巻き添えにしない
DisposableEffect(state) {
    onDispose { state.serviceRegistry.remove(MyCapabilityKey) }
}
キーは必ず解決する側(拡張モジュール)が公開してください。プロバイダはそのキーを import して登録します。逆にするとプロバイダが拡張に依存することになり、依存の向きが反転します。

守ること

この 4 つを守っていれば、利用者がプロバイダを差し替えても、別の拡張と同時に使っても壊れません。

1 · プロバイダのパッケージを import しない
必要になったら、それはサービスレジストリで受け取るべき機能です。
2 · コントローラをキャストして内部を覗かない
react-heatmap は以前これをやっていて、単一スロットのカメラリスナーを退避・復元していました。拡張が 2 つ載ると互いに上書きします。公開の registerOverlayController に置き換え済みです。
3 · 登録したものは必ず解除する
オーバーレイコントローラ、タイルサーバのグループ、サービスレジストリのキー。remove() は 1 件だけ取り下げます。
4 · 未登録なら静かに無効化する
必要な capability が無いプロバイダでは、例外を投げずに何も描かないでください。利用者は他の部分を動かし続けられます。

関連ページ