Crear un módulo de extensión
El mapa de calor, la capa GeoJSON y la agrupación de marcadores son todos paquetes separados que se agregan desde fuera sin modificar Core. Dado que la misma interfaz es pública, terceros también pueden distribuir sus propias extensiones de la misma manera. Esta página muestra los pasos.
00 · Forma de un módulo de extensión
Un módulo de extensión es un paquete independiente que depende solo del módulo principal. No depende de los paquetes del proveedor (react-for-*, android-for-*, ios-for-*). Si se cumple esto, su extensión funcionará sin importar qué proveedor de mapas elija el usuario.
Usar solo los tipos públicos del núcleo (estado, recopilador, contrato de controlador, TileServer, ServiceRegistry).
Importar el paquete del proveedor. Convertir el controlador a otro tipo y revisar los campos internos.
Los tres ya enviados sirven como implementaciones de referencia. El camino más rápido es leer el que más se parezca a lo que quieres crear.
heatmap dibuja mosaicos ráster por su cuenta · servidor de mosaicos + RasterLayer geojson-layer convierte vectores masivos en mosaicos · servidor de mosaicos + RasterLayer marker-clustering reelabora marcadores y los devuelve · MarkerState + registro de servicios
01 · Crear un paquete
Las dependencias se limitan solo al núcleo. El proveedor es lo que elige la aplicación del usuario, por lo que no es visible desde tu paquete.
dependencies {
implementation("com.mapconductor:core")
implementation("com.mapconductor:compose") // Si expones un Composable
// No dependas de android-for-*
}.package(url: "https://github.com/MapConductor/ios-sdk-core", from: "1.3.1"),
.target(
name: "MyMapExtension",
dependencies: [.product(name: "MapConductorCore", package: "ios-sdk-core")]
// No dependas de ios-for-*
){
"dependencies": {
"@mapconductor/js-sdk-core": "^0.1.1",
"@mapconductor/js-sdk-react": "^0.1.1"
},
"peerDependencies": { "react": "^18.0.0 || ^19.0.0" }
}02 · Definir el estado
El usuario interactúa con el objeto de estado. Si sigues la misma convención que los tipos de estado del núcleo (como `MarkerState`), el usuario no tendrá que aprender cosas adicionales. Si incluyes un mecanismo para notificar cambios de valor (huella digital), puedes ponerlo en el colector del núcleo para recibir actualizaciones incrementales.
OverlayCollector un mapa id → estado; reúne alta/baja/diferencia y avisos de cambio ComponentState el contrato de un estado que lleva un id (Android) fingerPrint() un valor de comparación con solo lo que afecta al dibujo
03 · Elegir la salida de renderizado
Esta es la decisión de diseño más importante. En lugar de escribir código de renderizado para cada proveedor, lo envías a las salidas que ya tiene el núcleo. Hay dos salidas.
Dibujar tiles uno mismo
Registrar una función de renderizado en un TileServer local y colocar una sola capa RasterLayer que apunte a esa plantilla de URL. Los mapas de calor y las capas de GeoJSON funcionan así. Cuantos más elementos, más ventajoso; la apariencia coincide completamente con cualquier proveedor.
Devolver como marcadores o formas
Procesar la entrada y convertirla a MarkerState / PolygonState, etc., y escribirla en el recopilador. A partir de ahí, la ruta de renderizado normal del proveedor se encarga. El agrupamiento funciona así. Los clics y el arrastre funcionan tal cual.
val tileServer = TileServerRegistry.get()
val renderer = MyTileRenderer(tileSize = 256) // Contiene la función que dibuja un solo mosaico
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) {
// Se enchufa exactamente igual que el HeatmapOverlay de 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); // El renderizador dibuja un mosaico
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 · Cámara y limpieza
Si desea seguir el zoom o el desplazamiento, no manipule directamente los eventos del controlador del mapa. Esto daría lugar a una competencia por los oyentes de ranura única y se rompería en entornos donde se cargan dos extensiones. Si se registra como controlador de superposición, los cambios de cámara llegarán por la misma ruta que otras superposiciones.
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() {}
}
// Regístralo
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) {
// En iOS el contenido del mapa es un valor MapViewContent y no una jerarquía de vistas, así que
// se llega al controlador mediante MapServiceRegistryScope
MapServiceRegistryScope.current
.get(OverlayControllerRegistryKey.self)?
.register(cameraController)
content.rasterLayers.append(RasterLayer(state: rasterLayerState))
}// La vía normal para leer la cámara es el estado; la región visible también sale de aquí:
// const camera = mapViewState.cameraPosition;
// const bounds = camera.visibleRegion?.bounds;
// Para seguir los cambios usa onCameraMove / onCameraMoveEnd, o como abajo
// registra un controlador de capa y recibe onCameraChanged.
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 {}
}
// Regístralo
const { controller } = useContext(MapContext) ?? {};
useEffect(() => {
if (!controller) return;
controller.registerOverlayController?.(cameraController);
return () => { controller.unregisterOverlayController?.(cameraController); };
}, [controller, cameraController]);05 · Transferir funcionalidad
Solo se usa si los 4 anteriores no son suficientes. Cuando necesita algo que solo el proveedor puede crear, lo recibe a través de `MapServiceRegistry`. Dado que el registro y la obtención se realizan con claves tipadas, no se requiere conversión.
Un ejemplo concreto es la agrupación de marcadores. Dado que el tipo de marcador nativo difiere según el proveedor, la agrupación no puede crear el renderizador por sí misma. Por lo tanto, se recupera el `MarkerRenderingSupport` registrado por el proveedor y se crea un renderizador desde allí.
// Define la clave como un object singleton object MyCapabilityKey : MapServiceKey<MyCapability> // Recupérala. Si no está registrada es null, así que decide si sigues sin la función o te detienes val services = LocalMapServiceRegistry.current val capability = services.get(MyCapabilityKey) ?: return
// Un registro por mapa, en poder del estado (igual que en react e ios)
state.serviceRegistry.put(MyCapabilityKey, myCapability)
// Cuando desaparezca usa remove(), no clear(), para no arrastrar otras capacidades
DisposableEffect(state) {
onDispose { state.serviceRegistry.remove(MyCapabilityKey) }
}// Define la clave como un tipo que cumple MapServiceKey
public enum MyCapabilityKey: MapServiceKey {
public typealias Value = any MyCapability
}
// El registro solo es visible mientras se arma el content
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; // Desactívalo en silencio en proveedores donde no esté registrado// Un registro por mapa, en poder del estado state.serviceRegistry.put(MyCapabilityKey, myCapability); // Al desmontar usa remove(), no clear(), para no arrastrar otras capacidades state.serviceRegistry.remove(MyCapabilityKey);
Que se debe cumplir
Si cumple estos 4 puntos, no se romperá cuando los usuarios cambien el proveedor o lo usen junto con otra extensión.
1 · No importar el paquete del proveedor
Si es necesario, debería ser una función que se reciba a través de la ServiceRegistry.
2 · No convertir el controlador para revisar su interior
react-heatmap hacía esto anteriormente y guardaba/restauraba un único slot de escucha de cámara. Cuando se cargan dos extensiones, se sobrescriben entre sí. Ya se reemplazó por registerOverlayController público.
3 · Deshacer siempre lo registrado
Controladores de superposición, grupos de TileServer, claves de ServiceRegistry. remove() solo quita un elemento.
4 · Si no está registrado, desactivar silenciosamente
En proveedores sin la capacidad necesaria, no debe dibujarse nada sin lanzar una excepción. Los usuarios pueden seguir usando otras partes.