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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ domain model is unambiguously millis. See `situationEpochToMillis` and `Situatio
- **Application ID**: `com.joulespersecond.seattlebusbot` (historical, must keep for Google Play)
- **Namespace**: `org.onebusaway.android`
- **Java compatibility**: 17
- **Kotlin version**: see `kotlin` in `gradle/libs.versions.toml` (2.3.20)
- **Kotlin version**: see `kotlin` in `gradle/libs.versions.toml` (2.4.10)

## Multi-Region Support

Expand Down
9 changes: 3 additions & 6 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ jdk = "17"

# --- Build tooling / plugins ---
agp = "9.3.0"
# Kotlin Gradle plugin + its compiler plugins (serialization, compose) all track the Kotlin
# version; kotlin-stdlib-jdk7 below pins to this too.
# Kotlin Gradle plugin + its compiler plugins (serialization, compose) all track this version.
kotlin = "2.4.10"
# KSP version is Kotlin-version-coupled but published on its own cadence (kotlinVersion-kspVersion).
# KSP2 is versioned independently of Kotlin — plain semver, not the old kotlinVersion-kspVersion
# format. Bump on its own cadence against the compatibility range in the KSP release notes.
ksp = "2.3.10"
googleServices = "4.5.0"
hilt = "2.60.1"
Expand All @@ -37,7 +37,6 @@ lint = "32.3.0"

# --- Libraries ---
desugar = "2.1.5"
firebaseCore = "21.1.1"
firebaseAnalytics = "23.2.0"
firebaseFirestore = "26.4.1"
firebaseAuth = "24.2.0"
Expand Down Expand Up @@ -91,7 +90,6 @@ junit = "4.13.2"
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" }

# --- Firebase / Google Play services ---
firebase-core = { module = "com.google.firebase:firebase-core", version.ref = "firebaseCore" }
firebase-analytics = { module = "com.google.firebase:firebase-analytics", version.ref = "firebaseAnalytics" }
firebase-firestore = { module = "com.google.firebase:firebase-firestore", version.ref = "firebaseFirestore" }
firebase-auth = { module = "com.google.firebase:firebase-auth", version.ref = "firebaseAuth" }
Expand Down Expand Up @@ -133,7 +131,6 @@ androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycle" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }

# --- Jetpack Compose (versions from the BOM unless noted) ---
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
Expand Down
5 changes: 1 addition & 4 deletions onebusaway-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,6 @@ dependencies {
// Core library desugaring: backports java.time.* to minSdk 23 (issue #1693)
"coreLibraryDesugaring"(libs.desugar.jdk.libs)
// Firebase Analytics
implementation(libs.firebase.core)
implementation(libs.firebase.analytics)
// Plausible Analytics
implementation(libs.plausible.android.sdk)
Expand Down Expand Up @@ -406,10 +405,8 @@ dependencies {
implementation(libs.androidx.concurrent.listenablefuture.callback)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.preference)
// Preferences DataStore — the backing store behind PreferencesRepository. 1.0.0 was the minSdk-21
// pin; now that minSdk is 23 a newer DataStore is eligible to be adopted as a follow-up.
// Preferences DataStore — the backing store behind PreferencesRepository.
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlin.stdlib.jdk7)
// RoomDB
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import org.onebusaway.android.map.render.ContinuationBadgeBitmaps
import org.onebusaway.android.map.render.CorrectionSmoother
import org.onebusaway.android.map.render.GeoPoint
import org.onebusaway.android.map.render.MapPing
import org.onebusaway.android.map.render.METERS_PER_PIXEL_AT_EQUATOR_ZOOM_ZERO
import org.onebusaway.android.map.render.MapRenderSnapshot
import org.onebusaway.android.map.render.MapRenderState
import org.onebusaway.android.map.render.MarkerRendering
Expand Down Expand Up @@ -83,8 +84,8 @@ import java.util.concurrent.TimeUnit
* Three redraw paths split by update cadence:
* - [renderRoutePolylines] independently reconciles the infrequently-changing route layer, so
* stop-only viewport updates retain every long native line.
* - [renderStatic] clear-and-redraws the remaining static annotations (bikes / generics / trip-stop
* dots); [GoogleStopMarkerLayer] reconciles stops in place so unchanged stops neither blink nor
* - [renderStatic] clear-and-redraws the remaining static annotations (bikes / generics);
* [GoogleStopMarkerLayer] reconciles stops in place so unchanged stops neither blink nor
* receive redundant native position/z-index writes.
* - [renderDynamic] (the live route vehicles + the selected vehicle's band/fast-estimate marker) is pulled at
* ~20Hz by the adapter's frame loop. It moves native markers **in place** to their freshly
Expand Down Expand Up @@ -225,8 +226,8 @@ class GoogleMapRenderer(
private val descriptorCache =
BitmapDescriptorCache(DESCRIPTOR_CACHE_SIZE) { BitmapDescriptorFactory.fromBitmap(it) }

// Remove the redrawn non-route static annotations — continuation polylines, trip-stop dots, bikes,
// generic markers (not map.clear(), which would also wipe the retained route and per-frame dynamic
// Remove the redrawn non-route static annotations — continuation polylines, bikes, generic markers
// (not map.clear(), which would also wipe the retained route and per-frame dynamic
// layers) — and clear their tap maps. Reconciled route/stop annotations deliberately survive.
// Shared by [renderStatic] (before it redraws) and [dispose].
private fun clearStatic() {
Expand Down Expand Up @@ -493,7 +494,8 @@ class GoogleMapRenderer(
val progress = MapPing.progress(elapsed)
val radiusPx = MapPing.MAX_RADIUS_DP * density * MapPing.radiusFraction(progress)
val metersPerPx =
156543.03392 * cos(Math.toRadians(center.latitude)) / 2.0.pow(map.cameraPosition.zoom.toDouble())
METERS_PER_PIXEL_AT_EQUATOR_ZOOM_ZERO * cos(Math.toRadians(center.latitude)) /
2.0.pow(map.cameraPosition.zoom.toDouble())
val radiusMeters = radiusPx * metersPerPx
val color = MapPing.withAlpha(pingColor, MapPing.alpha(progress))
val existing = pingCircle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,7 @@ data class FocusedTripGeometry(val shapes: List<FocusedTripShape>)
data class FocusedTripStops(
val stopIdsByTripId: Map<String, List<String>>,
val stopsById: Map<String, ObaStop>,
) {
val stopIds: Set<String>
get() = buildSet { stopIdsByTripId.values.forEach(::addAll) }
}
)

/** Exact shape and scheduled-stop pass-throughs for the trips displayed at a focused stop. */
interface FocusedTripRepository {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ import org.onebusaway.android.util.haversineDistance
*/
fun haversineMeters(a: GeoPoint, b: GeoPoint): Double =
haversineDistance(a.latitude, a.longitude, b.latitude, b.longitude)

/**
* Web-Mercator ground resolution at zoom 0 on the equator, in meters per pixel (256px tiles). Scale to
* a given zoom/latitude with `× cos(lat) / 2^zoom`. The single source for this constant, shared by the
* route-render pipeline and the map-ping radius math.
*/
internal const val METERS_PER_PIXEL_AT_EQUATOR_ZOOM_ZERO = 156543.03392804097
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ data class ContinuationBadge(
/**
* A tappable label for one route in focused-stop adjacency view (#1827), anchored once in geographic
* space so the map SDK naturally carries it through pan and zoom. Google renders these in the first
* badge phase; MapLibre deliberately ignores the layer until its follow-up.
* badge phase; MapLibre deliberately ignores the layer until its follow-up (#1913).
*/
data class RouteBadge(
val routeId: String,
Expand Down Expand Up @@ -397,7 +397,7 @@ class MapRenderState {

fun clearRoutePolylines() = setRoutePolylines(emptyList())

/** Sets the Google-first adjacency route badges (#1827); MapLibre currently ignores this layer. */
/** Sets the Google-first adjacency route badges (#1827); MapLibre currently ignores this layer (#1913). */
fun setRouteBadges(badges: List<RouteBadge>) {
_snapshot.update { it.copy(routeBadges = badges) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ private const val MAX_MERCATOR_LATITUDE = 85.05112878
private const val VIEWPORT_MARGIN_MULTIPLIER = 1.0
private const val SIMPLIFICATION_ERROR_PIXELS = 0.75
private const val MIN_SIMPLIFICATION_METERS = 2.0
private const val METERS_PER_PIXEL_AT_EQUATOR_ZOOM_ZERO = 156543.03392804097
private const val COORDINATE_EPSILON = 1e-12

internal data class RoutePolylineRenderContext(val camera: CameraSnapshot?)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import androidx.compose.ui.graphics.vector.PathBuilder
import androidx.compose.ui.graphics.vector.path
import androidx.compose.ui.unit.dp

/** Material Symbols used by secondary-action menus, generated at 24dp with outlined styling. */
/**
* Material Symbols used by secondary-action menus. Each path is transcribed by hand from the Material
* Symbols set at **24dp, weight 400, grade 0, optical size 24, "outlined" style** — record any icon's
* source glyph + those axes here when adding one, so the paths can be regenerated or verified. Prefer a
* `baseline_*_24.xml` vector drawable (the mechanism used elsewhere in this module) for new icons; keep
* these inline `ImageVector`s only where a menu needs them without a drawable round-trip.
*/
internal object MaterialSymbols {
val Schedule: ImageVector by lazy {
symbol("schedule") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ import org.onebusaway.android.util.getRouteDisplayName
* Three redraw paths split by update cadence:
* - [renderRoutePolylines] independently reconciles the infrequently-changing route layer, so
* stop-only viewport updates retain every long native line.
* - [renderStatic] clear-and-redraws the remaining static annotations (bikes / generics / trip-stop
* dots); [MapLibreStopMarkerLayer] reconciles stops in place so unchanged stops neither blink nor
* - [renderStatic] clear-and-redraws the remaining static annotations (bikes / generics);
* [MapLibreStopMarkerLayer] reconciles stops in place so unchanged stops neither blink nor
* receive redundant native position writes.
* - [renderDynamic] (the live vehicle markers + the selected vehicle's band/fast-estimate marker) is pulled each
* display frame by the adapter's vsync loop. It updates marker positions **in place** (so an open
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ private fun wireClicks(
return@setOnMarkerClickListener true
}
// Titled markers (the trip-focus estimate markers + the most-recent-data dot) fall through to
// the SDK's default title window. Untitled decorations (trip-stop dots, generic start/end
// markers) have nothing to show, so consume the tap (return true) — a no-op, not an empty bubble.
// the SDK's default title window. Untitled decorations (generic start/end markers) have
// nothing to show, so consume the tap (return true) — a no-op, not an empty bubble.
marker.title.isNullOrEmpty()
}
map.setOnInfoWindowClickListener { marker ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class FocusedTripRepositoryTest {
val result = repository.getStops(setOf(FocusedTrip("trip", "route", "shape", null)))

assertEquals(listOf("a", "c"), result.stopIdsByTripId["trip"])
assertEquals(setOf("a", "c"), result.stopIds)
assertEquals(setOf("a", "c"), result.stopIdsByTripId.values.flatten().toSet())
assertEquals(setOf("a", "b", "c"), result.stopsById.keys)
}

Expand Down Expand Up @@ -122,7 +122,7 @@ class FocusedTripRepositoryTest {
)
)

assertEquals(setOf("served"), result.stopIds)
assertEquals(setOf("served"), result.stopIdsByTripId.values.flatten().toSet())
assertEquals(setOf("good"), result.stopIdsByTripId.keys)
// The omission leaves a log trail, so a fetch failure stays distinguishable from no data.
assertEquals(listOf("Focused-trip schedule failed fetch failed"), loggedFailures)
Expand Down