Skip to content

Commit 193365b

Browse files
bmanderclaude
andauthored
MapLibre: implement the adjacency route-badge layer (#1913) (#1930)
Mirrors GoogleMapRenderer's route-badge rendering (#1827): a routeBadgeByMarker map tracks tap targets, renderRouteBadges() draws each badge as a classic Marker with a bitmap icon reused from the shared ContinuationBadgeBitmaps generator, and the compose adapter's click listener routes a badge tap to ObaMapCallbacks.onRouteBadgeClick(), same as the Google flavor. No anchor call is needed (maplibre centers a classic Marker's icon by default) and there's no z-index equivalent, so badges are simply drawn last to stay on top. Also updates the doc comments across MapRenderState.kt, GoogleMapRenderer.kt, and RouteViewGeometry.kt that described this as a Google-only / MapLibre- deferred layer, since both flavors now render it. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4e3dcec commit 193365b

5 files changed

Lines changed: 55 additions & 9 deletions

File tree

onebusaway-android/src/google/java/org/onebusaway/android/map/googlemapsv2/GoogleMapRenderer.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,9 @@ class GoogleMapRenderer(
112112
// the single selected vehicle), but kept as a map like the other *ByMarker lookups for symmetry.
113113
private val continuationBadgeByMarker = HashMap<Marker, ContinuationBadge>()
114114

115-
// Google-first adjacency route badge tap targets (#1827). Their geographic anchors are laid out
116-
// once upstream; these markers then move naturally with the map through pan and zoom.
115+
// Adjacency route badge tap targets (#1827), mirrored by the maplibre flavor (#1913). Their
116+
// geographic anchors are laid out once upstream; these markers then move naturally with the map
117+
// through pan and zoom.
117118
private val routeBadgeByMarker = HashMap<Marker, RouteBadge>()
118119

119120
// The latest trips-for-route poll, published as it changes (after the markers are reconciled). The

onebusaway-android/src/main/java/org/onebusaway/android/map/RouteViewGeometry.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ internal fun FocusedTripGeometry.toTripFocusedRoutePolylines(
9999
.toRoutePolylines(selectedRoute, routeColors) + selectedRouteUnderlay + selectedTrip
100100

101101
/**
102-
* One Google-first badge model per successfully drawn route-direction, preserving the focused-trip
103-
* order that mirrors the arrivals drawer. The shared layout chooses stable geographic line-center
104-
* anchors; flavor renderers only draw them.
102+
* One badge model per successfully drawn route-direction, preserving the focused-trip order that
103+
* mirrors the arrivals drawer. The shared layout chooses stable geographic line-center anchors;
104+
* flavor renderers only draw them.
105105
*/
106106
internal fun FocusedTripGeometry.toRouteBadges(
107107
routes: List<ObaRoute>,

onebusaway-android/src/main/java/org/onebusaway/android/map/render/MapRenderState.kt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,7 @@ data class ContinuationBadge(
180180

181181
/**
182182
* A tappable label for one route in focused-stop adjacency view (#1827), anchored once in geographic
183-
* space so the map SDK naturally carries it through pan and zoom. Google renders these in the first
184-
* badge phase; MapLibre deliberately ignores the layer until its follow-up (#1913).
183+
* space so the map SDK naturally carries it through pan and zoom. Rendered by both flavors (#1913).
185184
*/
186185
data class RouteBadge(
187186
val routeId: String,
@@ -397,7 +396,7 @@ class MapRenderState {
397396

398397
fun clearRoutePolylines() = setRoutePolylines(emptyList())
399398

400-
/** Sets the Google-first adjacency route badges (#1827); MapLibre currently ignores this layer (#1913). */
399+
/** Sets the adjacency route badges (#1827), rendered by both flavors (#1913). */
401400
fun setRouteBadges(badges: List<RouteBadge>) {
402401
_snapshot.update { it.copy(routeBadges = badges) }
403402
}

onebusaway-android/src/maplibre/java/org/onebusaway/android/map/maplibre/MapLibreRenderer.kt

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,15 @@ import org.onebusaway.android.map.compose.formatDataAge
4242
import org.onebusaway.android.map.render.BikeBand
4343
import org.onebusaway.android.map.render.BikeBitmaps
4444
import org.onebusaway.android.map.render.BikeMarker
45+
import org.onebusaway.android.map.render.ContinuationBadgeBitmaps
4546
import org.onebusaway.android.map.render.CorrectionSmoother
4647
import org.onebusaway.android.map.render.GeoPoint
4748
import org.onebusaway.android.map.render.MapPing
4849
import org.onebusaway.android.map.render.MapRenderSnapshot
4950
import org.onebusaway.android.map.render.MapRenderState
5051
import org.onebusaway.android.map.render.PingTarget
5152
import org.onebusaway.android.map.render.MapVehicles
53+
import org.onebusaway.android.map.render.RouteBadge
5254
import org.onebusaway.android.map.render.RoutePolyline
5355
import org.onebusaway.android.map.render.StopMarker
5456
import org.onebusaway.android.map.render.TripMarkerBitmaps
@@ -60,6 +62,7 @@ import org.onebusaway.android.map.render.RoutePolylineReconciler
6062
import org.onebusaway.android.map.render.routeLineWidthScale
6163
import org.onebusaway.android.time.WallTime
6264
import org.onebusaway.android.util.MyTextUtils
65+
import org.onebusaway.android.util.ThemeUtils
6366
import org.onebusaway.android.util.getRouteDisplayName
6467

6568
/**
@@ -98,6 +101,11 @@ class MapLibreRenderer(
98101

99102
private val vehicleByMarker = HashMap<Marker, VehicleMarker>()
100103

104+
// Adjacency route badge tap targets (#1827), mirroring the Google flavor's routeBadgeByMarker.
105+
// Their geographic anchors are laid out once upstream; these markers then move naturally with the
106+
// map through pan and zoom.
107+
private val routeBadgeByMarker = HashMap<Marker, RouteBadge>()
108+
101109
// The non-route static annotations added by the last [renderStatic], removed (not map.clear()) on
102110
// the next so the retained route and per-frame dynamic layers survive a static redraw.
103111
private val staticAnnotations = mutableListOf<Annotation>()
@@ -173,8 +181,9 @@ class MapLibreRenderer(
173181
staticAnnotations.clear()
174182
}
175183
// Stop markers are reconciled in place (not in staticAnnotations), so they survive this; only
176-
// the bike tap map is cleared here.
184+
// the bike / route-badge tap maps are cleared here.
177185
bikeByMarker.clear()
186+
routeBadgeByMarker.clear()
178187

179188
stopMarkerLayer.render(snapshot.stops, snapshot.focusedStopId, snapshot.stopBand)
180189
routeStopCircleLayer.render(
@@ -216,8 +225,36 @@ class MapLibreRenderer(
216225
)
217226
)
218227
}
228+
229+
renderRouteBadges(snapshot.routeBadges)
230+
}
231+
232+
// Parity with the Google flavor's renderRouteBadges (#1827/#1913): the classic Marker centers its
233+
// icon on the point by default, so no anchor call is needed here (contrast Google's explicit
234+
// .anchor(0.5f, 0.5f)). Draw order is add-order in maplibre (no z-index on classic markers), so
235+
// adding these last keeps them on top of the stops/bikes/generics drawn above.
236+
private fun renderRouteBadges(badges: List<RouteBadge>) {
237+
for (badge in badges) {
238+
val marker = map.addMarker(
239+
MarkerOptions()
240+
.position(badge.point.toLatLng())
241+
.icon(routeBadgeIcon(badge.routeShortName, badge.color))
242+
)
243+
staticAnnotations.add(marker)
244+
routeBadgeByMarker[marker] = badge
245+
}
219246
}
220247

248+
private fun routeBadgeIcon(routeShortName: String, color: Int): Icon =
249+
iconFactory.fromBitmap(
250+
ContinuationBadgeBitmaps.badge(
251+
routeShortName,
252+
color,
253+
density,
254+
darkMode = ThemeUtils.isInDarkMode(context),
255+
)
256+
)
257+
221258
/** Reconcile the independently collected route layer, retaining equal native polylines. */
222259
fun renderRoutePolylines(next: List<RoutePolyline> = renderState.snapshot.value.routePolylines) {
223260
routePolylineReconciler.reconcile(next, map.cameraPosition.zoom.toFloat())
@@ -265,6 +302,7 @@ class MapLibreRenderer(
265302
bandPolylines.clear()
266303
vehicleByMarker.clear()
267304
bikeByMarker.clear()
305+
routeBadgeByMarker.clear()
268306
vehicleIconDirection.clear()
269307
mostRecentDataMarker = null
270308
lastVehicleResponse = null
@@ -557,6 +595,9 @@ class MapLibreRenderer(
557595

558596
fun vehicleForMarker(marker: Marker): VehicleMarker? = vehicleByMarker[marker]
559597

598+
/** The adjacency route badge (#1827) tapped, or null if [marker] isn't one. */
599+
fun routeBadgeForMarker(marker: Marker): RouteBadge? = routeBadgeByMarker[marker]
600+
560601
/**
561602
* If [marker] is the ping ripple, the vehicle marker it's centered on (else null) — so a tap on the
562603
* ripple selects the vehicle underneath rather than being swallowed. maplibre's classic Marker has no

onebusaway-android/src/maplibre/java/org/onebusaway/android/map/maplibre/compose/MapLibreComposeAdapter.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ private fun wireClicks(
324324
}
325325
return@setOnMarkerClickListener true
326326
}
327+
val routeBadge = renderer.routeBadgeForMarker(marker)
328+
if (routeBadge != null) {
329+
callbacks.onRouteBadgeClick(routeBadge.routeId, routeBadge.routeShortName, routeBadge.directionId)
330+
return@setOnMarkerClickListener true
331+
}
327332
// Titled markers (the trip-focus estimate markers + the most-recent-data dot) fall through to
328333
// the SDK's default title window. Untitled decorations (generic start/end markers) have
329334
// nothing to show, so consume the tap (return true) — a no-op, not an empty bubble.

0 commit comments

Comments
 (0)