Docs / 확장 레이어 / GeoJSON Layer

GeoJSON 레이어

GeoJSON을 그대로 지도에 겹치기 위한 확장 패키지입니다. 피처를 타일로 래스터화해 그리기 때문에, 수만 건 규모여도 1피처 1오브젝트를 만들지 않아도 됩니다. Android・iOS・React에서 API 이름・스타일 기본값・히트 테스트의 거동을 맞춰 놓았습니다.

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

피처를 512px 타일로 래스터화해, 래스터 레이어로 지도에 얹습니다. 프로바이더의 벡터 기능에 의존하지 않습니다.

히트 테스트

03

렌더링에 쓴 것과 같은 좌표로 클릭 판정합니다. 구멍 뚫린 폴리곤・멀티파트・지오메트리 컬렉션에 대응.

Point
MultiPoint
LineString
MultiLineString
Polygon
MultiPolygon
GeometryCollection

모든 지오메트리 타입을 3개 플랫폼에서 동일하게 지원합니다.

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 · 스타일의 정하는 법

스타일은 3층으로 해결됩니다

「레이어 전체의 기본값」「피처마다의 덮어쓰기」「StyleProvider에 의한 동적인 결정」의 3층입니다. 아래 층일수록 강하고, 지정하지 않은 항목은 위 층에서 이어받습니다. 우선 레이어 기본값만으로 시작하고, 필요해진 만큼만 아래 층을 더해 가는 것이 기본적인 진행 방식입니다.

그림 · 스타일의 결정 순서
LAYER 1
레이어 기본값
GeoJSONLayerState
모든 피처에 적용되는 토대: strokeColor / fillColor / strokeWidth / pointRadius.
LAYER 2
피처 단위 덮어쓰기
GeoJSONFeature.strokeColor …
개별 피처가 가진 같은 이름의 필드. null(nil)이면 기본값이 그대로 쓰입니다.
LAYER 3
동적 결정
GeoJSONStyleProvider
피처 1건마다 호출되어 최종 스타일을 반환합니다. properties를 보고 색을 나누는 일은 여기서 합니다.
StyleProvider를 지정하지 않으면 DefaultGeoJSONStyleProvider가 쓰여, 「피처의 값이 있으면 그것, 없으면 레이어 기본값」이라는 동작이 됩니다. 즉 3층은 1층과 2층의 관계 자체를 갈아 끼우는 구조입니다.

3-1. 스타일 프로퍼티

다루는 프로퍼티는 4개뿐입니다. 더해서 레이어 쪽에는 표시 제어의 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,
    )
}

// 상태는 observable. 나중에 대입하면 타일이 다시 생성됩니다
layerState.fillColor = Color.argb(90, 214, 64, 69)

색의 지정 형식

Android와 React는 ARGB의 32비트 정수(알파가 최상위 바이트), iOS는 UIColor로 알파를 색 자신에게 가지게 합니다. React에는 colorArgb(a,r,g,b) / colorRgb(r,g,b) / argbToCss()의 헬퍼가 있고, Android의 Color.argb()와 같은 순서입니다. 3개 플랫폼의 기본색은 모두 #1E88E5(선은 불투명, 칠은 알파 128)로 맞춰 놓았습니다.

3-3. 피처마다 덮어쓴다

피처는 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)

3-4. StyleProvider로 동적으로 정한다

properties의 값으로 색을 나누고 싶다, 선택 중인 피처만 강조하고 싶다, 임계값을 UI에서 바꾸고 싶다 ── 같은 「규칙으로 정해지는 스타일」은 StyleProvider에 씁니다. 피처 1건마다 호출되고, 레이어 기본값을 받아 최종적인 스타일을 반환합니다.

넘겨받는 것

피처 본체(properties 포함)와, 그 시점의 레이어 기본값. 기본값을 copy 해서 일부만 바꾸는 것이 정석입니다.

반환하는 것

4항목 모두가 채워진 LayerStyle. 건드리지 않는 항목은 기본값을 그대로 반환하면, 제1층의 설정이 살아납니다.

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를 교체하거나, 참조하고 있는 상태가 바뀌었을 때는, 모든 피처의 스타일이 재평가되고 타일이 다시 만들어집니다. 1건마다 호출되므로, 무거운 처리(정규식・네트워크・날짜 파싱 등)는 provider의 밖에서 미리 계산해 두세요.

3-5. 어느 것을 쓸까

방법
맞는 장면
비고
LayerState
데이터 전체를 하나의 겉모습으로 그린다.
가장 가볍다. 우선 여기서부터.
Feature override
읽어 들일 때 스타일이 확정되어 있고, 이후 바뀌지 않는다.
파싱 결과를 map 하기만 하면 된다. provider 호출의 비용이 없다.
StyleProvider
규칙으로 스타일이 정해진다. 규칙 자체가 실행 중에 바뀐다.
로직을 한 군데에 모을 수 있다. RN에서는 네이티브 등록이 필요.

04 · 탭 판정

MapConductor의 클릭 리스너는 하나밖에 없기 때문에, 레이어로의 전송은 앱 쪽에서 합니다. 의도적으로 자동 등록하지 않습니다. processClick은, 피처에 맞았을 때만 true를 반환합니다.

Kotlin
val layerState = remember {
    GeoJSONLayerState(
        onClick = { feature, position -> selected = feature },
    )
}

MapLibreMapView(
    state = mapViewState,
    onMapClick = { point ->
        // 15px 상당의 허용 범위로 판정(줌에 따라 달라짐)
        val consumed = layerState.processClick(point, 15.0, mapViewState.zoom)
        if (!consumed) selected = null
    },
) {
    GeoJSONLayer(state = layerState, features = features)
}

픽셀 허용 범위

processClick에 허용 픽셀과 현재의 줌을 넘기면, 줌에 따라가는 판정이 됩니다. 생략 시에는 세계 좌표의 기본 허용값(약 0.0002°)입니다.

겹쳤을 때

마지막에 렌더링된(=가장 위의) 피처가 반환됩니다.

대응 지오메트리

포인트・라인・구멍 뚫린 폴리곤・멀티파트・지오메트리 컬렉션.

05 · 데이터 양과 읽어 들이기

큰 데이터에서는 스트리밍 파서를 쓰고, 백그라운드에서 파싱하세요. 피처를 가지는 방식도 2가지 있습니다.

정적・대량 대상

GeoJSONFeature

불변의 데이터 객체. 수만 건이어도 상태 객체를 만들지 않기 때문에 가볍습니다. 큰 GeoJSON은 이쪽.

소수・자주 바뀌는 것 대상

GeoJSONFeatureState

1건씩 리액티브하게 갱신할 수 있습니다. 건수가 많으면 상태 관리의 비용이 커지므로, 필요한 만큼만.

Kotlin · streaming
// 큰 FeatureCollection은 parseStream
val features = withContext(Dispatchers.IO) {
    GeoJSONParser.parseStream(input)
}

// RFC 8142의 GeoJSON Text Sequences
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 · 현재의 제한

레이어로의 클릭 리스너 자동 등록은 하지 않습니다. processClick에 전송해 주세요(의도적인 설계입니다).
히트 테스트의 선・점의 기본 허용값은 내부 상수입니다. 호출마다 픽셀 허용값을 넘김으로써 조정할 수 있습니다.
래스터 타일로 렌더링하기 때문에, 지도 SDK 네이티브의 벡터 피처 쿼리는 쓰지 않습니다.
파서는 부정한 입력에 대해 예외를 던지지 않고, 빈 피처 배열을 반환합니다(iOS).

관련 페이지