Skip to content
Draft
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
6 changes: 6 additions & 0 deletions Maps3DSamples/advanced/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@
android:name=".scenarios.ScenariosActivity"
android:exported="true"
/>

<activity
android:name=".trails.TrailsActivity"
android:exported="true"
/>

</application>

</manifest>
597 changes: 597 additions & 0 deletions Maps3DSamples/advanced/app/src/main/assets/OSMP_Trails.geojson

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions Maps3DSamples/advanced/app/src/main/assets/OSMP_head.geojson

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Maps3DSamples/advanced/app/src/main/assets/OSMP_test.geojson
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "FeatureCollection",
"name": "OSMP_Trails",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "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 ] ] } },
{ "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 ] ] } },
{ "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 ] ] } },
{ "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 ] ] } },
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.advancedmaps3dsamples.scenarios.ScenariosActivity
import com.example.advancedmaps3dsamples.trails.TrailsActivity
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
import dagger.hilt.android.AndroidEntryPoint

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

@OptIn(ExperimentalMaterial3Api::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlin.time.Duration

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

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

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

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

private val activeMapObjects = mutableMapOf<String, ActiveMapObject>()
internal val activeMapObjects = mutableMapOf<String, ActiveMapObject>()

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

Expand All @@ -108,7 +113,7 @@ abstract class Map3dViewModel : ViewModel() {
if (controller != null) {
launch {
getCameraFlow(controller).collect { camera ->
_currentCamera.value = camera
_currentCamera.tryEmit(camera)
}
}
addMapObjects(mapObjects, controller)
Expand Down Expand Up @@ -163,7 +168,7 @@ abstract class Map3dViewModel : ViewModel() {
// Send the new camera position to the flow's channel
trySend(newPosition)
// Also update the private state
_currentCamera.value = newPosition
_currentCamera.tryEmit(newPosition)
}

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

// The awaitClose block runs when the collector is cancelled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

package com.example.advancedmaps3dsamples.scenarios
package com.example.advancedmaps3dsamples.common

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
Expand All @@ -22,6 +22,18 @@ import com.google.android.gms.maps3d.Map3DOptions
import com.google.android.gms.maps3d.Map3DView
import com.google.android.gms.maps3d.OnMap3DViewReadyCallback

/**
* Composable function that wraps the AndroidView to display a 3D map.
*
* This function handles the creation and lifecycle of a Map3DView.
* It allows for customization through [Map3DOptions] and provides callbacks
* for when the map is ready and when it's being released.
*
* @param options The [Map3DOptions] to configure the map with.
* @param onMap3dViewReady A callback that is invoked when the [GoogleMap3D] instance is ready to be used.
* @param onReleaseMap A callback that is invoked when the map view is being released, allowing for cleanup.
* @param modifier A [Modifier] to be applied to the underlying [AndroidView].
*/
@Composable
internal fun ThreeDMap(
options: Map3DOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.example.advancedmaps3dsamples.common.ThreeDMap
import com.example.advancedmaps3dsamples.utils.wrapIn
import com.google.android.gms.maps3d.GoogleMap3D

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package com.example.advancedmaps3dsamples.scenarios

import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.example.advancedmaps3dsamples.common.ThreeDMap
import com.google.android.gms.maps3d.GoogleMap3D

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2025 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.example.advancedmaps3dsamples.trails

import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import com.google.android.gms.maps.model.LatLng
import com.google.android.gms.maps3d.model.AltitudeMode
import com.google.android.gms.maps3d.model.LatLngAltitude
import com.google.android.gms.maps3d.model.PolylineOptions
import com.google.android.gms.maps3d.model.polylineOptions

fun Trail.toPolylineOptions() : PolylineOptions {
val coordinates = this.coordinates.map { it.toLatLngAltitude(0.0) }

val color = when (difficulty) {
DifficultyLevel.EASY -> Color.Green
DifficultyLevel.MODERATE -> Color.Blue
DifficultyLevel.DIFFICULT -> Color.Red
}.toArgb()

return polylineOptions {
id = [email protected]
this.coordinates = coordinates
strokeColor = color
strokeWidth = 7.0
altitudeMode = AltitudeMode.CLAMP_TO_GROUND
zIndex = 5
drawsOccludedSegments = true
}
}

private fun LatLng.toLatLngAltitude(altitude: Double = 0.0) : LatLngAltitude =
LatLngAltitude(this.latitude, this.longitude, altitude)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2025 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.example.advancedmaps3dsamples.trails

import com.google.android.gms.maps.model.LatLng

data class Trail(
val id: Int = -1,
val name: String = "",
val type: TrailType = TrailType.HIKING,
val difficulty: DifficultyLevel = DifficultyLevel.EASY,
val mileage: Double = 0.0,
val measuredFeet: Int = 0,
val dogsAllowed: Boolean = false,
val dogRegulations: DogRegulation = DogRegulation.NO_DOGS_ALLOWED,
val status: TrailStatus = TrailStatus.OPEN,
val coordinates: List<LatLng> = emptyList(),
val dogRegulationDescription: String = "",
val segmentId: String = "",
val bicyclesAllowed: Boolean = false,
val eBikesAllowed: Boolean = false,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2025 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.example.advancedmaps3dsamples.trails

enum class TrailType {
HIKING,
BIKING,
MULTI_USE,
EQUESTRIAN;
}

enum class DifficultyLevel {
EASY,
MODERATE,
DIFFICULT;
}

enum class DogRegulation {
LEASH_REQUIRED,
VOICE_AND_SIGHT_CONTROL,
REGULATION_VARIES,
NO_DOGS_ALLOWED;
}

enum class TrailStatus {
OPEN,
CLOSED;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2025 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.example.advancedmaps3dsamples.trails

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import com.example.advancedmaps3dsamples.common.ThreeDMap
import com.example.advancedmaps3dsamples.ui.theme.AdvancedMaps3DSamplesTheme
import com.google.android.gms.maps3d.GoogleMap3D
import com.google.android.gms.maps3d.Map3DOptions
import dagger.hilt.android.AndroidEntryPoint

@AndroidEntryPoint
class TrailsActivity : ComponentActivity() {
private val viewModel by viewModels<TrailsViewModel>()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

enableEdgeToEdge()

val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE

setContent {
val viewState by viewModel.viewState.collectAsStateWithLifecycle()

AdvancedMaps3DSamplesTheme(
dynamicColor = false
) {
when (val vs = viewState) {
ViewState.Loading -> LoadingScreen(modifier = Modifier.fillMaxSize())
is ViewState.TrailMap -> MapScreen(
options = vs.options,
onMap3dViewReady = { viewModel.setGoogleMap3D(it) },
onReleaseMap = { viewModel.releaseGoogleMap3D() },
modifier = Modifier.fillMaxSize(),
)
}
}
}
}
}

@Composable
fun MapScreen(
options: Map3DOptions,
onMap3dViewReady: (GoogleMap3D) -> Unit,
onReleaseMap: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(modifier = modifier) {
ThreeDMap(
modifier = Modifier.fillMaxSize(),
options = options,
onMap3dViewReady = onMap3dViewReady,
onReleaseMap = onReleaseMap,
)
}
}

@Composable
fun LoadingScreen(modifier: Modifier) {
Box(modifier = modifier) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
CircularProgressIndicator()
Text(text = "Loading...")
}
}
}
Loading