เลเยอร์ GeoJSON
แพ็กเกจส่วนขยายสำหรับวาง GeoJSON ซ้อนทับบนแผนที่โดยตรง เนื่องจากฟีเจอร์จะถูกแรสเตอร์ลงในไทล์และวาด จึงไม่ต้องสร้างออบเจ็กต์หนึ่งต่อฟีเจอร์แม้จะมีจำนวนหลายหมื่นรายการ ชื่อ API, ค่าเริ่มต้นของสไตล์ และพฤติกรรมการทดสอบการชนจะเหมือนกันใน Android, iOS และ React
01 · ภาพรวม
GeoJSON จะถูกแจ้งวิเคราะห์และแปลงเป็นโมเดลฟีเจอร์ที่เบา จากนั้นวาดผ่านไปป์ไลน์ไทล์แรสเตอร์ของ MapConductor ไม่ว่าผู้ให้บริการจะเป็นผู้ใด (Google Maps, MapLibre, MapKit, HERE ฯลฯ) โค้ดและรูปลักษณ์จะเหมือนกัน
มุมมอง
รองรับ FeatureCollection, Feature เดี่ยว, เรขาคณิตดิบ และลำดับข้อความ RFC 8142 มีตัวแยกวิเคราะห์สตรีมด้วย
การวาดไทล์
แรสเตอร์ Feature เป็นไทล์ 512px และวางบนแผนที่เป็นเลเยอร์แรสเตอร์ ไม่ขึ้นอยู่กับฟังก์ชันเวกเตอร์ของผู้ให้บริการ
การทดสอบการชน
ตรวจจับการคลิกด้วยพิกัดเดียวกับที่ใช้วาด รองรับรูปหลายเหลี่ยมที่มีรู, เรขาคณิตหลายส่วน และคอลเลกชันเรขาคณิต
รองรับประเภทเรขาคณิตทั้งหมดเหมือนกันในทั้งสามแพลตฟอร์ม
02 · การใช้งานพื้นฐาน
วางเลเยอร์ไว้ในขอบเขต content ของมุมมองแผนที่เท่านั้น การแจ้งวิเคราะห์จะดำเนินการในเบื้องหลังและส่งผลลัพธ์ไปที่ features
dependencies {
implementation("com.mapconductor:geojson:<version>")
}val mapViewState = rememberMapLibreMapViewState(
cameraPosition = MapCameraPosition(
position = GeoPoint.fromLongLat(139.7671, 35.6812),
zoom = 12.0,
),
)
val layerState = remember { GeoJSONLayerState() }
var features by remember { mutableStateOf(emptyList<GeoJSONFeature>()) }
LaunchedEffect(Unit) {
features = withContext(Dispatchers.IO) {
assets.open("wards.geojson").use(GeoJSONParser::parseStream)
}
}
MapLibreMapView(state = mapViewState) {
GeoJSONLayer(state = layerState, features = features)
}.package(url: "https://github.com/MapConductor/ios-geojson-layer", from: "<version>") // iOS 15+ / depends on MapConductorCore
@StateObject private var mapViewState = MapKitViewState(
cameraPosition: MapCameraPosition(
position: GeoPoint.fromLongLat(longitude: 139.7671, latitude: 35.6812),
zoom: 12.0
)
)
@StateObject private var layerState = GeoJSONLayerState()
@State private var features: [GeoJSONFeature] = []
var body: some View {
MapKitMapView(state: mapViewState) {
GeoJSONLayer(state: layerState, features: features)
}
.task {
features = GeoJSONParser.parse(fileURL: wardsURL)
}
}npm install @mapconductor/react-geojson \
@mapconductor/js-sdk-core @mapconductor/js-sdk-reactconst state = useMapLibreViewState({
mapDesignType: MapLibreDesign.OsmBrightJa,
cameraPosition: createMapCameraPosition({
position: createGeoPoint({ latitude: 35.6812, longitude: 139.7671 }),
zoom: 12,
}),
});
const layerState = useMemo(() => new GeoJSONLayerState(), []);
const features = useMemo(() => GeoJSONParser.parseFeatures(GEOJSON), []);
return (
<MapLibreMapView2D state={state}>
<GeoJSONLayer state={layerState} features={features} />
</MapLibreMapView2D>
);เมื่อเนื้อหาหรือสไตล์ของฟีเจอร์เปลี่ยนแปลง เลเยอร์จะทำให้ URL ของไทล์ภายในใช้ไม่ได้ แคชแรสเตอร์ของแผนที่ SDK จะไม่คืนภาพเก่าอีกต่อไป
03 · การกำหนดสไตล์
สไตล์ได้รับการแก้ไขในสามเลเยอร์
สามเลเยอร์: 'ค่าเริ่มต้นสำหรับเลเยอร์ทั้งหมด', 'การแทนที่ต่อฟีเจอร์' และ 'การกำหนดแบบไดนามิกโดย StyleProvider' เลเยอร์ที่ต่ำกว่ามีความสำคัญมากกว่า รายการที่ไม่ได้ระบุจะถูกสืบทอดจากเลเยอร์ที่สูงกว่า ขั้นตอนพื้นฐานคือเริ่มต้นด้วยค่าเริ่มต้นของเลเยอร์เท่านั้น และเพิ่มเลเยอร์ที่ต่ำกว่าตามที่จำเป็น
3-1. คุณสมบัติของสไตล์
มีเพียงสี่คุณสมบัติที่จัดการ นอกจากนี้ทางด้านเลเยอร์มี opacity, visible และ minZoom / maxZoom สำหรับควบคุมการแสดงผล
3-2. กำหนดค่าเริ่มต้นของเลเยอร์
เริ่มต้นที่นี่ก่อน ค่าที่ส่งผ่านไปยัง `GeoJSONLayerState` จะเป็นพื้นฐานสำหรับฟีเจอร์ทั้งหมด เนื่องจากสถานะเป็น observable จึงจะถูกวาดใหม่หากมีการกำหนดค่าในภายหลัง
val layerState = remember {
GeoJSONLayerState(
strokeColor = Color.argb(220, 30, 136, 229),
fillColor = Color.argb(60, 30, 136, 229),
strokeWidth = 1.5f,
pointRadius = 8f,
opacity = 1f,
minZoom = 8, maxZoom = 22,
)
}
// สถานะสังเกตการณ์ได้ กำหนดค่าภายหลังแล้วไทล์จะถูกสร้างใหม่
layerState.fillColor = Color.argb(90, 214, 64, 69)@StateObject private var layerState = GeoJSONLayerState(
opacity: 1.0,
layerStyle: GeoJSONTileRenderer.LayerStyle(
strokeColor: UIColor(red: 30/255, green: 136/255, blue: 229/255, alpha: 0.86),
fillColor: UIColor(red: 30/255, green: 136/255, blue: 229/255, alpha: 0.24),
strokeWidth: 1.5,
pointRadius: 8.0
)
)
// ฟิลด์ของ LayerStyle เป็น let จึงต้องเปลี่ยนทั้งโครงสร้าง (alpha อยู่ฝั่ง UIColor)
layerState.layerStyle = GeoJSONTileRenderer.LayerStyle(
strokeColor: layerState.layerStyle.strokeColor,
fillColor: UIColor.systemRed.withAlphaComponent(0.35),
strokeWidth: layerState.layerStyle.strokeWidth,
pointRadius: layerState.layerStyle.pointRadius
)import { colorArgb, colorRgb, argbToCss } from '@mapconductor/react-geojson';
const layerState = useMemo(() => new GeoJSONLayerState({
strokeColor: colorArgb(220, 30, 136, 229),
fillColor: colorArgb(60, 30, 136, 229),
strokeWidth: 1.5,
pointRadius: 8,
opacity: 1,
minZoom: 8, maxZoom: 22,
}), []);
// ใช้ colorRgb สำหรับสีทึบ และ argbToCss เมื่อแปลงเป็น CSS
const legend = argbToCss(colorRgb(30, 136, 229)); // "rgba(30,136,229,1.0000)"รูปแบบการระบุสี
Android และ React ใช้จำนวนเต็ม 32 บิตแบบ ARGB (อัลฟาเป็นไบต์สูงสุด) iOS ใช้ `UIColor` โดยมีอัลฟาอยู่ในสีตัวเอง React มีฟังก์ชันช่วย `colorArgb(a,r,g,b)` / `colorRgb(r,g,b)` / `argbToCss()` ในลำดับเดียวกับ `Color.argb()` ของ Android สีเริ่มต้นของทั้ง 3 แพลตฟอร์มตั้งไว้ที่ #1E88E5 (เส้นไม่โปร่งแสง การเติมสีมีอัลฟา 128) เหมือนกันทั้งหมด
แทนที่สำหรับแต่ละฟีเจอร์
ฟีเจอร์สามารถมี `strokeColor` / `fillColor` / `strokeWidth` / `pointRadius` / `visible` เองได้ หากยังคงเป็น `null` จะใช้ค่าเริ่มต้น หากมีค่าอยู่ ค่านั้นจะมีผล หากสไตล์ถูกกำหนดเมื่อโหลดข้อมูล (และไม่เปลี่ยนแปลงภายหลัง) วิธีนี้คือวิธีที่ตรงไปตรงมาและเร็วที่สุด
val parsed = GeoJSONParser.parseStream(input)
// หลังแจงข้อมูล ให้ดูคุณสมบัติแล้วฝังสไตล์ลงไป
val styled = parsed.map { f ->
when (f.properties["status"]) {
"alert" -> f.copy(fillColor = Color.argb(120, 214, 64, 69), strokeWidth = 3f)
"closed" -> f.copy(visible = false)
else -> f // ปล่อยเป็น null จึงใช้ค่าเริ่มต้นของเลเยอร์
}
}
GeoJSONLayer(state = layerState, features = styled)let parsed = GeoJSONParser.parse(data: data)
let styled = parsed.map { f -> GeoJSONFeature in
switch f.properties["status"] as? String {
case "alert":
var copy = f
copy.fillColor = UIColor.systemRed.withAlphaComponent(0.47)
copy.strokeWidth = 3.0
return copy
case "closed":
var copy = f
copy.visible = false
return copy
default:
return f // หากปล่อยเป็น nil จะใช้ค่าเริ่มต้นของเลเยอร์
}
}const styled = useMemo(() =>
GeoJSONParser.parseFeatures(GEOJSON).map(f => {
const status = f.properties.status;
if (status === 'alert') {
return { ...f, fillColor: colorArgb(120, 214, 64, 69), strokeWidth: 3 };
}
if (status === 'closed') return { ...f, visible: false };
return f; // หากปล่อยเป็น null จะใช้ค่าเริ่มต้นของเลเยอร์
}), []);
<GeoJSONLayer state={layerState} features={styled} />กำหนดแบบไดนามิกด้วย StyleProvider
สไตล์ที่กำหนดโดย "กฎ" เช่น การใช้สีตามค่าของ `properties` การเน้นเฉพาะฟีเจอร์ที่เลือก การเปลี่ยนค่าเกณฑ์จาก UI ฯลฯ จะถูกเขียนไว้ใน `StyleProvider` จะถูกเรียกสำหรับแต่ละฟีเจอร์ รับค่าเริ่มต้นของเลเยอร์ และส่งคืนสไตล์สุดท้าย
สิ่งที่ส่งผ่าน
Feature ตัวเอง (รวม properties) และค่าเริ่มต้นของเลเยอร์ ณ จุดนั้น แนวทางปฏิบัติทั่วไปคือ copy ค่าเริ่มต้นแล้วเปลี่ยนเฉพาะบางส่วน
สิ่งที่ส่งคืน
LayerStyle ที่กรอกข้อมูลทั้ง 4 รายการ หากไม่แตะต้องรายการใด ให้คืนค่าเริ่มต้นตามเดิม การตั้งค่าเลเยอร์ระดับแรกจะยังคงใช้อยู่
// เป็น fun interface จึงเขียนด้วยแลมบ์ดาเดียวได้
val densityStyle = GeoJSONStyleProviderInterface { feature, defaultStyle ->
val pop = (feature.properties["population"] as? Number)?.toInt() ?: 0
val fill = when {
pop > 500_000 -> Color.argb(150, 173, 20, 87)
pop > 200_000 -> Color.argb(120, 244, 143, 177)
else -> Color.argb(80, 248, 187, 208)
}
defaultStyle.copy(fillColor = fill) // สิ่งที่ไม่ได้แตะต้องจะคงค่าเริ่มต้นไว้
}
val layerState = remember {
GeoJSONLayerState(styleProvider = densityStyle)
}
// หากเปลี่ยนภายหลัง ฟีเจอร์ทั้งหมดจะถูกประเมินใหม่
layerState.styleProvider = DefaultGeoJSONStyleProviderfinal class DensityStyleProvider: GeoJSONStyleProvider {
func style(
for feature: GeoJSONFeature,
defaultStyle: GeoJSONTileRenderer.LayerStyle
) -> GeoJSONTileRenderer.LayerStyle {
let pop = (feature.properties["population"] as? Int) ?? 0
let fill: UIColor =
pop > 500_000 ? UIColor(red: 173/255, green: 20/255, blue: 87/255, alpha: 0.59)
: pop > 200_000 ? UIColor(red: 244/255, green: 143/255, blue: 177/255, alpha: 0.47)
: UIColor(red: 248/255, green: 187/255, blue: 208/255, alpha: 0.31)
return GeoJSONTileRenderer.LayerStyle(
strokeColor: defaultStyle.strokeColor,
fillColor: fill,
strokeWidth: defaultStyle.strokeWidth,
pointRadius: defaultStyle.pointRadius
)
}
}
layerState.styleProvider = DensityStyleProvider()// บนเว็บ แทนที่จะใช้ StyleProvider ให้ map ผลลัพธ์การแจงข้อมูลแล้ว
// กำหนดสไตล์ให้แต่ละฟีเจอร์ (ผลลัพธ์เหมือนกัน)
const styleOf = (props: Record<string, unknown>) => {
const pop = Number(props.population ?? 0);
if (pop > 500_000) return colorArgb(150, 173, 20, 87);
if (pop > 200_000) return colorArgb(120, 244, 143, 177);
return colorArgb(80, 248, 187, 208);
};
const features = useMemo(
() => GeoJSONParser.parseFeatures(GEOJSON)
.map(f => ({ ...f, fillColor: styleOf(f.properties) })),
[],
);เมื่อสลับ `StyleProvider` หรือสถานะที่อ้างอิงถึงมีการเปลี่ยนแปลง สไตล์ของฟีเจอร์ทั้งหมดจะถูกประเมินใหม่และสร้างไทล์ใหม่ เนื่องจากมีการเรียกใช้สำหรับแต่ละฟีเจอร์ โปรดคำนวณล่วงหน้านอก provider สำหรับกระบวนการที่หนัก (นิพจน์ทั่วไป เครือข่าย การวิเคราะห์วันที่ ฯลฯ)
3-5. ควรใช้ตัวไหน
04 · การตรวจจับแตะ
เนื่องจาก `MapConductor` มี click-listener เพียงตัวเดียว การส่งต่อไปยังเลเยอร์จึงดำเนินการโดยแอป เราไม่ได้ลงทะเบียนโดยอัตโนมัติโดยเจตนา `processClick` จะคืนค่า `true` เฉพาะเมื่อโดนฟีเจอร์เท่านั้น
val layerState = remember {
GeoJSONLayerState(
onClick = { feature, position -> selected = feature },
)
}
MapLibreMapView(
state = mapViewState,
onMapClick = { point ->
// ตรวจด้วยระยะยอมรับเทียบเท่า 15 พิกเซล ซึ่งเปลี่ยนตามซูม
val consumed = layerState.processClick(point, 15.0, mapViewState.zoom)
if (!consumed) selected = null
},
) {
GeoJSONLayer(state = layerState, features = features)
}layerState.onClick = { feature, position in
selected = feature
}
MapKitMapView(
state: mapViewState,
onMapClick: { point in
selected = nil
layerState.processClick(geoPoint: point)
}
) {
GeoJSONLayer(state: layerState, features: features)
}const layerState = useMemo(() => new GeoJSONLayerState({
onClick: (feature, position) => setSelected(feature),
}), []);
// ส่งต่อจากตัวจัดการคลิกของแผนที่
const handleMapClick = (point: GeoPointInterface) => {
const consumed = layerState.processClick(point, 10, state.camera.zoom);
if (!consumed) setSelected(null);
};ค่าความคลาดเคลื่อนของพิกเซล
หากส่งค่าความคลาดเคลื่อนพิกเซลและการซูมปัจจุบันไปยัง processClick การตรวจจับจะปรับตามการซูม หากไม่ระบุ จะใช้ค่าความคลาดเคลื่อนเริ่มต้นของพิกัดโลก (ประมาณ 0.0002°)
เมื่อทับซ้อน
จะส่งคืน Feature ที่วาดล่าสุด (นั่นคืออยู่บนสุด)
เรขาคณิตที่รองรับ
จุด, เส้น, รูปหลายเหลี่ยมที่มีรู, คอลเลกชันเรขาคณิตหลายส่วน
05 · ปริมาณข้อมูลและการโหลด
สำหรับข้อมูลขนาดใหญ่ ให้ใช้ตัวแจงนับแบบสตรีมและแจงนับในเบื้องหลัง มีสองวิธีในการจัดเก็บฟีเจอร์
สำหรับข้อมูลจำนวนมากแบบคงที่
ออบเจกต์ข้อมูลที่เปลี่ยนแปลงไม่ได้ มีน้ำหนักเบาเพราะไม่สร้างออบเจกต์สถานะแม้มีหลายหมื่นรายการ สำหรับ GeoJSON ขนาดใหญ่ ให้ใช้อันนี้
สำหรับจำนวนน้อยที่เปลี่ยนแปลงบ่อย
สามารถอัปเดตแบบรีแอคทีฟทีละรายการได้ หากจำนวนมาก ต้นทุนการจัดการสถานะจะส่งผล ดังนั้นให้ใช้เฉพาะที่จำเป็นเท่านั้น
// ใช้ parseStream กับ FeatureCollection ขนาดใหญ่
val features = withContext(Dispatchers.IO) {
GeoJSONParser.parseStream(input)
}
// GeoJSON Text Sequences ตาม RFC 8142
val seq = withContext(Dispatchers.IO) { GeoJSONSeqParser.parse(file) }
GeoJSONSeqParser.streamParse(file) { feature -> buffer.add(feature) }let features = GeoJSONParser.parse(fileURL: fileURL)
GeoJSONSeqParser.streamParse(fileURL: fileURL) { feature in
// ต่อทีละรายการ เป็นชุด หรือเก็บถาวร
}const features = GeoJSONParser.parseFeatures(text);
const seq = GeoJSONSeqParser.parse(text); // ลำดับที่คั่นด้วยบรรทัด
// tileSize ค่าเริ่มต้นคือ 512 และเพิ่มให้เทียบเท่า Retina ได้
<GeoJSONLayer state={layerState} features={features} tileSize={512} />val layerState = remember { GeoJSONLayerState() }
var features by remember { mutableStateOf(emptyList<GeoJSONFeature>()) }
var loading by remember { mutableStateOf(true) }
// parseStream returns immutable GeoJSONFeature values — no state object per feature
LaunchedEffect(Unit) {
features = withContext(Dispatchers.IO) {
context.assets.open("tokyo-buildings.geojson")
.use(GeoJSONParser::parseStream)
}
loading = false
}
MapLibreMapView(state = mapViewState) {
GeoJSONLayer(state = layerState, features = features)
}
if (loading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}