ภาพรวมสถาปัตยกรรม
MapConductor เป็นสะพานเชื่อมระหว่างแอปพลิเคชันและ SDK แผนที่ต่างๆ แอปพลิเคชันเรียกใช้เพียง API แผนที่ที่รวมกัน และ MapConductor จะส่งต่อการประมวลผลไปยังผู้ให้บริการที่เลือก บน Android (Kotlin + Compose), iOS (Swift + SwiftUI) และ React (Web) การแบ่งแพ็กเกจและการแบ่งหน้าที่ภายในมีการออกแบบเหมือนกัน
01 · ภาพโดยรวม
ขั้นตอนการประมวลผลจะเหมือนกันในทุกแพลตฟอร์ม: แอปพลิเคชัน → API แผนที่ที่รวมกัน → Core → ไดรเวอร์ SDK แผนที่ต่างๆ → SDK แผนที่ต่างๆ เพียง 2 ชั้นบนสุดเท่านั้นที่เป็นโค้ดที่แอปพลิเคชันเขียน ส่วน 2 ชั้นล่างสุดสามารถเปลี่ยนแทนได้
UI และตรรกะทางธุรกิจ
API แผนที่แบบรวมศูนย์
ฟีเจอร์หลัก (แผนที่ มาร์กเกอร์ รูปทรง อีเวนต์)
ไดรเวอร์ของผู้ให้บริการ
SDK แผนที่ (เนทีฟ / JavaScript)
เมื่อเปลี่ยนผู้ให้บริการ สิ่งที่จะถูกเปลี่ยนคือเพียง 2 ชั้นล่างสุดเท่านั้น เนื่องจากโค้ดหน้าจอเขียนขึ้นสำหรับ API ที่รวมกัน จึงยังคงทำงานได้ตามเดิม
02 · การกำหนดค่าตามแพลตฟอร์ม
Core และไดรเวอร์แยกเป็นแพ็กเกจ แอปพลิเคชันจะเพิ่มเฉพาะ Core และไดรเวอร์ของผู้ให้บริการที่ใช้เป็น dependency เท่านั้น
android-sdk-core มีโมเดลที่ใช้ร่วมกันและการใช้ความแตกต่าง ในขณะที่ android-sdk-compose มอบที่เก็บสถานะสำหรับ Compose GeoJSON, การจัดกลุ่ม, และแผนที่ความร้อนแยกเป็นโมดูลเพิ่มเติม (android-geo-layer / android-marker-clustering / android-heatmap)
ios-sdk-core มีโมเดลที่ใช้ร่วมกันและพื้นฐานสำหรับมุมมอง SwiftUI และมุมมอง SwiftUI แต่ละตัวจัดเตรียมโดยแพ็กเกจผู้ให้บริการ (ios-for-*) ไดรเวอร์ MapKit สำหรับ SDK ของ Apple ดั้งเดิมและสามารถใช้งานได้โดยไม่มีค่าใช้จ่ายเพิ่มเติมหรือคีย์ API
js-sdk-core คือ TypeScript core ที่ไม่ขึ้นกับ React ซึ่งมี common model และ differential application (มีโครงสร้างเกือบเหมือนกับ core ของ Kotlin / Swift) js-sdk-react นำมันไปใช้บน React และมอบคอมโพเนนต์ เช่น Marker / Polygon / Polyline / Circle / GroundImage / RasterLayer / InfoBubble แผนที่วิวเองแต่ละไดรเวอร์ (react-for-*) จะเปิดเผยเป็นคู่ของ "view + hook" เช่น MapLibreMapView / MapLibreMapView2D และ useMapLibreViewState GeoJSON การจัดกลุ่ม แผนที่ความร้อน และไอคอนแยกเป็นแพ็กเกจเสริม (react-geo-layer / react-marker-clustering / react-heatmap / react-icons)
03 · องค์ประกอบของเลเยอร์
เพื่อให้นำกลับมาใช้ซ้ำข้ามแพลตฟอร์มได้ หน้าที่ถูกแบ่งออกเป็น 6 เลเยอร์ เลเยอร์บนไม่ทราบถึงเอนทิตีของเลเยอร์ล่าง
ด้วยการแยกส่วนนี้ โค้ดที่มุ่งเน้นที่ “สิ่งที่ต้องการทำกับแผนที่” มากกว่า “แพลตฟอร์มใด” สามารถนำไปใช้ในหลายสภาพแวดล้อมได้ทันที ความแตกต่างเฉพาะของแพลตฟอร์มจะถูกจำกัดไว้เป็นหลักการเฉพาะในวิธีการติดตั้ง / การประกาศของเลเยอร์ข้ามแพลตฟอร์มเท่านั้น
ในกรณีของ React Native
Web และ React Native แชร์โค้ดที่แอปเขียน วัตถุสถานะและคอมโพเนนต์ก็เหมือนกัน เพียงแค่เปลี่ยน MapView ที่เลือก ส่วนที่ต่างอยู่ด้านล่าง บน Web ไดรเวอร์เรียกใช้ไลบรารีแผนที่ JavaScript ในสภาพแวดล้อมรันไทม์เดียวกัน แต่ใน React Native จะผ่านไปยัง MapConductor Native SDK ผ่านบริดจ์ และในท้ายที่สุด Native Map SDK จะทำการเรนเดอร์
React
React Native
เลเยอร์ 3 ในตารางด้านบนคือบริดจ์ บน Web จะไม่มีส่วนนี้เพราะ UI และการเรนเดอร์เสร็จสมบูรณ์ในสภาพแวดล้อมรันไทม์เดียวกัน ใน React Native ต้องมีส่วนนี้เพราะทั้งสองแยกกันอยู่ในสภาพแวดล้อมรันไทม์ต่างกัน เฉพาะเลเยอร์นี้เท่านั้นที่แตกต่างตามแพลตฟอร์ม ส่วนด้านบนไม่เปลี่ยนแปลง
ผู้ให้บริการที่ใช้งานได้กับ React Native มีอยู่ 4 ราย คือ Google Maps, MapLibre, ArcGIS และ HERE เนื่องจากจำกัดเฉพาะรายที่มีโมดูลที่สอดคล้องกันในฝั่ง Native จึงมีจำนวนน้อยกว่าผู้ให้บริการ 13 รายบน Web
04 · โครงสร้างภายในของ Core
Core มีโครงสร้างซ้ำในรูปแบบของบทบาททั้ง 7 อย่างเดียวกันสำหรับแต่ละองค์ประกอบ เช่น เครื่องหมาย, เส้นโค้ง, รูปหลายเหลี่ยม, วงกลม, ภาพพื้นดิน และเลเยอร์แรสเตอร์ ถ้าจำได้อย่างเดียว สามารถติดตามองค์ประกอบอื่นได้ในลักษณะเดียวกัน
สิ่งสำคัญคือตำแหน่งของขอบเขต ด้าน 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 หรือตรรกะความแตกต่าง
// ไดรเวอร์สืบทอดจากเรนเดอเรอร์นามธรรม แล้วเติมเพียงสามการทำงาน
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 สืบทอดจากเรนเดอเรอร์นามธรรมแล้วเติมสามการทำงาน
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 ไม่มี เช่น รูในโพลีกอนหรือแอนิเมชันเครื่องหมาย จะแสดงออกโดยไม่ต้องใช้เรนเดอร์ที่เกี่ยวข้อง หรือสลับไปยังการแสดงผลทางเลือกด้าน Core (เช่น PolygonHoleSplit ซึ่งแบ่งเป็นวงแหวนง่ายๆ ที่ไม่มีรู)
// รูปแบบเดียวกับ Kotlin และ Swift สืบทอดจากเรนเดอเรอร์นามธรรมแล้วเติมสามการทำงาน
export class MapLibrePolygonOverlayRenderer extends AbstractPolygonOverlayRenderer<
MapLibreMapViewHolder,
MapLibreActualPolygon
> {
async createPolygon(state: PolygonState) { /* สร้างฟีเจอร์ GeoJSON */ }
async updatePolygonProperties({ current, prev }) { /* นำเฉพาะส่วนต่างไปใช้ */ }
async removePolygon(entity: PolygonEntity<MapLibreActualPolygon>) { /* เอาออก */ }
// หลังนำส่วนต่างไปใช้ ให้เขียนซอร์สใหม่จากเอนทิตีที่เหลืออยู่
override async onPostProcess() { this.layer.draw(this.polygonManager.allEntities()); }
}
// ประกาศสิ่งที่รองรับได้ด้วยการ implements อินเทอร์เฟซ Capable
export class MapLibreViewController extends BaseMapViewController
implements MapViewControllerInterface, MarkerCapable, PolygonCapable, /* … */ { }ใน TypeScript เช่นกัน การสร้าง อัปเดต และลบเท่านั้นที่เป็นเฉพาะของผู้ให้บริการ ใน SDK ที่วาดเลเยอร์แบบรวดเดียวเหมือน MapLibre ความแตกต่างในวิธีวาด เช่น การเขียน Source ทั้งหมดใหม่ใน onPostProcess แทนการลบทีละรายการ จะถูกปิดไว้ภายใน renderer องค์ประกอบที่ driver จัดการได้จะถูกแสดงด้วยอินเทอร์เฟซ Capable ที่ MapLibreViewController นำไปใช้ (เช่น PolygonCapable) และสามารถใช้งานได้หรือไม่สามารถรู้ได้จากประเภท
07 · ขอบเขตของการนามธรรมและทางออกฉุกเฉิน
MapConductor ไม่ได้รวมฟังก์ชันทั้งหมดของแต่ละ SDK แผนที่ มุ่งเน้นการดำเนินการที่ใช้ร่วมกันบ่อย และสำหรับความต้องการที่เกินกว่านั้น จะมีทางออกสองทางที่เตรียมไว้
// map ของอินเทอร์เฟซร่วมเป็น unknown ให้แคบลงเป็นชนิดของผู้ให้บริการก่อนใช้
const holder = mapViewState.getMapViewHolder();
const map = holder?.map as maplibregl.Map | undefined;
map?.addLayer({ id: 'buildings', type: 'fill-extrusion', source: 'composite' });การออกแบบที่ทำให้ API ทั่วไปยังคงเรียบง่าย และในขณะเดียวกันก็ไม่สูญเสียข้อดีเฉพาะของผู้ให้บริการแต่ละราย สำหรับรายละเอียด โปรดดู "Native Extensions"