Docs / Extension layers / Build your own

Building an extension module

Heatmaps, the GeoJSON layer and marker clustering are all separate packages bolted on from the outside without changing Core. The same seams are public, so anyone can ship their own extension the same way. This page is that procedure.

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

00 · What an extension is

An extension module is a standalone package that depends on the core module only — never on a provider package (react-for-*, android-for-*, ios-for-*). Hold that line and your extension works whichever map provider the user picked.

OK
Use only the core's public types — states, collectors, the controller contracts, the tile server, the service registry.
NG
Import a provider package. Cast the controller to a different shape to reach an internal field.

The three shipped extensions are the reference implementations. Reading whichever is closest to what you want is the shortest path.

What to read
heatmap          ラスタタイルを自前で描く      · タイルサーバ + RasterLayer
geojson-layer    大量のベクタをタイル化する    · タイルサーバ + RasterLayer
marker-clustering マーカーを加工して差し戻す   · MarkerState + サービスレジストリ
Platform

01 · Create the package

Depend on core alone. The provider is chosen by the consuming app, so your package never sees it.

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

02 · Define your state

Your users hold a state object. Follow the same shape as the core state types (MarkerState and friends) and they have nothing new to learn. Give it a fingerprint so it can ride the core collector and you get diffed updates for free.

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

03 · Pick a rendering output

This is the design decision that matters. Do not write per-provider drawing code — feed an output core already has. There are two.

A · RasterLayer

Draw your own tiles

Register a render function with the local tile server and place one RasterLayer pointing at its URL template. Heatmap and the GeoJSON layer both work this way. It scales with data size and looks identical on every provider.

B · Existing overlays

Emit markers or shapes

Transform your input into MarkerState / PolygonState and write it into the collector; the provider's normal path takes it from there. Clustering works this way, and clicks and drags keep working.

A · tile server + RasterLayer (as in 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)
If you pick B on React, do not write to the collector directly: resolve the MarkerRenderingSupport the provider registered and build your renderer from it (see 05), so the provider stays free to change how markers reach the map.

04 · Camera and teardown

If you need to follow zoom and pan, do not touch the map controller's own listeners: they are single-slot and two extensions will fight over them in a user's app. Register as an overlay controller and camera changes arrive through the same path every other overlay uses.

HeatmapCameraController.kt — same shape
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() }
}
Always unregister what you registered. Users swap providers without tearing down the map, so a leaked registration keeps a renderer from the previous provider alive.

05 · Hand over a capability

Only needed when the four steps above are not enough: when you need something only the provider can build, take it through the MapServiceRegistry. Registration and lookup are keyed by a typed key, so nothing gets cast.

Marker clustering is the live example: native marker types differ per provider, so clustering cannot build a renderer itself. It resolves the MarkerRenderingSupport the provider registered and has that build the renderer.

resolving side (your extension)
// キーは singleton object として定義する
object MyCapabilityKey : MapServiceKey<MyCapability>

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

// 消えるときは clear() ではなく remove()。他の capability を巻き添えにしない
DisposableEffect(state) {
    onDispose { state.serviceRegistry.remove(MyCapabilityKey) }
}
The key must be published by the resolving side — your extension. Providers import it to register. The other way round would make providers depend on extensions and invert the dependency direction.

Rules to keep

Keep these four and your extension survives a provider swap and coexists with other extensions.

1 · Never import a provider package
If you think you need to, that is a capability you should be taking through the service registry.
2 · Never cast the controller to reach internals
react-heatmap used to do exactly this, saving and restoring the single-slot camera listener. Two extensions would overwrite each other. It now uses the public registerOverlayController instead.
3 · Unregister everything you register
Overlay controllers, tile server groups, registry keys. remove() takes back exactly one entry.
4 · Degrade quietly when unresolved
On a provider that does not register what you need, draw nothing rather than throwing. The rest of the user's screen keeps working.

Related pages