Ein Erweiterungsmodul erstellen
Heatmap, GeoJSON-Layer und Marker-Clustering sind allesamt separate Pakete, die von außen hinzugefügt wurden, ohne den Core zu ändern. Da die gleiche Schnittstelle öffentlich verfügbar ist, können Dritte ihre eigenen Erweiterungen auf die gleiche Weise verteilen. Diese Seite beschreibt die Vorgehensweise.
00 · Form eines Erweiterungsmoduls
Ein Erweiterungsmodul ist ein unabhängiges Paket, das nur vom Kernmodul abhängt. Es hängt nicht von Provider-Paketen (react-for-*, android-for-*, ios-for-*) ab. Wenn dies eingehalten wird, funktioniert Ihre Erweiterung unabhängig davon, welchen Karten-Provider der Nutzer wählt.
Nur die öffentlichen Typen des Kerns verwenden (Status, Collector, Controller-Vertrag, TileServer, ServiceRegistry).
Das Paket des Providers importieren. Den Controller in einen anderen Typ casten und in die internen Felder schauen.
Die drei bereits ausgelieferten Module dienen als Referenzimplementierungen. Der kürzeste Weg ist, das Modul zu lesen, das dem am ähnlichsten ist, das Sie erstellen möchten.
heatmap zeichnet Rasterkacheln selbst · Kachelserver + RasterLayer geojson-layer kachelt große Vektormengen · Kachelserver + RasterLayer marker-clustering bearbeitet Marker und gibt zurück · MarkerState + Service-Registry
01 · Ein Paket erstellen
Die Abhängigkeiten beschränken sich auf den Core. Der Provider wird von der App des Nutzers gewählt und ist daher von Ihrem Paket aus nicht sichtbar.
dependencies {
implementation("com.mapconductor:core")
implementation("com.mapconductor:compose") // Wenn Sie ein Composable veröffentlichen
// Nicht von android-for-* abhängen
}.package(url: "https://github.com/MapConductor/ios-sdk-core", from: "1.3.1"),
.target(
name: "MyMapExtension",
dependencies: [.product(name: "MapConductorCore", package: "ios-sdk-core")]
// Nicht von ios-for-* abhängen
){
"dependencies": {
"@mapconductor/js-sdk-core": "^0.1.1",
"@mapconductor/js-sdk-react": "^0.1.1"
},
"peerDependencies": { "react": "^18.0.0 || ^19.0.0" }
}02 · Zustand definieren
Der Nutzer interagiert mit dem Zustandsobjekt. Wenn Sie dasselbe Muster wie die Core-Zustandstypen (z. B. `MarkerState`) beibehalten, muss der Nutzer nichts Neues lernen. Wenn Sie einen Mechanismus zum Benachrichtigen über Wertänderungen (Fingerabdruck) vorsehen, können Sie ihn im Core-Collector verwenden, um inkrementelle Updates zu erhalten.
OverlayCollector eine id → State-Map; bündelt Hinzufügen/Entfernen/Differenz und Änderungsmeldungen ComponentState der Vertrag für einen State mit id (Android) fingerPrint() ein Vergleichswert, der nur das Zeichnungsrelevante enthält
03 · Ausgabe für das Rendering wählen
Dies ist die wichtigste Designentscheidung. Anstatt für jeden Provider Rendering-Code zu schreiben, leiten Sie ihn an die bereits im Core vorhandenen Ausgaben weiter. Es gibt zwei Ausgaben.
Tiles selbst zeichnen
Eine Renderingfunktion bei einem lokalen TileServer registrieren und ein einziges RasterLayer platzieren, das auf dieses URL-Template zeigt. Heatmap und GeoJSON-Ebenen sind so aufgebaut. Je größer die Anzahl, desto vorteilhafter; das Erscheinungsbild ist bei jedem Provider völlig identisch.
Als Marker oder Form zurückgeben
Die Eingabe verarbeiten und in MarkerState / PolygonState usw. umwandeln, dann in den Collector schreiben. Ab dort übernimmt der normale Rendering-Pfad des Providers. Clustering funktioniert so. Klicks und Drag-and-Drop funktionieren unverändert.
val tileServer = TileServerRegistry.get()
val renderer = MyTileRenderer(tileSize = 256) // Enthält die Funktion, die eine einzelne Kachel zeichnet
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)public struct MyOverlay: MapOverlayItemProtocol, View {
let state: MyOverlayState
public func append(to content: inout MapViewContent) {
// Genau so eingehängt wie das HeatmapOverlay von ios-heatmap
content.rasterLayers.append(RasterLayer(state: state.rasterLayerState))
}
}import { createRasterLayerState, TileServerRegistry, TileScheme } from '@mapconductor/js-sdk-core';
import { RasterLayer } from '@mapconductor/js-sdk-react';
const tileServer = TileServerRegistry.get();
useEffect(() => {
tileServer.register(groupId, renderer); // Der Renderer zeichnet eine Kachel
return () => { tileServer.unregister(groupId); };
}, [groupId, tileServer, renderer]);
const state = useRef(createRasterLayerState({
id: `my-ext-${groupId}`,
source: {
kind: 'urlTemplate',
template: tileServer.urlTemplate(groupId, renderer.tileSize),
tileSize: renderer.tileSize,
scheme: TileScheme.XYZ,
},
})).current;
return <RasterLayer state={state} />;04 · Kamera und Bereinigung
Wenn Sie Zoom- oder Schwenkbewegungen folgen möchten, manipulieren Sie nicht direkt die Ereignisse des Karten-Controllers. Dies führt zu einer Konkurrenz um einzelne Slot-Listener und bricht in Umgebungen, in denen zwei Erweiterungen geladen sind. Wenn Sie sich als Overlay-Controller registrieren, erhalten Kameraänderungen über denselben Weg wie andere Overlays.
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() {}
}
// Registrieren
val mapController = LocalMapViewController.current
DisposableEffect(mapController, cameraController) {
mapController.registerOverlayController(cameraController)
onDispose { cameraController.destroy() }
}public final class MyCameraController: OverlayControllerProtocol {
public typealias StateType = Void
public typealias EntityType = Void
public typealias EventType = Void
public let zIndex: Int = 0
public var clickListener: ((Void) -> Void)?
public func add(data: [Void]) async {}
public func update(state: Void) async {}
public func clear() async {}
public func find(position: GeoPointProtocol) -> Void? { nil }
public func onCameraChanged(mapCameraPosition: MapCameraPosition) async {
renderer.updateCameraZoom(mapCameraPosition.zoom)
}
public func destroy() {}
}public func append(to content: inout MapViewContent) {
// Unter iOS ist der Karteninhalt ein MapViewContent-Wert und keine View-Hierarchie, daher
// wird der Controller über MapServiceRegistryScope erreicht
MapServiceRegistryScope.current
.get(OverlayControllerRegistryKey.self)?
.register(cameraController)
content.rasterLayers.append(RasterLayer(state: rasterLayerState))
}// Der reguläre Weg, die Kamera zu lesen, ist der State; auch der sichtbare Bereich kommt von hier:
// const camera = mapViewState.cameraPosition;
// const bounds = camera.visibleRegion?.bounds;
// Um Änderungen zu verfolgen, onCameraMove / onCameraMoveEnd nutzen oder wie unten
// einen Overlay-Controller registrieren und onCameraChanged empfangen.
export class MyCameraController implements OverlayController<void, void, void> {
readonly zIndex = 0;
clickListener: ((event: void) => void) | null = null;
constructor(private readonly renderer: MyTileRenderer) {}
add(): Promise<void> { return Promise.resolve(); }
update(): Promise<void> { return Promise.resolve(); }
clear(): Promise<void> { return Promise.resolve(); }
find(): void | null { return null; }
onCameraChanged(camera: MapCameraPosition): void {
this.renderer.updateCameraZoom(camera.zoom);
}
destroy(): void {}
}
// Registrieren
const { controller } = useContext(MapContext) ?? {};
useEffect(() => {
if (!controller) return;
controller.registerOverlayController?.(cameraController);
return () => { controller.unregisterOverlayController?.(cameraController); };
}, [controller, cameraController]);05 · Funktionalität übergeben
Wird nur verwendet, wenn die bisherigen 4 nicht ausreichen. Wenn Sie etwas benötigen, das nur vom Anbieter erstellt werden kann, erhalten Sie es über `MapServiceRegistry`. Da die Registrierung und der Abruf über typisierte Schlüssel erfolgen, ist kein Casting erforderlich.
Ein konkretes Beispiel ist das Marker-Clustering. Da der native Markertyp je nach Anbieter unterschiedlich ist, kann das Clustering den Renderer nicht selbst erstellen. Daher wird das vom Anbieter registrierte `MarkerRenderingSupport` abgerufen und von dort ein Renderer erstellt.
// Den Schlüssel als Singleton-Object definieren object MyCapabilityKey : MapServiceKey<MyCapability> // Abrufen. Nicht registriert heißt null — entscheiden Sie, ob Sie ohne die Funktion weitermachen oder abbrechen val services = LocalMapServiceRegistry.current val capability = services.get(MyCapabilityKey) ?: return
// Eine Registry pro Karte, gehalten vom State (wie bei react und ios)
state.serviceRegistry.put(MyCapabilityKey, myCapability)
// Beim Verschwinden remove() statt clear() — damit andere Capabilities nicht mitgerissen werden
DisposableEffect(state) {
onDispose { state.serviceRegistry.remove(MyCapabilityKey) }
}// Den Schlüssel als Typ definieren, der MapServiceKey entspricht
public enum MyCapabilityKey: MapServiceKey {
public typealias Value = any MyCapability
}
// Die Registry ist nur sichtbar, während der content zusammengesetzt wird
guard let capability = MapServiceRegistryScope.current.get(MyCapabilityKey.self) else { return }import { createMapServiceKey } from '@mapconductor/js-sdk-core';
import { useMapServiceRegistry } from '@mapconductor/js-sdk-react';
export const MyCapabilityKey = createMapServiceKey<MyCapability>();
const services = useMapServiceRegistry();
const capability = services.get(MyCapabilityKey);
if (!capability) return null; // Bei Anbietern ohne Registrierung still deaktivieren// Eine Registry pro Karte, gehalten vom State state.serviceRegistry.put(MyCapabilityKey, myCapability); // Beim Unmount remove() statt clear() — damit andere Capabilities nicht mitgerissen werden state.serviceRegistry.remove(MyCapabilityKey);
Einzuhalten
Wenn Sie diese 4 Punkte einhalten, wird es nicht beschädigt, wenn Nutzer den Anbieter austauschen oder es gleichzeitig mit einer anderen Erweiterung verwenden.
1 · Das Paket des Providers nicht importieren
Wenn es erforderlich wird, sollte es eine Funktion sein, die über die ServiceRegistry empfangen wird.
2 · Den Controller nicht casten, um in ihn hineinzuschauen
react-heatmap hat das früher getan und einen einzelnen Camera-Listener-Slot gesichert und wiederhergestellt. Wenn zwei Erweiterungen geladen sind, überschreiben sie sich gegenseitig. Es wurde bereits durch das öffentliche registerOverlayController ersetzt.
3 · Registrierte Dinge immer aufheben
Overlay-Controller, Gruppen von TileServern, Schlüssel der ServiceRegistry. remove() hebt nur einen Eintrag auf.
4 · Wenn nicht registriert, still deaktivieren
Bei Providern ohne die erforderliche Capability sollte nichts gezeichnet werden, ohne eine Exception zu werfen. Nutzer können den Rest der Anwendung weiter bedienen.