สร้างโมดูลส่วนขยาย
Heatmap, GeoJSON Layer และ Marker Clustering เป็นแพ็กเกจแยกต่างหากที่เพิ่มเข้ามาจากภายนอกโดยไม่ต้องแก้ไข Core ด้วยเหตุที่ส่วนติดต่อเดียวกันถูกเปิดเผยสู่สาธารณะ บุคคลที่สามจึงสามารถกระจายส่วนขยายของตนเองด้วยวิธีเดียวกันได้ หน้านี้แสดงขั้นตอนดังกล่าว
00 · รูปแบบของโมดูลส่วนขยาย
โมดูลส่วนขยายคือ แพ็กเกจอิสระที่ขึ้นอยู่กับโมดูลหลักเท่านั้น ไม่ได้ขึ้นอยู่กับแพ็กเกจของผู้ให้บริการ (react-for-*, android-for-*, ios-for-*) หากปฏิบัติตามสิ่งนี้ ส่วนขยายของคุณจะทำงานได้โดยไม่คำนึงถึงว่าผู้ใช้เลือกผู้ให้บริการแผนที่รายใด
ใช้เฉพาะประเภทสาธารณะของคอร์เสียง (สถานะ, ตัวรวบรวม, สัญญาคอนโทรลเลอร์, TileServer, ServiceRegistry).
นำเข้าแพ็กเกจของผู้ให้บริการ แคสต์คอนโทรลเลอร์ไปยังประเภทอื่นและดูภายในฟิลด์
ทั้ง 3 รายการที่จัดส่งแล้วทำหน้าที่เป็นการนำไปใช้จริงอ้างอิง วิธีที่เร็วที่สุดคืออ่านรายการที่ใกล้เคียงกับสิ่งที่คุณต้องการสร้างที่สุด
heatmap วาดไทล์ราสเตอร์เอง · เซิร์ฟเวอร์ไทล์ + RasterLayer geojson-layer แปลงเวกเตอร์จำนวนมากเป็นไทล์ · เซิร์ฟเวอร์ไทล์ + RasterLayer marker-clustering ปรับมาร์กเกอร์แล้วส่งกลับ · MarkerState + รีจิสทรีบริการ
01 · สร้างแพ็กเกจ
ข้อกำหนดเบื้องต้นจะจำกัดอยู่ที่ Core เท่านั้น ผู้ให้บริการคือสิ่งที่แอปของผู้ใช้เลือก ดังนั้นจึงมองไม่เห็นจากแพ็กเกจของคุณ
dependencies {
implementation("com.mapconductor:core")
implementation("com.mapconductor:compose") // หากจะเปิดเผย Composable
// ไม่ต้องพึ่ง 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")]
// ไม่ต้องพึ่ง 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 · กำหนดสถานะ
ผู้ใช้โต้ตอบกับออบเจ็กต์สถานะ หากคุณคงรูปแบบเดียวกับประเภทสถานะของ Core (เช่น `MarkerState`) ผู้ใช้จะไม่ต้องเรียนรู้สิ่งใหม่หากคุณรวมกลไกในการแจ้งเตือนการเปลี่ยนแปลงค่า (ลายนิ้วมือ) คุณสามารถวางไว้ในตัวรวบรวมของ Core เพื่อรับการอัปเดตแบบเพิ่มหลัง
OverlayCollector แมป id → สถานะ รวมการเพิ่ม/ลบ/ส่วนต่าง และการแจ้งค่าที่เปลี่ยน ComponentState สัญญาของสถานะที่มี id (Android) fingerPrint() ค่าสำหรับเปรียบเทียบ เก็บเฉพาะสิ่งที่มีผลต่อการวาด
03 · เลือกเอาต์พุตการเรนเดอร์
นี่คือการตัดสินใจด้านการออกแบบที่สำคัญที่สุด แทนที่จะเขียนโค้ดการเรนเดอร์สำหรับแต่ละผู้ให้บริการ ให้ส่งต่อไปยังเอาต์พุตที่ Core มีอยู่แล้ว มีเอาต์พุต 2 รายการ
วาดไทล์เอง
ลงทะเบียนฟังก์ชันการเรนเดอร์กับ TileServer ในเครื่อง และวาง RasterLayer ชั้นเดียวที่ชี้ไปที่เทมเพลต URL นั้น แผนที่ความร้อนและเลเยอร์ GeoJSON เป็นเช่นนี้ ยิ่งจำนวนมากยิ่งได้เปรียบ และรูปลักษณ์จะเหมือนกันทุกประการในทุกผู้ให้บริการ
ส่งคืนเป็นเครื่องหมายหรือรูปร่าง
ประมวลผลอินพุตและแปลงเป็น MarkerState / PolygonState ฯลฯ แล้วเขียนลงในตัวรวบรวม จากนั้นเส้นทางการเรนเดอร์ปกติของผู้ให้บริการจะดำเนินการต่อ การจัดกลุ่มเป็นเช่นนี้ การคลิกและลากยังคงทำงานได้ตามปกติ
val tileServer = TileServerRegistry.get()
val renderer = MyTileRenderer(tileSize = 256) // มีฟังก์ชันที่วาดไทล์หนึ่งแผ่น
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) {
// เสียบเข้าเหมือน HeatmapOverlay ของ 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); // renderer วาดไทล์หนึ่งแผ่น
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 · กล้องและการทำความสะอาด
หากคุณต้องการติดตามการซูมหรือการพาน อย่าแก้ไขเหตุการณ์ของตัวควบคุมแผนที่โดยตรง สิ่งนี้จะนำไปสู่การแย่งชิงผู้ฟังสล็อตเดียวและจะเสียหายในสภาพแวดล้อมที่มีการโหลดส่วนขยายสองตัว หากคุณลงทะเบียนเป็นตัวควบคุมซ้อนทับ การเปลี่ยนแปลงกล้องจะมาถึงผ่านเส้นทางเดียวกันกับซ้อนทับอื่นๆ
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() }
}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) {
// บน iOS เนื้อหาแผนที่เป็นค่า MapViewContent ไม่ใช่ลำดับชั้นของวิว ดังนั้น
// จะเข้าถึงคอนโทรลเลอร์ผ่าน MapServiceRegistryScope
MapServiceRegistryScope.current
.get(OverlayControllerRegistryKey.self)?
.register(cameraController)
content.rasterLayers.append(RasterLayer(state: rasterLayerState))
}// เส้นทางปกติในการอ่านกล้องคือ state และพื้นที่ที่มองเห็นก็มาจากที่นี่:
// const camera = mapViewState.cameraPosition;
// const bounds = camera.visibleRegion?.bounds;
// หากต้องการติดตามการเปลี่ยนแปลง ใช้ onCameraMove / onCameraMoveEnd หรือแบบด้านล่าง
// ลงทะเบียนคอนโทรลเลอร์โอเวอร์เลย์แล้วรับ 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 {}
}
// ลงทะเบียน
const { controller } = useContext(MapContext) ?? {};
useEffect(() => {
if (!controller) return;
controller.registerOverlayController?.(cameraController);
return () => { controller.unregisterOverlayController?.(cameraController); };
}, [controller, cameraController]);05 · ส่งต่อฟังก์ชันการทำงาน
จะใช้เฉพาะเมื่อ 4 รายการก่อนหน้านี้ไม่เพียงพอ เมื่อคุณต้องการสิ่งที่ ผู้ให้บริการเท่านั้นที่สร้างได้ คุณจะได้รับสิ่งนั้นผ่าน `MapServiceRegistry` เนื่องจากการลงทะเบียนและการดึงข้อมูลทำด้วยคีย์ที่มีประเภท จึงไม่ต้องการการแปลงประเภท
ตัวอย่างที่เป็นรูปธรรมคือการคลัสเตอร์มาร์กเกอร์ เนื่องจากประเภทมาร์กเกอร์เนทีฟแตกต่างกันไปในแต่ละผู้ให้บริการ การคลัสเตอร์จึงไม่สามารถสร้างเรนเดอเรอร์ด้วยตนเองได้ ดังนั้นจึงดึง `MarkerRenderingSupport` ที่ลงทะเบียนโดยผู้ให้บริการและสร้างเรนเดอเรอร์จากที่นั่น
// นิยามคีย์เป็น singleton object object MyCapabilityKey : MapServiceKey<MyCapability> // ดึงออกมา หากยังไม่ลงทะเบียนจะได้ null จึงต้องเลือกว่าจะทำงานต่อโดยไม่มีฟีเจอร์นี้หรือหยุด val services = LocalMapServiceRegistry.current val capability = services.get(MyCapabilityKey) ?: return
// หนึ่งรีจิสทรีต่อหนึ่งแผนที่ เก็บอยู่ใน state (เหมือน react และ ios)
state.serviceRegistry.put(MyCapabilityKey, myCapability)
// ตอนหายไปให้ใช้ remove() ไม่ใช่ clear() เพื่อไม่ให้ capability อื่นหลุดไปด้วย
DisposableEffect(state) {
onDispose { state.serviceRegistry.remove(MyCapabilityKey) }
}// นิยามคีย์เป็นชนิดที่สอดคล้องกับ MapServiceKey
public enum MyCapabilityKey: MapServiceKey {
public typealias Value = any MyCapability
}
// รีจิสทรีมองเห็นได้เฉพาะตอนกำลังประกอบ 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; // ปิดการทำงานอย่างเงียบ ๆ กับผู้ให้บริการที่ยังไม่ลงทะเบียน// หนึ่งรีจิสทรีต่อหนึ่งแผนที่ เก็บอยู่ใน state state.serviceRegistry.put(MyCapabilityKey, myCapability); // ตอน unmount ให้ใช้ remove() ไม่ใช่ clear() เพื่อไม่ให้ capability อื่นหลุดไปด้วย state.serviceRegistry.remove(MyCapabilityKey);
สิ่งที่ต้องปฏิบัติตาม
หากคุณปฏิบัติตามจุดทั้ง 4 นี้ จะไม่เสียหายเมื่อผู้ใช้เปลี่ยนผู้ให้บริการหรือใช้ร่วมกับส่วนขยายอื่น
1 · อย่านำเข้าแพ็กเกจของผู้ให้บริการ
หากจำเป็นต้องใช้ ควรเป็นฟังก์ชันที่ได้รับผ่าน ServiceRegistry
2 · อย่าแคสต์คอนโทรลเลอร์เพื่อดูภายใน
react-heatmap ทำเช่นนี้มาก่อน โดยบันทึกและคืนค่าสล็อตผู้ฟังกล้องเดียว เมื่อมีส่วนขยายสองตัว จะเขียนทับซึ่งกันและกัน ได้รับการเปลี่ยนแทนที่ด้วย registerOverlayController สาธารณะแล้ว
3 · ต้องยกเลิกสิ่งที่ลงทะเบียนไว้เสมอ
คอนโทรลเลอร์การซ้อนทับ, กลุ่ม TileServer, คีย์ของ ServiceRegistry remove() จะยกเลิกเพียงรายการเดียว
4 · หากไม่ได้ลงทะเบียน ให้ปิดใช้งานอย่างเงียบๆ
สำหรับผู้ให้บริการที่ไม่มีความสามารถที่จำเป็น ไม่ควรวาดอะไรเลยโดยไม่โยนข้อยกเว้น ผู้ใช้สามารถใช้งานส่วนอื่นต่อได้