Skip to content

Commit 85d63a6

Browse files
committed
Add GeoJsonParser
1 parent 8c21286 commit 85d63a6

4 files changed

Lines changed: 629 additions & 0 deletions

File tree

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
/*
2+
* Copyright (c) 2025 Alexander Farber
3+
* SPDX-License-Identifier: MIT
4+
*
5+
* This file is part of the OpenMapView project (https://github.com/afarber/OpenMapView)
6+
*/
7+
8+
package de.afarber.openmapview
9+
10+
import android.graphics.Color
11+
import org.json.JSONArray
12+
import org.json.JSONObject
13+
14+
/**
15+
* Parser for GeoJSON data to convert to map overlays.
16+
*
17+
* Supports GeoJSON Feature, FeatureCollection, and direct geometries:
18+
* - Point -> Marker
19+
* - LineString -> Polyline
20+
* - Polygon -> Polygon
21+
* - MultiPoint -> Multiple Markers
22+
* - MultiLineString -> Multiple Polylines
23+
* - MultiPolygon -> Multiple Polygons
24+
*/
25+
object GeoJsonParser {
26+
/**
27+
* Parse GeoJSON string and return parsed features.
28+
*
29+
* @param geoJsonString Valid GeoJSON string
30+
* @return GeoJsonResult containing markers, polylines, and polygons
31+
*/
32+
fun parse(geoJsonString: String): GeoJsonResult {
33+
val json = JSONObject(geoJsonString)
34+
return parseGeoJson(json)
35+
}
36+
37+
private fun parseGeoJson(json: JSONObject): GeoJsonResult {
38+
val type = json.optString("type", "")
39+
40+
return when (type) {
41+
"FeatureCollection" -> parseFeatureCollection(json)
42+
"Feature" -> parseFeature(json)
43+
"Point", "MultiPoint" -> parseGeometry(json, null)
44+
"LineString", "MultiLineString" -> parseGeometry(json, null)
45+
"Polygon", "MultiPolygon" -> parseGeometry(json, null)
46+
else -> throw IllegalArgumentException("Unsupported GeoJSON type: $type")
47+
}
48+
}
49+
50+
private fun parseFeatureCollection(json: JSONObject): GeoJsonResult {
51+
val features = json.optJSONArray("features") ?: return GeoJsonResult()
52+
val markers = mutableListOf<Marker>()
53+
val polylines = mutableListOf<Polyline>()
54+
val polygons = mutableListOf<Polygon>()
55+
56+
for (i in 0 until features.length()) {
57+
val feature = features.getJSONObject(i)
58+
val result = parseFeature(feature)
59+
markers.addAll(result.markers)
60+
polylines.addAll(result.polylines)
61+
polygons.addAll(result.polygons)
62+
}
63+
64+
return GeoJsonResult(markers, polylines, polygons)
65+
}
66+
67+
private fun parseFeature(json: JSONObject): GeoJsonResult {
68+
val geometry = json.optJSONObject("geometry") ?: return GeoJsonResult()
69+
val properties = json.optJSONObject("properties")
70+
71+
return parseGeometry(geometry, properties)
72+
}
73+
74+
private fun parseGeometry(
75+
geometry: JSONObject,
76+
properties: JSONObject?,
77+
): GeoJsonResult {
78+
val type = geometry.optString("type", "")
79+
val coordinates = geometry.optJSONArray("coordinates") ?: return GeoJsonResult()
80+
81+
return when (type) {
82+
"Point" -> parsePoint(coordinates, properties)
83+
"MultiPoint" -> parseMultiPoint(coordinates, properties)
84+
"LineString" -> parseLineString(coordinates, properties)
85+
"MultiLineString" -> parseMultiLineString(coordinates, properties)
86+
"Polygon" -> parsePolygon(coordinates, properties)
87+
"MultiPolygon" -> parseMultiPolygon(coordinates, properties)
88+
else -> throw IllegalArgumentException("Unsupported geometry type: $type")
89+
}
90+
}
91+
92+
private fun parsePoint(
93+
coordinates: JSONArray,
94+
properties: JSONObject?,
95+
): GeoJsonResult {
96+
val latLng = parseCoordinate(coordinates)
97+
val marker =
98+
Marker(
99+
position = latLng,
100+
title = properties?.optString("name") ?: properties?.optString("title"),
101+
snippet = properties?.optString("description"),
102+
)
103+
return GeoJsonResult(markers = listOf(marker))
104+
}
105+
106+
private fun parseMultiPoint(
107+
coordinates: JSONArray,
108+
properties: JSONObject?,
109+
): GeoJsonResult {
110+
val markers = mutableListOf<Marker>()
111+
for (i in 0 until coordinates.length()) {
112+
val coord = coordinates.getJSONArray(i)
113+
val latLng = parseCoordinate(coord)
114+
val marker =
115+
Marker(
116+
position = latLng,
117+
title = properties?.optString("name") ?: properties?.optString("title"),
118+
snippet = properties?.optString("description"),
119+
)
120+
markers.add(marker)
121+
}
122+
return GeoJsonResult(markers = markers)
123+
}
124+
125+
private fun parseLineString(
126+
coordinates: JSONArray,
127+
properties: JSONObject?,
128+
): GeoJsonResult {
129+
val points = parseCoordinateArray(coordinates)
130+
if (points.size < 2) return GeoJsonResult()
131+
132+
val polyline =
133+
Polyline(
134+
points = points,
135+
strokeColor = parseColor(properties?.optString("stroke")) ?: Color.BLUE,
136+
strokeWidth = properties?.optDouble("stroke-width", 5.0)?.toFloat() ?: 5f,
137+
tag = properties,
138+
)
139+
return GeoJsonResult(polylines = listOf(polyline))
140+
}
141+
142+
private fun parseMultiLineString(
143+
coordinates: JSONArray,
144+
properties: JSONObject?,
145+
): GeoJsonResult {
146+
val polylines = mutableListOf<Polyline>()
147+
for (i in 0 until coordinates.length()) {
148+
val lineCoords = coordinates.getJSONArray(i)
149+
val points = parseCoordinateArray(lineCoords)
150+
if (points.size >= 2) {
151+
val polyline =
152+
Polyline(
153+
points = points,
154+
strokeColor = parseColor(properties?.optString("stroke")) ?: Color.BLUE,
155+
strokeWidth = properties?.optDouble("stroke-width", 5.0)?.toFloat() ?: 5f,
156+
tag = properties,
157+
)
158+
polylines.add(polyline)
159+
}
160+
}
161+
return GeoJsonResult(polylines = polylines)
162+
}
163+
164+
private fun parsePolygon(
165+
coordinates: JSONArray,
166+
properties: JSONObject?,
167+
): GeoJsonResult {
168+
if (coordinates.length() == 0) return GeoJsonResult()
169+
170+
// First array is outer ring
171+
val outerRing = parseCoordinateArray(coordinates.getJSONArray(0))
172+
if (outerRing.size < 3) return GeoJsonResult()
173+
174+
// Remaining arrays are holes
175+
val holes = mutableListOf<List<LatLng>>()
176+
for (i in 1 until coordinates.length()) {
177+
val hole = parseCoordinateArray(coordinates.getJSONArray(i))
178+
if (hole.size >= 3) {
179+
holes.add(hole)
180+
}
181+
}
182+
183+
val polygon =
184+
Polygon(
185+
points = outerRing,
186+
holes = holes,
187+
strokeColor = parseColor(properties?.optString("stroke")) ?: Color.BLACK,
188+
strokeWidth = properties?.optDouble("stroke-width", 3.0)?.toFloat() ?: 3f,
189+
fillColor = parseColor(properties?.optString("fill")) ?: Color.argb(100, 128, 128, 128),
190+
tag = properties,
191+
)
192+
return GeoJsonResult(polygons = listOf(polygon))
193+
}
194+
195+
private fun parseMultiPolygon(
196+
coordinates: JSONArray,
197+
properties: JSONObject?,
198+
): GeoJsonResult {
199+
val polygons = mutableListOf<Polygon>()
200+
for (i in 0 until coordinates.length()) {
201+
val polygonCoords = coordinates.getJSONArray(i)
202+
val result = parsePolygon(polygonCoords, properties)
203+
polygons.addAll(result.polygons)
204+
}
205+
return GeoJsonResult(polygons = polygons)
206+
}
207+
208+
private fun parseCoordinate(coordinates: JSONArray): LatLng {
209+
val lng = coordinates.getDouble(0)
210+
val lat = coordinates.getDouble(1)
211+
return LatLng(lat, lng)
212+
}
213+
214+
private fun parseCoordinateArray(coordinates: JSONArray): List<LatLng> {
215+
val points = mutableListOf<LatLng>()
216+
for (i in 0 until coordinates.length()) {
217+
val coord = coordinates.getJSONArray(i)
218+
points.add(parseCoordinate(coord))
219+
}
220+
return points
221+
}
222+
223+
private fun parseColor(colorString: String?): Int? {
224+
if (colorString == null) return null
225+
return try {
226+
Color.parseColor(colorString)
227+
} catch (e: IllegalArgumentException) {
228+
null
229+
}
230+
}
231+
}
232+
233+
/**
234+
* Result of parsing GeoJSON data.
235+
*/
236+
data class GeoJsonResult(
237+
val markers: List<Marker> = emptyList(),
238+
val polylines: List<Polyline> = emptyList(),
239+
val polygons: List<Polygon> = emptyList(),
240+
)

openmapview/src/main/kotlin/de/afarber/openmapview/MapController.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,14 @@ class MapController(
405405

406406
fun getPolygons(): List<Polygon> = polygons.toList()
407407

408+
fun addGeoJson(geoJsonString: String): GeoJsonResult {
409+
val result = GeoJsonParser.parse(geoJsonString)
410+
result.markers.forEach { addMarker(it) }
411+
result.polylines.forEach { addPolyline(it) }
412+
result.polygons.forEach { addPolygon(it) }
413+
return result
414+
}
415+
408416
fun handleMarkerTouch(
409417
x: Float,
410418
y: Float,

openmapview/src/main/kotlin/de/afarber/openmapview/OpenMapView.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ class OpenMapView
197197

198198
fun getPolygons(): List<Polygon> = controller.getPolygons()
199199

200+
fun addGeoJson(geoJsonString: String): GeoJsonResult {
201+
val result = controller.addGeoJson(geoJsonString)
202+
invalidate()
203+
return result
204+
}
205+
200206
fun setOnAttributionClickListener(listener: () -> Unit) {
201207
attributionOverlay.onAttributionClickListener = listener
202208
}

0 commit comments

Comments
 (0)