Skip to content

Commit 162330b

Browse files
committed
feat: add Trails sample with polyline rendering
This commit introduces a new "Trails" sample to the AdvancedMaps3DSamples app. Key features and changes: - **Trails Data Model:** Defines data classes (`Trail`, `TrailType`, `DifficultyLevel`, `DogRegulation`, `TrailStatus`) to represent trail information. - **GeoJSON Parsing:** Implements `TrailsRepository` to read and parse GeoJSON data containing trail information. Includes logic for: - Reading from an asset file. - Parsing GeoJSON features, properties (including specific OSMP attributes), and LineString geometries. - Simplifying trail coordinates using `simplify()` from `com.google.maps.android.ktx.utils`. - Calculating the `LatLngBounds` of all trails. - **Polyline Rendering:** - Adds `Mappers.kt` with a `toPolylineOptions()` extension function to convert `Trail` objects into `PolylineOptions`. - Colors polylines based on trail difficulty (Easy: Green, Moderate: Blue, Difficult: Red). - Sets `altitudeMode` to `CLAMP_TO_GROUND` and `drawsOccludedSegments` to true. - **Trails UI:** - Creates `TrailsActivity`, `TrailsViewModel`, and associated composables (`MapScreen`, `LoadingScreen`). - The `TrailsViewModel` loads trail data, creates `Map3DOptions` centered on the trails' boundary, and adds polylines to the map. - Displays a loading indicator while data is being fetched. - Integrates the `ThreeDMap` composable for displaying the map. - **Common Map Composable:** Refactors `ThreeDMap.kt` from the `scenarios` package to the `common` package to be reused. - **ViewModel Enhancements:** - Modifies `Map3dViewModel` to use `MutableSharedFlow` with replay for `currentCamera` to ensure observers get the latest value upon subscription. - Makes `activeMapObjects` in `Map3dViewModel` internal for access within the module. - **MainActivity Update:** Adds the "OSMP Trails -- polyline demo" sample to the main list. - **Manifest and Resources:** Updates `AndroidManifest.xml` to include `TrailsActivity` and adds a new string resource for the sample title.
1 parent 8ba1c03 commit 162330b

16 files changed

Lines changed: 1341 additions & 7 deletions

File tree

Maps3DSamples/advanced/app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@
5151
android:name=".scenarios.ScenariosActivity"
5252
android:exported="true"
5353
/>
54+
55+
<activity
56+
android:name=".trails.TrailsActivity"
57+
android:exported="true"
58+
/>
59+
5460
</application>
5561

5662
</manifest>

Maps3DSamples/advanced/app/src/main/assets/OSMP_Trails.geojson

Lines changed: 597 additions & 0 deletions
Large diffs are not rendered by default.

Maps3DSamples/advanced/app/src/main/assets/OSMP_head.geojson

Lines changed: 20 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"type": "FeatureCollection",
3+
"name": "OSMP_Trails",
4+
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
5+
"features": [
6+
{ "type": "Feature", "properties": { "OSMPTrailsOSMPOBJECTID": 2669, "OSMPTrailsOSMPOWNER": "OSMP", "OSMPTrailsOSMPBICYCLES": "No", "OSMPTrailsOSMPDISPLAY": "Yes", "OSMPTrailsOSMPTRAILTYPE": "Hiking Trail", "OSMPTrailsOSMPDATEFROM": "2022-06-05T00:00:00Z", "OSMPTrailsOSMPDATETO": "2099-12-31T00:00:00Z", "OSMPTrailsOSMPSEGMENTID": "289-334-328", "OSMPTrailsOSMPHORSES": "Yes", "OSMPTrailsOSMPRID": 2052, "OSMPTrailsOSMPTRLID": 289, "OSMPTrailsOSMPMILEAGE": 1.17, "OSMPTrailsOSMPMEASUREDFEET": 6466, "OSMPTrailsOSMPTRAILNAME": "Mount Sanitas", "OSMPTrailsOSMPGlobalID": "{B2FFBAF8-5CA4-4DDE-9EF5-B83CCF6C50BB}", "OSMPTrailsOSMPDIFFICULTY": "Difficult", "OSMPTrailsOSMPDOGS": "Yes", "OSMPTrailsOSMPDOGREGGEN": "LVS", "OSMPTrailsOSMPDOGREGDESC": "Leash, Or Voice and Sight Control", "OSMPTrailsOSMPEBIKES": "No", "SHAPESTLength": 6180.0708380715032, "OSMPTrailClosuresOBJECTID": 2441, "OSMPTrailClosuresRID": 2052, "OSMPTrailClosuresCLOSUREDURATION": null, "OSMPTrailClosuresWEBLINK": null, "OSMPTrailClosuresCLOSUREAREA": null, "OSMPTrailClosuresTRAILSTATUS": "Open", "OSMPTrailClosuresCLOSUREREASON": "Hazardous Conditions", "OSMPTrailClosuresLOCATIONDESCRIPTION": null, "OSMPTrailClosuresCONTACT": "https://bouldercolorado.gov/services/osmp-closures", "OSMPTrailClosuresCOMMENTS": null, "OSMPTrailClosuresGLOBALID": "{3FFDA1D3-AF1A-46C7-87E2-4A4707D27043}", "OSMPTrailClosuresSEGMENTID": "289-334-328" }, "geometry": { "type": "LineString", "coordinates": [ [ -105.30544377586854, 40.034240911930176 ], [ -105.305456631901905, 40.034214903048209 ], [ -105.305461482097371, 40.034212217385786 ] ] } },
7+
{ "type": "Feature", "properties": { "OSMPTrailsOSMPOBJECTID": 2672, "OSMPTrailsOSMPOWNER": "OSMP", "OSMPTrailsOSMPBICYCLES": "Yes", "OSMPTrailsOSMPDISPLAY": "Yes", "OSMPTrailsOSMPTRAILTYPE": "Multi-Use Trail", "OSMPTrailsOSMPDATEFROM": "2023-05-21T00:00:00Z", "OSMPTrailsOSMPDATETO": "2099-12-31T00:00:00Z", "OSMPTrailsOSMPSEGMENTID": "506-550-678", "OSMPTrailsOSMPHORSES": "Yes", "OSMPTrailsOSMPRID": 2212, "OSMPTrailsOSMPTRLID": 506, "OSMPTrailsOSMPMILEAGE": 0.554, "OSMPTrailsOSMPMEASUREDFEET": 2909, "OSMPTrailsOSMPTRAILNAME": "Cottontail", "OSMPTrailsOSMPGlobalID": "{7945E318-34F8-4D35-A4B9-CBC719FEE03E}", "OSMPTrailsOSMPDIFFICULTY": "Easy", "OSMPTrailsOSMPDOGS": "Yes", "OSMPTrailsOSMPDOGREGGEN": "LR", "OSMPTrailsOSMPDOGREGDESC": "Leash Required", "OSMPTrailsOSMPEBIKES": "Yes", "SHAPESTLength": 2928.7298169670048, "OSMPTrailClosuresOBJECTID": 1082, "OSMPTrailClosuresRID": 2212, "OSMPTrailClosuresCLOSUREDURATION": null, "OSMPTrailClosuresWEBLINK": null, "OSMPTrailClosuresCLOSUREAREA": null, "OSMPTrailClosuresTRAILSTATUS": "Open", "OSMPTrailClosuresCLOSUREREASON": null, "OSMPTrailClosuresLOCATIONDESCRIPTION": null, "OSMPTrailClosuresCONTACT": "https://bouldercolorado.gov/services/osmp-closures", "OSMPTrailClosuresCOMMENTS": null, "OSMPTrailClosuresGLOBALID": "{AF6F3A35-0B7B-40B5-97B4-7E32A5F17791}", "OSMPTrailClosuresSEGMENTID": "506-550-678" }, "geometry": { "type": "LineString", "coordinates": [ [ -105.188245260507003, 40.076068062834487 ], [ -105.188250903292811, 40.076066519052269 ], [ -105.18825479155872, 40.076065365015204 ], [ -105.188259356432567, 40.076063925498559 ] ] } },
8+
{ "type": "Feature", "properties": { "OSMPTrailsOSMPOBJECTID": 2673, "OSMPTrailsOSMPOWNER": "OSMP", "OSMPTrailsOSMPBICYCLES": "Yes", "OSMPTrailsOSMPDISPLAY": "Yes", "OSMPTrailsOSMPTRAILTYPE": "Multi-Use Trail", "OSMPTrailsOSMPDATEFROM": "2023-05-21T00:00:00Z", "OSMPTrailsOSMPDATETO": "2099-12-31T00:00:00Z", "OSMPTrailsOSMPSEGMENTID": "506-678-551", "OSMPTrailsOSMPHORSES": "Yes", "OSMPTrailsOSMPRID": 2213, "OSMPTrailsOSMPTRLID": 506, "OSMPTrailsOSMPMILEAGE": 0.235, "OSMPTrailsOSMPMEASUREDFEET": 1223, "OSMPTrailsOSMPTRAILNAME": "Cottontail", "OSMPTrailsOSMPGlobalID": "{C6C184B2-983C-49F7-A600-4BDA8E89E07E}", "OSMPTrailsOSMPDIFFICULTY": "Easy", "OSMPTrailsOSMPDOGS": "Yes", "OSMPTrailsOSMPDOGREGGEN": "LR", "OSMPTrailsOSMPDOGREGDESC": "Leash Required", "OSMPTrailsOSMPEBIKES": "Yes", "SHAPESTLength": 1242.2713283160997, "OSMPTrailClosuresOBJECTID": 1083, "OSMPTrailClosuresRID": 2213, "OSMPTrailClosuresCLOSUREDURATION": null, "OSMPTrailClosuresWEBLINK": null, "OSMPTrailClosuresCLOSUREAREA": null, "OSMPTrailClosuresTRAILSTATUS": "Open", "OSMPTrailClosuresCLOSUREREASON": null, "OSMPTrailClosuresLOCATIONDESCRIPTION": null, "OSMPTrailClosuresCONTACT": "https://bouldercolorado.gov/services/osmp-closures", "OSMPTrailClosuresCOMMENTS": null, "OSMPTrailClosuresGLOBALID": "{E20E1606-73D2-4AA7-B6CD-1D7A3222D9D0}", "OSMPTrailClosuresSEGMENTID": "506-678-551" }, "geometry": { "type": "LineString", "coordinates": [ [ -105.197753276922469, 40.077185977924238 ], [ -105.197781787599666, 40.077194737520273 ], [ -105.197786697871052, 40.077195224116728 ], [ -105.197792423315008, 40.077195633590208 ] ] } },
9+
{ "type": "Feature", "properties": { "OSMPTrailsOSMPOBJECTID": 2675, "OSMPTrailsOSMPOWNER": "OSMP", "OSMPTrailsOSMPBICYCLES": "No", "OSMPTrailsOSMPDISPLAY": "Yes", "OSMPTrailsOSMPTRAILTYPE": "Hiking Trail", "OSMPTrailsOSMPDATEFROM": "2021-07-28T00:00:00Z", "OSMPTrailsOSMPDATETO": "2099-12-31T00:00:00Z", "OSMPTrailsOSMPSEGMENTID": "228-147-247", "OSMPTrailsOSMPHORSES": "Yes", "OSMPTrailsOSMPRID": 1979, "OSMPTrailsOSMPTRLID": 228, "OSMPTrailsOSMPMILEAGE": 0.071, "OSMPTrailsOSMPMEASUREDFEET": 439, "OSMPTrailsOSMPTRAILNAME": "Green Mountain West Ridge", "OSMPTrailsOSMPGlobalID": "{16900822-9700-4D25-8BA8-280FCCA71F61}", "OSMPTrailsOSMPDIFFICULTY": "Moderate", "OSMPTrailsOSMPDOGS": "Yes", "OSMPTrailsOSMPDOGREGGEN": "LVS", "OSMPTrailsOSMPDOGREGDESC": "On-Corridor Voice and Sight", "OSMPTrailsOSMPEBIKES": "No", "SHAPESTLength": 377.13724505847586, "OSMPTrailClosuresOBJECTID": 1085, "OSMPTrailClosuresRID": 1979, "OSMPTrailClosuresCLOSUREDURATION": null, "OSMPTrailClosuresWEBLINK": null, "OSMPTrailClosuresCLOSUREAREA": null, "OSMPTrailClosuresTRAILSTATUS": "Open", "OSMPTrailClosuresCLOSUREREASON": "Maintenance", "OSMPTrailClosuresLOCATIONDESCRIPTION": null, "OSMPTrailClosuresCONTACT": "https://bouldercolorado.gov/services/osmp-closures", "OSMPTrailClosuresCOMMENTS": "Closed for NCAR Fire", "OSMPTrailClosuresGLOBALID": "{2BE6F52A-EA3B-434B-B169-CEC9C646747B}", "OSMPTrailClosuresSEGMENTID": "228-147-247" }, "geometry": { "type": "LineString", "coordinates": [ [ -105.302684734024794, 39.982138131036649 ], [ -105.302655393425567, 39.982140448627973 ], [ -105.302623621031643, 39.982178807119261 ] ] } },
10+
]
11+
}

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
4040
import androidx.compose.ui.res.stringResource
4141
import androidx.compose.ui.unit.dp
4242
import com.example.advancedmaps3dsamples.scenarios.ScenariosActivity
43+
import com.example.advancedmaps3dsamples.trails.TrailsActivity
4344
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
4445
import dagger.hilt.android.AndroidEntryPoint
4546

@@ -48,6 +49,7 @@ data class MapSample(@StringRes val label: Int, val clazz: Class<*>)
4849
private val samples =
4950
listOf(
5051
MapSample(R.string.map_sample_scenarios, ScenariosActivity::class.java),
52+
MapSample(R.string.map_sample_osmp_trails, TrailsActivity::class.java),
5153
)
5254

5355
@OptIn(ExperimentalMaterial3Api::class)

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/Map3dViewModel.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.asStateFlow
4848
import kotlinx.coroutines.flow.callbackFlow
4949
import kotlinx.coroutines.flow.filterNotNull
5050
import kotlinx.coroutines.flow.map
51+
import kotlinx.coroutines.flow.stateIn
5152
import kotlinx.coroutines.launch
5253
import kotlin.time.Duration
5354

@@ -73,8 +74,12 @@ abstract class Map3dViewModel : ViewModel() {
7374

7475
// --- Camera Position from Map & Pending Requests ---
7576
// This is guaranteed to always be a valid camera
76-
private val _currentCamera = MutableStateFlow(DEFAULT_CAMERA)
77-
val currentCamera = _currentCamera.asStateFlow()
77+
private val _currentCamera = MutableSharedFlow<Camera>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
78+
val currentCamera = _currentCamera.stateIn(
79+
scope = viewModelScope,
80+
started = kotlinx.coroutines.flow.SharingStarted.WhileSubscribed(5000),
81+
initialValue = DEFAULT_CAMERA
82+
)
7883

7984
private val mapObjects = mutableMapOf<String, MapObject>()
8085

@@ -96,7 +101,7 @@ abstract class Map3dViewModel : ViewModel() {
96101
onBufferOverflow = BufferOverflow.DROP_OLDEST
97102
)
98103

99-
private val activeMapObjects = mutableMapOf<String, ActiveMapObject>()
104+
internal val activeMapObjects = mutableMapOf<String, ActiveMapObject>()
100105

101106
val mapReady = _googleMap3D.map { it != null }
102107

@@ -108,7 +113,7 @@ abstract class Map3dViewModel : ViewModel() {
108113
if (controller != null) {
109114
launch {
110115
getCameraFlow(controller).collect { camera ->
111-
_currentCamera.value = camera
116+
_currentCamera.tryEmit(camera)
112117
}
113118
}
114119
addMapObjects(mapObjects, controller)
@@ -163,7 +168,7 @@ abstract class Map3dViewModel : ViewModel() {
163168
// Send the new camera position to the flow's channel
164169
trySend(newPosition)
165170
// Also update the private state
166-
_currentCamera.value = newPosition
171+
_currentCamera.tryEmit(newPosition)
167172
}
168173

169174
// Get the current map instance (ensure it's not null before setting listener)
@@ -175,7 +180,8 @@ abstract class Map3dViewModel : ViewModel() {
175180
controller.getCamera()?.let { initial ->
176181
val newPosition = initial.toValidCamera()
177182
trySend(newPosition)
178-
_currentCamera.value = newPosition // Also update private state on collection
183+
// Also update the private state
184+
_currentCamera.tryEmit(newPosition)
179185
}
180186

181187
// The awaitClose block runs when the collector is cancelled

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ThreeDMap.kt renamed to Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/common/ThreeDMap.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package com.example.advancedmaps3dsamples.scenarios
15+
package com.example.advancedmaps3dsamples.common
1616

1717
import androidx.compose.runtime.Composable
1818
import androidx.compose.ui.Modifier
@@ -22,6 +22,18 @@ import com.google.android.gms.maps3d.Map3DOptions
2222
import com.google.android.gms.maps3d.Map3DView
2323
import com.google.android.gms.maps3d.OnMap3DViewReadyCallback
2424

25+
/**
26+
* Composable function that wraps the AndroidView to display a 3D map.
27+
*
28+
* This function handles the creation and lifecycle of a Map3DView.
29+
* It allows for customization through [Map3DOptions] and provides callbacks
30+
* for when the map is ready and when it's being released.
31+
*
32+
* @param options The [Map3DOptions] to configure the map with.
33+
* @param onMap3dViewReady A callback that is invoked when the [GoogleMap3D] instance is ready to be used.
34+
* @param onReleaseMap A callback that is invoked when the map view is being released, allowing for cleanup.
35+
* @param modifier A [Modifier] to be applied to the underlying [AndroidView].
36+
*/
2537
@Composable
2638
internal fun ThreeDMap(
2739
options: Map3DOptions,

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/CameraAttributesScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
3232
import androidx.compose.ui.Modifier
3333
import androidx.compose.ui.res.stringResource
3434
import androidx.compose.ui.unit.dp
35+
import com.example.advancedmaps3dsamples.common.ThreeDMap
3536
import com.example.advancedmaps3dsamples.utils.wrapIn
3637
import com.google.android.gms.maps3d.GoogleMap3D
3738

Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/scenarios/ScenarioScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package com.example.advancedmaps3dsamples.scenarios
1616

1717
import androidx.compose.runtime.Composable
1818
import androidx.compose.ui.Modifier
19+
import com.example.advancedmaps3dsamples.common.ThreeDMap
1920
import com.google.android.gms.maps3d.GoogleMap3D
2021

2122
@Composable
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.example.advancedmaps3dsamples.trails
16+
17+
import androidx.compose.ui.graphics.Color
18+
import androidx.compose.ui.graphics.toArgb
19+
import com.google.android.gms.maps.model.LatLng
20+
import com.google.android.gms.maps3d.model.AltitudeMode
21+
import com.google.android.gms.maps3d.model.LatLngAltitude
22+
import com.google.android.gms.maps3d.model.PolylineOptions
23+
import com.google.android.gms.maps3d.model.polylineOptions
24+
25+
fun Trail.toPolylineOptions() : PolylineOptions {
26+
val coordinates = this.coordinates.map { it.toLatLngAltitude(0.0) }
27+
28+
val color = when (difficulty) {
29+
DifficultyLevel.EASY -> Color.Green
30+
DifficultyLevel.MODERATE -> Color.Blue
31+
DifficultyLevel.DIFFICULT -> Color.Red
32+
}.toArgb()
33+
34+
return polylineOptions {
35+
id = this@toPolylineOptions.segmentId
36+
this.coordinates = coordinates
37+
strokeColor = color
38+
strokeWidth = 7.0
39+
altitudeMode = AltitudeMode.CLAMP_TO_GROUND
40+
zIndex = 5
41+
drawsOccludedSegments = true
42+
}
43+
}
44+
45+
private fun LatLng.toLatLngAltitude(altitude: Double = 0.0) : LatLngAltitude =
46+
LatLngAltitude(this.latitude, this.longitude, altitude)

0 commit comments

Comments
 (0)