เอกสาร / เลเยอร์ส่วนขยาย / เลเยอร์ GeoJSON

เลเยอร์ GeoJSON

แพ็กเกจส่วนขยายสำหรับวาง GeoJSON ซ้อนทับบนแผนที่โดยตรง เนื่องจากฟีเจอร์จะถูกแรสเตอร์ลงในไทล์และวาด จึงไม่ต้องสร้างออบเจ็กต์หนึ่งต่อฟีเจอร์แม้จะมีจำนวนหลายหมื่นรายการ ชื่อ API, ค่าเริ่มต้นของสไตล์ และพฤติกรรมการทดสอบการชนจะเหมือนกันใน Android, iOS และ React

ANDROID
com.mapconductor:geojson
IOS
MapConductorGeoJSON
REACT
@mapconductor/react-geojson-layer
วิธีการวาด
ไทล์แรสเตอร์
แพลตฟอร์ม

01 · ภาพรวม

GeoJSON จะถูกแจ้งวิเคราะห์และแปลงเป็นโมเดลฟีเจอร์ที่เบา จากนั้นวาดผ่านไปป์ไลน์ไทล์แรสเตอร์ของ MapConductor ไม่ว่าผู้ให้บริการจะเป็นผู้ใด (Google Maps, MapLibre, MapKit, HERE ฯลฯ) โค้ดและรูปลักษณ์จะเหมือนกัน

มุมมอง

01

รองรับ FeatureCollection, Feature เดี่ยว, เรขาคณิตดิบ และลำดับข้อความ RFC 8142 มีตัวแยกวิเคราะห์สตรีมด้วย

การวาดไทล์

02

แรสเตอร์ Feature เป็นไทล์ 512px และวางบนแผนที่เป็นเลเยอร์แรสเตอร์ ไม่ขึ้นอยู่กับฟังก์ชันเวกเตอร์ของผู้ให้บริการ

การทดสอบการชน

03

ตรวจจับการคลิกด้วยพิกัดเดียวกับที่ใช้วาด รองรับรูปหลายเหลี่ยมที่มีรู, เรขาคณิตหลายส่วน และคอลเลกชันเรขาคณิต

Point
MultiPoint
LineString
MultiLineString
Polygon
MultiPolygon
GeometryCollection

รองรับประเภทเรขาคณิตทั้งหมดเหมือนกันในทั้งสามแพลตฟอร์ม

02 · การใช้งานพื้นฐาน

วางเลเยอร์ไว้ในขอบเขต content ของมุมมองแผนที่เท่านั้น การแจ้งวิเคราะห์จะดำเนินการในเบื้องหลังและส่งผลลัพธ์ไปที่ features

build.gradle.kts
dependencies {
    implementation("com.mapconductor:geojson:<version>")
}
com.mapconductor.geojson · Compose
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)
}

เมื่อเนื้อหาหรือสไตล์ของฟีเจอร์เปลี่ยนแปลง เลเยอร์จะทำให้ URL ของไทล์ภายในใช้ไม่ได้ แคชแรสเตอร์ของแผนที่ SDK จะไม่คืนภาพเก่าอีกต่อไป

03 · การกำหนดสไตล์

สไตล์ได้รับการแก้ไขในสามเลเยอร์

สามเลเยอร์: 'ค่าเริ่มต้นสำหรับเลเยอร์ทั้งหมด', 'การแทนที่ต่อฟีเจอร์' และ 'การกำหนดแบบไดนามิกโดย StyleProvider' เลเยอร์ที่ต่ำกว่ามีความสำคัญมากกว่า รายการที่ไม่ได้ระบุจะถูกสืบทอดจากเลเยอร์ที่สูงกว่า ขั้นตอนพื้นฐานคือเริ่มต้นด้วยค่าเริ่มต้นของเลเยอร์เท่านั้น และเพิ่มเลเยอร์ที่ต่ำกว่าตามที่จำเป็น

ภาพ · ลำดับการตัดสินสไตล์
LAYER 1
ค่าเริ่มต้นของเลเยอร์
GeoJSONLayerState
ค่าพื้นฐานที่ใช้กับทุกฟีเจอร์ ได้แก่ strokeColor, fillColor, strokeWidth, pointRadius
LAYER 2
การกำหนดทับรายฟีเจอร์
GeoJSONFeature.strokeColor …
ฟิลด์ชื่อเดียวกันบนแต่ละฟีเจอร์ หากปล่อยเป็น null จะใช้ค่าเริ่มต้นของเลเยอร์ตามเดิม
LAYER 3
การตัดสินแบบไดนามิก
GeoJSONStyleProvider
ถูกเรียกหนึ่งครั้งต่อฟีเจอร์เพื่อคืนสไตล์สุดท้าย การไล่สีตาม properties อยู่ตรงนี้
หากไม่ได้กำหนด provider ระบบจะใช้ DefaultGeoJSONStyleProvider ซึ่งใช้ค่าของฟีเจอร์ถ้ามี ถ้าไม่มีก็ใช้ค่าเริ่มต้นของเลเยอร์ ชั้นที่สามจึงเป็นวิธีแทนที่ความสัมพันธ์นั้นทั้งชุด

3-1. คุณสมบัติของสไตล์

มีเพียงสี่คุณสมบัติที่จัดการ นอกจากนี้ทางด้านเลเยอร์มี opacity, visible และ minZoom / maxZoom สำหรับควบคุมการแสดงผล

คุณสมบัติ
ประเภท
ค่าเริ่มต้น
คำอธิบาย
strokeColor
ARGB Int / UIColor / number
#FF1E88E5
สีของเส้นและเส้นรอบรูปของโพลีกอน
fillColor
ARGB Int / UIColor / number
#801E88E5
สีเติมของโพลีกอนและสีวงกลมของจุด ค่าเริ่มต้นคือกึ่งโปร่งใส
strokeWidth
Float / CGFloat / number
2.0
ความกว้างของเส้น (พิกเซล) เนื่องจากถูกแรสเตอร์ลงในไทล์ ความกว้างจึงคงที่เมื่อซูม
pointRadius
Float / CGFloat / number
8.0
รัศมีของวงกลมที่ใช้วาดจุด (พิกเซล)
opacity
Float / Double / number
1.0
ความทึบแสงของเลเยอร์แรสเตอร์ทั้งหมด มีผลแยกจากอัลฟาของสีแต่ละสี
visible
Boolean
true
จะถูกยกเว้นทั้งจากการวาดและการทดสอบการชน ทางด้านฟีเจอร์มีฟิลด์ชื่อเดียวกันเช่นกัน
minZoom / maxZoom
Int / number
0 / 22
ช่วงซูมที่จะแสดงเลเยอร์แรสเตอร์ที่สร้างขึ้น นอกช่วงจะไม่มีการสร้างไทล์เช่นกัน

3-2. กำหนดค่าเริ่มต้นของเลเยอร์

เริ่มต้นที่นี่ก่อน ค่าที่ส่งผ่านไปยัง `GeoJSONLayerState` จะเป็นพื้นฐานสำหรับฟีเจอร์ทั้งหมด เนื่องจากสถานะเป็น observable จึงจะถูกวาดใหม่หากมีการกำหนดค่าในภายหลัง

Kotlin · ARGB Int
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)

รูปแบบการระบุสี

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` จะใช้ค่าเริ่มต้น หากมีค่าอยู่ ค่านั้นจะมีผล หากสไตล์ถูกกำหนดเมื่อโหลดข้อมูล (และไม่เปลี่ยนแปลงภายหลัง) วิธีนี้คือวิธีที่ตรงไปตรงมาและเร็วที่สุด

Kotlin · GeoJSONFeature.copy
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)

กำหนดแบบไดนามิกด้วย StyleProvider

สไตล์ที่กำหนดโดย "กฎ" เช่น การใช้สีตามค่าของ `properties` การเน้นเฉพาะฟีเจอร์ที่เลือก การเปลี่ยนค่าเกณฑ์จาก UI ฯลฯ จะถูกเขียนไว้ใน `StyleProvider` จะถูกเรียกสำหรับแต่ละฟีเจอร์ รับค่าเริ่มต้นของเลเยอร์ และส่งคืนสไตล์สุดท้าย

สิ่งที่ส่งผ่าน

Feature ตัวเอง (รวม properties) และค่าเริ่มต้นของเลเยอร์ ณ จุดนั้น แนวทางปฏิบัติทั่วไปคือ copy ค่าเริ่มต้นแล้วเปลี่ยนเฉพาะบางส่วน

สิ่งที่ส่งคืน

LayerStyle ที่กรอกข้อมูลทั้ง 4 รายการ หากไม่แตะต้องรายการใด ให้คืนค่าเริ่มต้นตามเดิม การตั้งค่าเลเยอร์ระดับแรกจะยังคงใช้อยู่

Kotlin · GeoJSONStyleProviderInterface
// เป็น 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 = DefaultGeoJSONStyleProvider

เมื่อสลับ `StyleProvider` หรือสถานะที่อ้างอิงถึงมีการเปลี่ยนแปลง สไตล์ของฟีเจอร์ทั้งหมดจะถูกประเมินใหม่และสร้างไทล์ใหม่ เนื่องจากมีการเรียกใช้สำหรับแต่ละฟีเจอร์ โปรดคำนวณล่วงหน้านอก provider สำหรับกระบวนการที่หนัก (นิพจน์ทั่วไป เครือข่าย การวิเคราะห์วันที่ ฯลฯ)

3-5. ควรใช้ตัวไหน

วิธีการ
สถานการณ์ที่เหมาะสม
หมายเหตุ
LayerState
วาดข้อมูลทั้งหมดด้วยรูปลักษณ์เดียว
เบาที่สุด เริ่มต้นที่นี่ก่อน
Feature override
สไตล์ถูกกำหนดเมื่อโหลดและไม่เปลี่ยนแปลงหลังจากนั้น
แค่แมปผลลัพธ์การแจงนับ ไม่มีค่าใช้จ่ายในการเรียก provider
StyleProvider
สไตล์ถูกกำหนดโดยกฎ กฎเองเปลี่ยนระหว่างการทำงาน
รวบรวมตรรกะไว้ในที่เดียว ใน RN จำเป็นต้องลงทะเบียนแบบเนทีฟ

04 · การตรวจจับแตะ

เนื่องจาก `MapConductor` มี click-listener เพียงตัวเดียว การส่งต่อไปยังเลเยอร์จึงดำเนินการโดยแอป เราไม่ได้ลงทะเบียนโดยอัตโนมัติโดยเจตนา `processClick` จะคืนค่า `true` เฉพาะเมื่อโดนฟีเจอร์เท่านั้น

Kotlin
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)
}

ค่าความคลาดเคลื่อนของพิกเซล

หากส่งค่าความคลาดเคลื่อนพิกเซลและการซูมปัจจุบันไปยัง processClick การตรวจจับจะปรับตามการซูม หากไม่ระบุ จะใช้ค่าความคลาดเคลื่อนเริ่มต้นของพิกัดโลก (ประมาณ 0.0002°)

เมื่อทับซ้อน

จะส่งคืน Feature ที่วาดล่าสุด (นั่นคืออยู่บนสุด)

เรขาคณิตที่รองรับ

จุด, เส้น, รูปหลายเหลี่ยมที่มีรู, คอลเลกชันเรขาคณิตหลายส่วน

05 · ปริมาณข้อมูลและการโหลด

สำหรับข้อมูลขนาดใหญ่ ให้ใช้ตัวแจงนับแบบสตรีมและแจงนับในเบื้องหลัง มีสองวิธีในการจัดเก็บฟีเจอร์

สำหรับข้อมูลจำนวนมากแบบคงที่

GeoJSONFeature

ออบเจกต์ข้อมูลที่เปลี่ยนแปลงไม่ได้ มีน้ำหนักเบาเพราะไม่สร้างออบเจกต์สถานะแม้มีหลายหมื่นรายการ สำหรับ GeoJSON ขนาดใหญ่ ให้ใช้อันนี้

สำหรับจำนวนน้อยที่เปลี่ยนแปลงบ่อย

GeoJSONFeatureState

สามารถอัปเดตแบบรีแอคทีฟทีละรายการได้ หากจำนวนมาก ต้นทุนการจัดการสถานะจะส่งผล ดังนั้นให้ใช้เฉพาะที่จำเป็นเท่านั้น

Kotlin · streaming
// ใช้ 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) }
การแสดงการใช้งานในวิดีโอ · Android + MapLibreKotlin · Jetpack Compose
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())
}
วิดีโอตัวอย่าง · โหลด GeoJSON ขนาดใหญ่เป็นไทล์
ยังไม่ได้ถ่ายวิดีโอไทล์ถูกโหลดตามลำดับเมื่อเลื่อนและการวาดจะค่อยๆ เต็มไปด้วยข้อมูล การที่การทำงานไม่หยุดแม้ปริมาณข้อมูลจะเพิ่มขึ้นนั้น ภาพนิ่งสื่อสื่อได้ไม่ชัดเจน

06 · ข้อจำกัดปัจจุบัน

ไม่มีการลงทะเบียน Click Listener ให้กับ Layer โดยอัตโนมัติ โปรดส่งต่อไปยัง `processClick` (เป็นการออกแบบโดยตั้งใจ)
ค่าความอดทนเริ่มต้นสำหรับเส้นและจุดในการทดสอบการชนคือค่าคงที่ภายใน สามารถปรับได้โดยส่งค่าความอดทนเป็นพิกเซลในแต่ละการเรียก
เนื่องจากวาดเป็น Raster Tiles จึงไม่ใช้การค้นหา Vector Feature แบบเนทีฟของ Map SDK
Parser จะไม่โยน Exception เมื่อข้อมูลนำเข้าไม่ถูกต้อง แต่จะส่งคืน Feature Array ที่ว่างเปล่า (iOS)

หน้าที่เกี่ยวข้อง