GeoJSON レイヤー
GeoJSON をそのまま地図に重ねるための拡張パッケージです。フィーチャをタイルにラスタライズして描画するため、数万件規模でも1フィーチャ1オブジェクトを作らずに済みます。Android・iOS・React で API 名・スタイル既定値・ヒットテストの挙動を揃えてあります。
01 · 概要
GeoJSON をパースして軽量なフィーチャモデルに変換し、MapConductor のラスタタイルパイプラインを通して描画します。プロバイダ(Google Maps・MapLibre・MapKit・HERE など)が何であっても、同じコード・同じ見た目になります。
パース
タイル描画
ヒットテスト
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-layer \
@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>
);03 · スタイルの決め方
「レイヤー全体の既定値」「フィーチャごとの上書き」「StyleProvider による動的な決定」の3層です。下の層ほど強く、指定しなかった項目は上の層から引き継がれます。まずレイヤー既定値だけで始め、必要になった分だけ下の層を足していくのが基本の進め方です。
扱うプロパティは4つだけです。加えてレイヤー側には表示制御の opacity・visible・minZoom / maxZoom があります。
まずここから始めます。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,
)
}
// 状態は observable。あとから代入すればタイルが再生成されます
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-layer';
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、CSS に落とすなら argbToCss
const legend = argbToCss(colorRgb(30, 136, 229)); // "rgba(30,136,229,1.0000)"フィーチャは 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} />properties の値で色分けしたい、選択中のフィーチャだけ強調したい、しきい値をUIから変えたい ── といった「ルールで決まるスタイル」は StyleProvider に書きます。フィーチャ1件ごとに呼ばれ、レイヤー既定値を受け取って最終的なスタイルを返します。
// fun interface なのでラムダ1つで書けます
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()// Web では 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) })),
[],
);04 · タップ判定
MapConductor のクリックリスナーは1つしか無いため、レイヤーへの転送はアプリ側で行います。意図的に自動登録していません。processClick は、フィーチャに当たったときだけ true を返します。
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)
}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);
};05 · データ量と読み込み
大きなデータではストリーミングパーサを使い、バックグラウンドでパースしてください。フィーチャの持ち方も2通りあります。
// 大きな 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) }let features = GeoJSONParser.parse(fileURL: fileURL)
GeoJSONSeqParser.streamParse(fileURL: fileURL) { feature in
// 1件ずつ追記・バッチ・永続化
}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 は GeoJSONFeature(不変)を返す。数万件でも状態オブジェクトを作らない
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())
}