Skip to content

Commit 91f360d

Browse files
committed
Refactor: Rename AltitudeSliderDemo to AnimatedPolygon Activity and add literate programming docs
1 parent cf2024a commit 91f360d

14 files changed

Lines changed: 634 additions & 1 deletion

File tree

.gemini/skills/android-maps3d-sdk/SKILL.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,83 @@ suspend fun awaitCameraAnimation(map: GoogleMap3D) = suspendCancellableCoroutine
147147
```
148148

149149
* **Lifecycle:** You must pass lifecycle events down to `Map3DView`. In Compose, `factory` block takes care of instantiation and `onRelease` handles cleanup (`onDestroy()`). Ensure `onCreate` is called in the factory block.
150+
* *Critical Note:* The underlying `GoogleMap3D` engine instance is effectively created once per application lifecycle. If your `AndroidView` Composable leaves the composition and later returns (creating a new `Map3DView`), the underlying 3D engine may still retain previously added objects (like Polygons) from the destroyed view. You must manually clear or track your objects to avoid duplicates across recompositions or Navigation transitions.
151+
* **Initialization & Adding Objects:** Do **not** attempt to set the camera or add 3D objects (like Polygons) immediately after the `GoogleMap3D` reference is ready. The renderer needs time to warm up.
152+
* **Initial Camera:** Always set the initial camera position declaratively via `Map3DOptions` (passed into your container view) rather than imperatively moving the camera after the map loads. This avoids dizzying "flight" animations from coordinate `(0,0)` on startup.
153+
* **Adding Objects:** Only inject geometries into the scene after the map has signaled it is fully ready and stable. Typically, this means waiting for an `onMapSteady` callback.
154+
* **Updating Map Objects:** When updating an existing Map Object (e.g., `Polygon`, `Polyline`), do **not** use `remove()` and re-add a new one, as this causes flickering. Instead, use `getId()` from the existing object and pass it to a new `PolygonOptions` (or equivalent) builder, then call `addPolygon()` with those new options on the same `GoogleMap3D` instance. The SDK uses the matching ID to update the existing object gracefully without flickering.
155+
* **Nullable Camera Properties:** The 3D SDK's `Camera` object has 6 degrees of freedom. Properties like `heading`, `tilt`, `roll`, and `range` are returned as `Double?` (nullable) since the renderer does not always guarantee a value for every property. Handle these nulls defensively when extracting camera telemetry, especially when persisting position data.
156+
* **Parameter Validation:** The Maps 3D library will throw exceptions and crash if passed out-of-bounds telemetry for camera movements or locations. Standardize a validation/coercion layer (e.g., returning a `toValidCamera()` extension object) covering:
157+
* `latitude`: clamped to `[-90.0, 90.0]`
158+
* `longitude`: clamped to `[-180.0, 180.0]`
159+
* `tilt`: clamped to `[0.0, 90.0]`
160+
* `range`: clamped to `[0.0, 63170000.0]`
161+
* `heading`: wrapped to `[0.0, 360.0]`
162+
* `roll`: wrapped to `[-360.0, 360.0]`
163+
* `altitude`: clamped to `[0.0, MAX_ALTITUDE_METERS]`
164+
165+
**Example Extension:**
166+
```kotlin
167+
/** Helper to wrap cyclic values like heading and roll */
168+
fun Double.wrapIn(lower: Double, upper: Double): Double {
169+
val range = upper - lower
170+
if (range <= 0) return this
171+
val offset = this - lower
172+
return lower + (offset - Math.floor(offset / range) * range)
173+
}
174+
175+
/** Extension to sanitize camera telemetry before passing to engine */
176+
fun Camera?.toValidCamera(): Camera {
177+
val source = this ?: return Camera.DEFAULT_CAMERA
178+
return camera {
179+
center = latLngAltitude {
180+
latitude = source.center.latitude.coerceIn(-90.0..90.0)
181+
longitude = source.center.longitude.coerceIn(-180.0..180.0)
182+
altitude = source.center.altitude.coerceIn(0.0..LatLngAltitude.MAX_ALTITUDE_METERS)
183+
}
184+
heading = source.heading?.toDouble()?.wrapIn(0.0, 360.0) ?: 0.0
185+
tilt = source.tilt?.toDouble()?.coerceIn(0.0..90.0) ?: 60.0
186+
roll = source.roll?.toDouble()?.wrapIn(-360.0, 360.0) ?: 0.0
187+
range = source.range?.toDouble()?.coerceIn(0.0..63170000.0) ?: 1500.0
188+
}
189+
}
190+
```
191+
192+
* **Immutable Updates (`copy` Extensions):** The 3D SDK builders (like `camera {}` or `latLngAltitude {}`) do not natively provide a `copy()` method like Kotlin data classes. To gracefully update a single property (like altitude) while retaining the rest of the object's complex state, implement custom `.copy()` extensions:
193+
194+
```kotlin
195+
/** Extension to clone and modify a Camera */
196+
fun Camera.copy(
197+
center: LatLngAltitude? = null,
198+
heading: Double? = null,
199+
tilt: Double? = null,
200+
range: Double? = null,
201+
roll: Double? = null,
202+
): Camera {
203+
val objectToCopy = this
204+
return camera {
205+
this.center = center ?: objectToCopy.center
206+
this.heading = heading ?: objectToCopy.heading
207+
this.tilt = tilt ?: objectToCopy.tilt
208+
this.range = range ?: objectToCopy.range
209+
this.roll = roll ?: objectToCopy.roll
210+
}
211+
}
212+
213+
/** Extension to clone and modify a LatLngAltitude */
214+
fun LatLngAltitude.copy(
215+
latitude: Double? = null,
216+
longitude: Double? = null,
217+
altitude: Double? = null,
218+
): LatLngAltitude {
219+
val objectToCopy = this
220+
return latLngAltitude {
221+
this.latitude = latitude ?: objectToCopy.latitude
222+
this.longitude = longitude ?: objectToCopy.longitude
223+
this.altitude = altitude ?: objectToCopy.altitude
224+
}
225+
}
226+
```
150227
151228
## 5. Execution Steps
152229
1. Add the 3D Maps SDK dependencies.

Dump.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import com.google.android.gms.maps3d.GoogleMap3D
2+
fun main() {
3+
GoogleMap3D::class.java.methods.forEach { println(it.name) }
4+
}

Maps3DSamples/advanced/app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ android {
126126
dependencies {
127127

128128
implementation(libs.androidx.core.ktx)
129+
implementation(libs.androidx.datastore.preferences)
129130
implementation(libs.androidx.lifecycle.runtime.ktx)
130131
implementation(libs.androidx.activity.compose)
131132
implementation(platform(libs.androidx.compose.bom))

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
android:name=".scenarios.ScenariosActivity"
5151
android:exported="true"
5252
/>
53+
<activity
54+
android:name=".animatedpolygon.AnimatedPolygonActivity"
55+
android:exported="true"
56+
android:theme="@style/Theme.AdvancedMaps3DSamples"
57+
/>
5358
</application>
5459

5560
</manifest>

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.animatedpolygon.AnimatedPolygonActivity
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.scenarios_altitude_slider, AnimatedPolygonActivity::class.java),
5153
)
5254

5355
@OptIn(ExperimentalMaterial3Api::class)

0 commit comments

Comments
 (0)