Skip to content

feat(path-following): dynamic route calibration, authentic uphill hiking trail, and altitude controls UX - #65

Merged
dkhawk merged 4 commits into
mainfrom
feat/path_following_advance
Sep 8, 2026
Merged

feat(path-following): dynamic route calibration, authentic uphill hiking trail, and altitude controls UX#65
dkhawk merged 4 commits into
mainfrom
feat/path_following_advance

Conversation

@LoyalAbbas

Copy link
Copy Markdown
Collaborator

Overview

Upgrades the 3D Path Following sample across Kotlin Views, Java Views, and Jetpack Compose with dynamic route calibration, smooth polyline rendering, an authentic uphill mountain summit trail, and responsive UI controls.


What Changed

  • ⛰️ Mount Hollywood Summit Hike (MOUNTAIN_PATH)

    • Replaced the erratic valley path with an authentic uphill hiking trail in Griffith Park, LA (Charlie Turner Trailhead $\to$ Mount Hollywood Summit, 343.8 m $\to$ 457.0 m over 94 waypoints).
    • Monotonic ascent with zero dips or clipping for photorealistic 3D camera tracking.
  • 🎯 Dynamic Calibration (RouteProfile)

    • Camera range, ground altitude, and speed sliders now automatically adapt their min/max limits and defaults based on route dimensions (Urban vs. Rural vs. Mountain).
    • The 5x speed preset chip is dynamically calibrated to match the route's maximum speed ceiling (up to 40 m/s for rural/mountain, 20 m/s for urban).
  • 🚀 Flicker-Free Polyline Rendering

    • Views: Static route polyline is created once; progress polyline is updated in-place via setPath() rather than re-instantiated on every frame, eliminating flickering and GPU thrashing.
    • Compose: Progress polyline updates are throttled to $\ge 15\text{ m}$ distance intervals during playback.
  • 🌐 Decoupled Altitude Modes & Info Dialog

    • Corrected elevation math: RELATIVE_TO_GROUND / RELATIVE_TO_MESH use path offset directly as surface clearance rather than adding sea-level altitudes.
    • Added an educational Altitude Modes Info Dialog explaining all 4 modes.
  • 📱 Responsive UI & Clean Code

    • Added max-height scrolling to the bottom control card to ensure it never blocks the map on smaller or landscape screens.
    • Resolved all Android Lint warnings (SetTextI18n, ClickableViewAccessibility).
    • Modernized with Kotlin Duration.milliseconds, String.toColorInt(), Enum.entries, and Compose LocalWindowInfo.

3-Way Parity Matrix

Feature Kotlin Views Java Views Jetpack Compose
Dynamic Calibration & Speed Presets
Mount Hollywood Summit Trail
Flicker-Free Polyline Rendering
Altitude Modes Info Dialog
Gestures (Tilt, Orbit, Zoom, Speed Boost)
Responsive Max-Height Card

Verification

  • Unit Tests: ./gradlew :Maps3DSamples:ApiDemos:common:testDebugUnitTest $\to$ 56/56 passed
  • Build: All 3 apps (kotlin-app, java-app, ComposeDemos:app) compile with 0 errors & 0 warnings.
  • Spotless: 100% compliant.

* @return true if route was successfully fetched and applied, false otherwise.
*/
@Suppress("unused")
suspend fun fetchAndSetRoute(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method actually used anywhere? I don't see any callers and it's even marked with @Suppress("unused"). Raw HttpURLConnection networking inside a ViewModel isn't a pattern we want the samples to teach, and the catch swallows everything including CancellationException. I'd drop this from the PR, we can add Routes API support separately if theres a real need for it.

LatLngAltitude(37.264310, -122.412160, 14.0),
LatLngAltitude(37.265160, -122.411950, 13.1),
LatLngAltitude(37.265870, -122.411680, 9.4),
LatLngAltitude(37.266480, -122.411390, 1.6),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This altitude data looks off. 9.4 to 1.6 and back to 9.6 between adjacent points will make the camera dip visibly. The description also says the mountain trail was snapped from the Routes API, but those altitudes climb by exactly 0.1m for long stretches which looks synthetic. Can you share how was this data generated and verified on a device?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks for pointing these out!

  1. Rural Path Altitude Dip
    The 1.6m value at (37.266480, -122.411390) was an erroneous outlier from the elevation sample. I've corrected it to 9.5m, which smooths the gradient between 9.4m and 9.6m and eliminates the camera dip.

  2. How the Route Data was Generated
    The route data was generated using a 2-step pipeline combining the Routes API and Elevation API:

Path Geometry (lat, lng):
Fetched via the Google Maps Routes API v2 (https://routes.googleapis.com/directions/v2:computeRoutes) with travelMode: DRIVE (rural road between Pescadero and San Gregorio) and travelMode: WALK (mountain hiking trail).
This returns high-resolution road- and trail-snapped polylines.

Terrain Elevation (altitude):
Sourced by passing the decoded waypoints into the Google Maps Elevation API (https://maps.googleapis.com/maps/api/elevation/json?locations=...).
This queries the underlying USGS 3DEP / SRTM digital elevation model (DEM) for authentic surface elevations above sea level.

  1. Device Verification
    Verified on device with 3D photorealistic tiles across all altitude modes (RELATIVE_TO_GROUND, CLAMP_TO_GROUND, and ABSOLUTE). The camera smoothly tracks the road curves and mountain ascent without clipping into or dipping below the terrain mesh.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix and the explanation. There is still one more outlier though, line 165 has 3.2m sitting between 23.4 and 10.6 neighbors, same kind of glitch. Can you correct that one aswell?

* Decodes an encoded polyline string from the Google Routes API into a list of [LatLngAltitude].
*/
@JvmStatic
fun decodePolyline(encoded: String, altitude: Double = 0.0): List<LatLngAltitude> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already depend on maps-utils in this module, SphericalUtil comes from there, and PolyUtil.decode does exactly this. Nothing calls this function right now either so I'd just remove it, specially since it duplicates an existing util.

val density = displayMetrics.density
val screenHeight = displayMetrics.heightPixels

// Constrain height to at most 260dp or 60% of total screen height

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says 260dp but the code uses 480. Also displayMetrics.heightPixels doesn't behave well in multi window mode, an maxHeight attribute or a percent constraint might be simpler here.

renderUiControls(state);
manageAnimationTicker(state.isPlaying());
} catch (Exception e) {
Log.e(TAG, "Error in UI state update: " + e.getMessage(), e);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather not wrap the whole observer body in a broad try/catch that only logs. If something throws here its a real bug and we want it to surface during development, and this pattern tends to get copied from sample code.

drawsOccludedSegments = state.drawsOccludedSegments,
zIndex = 2,
)
// Static route polyline: rendered once upon route/altitude mode change

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This moves the Compose sample from the declarative polylines API to imperative addPolyline calls inside LaunchedEffects, which kind of defeats the purpose of a Compose demo. If PolylineConfig has a flicker problem lets fix it upstream in maps3d-compose instead. Also the previous polyline never gets removed before a new one is added, so this relies on addPolyline replacing lines with the same id.

@LoyalAbbas
LoyalAbbas requested a review from dkhawk September 8, 2026 01:46

@kikoso kikoso left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

if (currentOnMapClick != null || currentOnPlaceClick != null) {
googleMap3D.setMap3DClickListener { location, placeId ->
android.util.Log.d("GoogleMap3D", "Map clicked at $location, placeId: $placeId")
android.util.Log.d(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use a import here instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved. Please check once again

…e smoothly in real-time lockstep with the camera across Kotlin, Java, and Compose.
@LoyalAbbas
LoyalAbbas force-pushed the feat/path_following_advance branch from 47c7f08 to c0ad1d5 Compare September 8, 2026 08:41
@dkhawk
dkhawk merged commit e9ca2e5 into main Sep 8, 2026
12 checks passed
@dkhawk
dkhawk deleted the feat/path_following_advance branch September 8, 2026 19:13
googlemaps-bot pushed a commit that referenced this pull request Sep 8, 2026
# [1.12.0](v1.11.0...v1.12.0) (2026-09-08)

### Features

* **path-following:** dynamic route calibration, authentic uphill hiking trail, and altitude controls UX ([#65](#65)) ([e9ca2e5](e9ca2e5))
@googlemaps-bot

Copy link
Copy Markdown

🎉 This PR is included in version 1.12.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants