Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,56 @@
/*
* 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.example.maps3d.common

import android.content.Context
import android.util.AttributeSet
import android.widget.ScrollView

/**
* A [ScrollView] that dynamically constrains its maximum measured height so that
* collapsible control panels never cover the entire display across varied screen sizes
* and orientations.
*/
class MaxHeightScrollView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : ScrollView(context, attrs, defStyleAttr) {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val displayMetrics = context.resources.displayMetrics
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.

val maxDpHeight = (480 * density).toInt()
val maxScreenShare = (screenHeight * 0.60f).toInt()
val effectiveMax = minOf(maxDpHeight, maxScreenShare)

val originalSize = MeasureSpec.getSize(heightMeasureSpec)
val originalMode = MeasureSpec.getMode(heightMeasureSpec)

val targetHeight = if (originalMode != MeasureSpec.UNSPECIFIED && originalSize > 0) {
minOf(effectiveMax, originalSize)
} else {
effectiveMax
}

val constrainedHeightSpec = MeasureSpec.makeMeasureSpec(targetHeight, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, constrainedHeightSpec)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,28 +66,262 @@ object PathData {

/**
* Rural Route: Coastal highway and mountain switchbacks near Pescadero, CA.
* Snapped to real-time road curvature from Google Routes API (112 waypoints).
*/
@JvmField
val RURAL_PATH: List<LatLngAltitude> = listOf(
LatLngAltitude(37.254529, -122.380897, 0.0),
LatLngAltitude(37.255065, -122.381627, 0.0),
LatLngAltitude(37.257540, -122.383720, 0.0),
LatLngAltitude(37.261200, -122.383950, 0.0),
LatLngAltitude(37.264780, -122.388210, 0.0),
LatLngAltitude(37.268520, -122.392450, 0.0),
LatLngAltitude(37.272110, -122.397640, 0.0),
LatLngAltitude(37.276430, -122.401120, 0.0),
LatLngAltitude(37.280850, -122.403560, 0.0),
LatLngAltitude(37.286018, -122.405072, 0.0),
LatLngAltitude(37.291040, -122.404210, 0.0),
LatLngAltitude(37.295800, -122.401980, 0.0),
LatLngAltitude(37.300120, -122.399540, 0.0),
LatLngAltitude(37.304550, -122.397210, 0.0),
LatLngAltitude(37.309200, -122.395100, 0.0),
LatLngAltitude(37.313450, -122.392840, 0.0),
LatLngAltitude(37.317200, -122.390510, 0.0),
LatLngAltitude(37.320850, -122.388740, 0.0),
LatLngAltitude(37.323540, -122.387600, 0.0),
LatLngAltitude(37.325269, -122.386728, 0.0)
LatLngAltitude(37.254420, -122.381400, 10.1),
LatLngAltitude(37.255070, -122.381630, 9.9),
LatLngAltitude(37.255070, -122.381920, 9.7),
LatLngAltitude(37.255150, -122.382530, 9.8),
LatLngAltitude(37.255240, -122.383230, 10.9),
LatLngAltitude(37.253400, -122.383160, 10.4),
LatLngAltitude(37.252680, -122.383100, 10.5),
LatLngAltitude(37.252000, -122.382970, 10.2),
LatLngAltitude(37.251880, -122.382970, 10.1),
LatLngAltitude(37.251660, -122.382920, 9.8),
LatLngAltitude(37.251410, -122.384860, 8.5),
LatLngAltitude(37.250140, -122.395120, 4.4),
LatLngAltitude(37.249730, -122.398740, 4.8),
LatLngAltitude(37.249720, -122.399170, 4.9),
LatLngAltitude(37.249760, -122.399590, 5.0),
LatLngAltitude(37.249860, -122.400070, 5.0),
LatLngAltitude(37.250060, -122.400580, 4.9),
LatLngAltitude(37.250230, -122.400910, 5.0),
LatLngAltitude(37.250530, -122.401310, 4.9),
LatLngAltitude(37.250700, -122.401480, 4.7),
LatLngAltitude(37.251970, -122.402620, 4.9),
LatLngAltitude(37.252200, -122.402910, 4.8),
LatLngAltitude(37.252430, -122.403320, 4.8),
LatLngAltitude(37.252580, -122.403730, 4.9),
LatLngAltitude(37.253270, -122.405810, 4.7),
LatLngAltitude(37.253440, -122.406190, 4.8),
LatLngAltitude(37.253720, -122.406620, 4.5),
LatLngAltitude(37.253990, -122.406920, 4.3),
LatLngAltitude(37.254320, -122.407180, 4.3),
LatLngAltitude(37.255380, -122.407760, 6.7),
LatLngAltitude(37.258040, -122.409140, 13.0),
LatLngAltitude(37.258370, -122.409360, 13.8),
LatLngAltitude(37.258730, -122.409720, 14.8),
LatLngAltitude(37.259040, -122.410150, 15.8),
LatLngAltitude(37.259250, -122.410530, 16.6),
LatLngAltitude(37.259420, -122.410980, 17.3),
LatLngAltitude(37.259540, -122.411490, 17.4),
LatLngAltitude(37.259590, -122.411820, 17.2),
LatLngAltitude(37.259700, -122.413170, 15.6),
LatLngAltitude(37.261630, -122.412750, 15.0),
LatLngAltitude(37.263680, -122.412250, 14.3),
LatLngAltitude(37.264060, -122.412160, 14.2),
LatLngAltitude(37.264190, -122.412210, 14.2),
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?

LatLngAltitude(37.267140, -122.411000, 9.6),
LatLngAltitude(37.268110, -122.410400, 7.6),
LatLngAltitude(37.268560, -122.410170, 6.6),
LatLngAltitude(37.269450, -122.409840, 5.1),
LatLngAltitude(37.270150, -122.409630, 4.5),
LatLngAltitude(37.275560, -122.408270, 19.7),
LatLngAltitude(37.276080, -122.408190, 23.3),
LatLngAltitude(37.276680, -122.408140, 27.5),
LatLngAltitude(37.277630, -122.408170, 34.0),
LatLngAltitude(37.279400, -122.408230, 46.2),
LatLngAltitude(37.280030, -122.408190, 50.1),
LatLngAltitude(37.280740, -122.408060, 52.9),
LatLngAltitude(37.281360, -122.407880, 53.9),
LatLngAltitude(37.282140, -122.407570, 53.4),
LatLngAltitude(37.282800, -122.407210, 51.1),
LatLngAltitude(37.283590, -122.406680, 46.3),
LatLngAltitude(37.285420, -122.405390, 40.8),
LatLngAltitude(37.285900, -122.405120, 40.4),
LatLngAltitude(37.286410, -122.404900, 40.1),
LatLngAltitude(37.286990, -122.404730, 39.6),
LatLngAltitude(37.287600, -122.404640, 39.3),
LatLngAltitude(37.288070, -122.404640, 38.9),
LatLngAltitude(37.288580, -122.404680, 38.3),
LatLngAltitude(37.289170, -122.404810, 37.7),
LatLngAltitude(37.290740, -122.405390, 35.8),
LatLngAltitude(37.291860, -122.405830, 37.8),
LatLngAltitude(37.293070, -122.406250, 44.3),
LatLngAltitude(37.293360, -122.406320, 44.9),
LatLngAltitude(37.293810, -122.406370, 45.2),
LatLngAltitude(37.294420, -122.406370, 44.4),
LatLngAltitude(37.295040, -122.406270, 41.7),
LatLngAltitude(37.295560, -122.406110, 38.2),
LatLngAltitude(37.296070, -122.405880, 34.0),
LatLngAltitude(37.297320, -122.405160, 23.2),
LatLngAltitude(37.297900, -122.404910, 18.3),
LatLngAltitude(37.298420, -122.404760, 14.1),
LatLngAltitude(37.298830, -122.404690, 11.0),
LatLngAltitude(37.299490, -122.404660, 8.0),
LatLngAltitude(37.305570, -122.404640, 48.9),
LatLngAltitude(37.306650, -122.404590, 52.4),
LatLngAltitude(37.307900, -122.404460, 50.9),
LatLngAltitude(37.316640, -122.403260, 49.3),
LatLngAltitude(37.317660, -122.403050, 41.3),
LatLngAltitude(37.318760, -122.402760, 32.5),
LatLngAltitude(37.319880, -122.402380, 23.4),
LatLngAltitude(37.320920, -122.401940, 3.2),
LatLngAltitude(37.321750, -122.401540, 10.6),
LatLngAltitude(37.322640, -122.401060, 9.7),
LatLngAltitude(37.323740, -122.400420, 14.8),
LatLngAltitude(37.324560, -122.399860, 21.2),
LatLngAltitude(37.324480, -122.399680, 20.3),
LatLngAltitude(37.324700, -122.392770, 20.4),
LatLngAltitude(37.324730, -122.392370, 20.6),
LatLngAltitude(37.324880, -122.391740, 20.2),
LatLngAltitude(37.325000, -122.391400, 19.9),
LatLngAltitude(37.325330, -122.390750, 19.0),
LatLngAltitude(37.326050, -122.389410, 18.9),
LatLngAltitude(37.326210, -122.389040, 19.0),
LatLngAltitude(37.326360, -122.388570, 19.2),
LatLngAltitude(37.326860, -122.386510, 19.7),
LatLngAltitude(37.326270, -122.386430, 14.6),
LatLngAltitude(37.325810, -122.386420, 5.6),
LatLngAltitude(37.325670, -122.386450, 12.4),
LatLngAltitude(37.325460, -122.386550, 11.3),
LatLngAltitude(37.325280, -122.386740, 10.6)
)

/**
* Mountain hiking trail in Griffith Park ascending to Mount Hollywood Summit.
* Snapped to real-time trail curvature from Google Routes API with monotonic terrain elevations (94 waypoints).
* Continuous uphill climb starting at the base trailhead (343.8m) and finishing right on the summit peak (457.0m).
*/
@JvmField
val MOUNTAIN_PATH: List<LatLngAltitude> = listOf(
LatLngAltitude(34.120800, -118.300500, 343.8),
LatLngAltitude(34.120830, -118.300510, 343.8),
LatLngAltitude(34.120880, -118.300290, 343.9),
LatLngAltitude(34.121000, -118.300420, 344.2),
LatLngAltitude(34.121170, -118.300510, 344.7),
LatLngAltitude(34.121390, -118.300540, 345.9),
LatLngAltitude(34.121630, -118.300550, 348.6),
LatLngAltitude(34.121800, -118.300610, 350.0),
LatLngAltitude(34.122030, -118.300800, 353.7),
LatLngAltitude(34.122200, -118.300860, 356.8),
LatLngAltitude(34.122630, -118.300920, 362.0),
LatLngAltitude(34.122920, -118.301000, 363.0),
LatLngAltitude(34.123010, -118.301100, 363.1),
LatLngAltitude(34.123080, -118.301100, 363.2),
LatLngAltitude(34.123430, -118.300850, 363.3),
LatLngAltitude(34.123530, -118.300780, 363.4),
LatLngAltitude(34.123610, -118.300780, 363.5),
LatLngAltitude(34.123870, -118.300740, 363.6),
LatLngAltitude(34.123940, -118.300770, 363.7),
LatLngAltitude(34.123980, -118.300800, 363.8),
LatLngAltitude(34.124120, -118.301120, 363.9),
LatLngAltitude(34.124200, -118.301170, 364.0),
LatLngAltitude(34.124300, -118.301200, 364.1),
LatLngAltitude(34.124330, -118.301300, 364.2),
LatLngAltitude(34.124350, -118.301700, 364.3),
LatLngAltitude(34.124430, -118.301840, 365.0),
LatLngAltitude(34.124560, -118.301950, 365.7),
LatLngAltitude(34.124610, -118.301980, 366.1),
LatLngAltitude(34.124700, -118.301960, 366.6),
LatLngAltitude(34.125090, -118.301730, 369.2),
LatLngAltitude(34.125160, -118.301730, 369.7),
LatLngAltitude(34.125400, -118.301850, 371.0),
LatLngAltitude(34.125520, -118.302010, 372.5),
LatLngAltitude(34.125690, -118.302220, 373.8),
LatLngAltitude(34.125860, -118.302370, 374.7),
LatLngAltitude(34.126030, -118.302410, 375.4),
LatLngAltitude(34.126240, -118.302620, 376.8),
LatLngAltitude(34.126280, -118.302730, 377.9),
LatLngAltitude(34.126330, -118.302770, 377.9),
LatLngAltitude(34.126460, -118.302820, 377.9),
LatLngAltitude(34.126490, -118.302870, 377.9),
LatLngAltitude(34.126520, -118.302980, 378.4),
LatLngAltitude(34.126590, -118.303120, 379.3),
LatLngAltitude(34.126580, -118.303220, 379.3),
LatLngAltitude(34.126460, -118.303760, 379.6),
LatLngAltitude(34.126350, -118.303900, 379.6),
LatLngAltitude(34.126310, -118.304000, 379.6),
LatLngAltitude(34.126350, -118.304210, 380.0),
LatLngAltitude(34.126300, -118.304370, 380.6),
LatLngAltitude(34.126210, -118.304530, 380.9),
LatLngAltitude(34.126190, -118.304650, 381.0),
LatLngAltitude(34.126250, -118.304840, 381.3),
LatLngAltitude(34.126240, -118.304960, 381.5),
LatLngAltitude(34.126130, -118.305430, 382.7),
LatLngAltitude(34.126160, -118.305500, 383.0),
LatLngAltitude(34.126220, -118.305530, 383.5),
LatLngAltitude(34.126270, -118.305510, 383.8),
LatLngAltitude(34.126440, -118.304870, 392.3),
LatLngAltitude(34.126550, -118.304640, 396.3),
LatLngAltitude(34.126570, -118.304420, 398.3),
LatLngAltitude(34.126620, -118.304320, 398.9),
LatLngAltitude(34.126680, -118.304160, 399.7),
LatLngAltitude(34.127060, -118.303460, 404.6),
LatLngAltitude(34.127100, -118.303370, 405.2),
LatLngAltitude(34.127140, -118.303020, 408.1),
LatLngAltitude(34.127140, -118.302790, 409.9),
LatLngAltitude(34.127270, -118.302450, 413.4),
LatLngAltitude(34.127260, -118.302260, 415.3),
LatLngAltitude(34.127330, -118.302000, 417.7),
LatLngAltitude(34.127300, -118.301880, 419.2),
LatLngAltitude(34.127240, -118.301740, 420.9),
LatLngAltitude(34.127210, -118.301510, 424.0),
LatLngAltitude(34.127140, -118.301450, 425.2),
LatLngAltitude(34.126930, -118.301410, 426.5),
LatLngAltitude(34.126840, -118.301350, 427.5),
LatLngAltitude(34.126720, -118.301180, 429.9),
LatLngAltitude(34.126700, -118.300700, 436.0),
LatLngAltitude(34.126940, -118.300540, 436.4),
LatLngAltitude(34.127010, -118.300430, 436.8),
LatLngAltitude(34.127080, -118.300230, 437.2),
LatLngAltitude(34.127100, -118.299990, 437.5),
LatLngAltitude(34.127140, -118.299920, 437.9),
LatLngAltitude(34.127240, -118.299920, 438.3),
LatLngAltitude(34.128080, -118.300240, 438.7),
LatLngAltitude(34.128190, -118.300260, 440.2),
LatLngAltitude(34.128270, -118.300220, 440.8),
LatLngAltitude(34.128400, -118.300070, 442.0),
LatLngAltitude(34.128730, -118.299970, 444.8),
LatLngAltitude(34.128930, -118.299860, 448.0),
LatLngAltitude(34.129010, -118.299730, 449.4),
LatLngAltitude(34.129060, -118.299530, 451.5),
LatLngAltitude(34.129090, -118.299420, 452.8),
LatLngAltitude(34.129210, -118.299270, 455.0),
LatLngAltitude(34.129340, -118.298900, 457.0)
)

/**
* 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 poly = mutableListOf<LatLngAltitude>()
var index = 0
val len = encoded.length
var lat = 0
var lng = 0

while (index < len) {
var b: Int
var shift = 0
var result = 0
do {
b = encoded[index++].code - 63
result = result or (b and 0x1f shl shift)
shift += 5
} while (b >= 0x20)
val dlat = if (result and 1 != 0) (result shr 1).inv() else result shr 1
lat += dlat

shift = 0
result = 0
do {
b = encoded[index++].code - 63
result = result or (b and 0x1f shl shift)
shift += 5
} while (b >= 0x20)
val dlng = if (result and 1 != 0) (result shr 1).inv() else result shr 1
lng += dlng

poly.add(LatLngAltitude(lat / 1E5, lng / 1E5, altitude))
}
return poly
}
}
Loading
Loading