Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ class GoogleMapRenderer(
// making the common stop-only update an O(1) identity check; equal republished values are retained too.
private val routePolylines = mutableListOf<Polyline>()
private var renderedRoutePolylines: List<RoutePolyline> = emptyList()
private var renderedRouteWidthScale: Float? = null
private var renderedRouteWidths: List<Float> = emptyList()

// The dynamic layer, tracked by identity so [renderDynamic] can move markers in place: route vehicles
// keyed by active trip id, and the band's (interaction-free) polylines re-added each frame. The
Expand Down Expand Up @@ -244,7 +244,12 @@ class GoogleMapRenderer(
clearStatic()

stopMarkerLayer.render(snapshot.stops, snapshot.focusedStopId, snapshot.stopBand)
routeStopLayer.render(snapshot.stops, snapshot.focusedStopId, map.cameraPosition.zoom)
routeStopLayer.render(
snapshot.stops,
snapshot.focusedStopId,
snapshot.routeStopsScaleWithZoom,
map.cameraPosition.zoom,
)

if (snapshot.bikeshareVisible) {
val band = bikeZoomBand(map.cameraPosition.zoom)
Expand Down Expand Up @@ -298,26 +303,29 @@ class GoogleMapRenderer(
if (renderedRoutePolylines === next || renderedRoutePolylines == next) return

val previousNative = routePolylines.toList()
val previousWidths = renderedRouteWidths
val reconciliation = reconcileEqualItems(renderedRoutePolylines, next)
reconciliation.removedPreviousIndices.forEach { previousNative[it].remove() }
renderedRoutePolylines = next
val widthScale = routeLineWidthScale(map.cameraPosition.zoom)
// Retained natives carry the previously stamped scale; bring them to the new one before
// recording it, or a later onCameraSettled at this zoom would skip the resize they still need.
val retainedStale = renderedRouteWidthScale != widthScale
renderedRouteWidthScale = widthScale
val nextWidths = next.map { routeWidthPx(it, map.cameraPosition.zoom) }
val reconciled = next.mapIndexed { index, polyline ->
reconciliation.previousIndexForNext[index]?.let(previousNative::get)
?.also { if (retainedStale) it.width = widthPx(polyline) * widthScale }
?: addRoutePolyline(polyline, widthScale)
val previousIndex = reconciliation.previousIndexForNext[index]
previousIndex?.let(previousNative::get)
?.also {
if (previousWidths.getOrNull(previousIndex) != nextWidths[index]) {
it.width = nextWidths[index]
}
}
?: addRoutePolyline(polyline, nextWidths[index])
}
renderedRoutePolylines = next
renderedRouteWidths = nextWidths
routePolylines.clear()
routePolylines.addAll(reconciled)
}

private fun addRoutePolyline(polyline: RoutePolyline, widthScale: Float): Polyline {
private fun addRoutePolyline(polyline: RoutePolyline, widthPx: Float): Polyline {
val options = PolylineOptions()
.width(widthPx(polyline) * widthScale)
.width(widthPx)
.addPoints(polyline.points)
.applyDashPattern(polyline)
if (polyline.directional) {
Expand Down Expand Up @@ -345,7 +353,7 @@ class GoogleMapRenderer(
val polyline = continuation.polyline
val line = PolylineOptions()
.color(polyline.resolvedColor)
.width(widthPx(polyline))
.width(routeWidthPx(polyline, map.cameraPosition.zoom))
.addPoints(polyline.points)
.applyDashPattern(polyline)
staticPolylines.add(map.addPolyline(line))
Expand Down Expand Up @@ -390,9 +398,10 @@ class GoogleMapRenderer(
ContinuationBadgeBitmaps.arrow(color)
}

/** [RoutePolyline.widthDp] scaled to screen pixels, or the shared default when it carries none. */
private fun widthPx(polyline: RoutePolyline): Float =
polyline.widthDp?.let { it * density } ?: DEFAULT_ROUTE_WIDTH_PX
/** Resolve one line's complete width profile at [zoom]. */
private fun routeWidthPx(polyline: RoutePolyline, zoom: Float): Float = polyline.widthProfile
?.let { it.thicknessAt(zoom) * density }
?: (DEFAULT_ROUTE_WIDTH_PX * routeLineWidthScale(zoom))

/** Appends [points] to the receiver, shared by every static polyline draw. */
private fun PolylineOptions.addPoints(points: List<GeoPoint>): PolylineOptions {
Expand Down Expand Up @@ -421,7 +430,7 @@ class GoogleMapRenderer(
routePolylines.forEach { it.remove() }
routePolylines.clear()
renderedRoutePolylines = emptyList()
renderedRouteWidthScale = null
renderedRouteWidths = emptyList()

stopMarkerLayer.dispose()

Expand Down Expand Up @@ -769,13 +778,14 @@ class GoogleMapRenderer(

fun onCameraSettled(zoom: Float) {
routeStopLayer.onCameraSettled(zoom)
val detailScale = routeLineWidthScale(zoom)
if (detailScale != renderedRouteWidthScale) {
renderedRouteWidthScale = detailScale
val nextWidths = renderedRoutePolylines.map { routeWidthPx(it, zoom) }
if (nextWidths != renderedRouteWidths) {
renderedRouteWidths = nextWidths
for (index in routePolylines.indices) {
routePolylines[index].width = widthPx(renderedRoutePolylines[index]) * detailScale
routePolylines[index].width = nextWidths[index]
}
}
val detailScale = routeLineWidthScale(zoom)
updateVehicleScale(detailScale)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,28 @@ internal class GoogleRouteStopBitmapLayer(
private val icons = HashMap<IconKey, BitmapDescriptor>()
private var renderedStops: List<StopMarker> = emptyList()
private var renderedFocusedStopId: String? = null
private var renderedScaleWithZoom = false
private var renderedSizes: StampSizes? = null

override fun render(stops: List<StopMarker>, focusedStopId: String?, zoom: Float) {
override fun render(
stops: List<StopMarker>,
focusedStopId: String?,
scaleWithZoom: Boolean,
zoom: Float,
) {
val routeStops = stops.filter(StopMarker::routeStop)
if (routeStops == renderedStops && focusedStopId == renderedFocusedStopId) return
if (
routeStops == renderedStops &&
focusedStopId == renderedFocusedStopId &&
scaleWithZoom == renderedScaleWithZoom
) return
val previousFocusedStopId = renderedFocusedStopId
val previousSizes = renderedSizes
renderedStops = routeStops
renderedFocusedStopId = focusedStopId
renderedScaleWithZoom = scaleWithZoom

val sizes = stampSizes(zoom, focusedStopId != null)
val sizes = stampSizes(zoom, scaleWithZoom)
val liveIds = routeStops.mapTo(HashSet(), StopMarker::id)
val gone = stopsById.iterator()
while (gone.hasNext()) {
Expand Down Expand Up @@ -114,7 +125,7 @@ internal class GoogleRouteStopBitmapLayer(
}

override fun onCameraSettled(zoom: Float) {
val sizes = stampSizes(zoom, renderedFocusedStopId != null)
val sizes = stampSizes(zoom, renderedScaleWithZoom)
if (sizes == renderedSizes) return
renderedSizes = sizes
stamp(sizes)
Expand All @@ -129,6 +140,7 @@ internal class GoogleRouteStopBitmapLayer(
icons.clear()
renderedStops = emptyList()
renderedFocusedStopId = null
renderedScaleWithZoom = false
renderedSizes = null
}

Expand Down Expand Up @@ -159,19 +171,19 @@ internal class GoogleRouteStopBitmapLayer(

private fun StampSizes.forSelection(selected: Boolean): Int = if (selected) this.selected else normal

private fun stampSizes(zoom: Float, stopFocused: Boolean): StampSizes = StampSizes(
normal = routeStopDiameterPx(zoom, stopFocused, selected = false, density),
selected = routeStopDiameterPx(zoom, stopFocused, selected = true, density),
private fun stampSizes(zoom: Float, scaleWithZoom: Boolean): StampSizes = StampSizes(
normal = routeStopDiameterPx(zoom, scaleWithZoom, selected = false, density),
selected = routeStopDiameterPx(zoom, scaleWithZoom, selected = true, density),
)
}

internal fun routeStopDiameterPx(
zoom: Float,
stopFocused: Boolean,
scaleWithZoom: Boolean,
selected: Boolean,
density: Float,
): Int {
val focusScale = if (stopFocused) focusedRouteStopScale(zoom) else 1f
val focusScale = if (scaleWithZoom) focusedRouteStopScale(zoom) else 1f
val selectedScale = if (selected) RouteStopCircles.FOCUSED_SCALE else 1f
return (2f * RouteStopCircles.RADIUS_PX * focusScale * selectedScale * density)
.roundToInt()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import org.onebusaway.android.map.render.StopMarker

/** Renderer-independent boundary for interchangeable Google route-stop drawing strategies. */
internal interface GoogleRouteStopLayer {
fun render(stops: List<StopMarker>, focusedStopId: String?, zoom: Float)
fun render(stops: List<StopMarker>, focusedStopId: String?, scaleWithZoom: Boolean, zoom: Float)

fun onCameraMoveStarted() = Unit

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,10 @@ class GoogleComposeAdapter : ObaComposeMapAdapter {
val activeRenderer = renderer
val activeInfoWindows = infoWindows
if (activeRenderer != null && activeInfoWindows != null) {
// Non-route static layer (stops / bikes / generics): strip route lines before distinctness
// so a route-only change never churns these annotations, and stop-only emissions cannot
// touch the independently collected native route layer below.
// Non-route static layer (stops / bikes / generics): strip route geometry before
// distinctness so geometry-only changes do not churn these annotations. The derived
// route-stop zoom-scale flag remains, allowing a route-focus change to restyle its stops;
// stop-only emissions still cannot touch the independently collected route layer below.
LaunchedEffect(activeRenderer) {
renderState.snapshot
.map { it.copy(routePolylines = emptyList()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import org.onebusaway.android.map.render.GeoPoint
import org.onebusaway.android.map.render.MapRenderState
import org.onebusaway.android.map.render.MapVehicles
import org.onebusaway.android.map.render.ROUTE_LINE_WIDTH_DP
import org.onebusaway.android.map.render.RouteLineWidthProfile
import org.onebusaway.android.map.render.RouteContinuation
import org.onebusaway.android.map.render.RoutePolyline
import org.onebusaway.android.map.render.VehicleMarker
Expand Down Expand Up @@ -412,7 +413,7 @@ class RouteMapController(
polyline = RoutePolyline(
color = lineColor,
points = listOf(anchor) + tail.map { it.toGeoPoint() },
widthDp = CONTINUATION_LINE_WIDTH_DP,
widthProfile = CONTINUATION_LINE_WIDTH_PROFILE,
dashed = true,
),
arrow = ContinuationArrow(arrowPoint.toGeoPoint(), neighborShape.bearingAt(endSeg)),
Expand Down Expand Up @@ -569,7 +570,10 @@ class RouteMapController(
private fun publishMapPresentation() {
val focus = stopFocusSession
if (focus == null) {
renderState.setRoutePolylines(basePolylines)
renderState.setRoutePolylines(
basePolylines,
routeModeScalesStopsWithZoom = isActive,
)
renderState.setRouteBadges(emptyList())
stopsController.setRoutePresentation(baseStopPresentation)
return
Expand All @@ -586,6 +590,7 @@ class RouteMapController(
// shape alone defines FramingIntent.Route. Using the displayed adjacency lines here would
// fit the union of every route serving the focused stop.
framingPolylines = if (isActive) basePolylines else emptyList(),
routeModeScalesStopsWithZoom = isActive,
)
renderState.setRouteBadges(
if (emphasizedRoute == null) {
Expand Down Expand Up @@ -739,8 +744,8 @@ class RouteMapController(
* Re-draw the route shape for [currentDirectionId]: the selected direction's own travel-ordered
* shape (with direction arrows), or the whole-route merged shape drawn undirected when none is
* selected. Passes the route's raw GTFS color through; the render layer picks the fallback when
* it's absent. Drawn at the shared [ROUTE_LINE_WIDTH_DP] so the overview map's route reads the same
* as the trip-focus map (#1752). Called on load and on every direction switch.
* it's absent. Uses [focusedRoutePolyline], the same complete line presentation as a route selected
* from focused-stop mode. Called on load and on every direction switch.
*/
private fun showDirectionPolylines() {
val route = routeShape ?: return
Expand All @@ -749,12 +754,10 @@ class RouteMapController(
// merged fallback (a direction that carried no shape on the wire).
val shape = route.shapeForDirection(currentDirectionId)
basePolylines = shape.polylines.map { points ->
RoutePolyline(
route.route?.color,
points,
widthDp = ROUTE_LINE_WIDTH_DP,
focusedRoutePolyline(
color = route.route?.color,
points = points,
directional = shape.directional,
transforms = ROUTE_VIEW_TRANSFORMS,
)
}
publishMapPresentation()
Expand Down Expand Up @@ -874,6 +877,10 @@ private const val CONTINUATION_LINE_LENGTH_METERS = 900.0
// Drawn narrower than the shown route's own line ([ROUTE_LINE_WIDTH_DP]) so a continuation reads as a
// preview/hint rather than as equally-weighted with the route the rider is actually looking at.
private const val CONTINUATION_LINE_WIDTH_DP = ROUTE_LINE_WIDTH_DP * 0.7f
private val CONTINUATION_LINE_WIDTH_PROFILE = RouteLineWidthProfile(
thicknessDp = CONTINUATION_LINE_WIDTH_DP,
distantThicknessMultiplier = 1f,
)

// Used only when the neighbor route carries no GTFS color to draw the continuation line in its own
// color with.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import org.onebusaway.android.map.layout.RouteBadgeLayoutInput
import org.onebusaway.android.map.layout.RouteBadgePath
import org.onebusaway.android.map.layout.layoutRouteBadges
import org.onebusaway.android.map.render.DEFAULT_ROUTE_LINE_COLOR
import org.onebusaway.android.map.render.ROUTE_LINE_WIDTH_DP
import org.onebusaway.android.map.render.DEEMPHASIZED_ROUTE_LINE_WIDTH_PROFILE
import org.onebusaway.android.map.render.FOCUSED_ROUTE_LINE_WIDTH_PROFILE
import org.onebusaway.android.map.render.GeoPoint
import org.onebusaway.android.map.render.ROUTE_LINE_WIDTH_PROFILE
import org.onebusaway.android.map.render.RouteBadge
import org.onebusaway.android.map.render.RoutePolyline
import org.onebusaway.android.map.render.RoutePolylineTransform
Expand All @@ -23,8 +26,18 @@ internal val ROUTE_VIEW_TRANSFORMS = setOf(
RoutePolylineTransform.ZOOM_SIMPLIFY,
)

internal const val ROUTE_DEEMPHASIZED_LINE_WIDTH_DP = ROUTE_LINE_WIDTH_DP * 0.275f
internal const val ROUTE_EMPHASIZED_LINE_WIDTH_DP = ROUTE_LINE_WIDTH_DP * 1.5f
/** The line presentation shared by single-route view and a selected route in focused-stop mode. */
internal fun focusedRoutePolyline(
color: Int?,
points: List<GeoPoint>,
directional: Boolean,
) = RoutePolyline(
color = color,
points = points,
widthProfile = FOCUSED_ROUTE_LINE_WIDTH_PROFILE,
directional = directional,
transforms = ROUTE_VIEW_TRANSFORMS,
)

/**
* Convert exact trip shapes into uniform-width directional route lines. When [emphasizedRoute] is
Expand All @@ -43,20 +56,27 @@ internal fun FocusedTripGeometry.toRoutePolylines(
orderedShapes.forEach { shape ->
if (shape.points.size < 2) return@forEach
val emphasized = emphasizedRoute == shape.routeDirection
val widthDp = when {
emphasizedRoute == null -> ROUTE_LINE_WIDTH_DP
emphasized -> ROUTE_EMPHASIZED_LINE_WIDTH_DP
else -> ROUTE_DEEMPHASIZED_LINE_WIDTH_DP
}
add(
val polyline = if (emphasized) {
focusedRoutePolyline(
routeColors[shape.routeDirection] ?: shape.routeColor,
shape.points,
directional = true,
)
} else {
val widthProfile = if (emphasizedRoute == null) {
ROUTE_LINE_WIDTH_PROFILE
} else {
DEEMPHASIZED_ROUTE_LINE_WIDTH_PROFILE
}
RoutePolyline(
routeColors[shape.routeDirection] ?: shape.routeColor,
shape.points,
widthDp,
directional = emphasizedRoute == null || emphasized,
widthProfile,
directional = emphasizedRoute == null,
transforms = ROUTE_VIEW_TRANSFORMS,
)
)
}
add(polyline)
}
}

Expand Down
Loading