アーキテクチャ概要
MapConductor はアプリと各種地図 SDK の橋渡しをします。アプリは統一された地図 API だけを呼び、MapConductor が選択されたプロバイダへ処理を転送します。Android(Kotlin + Compose)・iOS(Swift + SwiftUI)・React(Web)で、パッケージの分け方も内部の役割分担も同じ設計になっています。
01 · 全体像
どのプラットフォームでも処理の流れは同じです。アプリ → 統一された地図 API → Core → 各地図 SDK ドライバー → 各地図 SDK。上の 2 層だけがアプリの書くコードで、下の 2 層は差し替え可能です。
UI とビジネスロジック
統一された地図 API
コア機能(地図・マーカー・図形・イベント)
各地図 SDK ドライバー
各地図 SDK(ネイティブ / JavaScript)
プロバイダを変えるとき差し替わるのは最下層の 2 つだけです。画面のコードは統一 API に対して書かれているため、そのまま動きます。
02 · プラットフォーム別の構成
Core とドライバーはパッケージとして分かれています。アプリは Core と、使うプロバイダのドライバーだけを依存に加えます。
android-sdk-core が共通モデルと差分適用を持ち、android-sdk-compose が Compose 向けの状態ホルダーを提供します。GeoJSON・クラスタリング・ヒートマップは追加モジュール(android-geojson-layer / android-marker-clustering / android-heatmap)として独立しています。
ios-sdk-core が共通モデルと SwiftUI ビューの土台を持ち、各 SwiftUI ビューはプロバイダパッケージ(ios-for-*)が提供します。MapKit ドライバーは Apple 純正 SDK 用で、追加の課金や API キーなしに使えます。
js-sdk-core は React に依存しない TypeScript のコアで、共通モデルと差分適用を持ちます(Kotlin / Swift の core とほぼ同じ構成です)。js-sdk-react がそれを React に載せ替え、Marker / Polygon / Polyline / Circle / GroundImage / RasterLayer / InfoBubble といったコンポーネントを提供します。地図ビュー自体は各ドライバー(react-for-*)が MapLibreMapView / MapLibreMapView2D と useMapLibreViewState のように「ビュー + フック」の組で公開します。GeoJSON・クラスタリング・ヒートマップ・アイコンは追加パッケージ(react-geojson-layer / react-marker-clustering / react-heatmap / react-icons)として独立しています。
03 · レイヤー構成
プラットフォームをまたいで再利用できるように、責務を 6 つのレイヤーに分けています。上のレイヤーは下のレイヤーの実体を知りません。
この分離により、「どのプラットフォームか」ではなく「地図で何をしたいか」に集中したコードをそのまま複数環境へ展開できます。プラットフォーム固有の差分は、原則としてクロスプラットフォーム層のインストール/宣言方法だけに閉じ込められます。
React Native の場合
Web と React Native は、アプリが書くコードを共有します。状態オブジェクトもコンポーネントも同じで、選ぶ MapView を差し替えるだけです。違うのはその下です。Web ではドライバーが同じ実行環境の JavaScript の地図ライブラリを呼びますが、React Native ではブリッジを介して MapConductor のネイティブ SDK へ渡り、最終的にネイティブの地図 SDK が描画します。
React
React Native
上の表でいうレイヤー 3 がブリッジです。Web では UI と描画が同じ実行環境で完結するので介在しません。React Native では両者が別の実行環境に分かれるため、ここが要ります。プラットフォームごとに違うのはこの層だけで、その上は変わりません。
React Native で使えるプロバイダは Google Maps・MapLibre・ArcGIS・HERE の 4 つです。ネイティブ側に対応するモジュールがあるものに限られるため、Web の 13 プロバイダより少なくなります。
04 · Core の内部構造
Core はマーカー・ポリライン・ポリゴン・円・地上画像・ラスターレイヤーといった要素ごとに、同じ 7 つの役割を繰り返す形で構成されています。1 つ覚えれば、他の要素も同じ読み方で追えます。
重要なのは境界の位置です。Core 側(State / Entity / Manager / Controller / Overlay)は 3 言語でほぼ同じコードとして書かれており、プロバイダごとに書くのは Renderer と、その要素を扱えるという宣言だけです。
PolygonCapableInterface.kt
AbstractPolygonOverlayRenderer.kt
polygon/PolygonOverlayRenderer.swift
polygon/PolygonHoleSplit.swift
polygon/PolygonCapable.ts
AbstractPolygonOverlayRenderer.ts
05 · 差分適用の流れ
アプリは「今あるべき状態の配列」を渡すだけです。Core が前回との差分を求め、増えたもの・変わったもの・消えたものに分けてドライバーへ渡します。地図オブジェクトを作り直さないので、大量の要素でも描画が安定します。
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 のモデルや差分ロジックには手を入れません。
// ドライバーは抽象レンダラーを継承し、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 が済ませた状態で呼ばれます。
// Kotlin と同じ形。抽象レンダラーを継承して3つの操作だけを埋める
final class MapKitPolygonOverlayRenderer: AbstractPolygonOverlayRenderer<MKPolygon> {
override func createPolygon(state: PolygonState) async -> MKPolygon? { /* MKPolygon(...) */ }
override func updatePolygonProperties(polygon, current, prev) async -> MKPolygon? { /* 差分だけ反映 */ }
override func removePolygon(entity: PolygonEntity<MKPolygon>) async { /* mapView.removeOverlay */ }
}
// コントローラーは Core のジェネリック実装をそのまま使う
final class MapKitPolygonController: PolygonController<MKPolygon, MapKitPolygonOverlayRenderer> { }Swift では「そのプロバイダが持つコントローラー」が機能一覧になります。ポリゴンの穴やマーカーアニメーションのように SDK 側が持たない機能は、対応するレンダラーを実装しないか、コア側の代替表現(穴を持たない単純リングへ割る PolygonHoleSplit など)に切り替えることで表現します。
// Kotlin / Swift と同じ形。抽象レンダラーを継承して3つの操作だけを埋める
export class MapLibrePolygonOverlayRenderer extends AbstractPolygonOverlayRenderer<
MapLibreMapViewHolder,
MapLibreActualPolygon
> {
async createPolygon(state: PolygonState) { /* GeoJSON フィーチャを作る */ }
async updatePolygonProperties({ current, prev }) { /* 差分だけ反映 */ }
async removePolygon(entity: PolygonEntity<MapLibreActualPolygon>) { /* 取り除く */ }
// 差分適用のあと、残った Entity からソースを書き直す
override async onPostProcess() { this.layer.draw(this.polygonManager.allEntities()); }
}
// 扱える要素は Capable インターフェースを implements することで型として宣言する
export class MapLibreViewController extends BaseMapViewController
implements MapViewControllerInterface, MarkerCapable, PolygonCapable, /* … */ { }TypeScript でも生成・更新・削除の 3 つだけがプロバイダ固有です。MapLibre のようにレイヤーへまとめて描く SDK では、1 件ずつの削除ではなく onPostProcess でソース全体を書き直す、といった描き方の違いもレンダラーの中に閉じます。ドライバーが扱える要素は MapLibreViewController が実装する Capable インターフェース(PolygonCapable など)で表され、使えるかどうかは型で分かります。
07 · 抽象化の範囲とエスケープハッチ
MapConductor は各地図 SDK の全機能をラップしません。共通してよく使われる操作にフォーカスし、それを超える要求には 2 つの逃げ道を用意しています。
// 共通インターフェースの map は unknown。プロバイダの型へ絞り込んでから使う
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;
map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });共通 API はシンプルなまま、各プロバイダ固有の強みも損なわない設計です。詳しくは「ネイティブ拡張」を参照してください。