ドキュメント / コンセプト / アーキテクチャ

アーキテクチャ概要

MapConductor はアプリと各種地図 SDK の橋渡しをします。アプリは統一された地図 API だけを呼び、MapConductor が選択されたプロバイダへ処理を転送します。Android(Kotlin + Compose)・iOS(Swift + SwiftUI)・React(Web)で、パッケージの分け方も内部の役割分担も同じ設計になっています。

ANDROID
android-sdk-core
iOS
ios-sdk-core
REACT
js-sdk-core / js-sdk-react

01 · 全体像

どのプラットフォームでも処理の流れは同じです。アプリ → 統一された地図 API → Core → 各地図 SDK ドライバー → 各地図 SDK。上の 2 層だけがアプリの書くコードで、下の 2 層は差し替え可能です。

APP

UI とビジネスロジック

React · Compose · SwiftUI
UNIFIED API

統一された地図 API

MapViewState · Marker · Camera
CORE

コア機能(地図・マーカー・図形・イベント)

Manager · Controller · Overlay
DRIVER

各地図 SDK ドライバー

*-for-googlemaps / -maplibre / …
MAP SDK

各地図 SDK(ネイティブ / JavaScript)

Google Maps · MapLibre · Mapbox · MapKit · ArcGIS · HERE …

プロバイダを変えるとき差し替わるのは最下層の 2 つだけです。画面のコードは統一 API に対して書かれているため、そのまま動きます。

プラットフォーム

02 · プラットフォーム別の構成

Core とドライバーはパッケージとして分かれています。アプリは Core と、使うプロバイダのドライバーだけを依存に加えます。

android-sdk-core が共通モデルと差分適用を持ち、android-sdk-compose が Compose 向けの状態ホルダーを提供します。GeoJSON・クラスタリング・ヒートマップは追加モジュール(android-geojson-layer / android-marker-clustering / android-heatmap)として独立しています。

03 · レイヤー構成

プラットフォームをまたいで再利用できるように、責務を 6 つのレイヤーに分けています。上のレイヤーは下のレイヤーの実体を知りません。

#
レイヤー
役割
1
UI フレームワーク
React / Vue / Jetpack Compose / SwiftUI など、アプリが使う宣言的 UI。
2
統一された地図 API
マーカー・図形・カメラ・イベントを共通の概念で扱う MapConductor の API。アプリが実際に書くコードはここに対して記述します。
3
ブリッジ
統一 API の操作を実行環境に応じて下位へ橋渡しします。UI と描画が同じ実行環境で完結する場合は不要で、両者が別の実行環境に分かれるときだけ介在します。
4
クロスプラットフォーム層
ネイティブ地図 SDK の組み込み方法と宣言方法は、この層だけがプラットフォームごとに異なります。
5
ネイティブ SDK / ドライバー
カメラ・スタイル・オーバーレイの描画を実際に受け持つ層(Android / iOS のネイティブ、または Web の JS ドライバー)。
6
地図 SDK
Google Maps / MapKit / MapLibre / Mapbox / ArcGIS / HERE などの実体。

この分離により、「どのプラットフォームか」ではなく「地図で何をしたいか」に集中したコードをそのまま複数環境へ展開できます。プラットフォーム固有の差分は、原則としてクロスプラットフォーム層のインストール/宣言方法だけに閉じ込められます。

React Native の場合

Web と React Native は、アプリが書くコードを共有します。状態オブジェクトもコンポーネントも同じで、選ぶ MapView を差し替えるだけです。違うのはその下です。Web ではドライバーが同じ実行環境の JavaScript の地図ライブラリを呼びますが、React Native ではブリッジを介して MapConductor のネイティブ SDK へ渡り、最終的にネイティブの地図 SDK が描画します。

同じコードが、どこで分かれるか
SHARED

アプリが書くコード

js-sdk-core · js-sdk-react
WEB

React

DRIVER
JavaScript のドライバー
react-for-*
ブリッジは要りません(同じ実行環境)
MAP SDK
JavaScript の地図 SDK
Google Maps · MapLibre · Leaflet · Cesium …
REACT NATIVE

React Native

DRIVER
ネイティブへ渡すドライバー
reactnative-for-*
BRIDGE
RN のネイティブモジュール
レイヤー 3
NATIVE SDK
MapConductor ネイティブ SDK → 地図 SDK
Google Maps · MapLibre · ArcGIS · HERE

上の表でいうレイヤー 3 がブリッジです。Web では UI と描画が同じ実行環境で完結するので介在しません。React Native では両者が別の実行環境に分かれるため、ここが要ります。プラットフォームごとに違うのはこの層だけで、その上は変わりません。

React Native で使えるプロバイダは Google Maps・MapLibre・ArcGIS・HERE の 4 つです。ネイティブ側に対応するモジュールがあるものに限られるため、Web の 13 プロバイダより少なくなります。

04 · Core の内部構造

Core はマーカー・ポリライン・ポリゴン・円・地上画像・ラスターレイヤーといった要素ごとに、同じ 7 つの役割を繰り返す形で構成されています。1 つ覚えれば、他の要素も同じ読み方で追えます。

役割
責務
実装場所
State
アプリが渡す不変の値。座標・色・zIndex など。fingerPrint() で内容のハッシュを返します。
core
Entity
State と、プロバイダが実際に作ったオブジェクト、そのときの fingerPrint を束ねた 1 件分の記録。
core
Manager
Entity を id で保持する台帳。座標からのヒットテスト(find)もここが担当します。
core
Controller
add / update / clear / find / onCameraChanged / destroy を持つ操作の窓口。差分の判定を行います。
core
Overlay
同種の要素をまとめた描画単位。zIndex による重なり順を決めます。
core
OverlayRenderer
onAdd / onChange / onRemove / onPostProcess を各地図 SDK の呼び出しに変換します。要素ごとに用意された抽象クラス(AbstractPolygonOverlayRenderer など)を継承して 3 メソッドを埋めるだけです。
driver
Capable
そのプロバイダがどの要素を扱えるかを型として宣言します(compositionPolygons / updatePolygon / hasPolygon)。要素ごとに 1 つ用意され、Kotlin では PolygonCapableInterface、TypeScript では PolygonCapable のように命名されます。Swift はプロトコルではなく、そのプロバイダが PolygonController を持つかどうかで表します。
driver

重要なのは境界の位置です。Core 側(State / Entity / Manager / Controller / Overlay)は 3 言語でほぼ同じコードとして書かれており、プロバイダごとに書くのは Renderer と、その要素を扱えるという宣言だけです。

ANDROID · Kotlin
iOS · Swift
REACT · TypeScript
core/polygon/PolygonManager.kt
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonManager.swift
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonManager.ts
polygon/PolygonCapable.ts
AbstractPolygonOverlayRenderer.ts

05 · 差分適用の流れ

アプリは「今あるべき状態の配列」を渡すだけです。Core が前回との差分を求め、増えたもの・変わったもの・消えたものに分けてドライバーへ渡します。地図オブジェクトを作り直さないので、大量の要素でも描画が安定します。

controller/OverlayRendererInterface.ts · 3プラットフォーム共通の契約
export interface OverlayRendererInterface<ActualType, StateType, EntityType> {
    onAdd(data: StateType[]): Promise<Array<ActualType | null>> | Array<ActualType | null>;
    onChange(data: Array<ChangeParamsInterface<EntityType>>): Promise<Array<ActualType | null>> | Array<ActualType | null>;
    onRemove(data: EntityType[]): Promise<void> | void;
    onPostProcess(): Promise<void> | void;
}

onChange は前回の Entity(prev)と今回の State(current)の両方を受け取ります。ドライバーは変わったプロパティだけを更新でき、Kotlin / Swift でもシグネチャは同じです。

06 · ドライバーが実装するもの

新しい地図 SDK に対応する作業は、要素ごとの抽象レンダラーを継承して 3 つの操作を埋めることに閉じます。Core のモデルや差分ロジックには手を入れません。

android-for-googlemaps · レンダラーの実装
// ドライバーは抽象レンダラーを継承し、3つの操作だけを埋める
internal class GoogleMapPolygonOverlayRenderer(
    override val holder: GoogleMapViewHolder,
    override val coroutine: CoroutineScope,
) : AbstractPolygonOverlayRenderer<GoogleMapActualPolygon>() {

    override suspend fun createPolygon(state: PolygonState) = /* map.addPolygon(...) */
    override suspend fun updatePolygonProperties(polygon, current, prev) = /* 差分だけ反映 */
    override suspend fun removePolygon(entity: PolygonEntityInterface<GoogleMapActualPolygon>) =
        entity.polygon.remove()
}

生成・更新・削除の 3 つだけがプロバイダ固有です。差分の判定・台帳の管理・ヒットテストは Core が済ませた状態で呼ばれます。

07 · 抽象化の範囲とエスケープハッチ

MapConductor は各地図 SDK の全機能をラップしません。共通してよく使われる操作にフォーカスし、それを超える要求には 2 つの逃げ道を用意しています。

getMapViewHolder() · ネイティブ機能を直接呼ぶ
// 共通インターフェースの map は unknown。プロバイダの型へ絞り込んでから使う
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;

map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });

共通 API はシンプルなまま、各プロバイダ固有の強みも損なわない設計です。詳しくは「ネイティブ拡張」を参照してください。

関連ページ