Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,9 @@ object GeoJsonMapper {
val finalProperties = if (id != null) featureProperties + ("id" to id) else featureProperties

val style = properties?.let { props ->
when (geometry) {
is LineString, is MultiGeometry -> { // MultiGeometry could contain lines
when {
geometry is LineString || (geometry is MultiGeometry && !geometry.isPolygonal()) -> {
// MultiGeometry could contain lines
val strokeColor = props["stroke"]?.let { parseColor(it) }
val strokeWidth = props["stroke-width"]?.toFloatOrNull()
if (strokeColor != null || strokeWidth != null) {
Expand All @@ -102,7 +103,7 @@ object GeoJsonMapper {
)
} else null
}
is ModelPolygon -> {
geometry is ModelPolygon || (geometry is MultiGeometry && geometry.isPolygonal()) -> {
val strokeColor = props["stroke"]?.let { parseColor(it) }
val strokeWidth = props["stroke-width"]?.toFloatOrNull()
val fillColor = props["fill"]?.let { parseColor(it) }
Expand All @@ -125,7 +126,7 @@ object GeoJsonMapper {
)
} else null
}
is PointGeometry -> {
geometry is PointGeometry -> {
// TODO: Marker styling (marker-color, marker-size, marker-symbol)
null
}
Expand All @@ -136,6 +137,14 @@ object GeoJsonMapper {
return Feature(geometry, style = style, properties = finalProperties)
}

/**
* True for a Polygon or a multi-geometry whose members are all (recursively) polygons — i.e. a
* GeoJSON MultiPolygon — which per the simplestyle-spec carries fill styling rather than line styling.
*/
private fun Geometry.isPolygonal(): Boolean =
this is ModelPolygon ||
(this is MultiGeometry && geometries.isNotEmpty() && geometries.all { it.isPolygonal() })

private fun parseColor(colorString: String): Int? {
if (colorString.startsWith("#")) {
// Handle hex color
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,11 @@ class MapViewRenderer(

is MultiGeometry -> {
feature.geometry.geometries.forEach { geometry ->
// Recursively add each geometry in the MultiGeometry
addFeature(feature.copy(geometry = geometry))
val childFeature = feature.copy(geometry = geometry)
addFeature(childFeature)
renderedFeatures[childFeature]?.let { childObjects ->
mapObjects.addAll(childObjects)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,37 +34,53 @@ data class DataLayer(
val boundsBuilder = LatLngBounds.builder()
var hasPoints = false
features.forEach { feature ->
when (val geometry = feature.geometry) {
is PointGeometry -> {
boundsBuilder.include(LatLng(geometry.point.lat, geometry.point.lng))
hasPoints = true
}
if (includeGeometryPoints(feature.geometry, boundsBuilder)) {
hasPoints = true
}
}
if (hasPoints) boundsBuilder.build() else null
}

is LineString -> {
geometry.points.forEach {
boundsBuilder.include(LatLng(it.lat, it.lng))
hasPoints = true
}
}
private fun includeGeometryPoints(
geometry: Geometry,
boundsBuilder: LatLngBounds.Builder,
): Boolean {
var added = false
when (geometry) {
is PointGeometry -> {
boundsBuilder.include(LatLng(geometry.point.lat, geometry.point.lng))
added = true
}

is Polygon -> {
geometry.outerBoundary.forEach {
boundsBuilder.include(LatLng(it.lat, it.lng))
hasPoints = true
}
is LineString -> {
geometry.points.forEach {
boundsBuilder.include(LatLng(it.lat, it.lng))
added = true
}
}

is MultiGeometry -> {
// TODO: Implement MultiGeometry bounds calculation if needed
is Polygon -> {
geometry.outerBoundary.forEach {
boundsBuilder.include(LatLng(it.lat, it.lng))
added = true
}
}

is GroundOverlay -> {
boundsBuilder.include(geometry.latLngBounds.northeast)
boundsBuilder.include(geometry.latLngBounds.southwest)
hasPoints = true
is MultiGeometry -> {
geometry.geometries.forEach { subGeom ->
if (includeGeometryPoints(subGeom, boundsBuilder)) {
added = true
}
}
}

is GroundOverlay -> {
boundsBuilder.include(geometry.latLngBounds.northeast)
boundsBuilder.include(geometry.latLngBounds.southwest)
added = true
}
}
if (hasPoints) boundsBuilder.build() else null
return added
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.maps.android.data.renderer

import com.google.maps.android.data.parser.geojson.GeoJsonParser
import com.google.maps.android.data.renderer.mapper.GeoJsonMapper
import com.google.maps.android.data.renderer.model.LineStyle
import com.google.maps.android.data.renderer.model.MultiGeometry
import com.google.maps.android.data.renderer.model.PolygonStyle
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import java.io.ByteArrayInputStream

/**
* End-to-end integration tests verifying GeoJSON parsing and mapping into [DataLayer] features.
*/
class GeoJsonIntegrationTest {

@Test
fun testParseAndMapGeoJsonFeatureCollectionWithMultipleGeometryTypes() {
val geoJsonContent =
"""
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Zoned Region",
"stroke": "#ff0000",
"stroke-width": 3.0,
"fill": "#00ff00",
"fill-opacity": 0.4
},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[100.0, 0.0],
[101.0, 0.0],
[101.0, 1.0],
[100.0, 0.0]
]
]
]
}
},
{
"type": "Feature",
"properties": {
"name": "Transit Line",
"stroke": "#0000ff",
"stroke-width": 4.0
},
"geometry": {
"type": "MultiLineString",
"coordinates": [
[
[100.0, 0.0],
[101.0, 1.0]
]
]
}
}
]
}
""".trimIndent()

val parser = GeoJsonParser()
val parsedObject = parser.parse(ByteArrayInputStream(geoJsonContent.toByteArray()))
assertNotNull(parsedObject)

val layer = GeoJsonMapper.toLayer(parsedObject!!)
assertEquals(2, layer.features.size)

// Verify MultiPolygon feature
val multiPolygonFeature = layer.features[0]
assertTrue(multiPolygonFeature.geometry is MultiGeometry)
assertTrue(multiPolygonFeature.style is PolygonStyle)
val polygonStyle = multiPolygonFeature.style as PolygonStyle
assertEquals(0xFFFF0000.toInt(), polygonStyle.strokeColor)
assertEquals(3.0f, polygonStyle.strokeWidth, 0.001f)
val expectedFillColor = (0x66 shl 24) or 0x00FF00 // 0.4 * 255 = 102 = 0x66
assertEquals(expectedFillColor, polygonStyle.fillColor)

// Verify MultiLineString feature
val multiLineFeature = layer.features[1]
assertTrue(multiLineFeature.geometry is MultiGeometry)
assertTrue(multiLineFeature.style is LineStyle)
val lineStyle = multiLineFeature.style as LineStyle
assertEquals(0xFF0000FF.toInt(), lineStyle.color)
assertEquals(4.0f, lineStyle.width, 0.001f)
}

@Test
fun testDataLayerBoundingBox_includesMultiGeometryCoordinates() {
val geoJsonContent =
"""
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "MultiPolygon",
"coordinates": [
[
[
[-74.0250, 40.7000],
[-74.0100, 40.7000],
[-74.0100, 40.7150],
[-74.0250, 40.7000]
]
]
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "MultiLineString",
"coordinates": [
[
[-73.9700, 40.7450],
[-73.9600, 40.7715]
]
]
}
}
]
}
""".trimIndent()

val parser = GeoJsonParser()
val parsedObject = parser.parse(ByteArrayInputStream(geoJsonContent.toByteArray()))
assertNotNull(parsedObject)

val layer = GeoJsonMapper.toLayer(parsedObject!!)
val bounds = layer.boundingBox
assertNotNull(bounds)

assertEquals(40.7000, bounds!!.southwest.latitude, 0.0001)
assertEquals(-74.0250, bounds.southwest.longitude, 0.0001)
assertEquals(40.7715, bounds.northeast.latitude, 0.0001)
assertEquals(-73.9600, bounds.northeast.longitude, 0.0001)
}
}

Loading
Loading