GeoJSON-Ebene
Erweiterungspaket, um GeoJSON direkt über eine Karte zu legen. Da Features in Kacheln gerastert und gezeichnet werden, müssen selbst bei Zehntausenden von Elementen keine Objekte pro Feature erstellt werden. API-Namen, Standardwerte für Stile und das Verhalten von Hit-Tests sind unter Android, iOS und React vereinheitlicht.
01 · Übersicht
GeoJSON wird geparst und in ein leichtgewichtiges Feature-Modell konvertiert, dann über die Rasterkachel-Pipeline von MapConductor gezeichnet. Egal welcher Anbieter (Google Maps, MapLibre, MapKit, HERE usw.), der Code und das Aussehen sind identisch.
Perspektive
FeatureCollection, einzelne Features, rohe Geometrien sowie Textfolgen nach RFC 8142 werden unterstützt. Es gibt auch einen Streaming-Parser.
Kacheln zeichnen
Features werden in 512px-Kacheln gerastert und als Rasterebene in die Karte eingefügt. Nicht abhängig von der Vektorfunktion des Anbieters.
Hit-Test
Klick-Erkennung mit denselben Koordinaten wie beim Zeichnen. Unterstützt Polygone mit Löchern, mehrteilige Geometrien und Geometriesammlungen.
Alle Geometrietypen werden auf den drei Plattformen identisch unterstützt.
02 · Grundlegende Verwendung
Platzieren Sie die Ebene einfach im content-Bereich der Kartenansicht. Das Parsen erfolgt im Hintergrund, das Ergebnis wird an features übergeben.
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>
);Wenn sich der Inhalt oder der Stil eines Features ändert, macht die Ebene die interne Kachel-URL ungültig. Das Raster-Cache des Karten-SDK gibt keine veralteten Bilder zurück.
03 · Bestimmung des Stils
Der Stil wird in drei Schichten aufgelöst
Drei Schichten: „Standardwerte für die gesamte Ebene“, „Überschreibungen pro Feature“ und „dynamische Festlegung durch StyleProvider“. Untere Schichten sind stärker; nicht angegebene Elemente werden von höheren Schichten geerbt. Grundlegendes Vorgehen: beginnen Sie nur mit den Ebenen-Standards und fügen Sie bei Bedarf schrittweise untere Schichten hinzu.
3-1. Styleigenschaften
Es werden nur vier Eigenschaften behandelt. Zusätzlich stehen auf Ebenenebene opacity, visible und minZoom / maxZoom für die Anzeigesteuerung zur Verfügung.
3-2. Ebenen-Standardwerte festlegen
Beginnen Sie hier zuerst. Der an `GeoJSONLayerState` übergebene Wert ist die Basis für alle Features. Da der Zustand observable ist, wird bei späterer Zuweisung neu gezeichnet.
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,
)
}
// Der State ist observable: später zuweisen und die Kacheln werden neu erzeugt
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
)
)
// Die Felder von LayerStyle sind let, also die ganze Struktur austauschen (alpha sitzt bei 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 für eine deckende Farbe, argbToCss beim Übergang zu CSS
const legend = argbToCss(colorRgb(30, 136, 229)); // "rgba(30,136,229,1.0000)"Farbspezifikationsformate
Android und React verwenden 32-Bit-Ganzzahlen im ARGB-Format (Alpha ist das höchstwertige Byte), iOS verwendet `UIColor` mit Alpha in der Farbe selbst. React verfügt über die Hilfsfunktionen `colorArgb(a,r,g,b)` / `colorRgb(r,g,b)` / `argbToCss()` in derselben Reihenfolge wie `Color.argb()` unter Android. Die Standardfarben auf allen drei Plattformen sind einheitlich auf #1E88E5 (Linien deckend, Füllung Alpha 128) festgelegt.
Für jedes Feature überschreiben
Ein Feature kann selbst `strokeColor` / `fillColor` / `strokeWidth` / `pointRadius` / `visible` enthalten. Bleibt es `null`, gilt der Standardwert, ist ein Wert vorhanden, gewinnt dieser. Wenn der Stil beim Laden der Daten feststeht (und sich später nicht ändert), ist dies der unkomplizierteste und schnellste Weg.
val parsed = GeoJSONParser.parseStream(input)
// Nach dem Parsen die Eigenschaften lesen und den Stil einbrennen
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 // Bleibt null, also wird der Ebenen-Standard verwendet
}
}
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 // Bleibt nil, gilt der Ebenen-Standard
}
}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; // Bleibt null, gilt der Ebenen-Standard
}), []);
<GeoJSONLayer state={layerState} features={styled} />Dynamisch mit StyleProvider festlegen
Stile, die nach „Regeln“ bestimmt werden – z. B. Farben basierend auf Werten aus `properties`, nur das ausgewählte Feature hervorheben, Schwellenwerte über die UI ändern –, werden im `StyleProvider` definiert. Er wird für jedes einzelne Feature aufgerufen, empfängt die Ebenen-Standardwerte und gibt den endgültigen Stil zurück.
Übergebenes
Feature-Objekt (einschließlich properties) und die aktuellen Standardwerte der Ebene. Üblicherweise kopiert man die Standardwerte und ändert nur einen Teil.
Rückgabe
Ein LayerStyle, in dem alle 4 Elemente ausgefüllt sind. Für Elemente, die nicht angefasst werden, können einfach die Standardwerte zurückgegeben werden, damit die Einstellungen der ersten Ebene wirksam bleiben.
// Es ist ein fun interface, also genügt ein Lambda
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) // Was Sie nicht anfassen, behält seinen Standardwert
}
val layerState = remember {
GeoJSONLayerState(styleProvider = densityStyle)
}
// Ein späterer Austausch wertet alle Features neu aus
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()// Im Web statt eines StyleProvider das Parse-Ergebnis mappen und
// jedem Feature einen eigenen Stil geben (das Ergebnis ist dasselbe)
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) })),
[],
);Wenn der `StyleProvider` ausgetauscht wird oder sich der referenzierte Zustand ändert, werden die Stile aller Features neu bewertet und die Tiles neu erstellt. Da er für jedes einzelne Feature aufgerufen wird, sollten Sie rechenintensive Vorgänge (reguläre Ausdrücke, Netzwerk, Datumsparsing usw.) außerhalb des Providers vorab berechnen.
3-5. Was verwenden?
04 · Tap-Erkennung
Da `MapConductor` nur einen einzelnen Click-Listener hat, erfolgt die Weiterleitung an die Ebene durch die App. Eine automatische Registrierung erfolgt nicht absichtlicherweise. `processClick` gibt nur dann `true` zurück, wenn ein Feature getroffen wurde.
val layerState = remember {
GeoJSONLayerState(
onClick = { feature, position -> selected = feature },
)
}
MapLibreMapView(
state = mapViewState,
onMapClick = { point ->
// Trefferprüfung mit 15 px Toleranz, die dem Zoom folgt
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),
}), []);
// Aus dem Klick-Handler der Karte weiterreichen
const handleMapClick = (point: GeoPointInterface) => {
const consumed = layerState.processClick(point, 10, state.camera.zoom);
if (!consumed) setSelected(null);
};Pixel-Toleranz
Wenn Sie processClick eine Toleranz in Pixeln und den aktuellen Zoom übergeben, erfolgt die Erkennung zoomabhängig. Ohne Angabe ist es die Standardtoleranz in Weltkoordinaten (ca. 0,0002°).
Bei Überlappung
Es wird das zuletzt gezeichnete Feature (also das oberste) zurückgegeben.
Unterstützte Geometrien
Punkte, Linien, Polygone mit Löchern, mehrteilige Geometriesammlungen.
05 · Datenvolumen und Laden
Verwenden Sie für große Daten einen Streaming-Parser und parsen Sie im Hintergrund. Es gibt zwei Möglichkeiten, Features zu speichern.
Statisch, für große Mengen
Unveränderliche Datenobjekte. Leichtgewichtig, da selbst bei zehntausenden Elementen keine Zustandsobjekte erstellt werden. Für große GeoJSON-Dateien verwenden Sie dies.
Für wenige, häufig wechselnde
Kann einzeln reaktiv aktualisiert werden. Bei vielen Elementen schlagen die Kosten der Zustandsverwaltung zu Buche, also nur so viele wie nötig verwenden.
// parseStream für eine große FeatureCollection
val features = withContext(Dispatchers.IO) {
GeoJSONParser.parseStream(input)
}
// GeoJSON Text Sequences nach 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
// Einzeln anhängen, in Stapeln oder persistent
}const features = GeoJSONParser.parseFeatures(text);
const seq = GeoJSONSeqParser.parse(text); // Zeilengetrennte Sequenz
// tileSize ist standardmäßig 512; für Retina lässt es sich erhöhen
<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())
}