diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 410e40f..996f31a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,10 @@ + + diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchConstants.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchConstants.kt index ab02486..8ee0798 100644 --- a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchConstants.kt +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchConstants.kt @@ -26,9 +26,9 @@ object AppLaunchConstants { routeKey: Int, routeId: String? = null, pathId: Int? = null, - stopId: Int? = null, + stopId: Long? = null, destinationPathId: Int? = null, - destinationStopId: Int? = null, + destinationStopId: Long? = null, ): Intent { return Intent(context, MainActivity::class.java).apply { putExtra(EXTRA_TARGET, TARGET_ROUTE_DETAIL) @@ -100,9 +100,12 @@ object AppLaunchConstants { if (intent.hasExtra(EXTRA_PATH_ID)) { payload["pathId"] = intent.getIntExtra(EXTRA_PATH_ID, 0) } - if (intent.hasExtra(EXTRA_STOP_ID)) { - payload["stopId"] = intent.getIntExtra(EXTRA_STOP_ID, 0) - } + // Stop ids are sent at full width, because Dart keys stops by + // the raw feed id: 高雄's exceed Int, and narrowing them here + // would name a different stop than the app's models hold. The + // native-only fold (`foldStopId`) is for joining the live + // feeds, not for identifying a stop to Flutter. + stopIdExtra(intent, EXTRA_STOP_ID)?.let { payload["stopId"] = it } val destinationPathId = intent.getIntExtra( EXTRA_DESTINATION_PATH_ID, Int.MIN_VALUE, @@ -110,13 +113,8 @@ object AppLaunchConstants { if (destinationPathId != Int.MIN_VALUE) { payload["destinationPathId"] = destinationPathId } - val destinationStopId = intent.getIntExtra( - EXTRA_DESTINATION_STOP_ID, - Int.MIN_VALUE, - ) - if (destinationStopId != Int.MIN_VALUE) { - payload["destinationStopId"] = destinationStopId - } + stopIdExtra(intent, EXTRA_DESTINATION_STOP_ID) + ?.let { payload["destinationStopId"] = it } payload } @@ -145,6 +143,19 @@ object AppLaunchConstants { } } + /** + * Reads a stop id extra as a `Long` whichever width it was stored at. + * + * New intents store a `Long`. Shortcuts pinned to the home screen before + * that keep the `Int` they were created with for as long as they exist, and + * `getLongExtra` answers its default for an extra of another type — which + * would quietly open those shortcuts with no stop selected. + */ + @Suppress("DEPRECATION") + private fun stopIdExtra(intent: Intent, key: String): Long? { + return (intent.extras?.get(key) as? Number)?.toLong() + } + private fun extractAuthCallbackPayload(uri: android.net.Uri?): Map? { uri ?: return null if (uri.scheme?.lowercase() != "yabus" || uri.host?.lowercase() != "auth-callback") { diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/FavoriteGroupWidgetProvider.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/FavoriteGroupWidgetProvider.kt index 1f16c4f..9800e20 100644 --- a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/FavoriteGroupWidgetProvider.kt +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/FavoriteGroupWidgetProvider.kt @@ -390,7 +390,8 @@ object FavoriteGroupWidgetSupport { ) } - private fun createItemPendingIntent( + /** The tap intent for one widget row. Internal so a test can read it back. */ + internal fun createItemPendingIntent( context: Context, item: FavoriteWidgetItem, ): PendingIntent { @@ -409,9 +410,12 @@ object FavoriteGroupWidgetSupport { routeKey = item.routeKey, routeId = item.routeId, pathId = item.pathId.takeIf { item.type == "boarding" }, - stopId = item.stopId.takeIf { item.type == "boarding" }, + // The full-width id, not the folded one: Flutter compares this + // against the ids its own stop models carry, and a folded + // 高雄 id matches nothing there. + stopId = item.launchStopId.takeIf { item.type == "boarding" }, destinationPathId = item.destinationPathId, - destinationStopId = item.destinationStopId, + destinationStopId = item.launchDestinationStopId, ).apply { data = Uri.parse( "yabus://route/${item.provider}/${item.routeKey}/${Uri.encode(item.routeId.orEmpty())}", @@ -434,9 +438,14 @@ object FavoriteGroupWidgetSupport { val raw = runCatching { preferences.getString(FAVORITE_GROUPS_KEY, null) ?: preferences.getString(FAVORITE_GROUPS_FALLBACK_KEY, null) - }.getOrNull() ?: return emptyMap() + }.getOrNull() + return parseFavoriteGroups(raw) + } + + /** Reads the favourites Dart saved. Internal so a test can feed it a config. */ + internal fun parseFavoriteGroups(raw: String?): Map> { val root = try { - JSONObject(raw) + JSONObject(raw ?: return emptyMap()) } catch (_: Exception) { return emptyMap() } @@ -449,7 +458,13 @@ object FavoriteGroupWidgetSupport { val item = groupArray.optJSONObject(index) ?: continue val type = item.optString("type", "boarding").trim().ifEmpty { "boarding" } val routeKey = item.optInt("routeKey", 0) - val stopId = item.optInt("stopId", 0) + // The config is written by Dart with the id as Dart holds it, + // which exceeds Int for 高雄. It is needed twice, as two + // different numbers: folded the way the live feed is, for the + // ETA lookup; and at full width, for the tap that launches + // back into Flutter. + val dartStopId = item.opt("stopId") + val stopId = foldStopId(dartStopId) val routeId = item.optString("routeId", "").trim().takeIf { it.isNotEmpty() } val stationId = item.optString("stationId", "").trim() val valid = when (type) { @@ -461,19 +476,15 @@ object FavoriteGroupWidgetSupport { if (!valid) { continue } - val destinationStopId = item.optInt("destinationStopId", 0) - .takeIf { it > 0 } - val destinationPathId = if (destinationStopId == null) { - null - } else { - item.optInt("destinationPathId", item.optInt("pathId", 0)) - } + val dartDestinationStopId = item.opt("destinationStopId") + val hasDestination = foldStopId(dartDestinationStopId) > 0 groupItems += FavoriteWidgetItem( type = type, provider = item.optString("provider", "twn"), routeKey = routeKey, pathId = item.optInt("pathId", 0), stopId = stopId, + launchStopId = launchStopId(dartStopId).takeIf { stopId > 0 }, routeId = routeId, routeName = item.optString("routeName", ""), routeDescription = item.optString("routeDescription", "") @@ -482,8 +493,13 @@ object FavoriteGroupWidgetSupport { stopName = item.optString("stopName", ""), stationId = stationId, stationName = item.optString("stationName", ""), - destinationPathId = destinationPathId, - destinationStopId = destinationStopId, + destinationPathId = if (hasDestination) { + item.optInt("destinationPathId", item.optInt("pathId", 0)) + } else { + null + }, + launchDestinationStopId = launchStopId(dartDestinationStopId) + .takeIf { hasDestination }, destinationStopName = item.optString("destinationStopName", "") .trim() .takeIf { it.isNotEmpty() }, @@ -690,21 +706,7 @@ object FavoriteGroupWidgetSupport { return result } - private fun parseStopId(raw: Any?): Int { - if (raw is Number) { - return raw.toInt() - } - val text = raw?.toString()?.trim().orEmpty() - val parsed = text.toIntOrNull() - if (parsed != null) { - return parsed - } - var hash = 17 - for (char in text) { - hash = (hash * 31 + char.code) and 0x7fffffff - } - return hash - } + private fun parseStopId(raw: Any?): Int = foldStopId(raw) private fun toIntOrNull(raw: Any?): Int? { return when (raw) { @@ -807,7 +809,10 @@ data class FavoriteWidgetItem( val provider: String, val routeKey: Int, val pathId: Int, + /** Folded, as the live feeds are: only usable for looking up ETAs. */ val stopId: Int, + /** Full-width, as Flutter holds it: what a tap is launched with. */ + val launchStopId: Long?, val routeId: String?, val routeName: String, val routeDescription: String?, @@ -815,7 +820,7 @@ data class FavoriteWidgetItem( val stationId: String, val stationName: String, val destinationPathId: Int?, - val destinationStopId: Int?, + val launchDestinationStopId: Long?, val destinationStopName: String?, ) { val identity: String diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/MainActivity.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/MainActivity.kt index 11a161c..b91cb62 100644 --- a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/MainActivity.kt +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/MainActivity.kt @@ -306,7 +306,10 @@ class MainActivity : FlutterActivity() { val routeKey = call.argument("routeKey") val routeId = call.argument("routeId")?.trim().orEmpty() val pathId = call.argument("pathId") - val stopId = call.argument("stopId") + // Stop ids can exceed Int (高雄's are 64-bit), and the channel decodes a + // Dart int to Integer or Long depending on its magnitude, so both are + // accepted rather than letting the larger ones read back as null. + val stopId = call.argument("stopId")?.toLong() val routeName = call.argument("routeName")?.trim().orEmpty() val stopName = call.argument("stopName")?.trim().orEmpty() val stationId = call.argument("stationId")?.trim().orEmpty() diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RideFixTime.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RideFixTime.kt new file mode 100644 index 0000000..342e52a --- /dev/null +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RideFixTime.kt @@ -0,0 +1,33 @@ +package tw.avianjay.taiwanbus.flutter + +import android.location.Location +import android.os.SystemClock + +/** + * When [location] was measured, on the wall clock the ride detector runs on. + * + * A fix's age is read off the monotonic clock, the only one a fix and "now" are + * guaranteed to share: `Location.time` may come from the satellites, the + * network or the system clock, and none of those has to agree with + * `System.currentTimeMillis()`. Taking that age off the current wall-clock time + * puts a fix cached five minutes ago five minutes in the past — where the + * detector can see it is stale — however the device's clock happens to be set. + * + * A location with no monotonic stamp did not come from the platform (the + * session's seed position is built by hand). Its own `time` is used when it has + * a believable one, and it counts as current otherwise. + */ +internal fun rideFixTimestampMs( + location: Location, + nowWallMs: Long = System.currentTimeMillis(), + nowElapsedNanos: Long = SystemClock.elapsedRealtimeNanos(), +): Long { + val fixElapsedNanos = location.elapsedRealtimeNanos + if (fixElapsedNanos > 0L) { + val ageMs = ((nowElapsedNanos - fixElapsedNanos) / NANOS_PER_MILLI).coerceAtLeast(0L) + return nowWallMs - ageMs + } + return location.time.takeIf { it in 1L..nowWallMs } ?: nowWallMs +} + +private const val NANOS_PER_MILLI = 1_000_000L diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RideRouteLine.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RideRouteLine.kt new file mode 100644 index 0000000..06ad82b --- /dev/null +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RideRouteLine.kt @@ -0,0 +1,127 @@ +package tw.avianjay.taiwanbus.flutter + +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.cos +import kotlin.math.hypot + +internal data class RideProjection( + val alongMeters: Double, + val crossTrackMeters: Double, + val segmentIndex: Int, +) + +/** + * The stop sequence as a polyline in a local equirectangular plane, with + * cumulative distance, so that the user and every bus reduce to one comparable + * number: metres travelled from the first stop. + */ +internal class RideRouteLine(private val stops: List) { + private val originLat = stops.first().lat + private val originLon = stops.first().lon + private val metersPerDegreeLon = EARTH_RADIUS_METERS * cos(originLat * PI / 180.0) * PI / 180.0 + private val metersPerDegreeLat = EARTH_RADIUS_METERS * PI / 180.0 + private val xs = DoubleArray(stops.size) { (stops[it].lon - originLon) * metersPerDegreeLon } + private val ys = DoubleArray(stops.size) { (stops[it].lat - originLat) * metersPerDegreeLat } + private val cumulativeMeters = DoubleArray(stops.size).also { cumulative -> + for (index in 1 until stops.size) { + cumulative[index] = cumulative[index - 1] + + hypot(xs[index] - xs[index - 1], ys[index] - ys[index - 1]) + } + } + + /** Metres from the first stop to [stopIndex]. */ + fun alongOfStop(stopIndex: Int): Double = cumulativeMeters[stopIndex.coerceIn(0, stops.size - 1)] + + fun segmentLengthMeters(segmentIndex: Int): Double { + val index = segmentIndex.coerceIn(0, (stops.size - 2).coerceAtLeast(0)) + return if (stops.size < 2) 0.0 else cumulativeMeters[index + 1] - cumulativeMeters[index] + } + + fun metersBetween(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + return hypot((lon2 - lon1) * metersPerDegreeLon, (lat2 - lat1) * metersPerDegreeLat) + } + + fun metersToStop(latitude: Double, longitude: Double, stopIndex: Int): Double { + val stop = stops[stopIndex.coerceIn(0, stops.size - 1)] + return metersBetween(latitude, longitude, stop.lat, stop.lon) + } + + /** + * Projects a fix onto the polyline. + * + * [hintMeters] disambiguates routes that run along the same street twice + * (loops, out-and-back sections): among the segments about as close as the + * closest one, the one nearest the previous along-route position wins. + */ + fun project(latitude: Double, longitude: Double, hintMeters: Double?): RideProjection { + val segmentCount = stops.size - 1 + if (segmentCount <= 0) { + return RideProjection(0.0, metersToStop(latitude, longitude, 0), 0) + } + val px = (longitude - originLon) * metersPerDegreeLon + val py = (latitude - originLat) * metersPerDegreeLat + val distances = DoubleArray(segmentCount) + val alongs = DoubleArray(segmentCount) + var closest = 0 + for (index in 0 until segmentCount) { + val vx = xs[index + 1] - xs[index] + val vy = ys[index + 1] - ys[index] + val lengthSquared = vx * vx + vy * vy + val t = if (lengthSquared <= 1e-9) { + 0.0 + } else { + (((px - xs[index]) * vx + (py - ys[index]) * vy) / lengthSquared).coerceIn(0.0, 1.0) + } + distances[index] = hypot(px - (xs[index] + t * vx), py - (ys[index] + t * vy)) + alongs[index] = cumulativeMeters[index] + + t * (cumulativeMeters[index + 1] - cumulativeMeters[index]) + if (distances[index] < distances[closest]) { + closest = index + } + } + var chosen = closest + if (hintMeters != null) { + for (index in 0 until segmentCount) { + if (distances[index] <= distances[closest] + HINT_SLACK_METERS && + abs(alongs[index] - hintMeters) < abs(alongs[chosen] - hintMeters) + ) { + chosen = index + } + } + } + return RideProjection(alongs[chosen], distances[closest], chosen) + } + + fun nearestStopIndex(alongMeters: Double): Int { + var best = 0 + for (index in cumulativeMeters.indices) { + if (abs(cumulativeMeters[index] - alongMeters) < abs(cumulativeMeters[best] - alongMeters)) { + best = index + } + } + return best + } + + /** + * The last stop reached at [alongMeters]. A vehicle pulling in counts as + * having reached the stop, hence the tolerance. Unlike the nearest stop, + * this only moves forward as the vehicle does, so "stops to go" counts down + * at the stops themselves rather than midway between them. + */ + fun stopIndexAt(alongMeters: Double): Int { + var reached = 0 + for (index in cumulativeMeters.indices) { + if (cumulativeMeters[index] <= alongMeters + STOP_REACHED_TOLERANCE_METERS) { + reached = index + } + } + return reached + } + + private companion object { + const val EARTH_RADIUS_METERS = 6_371_000.0 + const val HINT_SLACK_METERS = 60.0 + const val STOP_REACHED_TOLERANCE_METERS = 25.0 + } +} diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorService.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorService.kt index 2cad1d1..a509e92 100644 --- a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorService.kt +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorService.kt @@ -15,6 +15,7 @@ import android.os.Build import android.os.Handler import android.os.IBinder import android.os.Looper +import android.os.PowerManager import android.os.SystemClock import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -27,6 +28,7 @@ import com.google.android.gms.location.LocationResult import com.google.android.gms.location.LocationServices import com.google.android.gms.location.Priority import java.net.HttpURLConnection +import java.net.URLEncoder import java.net.URL import java.util.concurrent.Executors import kotlin.math.abs @@ -46,29 +48,25 @@ class RouteTripMonitorService : Service() { @Volatile private var session: TrackingSession? = null + @Volatile private var appInForeground = true private var foregroundStarted = false + @Volatile private var latestLocation: Location? = null - private var lastMovementLocation: Location? = null - private var lastMovementNearestIndex: Int? = null - private var lastMovementRecordedAtMs = 0L - private var lastTransitLikeAtMs = 0L + + /** + * Owns the "is the user on the bus" question. It is fed the user's own fixes + * and the live vehicle positions, and never looks at whether the app is in + * the foreground — a trip does not stop being a trip when the user opens + * the app. + */ + private var detector: TripRideDetector? = null + private var latestRide = RideSnapshot() + private val detectorLock = Any() + /** Under detectorLock: newer app-provided stop data supersedes pending stop requests. */ + private var stopFeedRevision = 0L private var boardingAlertSent = false - private var boardingCheckPromptSent = false - private var boardingCheckPromptSentAtMs = 0L - private var boardingWindowOpen = false - private var boardingWindowOpenedAtMs = 0L - private var boardingCheckSnoozeUntilMs = 0L - private var rideConfirmed = false - private var rideConfirmationSamples = 0 - private var lastNearestStopIndex: Int? = null - private var lastBusStopIndex: Int? = null - private var activeBoardingVehicleId: String? = null - private var activeBoardingVehicleStopIndex: Int? = null - private var activeBoardingVehicleLastSeenAtMs = 0L - private var activeBoardingVehiclePassedAtMs = 0L - private var trackedBusId: String? = null - private var trackedBusLastSeenAtMs = 0L + private var boardingCheckNotificationId = 0L private var destinationSetupPromptSent = false private var arrivalDetectedAtMs = 0L private var destinationAlertStage = 0 @@ -81,26 +79,46 @@ class RouteTripMonitorService : Service() { private var cachedLivePathId: Int? = null private var cachedLiveFetchedAtMs = 0L private var cachedLiveStops: Map = emptyMap() + /** Under refreshLock: every family route shares the same polling policy. */ + private val busFeedRefreshByRoute = mutableMapOf() + private var liveStopBackoffUntilMs = 0L + private var wakeLock: PowerManager.WakeLock? = null private val pollingRunnable = object : Runnable { override fun run() { if (!foregroundStarted) { return } - if (!appInForeground) { - refreshNotification() - } + // Refresh in the foreground too: the notification, the ETA text and + // the boarding question all depend on this loop, and skipping it + // while the app is open was why boarding went unnoticed. + refreshNotification() mainHandler.postDelayed(this, POLL_INTERVAL_MS) } } private val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { - latestLocation = locationResult.lastLocation - if (!appInForeground) { - refreshNotification() - } - } + val location = locationResult.lastLocation ?: return + latestLocation = location + onUserLocation(location) + refreshNotification() + } + } + + private fun onUserLocation(location: Location): Unit = synchronized(detectorLock) { + val currentDetector = detector ?: return + currentDetector.onUserLocation( + RideGeoPoint( + latitude = location.latitude, + longitude = location.longitude, + // The fix's real age, so that a cached one from minutes ago is + // recognised as stale instead of passing for the present. + timestampMs = rideFixTimestampMs(location), + accuracyMeters = if (location.hasAccuracy()) location.accuracy else 0f, + speedMetersPerSecond = location.speed.takeIf { location.hasSpeed() && it > 0f }, + ), + ) } override fun onCreate() { @@ -157,11 +175,7 @@ class RouteTripMonitorService : Service() { return START_NOT_STICKY } refreshNotification(force = true) - if (appInForeground) { - stopPolling() - } else { - startPolling() - } + startPolling() return START_STICKY } @@ -180,17 +194,13 @@ class RouteTripMonitorService : Service() { stopTracking() return START_NOT_STICKY } - rideConfirmed = true - rideConfirmationSamples = REQUIRED_RIDE_CONFIRMATION_SAMPLES - boardingWindowOpen = true - boardingCheckPromptSent = true - boardingCheckPromptSentAtMs = System.currentTimeMillis() - normalizeVehicleId(activeBoardingVehicleId)?.let { vehicleId -> - trackedBusId = vehicleId - trackedBusLastSeenAtMs = System.currentTimeMillis() + val now = System.currentTimeMillis() + synchronized(detectorLock) { + detector?.confirmBoardedByUser(now, answeredBoardingCheckId(intent)) + latestRide = detector?.snapshot(now) ?: RideSnapshot() } - activeBoardingVehiclePassedAtMs = 0L - notificationManager.cancel(ALERT_NOTIFICATION_ID) + boardingAlertSent = true + cancelBoardingCheckPrompt() refreshNotification(force = true) return START_STICKY } @@ -200,26 +210,19 @@ class RouteTripMonitorService : Service() { stopTracking() return START_NOT_STICKY } + val now = System.currentTimeMillis() + synchronized(detectorLock) { + // Only the bus that was asked about is set aside; the next + // one still gets detected and asked about in its turn. + detector?.declineBoarding(now, answeredBoardingCheckId(intent)) + latestRide = detector?.snapshot(now) ?: RideSnapshot() + } boardingAlertSent = false - rideConfirmed = false - rideConfirmationSamples = 0 - boardingCheckPromptSent = false - boardingCheckPromptSentAtMs = 0L - boardingWindowOpen = false - boardingWindowOpenedAtMs = 0L - trackedBusId = null - trackedBusLastSeenAtMs = 0L - activeBoardingVehicleId = null - activeBoardingVehicleStopIndex = null - activeBoardingVehicleLastSeenAtMs = 0L - activeBoardingVehiclePassedAtMs = 0L - lastMovementLocation = null - lastMovementNearestIndex = null - lastMovementRecordedAtMs = 0L - lastTransitLikeAtMs = 0L - boardingCheckSnoozeUntilMs = - System.currentTimeMillis() + BOARDING_CHECK_SNOOZE_MS - notificationManager.cancel(ALERT_NOTIFICATION_ID) + destinationSetupPromptSent = false + arrivalDetectedAtMs = 0L + destinationAlertStage = 0 + overshootAlertSent = false + cancelBoardingCheckPrompt() refreshNotification(force = true) return START_STICKY } @@ -235,63 +238,61 @@ class RouteTripMonitorService : Service() { val previousDestination = session?.destinationStopId val previousBoarding = session?.boardingStopId val previousRouteId = session?.routeId + val previousRouteKey = session?.routeKey + val previousPathId = session?.pathId + val previousStopIds = session?.stops?.map { it.stopId } val previousFamily = session?.realtimeRouteIds?.toSet() + val hadSession = session != null + val hadDetector = session != null && detectorExists() if (AppRuntimeStateStore.isTripMonitorPausedFor(this, parsedSession)) { - session = parsedSession appInForeground = parsedSession.appInForeground stopTracking() return START_NOT_STICKY } - session = parsedSession + val startedAtMs = System.currentTimeMillis() + val continuesBoardedRide = !hadDetector && continuesBoardedRide( + paused = AppRuntimeStateStore.loadPausedTripMonitor(this), + session = parsedSession, + nowMs = startedAtMs, + ) appInForeground = parsedSession.appInForeground val providedStops = parsedSession.stops.mapNotNull { stop -> stop.liveData?.let { stop.stopId to it } }.toMap() - if (providedStops.isNotEmpty()) { - synchronized(refreshLock) { - cachedLiveRouteIds = parsedSession.realtimeRouteIds - cachedLivePathId = parsedSession.pathId - cachedLiveFetchedAtMs = SystemClock.elapsedRealtime() - cachedLiveStops = providedStops - } - } if (!appInForeground && lastWentBackgroundAtMs == 0L) { lastWentBackgroundAtMs = System.currentTimeMillis() } AppRuntimeStateStore.setAppInForeground(this, appInForeground) val tripOriginChanged = previousBoarding != parsedSession.boardingStopId || - previousRouteId != parsedSession.routeId + previousRouteId != parsedSession.routeId || + // Some providers leave routeId blank, so the route key + // is what actually tells two routes apart. + previousRouteKey != parsedSession.routeKey || + // The other direction of the same route can share a + // boarding stop, yet it is another line of stops: the + // detector measures everything along the one it was + // built with. + previousPathId != parsedSession.pathId || + previousStopIds != parsedSession.stops.map { it.stopId } val destinationChanged = previousDestination != parsedSession.destinationStopId val familyChanged = previousFamily != parsedSession.realtimeRouteIds.toSet() // A preliminary primary-route response must not keep a waiting - // reminder locked to that bus once the family arrives. - if (tripOriginChanged || (familyChanged && !rideConfirmed)) { + // reminder locked to that bus once the family arrives, and the + // detector must exist before anything can consult it. A ride the + // user already confirmed is never rebuilt out from under them. + val rideAlreadyConfirmed = currentRideSnapshot().hasBoarded + val startsNewTrip = tripOriginChanged || + (familyChanged && !rideAlreadyConfirmed) || + !hadDetector + if (startsNewTrip) { boardingAlertSent = false - boardingCheckPromptSent = false - boardingCheckPromptSentAtMs = 0L - boardingWindowOpen = false - boardingWindowOpenedAtMs = 0L - boardingCheckSnoozeUntilMs = 0L - rideConfirmed = false - rideConfirmationSamples = 0 - lastNearestStopIndex = null - lastBusStopIndex = null - activeBoardingVehicleId = null - activeBoardingVehicleStopIndex = null - activeBoardingVehicleLastSeenAtMs = 0L - activeBoardingVehiclePassedAtMs = 0L - lastMovementLocation = null - lastMovementNearestIndex = null - lastMovementRecordedAtMs = 0L - lastTransitLikeAtMs = 0L - trackedBusId = null - trackedBusLastSeenAtMs = 0L destinationSetupPromptSent = false arrivalDetectedAtMs = 0L destinationAlertStage = 0 overshootAlertSent = false + cancelBoardingCheckPrompt() } else if (destinationChanged) { // Choosing a destination after boarding must not unlock the // current vehicle and fall back to the next bus at the @@ -301,32 +302,203 @@ class RouteTripMonitorService : Service() { destinationAlertStage = 0 overshootAlertSent = false } - latestLocation = createSessionLocation(parsedSession) ?: latestLocation + // The session and the detector built for it change hands in one + // step: a refresh still fetching for the previous trip looks + // for exactly this before it applies what it brought back. + synchronized(detectorLock) { + session = parsedSession + val tracking = if (startsNewTrip) { + createDetector(parsedSession).also { created -> + if (continuesBoardedRide) { + // Choosing where to get off, in answer to "you + // have boarded", is the rider confirming it — + // but of a ride that began some stops back. + created.resumeConfirmedRide() + } + detector = created + latestRide = RideSnapshot() + // It starts knowing nothing, so both feeds are due + // at once, and the last trip's failures are not its + // own to sit out. + forgetLiveFeeds() + } + } else { + if (familyChanged) { + synchronized(refreshLock) { + busFeedRefreshByRoute.keys.retainAll(parsedSession.realtimeRouteIds.toSet()) + } + } + detector?.also { it.updateDestination(parsedSession.destinationStopId) } + } + if (providedStops.isNotEmpty()) { + stopFeedRevision++ + synchronized(refreshLock) { + cachedLiveRouteIds = parsedSession.realtimeRouteIds + cachedLivePathId = parsedSession.pathId + cachedLiveFetchedAtMs = SystemClock.elapsedRealtime() + cachedLiveStops = providedStops + } + // While the app is open it does the fetching, and this is + // all the detector hears of the boarding stop's ETA. + tracking?.let { feedBoardingEta(it, parsedSession, providedStops) } + } + } + if (startsNewTrip) { + if (continuesBoardedRide) { + boardingAlertSent = true + AppRuntimeStateStore.clearPausedTripMonitor(this) + } + refreshRideSnapshot(System.currentTimeMillis()) + // Only the very first update seeds the detector from the + // session's stale position; a later re-seed would replay an + // old fix as if it were current. + if (!hadSession) { + latestLocation = createSessionLocation(parsedSession) ?: latestLocation + latestLocation?.let(::onUserLocation) + } + } createAlertNotificationChannel() requestLocationUpdates() + acquireWakeLock() refreshNotification(force = true) - if (appInForeground) { - stopPolling() - } else { - startPolling() - } + startPolling() } } return START_STICKY } + private fun detectorExists(): Boolean = synchronized(detectorLock) { detector != null } + + /** A trip as one refresh sees it: the session, and the detector built for it. */ + private class TrackedTrip( + val session: TrackingSession, + val detector: TripRideDetector, + val stopFeedRevision: Long, + ) + + private data class BusFeedRefresh( + val fetchedAtElapsedMs: Long? = null, + val retryAtElapsedMs: Long = 0L, + ) + + private fun currentTrip(): TrackedTrip? = synchronized(detectorLock) { + val currentSession = session ?: return null + val currentDetector = detector ?: return null + TrackedTrip(currentSession, currentDetector, stopFeedRevision) + } + + /** + * Applies what a request brought back for [trip], unless the trip has been + * replaced while the request was out; returns whether it was applied. + * + * The rider can open another route, or the other direction, while a request + * is in flight. The check and [apply] share the lock the detector is + * replaced under, so what was fetched for one trip never lands in the next. + */ + private fun applyToTrip(trip: TrackedTrip, apply: () -> Unit): Boolean = + synchronized(detectorLock) { + if (detector !== trip.detector) { + return false + } + apply() + true + } + + /** + * Runs [action] on the main thread unless by then [trackedSession] has been + * replaced, or the ride has moved on from [ride] — the rider has answered + * the boarding question, say. Sessions are only ever replaced on the main + * thread and answers arrive there, so once the check has passed neither can + * happen before [action] has run. + */ + private fun postIfStillTracking( + trackedSession: TrackingSession, + ride: RideSnapshot? = null, + action: () -> Unit, + ) { + mainHandler.post { + if (session === trackedSession && (ride == null || currentRideSnapshot() === ride)) { + action() + } + } + } + + /** + * Whether [session] is the ride that was paused for want of a destination, + * now given one. + * + * On board with nowhere to get off there is nothing to remind the rider of, + * so the monitor asks them to choose a stop and pauses. Their answer starts + * it again — on the same bus. Coming back as "waiting to board" would throw + * the ride away at the very moment the reminders it was chosen for become + * possible, and mid-route there may be nothing left to detect it from. + */ + private fun continuesBoardedRide( + paused: TripMonitorPauseState?, + session: TrackingSession, + nowMs: Long, + ): Boolean { + paused ?: return false + return paused.reason == PAUSE_REASON_BOARDED_NO_DESTINATION && + paused.destinationStopId == null && + session.destinationStopId != null && + paused.provider == session.provider && + paused.routeKey == session.routeKey && + paused.pathId == session.pathId && + paused.boardingStopId == session.boardingStopId && + nowMs - paused.pausedAtMs in 0L..BOARDED_RIDE_RESUME_WINDOW_MS + } + override fun onDestroy() { + // A refresh still in flight has no trip left to apply itself to. + synchronized(detectorLock) { + session = null + detector = null + } mainHandler.removeCallbacksAndMessages(null) runCatching { if (::fusedLocationClient.isInitialized) { fusedLocationClient.removeLocationUpdates(locationCallback) } } + releaseWakeLock() ioExecutor.shutdownNow() super.onDestroy() } + /** + * A partial wake lock for as long as a trip is being tracked. + * + * With the screen off and the phone still, the CPU suspends between wakeups + * and the 15-second refresh loop stalls — the app cannot then tell "still + * waiting at the stop" from "already on the bus". Holding this keeps the + * loop and the detector's timers running for the length of the trip. + * + * This covers the idle/screen-off case. Deep Doze ignores wake locks + * outright, so a phone that has been motionless for half an hour is still + * at the mercy of the platform and the OEM; the lock is a mitigation, not + * a guarantee. + */ + private fun acquireWakeLock() { + val manager = getSystemService(PowerManager::class.java) ?: return + if (wakeLock?.isHeld == true) { + return + } + val lock = runCatching { + manager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG) + }.getOrNull() ?: return + lock.setReferenceCounted(false) + runCatching { lock.acquire(WAKE_LOCK_TIMEOUT_MS) } + wakeLock = lock + } + + private fun releaseWakeLock() { + val lock = wakeLock ?: return + wakeLock = null + runCatching { if (lock.isHeld) lock.release() } + } + private fun buildStartupSnapshot(session: TrackingSession): TrackingSnapshot { val startupLocation = latestLocation ?: createSessionLocation(session) val nearestIndex = startupLocation?.let { location -> @@ -367,11 +539,24 @@ class RouteTripMonitorService : Service() { Location("trip_monitor_session").apply { this.latitude = latitude this.longitude = longitude + // The route screen's last position can be minutes old by + // the time the trip starts; carrying when it was taken lets + // the detector judge that for itself. + session.initialLocationTimestampMs?.let { time = it } } } } } + private fun createDetector(session: TrackingSession): TripRideDetector { + return TripRideDetector( + stops = session.stops, + pathId = session.pathId, + boardingStopId = session.boardingStopId, + destinationStopId = session.destinationStopId, + ) + } + private fun ensureForegroundStarted(): Boolean { if (foregroundStarted) { return true @@ -417,7 +602,13 @@ class RouteTripMonitorService : Service() { } private fun requestLocationUpdates() { - if (session?.backgroundLocationAlwaysGranted != true) { + // Detection needs the user's own track, so updates are requested + // whenever any location permission is held. Requiring the "allow all + // the time" grant here is what used to leave the whole boarding state + // machine inert: a location-type foreground service started while the + // app was visible keeps while-in-use access after it goes to the + // background, so the stricter grant is not needed to keep tracking. + if (!hasLocationPermission()) { return } if (!::fusedLocationClient.isInitialized) { @@ -426,8 +617,12 @@ class RouteTripMonitorService : Service() { val request = LocationRequest.Builder( Priority.PRIORITY_HIGH_ACCURACY, LOCATION_UPDATE_INTERVAL_MS, - ).setMinUpdateDistanceMeters(20f) - .setMinUpdateIntervalMillis(LOCATION_MIN_UPDATE_INTERVAL_MS) + ).setMinUpdateIntervalMillis(LOCATION_MIN_UPDATE_INTERVAL_MS) + // No distance filter: a phone waiting at the stop must keep + // reporting, both so the app can tell the user is still there and + // so a fix is ready the moment they board. + .setMinUpdateDistanceMeters(0f) + .setWaitForAccurateLocation(false) .build() runCatching { @@ -441,14 +636,24 @@ class RouteTripMonitorService : Service() { fusedLocationClient.lastLocation.addOnSuccessListener { location -> if (location != null) { latestLocation = location - if (!appInForeground) { - refreshNotification() - } + onUserLocation(location) + refreshNotification() } } } } + private fun hasLocationPermission(): Boolean { + return ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.ACCESS_FINE_LOCATION, + ) == android.content.pm.PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission( + this, + android.Manifest.permission.ACCESS_COARSE_LOCATION, + ) == android.content.pm.PackageManager.PERMISSION_GRANTED + } + private fun startPolling() { mainHandler.removeCallbacks(pollingRunnable) if (!foregroundStarted) { @@ -462,7 +667,9 @@ class RouteTripMonitorService : Service() { } private fun refreshNotification(force: Boolean = false) { - val currentSession = session ?: return + if (session == null) { + return + } synchronized(refreshLock) { val now = SystemClock.elapsedRealtime() if (refreshInFlight) { @@ -482,25 +689,7 @@ class RouteTripMonitorService : Service() { val submitted = runCatching { ioExecutor.execute { try { - runCatching { - if (session !== currentSession) return@runCatching - if (force) { - // Rich notifications (including OEM integrations) are - // built off the main thread, after foreground promotion. - notificationManager.notify( - TRACKING_NOTIFICATION_ID, - buildTrackingNotification(buildStartupSnapshot(currentSession)), - ) - } - val trackingSnapshot = buildSnapshot( - currentSession, - latestLocation ?: createSessionLocation(currentSession), - ) - if (session !== currentSession) return@runCatching - val notification = buildTrackingNotification(trackingSnapshot) - notificationManager.notify(TRACKING_NOTIFICATION_ID, notification) - maybeSendTripAlerts(currentSession, trackingSnapshot) - } + runCatching { refreshTrip(force) } } finally { var shouldRefreshAgain = false synchronized(refreshLock) { @@ -530,306 +719,407 @@ class RouteTripMonitorService : Service() { } } - private fun buildSnapshot( + /** + * One refresh, on the IO thread: fetch, let the detector look again, and + * hand what is to be shown and alerted to the main thread. + * + * The trip is read here rather than where the refresh was asked for, and is + * checked again after every wait, because the main thread can replace it at + * any moment. + */ + private fun refreshTrip(force: Boolean) { + val trip = currentTrip() ?: return + val trackedSession = trip.session + if (force) { + // Rich notifications (including OEM integrations) are built off the + // main thread, after foreground promotion. + val startup = buildTrackingNotification(buildStartupSnapshot(trackedSession)) + postIfStillTracking(trackedSession) { + notificationManager.notify(TRACKING_NOTIFICATION_ID, startup) + } + } + // Fetched before the snapshot: everything the detector learns arrives + // through here. + if (!updateLiveData(trip)) { + return + } + // Advanced on every refresh, not just when the bus feed came back, + // because the question timing and the user's own progress move + // independently of it. + val ride = advanceRide(trip, System.currentTimeMillis()) ?: return + val trackingSnapshot = buildSnapshot( + trackedSession, + latestLocation ?: createSessionLocation(trackedSession), + ride, + ) + val notification = buildTrackingNotification(trackingSnapshot) + // Alerts can pause or stop the trip, so they are decided where trips + // are replaced and the rider's answers arrive: one worked out for the + // last trip, or from before "I am not on it", must not be acted on. + postIfStillTracking(trackedSession, ride) { + notificationManager.notify(TRACKING_NOTIFICATION_ID, notification) + maybeSendTripAlerts(trackedSession, trackingSnapshot) + } + } + + /** + * Pulls whatever the detector needs from the backend: the boarding stop's + * ETA, and the live positions of every vehicle on the route. + * + * Both feeds share one rate-limit bucket with the rest of the app, so they + * are throttled by age rather than fetched on every refresh, and a failed + * request backs that endpoint off instead of hammering the server. Every + * family route uses that same interval/back-off policy independently, so a + * failed stop feed or variant does not silence healthy vehicle endpoints. + * + * Returns false when [trip] was replaced while a request was out; nothing + * fetched after that point has been applied, and the caller should stop. + */ + private fun updateLiveData(trip: TrackedTrip): Boolean { + val session = trip.session + val routeIds = session.realtimeRouteIds + if (routeIds.isEmpty()) { + return true + } + val nowElapsed = SystemClock.elapsedRealtime() + val (wantsStops, busRouteIdsDue) = synchronized(refreshLock) { + val stopsDue = nowElapsed >= liveStopBackoffUntilMs && ( + cachedLiveRouteIds != routeIds || + cachedLivePathId != session.pathId || + nowElapsed - cachedLiveFetchedAtMs >= LIVE_STOP_REFRESH_INTERVAL_MS + ) + val busesDue = routeIds.filter { routeId -> + val state = busFeedRefreshByRoute[routeId] + state == null || (nowElapsed >= state.retryAtElapsedMs && + (state.fetchedAtElapsedMs == null || + nowElapsed - state.fetchedAtElapsedMs >= LIVE_BUS_REFRESH_INTERVAL_MS)) + } + stopsDue to busesDue + } + + if (wantsStops) { + val liveStops = fetchLiveStopMapFromBackend(session) + val stillTracked = applyToTrip(trip) { + // The same detector can receive a newer foreground update while + // this request is out. Discard its stop data and back-off only; + // the independently fetched vehicle positions remain useful. + if (stopFeedRevision != trip.stopFeedRevision) { + return@applyToTrip + } + // familyRealtime folds a failed request into an empty map, and a + // route with stops to look up that answers with nothing is a + // failure rather than an empty feed — so back off rather than + // spend the shared rate limit on retries. + if (liveStops.isEmpty()) { + synchronized(refreshLock) { + liveStopBackoffUntilMs = nowElapsed + LIVE_FETCH_BACKOFF_MS + } + } else { + synchronized(refreshLock) { + cachedLiveRouteIds = routeIds + cachedLivePathId = session.pathId + cachedLiveFetchedAtMs = nowElapsed + cachedLiveStops = liveStops + } + feedBoardingEta(trip.detector, session, liveStops) + } + } + if (!stillTracked) { + return false + } + } + + val reportsByRoute = linkedMapOf>() + for (routeId in busRouteIdsDue) { + val reports = fetchBusReportsFromBackend(routeId) + if (!applyToTrip(trip) { + // A confirmed ride survives family updates, but an endpoint + // removed from its family must not publish an in-flight result. + if (routeId !in this.session?.realtimeRouteIds.orEmpty()) { + return@applyToTrip + } + synchronized(refreshLock) { + val receivedAt = SystemClock.elapsedRealtime() + busFeedRefreshByRoute[routeId] = if (reports == null) { + (busFeedRefreshByRoute[routeId] ?: BusFeedRefresh()).copy( + retryAtElapsedMs = receivedAt + LIVE_FETCH_BACKOFF_MS, + ) + } else { + BusFeedRefresh(fetchedAtElapsedMs = receivedAt) + } + } + reports?.let { reportsByRoute[routeId] = it } + }) { + return false + } + } + return applyToTrip(trip) { + val currentRoutes = this.session?.realtimeRouteIds.orEmpty().toSet() + // A plate can occur in more than one endpoint; it is still one bus. + // Keep its newest report, rather than letting route iteration order + // choose which position drives a ride with no phone location. + val reports = reportsByRoute.filterKeys { it in currentRoutes }.values.flatten() + .groupBy { it.vehicleId }.values.mapNotNull { track -> track.maxByOrNull { it.gpsTimeMs } } + trip.detector.onBusReports(reports) + } + } + + private fun feedBoardingEta( + target: TripRideDetector, session: TrackingSession, - location: Location?, - ): TrackingSnapshot { - val liveStops = fetchLiveStopMap(session) - if (this.session !== session) return buildStartupSnapshot(session) - if (location == null) { - return TrackingSnapshot( - title = session.routeName, - content = "等待目前位置...", - subText = session.pathName.ifBlank { "背景乘車提醒進行中" }, - progressMax = null, - progressValue = null, - shortCriticalText = "定位中", + liveStops: Map, + ) { + val boardingStopId = session.boardingStopId ?: return + val liveStop = liveStops[boardingStopId] ?: return + val etaEntry = liveStop.etaEntries.firstOrNull() + // A plate on an ETA entry is a bus still on its way, possibly ten + // minutes out; only a bus the feed places at the stop counts as here. + val arriving = liveStop.seconds?.let { it <= 0 } == true || + liveStop.busesAtStop.isNotEmpty() || + liveStop.message?.contains("進站") == true || + liveStop.message?.contains("到站") == true + // Which bus that is, where the feed leaves no doubt: its leaving is + // what the ETA rolling over will report next. + val atStop = liveStop.busesAtStop.ifEmpty { + liveStop.etaEntries.filter { (it.seconds ?: 1) <= 0 }.mapNotNull { it.vehicleId } + }.mapNotNull(::normalizeVehicleId).distinct().singleOrNull() + val now = System.currentTimeMillis() + synchronized(detectorLock) { + target.onBoardingStopEta( + etaSeconds = liveStop.seconds ?: etaEntry?.seconds, + arriving = arriving, + nowMs = now, + arrivingVehicleId = atStop.takeIf { arriving }, ) } + } - val nearestIndex = findNearestStopIndex(session.stops, location) - if (nearestIndex == -1) { - return TrackingSnapshot( - title = session.routeName, - content = "暫時無法判斷最近站牌", - subText = session.pathName.ifBlank { "背景乘車提醒進行中" }, - progressMax = null, - progressValue = null, - shortCriticalText = "更新中", - ) + private fun refreshRideSnapshot(nowMs: Long) { + synchronized(detectorLock) { + latestRide = detector?.snapshot(nowMs) ?: RideSnapshot() } + } - val nearestStop = session.stops[nearestIndex] - val nearestStopDistanceMeters = distanceMeters( - location.latitude, - location.longitude, - nearestStop.lat, - nearestStop.lon, - ) - val nearestLiveStop = liveStops[nearestStop.stopId] + /** + * Lets [trip]'s detector look again and returns what it now makes of the + * ride; null when the trip has been replaced. + */ + private fun advanceRide(trip: TrackedTrip, nowMs: Long): RideSnapshot? = synchronized(detectorLock) { + if (detector !== trip.detector) { + return null + } + trip.detector.snapshot(nowMs).also { latestRide = it } + } + + private fun currentRideSnapshot(): RideSnapshot = synchronized(detectorLock) { latestRide } + + /** + * Builds the notification from the detector's view of the trip plus the + * live stop feed. The detector owns every "where is the user in the trip" + * decision; this only renders it. + */ + private fun buildSnapshot( + session: TrackingSession, + location: Location?, + ride: RideSnapshot, + ): TrackingSnapshot { + val liveStops = fetchLiveStopMap(session) + if (this.session !== session) return buildStartupSnapshot(session) + // A missing fix is not an early exit: the detector can still raise the + // boarding question without any location at all, and the notification + // is built from what it knows rather than from a nearest-stop guess. + val nearestIndex = location?.let { findNearestStopIndex(session.stops, it) } ?: -1 + val nearestStop = session.stops.getOrNull(nearestIndex) + val nearestLiveStop = nearestStop?.let { liveStops[it.stopId] } val nearestEtaText = displayEtaText(nearestLiveStop) - val busIndex = findClosestBusIndex(session.stops, liveStops, nearestIndex) - val destinationIndex = session.destinationStopId?.let { destinationStopId -> - session.stops.indexOfFirst { stop -> stop.stopId == destinationStopId } - .takeIf { it >= 0 } - } - - if (destinationIndex == null && session.boardingStopId != null) { - val boardingIndex = session.stops.indexOfFirst { stop -> - stop.stopId == session.boardingStopId - }.takeIf { it >= 0 } ?: nearestIndex - val boardingStop = session.stops[boardingIndex] - val boardingLiveStop = liveStops[boardingStop.stopId] - val boardingVehicleCandidate = resolveBoardingVehicleCandidate( - stops = session.stops, - liveStops = liveStops, - location = location, - nearestIndex = nearestIndex, - boardingIndex = boardingIndex, - ) - val boardingVehicleId = boardingVehicleCandidate?.vehicleId ?: activeBoardingVehicleId - val boardingEtaText = displayEtaText(boardingLiveStop, boardingVehicleId) - val boardingEtaSeconds = resolveEtaSeconds(boardingLiveStop, boardingVehicleId) - val boardingDistanceMeters = distanceMeters( - location.latitude, - location.longitude, - boardingStop.lat, - boardingStop.lon, - ) - val fallbackBoardingBusIndex = findClosestBusIndexForBoarding( - stops = session.stops, - liveStops = liveStops, - boardingIndex = boardingIndex, - ) - val boardingBusIndex = - boardingVehicleCandidate?.stopIndex ?: activeBoardingVehicleStopIndex ?: fallbackBoardingBusIndex - val busStopsUntilBoarding = boardingBusIndex?.let { - (boardingIndex - it).coerceAtLeast(0) - } - val rideState = if (session.backgroundLocationAlwaysGranted) { - updateRideState( - location = location, - nearestIndex = nearestIndex, - boardingIndex = boardingIndex, - boardingBusIndex = boardingBusIndex, - boardingVehicleId = boardingVehicleId, - boardingEtaText = boardingEtaText, - boardingEtaSeconds = boardingEtaSeconds, - boardingDistanceMeters = boardingDistanceMeters, - ) - } else { - resolveNoLocationRideState( - busStopsUntilBoarding = busStopsUntilBoarding, - boardingEtaText = boardingEtaText, - boardingEtaSeconds = boardingEtaSeconds, - ) - } - val userBusLiveStop = busIndex?.let { liveStops[session.stops[it].stopId] } - trackedBusId = if (rideState.hasBoarded) { - selectTrackedBusId( + val nearestStopDistanceMeters = if (location != null && nearestStop != null) { + distanceMeters(location.latitude, location.longitude, nearestStop.lat, nearestStop.lon) + } else { + null + } + val destinationIndex = session.destinationStopId + ?.let { id -> session.stops.indexOfFirst { it.stopId == id } } + ?.takeIf { it >= 0 } + ?: -1 + val destinationStop = session.stops.getOrNull(destinationIndex) + val boardingIndex = ride.boardingStopIndex.takeIf { it >= 0 } + ?: session.boardingStopId + ?.let { id -> session.stops.indexOfFirst { it.stopId == id } } + ?.takeIf { it >= 0 } + ?: -1 + val boardingStop = session.stops.getOrNull(boardingIndex) + + val boardingLiveStop = boardingStop?.let { liveStops[it.stopId] } + // Prefer the vehicle the detector is following; fall back to whatever + // the stop-level feed says so the countdown still works where the ETA + // entries carry no plate (雙北). + val boardingVehicleId = ride.nextBusVehicleId + val boardingEtaText = displayEtaText(boardingLiveStop, boardingVehicleId) + val boardingEtaSeconds = resolveEtaSeconds(boardingLiveStop, boardingVehicleId) + val boardingEtaShort = displayShortEtaText(boardingLiveStop, boardingVehicleId) + val busStopsUntilBoarding = ride.busStopsUntilBoarding + val busStopsAway = nearestIndex.takeIf { it >= 0 }?.let { index -> + ride.trackedVehicleId?.let { tracked -> + findTrackedBusIndex( stops = session.stops, liveStops = liveStops, - currentTrackedBusId = trackedBusId ?: normalizeVehicleId(boardingVehicleId), - fallbackLiveStops = listOf( - userBusLiveStop, - nearestLiveStop, - boardingLiveStop, - ), + preferredVehicleId = tracked, ) - } else { - trackedBusLastSeenAtMs = 0L - null - } - val busStopsAway = busIndex?.let { (nearestIndex - it).coerceAtLeast(0) } - val boardingPointCount = boardingIndex + 1 - val boardingProgressPoint = if (rideState.hasBoarded) { - val trackedPos = boardingBusIndex ?: nearestIndex - (trackedPos + 1).coerceIn(1, boardingPointCount) - } else { - boardingBusIndex?.plus(1)?.coerceAtMost(boardingPointCount) ?: 0 - } + }?.let { (index - it).coerceAtLeast(0) } + } + + if (ride.hasBoarded && destinationStop == null) { + // On board with nowhere to get off yet. This comes before every + // waiting branch — including the one for a trip with neither end + // chosen — because a confirmed ride has to be reported as one: + // that is what makes the destination-setup prompt fire. + val travelIndex = ride.progressStopIndex.takeIf { it >= 0 } + ?: ride.nearestStopIndexOnBoard(liveStops, session.stops) + ?: nearestIndex + val currentStop = session.stops.getOrNull(travelIndex) return TrackingSnapshot( title = session.routeName, - content = "${nearestStop.stopName} 約 $nearestEtaText", - subText = buildNearestStatusText( - session = session, - nearestStop = nearestStop, - nearestEtaText = nearestEtaText, - busStopsAway = busStopsAway, - ), - progressMax = toProgressMax(boardingPointCount), - progressValue = toProgressValue(boardingProgressPoint, boardingPointCount), - shortCriticalText = buildShortCriticalText( - busStopsAway, - displayShortEtaText(nearestLiveStop), + content = if (currentStop != null) { + "已上車 · 最近站牌 ${currentStop.stopName}" + } else { + "已上車 · 追蹤中" + }, + subText = buildOnboardTripSubText( + currentStop?.stopName ?: session.pathName.ifBlank { "乘車中" }, ), - hasBoarded = rideState.hasBoarded, - boardingName = boardingStop.stopName, - boardingEtaText = boardingEtaText, - boardingEtaSeconds = boardingEtaSeconds, - boardingStopsAway = busStopsUntilBoarding, - boardingDistanceMeters = boardingDistanceMeters, - boardingVehicleId = boardingVehicleId, - boardingVehiclePassedStop = rideState.boardingVehiclePassedStop, - boardingPromptEligible = rideState.boardingPromptEligible, + progressMax = null, + progressValue = null, + shortCriticalText = null, + hasBoarded = true, + boardingName = boardingStop?.stopName, + boardingVehicleId = ride.trackedVehicleId ?: boardingVehicleId, nearestStopDistanceMeters = nearestStopDistanceMeters, ) } - if (destinationIndex == null) { - trackedBusId = null - trackedBusLastSeenAtMs = 0L - val busStopsAway = busIndex?.let { (nearestIndex - it).coerceAtLeast(0) } + if (boardingStop == null && destinationStop == null) { + // Neither end of the trip is known: just report the nearest stop. return TrackingSnapshot( title = session.routeName, - content = "${nearestStop.stopName} · $nearestEtaText", - subText = buildNearestStatusText( - session = session, - nearestStop = nearestStop, - nearestEtaText = nearestEtaText, - busStopsAway = busStopsAway, - ), + content = if (nearestStop != null) { + "${nearestStop.stopName} · $nearestEtaText" + } else { + session.pathName.ifBlank { "乘車提醒進行中" } + }, + subText = if (nearestStop != null) { + buildNearestStatusText(session, nearestStop, nearestEtaText, busStopsAway) + } else { + session.pathName.ifBlank { "背景乘車提醒進行中" } + }, progressMax = null, progressValue = null, shortCriticalText = buildShortCriticalText( busStopsAway, displayShortEtaText(nearestLiveStop), ), + nearestStopDistanceMeters = nearestStopDistanceMeters, ) } - val destinationStop = session.stops[destinationIndex] - val boardingIndex = resolveBoardingIndex(session, nearestIndex, destinationIndex) - val boardingStop = session.stops[boardingIndex] - val boardingLiveStop = liveStops[boardingStop.stopId] - val boardingVehicleCandidate = resolveBoardingVehicleCandidate( - stops = session.stops, - liveStops = liveStops, - location = location, - nearestIndex = nearestIndex, - boardingIndex = boardingIndex, - ) - val boardingVehicleId = boardingVehicleCandidate?.vehicleId ?: activeBoardingVehicleId - val boardingEtaText = displayEtaText(boardingLiveStop, boardingVehicleId) - val boardingEtaShort = displayShortEtaText(boardingLiveStop, boardingVehicleId) - val boardingEtaSeconds = resolveEtaSeconds(boardingLiveStop, boardingVehicleId) - val boardingDistanceMeters = distanceMeters( - location.latitude, - location.longitude, - boardingStop.lat, - boardingStop.lon, - ) - val fallbackBoardingBusIndex = findClosestBusIndexForBoarding( - stops = session.stops, - liveStops = liveStops, - boardingIndex = boardingIndex, - ) - val boardingBusIndex = - boardingVehicleCandidate?.stopIndex ?: activeBoardingVehicleStopIndex ?: fallbackBoardingBusIndex - val busStopsUntilBoarding = boardingBusIndex?.let { - (boardingIndex - it).coerceAtLeast(0) - } - val rideState = if (session.backgroundLocationAlwaysGranted) { - updateRideState( - location = location, - nearestIndex = nearestIndex, - boardingIndex = boardingIndex, - boardingBusIndex = boardingBusIndex, - boardingVehicleId = boardingVehicleId, - boardingEtaText = boardingEtaText, - boardingEtaSeconds = boardingEtaSeconds, - boardingDistanceMeters = boardingDistanceMeters, - ) - } else { - resolveNoLocationRideState( - busStopsUntilBoarding = busStopsUntilBoarding, - boardingEtaText = boardingEtaText, - boardingEtaSeconds = boardingEtaSeconds, - ) - } - - if (!rideState.hasBoarded) { - trackedBusId = null - trackedBusLastSeenAtMs = 0L - val boardingPointCount = boardingIndex + 1 - val boardingProgressPoint = - boardingBusIndex?.plus(1)?.coerceAtMost(boardingPointCount) ?: 0 + if (destinationStop == null || !ride.hasBoarded) { + val waitingStop = boardingStop ?: nearestStop + if (waitingStop == null) { + return TrackingSnapshot( + title = session.routeName, + content = "等待目前位置...", + subText = session.pathName.ifBlank { "背景乘車提醒進行中" }, + progressMax = null, + progressValue = null, + shortCriticalText = "定位中", + ) + } + val boardingPointCount = boardingIndex.takeIf { it >= 0 }?.plus(1) ?: 1 + val busPoint = ride.nextBusVehicleId + ?.let { tracked -> + findTrackedBusIndex( + stops = session.stops, + liveStops = liveStops, + preferredVehicleId = tracked, + ) + } + ?.plus(1) + ?.coerceAtMost(boardingPointCount) + ?: 0 return TrackingSnapshot( - title = "${session.routeName} · ${boardingStop.stopName}", - content = "公車到 ${boardingStop.stopName} 約 $boardingEtaText", - subText = buildWaitingBoardingText( - session = session, - boardingStop = boardingStop, - destinationStop = destinationStop, - busStopsUntilBoarding = busStopsUntilBoarding, - ), + title = if (destinationStop == null) { + session.routeName + } else { + "${session.routeName} · ${waitingStop.stopName}" + }, + content = if (destinationStop == null) { + "${waitingStop.stopName} 約 ${displayEtaText(liveStops[waitingStop.stopId])}" + } else { + "公車到 ${waitingStop.stopName} 約 $boardingEtaText" + }, + subText = if (destinationStop == null) { + buildNearestStatusText( + session = session, + nearestStop = waitingStop, + nearestEtaText = displayEtaText(liveStops[waitingStop.stopId]), + busStopsAway = busStopsUntilBoarding, + ) + } else { + buildWaitingBoardingText( + session = session, + boardingStop = waitingStop, + destinationStop = destinationStop, + busStopsUntilBoarding = busStopsUntilBoarding, + ) + }, progressMax = toProgressMax(boardingPointCount), - progressValue = toProgressValue(boardingProgressPoint, boardingPointCount), + progressValue = toProgressValue(busPoint, boardingPointCount), shortCriticalText = buildShortCriticalText( busStopsUntilBoarding, - boardingEtaShort, + if (destinationStop == null) { + displayShortEtaText(liveStops[waitingStop.stopId]) + } else { + boardingEtaShort + }, ), - boardingName = boardingStop.stopName, + hasBoarded = false, + boardingName = waitingStop.stopName, boardingEtaText = boardingEtaText, boardingEtaSeconds = boardingEtaSeconds, boardingStopsAway = busStopsUntilBoarding, - boardingDistanceMeters = boardingDistanceMeters, - hasBoarded = false, + boardingDistanceMeters = ride.boardingStopDistanceMeters, boardingVehicleId = boardingVehicleId, - boardingVehiclePassedStop = rideState.boardingVehiclePassedStop, - boardingPromptEligible = rideState.boardingPromptEligible, - destinationName = destinationStop.stopName, + boardingVehiclePassedStop = false, + boardingPromptEligible = ride.boardingPromptDue, + destinationName = destinationStop?.stopName, nearestStopDistanceMeters = nearestStopDistanceMeters, ) } + // On board: progress comes from the user's own position, falling back to + // the vehicle they are riding when the phone stops reporting. + val travelIndex = ride.progressStopIndex.takeIf { it >= 0 } + ?: ride.nearestStopIndexOnBoard(liveStops, session.stops) + ?: nearestIndex + val journeyStartIndex = boardingIndex.coerceAtLeast(0).coerceAtMost(destinationIndex) + val journeyPointCount = (destinationIndex - journeyStartIndex + 1).coerceAtLeast(1) + val currentProgressPoint = if (travelIndex >= 0) { + (travelIndex.coerceAtLeast(journeyStartIndex) - journeyStartIndex) + 1 + } else { + 1 + }.coerceIn(1, journeyPointCount) val destinationLive = liveStops[destinationStop.stopId] - val userBusLiveStop = busIndex?.let { liveStops[session.stops[it].stopId] } - trackedBusId = selectTrackedBusId( - stops = session.stops, - liveStops = liveStops, - currentTrackedBusId = trackedBusId ?: normalizeVehicleId(boardingVehicleId), - fallbackLiveStops = listOf( - userBusLiveStop, - nearestLiveStop, - boardingLiveStop, - destinationLive, - ), - ) - val trackedBusIndex = - findTrackedBusIndex( - stops = session.stops, - liveStops = liveStops, - preferredVehicleId = trackedBusId, - ) - // Once on board the user travels with the bus, so their own position is - // a better stand-in than some other vehicle when the tracked one drops - // out of the feed. - val travelIndex = trackedBusIndex ?: nearestIndex - val rawRemainingStops = destinationIndex - travelIndex - val remainingStops = rawRemainingStops.coerceAtLeast(0) - val allowAnyDestinationVehicle = trackedBusId == null val destinationEtaText = displayEtaText( destinationLive, - trackedBusId, - fallbackToAnyVehicle = allowAnyDestinationVehicle, + ride.trackedVehicleId, + fallbackToAnyVehicle = ride.trackedVehicleId == null, ) val destinationEtaShort = displayShortEtaText( destinationLive, - trackedBusId, - fallbackToAnyVehicle = allowAnyDestinationVehicle, + ride.trackedVehicleId, + fallbackToAnyVehicle = ride.trackedVehicleId == null, ) - val destinationDistanceMeters = distanceMeters( - location.latitude, - location.longitude, - destinationStop.lat, - destinationStop.lon, - ) - val journeyStartIndex = boardingIndex.coerceAtMost(destinationIndex) - val journeyPointCount = (destinationIndex - journeyStartIndex + 1).coerceAtLeast(1) - val currentProgressPoint = - ((travelIndex.coerceAtLeast(journeyStartIndex) - journeyStartIndex) + 1) - .coerceIn(1, journeyPointCount) + val remainingStops = ride.remainingStops ?: (destinationIndex - journeyStartIndex) + .coerceAtLeast(0) return TrackingSnapshot( title = "${session.routeName} · ${destinationStop.stopName}", @@ -837,519 +1127,53 @@ class RouteTripMonitorService : Service() { remainingStops == 0 -> "已接近 ${destinationStop.stopName}" else -> "距離 ${destinationStop.stopName} 還有 $remainingStops 站 · $destinationEtaText" }, - subText = buildOnboardTripSubText(nearestStop.stopName), + subText = buildOnboardTripSubText( + nearestStop?.stopName ?: destinationStop.stopName, + ), progressMax = toProgressMax(journeyPointCount), progressValue = toProgressValue(currentProgressPoint, journeyPointCount), - shortCriticalText = buildShortCriticalText( - remainingStops, - destinationEtaShort, - ), + shortCriticalText = buildShortCriticalText(remainingStops, destinationEtaShort), hasBoarded = true, destinationName = destinationStop.stopName, remainingStops = remainingStops, - destinationDistanceMeters = destinationDistanceMeters, - boardingName = boardingStop.stopName, - boardingVehicleId = trackedBusId ?: boardingVehicleId, - passedDestinationByStops = (-rawRemainingStops).takeIf { it > 0 }, + destinationDistanceMeters = ride.destinationDistanceMeters, + boardingName = boardingStop?.stopName, + boardingVehicleId = ride.trackedVehicleId ?: boardingVehicleId, + passedDestinationByStops = ride.passedDestinationByStops, nearestStopDistanceMeters = nearestStopDistanceMeters, + hasRouteValidProgress = ride.hasRouteValidProgress, ) } - private fun buildSnapshotLegacy( - session: TrackingSession, - location: Location?, - ): TrackingSnapshot { - val liveStops = fetchLiveStopMap(session) - if (location == null) { - return TrackingSnapshot( - title = session.routeName, - content = "等待目前位置...", - subText = session.pathName.ifBlank { "背景乘車提醒進行中" }, - progressMax = null, - progressValue = null, - shortCriticalText = "定位中", - ) - } - - val nearestIndex = session.stops - .indices - .minByOrNull { index -> - val stop = session.stops[index] - distanceMeters( - location.latitude, - location.longitude, - stop.lat, - stop.lon, - ) - } ?: -1 - if (nearestIndex == -1) { - return TrackingSnapshot( - title = session.routeName, - content = "暫時無法判斷最近站牌", - subText = session.pathName.ifBlank { "背景乘車提醒進行中" }, - progressMax = null, - progressValue = null, - shortCriticalText = "更新中", - ) - } - - val nearestStop = session.stops[nearestIndex] - val nearestLiveStop = liveStops[nearestStop.stopId] - val busIndex = findClosestBusIndex(session.stops, liveStops, nearestIndex) - val busStopsAway = busIndex?.let { (nearestIndex - it).coerceAtLeast(0) } - val nearestEtaText = formatEtaText(nearestLiveStop) - val nearestEtaShort = formatShortEtaText(nearestLiveStop) - val nearestSubText = buildNearestSubText( - session = session, - nearestStop = nearestStop, - busStopsAway = busStopsAway, - nearestEtaText = nearestEtaText, - ) - - val destinationIndex = session.destinationStopId?.let { destinationStopId -> - session.stops.indexOfFirst { stop -> stop.stopId == destinationStopId } - .takeIf { it >= 0 } - } - if (destinationIndex == null) { - return TrackingSnapshot( - title = session.routeName, - content = "${nearestStop.stopName} · $nearestEtaText", - subText = nearestSubText, - progressMax = null, - progressValue = null, - shortCriticalText = buildShortCriticalText(busStopsAway, nearestEtaShort), - ) - } - - val destinationStop = session.stops[destinationIndex] - val destinationLive = liveStops[destinationStop.stopId] - val remainingStops = (destinationIndex - nearestIndex).coerceAtLeast(0) - val destinationEtaText = formatEtaText(destinationLive) - val destinationEtaShort = formatShortEtaText(destinationLive) - val destinationDistanceMeters = distanceMeters( - location.latitude, - location.longitude, - destinationStop.lat, - destinationStop.lon, - ) - val pointCount = destinationIndex + 1 - val currentProgressPoint = (busIndex ?: nearestIndex).plus(1).coerceAtMost(pointCount) - - return TrackingSnapshot( - title = "${session.routeName} · ${destinationStop.stopName}", - content = when { - remainingStops == 0 -> "已接近 ${destinationStop.stopName}" - else -> "${destinationStop.stopName} 還有 $remainingStops 站 · $destinationEtaText" - }, - subText = "最近站牌 ${nearestStop.stopName} · $nearestEtaText", - progressMax = toProgressMax(pointCount), - progressValue = toProgressValue(currentProgressPoint, pointCount), - destinationName = destinationStop.stopName, - remainingStops = remainingStops, - destinationDistanceMeters = destinationDistanceMeters, - shortCriticalText = buildShortCriticalText(remainingStops, destinationEtaShort), - ) - } - - private fun resolveBoardingIndex( - session: TrackingSession, - nearestIndex: Int, - destinationIndex: Int, - ): Int { - val explicitBoardingIndex = session.boardingStopId?.let { boardingStopId -> - session.stops.indexOfFirst { stop -> stop.stopId == boardingStopId } - .takeIf { it >= 0 } - } - return (explicitBoardingIndex ?: nearestIndex).coerceAtMost(destinationIndex) - } - - private fun findNearestStopIndex( - stops: List, - location: Location, - ): Int { - return stops.indices.minByOrNull { index -> - val stop = stops[index] - distanceMeters( - location.latitude, - location.longitude, - stop.lat, - stop.lon, - ) - } ?: -1 - } - - private fun findClosestStopIndices( - stops: List, - location: Location, - count: Int, - ): List { - if (count <= 0) { - return emptyList() - } - return stops.indices - .sortedBy { index -> - val stop = stops[index] - distanceMeters( - location.latitude, - location.longitude, - stop.lat, - stop.lon, - ) - } - .take(count) - } - - private fun resolveBoardingVehicleCandidate( - stops: List, + /** + * Last resort for on-board progress when the user's own fixes are gone: the + * stop the tracked vehicle has reached, found through the live feed. + */ + private fun RideSnapshot.nearestStopIndexOnBoard( liveStops: Map, - location: Location, - nearestIndex: Int, - boardingIndex: Int, - ): VehicleCandidate? { - val now = System.currentTimeMillis() - val nearbyVehicleIds = linkedSetOf() - findClosestStopIndices(stops, location, USER_NEARBY_STOP_COUNT).forEach { index -> - nearbyVehicleIds += extractNormalizedVehicleIds(liveStops[stops[index].stopId]) - } - val normalizedActiveVehicleId = normalizeVehicleId(activeBoardingVehicleId) - val activeVehicleStillFresh = normalizedActiveVehicleId != null && - activeBoardingVehicleLastSeenAtMs != 0L && - now - activeBoardingVehicleLastSeenAtMs < ACTIVE_BOARDING_VEHICLE_STALE_MS - if (activeVehicleStillFresh) { - nearbyVehicleIds += normalizedActiveVehicleId!! - } - - val candidates = nearbyVehicleIds.mapNotNull { vehicleId -> - buildVehicleCandidate( - stops = stops, - liveStops = liveStops, - location = location, - vehicleId = vehicleId, - ) - } - val activeCandidate = candidates.firstOrNull { it.vehicleId == normalizedActiveVehicleId } - if (activeCandidate != null) { - activeBoardingVehicleStopIndex = activeCandidate.stopIndex - activeBoardingVehicleLastSeenAtMs = now - return activeCandidate - } - - // The locked vehicle dropped out of the live feed (bus GPS hiccups are - // routine). Hold the lock during the grace window instead of jumping - // to the next bus, so the original bus re-locks when it reappears. - if (activeVehicleStillFresh) { - return null - } - if (normalizedActiveVehicleId != null) { - activeBoardingVehicleId = null - activeBoardingVehicleStopIndex = null - activeBoardingVehicleLastSeenAtMs = 0L - activeBoardingVehiclePassedAtMs = 0L - } - - val selectedCandidate = candidates.minWithOrNull( - compareBy { if (it.stopIndex > boardingIndex) 1 else 0 } - .thenBy { - if (it.stopIndex <= boardingIndex) { - boardingIndex - it.stopIndex - } else { - it.stopIndex - boardingIndex - } - } - .thenBy { abs(it.stopIndex - nearestIndex) } - .thenBy { it.distanceMeters } - .thenBy { it.etaSeconds ?: Int.MAX_VALUE }, - ) ?: return null - - val nextVehicleId = selectedCandidate.vehicleId - if (normalizedActiveVehicleId != null && normalizedActiveVehicleId != nextVehicleId && !rideConfirmed) { - boardingAlertSent = false - boardingCheckPromptSent = false - boardingCheckPromptSentAtMs = 0L - boardingWindowOpen = false - activeBoardingVehiclePassedAtMs = 0L - rideConfirmationSamples = 0 - notificationManager.cancel(ALERT_NOTIFICATION_ID) - } - activeBoardingVehicleId = nextVehicleId - activeBoardingVehicleStopIndex = selectedCandidate.stopIndex - activeBoardingVehicleLastSeenAtMs = now - return selectedCandidate - } - - private fun buildVehicleCandidate( stops: List, - liveStops: Map, - location: Location, - vehicleId: String, - ): VehicleCandidate? { - val normalizedVehicleId = normalizeVehicleId(vehicleId) ?: return null - val stopIndex = findTrackedBusIndex( - stops = stops, - liveStops = liveStops, - preferredVehicleId = normalizedVehicleId, - ) ?: return null - val stop = stops[stopIndex] - val stopState = liveStops[stop.stopId] - return VehicleCandidate( - vehicleId = normalizedVehicleId, - stopIndex = stopIndex, - distanceMeters = distanceMeters( - location.latitude, - location.longitude, - stop.lat, - stop.lon, - ), - etaSeconds = resolveEtaSeconds(stopState, normalizedVehicleId), - ) - } - - private fun extractNormalizedVehicleIds(stopState: LiveStopState?): Set { - val result = linkedSetOf() - stopState?.etaEntries?.forEach { etaEntry -> - normalizeVehicleId(etaEntry.vehicleId)?.let(result::add) - } - stopState?.vehicleIds?.forEach { vehicleId -> - normalizeVehicleId(vehicleId)?.let(result::add) - } - return result - } - - private fun updateRideState( - location: Location, - nearestIndex: Int, - boardingIndex: Int, - boardingBusIndex: Int?, - boardingVehicleId: String?, - boardingEtaText: String, - boardingEtaSeconds: Int?, - boardingDistanceMeters: Double, - ): RideStateUpdate { - val userNearBoardingStop = - boardingDistanceMeters <= BOARDING_STOP_RADIUS_METERS || nearestIndex == boardingIndex - val busNearBoardingStop = - ( - boardingBusIndex != null && - boardingBusIndex <= boardingIndex && - boardingIndex - boardingBusIndex <= 1 && - isLikelyBoardingArrival( - etaSeconds = boardingEtaSeconds, - etaText = boardingEtaText, - ) - ) || - isImmediateEtaText(boardingEtaText) - if (userNearBoardingStop && busNearBoardingStop) { - if (!boardingWindowOpen) { - boardingWindowOpenedAtMs = System.currentTimeMillis() - } - boardingWindowOpen = true - } else if (boardingWindowOpen && boardingWindowOpenedAtMs != 0L) { - val windowAge = System.currentTimeMillis() - boardingWindowOpenedAtMs - if (windowAge >= BOARDING_WINDOW_MAX_DURATION_MS) { - boardingWindowOpen = false - boardingWindowOpenedAtMs = 0L - } - } - - val movement = updateUserMovement(location, nearestIndex) - val previousNearest = lastNearestStopIndex - val previousBusIndex = lastBusStopIndex - if (movement.sampleAdvanced) { - lastNearestStopIndex = nearestIndex - } - lastBusStopIndex = boardingBusIndex - val movedForward = previousNearest != null && nearestIndex > previousNearest - val busMovedForward = - previousBusIndex != null && - boardingBusIndex != null && - boardingBusIndex > previousBusIndex - val busNearUser = boardingBusIndex != null && abs(nearestIndex - boardingBusIndex) <= 1 - val vehiclePassedBoarding = - normalizeVehicleId(boardingVehicleId) != null && - boardingBusIndex != null && - boardingBusIndex > boardingIndex - if (vehiclePassedBoarding) { - if (activeBoardingVehiclePassedAtMs == 0L) { - activeBoardingVehiclePassedAtMs = System.currentTimeMillis() - } - } else if (boardingBusIndex != null && boardingBusIndex <= boardingIndex) { - activeBoardingVehiclePassedAtMs = 0L - } - val userReachedBoarding = nearestIndex >= boardingIndex - val userAdvancedPastBoardingStop = nearestIndex > boardingIndex - val userMovingWithBus = - movement.isTransitLike && - (busNearUser || vehiclePassedBoarding) && - (movedForward || movement.routeIndexDelta > 0 || boardingDistanceMeters > BOARDING_CONFIRM_DISTANCE_METERS) - val strongBoardingSignal = - boardingWindowOpen && - userReachedBoarding && - vehiclePassedBoarding && - ( - userMovingWithBus || - ( - busMovedForward && - userAdvancedPastBoardingStop && - boardingDistanceMeters <= BOARDING_CONFIRM_DISTANCE_METERS - ) - ) - - if (!rideConfirmed && movement.sampleAdvanced) { - // Count evidence only on fresh GPS samples, and decay instead of - // hard-resetting so a single noisy fix cannot erase progress. - rideConfirmationSamples = if (strongBoardingSignal) { - rideConfirmationSamples + 1 - } else { - (rideConfirmationSamples - 1).coerceAtLeast(0) - } - if (rideConfirmationSamples >= REQUIRED_RIDE_CONFIRMATION_SAMPLES) { - rideConfirmed = true - boardingCheckPromptSent = true - boardingCheckPromptSentAtMs = System.currentTimeMillis() - normalizeVehicleId(boardingVehicleId)?.let { vehicleId -> - trackedBusId = vehicleId - trackedBusLastSeenAtMs = System.currentTimeMillis() - } - notificationManager.cancel(ALERT_NOTIFICATION_ID) - } - } - - val boardingPromptEligible = - !rideConfirmed && - boardingWindowOpen && - vehiclePassedBoarding && - !movement.isTransitLike && - activeBoardingVehiclePassedAtMs != 0L && - System.currentTimeMillis() - activeBoardingVehiclePassedAtMs >= - BOARDING_CHECK_AFTER_PASS_DELAY_MS - - return RideStateUpdate( - hasBoarded = rideConfirmed, - boardingVehiclePassedStop = vehiclePassedBoarding, - boardingPromptEligible = boardingPromptEligible, - ) - } - - private fun updateUserMovement( - location: Location, - nearestIndex: Int, - ): UserMovementState { - val now = SystemClock.elapsedRealtime() - val previousLocation = lastMovementLocation - - // Re-processing the same fix (refreshes happen more often than GPS - // updates) or a junk fix must not count as "the user stopped moving" — - // keep the previous verdict instead of resetting it. - val isSameFix = previousLocation != null && - previousLocation.time == location.time && - previousLocation.latitude == location.latitude && - previousLocation.longitude == location.longitude - val isPoorAccuracy = location.hasAccuracy() && - location.accuracy > MAX_USABLE_LOCATION_ACCURACY_METERS - if (isSameFix || isPoorAccuracy) { - return UserMovementState( - routeIndexDelta = 0, - isTransitLike = isTransitLikeStillFresh(now), - sampleAdvanced = false, - ) - } - - val previousNearestIndex = lastMovementNearestIndex - val previousRecordedAtMs = lastMovementRecordedAtMs - val distanceMovedMeters = if (previousLocation == null) { - 0.0 - } else { - distanceMeters( - previousLocation.latitude, - previousLocation.longitude, - location.latitude, - location.longitude, - ) - } - val elapsedMs = if (previousRecordedAtMs == 0L) 0L else now - previousRecordedAtMs - val estimatedSpeedMetersPerSecond = - elapsedMs.takeIf { it > 0L }?.let { distanceMovedMeters / (it / 1_000.0) } - // GPS chips report instantaneous speed far more reliably than the - // distance between two throttled notification refreshes. - val reportedSpeedMetersPerSecond = location.speed - .takeIf { location.hasSpeed() && it > 0f } - ?.toDouble() - val routeIndexDelta = if (previousNearestIndex == null) { - 0 - } else { - nearestIndex - previousNearestIndex - } - lastMovementLocation = Location(location) - lastMovementNearestIndex = nearestIndex - lastMovementRecordedAtMs = now - - val transitLikeEvidence = - ( - reportedSpeedMetersPerSecond != null && - reportedSpeedMetersPerSecond >= USER_TRANSIT_LIKE_SPEED_MPS - ) || - ( - estimatedSpeedMetersPerSecond != null && - distanceMovedMeters >= USER_MOVEMENT_MIN_DISTANCE_METERS && - estimatedSpeedMetersPerSecond >= USER_TRANSIT_LIKE_SPEED_MPS - ) || - ( - routeIndexDelta > 0 && - distanceMovedMeters >= USER_ROUTE_PROGRESS_MIN_DISTANCE_METERS - ) - if (transitLikeEvidence) { - lastTransitLikeAtMs = now - } - return UserMovementState( - routeIndexDelta = routeIndexDelta, - // Buses stop at lights and stations; hold the transit verdict for a - // short window instead of flapping on every slow sample. - isTransitLike = transitLikeEvidence || isTransitLikeStillFresh(now), - sampleAdvanced = true, - ) - } - - private fun isTransitLikeStillFresh(nowElapsedMs: Long): Boolean { - return lastTransitLikeAtMs != 0L && - nowElapsedMs - lastTransitLikeAtMs < TRANSIT_LIKE_STICKY_MS - } - - private fun hasBusReachedBoardingStop( - busStopsUntilBoarding: Int?, - boardingEtaText: String, - boardingEtaSeconds: Int?, - ): Boolean { - if (busStopsUntilBoarding == null || busStopsUntilBoarding > 0) { - return false - } - if (isImmediateEtaText(boardingEtaText)) { - return true - } - // Without background location we cannot verify the user actually got - // on, so only treat the bus as boarded when it is genuinely at the - // stop — a 3-minute ETA is way too early. - return boardingEtaSeconds != null && - boardingEtaSeconds <= BOARDING_NO_LOCATION_MAX_ETA_SECONDS + ): Int? { + val vehicleId = trackedVehicleId ?: return null + return findTrackedBusIndex( + stops = stops, + liveStops = liveStops, + preferredVehicleId = vehicleId, + ) } - private fun resolveNoLocationRideState( - busStopsUntilBoarding: Int?, - boardingEtaText: String, - boardingEtaSeconds: Int?, - ): RideStateUpdate { - // Latch the boarded state: once the tracked bus reaches the boarding - // stop the ETA feed rolls over to the NEXT bus, which used to flip the - // notification back to "not boarded" until the user re-confirmed. - if (!rideConfirmed && - hasBusReachedBoardingStop( - busStopsUntilBoarding = busStopsUntilBoarding, - boardingEtaText = boardingEtaText, - boardingEtaSeconds = boardingEtaSeconds, + private fun findNearestStopIndex( + stops: List, + location: Location, + ): Int { + return stops.indices.minByOrNull { index -> + val stop = stops[index] + distanceMeters( + location.latitude, + location.longitude, + stop.lat, + stop.lon, ) - ) { - rideConfirmed = true - } - return RideStateUpdate(hasBoarded = rideConfirmed) + } ?: -1 } private fun buildNearestStatusText( @@ -1419,70 +1243,6 @@ class RouteTripMonitorService : Service() { ) } - private fun selectTrackedBusId( - stops: List, - liveStops: Map, - currentTrackedBusId: String?, - fallbackLiveStops: List, - ): String? { - val now = System.currentTimeMillis() - val normalizedCurrent = normalizeVehicleId(currentTrackedBusId) - if (normalizedCurrent != null) { - val seenAnywhereOnRoute = findTrackedBusIndex( - stops = stops, - liveStops = liveStops, - preferredVehicleId = normalizedCurrent, - ) != null - if (seenAnywhereOnRoute) { - trackedBusLastSeenAtMs = now - return normalizedCurrent - } - if (trackedBusLastSeenAtMs == 0L) { - trackedBusLastSeenAtMs = now - return normalizedCurrent - } - // Vehicle temporarily missing from the live feed — keep the lock - // during the grace window so it re-attaches when the feed recovers - // instead of hopping to the next bus. - if (now - trackedBusLastSeenAtMs < TRACKED_BUS_STALE_MS) { - return normalizedCurrent - } - } - - fallbackLiveStops.forEach { stopState -> - firstKnownVehicleId(stopState)?.let { vehicleId -> - trackedBusLastSeenAtMs = now - return vehicleId - } - } - - trackedBusLastSeenAtMs = 0L - return null - } - - private fun firstKnownVehicleId(stopState: LiveStopState?): String? { - stopState ?: return null - stopState.etaEntries.firstNotNullOfOrNull { entry -> - normalizeVehicleId(entry.vehicleId) - }?.let { return it } - return stopState.vehicleIds.firstNotNullOfOrNull { vehicleId -> - normalizeVehicleId(vehicleId) - } - } - - private fun isVehicleSeenAtStop( - stopState: LiveStopState?, - normalizedVehicleId: String, - ): Boolean { - stopState ?: return false - if (stopState.vehicleIds.any { normalizeVehicleId(it) == normalizedVehicleId }) { - return true - } - return stopState.etaEntries.any { etaEntry -> - normalizeVehicleId(etaEntry.vehicleId) == normalizedVehicleId - } - } - private fun findEtaForVehicle( stopState: LiveStopState?, preferredVehicleId: String?, @@ -1512,16 +1272,6 @@ class RouteTripMonitorService : Service() { return selectedEta?.seconds ?: stopState?.seconds } - private fun isLikelyBoardingArrival( - etaSeconds: Int?, - etaText: String?, - ): Boolean { - if (isImmediateEtaText(etaText)) { - return true - } - return etaSeconds != null && etaSeconds <= BOARDING_ARRIVAL_MAX_ETA_SECONDS - } - private fun composeEtaText(seconds: Int?, message: String?): String { val trimmedMessage = message?.trim().orEmpty() if (trimmedMessage.isNotEmpty()) { @@ -1571,22 +1321,22 @@ class RouteTripMonitorService : Service() { return value.contains("進站") || value.contains("即將") } - private fun buildNearestSubText( - session: TrackingSession, - nearestStop: TrackingStop, - busStopsAway: Int?, - nearestEtaText: String, - ): String { - val parts = mutableListOf() - if (session.pathName.isNotBlank()) { - parts += session.pathName - } - parts += "最近站牌 ${nearestStop.stopName}" - if (nearestEtaText != "--") { - parts += nearestEtaText + /** + * The button that lets the user answer the boarding question by hand. It is + * offered while waiting for the bus as well, so detection is never the only + * way to start a trip. + */ + private fun TrackingSnapshot.boardingActions(): String? { + return when { + hasBoarded -> null + boardingName != null -> "我已上車" + else -> null } - buildBusDistanceText(busStopsAway)?.let(parts::add) - return parts.joinToString(" · ") + } + + /** The counterpart button, offered once a ride is being tracked. */ + private fun TrackingSnapshot.notBoardedAction(): String? { + return if (hasBoarded) "還沒上車" else null } private fun buildTrackingNotification(snapshot: TrackingSnapshot): Notification { @@ -1705,6 +1455,17 @@ class RouteTripMonitorService : Service() { ).build(), ) + snapshot.boardingActions()?.let { action -> + builder.addAction( + NotificationCompat.Action.Builder(0, action, markBoardedIntent).build(), + ) + } + snapshot.notBoardedAction()?.let { action -> + builder.addAction( + NotificationCompat.Action.Builder(0, action, notBoardedIntent).build(), + ) + } + applyShortCriticalPresentation(builder, snapshot.shortCriticalText) requestPromotedOngoing(builder) @@ -1780,6 +1541,13 @@ class RouteTripMonitorService : Service() { ).build(), ) + snapshot.boardingActions()?.let { action -> + builder.addAction(Notification.Action.Builder(null, action, markBoardedIntent).build()) + } + snapshot.notBoardedAction()?.let { action -> + builder.addAction(Notification.Action.Builder(null, action, notBoardedIntent).build()) + } + applyShortCriticalPresentation(builder, snapshot.shortCriticalText) requestPromotedOngoing(builder) @@ -1849,79 +1617,16 @@ class RouteTripMonitorService : Service() { ) } - private fun buildLegacyTrackingNotification(snapshot: TrackingSnapshot): android.app.Notification { - val currentSession = session ?: return NotificationCompat.Builder( - this, - TRACKING_CHANNEL_ID, - ).setSmallIcon(R.drawable.ic_status_bus) - .setContentTitle("YABus") - .setOnlyAlertOnce(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .build() - - val builder = NotificationCompat.Builder(this, TRACKING_CHANNEL_ID) - .setSmallIcon(R.drawable.ic_status_bus) - .setContentTitle(snapshot.title) - .setContentText(snapshot.content) - .setContentIntent(createOpenRoutePendingIntent(currentSession)) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setCategory(NotificationCompat.CATEGORY_NAVIGATION) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) - .setRequestPromotedOngoing(true) - .addAction( - NotificationCompat.Action.Builder( - 0, - "停止", - createStopPendingIntent(), - ).build(), - ) - - if (snapshot.subText.isNotBlank()) { - builder.setSubText(snapshot.subText) - } - applyShortCriticalPresentation(builder, snapshot.shortCriticalText) - - val progressMax = snapshot.progressMax - val progressValue = snapshot.progressValue - if (progressMax != null && progressValue != null && progressMax > 0) { - val clampedValue = progressValue.coerceIn(0, progressMax) - val progressPoints = buildProgressPointPositions(progressMax) - builder.setProgress(progressMax, clampedValue, false) - val progressStyle = NotificationCompat.ProgressStyle() - .setStyledByProgress(true) - .setProgress(clampedValue) - .setProgressTrackerIcon( - IconCompat.createWithResource(this, R.drawable.ic_progress_bus), - ) - .setProgressSegments( - mutableListOf( - NotificationCompat.ProgressStyle.Segment(progressMax), - ), - ) - .setProgressPoints( - progressPoints.mapTo(mutableListOf()) { position -> - NotificationCompat.ProgressStyle.Point(position) - .setColor(PROGRESS_POINT_COLOR) - }, - ) - .setProgressEndIcon( - IconCompat.createWithResource(this, R.drawable.ic_progress_flag), - ) - builder.setStyle(progressStyle) - } else { - builder.setProgress(0, 0, false) - } - - return builder.build() - } - private fun maybeSendTripAlerts( session: TrackingSession, snapshot: TrackingSnapshot, ) { if (snapshot.passedDestinationByStops != null) { + // Fixes can skip the destination entirely. Passing it is also an + // arrival baseline, without restarting a countdown already running. + if (arrivalDetectedAtMs == 0L) { + arrivalDetectedAtMs = System.currentTimeMillis() + } if (maybeSendOvershotAlert(session, snapshot)) { return } @@ -1944,11 +1649,11 @@ class RouteTripMonitorService : Service() { return } if (!snapshot.hasBoarded) { - maybeExpireBoardingCheckPrompt() maybeSendBoardingAlert(session, snapshot) maybeSendBoardingCheckPrompt(session, snapshot) return } + cancelBoardingCheckPrompt() maybeSendDestinationAlert(session, snapshot) maybePauseAfterArrivalGrace() } @@ -1999,49 +1704,30 @@ class RouteTripMonitorService : Service() { ) } - private fun maybeExpireBoardingCheckPrompt() { - if (!boardingCheckPromptSent || boardingCheckPromptSentAtMs == 0L) { - return - } - val now = System.currentTimeMillis() - if (now - boardingCheckPromptSentAtMs < BOARDING_CHECK_PROMPT_MAX_AGE_MS) { - return - } - if (!boardingWindowOpen) { - boardingCheckPromptSent = false - boardingCheckPromptSentAtMs = 0L - boardingAlertSent = false - notificationManager.cancel(ALERT_NOTIFICATION_ID) - } - } - private fun maybeSendBoardingCheckPrompt( session: TrackingSession, snapshot: TrackingSnapshot, ) { - if (!session.backgroundLocationAlwaysGranted || snapshot.hasBoarded) { - return - } - if (boardingCheckPromptSent) { - return - } - if (!snapshot.boardingPromptEligible) { + val ride = currentRideSnapshot() + if (!ride.boardingPromptDue || ride.boardingCheckId == 0L) { + // The question was answered, retired or overtaken by a detection. + if (snapshot.hasBoarded || ride.boardingCheckId == 0L) { + cancelBoardingCheckPrompt() + } return } - val now = System.currentTimeMillis() - if (now < boardingCheckSnoozeUntilMs) { + if (boardingCheckNotificationId == ride.boardingCheckId) { return } val boardingName = snapshot.boardingName ?: return - boardingCheckPromptSent = true - boardingCheckPromptSentAtMs = System.currentTimeMillis() + boardingCheckNotificationId = ride.boardingCheckId notificationManager.notify( - ALERT_NOTIFICATION_ID, + BOARDING_CHECK_NOTIFICATION_ID, NotificationCompat.Builder(this, ALERT_CHANNEL_ID) .setSmallIcon(R.drawable.ic_status_bus) .setContentTitle("你有上車嗎?") - .setContentText("$boardingName 的那台車已經過站了。") + .setContentText("$boardingName 的公車剛剛離站了。") .setSubText(session.pathName) .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_REMINDER) @@ -2052,20 +1738,25 @@ class RouteTripMonitorService : Service() { NotificationCompat.Action.Builder( 0, "我已上車", - createMarkBoardedPendingIntent(), + createMarkBoardedPendingIntent(ride.boardingCheckId), ).build(), ) .addAction( NotificationCompat.Action.Builder( 0, "還沒上車", - createNotBoardedPendingIntent(), + createNotBoardedPendingIntent(ride.boardingCheckId), ).build(), ) .build(), ) } + private fun cancelBoardingCheckPrompt() { + boardingCheckNotificationId = 0L + notificationManager.cancel(BOARDING_CHECK_NOTIFICATION_ID) + } + private fun maybeSendDestinationSetupPrompt( session: TrackingSession, snapshot: TrackingSnapshot, @@ -2096,6 +1787,12 @@ class RouteTripMonitorService : Service() { ) } + /** + * Two heads-up alerts. "Stops remaining" counts the destination itself, so + * one remaining means the next stop is the user's — that, rather than a + * fixed distance, is what should prompt them to get ready, because a + * long segment can easily run past a couple of hundred metres. + */ private fun maybeSendDestinationAlert( session: TrackingSession, snapshot: TrackingSnapshot, @@ -2104,6 +1801,7 @@ class RouteTripMonitorService : Service() { val remainingStops = snapshot.remainingStops ?: return val distanceMeters = snapshot.destinationDistanceMeters ?: return val now = System.currentTimeMillis() + val nextStopIsDestination = remainingStops <= 1 if (remainingStops <= 2 && destinationAlertStage < 1) { destinationAlertStage = 1 @@ -2136,9 +1834,10 @@ class RouteTripMonitorService : Service() { ) } - if ((remainingStops == 0 || distanceMeters <= 120.0) && destinationAlertStage < 2) { + if ((nextStopIsDestination || distanceMeters <= DESTINATION_ARRIVED_DISTANCE_METERS) && + destinationAlertStage < 2 + ) { destinationAlertStage = 2 - arrivalDetectedAtMs = now notificationManager.notify( ALERT_NOTIFICATION_ID, NotificationCompat.Builder(this, ALERT_CHANNEL_ID) @@ -2166,7 +1865,13 @@ class RouteTripMonitorService : Service() { .setContentIntent(createOpenRoutePendingIntent(session)) .build(), ) - } else if (destinationAlertStage >= 2 && arrivalDetectedAtMs == 0L) { + } + + // Arrival — not "the next stop is yours" — is what starts the countdown + // to the auto-pause and the overshoot check. + if (arrivalDetectedAtMs == 0L && + (remainingStops == 0 || distanceMeters <= DESTINATION_ARRIVED_DISTANCE_METERS) + ) { arrivalDetectedAtMs = now } } @@ -2187,10 +1892,9 @@ class RouteTripMonitorService : Service() { val destinationName = snapshot.destinationName ?: return false val passedByStops = snapshot.passedDestinationByStops ?: return false val destinationDistanceMeters = snapshot.destinationDistanceMeters ?: return false - val nearestStopDistanceMeters = snapshot.nearestStopDistanceMeters ?: return false if ( destinationDistanceMeters <= DESTINATION_OVERSHOOT_DISTANCE_METERS || - nearestStopDistanceMeters > ROUTE_PROXIMITY_MAX_DISTANCE_METERS + !snapshot.hasRouteValidProgress ) { return false } @@ -2273,7 +1977,11 @@ class RouteTripMonitorService : Service() { provider = session.provider, routeKey = session.routeKey, pathId = session.pathId, - stopId = session.destinationStopId ?: session.stops.firstOrNull()?.stopId ?: 0, + // The full-width id, because Dart compares it against the ids its + // own stop models carry: a folded 高雄 id matches nothing there, so + // tapping the notification would not select the stop. + stopId = session.destinationLaunchStopId + ?: session.stops.firstOrNull()?.launchStopId?.takeIf { it > 0L }, ) return PendingIntent.getActivity( this, @@ -2295,29 +2003,42 @@ class RouteTripMonitorService : Service() { ) } - private fun createMarkBoardedPendingIntent(): PendingIntent { + /** + * "I have boarded". [answeredCheckId] is set only on the buttons of the + * boarding question, which answer that question; the same words on the + * tracking notification are the rider speaking unprompted, and must not be + * read as an answer to a question they may not have been shown. + */ + private fun createMarkBoardedPendingIntent(answeredCheckId: Long = 0L): PendingIntent = + createBoardingAnswerPendingIntent(ACTION_MARK_BOARDED, 405, 407, answeredCheckId) + + private fun createNotBoardedPendingIntent(answeredCheckId: Long = 0L): PendingIntent = + createBoardingAnswerPendingIntent(ACTION_NOT_BOARDED, 406, 408, answeredCheckId) + + private fun createBoardingAnswerPendingIntent( + action: String, + unpromptedRequestCode: Int, + answerRequestCode: Int, + answeredCheckId: Long, + ): PendingIntent { val intent = Intent(this, RouteTripMonitorService::class.java).apply { - action = ACTION_MARK_BOARDED + this.action = action + if (answeredCheckId != 0L) { + putExtra(EXTRA_BOARDING_CHECK_ID, answeredCheckId) + } } + // Extras do not tell two PendingIntents apart, so the answer gets a + // request code of its own rather than rewriting the unprompted button. return PendingIntent.getService( this, - 405, + if (answeredCheckId != 0L) answerRequestCode else unpromptedRequestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) } - private fun createNotBoardedPendingIntent(): PendingIntent { - val intent = Intent(this, RouteTripMonitorService::class.java).apply { - action = ACTION_NOT_BOARDED - } - return PendingIntent.getService( - this, - 406, - intent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, - ) - } + private fun answeredBoardingCheckId(intent: Intent): Long? = + intent.getLongExtra(EXTRA_BOARDING_CHECK_ID, 0L).takeIf { it != 0L } override fun onTaskRemoved(rootIntent: Intent?) { AppRuntimeStateStore.setAppInForeground(this, false) @@ -2342,39 +2063,25 @@ class RouteTripMonitorService : Service() { // involve other processes (location and notification services). stopSelf() foregroundStarted = false - session = null latestLocation = null - lastMovementLocation = null - lastMovementNearestIndex = null - lastMovementRecordedAtMs = 0L - lastTransitLikeAtMs = 0L boardingAlertSent = false - boardingCheckPromptSent = false - boardingCheckPromptSentAtMs = 0L - boardingWindowOpen = false - boardingWindowOpenedAtMs = 0L - boardingCheckSnoozeUntilMs = 0L - rideConfirmed = false - rideConfirmationSamples = 0 - lastNearestStopIndex = null - lastBusStopIndex = null - activeBoardingVehicleId = null - activeBoardingVehicleStopIndex = null - activeBoardingVehicleLastSeenAtMs = 0L - activeBoardingVehiclePassedAtMs = 0L - trackedBusId = null - trackedBusLastSeenAtMs = 0L destinationSetupPromptSent = false arrivalDetectedAtMs = 0L destinationAlertStage = 0 overshootAlertSent = false - refreshInFlight = false - refreshPending = false - lastRefreshStartedAtMs = 0L - cachedLiveRouteIds = emptyList() - cachedLivePathId = null - cachedLiveFetchedAtMs = 0L - cachedLiveStops = emptyMap() + synchronized(detectorLock) { + session = null + detector = null + latestRide = RideSnapshot() + } + synchronized(refreshLock) { + refreshInFlight = false + refreshPending = false + lastRefreshStartedAtMs = 0L + } + forgetLiveFeeds() + cancelBoardingCheckPrompt() + releaseWakeLock() mainHandler.removeCallbacksAndMessages(null) runCatching { if (::fusedLocationClient.isInitialized) { @@ -2393,49 +2100,92 @@ class RouteTripMonitorService : Service() { } } + /** Forgets what was fetched and when, which makes both feeds due at once. */ + private fun forgetLiveFeeds() = synchronized(refreshLock) { + cachedLiveRouteIds = emptyList() + cachedLivePathId = null + cachedLiveFetchedAtMs = 0L + cachedLiveStops = emptyMap() + busFeedRefreshByRoute.clear() + liveStopBackoffUntilMs = 0L + } + + /** + * The cached stop feed. Fetching happens in [updateLiveData] so that the + * snapshot builder never blocks on the network and both feeds are + * throttled together. + */ private fun fetchLiveStopMap(session: TrackingSession): Map { - val routeIds = session.realtimeRouteIds - if (routeIds.isEmpty()) { + if (session.realtimeRouteIds.isEmpty()) { return emptyMap() } - val now = SystemClock.elapsedRealtime() - synchronized(refreshLock) { - if ( - appInForeground && - cachedLiveRouteIds == routeIds && - cachedLivePathId == session.pathId - ) { - return cachedLiveStops - } - if ( - cachedLiveRouteIds == routeIds && - cachedLivePathId == session.pathId && - cachedLiveFetchedAtMs != 0L && - now - cachedLiveFetchedAtMs < LIVE_STOP_CACHE_TTL_MS - ) { - return cachedLiveStops + return synchronized(refreshLock) { + if (cachedLiveRouteIds == session.realtimeRouteIds && cachedLivePathId == session.pathId) { + cachedLiveStops + } else { + emptyMap() } } - if (appInForeground) { + } + + /** The live feed for the route and its variants; empty when fetching failed. */ + private fun fetchLiveStopMapFromBackend( + session: TrackingSession, + ): Map { + if (session.realtimeRouteIds.isEmpty()) { return emptyMap() } - val liveStops = familyRealtime.fetch(session) - synchronized(refreshLock) { - if (this.session === session) { - cachedLiveRouteIds = routeIds - cachedLivePathId = session.pathId - cachedLiveFetchedAtMs = now - cachedLiveStops = liveStops + // The family fetch covers the route and its variants; a single-route + // session is just the one-id case of it. + return familyRealtime.fetch(session) + } + + /** Live positions of every vehicle on the route; null when the request failed. */ + private fun fetchBusReportsFromBackend(routeId: String): List? { + val encodedRouteId = URLEncoder.encode(routeId, Charsets.UTF_8.name()) + val array = fetchBackendJsonArray("/api/v1/routes/$encodedRouteId/realtime/buses") + ?: return null + val receivedAtMs = System.currentTimeMillis() + return runCatching { parseBusReports(array, receivedAtMs) }.getOrNull() + } + + private fun parseBusReports( + array: JSONArray, + receivedAtMs: Long, + ): List { + val reports = ArrayList(array.length()) + for (index in 0 until array.length()) { + val bus = array.optJSONObject(index) ?: continue + val vehicleId = normalizeVehicleId(bus.opt("id")?.toString()) + ?: normalizeVehicleId(bus.opt("plate")?.toString()) + ?: continue + val latitude = bus.optDouble("lat", Double.NaN) + val longitude = bus.optDouble("lon", Double.NaN) + if (latitude.isNaN() || longitude.isNaN()) { + continue } + // GPSTime is unix seconds; a missing one makes the report unusable + // because there is nothing to line the user's track up against. + val gpsTimeMs = (toIntOrNull(bus.opt("time"))?.toLong() ?: continue) * 1000L + reports += RideBusReport( + vehicleId = vehicleId, + pathId = toIntOrNull(bus.opt("direction")), + latitude = latitude, + longitude = longitude, + gpsTimeMs = gpsTimeMs, + receivedAtMs = receivedAtMs, + ) } - return liveStops + return reports } - private fun fetchBackendJson(path: String): JSONObject? { + /** Raw body of a backend GET; null when the request failed. Replaced in tests. */ + internal var fetchBackendJsonText: (String) -> String? = ::httpGetBackend + + private fun httpGetBackend(path: String): String? { var connection: HttpURLConnection? = null return try { - connection = URL("https://bus.avianjay.sbs/api/v1/$path") - .openConnection() as HttpURLConnection + connection = URL("$BACKEND_BASE_URL$path").openConnection() as HttpURLConnection connection.connectTimeout = 10_000 connection.readTimeout = 10_000 connection.requestMethod = "GET" @@ -2446,10 +2196,9 @@ class RouteTripMonitorService : Service() { if (connection.responseCode !in 200..299) { return null } - val jsonText = connection.inputStream.bufferedReader(Charsets.UTF_8).use { reader -> + connection.inputStream.bufferedReader(Charsets.UTF_8).use { reader -> reader.readText() } - JSONObject(jsonText) } catch (_: Exception) { null } finally { @@ -2457,66 +2206,24 @@ class RouteTripMonitorService : Service() { } } - private fun formatEtaText(liveStopState: LiveStopState?): String { - liveStopState ?: return "--" - val message = normalizeEtaMessage(liveStopState.message) - if (message != null) { - return message - } - - val seconds = liveStopState.seconds ?: return "--" - if (seconds <= 0) { - return "進站中" - } - if (seconds < 60) { - return "即將進站" - } - return "${seconds / 60} 分" - } - - private fun formatShortEtaText(liveStopState: LiveStopState?): String { - liveStopState ?: return "--" - val message = liveStopState.message?.trim().orEmpty() - if (message.isNotEmpty()) { - return when { - message.contains("進站") || message.contains("到站") -> "進站" - message.contains("即將") -> "即將" - message.contains("未發車") -> "未發" - message.contains("末班") -> "末班" - else -> message.take(6) - } - } - - val seconds = liveStopState.seconds ?: return "--" - if (seconds <= 0) { - return "進站" - } - if (seconds < 60) { - return "<1分" + /** A JSON object response, addressed relative to `/api/v1/`. */ + private fun fetchBackendJson(path: String): JSONObject? { + return fetchBackendJsonText("/api/v1/$path")?.let { text -> + runCatching { JSONObject(text) }.getOrNull() } - return "${seconds / 60}分" } - private fun normalizeEtaMessage(message: String?): String? { - val trimmed = message?.trim().orEmpty() - if (trimmed.isEmpty()) { - return null - } - - return when { - trimmed.contains("進站") || trimmed.contains("到站") -> "進站中" - trimmed.contains("即將") -> "即將進站" - trimmed.contains("未發車") -> "未發車" - trimmed.contains("末班") -> "末班已過" - else -> trimmed + /** A JSON array response; `/realtime/buses` returns one. */ + private fun fetchBackendJsonArray(path: String): JSONArray? { + return fetchBackendJsonText(path)?.let { text -> + runCatching { JSONArray(text) }.getOrNull() } } - private fun buildBusDistanceText(busStopsAway: Int?): String? { - return when (busStopsAway) { - null -> null - 0 -> "公車就在附近" - else -> "公車還有 $busStopsAway 站" + private fun toIntOrNull(raw: Any?): Int? { + return when (raw) { + is Number -> raw.toInt() + else -> raw?.toString()?.trim()?.toIntOrNull() } } @@ -2548,67 +2255,7 @@ class RouteTripMonitorService : Service() { return trimmed.take(7) } - private fun buildLegacyShortCriticalText(stopsAway: Int?, etaText: String): String? { - if (stopsAway == null && etaText == "--") { - return null - } - - if (stopsAway == 0 && etaText != "--") { - return etaText.take(7) - } - - val left = when (stopsAway) { - null -> null - 0 -> "到站" - else -> "${stopsAway}站" - } - if (left == null) { - return etaText - } - if (etaText == "--") { - return left - } - return "$left | $etaText" - } - - private fun findClosestBusIndex( - stops: List, - liveStops: Map, - nearestIndex: Int, - ): Int? { - val busIndexes = stops.mapIndexedNotNull { index, stop -> - val liveStop = liveStops[stop.stopId] ?: return@mapIndexedNotNull null - if (isBusApproachingStop(liveStop)) { - index - } else { - null - } - } - val behindOrAtUser = busIndexes.filter { it <= nearestIndex } - if (behindOrAtUser.isNotEmpty()) { - return behindOrAtUser.maxOrNull() - } - return busIndexes.minOrNull() - } - - private fun findClosestBusIndexForBoarding( - stops: List, - liveStops: Map, - boardingIndex: Int, - ): Int? { - val busIndexes = stops.mapIndexedNotNull { index, stop -> - val liveStop = liveStops[stop.stopId] ?: return@mapIndexedNotNull null - if (isBusApproachingStop(liveStop)) { - index - } else { - null - } - } - return busIndexes - .filter { it <= boardingIndex } - .maxOrNull() - } - + /** Index of the stop where the live feed last saw [preferredVehicleId]. */ private fun findTrackedBusIndex( stops: List, liveStops: Map, @@ -2616,21 +2263,17 @@ class RouteTripMonitorService : Service() { ): Int? { val normalizedVehicleId = normalizeVehicleId(preferredVehicleId) ?: return null return stops.indexOfFirst { stop -> - isVehicleSeenAtStop( - stopState = liveStops[stop.stopId], - normalizedVehicleId = normalizedVehicleId, - ) + val stopState = liveStops[stop.stopId] + stopState != null && + ( + stopState.vehicleIds.any { normalizeVehicleId(it) == normalizedVehicleId } || + stopState.etaEntries.any { + normalizeVehicleId(it.vehicleId) == normalizedVehicleId + } + ) }.takeIf { it >= 0 } } - private fun isBusApproachingStop(liveStop: LiveStopState): Boolean { - val message = liveStop.message?.trim().orEmpty() - return liveStop.vehicleIds.isNotEmpty() || - (liveStop.seconds != null && liveStop.seconds <= 0) || - message.contains("進站") || - message.contains("到站") - } - private fun distanceMeters( lat1: Double, lon1: Double, @@ -2678,44 +2321,41 @@ class RouteTripMonitorService : Service() { private const val EXTRA_SESSION_JSON = "session_json" private const val EXTRA_APP_IN_FOREGROUND = "app_in_foreground" private const val EXTRA_PAUSE_REASON = "pause_reason" + private const val EXTRA_BOARDING_CHECK_ID = "boarding_check_id" private const val LEGACY_TRACKING_CHANNEL_ID = "trip_monitor_tracking" private const val TRACKING_CHANNEL_ID = "trip_monitor_tracking_v2" private const val ALERT_CHANNEL_ID = "trip_monitor_alerts" private const val TRACKING_NOTIFICATION_ID = 6021 private const val ALERT_NOTIFICATION_ID = 6022 + private const val BOARDING_CHECK_NOTIFICATION_ID = 6023 + private const val BACKEND_BASE_URL = "https://bus.avianjay.sbs" private const val POLL_INTERVAL_MS = 15_000L + private const val LIVE_STOP_REFRESH_INTERVAL_MS = 20_000L + private const val LIVE_BUS_REFRESH_INTERVAL_MS = 15_000L + private const val LIVE_FETCH_BACKOFF_MS = 60_000L private const val MIN_REFRESH_INTERVAL_MS = 2_000L - private const val LIVE_STOP_CACHE_TTL_MS = 2_000L private const val LOCATION_UPDATE_INTERVAL_MS = 12_000L private const val LOCATION_MIN_UPDATE_INTERVAL_MS = 6_000L - private const val BOARDING_STOP_RADIUS_METERS = 180.0 - private const val BOARDING_CONFIRM_DISTANCE_METERS = 250.0 - private const val BOARDING_ARRIVAL_MAX_ETA_SECONDS = 180 - private const val BOARDING_NO_LOCATION_MAX_ETA_SECONDS = 40 - private const val BOARDING_CHECK_PROMPT_MAX_ETA_SECONDS = 180 - private const val REQUIRED_RIDE_CONFIRMATION_SAMPLES = 2 private const val BACKGROUND_AUTO_PAUSE_GRACE_MS = 90_000L - private const val BOARDING_CHECK_PROMPT_DELAY_MS = 45_000L - private const val BOARDING_CHECK_SNOOZE_MS = 180_000L - private const val BOARDING_CHECK_AFTER_PASS_DELAY_MS = 20_000L - private const val ACTIVE_BOARDING_VEHICLE_STALE_MS = 90_000L - private const val TRACKED_BUS_STALE_MS = 120_000L - private const val BOARDING_WINDOW_MAX_DURATION_MS = 300_000L - private const val BOARDING_CHECK_PROMPT_MAX_AGE_MS = 300_000L - private const val USER_NEARBY_STOP_COUNT = 2 - private const val USER_MOVEMENT_MIN_DISTANCE_METERS = 35.0 - private const val USER_ROUTE_PROGRESS_MIN_DISTANCE_METERS = 45.0 - private const val USER_TRANSIT_LIKE_SPEED_MPS = 4.2 - private const val MAX_USABLE_LOCATION_ACCURACY_METERS = 100f - private const val TRANSIT_LIKE_STICKY_MS = 90_000L + + /** + * How long after pausing for a destination the same ride may still be + * picked up. A city ride is rarely longer; past this the rider is + * re-detected from their movement like anyone else. + */ + private const val BOARDED_RIDE_RESUME_WINDOW_MS = 60 * 60 * 1000L + private const val WAKE_LOCK_TAG = "YABus::TripMonitor" + // The lock is renewed on every start, so this only bounds a session that + // was never cleaned up; it is not the expected trip length. + private const val WAKE_LOCK_TIMEOUT_MS = 6 * 60 * 60 * 1000L private const val PROGRESS_POINT_UNIT = 100 private const val PROGRESS_POINT_COLOR = 0x80000000.toInt() private const val OVERSHOOT_CONFIRM_DELAY_MS = 45_000L private const val ARRIVAL_AUTO_PAUSE_DELAY_MS = 60_000L - private const val ROUTE_PROXIMITY_MAX_DISTANCE_METERS = 100.0 private const val DESTINATION_OVERSHOOT_DISTANCE_METERS = 100.0 + private const val DESTINATION_ARRIVED_DISTANCE_METERS = 120.0 private const val LIVE_UPDATE_SDK_INT = 36 private const val PAUSE_REASON_USER = "user" private const val PAUSE_REASON_ARRIVED = "arrived" @@ -2732,8 +2372,16 @@ class RouteTripMonitorService : Service() { val stops = mutableListOf() for (index in 0 until stopsJson.length()) { val stop = stopsJson.optJSONObject(index) ?: continue + // The id as Dart holds it, which is 64-bit for 高雄. It is + // needed twice, as two different numbers: folded, to key + // the live feeds; and at full width, for anything a + // notification tap carries back into Flutter. + val dartStopId = stop.opt("stopId") stops += TrackingStop( - stopId = stop.optInt("stopId", 0), + // Must match how the live feed folds the same id, or the + // two maps never line up for 64-bit ids (高雄). + stopId = foldStopId(dartStopId), + launchStopId = launchStopId(dartStopId), rawStopId = stop.optString("rawStopId", "") .trim().takeIf { it.isNotEmpty() && it != "null" }, stopName = stop.optString("stopName", ""), @@ -2767,14 +2415,20 @@ class RouteTripMonitorService : Service() { .takeUnless { it.isNaN() }, initialLongitude = root.optDouble("initialLongitude", Double.NaN) .takeUnless { it.isNaN() }, - boardingStopId = root.optInt("boardingStopId", -1) + initialLocationTimestampMs = root.optLong("initialLocationTimestampMs", 0L) + .takeIf { it > 0L }, + boardingStopId = foldStopId(root.opt("boardingStopId")) .takeIf { it > 0 }, boardingStopName = root.optString("boardingStopName", "") .takeIf { it.isNotBlank() }, - destinationStopId = root.optInt("destinationStopId", -1) + destinationStopId = foldStopId(root.opt("destinationStopId")) .takeIf { it > 0 }, destinationStopName = root.optString("destinationStopName", "") .takeIf { it.isNotBlank() }, + boardingLaunchStopId = launchStopId(root.opt("boardingStopId")) + .takeIf { it > 0L }, + destinationLaunchStopId = launchStopId(root.opt("destinationStopId")) + .takeIf { it > 0L }, stops = stops.sortedBy { stop -> stop.sequence }, ) } catch (_: Exception) { @@ -2864,6 +2518,11 @@ data class TrackingSession( val destinationStopName: String?, val stops: List, val familyRouteIds: List = emptyList(), + /** Full-width ids as Flutter holds them; only for launch payloads. */ + val boardingLaunchStopId: Long? = null, + val destinationLaunchStopId: Long? = null, + /** Epoch milliseconds of the fix behind the initial position, when known. */ + val initialLocationTimestampMs: Long? = null, ) { val realtimeRouteIds: List get() = (listOfNotNull(routeId) + familyRouteIds).map { it.trim() } @@ -2878,13 +2537,18 @@ data class TrackingStop( val lon: Double, val rawStopId: String? = null, val liveData: LiveStopState? = null, + /** Full-width id as Flutter holds it; only for launch payloads. */ + val launchStopId: Long = 0L, ) data class LiveStopState( val seconds: Int?, val message: String?, + /** Every vehicle the stop's feed names, including buses still on their way. */ val vehicleIds: List, val etaEntries: List = emptyList(), + /** Only the vehicles the feed places at this stop right now. */ + val busesAtStop: List = emptyList(), ) data class LiveEtaEntry( @@ -2914,6 +2578,7 @@ data class TrackingSnapshot( val destinationDistanceMeters: Double? = null, val passedDestinationByStops: Int? = null, val nearestStopDistanceMeters: Double? = null, + val hasRouteValidProgress: Boolean = false, ) data class VehicleCandidate( diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/SmartRouteNotificationWorker.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/SmartRouteNotificationWorker.kt index db3b5e7..f9bf43c 100644 --- a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/SmartRouteNotificationWorker.kt +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/SmartRouteNotificationWorker.kt @@ -369,6 +369,7 @@ internal object SmartRouteNotificationSupport { bestStop = SmartNearestStop( pathId = stop.pathId, stopId = stop.stopId, + launchStopId = stop.launchStopId, stopName = stop.stopName, distanceMeters = distance, ) @@ -452,7 +453,9 @@ internal object SmartRouteNotificationSupport { provider = profile.provider, routeKey = profile.routeKey, pathId = nearestStop.pathId, - stopId = nearestStop.stopId, + // Full-width: Flutter matches this against its own stop ids, and + // the folded key would select nothing there. + stopId = nearestStop.launchStopId, ), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, ) @@ -620,9 +623,15 @@ internal object SmartRouteNotificationSupport { arrayOf(routeKey.toString()), ).use { cursor -> while (cursor.moveToNext()) { + // stop_id is a TEXT column and 高雄 ids exceed Int. It is needed + // twice, as two different numbers: folded, to key the live + // feed; and at full width, for the notification tap that goes + // back into Flutter, whose stop models hold the unfolded id. + val storedStopId = cursor.getString(1) result += SmartRouteStop( pathId = cursor.getInt(0), - stopId = cursor.getInt(1), + stopId = foldStopId(storedStopId), + launchStopId = launchStopId(storedStopId), stopName = cursor.getString(2).orEmpty(), sequence = cursor.getInt(3), lon = cursor.getDouble(4), @@ -678,21 +687,7 @@ internal object SmartRouteNotificationSupport { return result } - private fun parseStopId(raw: Any?): Int { - if (raw is Number) { - return raw.toInt() - } - val text = raw?.toString()?.trim().orEmpty() - val parsed = text.toIntOrNull() - if (parsed != null) { - return parsed - } - var hash = 17 - for (char in text) { - hash = (hash * 31 + char.code) and 0x7fffffff - } - return hash - } + private fun parseStopId(raw: Any?): Int = foldStopId(raw) private fun toIntOrNull(raw: Any?): Int? { return when (raw) { @@ -801,7 +796,10 @@ internal data class SmartRouteData( internal data class SmartRouteStop( val pathId: Int, + /** Folded, as the live feeds are: only usable for looking up ETAs. */ val stopId: Int, + /** Full-width, as Flutter holds it: what a notification tap may carry. */ + val launchStopId: Long, val stopName: String, val sequence: Int, val lon: Double, @@ -811,6 +809,7 @@ internal data class SmartRouteStop( internal data class SmartNearestStop( val pathId: Int, val stopId: Int, + val launchStopId: Long, val stopName: String, val distanceMeters: Double, ) diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/StopIdCodec.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/StopIdCodec.kt new file mode 100644 index 0000000..4c3d031 --- /dev/null +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/StopIdCodec.kt @@ -0,0 +1,70 @@ +package tw.avianjay.taiwanbus.flutter + +/** + * Folds a stop id from any source into the single positive `Int` the app keys + * stops by. + * + * Stop ids are 64-bit in the feeds (高雄's look like `100100128221`) but every + * stop map in the app is keyed by `Int`, and the two sides of that key used to + * disagree: the session payload was read with `optInt`, which wraps to the low + * 32 bits, while the live feed fell back to hashing the id as text. The two + * values can never be equal, so a 高雄 stop's live ETA was unreachable — and + * ids that wrapped negative were dropped by the `> 0` filters as well. + * + * Both sides call this now. Values that already fit in 31 bits pass through + * untouched, so 台中 and 雙北 ids are unaffected, and the fold is idempotent, + * so a value that has been through it stays put. It never returns a negative + * number, which is what makes the `> 0` filters safe. Real ids do not collide + * under it, and the hash fallback still covers the non-numeric ids some + * providers use (`101-11009`). + * + * Returns 0 for a missing or blank id so callers can keep filtering on `> 0`. + */ +internal fun foldStopId(raw: Any?): Int { + val text = when (raw) { + // JSON numbers can arrive as any boxed type; going through Long keeps + // an integral id from being mangled into exponent notation. + is Number -> raw.toLong().toString() + else -> raw?.toString()?.trim().orEmpty() + } + // org.json hands back JSONObject.NULL rather than null for absent fields, + // and hashing its text would invent an id out of nothing. + if (text.isEmpty() || text.equals("null", ignoreCase = true)) { + return 0 + } + text.toLongOrNull()?.let { value -> + if (value >= 0L) { + return (value and 0x7FFFFFFFL).toInt() + } + } + var hash = 17 + for (char in text) { + hash = (hash * 31 + char.code) and 0x7fffffff + } + return hash +} + +/** + * The stop id as Flutter holds it, for launch payloads (widget taps, notification + * taps, pinned shortcuts). + * + * Dart keys stops by the id it read from the feed: numeric ids stay 64-bit + * (高雄's `100100128221` would not fit in an `Int`), non-numeric ones hash. The + * native fold is only for joining the live feeds and must not be sent to Dart — + * a folded id names no stop the Flutter side knows, so the tap would silently + * open the wrong stop or none at all. This returns the value Dart itself would + * have computed for the same raw id. + */ +internal fun launchStopId(raw: Any?): Long { + val text = when (raw) { + is Number -> raw.toLong().toString() + else -> raw?.toString()?.trim().orEmpty() + } + if (text.isEmpty() || text.equals("null", ignoreCase = true)) { + return 0L + } + text.toLongOrNull()?.let { return it } + // Non-numeric ids hash on both sides, and both hashes start at 17, so the + // folded value already is what Dart computed. + return foldStopId(raw).toLong() +} diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripMonitorFamilyRealtime.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripMonitorFamilyRealtime.kt index d4e0a70..ef86fbd 100644 --- a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripMonitorFamilyRealtime.kt +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripMonitorFamilyRealtime.kt @@ -116,6 +116,7 @@ internal fun parseTripMonitorLiveStop(stop: JSONObject): LiveStopState { message = text(stop, "message"), vehicleIds = (buses + etas.mapNotNull { it.vehicleId }).distinct(), etaEntries = etas, + busesAtStop = buses, ) } @@ -131,6 +132,7 @@ private fun mergeStops(stops: List): LiveStopState { message = if (earliest == null) stops.firstNotNullOfOrNull { it.message } else null, vehicleIds = (entries.mapNotNull { it.vehicleId } + stops.flatMap { it.vehicleIds }).distinct(), etaEntries = entries, + busesAtStop = stops.flatMap { it.busesAtStop }.distinct(), ) } @@ -139,15 +141,7 @@ private fun JSONArray?.objects(): List = private fun integer(raw: Any?): Int? = if (raw is Number) raw.toInt() else raw?.toString()?.trim()?.toIntOrNull() -internal fun stopId(raw: Any?): Int { - if (raw is Number) return raw.toInt() - val value = raw?.takeUnless { it == JSONObject.NULL }?.toString()?.trim().orEmpty() - if (value.isEmpty()) return 0 - value.toIntOrNull()?.let { return it } - var hash = 17 - for (char in value) hash = (hash * 31 + char.code) and 0x7fffffff - return hash -} +internal fun stopId(raw: Any?): Int = foldStopId(raw) private fun text(json: JSONObject, key: String): String? = json.opt(key) ?.takeUnless { it == JSONObject.NULL }?.toString()?.trim() diff --git a/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetector.kt b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetector.kt new file mode 100644 index 0000000..d67c2b9 --- /dev/null +++ b/android/app/src/main/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetector.kt @@ -0,0 +1,1190 @@ +package tw.avianjay.taiwanbus.flutter + +import kotlin.math.abs + +/** + * One user position fix. Deliberately free of android.location types so the + * detection logic can be replayed against recorded feeds in plain JVM tests. + */ +internal data class RideGeoPoint( + val latitude: Double, + val longitude: Double, + val timestampMs: Long, + val accuracyMeters: Float = 0f, + /** Doppler speed from the GPS chip, when the fix has one. */ + val speedMetersPerSecond: Float? = null, +) + +/** + * One vehicle position from `/realtime/buses`. + * + * [gpsTimeMs] is the vehicle's own clock and is what gets compared against the + * user's track. It reaches the app 20-40 s late at the median and up to ~200 s + * late in the tail, so "is this vehicle still reporting" is judged by + * [receivedAtMs] instead. + */ +internal data class RideBusReport( + val vehicleId: String, + val pathId: Int?, + val latitude: Double, + val longitude: Double, + val gpsTimeMs: Long, + val receivedAtMs: Long, +) + +internal enum class BoardingEvidence { + NONE, + + /** The user covered the same stretch of route as a live vehicle, at the same time. */ + BUS_CO_MOVEMENT, + + /** The user's own track left the boarding stop along the route at vehicle pace. */ + SELF_PROGRESS, + + /** The user tapped "我已上車". */ + USER_CONFIRMED, +} + +internal data class RideSnapshot( + val hasBoarded: Boolean = false, + val evidence: BoardingEvidence = BoardingEvidence.NONE, + val trackedVehicleId: String? = null, + val hasUsableLocation: Boolean = false, + val boardingStopIndex: Int = -1, + val boardingStopDistanceMeters: Double? = null, + /** Last stop reached by the rider (their own fix, else the tracked vehicle); -1 when unknown. */ + val progressStopIndex: Int = -1, + val nextBusVehicleId: String? = null, + val busStopsUntilBoarding: Int? = null, + /** Identifies the pending "did you get on?" question; 0 when there is none. */ + val boardingCheckId: Long = 0L, + val boardingPromptDue: Boolean = false, + val remainingStops: Int? = null, + val passedDestinationByStops: Int? = null, + val destinationDistanceMeters: Double? = null, + /** Validity of the same fresh phone/bus sample that supplied the progress. */ + val hasRouteValidProgress: Boolean = false, +) + +/** + * Decides whether the user is riding the monitored route. + * + * Three layers, strongest evidence first: + * + * A. **Co-movement.** For every live vehicle, look up where the user was at the + * instants that vehicle reported its position. Riding together keeps the gap + * between the two roughly constant while both advance. This needs no plate + * in the ETA feed (雙北 ETAs carry none; `/realtime/buses` always does). + * + * B. **Self progress.** The user's own track leaving the boarding stop along the + * route at vehicle pace. Covers a boarded vehicle with no position feed. + * + * C. **The user's answer.** Once a bus has left the boarding stop and nothing + * shows the user stayed behind, ask. Works with no location at all. + * + * Everything is measured in metres along the stop sequence. A 2-D proximity + * test would be dominated by cross-track error: against the stop-chord line, + * bus fixes sit 8-16 m off at the median but 65-295 m at p90. + */ +internal class TripRideDetector( + stops: List, + private val pathId: Int, + boardingStopId: Int?, + destinationStopId: Int?, +) { + private val stopIds = stops.map { it.stopId } + private val line = RideRouteLine(stops) + + private val userTrack = ArrayDeque() + private val busTracks = LinkedHashMap>() + /** Vehicles last seen short of the boarding stop, with when that report arrived. */ + private val seenBeforeBoardingStopAt = HashMap() + + private val explicitBoardingIndex = indexOfStop(boardingStopId) + private var anchoredBoardingIndex = -1 + private var destinationIndex = indexOfStop(destinationStopId) + + private var userAlongHint: Double? = null + private var boarded = false + private var evidence = BoardingEvidence.NONE + private var trackedVehicleId: String? = null + private var manualConfirmationAtMs: Long? = null + private var manualVehicleAmbiguous = false + + /** + * Vehicles the rider has answered "not this one" about, each with the moment + * of that answer. The answer is about one bus, not about the trip: only the + * vehicle it named is set aside, and only for [REJECTED_VEHICLE_TTL_MS], so + * the next bus both raises the question and gets detected as normal. See + * [declineBoarding]. A later "I have boarded" leaves these standing: it is + * said of some other bus, and they are how that bus is told from these. + */ + private val rejectedAt = HashMap() + private var lastDeclineAtMs = 0L + + /** + * The departures the last "not yet" answered, kept for as long as the other + * feed may still report them; and when a detected ride was last turned down. + */ + private val declinedDepartures = ArrayList() + private var declinedRideAtMs = 0L + + private var etaArrivingAtMs = 0L + private var etaArrivingVehicleId: String? = null + + /** + * The pending "did you get on?" question: its id, and every bus that has + * left the boarding stop while it has stood. The question says only that a + * bus has just left, so it is about all of them. + */ + private var pendingCheckSinceMs = 0L + private val pendingDepartures = ArrayList() + private var pendingCheckAnchor: UserSample? = null + + private val boardingIndex: Int + get() { + val index = if (explicitBoardingIndex >= 0) explicitBoardingIndex else anchoredBoardingIndex + return if (index >= 0 && destinationIndex >= 0) index.coerceAtMost(destinationIndex) else index + } + + /** Choosing a destination mid-ride must not disturb what is already known about the ride. */ + fun updateDestination(destinationStopId: Int?) { + destinationIndex = indexOfStop(destinationStopId) + } + + /** Feeds one user fix. Returns false when the fix was rejected as unusable. */ + fun onUserLocation(point: RideGeoPoint): Boolean { + if (point.accuracyMeters > MAX_USABLE_ACCURACY_METERS) { + return false + } + val previous = userTrack.lastOrNull() + if (previous != null) { + val elapsedSeconds = (point.timestampMs - previous.timestampMs) / 1000.0 + if (elapsedSeconds <= 0.0) { + // The same fix delivered again, or a clock step backwards. + return false + } + val stepMeters = line.metersBetween( + previous.latitude, + previous.longitude, + point.latitude, + point.longitude, + ) + // Faster than any bus: a GPS jump, not travel. Keeping it would + // corrupt every window that spans it. + if (stepMeters / elapsedSeconds > MAX_PLAUSIBLE_SPEED_MPS) { + return false + } + } + val projection = line.project(point.latitude, point.longitude, userAlongHint) + userAlongHint = projection.alongMeters + userTrack += UserSample( + latitude = point.latitude, + longitude = point.longitude, + alongMeters = projection.alongMeters, + timestampMs = point.timestampMs, + accuracyMeters = point.accuracyMeters, + speedMetersPerSecond = point.speedMetersPerSecond, + ) + while (userTrack.size > MAX_USER_SAMPLES) { + userTrack.removeFirst() + } + return true + } + + fun onBusReports(reports: List) { + reports.forEach { report -> + if (report.vehicleId.isBlank() || report.gpsTimeMs <= 0L) { + return@forEach + } + val track = busTracks.getOrPut(report.vehicleId) { ArrayDeque() } + val last = track.lastOrNull() + // Vehicles report every ~20 s but the feed is polled more often, so + // most polls repeat the previous fix. + if (last != null && report.gpsTimeMs <= last.gpsTimeMs) { + return@forEach + } + val projection = line.project(report.latitude, report.longitude, last?.alongMeters) + track += BusSample( + latitude = report.latitude, + longitude = report.longitude, + alongMeters = projection.alongMeters, + gpsTimeMs = report.gpsTimeMs, + receivedAtMs = report.receivedAtMs, + samePath = report.pathId == null || report.pathId == pathId, + // Parked and deadheading vehicles project onto the route from + // far away; only vehicles actually on it may announce arrivals. + onRoute = projection.crossTrackMeters <= maxOf( + ON_ROUTE_MIN_CROSS_TRACK_METERS, + ON_ROUTE_SEGMENT_SHARE * line.segmentLengthMeters(projection.segmentIndex), + ), + ) + while (track.size > MAX_BUS_SAMPLES) { + track.removeFirst() + } + } + } + + /** + * Feeds the boarding stop's ETA. An estimate that reaches zero and then + * rolls over to the next bus means a vehicle has left, which is the only + * departure signal available when that vehicle has no position feed. + */ + fun onBoardingStopEta( + etaSeconds: Int?, + arriving: Boolean, + nowMs: Long, + arrivingVehicleId: String? = null, + ) { + if (arriving || (etaSeconds != null && etaSeconds <= ETA_ARRIVING_SECONDS)) { + if (etaArrivingAtMs == 0L || nowMs - etaArrivingAtMs > ETA_ROLLOVER_WINDOW_MS) { + etaArrivingVehicleId = null + } + etaArrivingAtMs = nowMs + // Where the stop feed says which bus is standing there, the + // rollover that follows is that bus leaving — the one thing that + // ties an ETA departure to a vehicle. + if (arrivingVehicleId != null) { + etaArrivingVehicleId = arrivingVehicleId + } + return + } + // Once the estimate reaches zero it either jumps to the next bus or + // disappears entirely; both mean the vehicle that was arriving has gone. + val rolledOver = etaArrivingAtMs != 0L && + nowMs - etaArrivingAtMs <= ETA_ROLLOVER_WINDOW_MS && + (etaSeconds == null || etaSeconds >= ETA_ROLLOVER_SECONDS) + if (rolledOver) { + val departed = etaArrivingVehicleId + etaArrivingAtMs = 0L + etaArrivingVehicleId = null + registerDeparture(vehicleId = departed, nowMs = nowMs, byEta = true) + } + } + + /** + * The rider says they are on board. + * + * [answeredCheckId] is the [RideSnapshot.boardingCheckId] of the question + * being answered, and null when nothing was asked: "I have boarded" is + * always on offer, and a rider who uses it may never have seen a question — + * one can be pending, about a bus that just left, before it is ever shown. + * + * Either way this confirms the ride, not a plate. A vehicle is named only + * when the feeds leave one bus the rider can be on: in answer to the + * question, the one bus that left while it stood ([busTheQuestionIsAbout]); + * unprompted, the one bus standing at the stop ([identifyBoardingVehicle]). + * Following the wrong bus sounds the destination reminders for a journey + * the rider is not making, so anything less leaves the vehicle unknown + * until the rider's own position picks it out. + */ + fun confirmBoardedByUser(nowMs: Long, answeredCheckId: Long? = null) { + noteDepartures(nowMs) + val identifiedVehicle = trackedVehicleId.takeIf { boarded } + // An answer to a question since replaced by another names nothing. + val askedAbout = if (answeredCheckId != null && answeredCheckId == pendingCheckSinceMs) { + busTheQuestionIsAbout(nowMs) + } else { + null + } + if (!boarded || evidence != BoardingEvidence.USER_CONFIRMED) { + // Only unprompted is "I have boarded" read against what is standing + // at the stop; an answer names a bus through its question or not + // at all. + manualConfirmationAtMs = nowMs.takeIf { answeredCheckId == null } + // And not even unprompted while a bus that has left is still to be + // answered for: the rider may be on that one, and has not said. + manualVehicleAmbiguous = departuresTheQuestionCovers(nowMs).isNotEmpty() + } + boarded = true + evidence = BoardingEvidence.USER_CONFIRMED + // An already identified vehicle remains the ride even if another bus + // is nearer the stop now. + trackedVehicleId = identifiedVehicle ?: askedAbout ?: identifyBoardingVehicle(nowMs) + clearPendingCheck() + } + + /** + * Counts the buses that have left the boarding stop according to the + * reports received so far. What an answer means depends on that count, and + * it must not depend on whether a snapshot happened to be taken between + * the last report coming in and the rider answering. + */ + private fun noteDepartures(nowMs: Long) { + val boardingStop = boardingIndex + if (boardingStop >= 0) { + trackDepartures(liveVehicles(nowMs), line.alongOfStop(boardingStop), nowMs) + } + } + + /** The buses that have left the boarding stop while the pending question has stood. */ + private fun departuresTheQuestionCovers(nowMs: Long): List = + pendingDepartures.filter { nowMs - it.atMs in 0L..BOARDING_CHECK_MAX_AGE_MS } + + /** + * The one bus a "yes" to the pending question can mean; null when that + * cannot be told. + * + * The question does not name a bus — riders do not know plates — only says + * that one has just left. So the answer names a vehicle when exactly one + * has left while the question stood, a feed named it, no rollover of the + * ETA is unaccounted for beside it, and no other bus is at the stop for the + * rider to have boarded since. Two that + * left close together, or one that left as another pulled in, leave "yes" + * true of either. + */ + private fun busTheQuestionIsAbout(nowMs: Long): String? { + val departure = departuresTheQuestionCovers(nowMs).singleOrNull() ?: return null + if (departure.mayBeTwoBuses) { + return null + } + val vehicleId = departure.vehicleId ?: return null + val boardingStop = boardingIndex + if (boardingStop < 0 || anotherBusIsBoardable(nowMs, line.alongOfStop(boardingStop), vehicleId)) { + return null + } + return vehicleId + } + + /** + * Takes up a ride the rider confirmed earlier, on a detector built since. + * + * The confirmation was given some way back along the route, so nothing at + * the boarding stop now has anything to do with it: the vehicle stays + * unknown until the rider's own position picks it out. + */ + fun resumeConfirmedRide() { + boarded = true + evidence = BoardingEvidence.USER_CONFIRMED + trackedVehicleId = null + manualConfirmationAtMs = null + manualVehicleAmbiguous = false + clearPendingCheck() + } + + /** + * The user says they are not on the bus the app thinks they are on. + * + * The answer is about the buses that have gone, not about the trip, so only + * what was just asserted is set aside: every bus that left while the + * pending question stood — a rider still waiting is on none of them — else + * the vehicle the detected ride named, for [REJECTED_VEHICLE_TTL_MS]; and + * the rider's own movement up to this moment, which no longer counts as + * evidence of a ride. The next bus raises its own question and is detected + * exactly as these were. + * + * Switching detection off for the rest of the trip instead would leave the + * rider sitting on their real bus with no boarding alert and no destination + * reminders, so one repeated question is the better failure. + * + * [answeredCheckId] is the question being answered, as for + * [confirmBoardedByUser]. "Not yet" to a question that has since been + * replaced says nothing about the buses the new one is about. + */ + fun declineBoarding(nowMs: Long, answeredCheckId: Long? = null) { + noteDepartures(nowMs) + if (!boarded && answeredCheckId != null && answeredCheckId != pendingCheckSinceMs) { + // Still true of the rider's movement so far, and of nothing else. + lastDeclineAtMs = nowMs + return + } + val askedAbout = pendingDepartures.toList() + val rejected = if (askedAbout.isEmpty()) { + listOfNotNull(trackedVehicleId) + } else { + askedAbout.mapNotNull { it.vehicleId } + } + boarded = false + evidence = BoardingEvidence.NONE + trackedVehicleId = null + manualConfirmationAtMs = null + manualVehicleAmbiguous = false + lastDeclineAtMs = nowMs + declinedDepartures.clear() + declinedDepartures += askedAbout + declinedRideAtMs = if (askedAbout.isEmpty()) nowMs else 0L + rejected.forEach { rejectedAt[it] = nowMs } + clearPendingCheck() + } + + /** + * Whether [vehicleId] is a bus the rider has already turned down and whose + * rejection is still in force. Entries lapse after + * [REJECTED_VEHICLE_TTL_MS] so a vehicle that comes round again is treated + * as new. + */ + private fun isRejected(vehicleId: String, nowMs: Long): Boolean { + val declinedAt = rejectedAt[vehicleId] ?: return false + return nowMs - declinedAt <= REJECTED_VEHICLE_TTL_MS + } + + fun snapshot(nowMs: Long): RideSnapshot { + prune(nowMs) + val latestUser = userTrack.lastOrNull() + val usableUser = latestUser?.takeIf { nowMs - it.timestampMs <= LOCATION_STALE_MS } + if (anchoredBoardingIndex < 0 && usableUser != null) { + // Used only when no boarding stop was chosen: the trip starts + // wherever the user actually is. Only a current fix can say that — + // a cached one from minutes ago would pin the whole trip to a stop + // the user may have left long before. + anchoredBoardingIndex = line.nearestStopIndex(usableUser.alongMeters) + } + val boardingStop = boardingIndex + val boardingAlong = if (boardingStop >= 0) line.alongOfStop(boardingStop) else null + val vehicles = liveVehicles(nowMs) + + if (boardingAlong != null) { + trackDepartures(vehicles, boardingAlong, nowMs) + } + if (!boarded) { + val coMovingVehicleId = detectCoMovement(nowMs) + if (coMovingVehicleId != null) { + markBoarded(BoardingEvidence.BUS_CO_MOVEMENT, coMovingVehicleId) + } else if (boardingAlong != null && usableUser != null && + detectSelfProgress(nowMs, boardingAlong, usableUser) + ) { + markBoarded(BoardingEvidence.SELF_PROGRESS, vehicleId = null) + } + } + trackedVehicleId = resolveTrackedVehicle(vehicles, usableUser, nowMs) + val trackedVehicle = vehicles.firstOrNull { + it.vehicleId == trackedVehicleId && it.samePath && it.onRoute + } + + val nextBus = if (boardingAlong == null) { + null + } else { + vehicles + .filter { it.samePath && it.onRoute && it.alongMeters <= boardingAlong + AT_STOP_MARGIN_METERS } + .maxByOrNull { it.alongMeters } + } + + val progressAlong = usableUser?.alongMeters + ?: trackedVehicle?.alongMeters?.takeIf { boarded } + val progressStopIndex = progressAlong?.let(line::stopIndexAt) ?: -1 + val destinationStop = destinationIndex + val stopsToGo = if (destinationStop >= 0 && progressStopIndex >= 0) { + destinationStop - progressStopIndex + } else { + null + } + val destinationDistanceMeters = when { + destinationStop < 0 -> null + usableUser != null -> line.metersToStop(usableUser.latitude, usableUser.longitude, destinationStop) + progressAlong != null -> abs(line.alongOfStop(destinationStop) - progressAlong) + else -> null + } + // Resolved before the snapshot is assembled: it may retire the pending + // question, and the id reported alongside must agree with it. + val boardingPromptDue = resolveBoardingPromptDue(nowMs, usableUser, boardingStop) + + return RideSnapshot( + hasBoarded = boarded, + evidence = evidence, + trackedVehicleId = trackedVehicleId, + hasUsableLocation = usableUser != null, + boardingStopIndex = boardingStop, + boardingStopDistanceMeters = if (usableUser != null && boardingStop >= 0) { + line.metersToStop(usableUser.latitude, usableUser.longitude, boardingStop) + } else { + null + }, + progressStopIndex = progressStopIndex, + nextBusVehicleId = nextBus?.vehicleId, + busStopsUntilBoarding = nextBus?.let { + (boardingStop - line.stopIndexAt(it.alongMeters)).coerceAtLeast(0) + }, + boardingCheckId = pendingCheckSinceMs, + boardingPromptDue = boardingPromptDue, + remainingStops = stopsToGo?.coerceAtLeast(0), + passedDestinationByStops = stopsToGo?.let { -it }?.takeIf { it > 0 }, + destinationDistanceMeters = destinationDistanceMeters, + // Keep the phone's existing near-stop guard. Without a usable phone + // fix, the tracked bus's direction and route geometry are the guard; + // an unrelated cached phone location must not veto its progress. + hasRouteValidProgress = when { + usableUser != null -> line.metersToStop( + usableUser.latitude, usableUser.longitude, line.nearestStopIndex(usableUser.alongMeters), + ) <= USER_STOP_PROXIMITY_MAX_METERS + trackedVehicle != null -> trackedVehicle.samePath && trackedVehicle.onRoute + else -> false + }, + ) + } + + private fun markBoarded(evidence: BoardingEvidence, vehicleId: String?) { + boarded = true + this.evidence = evidence + trackedVehicleId = vehicleId + manualConfirmationAtMs = null + manualVehicleAmbiguous = false + clearPendingCheck() + } + + /** + * Layer A. The gap between the user and a vehicle is sampled at each of the + * vehicle's reports, newest first, and must stay within + * [COMOVE_MAX_GAP_DRIFT_METERS] over a stretch in which both really moved. + * + * Standing at the stop while the bus drives off makes the gap grow by the + * bus's whole travel, and walking beside a bus stuck in traffic fails the + * speed floor, so neither counts. + * + * Only an unbroken run of reports made on this trip's direction, on the + * route, is evidence. The feed lists every vehicle of the route — the other + * direction's, and ones parked or deadheading beside it — and each of those + * still projects to some position along this line. + */ + private fun detectCoMovement(nowMs: Long): String? { + if (userTrack.size < 2) { + return null + } + val pairs = ArrayList() + busTracks.forEach { (vehicleId, track) -> + if (isRejected(vehicleId, nowMs)) { + return@forEach + } + pairs.clear() + track.forEach { report -> + if (!report.samePath || !report.onRoute) { + // Whatever came before is not joined to what follows as + // though the bus had run on through. + pairs.clear() + return@forEach + } + if (report.receivedAtMs < nowMs - COMOVE_WINDOW_MS) { + return@forEach + } + val userAlong = userAlongAt(report.gpsTimeMs) ?: return@forEach + pairs += CoMovementPair(userAlong, report.alongMeters, report.gpsTimeMs) + } + if (isCoMovement(pairs)) { + return vehicleId + } + } + return null + } + + private fun isCoMovement(pairs: List): Boolean { + val newest = pairs.lastOrNull() ?: return false + var minGap = Double.MAX_VALUE + var maxGap = -Double.MAX_VALUE + for (start in pairs.indices.reversed()) { + val oldest = pairs[start] + val gap = oldest.userAlongMeters - oldest.busAlongMeters + minGap = minOf(minGap, gap) + maxGap = maxOf(maxGap, gap) + if (maxGap - minGap > COMOVE_MAX_GAP_DRIFT_METERS) { + // Reaching further back can only widen the drift. + return false + } + if (pairs.size - start < COMOVE_MIN_REPORTS) { + continue + } + val spanSeconds = (newest.gpsTimeMs - oldest.gpsTimeMs) / 1000.0 + if (spanSeconds < COMOVE_MIN_SPAN_SECONDS) { + continue + } + val busTravel = newest.busAlongMeters - oldest.busAlongMeters + val userTravel = newest.userAlongMeters - oldest.userAlongMeters + if (busTravel < COMOVE_MIN_TRAVEL_METERS || + userTravel < busTravel * COMOVE_MIN_USER_TRAVEL_SHARE || + userTravel / spanSeconds < COMOVE_MIN_USER_SPEED_MPS + ) { + continue + } + // A fixed offset between the two clocks shows up as a fixed number + // of metres at a given speed, so the allowance grows with speed. + val gapLimit = COMOVE_MAX_GAP_METERS + + COMOVE_CLOCK_ALLOWANCE_SECONDS * (busTravel / spanSeconds) + if (maxGap > gapLimit || minGap < -gapLimit) { + continue + } + if (!movedAtVehicleSpeed(oldest.gpsTimeMs, newest.gpsTimeMs)) { + continue + } + return true + } + return false + } + + /** + * Layer B. 500 m in 200 s is 2.5 m/s — above a brisk walk and above a bus + * that sat in traffic for the whole window — so a pedestrian following the + * same street from the stop is not mistaken for a passenger. + * + * Travel that happened before the rider last answered "not yet" does not + * count: that is the movement the answer was about, and letting it count + * again would overrule the answer a moment after it was given. Evidence + * starts accumulating from scratch instead. + */ + private fun detectSelfProgress( + nowMs: Long, + boardingAlong: Double, + latestUser: UserSample, + ): Boolean { + if (latestUser.timestampMs < lastDeclineAtMs) { + return false + } + val windowStartMs = maxOf(nowMs - SELF_PROGRESS_WINDOW_MS, lastDeclineAtMs) + val origin = userTrack.firstOrNull { sample -> + sample.timestampMs >= windowStartMs && + sample.accuracyMeters <= SELF_PROGRESS_MAX_ACCURACY_METERS && + abs(sample.alongMeters - boardingAlong) <= BOARDING_ORIGIN_RADIUS_METERS + } ?: return false + val travel = latestUser.alongMeters - origin.alongMeters + val elapsedSeconds = (latestUser.timestampMs - origin.timestampMs) / 1000.0 + if (travel < SELF_PROGRESS_MIN_METERS || elapsedSeconds <= 0.0) { + return false + } + return travel / elapsedSeconds >= SELF_PROGRESS_MIN_SPEED_MPS && + movedAtVehicleSpeed(origin.timestampMs, latestUser.timestampMs) + } + + /** + * Doppler speed is immune to the position wander that makes a stationary + * phone look like it travelled, so when the fixes carry it, real movement + * must show up there too. + */ + private fun movedAtVehicleSpeed(fromMs: Long, toMs: Long): Boolean { + var withSpeed = 0 + var fast = 0 + userTrack.forEach { sample -> + if (sample.timestampMs < fromMs || sample.timestampMs > toMs) { + return@forEach + } + val speed = sample.speedMetersPerSecond ?: return@forEach + withSpeed++ + if (speed >= VEHICLE_SPEED_MPS) { + fast++ + } + } + return withSpeed < MIN_SPEED_SAMPLES || fast >= MIN_FAST_SAMPLES + } + + private fun trackDepartures( + vehicles: List, + boardingAlong: Double, + nowMs: Long, + ) { + vehicles.forEach { vehicle -> + if (!vehicle.samePath || !vehicle.onRoute) { + return@forEach + } + val offset = vehicle.alongMeters - boardingAlong + if (offset <= AT_STOP_MARGIN_METERS) { + seenBeforeBoardingStopAt[vehicle.vehicleId] = vehicle.receivedAtMs + } else if (offset > LEFT_STOP_MARGIN_METERS) { + val seenAt = seenBeforeBoardingStopAt.remove(vehicle.vehicleId) ?: return@forEach + // A vehicle that went quiet short of the stop and turns up past + // it much later left long ago; nobody is boarding it now. + if (vehicle.receivedAtMs - seenAt <= DEPARTURE_MAX_REPORT_GAP_MS) { + registerDeparture(vehicle.vehicleId, nowMs) + } + } + } + } + + /** + * Counts a bus leaving the boarding stop, which is what the question is + * asked about. + * + * One departure can reach the app twice, up to a minute and a half apart: + * from the position feed, which names the vehicle, and from the ETA rolling + * over, which names it only where the stop feed says which bus was at the + * stop. Two reports naming the same vehicle are one departure, and two + * naming different vehicles are two, however close together. + * + * A rollover that names nothing, beside a departure the position feed + * reported, is not asked about twice — it is most often the same bus — but + * nothing shows that it is. It may be a second bus with no position feed, + * so the pair is remembered as possibly two ([Departure.mayBeTwoBuses]) + * and an answer does not name the one that happens to have a plate. + */ + private fun registerDeparture(vehicleId: String?, nowMs: Long, byEta: Boolean = false) { + if (boarded) { + return + } + if (vehicleId != null && isRejected(vehicleId, nowMs)) { + // The rider already said this bus was not theirs. Asking again + // about the same one is the noise the rejection exists to avoid; + // every other vehicle is untouched. + if (byEta) { + declinedDepartures.firstOrNull { it.vehicleId == vehicleId }?.reportedByEta = true + } + return + } + if (answeredByTheLastDecline(vehicleId, nowMs, byEta)) { + return + } + val boardingStop = boardingIndex + val recentUser = userTrack.filter { nowMs - it.timestampMs <= DEPARTURE_USER_LOOKBACK_MS } + // With no fix at all there is no telling where the user is, and asking + // is the only tool left. + val userWasAtStop = recentUser.isEmpty() || recentUser.any { sample -> + boardingStop >= 0 && + line.metersToStop(sample.latitude, sample.longitude, boardingStop) <= + BOARDING_STOP_RADIUS_METERS + } + if (pendingCheckSinceMs != 0L) { + if (coveredByThePendingQuestion(vehicleId, nowMs, byEta)) { + return + } + // Another bus has left with the question unanswered. It joins what + // the question is about rather than taking it over: the rider may + // be on either, and "yes" will not say which. + pendingDepartures.removeAll { nowMs - it.atMs > BOARDING_CHECK_MAX_AGE_MS } + pendingDepartures += Departure(nowMs, vehicleId, reportedByEta = byEta) + if (userWasAtStop && nowMs - pendingCheckSinceMs >= DEPARTURE_DEDUPE_MS) { + // Long enough after the last time of asking to ask afresh. + pendingCheckSinceMs = nowMs + pendingCheckAnchor = recentUser.lastOrNull() + } + return + } + if (!userWasAtStop) { + return + } + pendingCheckSinceMs = nowMs + pendingDepartures.clear() + pendingDepartures += Departure(nowMs, vehicleId, reportedByEta = byEta) + pendingCheckAnchor = recentUser.lastOrNull() + } + + /** + * Whether this report is of a departure the rider's last "not yet" already + * covered, reaching the app a second time. + */ + private fun answeredByTheLastDecline(vehicleId: String?, nowMs: Long, byEta: Boolean): Boolean { + // The window runs from the departure, not from the answer: a rider may + // take minutes to reply, and what leaves the stop after that is the + // next bus. + declinedDepartures.removeAll { nowMs - it.atMs >= DEPARTURE_DEDUPE_MS } + if (vehicleId == null) { + // The ETA catching up with a bus the rider has answered for. Each + // bus rolls the ETA over once, so each is caught up with once: a + // rollover beyond that is another bus leaving. + val awaited = declinedDepartures.firstOrNull { !it.reportedByEta } + if (awaited != null) { + awaited.reportedByEta = true + return true + } + if (declinedRideAtMs != 0L && nowMs - declinedRideAtMs < DEPARTURE_DEDUPE_MS) { + declinedRideAtMs = 0L + return true + } + return false + } + if (byEta) { + // A second rollover, with a plate: not the echo of the first. + return false + } + // The question came from the ETA alone, so the answer had no vehicle to + // be held against. This is that bus, now named — unless it was still + // at the stop after that one had gone. + val unnamed = declinedDepartures.firstOrNull { it.vehicleId == null } ?: return false + if (wasAtTheStopAfter(vehicleId, unnamed.atMs)) { + return false + } + unnamed.vehicleId = vehicleId + rejectedAt[vehicleId] = lastDeclineAtMs + return true + } + + /** + * Whether this report is the other feed's account, or a repeat, of a + * departure the pending question already covers. + */ + private fun coveredByThePendingQuestion(vehicleId: String?, nowMs: Long, byEta: Boolean): Boolean { + if (vehicleId != null) { + val same = pendingDepartures.firstOrNull { it.vehicleId == vehicleId } + if (same != null) { + same.reportedByEta = same.reportedByEta || byEta + return true + } + } + val recent = pendingDepartures.filter { nowMs - it.atMs < DEPARTURE_DEDUPE_MS } + if (vehicleId == null) { + // A rollover naming nothing, beside a bus the position feed saw + // leave: probably that bus, possibly another. + val unreported = recent.firstOrNull { !it.reportedByEta } ?: return false + unreported.reportedByEta = true + unreported.mayBeTwoBuses = true + return true + } + if (byEta) { + return false + } + // The position feed reporting a bus beside a rollover that named + // nothing. One still at the stop after the rollover is another bus. + val unnamed = recent.firstOrNull { it.vehicleId == null } ?: return false + if (wasAtTheStopAfter(vehicleId, unnamed.atMs)) { + return false + } + unnamed.vehicleId = vehicleId + unnamed.mayBeTwoBuses = true + return true + } + + /** Whether [vehicleId] reported itself at or short of the boarding stop later than [atMs]. */ + private fun wasAtTheStopAfter(vehicleId: String, atMs: Long): Boolean { + val boardingStop = boardingIndex + if (boardingStop < 0) { + return false + } + val boardingAlong = line.alongOfStop(boardingStop) + return busTracks[vehicleId].orEmpty().any { sample -> + sample.samePath && sample.onRoute && sample.gpsTimeMs > atMs && + sample.alongMeters - boardingAlong <= AT_STOP_MARGIN_METERS + } + } + + private fun resolveBoardingPromptDue( + nowMs: Long, + usableUser: UserSample?, + boardingStop: Int, + ): Boolean { + if (boarded || pendingCheckSinceMs == 0L) { + return false + } + val ageMs = nowMs - pendingCheckSinceMs + if (ageMs > BOARDING_CHECK_MAX_AGE_MS) { + clearPendingCheck() + return false + } + if (boardingStop >= 0 && usableUser != null && + line.metersToStop(usableUser.latitude, usableUser.longitude, boardingStop) > + BOARDING_STOP_ABANDONED_RADIUS_METERS + ) { + // The user is nowhere near the stop any more, so this bus was never + // their ride; stop waiting for an answer. + clearPendingCheck() + return false + } + if (ageMs < BOARDING_PROMPT_DELAY_MS) { + return false + } + // A stationary phone is the one thing that reliably means "still + // waiting", so while the user has not moved there is nothing to ask. + return !stayedBehind(nowMs, usableUser) + } + + private fun stayedBehind(nowMs: Long, user: UserSample?): Boolean { + user ?: return false + if (nowMs - user.timestampMs > STAYED_BEHIND_MAX_FIX_AGE_MS || + user.accuracyMeters > STAYED_BEHIND_MAX_ACCURACY_METERS + ) { + return false + } + val anchor = pendingCheckAnchor ?: return false + return line.metersBetween(anchor.latitude, anchor.longitude, user.latitude, user.longitude) < + STAYED_BEHIND_MAX_MOVE_METERS + } + + private fun clearPendingCheck() { + pendingCheckSinceMs = 0L + pendingDepartures.clear() + pendingCheckAnchor = null + } + + /** + * Once on board, the vehicle to follow is the one the user is inside. Its + * reported position trails the user's own fix by the feed's delay, so it is + * expected a little behind and never far ahead. It is also on this trip's + * direction and on the route: a vehicle parked beside the route projects + * onto it too, and can be the nearest one there is. + */ + private fun resolveTrackedVehicle( + vehicles: List, + usableUser: UserSample?, + nowMs: Long, + ): String? { + if (!boarded) { + return null + } + val locked = vehicles.firstOrNull { it.vehicleId == trackedVehicleId && it.samePath && it.onRoute } + if (usableUser == null) { + // Remember the ride through an outage or an invalid report, but + // only a fresh, valid report supplies progress in snapshot(). A + // manual confirmation may also precede the first vehicle response. + return trackedVehicleId ?: if (evidence == BoardingEvidence.USER_CONFIRMED) { + identifyBoardingVehicle(nowMs) + } else { + null + } + } + if (locked != null && + usableUser.alongMeters - locked.alongMeters in -TRACK_MAX_AHEAD_METERS..TRACK_KEEP_BEHIND_METERS + ) { + return locked.vehicleId + } + return vehicles + .filter { it.samePath && it.onRoute } + .filter { usableUser.alongMeters - it.alongMeters in -TRACK_MAX_AHEAD_METERS..TRACK_PICK_BEHIND_METERS } + .minByOrNull { abs(usableUser.alongMeters - it.alongMeters) } + ?.vehicleId + } + + /** + * Associates an unprompted "I have boarded" with a vehicle, not just a + * route. + * + * The bus must have an unbroken run on this direction and route containing + * a fix physically at the boarding stop, and still be at most 250 m ahead. + * Projection alone would clamp a bus before the first stop to zero + * progress, or put a bus on a parallel street level with the stop. + * + * That fix must have been made in the minute and a half up to the moment + * the rider confirmed, by the vehicle's own clock. The report carrying it + * can reach the app afterwards — the feed runs half a minute behind — which + * is what the two-minute window is for. A bus that pulls in after the + * rider said they were already on one is the next bus, whenever it is seen. + * + * Nor is it a bus the app had already watched leave by then: whether the + * rider was on that one is what the boarding question asks. + * + * And it must be the only bus there. With another at the stop, just gone + * from it, or near enough to have pulled in, the rider can be on either. + */ + private fun identifyBoardingVehicle(nowMs: Long): String? { + val confirmedAt = manualConfirmationAtMs ?: return null + if (nowMs - confirmedAt !in 0L..MANUAL_IDENTIFICATION_WINDOW_MS || manualVehicleAmbiguous) { + return null + } + val boardingStop = boardingIndex + val hasUsableUser = userTrack.lastOrNull()?.let { nowMs - it.timestampMs <= LOCATION_STALE_MS } == true + if (boardingStop < 0 || hasUsableUser) { + // With a usable location the nearest vehicle is resolved from the user. + return null + } + val boardingAlong = line.alongOfStop(boardingStop) + val candidates = liveVehicles(nowMs).filter { vehicle -> + if (!vehicle.samePath || !vehicle.onRoute || + vehicle.alongMeters - boardingAlong !in -AT_STOP_MARGIN_METERS..MANUAL_VEHICLE_MAX_AHEAD_METERS + ) { + return@filter false + } + val track = busTracks[vehicle.vehicleId] ?: return@filter false + val latest = track.lastOrNull() ?: return@filter false + if (nowMs - latest.receivedAtMs !in 0L..MANUAL_VEHICLE_EVIDENCE_MAX_AGE_MS) { + return@filter false + } + val run = track.reversed().takeWhile { it.samePath && it.onRoute } + // A bus already seen pulling away before the rider spoke up is one + // the app knew had gone: whether they were on it is what the + // boarding question asks, and only an answer to that settles it. + val hadLeftBeforeConfirming = run.any { sample -> + sample.gpsTimeMs <= confirmedAt && sample.alongMeters - boardingAlong > LEFT_STOP_MARGIN_METERS + } + !hadLeftBeforeConfirming && run.any { sample -> + confirmedAt - sample.gpsTimeMs in 0L..MANUAL_VEHICLE_EVIDENCE_MAX_AGE_MS && + abs(sample.alongMeters - boardingAlong) <= AT_STOP_MARGIN_METERS && + line.metersToStop(sample.latitude, sample.longitude, boardingStop) <= + MANUAL_AT_STOP_RADIUS_METERS + } + } + val candidate = candidates.singleOrNull() + if (candidates.size > 1 || + (candidate != null && anotherBusIsBoardable(confirmedAt, boardingAlong, candidate.vehicleId)) + ) { + // Which of them the rider took is not something a later report + // settles: one leaving the distance bound first proves nothing. A + // usable phone fix can still identify the ride through + // resolveTrackedVehicle(). + manualVehicleAmbiguous = true + return null + } + return candidate?.vehicleId + } + + /** + * Whether the feeds place a vehicle other than [except] where the rider + * could have boarded it around [atMs]: at the boarding stop, just pulling + * away from it, or near enough short of it to have pulled in since — a + * report is half a minute old by the time it arrives. A bus the rider has + * turned down is not one of them. + */ + private fun anotherBusIsBoardable(atMs: Long, boardingAlong: Double, except: String): Boolean { + return busTracks.any { (vehicleId, track) -> + if (vehicleId == except || isRejected(vehicleId, atMs)) { + return@any false + } + val last = track.lastOrNull { it.gpsTimeMs <= atMs } ?: return@any false + last.samePath && last.onRoute && + atMs - last.receivedAtMs <= BUS_REPORT_STALE_MS && + last.alongMeters - boardingAlong in -BOARDABLE_APPROACH_METERS..MANUAL_VEHICLE_MAX_AHEAD_METERS + } + } + + private fun liveVehicles(nowMs: Long): List { + return busTracks.entries.mapNotNull { (vehicleId, track) -> + val last = track.lastOrNull() ?: return@mapNotNull null + if (nowMs - last.receivedAtMs > BUS_REPORT_STALE_MS) { + return@mapNotNull null + } + LiveVehicle(vehicleId, last.alongMeters, last.samePath, last.onRoute, last.receivedAtMs) + } + } + + /** The user's along-route position at [timestampMs], linearly interpolated. */ + private fun userAlongAt(timestampMs: Long): Double? { + var previous: UserSample? = null + userTrack.forEach { sample -> + if (sample.timestampMs >= timestampMs) { + val before = previous + if (before == null) { + return sample.alongMeters.takeIf { + sample.timestampMs - timestampMs <= INTERPOLATION_TOLERANCE_MS + } + } + val spanMs = sample.timestampMs - before.timestampMs + if (spanMs > MAX_INTERPOLATION_SPAN_MS) { + // No fixes for too long to guess where the user was. + return null + } + val fraction = (timestampMs - before.timestampMs).toDouble() / spanMs + return before.alongMeters + fraction * (sample.alongMeters - before.alongMeters) + } + previous = sample + } + return previous?.alongMeters?.takeIf { + timestampMs - (previous?.timestampMs ?: 0L) <= INTERPOLATION_TOLERANCE_MS + } + } + + private fun prune(nowMs: Long) { + while (userTrack.isNotEmpty() && nowMs - userTrack.first().timestampMs > USER_TRACK_MAX_AGE_MS) { + userTrack.removeFirst() + } + busTracks.values.forEach { track -> + while (track.isNotEmpty() && nowMs - track.first().receivedAtMs > BUS_TRACK_MAX_AGE_MS) { + track.removeFirst() + } + } + busTracks.entries.removeAll { it.value.isEmpty() } + rejectedAt.entries.removeAll { nowMs - it.value > REJECTED_VEHICLE_TTL_MS } + seenBeforeBoardingStopAt.keys.retainAll(busTracks.keys) + } + + private fun indexOfStop(stopId: Int?): Int { + return if (stopId == null) -1 else stopIds.indexOf(stopId) + } + + private data class UserSample( + val latitude: Double, + val longitude: Double, + val alongMeters: Double, + val timestampMs: Long, + val accuracyMeters: Float, + val speedMetersPerSecond: Float?, + ) + + private data class BusSample( + val latitude: Double, + val longitude: Double, + val alongMeters: Double, + val gpsTimeMs: Long, + val receivedAtMs: Long, + val samePath: Boolean, + val onRoute: Boolean, + ) + + /** + * One bus leaving the boarding stop. [vehicleId] is set once a feed has + * named it, [reportedByEta] once the ETA rolling over has reported it. + * [mayBeTwoBuses] marks a position report and an unnamed rollover taken + * together without anything to show they were the same bus. + */ + private class Departure( + val atMs: Long, + var vehicleId: String?, + var reportedByEta: Boolean, + var mayBeTwoBuses: Boolean = false, + ) + + private data class LiveVehicle( + val vehicleId: String, + val alongMeters: Double, + val samePath: Boolean, + val onRoute: Boolean, + val receivedAtMs: Long, + ) + + private data class CoMovementPair( + val userAlongMeters: Double, + val busAlongMeters: Double, + val gpsTimeMs: Long, + ) + + private companion object { + const val MAX_USER_SAMPLES = 240 + const val MAX_BUS_SAMPLES = 60 + const val USER_TRACK_MAX_AGE_MS = 400_000L + const val BUS_TRACK_MAX_AGE_MS = 360_000L + + const val MAX_USABLE_ACCURACY_METERS = 100f + const val LOCATION_STALE_MS = 90_000L + const val USER_STOP_PROXIMITY_MAX_METERS = 100.0 + const val MAX_PLAUSIBLE_SPEED_MPS = 55.0 + const val INTERPOLATION_TOLERANCE_MS = 6_000L + const val MAX_INTERPOLATION_SPAN_MS = 30_000L + + const val COMOVE_WINDOW_MS = 180_000L + const val COMOVE_MIN_REPORTS = 3 + const val COMOVE_MIN_SPAN_SECONDS = 30.0 + const val COMOVE_MIN_TRAVEL_METERS = 150.0 + const val COMOVE_MIN_USER_TRAVEL_SHARE = 0.7 + const val COMOVE_MIN_USER_SPEED_MPS = 2.2 + const val COMOVE_MAX_GAP_DRIFT_METERS = 110.0 + const val COMOVE_MAX_GAP_METERS = 200.0 + const val COMOVE_CLOCK_ALLOWANCE_SECONDS = 12.0 + + const val SELF_PROGRESS_WINDOW_MS = 200_000L + const val SELF_PROGRESS_MIN_METERS = 500.0 + const val SELF_PROGRESS_MIN_SPEED_MPS = 2.5 + const val SELF_PROGRESS_MAX_ACCURACY_METERS = 60f + const val BOARDING_ORIGIN_RADIUS_METERS = 250.0 + + const val VEHICLE_SPEED_MPS = 3f + const val MIN_SPEED_SAMPLES = 3 + const val MIN_FAST_SAMPLES = 2 + + const val BUS_REPORT_STALE_MS = 180_000L + const val ON_ROUTE_MIN_CROSS_TRACK_METERS = 150.0 + const val ON_ROUTE_SEGMENT_SHARE = 0.6 + const val AT_STOP_MARGIN_METERS = 40.0 + const val LEFT_STOP_MARGIN_METERS = 120.0 + const val DEPARTURE_MAX_REPORT_GAP_MS = 180_000L + const val MANUAL_AT_STOP_RADIUS_METERS = 80.0 + const val MANUAL_VEHICLE_MAX_AHEAD_METERS = 250.0 + const val MANUAL_VEHICLE_EVIDENCE_MAX_AGE_MS = 90_000L + const val MANUAL_IDENTIFICATION_WINDOW_MS = 120_000L + const val BOARDABLE_APPROACH_METERS = 300.0 + + const val ETA_ARRIVING_SECONDS = 30 + const val ETA_ROLLOVER_SECONDS = 240 + const val ETA_ROLLOVER_WINDOW_MS = 150_000L + + const val DEPARTURE_DEDUPE_MS = 90_000L + const val DEPARTURE_USER_LOOKBACK_MS = 150_000L + const val BOARDING_STOP_RADIUS_METERS = 300.0 + const val BOARDING_STOP_ABANDONED_RADIUS_METERS = 400.0 + const val BOARDING_PROMPT_DELAY_MS = 40_000L + const val BOARDING_CHECK_MAX_AGE_MS = 300_000L + + /** + * How long a turned-down vehicle stays set aside. Long enough to cover + * the wait for the next bus being asked about, short enough that a + * vehicle which really is the rider's — a late arrival, or the same + * vehicle on its next trip — is not written off for the rest of the day. + */ + const val REJECTED_VEHICLE_TTL_MS = 1_200_000L + const val STAYED_BEHIND_MAX_FIX_AGE_MS = 30_000L + const val STAYED_BEHIND_MAX_ACCURACY_METERS = 50f + const val STAYED_BEHIND_MAX_MOVE_METERS = 60.0 + + const val TRACK_MAX_AHEAD_METERS = 250.0 + const val TRACK_KEEP_BEHIND_METERS = 2_500.0 + const val TRACK_PICK_BEHIND_METERS = 1_500.0 + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchStopIdTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchStopIdTest.kt new file mode 100644 index 0000000..d87f3a2 --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/AppLaunchStopIdTest.kt @@ -0,0 +1,228 @@ +package tw.avianjay.taiwanbus.flutter + +import android.app.Application +import android.app.NotificationManager +import android.content.Intent +import android.database.sqlite.SQLiteDatabase +import android.location.Location +import java.io.File +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +/** + * A stop the user taps has to arrive in Flutter as the id Flutter knows. + * + * Stop ids are 64-bit in some feeds (高雄's are 11-12 digits), and Dart keys its + * stops by the raw value. The native side folds ids to a positive `Int` to join + * the live feeds, and that folded number used to be what widget and + * notification taps carried too — naming no stop the Flutter side has, so a + * 高雄 favourite opened its route with nothing selected. + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34], application = Application::class) +class AppLaunchStopIdTest { + private val application: Application get() = RuntimeEnvironment.getApplication() + + @Test + fun theFullWidthIdSurvivesTheLaunchPayload() { + assertNotEquals( + "this id only proves anything if folding changes it", + RAW_STOP_ID, + foldStopId(RAW_STOP_ID).toLong(), + ) + + val payload = launchPayloadOf( + AppLaunchConstants.createRouteDetailIntent( + context = application, + provider = "khh", + routeKey = 100, + routeId = "KHH100", + pathId = 0, + stopId = RAW_STOP_ID, + destinationPathId = 0, + destinationStopId = RAW_STOP_ID + 30, + ), + ) + + assertEquals("khh", payload["provider"]) + assertEquals(RAW_STOP_ID, payload.stopId("stopId")) + assertEquals(RAW_STOP_ID + 30, payload.stopId("destinationStopId")) + } + + @Test + fun idsThatFitInAnIntAreUnchanged() { + // 台中 and 雙北 ids are small, and must keep arriving exactly as before. + val payload = launchPayloadOf( + AppLaunchConstants.createRouteDetailIntent( + context = application, + provider = "txg", + routeKey = 1, + pathId = 0, + stopId = 13567L, + ), + ) + + assertEquals(13567L, payload.stopId("stopId")) + } + + @Test + fun aMissingStopIdIsStillAbsentFromThePayload() { + val payload = launchPayloadOf( + AppLaunchConstants.createRouteDetailIntent( + context = application, + provider = "txg", + routeKey = 1, + ), + ) + + assertFalse("no stop chosen means no stop in the payload", payload.containsKey("stopId")) + assertFalse(payload.containsKey("destinationStopId")) + } + + @Test + fun aShortcutPinnedBeforeTheIdsWereWidenedStillOpensItsStop() { + // A pinned shortcut keeps the intent it was created with for as long as + // it sits on the home screen, so the extras older builds wrote — these + // names, holding Ints — are a format that has to stay readable. + val pinnedByAnOlderBuild = Intent() + .putExtra("launch_target", "route_detail") + .putExtra("provider", "txg") + .putExtra("route_key", 1) + .putExtra("path_id", 0) + .putExtra("stop_id", 13567) + .putExtra("destination_path_id", 0) + .putExtra("destination_stop_id", 13570) + + val payload = launchPayloadOf(pinnedByAnOlderBuild) + + assertEquals(13567L, payload.stopId("stopId")) + assertEquals(13570L, payload.stopId("destinationStopId")) + } + + @Test + fun tappingAWidgetRowLaunchesWithTheFullWidthId() { + // The config is what Dart saves: the ids as Dart holds them. + val config = JSONObject().put( + "通勤", + JSONArray().put( + JSONObject() + .put("type", "boarding") + .put("provider", "khh") + .put("routeKey", 100) + .put("pathId", 0) + .put("stopId", RAW_STOP_ID) + .put("routeId", "KHH100") + .put("stopName", "測試站") + .put("destinationPathId", 0) + .put("destinationStopId", RAW_STOP_ID + 30), + ), + ).toString() + val item = FavoriteGroupWidgetSupport.parseFavoriteGroups(config).getValue("通勤").single() + + assertEquals( + "the live feed is still looked up by the folded key", + foldStopId(RAW_STOP_ID), + item.stopId, + ) + + val tap = FavoriteGroupWidgetSupport.createItemPendingIntent(application, item) + val payload = launchPayloadOf(shadowOf(tap).savedIntent) + + assertEquals(RAW_STOP_ID, payload.stopId("stopId")) + assertEquals(RAW_STOP_ID + 30, payload.stopId("destinationStopId")) + } + + @Test + fun tappingASmartRouteNotificationLaunchesWithTheFullWidthId() { + // The stop comes out of the route database Flutter syncs, where the id + // is stored as text. + val databaseFile = File( + application.filesDir.parentFile, + "app_flutter/.yabus_backend/bus_khh_v2.sqlite", + ) + databaseFile.parentFile!!.mkdirs() + SQLiteDatabase.openOrCreateDatabase(databaseFile, null).use { database -> + database.execSQL("CREATE TABLE routes (route_key INTEGER, route_id TEXT, route_name TEXT)") + database.execSQL("CREATE TABLE paths (route_key INTEGER, path_id INTEGER, path_name TEXT)") + database.execSQL( + "CREATE TABLE stops (route_key INTEGER, path_id INTEGER, stop_id TEXT, " + + "stop_name TEXT, sequence INTEGER, lon REAL, lat REAL)", + ) + database.execSQL("INSERT INTO routes VALUES (100, 'KHH100', '100')") + database.execSQL("INSERT INTO paths VALUES (100, 0, '去程')") + database.execSQL( + "INSERT INTO stops VALUES (100, 0, '$RAW_STOP_ID', '測試站', 1, 120.338401, 22.60612)", + ) + } + val routeData = requireNotNull( + SmartRouteNotificationSupport.loadRouteData(application, "khh", 100, "100"), + ) + val here = Location("test").apply { + latitude = 22.60612 + longitude = 120.338401 + } + val nearestStop = requireNotNull( + SmartRouteNotificationSupport.findNearestStop(here, routeData, pathId = 0), + ) + + assertEquals( + "the live feed is still looked up by the folded key", + foldStopId(RAW_STOP_ID), + nearestStop.stopId, + ) + + SmartRouteNotificationSupport.ensureChannel(application) + SmartRouteNotificationSupport.showNotification( + context = application, + profile = SmartRouteProfile( + provider = "khh", + routeKey = 100, + pathId = 0, + routeName = "100", + totalOpens = 5, + lastOpenedAtMs = 0L, + totalSelections = 0, + lastSelectedAtMs = 0L, + hourlyOpens = mapOf(8 to 5), + hourlySelections = emptyMap(), + ), + routeData = routeData, + nearestStop = nearestStop, + liveStop = SmartLiveStop(sec = 120, msg = null), + distanceMeters = nearestStop.distanceMeters, + nowMs = 1L, + ) + val posted = shadowOf(application.getSystemService(NotificationManager::class.java)) + .allNotifications + .single() + val payload = launchPayloadOf(shadowOf(posted.contentIntent).savedIntent) + + assertEquals(RAW_STOP_ID, payload.stopId("stopId")) + } + + private fun launchPayloadOf(intent: Intent): Map { + val payload = AppLaunchConstants.extractLaunchPayload(intent) + assertTrue("expected a launch payload", payload != null) + return payload!! + } + + private fun Map.stopId(key: String): Long? = (this[key] as Number?)?.toLong() + + private companion object { + /** + * A real 高雄 stop id. Its low 32 bits are negative, so any path that + * narrows it to an `Int` — by folding or by truncating — changes it. + */ + const val RAW_STOP_ID = 101111144601L + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RideFixTimeTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RideFixTimeTest.kt new file mode 100644 index 0000000..e92fc85 --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RideFixTimeTest.kt @@ -0,0 +1,68 @@ +package tw.avianjay.taiwanbus.flutter + +import android.app.Application +import android.location.Location +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * A fix handed to the ride detector has to carry when it was really measured. + * + * It used to be aged by subtracting `Location.time` (Unix epoch) from + * `SystemClock.elapsedRealtime()` (time since boot). That difference is hugely + * negative and was clamped to zero, so every fix — including a position cached + * minutes earlier — reached the detector stamped "now". + */ +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34], application = Application::class) +class RideFixTimeTest { + private val nowWallMs = 1_700_000_000_000L + private val nowElapsedNanos = TimeUnit.HOURS.toNanos(3) + + @Test + fun aCachedFixIsPlacedItsRealAgeInThePast() { + val cached = Location("fused").apply { + elapsedRealtimeNanos = nowElapsedNanos - TimeUnit.MINUTES.toNanos(5) + // A provider's clock need not agree with the device's: the age has + // to come from the monotonic stamp, not from this. + time = nowWallMs + TimeUnit.DAYS.toMillis(1) + } + + assertEquals(nowWallMs - 300_000L, rideFixTimestampMs(cached, nowWallMs, nowElapsedNanos)) + } + + @Test + fun aFixStampedAheadOfTheClockCountsAsCurrent() { + val ahead = Location("fused").apply { + elapsedRealtimeNanos = nowElapsedNanos + TimeUnit.SECONDS.toNanos(2) + } + + assertEquals(nowWallMs, rideFixTimestampMs(ahead, nowWallMs, nowElapsedNanos)) + } + + @Test + fun aHandBuiltPositionIsAgedByItsOwnTime() { + // The session's starting position is built from what Flutter sent, and + // has no monotonic stamp; Flutter says when it was measured instead. + val seed = Location("trip_monitor_session").apply { + time = nowWallMs - 180_000L + } + + assertEquals(nowWallMs - 180_000L, rideFixTimestampMs(seed, nowWallMs, nowElapsedNanos)) + } + + @Test + fun aHandBuiltPositionWithNoUsableTimeCountsAsCurrent() { + val untimed = Location("trip_monitor_session") + val fromTheFuture = Location("trip_monitor_session").apply { + time = nowWallMs + 60_000L + } + + assertEquals(nowWallMs, rideFixTimestampMs(untimed, nowWallMs, nowElapsedNanos)) + assertEquals(nowWallMs, rideFixTimestampMs(fromTheFuture, nowWallMs, nowElapsedNanos)) + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorServiceTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorServiceTest.kt index 20b3dbc..a6858a6 100644 --- a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorServiceTest.kt +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/RouteTripMonitorServiceTest.kt @@ -3,11 +3,20 @@ package tw.avianjay.taiwanbus.flutter import android.app.Application import android.app.Notification import android.app.NotificationManager +import android.app.PendingIntent import android.app.Service +import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo +import android.location.Location +import android.os.Looper +import android.os.SystemClock +import org.robolectric.Shadows.shadowOf +import org.robolectric.shadows.ShadowSystemClock +import java.util.concurrent.CountDownLatch import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse @@ -39,19 +48,23 @@ class RouteTripMonitorServiceTest { private lateinit var service: RouteTripMonitorService private lateinit var shadow: RecordingTripServiceShadow + @Volatile + private var backend: (String) -> String? = { null } + @Before fun setUp() { controller = Robolectric.buildService(RouteTripMonitorService::class.java).create() service = controller.get() shadow = Shadow.extract(service) + // No test reaches the real backend; each one says what it answers. + service.fetchBackendJsonText = { path -> backend(path) } } @After fun tearDown() { service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 99) controller.destroy() - val executor = ReflectionHelpers.getField(service, "ioExecutor") - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)) + assertTrue(ioExecutor().awaitTermination(5, TimeUnit.SECONDS)) } @Test @@ -150,7 +163,7 @@ class RouteTripMonitorServiceTest { } @Test - fun addingDestinationAfterBoardingKeepsRideAndTrackedVehicle() { + fun addingDestinationAfterBoardingKeepsTheRide() { val initialSession = sessionPayload() + mapOf( "routeId" to "TXG1", "boardingStopId" to 1, @@ -173,18 +186,20 @@ class RouteTripMonitorServiceTest { ), ) service.onStartCommand(startIntent(initialSession), 0, 1) - ReflectionHelpers.setField(service, "rideConfirmed", true) - ReflectionHelpers.setField(service, "trackedBusId", "KKA-1234") + // The user confirms the ride while waiting, exactly as the notification + // action does. + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + assertTrue(ride(service).hasBoarded) ReflectionHelpers.setField(service, "destinationAlertStage", 2) val destinationSession = initialSession + mapOf( "destinationStopId" to 2, "destinationStopName" to "下車站", ) - service.onStartCommand(startIntent(destinationSession), 0, 2) + service.onStartCommand(startIntent(destinationSession), 0, 3) - assertTrue(ReflectionHelpers.getField(service, "rideConfirmed")) - assertEquals("KKA-1234", ReflectionHelpers.getField(service, "trackedBusId")) + assertTrue("choosing a destination must not drop the ride", ride(service).hasBoarded) + assertEquals(BoardingEvidence.USER_CONFIRMED, ride(service).evidence) assertEquals(0, ReflectionHelpers.getField(service, "destinationAlertStage")) } @@ -200,8 +215,6 @@ class RouteTripMonitorServiceTest { )), ) service.onStartCommand(startIntent(payload(600, emptyList(), "MAIN-001")), 0, 1) - ReflectionHelpers.setField(service, "activeBoardingVehicleId", "MAIN-001") - ReflectionHelpers.setField(service, "boardingAlertSent", true) service.onStartCommand(startIntent(payload(60, listOf("TXG1", "TXG2"), "FAMILY-001")), 0, 2) val session = ReflectionHelpers.getField(service, "session") @@ -210,21 +223,1250 @@ class RouteTripMonitorServiceTest { val live = ReflectionHelpers.getField>(service, "cachedLiveStops").getValue(1) assertEquals(60, live.seconds) assertEquals(listOf("FAMILY-001"), live.vehicleIds) - assertNull(ReflectionHelpers.getField(service, "activeBoardingVehicleId")) - assertFalse(ReflectionHelpers.getField(service, "boardingAlertSent")) + // The family arriving re-seeds the trip, so no ride survives from the + // preliminary primary-route response. + assertFalse(ride(service).hasBoarded) } @Test fun familyChangesDoNotResetConfirmedRide() { - val initial = sessionPayload() + mapOf("routeId" to "TXG1") + val initial = tripWithDestination() + mapOf("routeId" to "TXG1") service.onStartCommand(startIntent(initial), 0, 1) - ReflectionHelpers.setField(service, "rideConfirmed", true) - ReflectionHelpers.setField(service, "trackedBusId", "FAMILY-001") - service.onStartCommand(startIntent(initial + mapOf("familyRouteIds" to listOf("TXG1", "TXG2"))), 0, 2) - assertTrue(ReflectionHelpers.getField(service, "rideConfirmed")) - assertEquals("FAMILY-001", ReflectionHelpers.getField(service, "trackedBusId")) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + assertTrue(ride(service).hasBoarded) + + service.onStartCommand( + startIntent(initial + mapOf("familyRouteIds" to listOf("TXG1", "TXG2"))), + 0, + 3, + ) + + assertTrue("a confirmed ride must survive the family arriving", ride(service).hasBoarded) + assertEquals(BoardingEvidence.USER_CONFIRMED, ride(service).evidence) + } + + @Test + fun reportingNotBoardedClearsTheRide() { + service.onStartCommand(startIntent(tripWithDestination()), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + assertTrue(ride(service).hasBoarded) + + service.onStartCommand(command("NOT_BOARDED"), 0, 3) + + assertFalse(ride(service).hasBoarded) + assertEquals(BoardingEvidence.NONE, ride(service).evidence) + } + + @Test + fun switchingRouteRebuildsTheRideState() { + service.onStartCommand(startIntent(tripWithDestination()), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + assertTrue(ride(service).hasBoarded) + + service.onStartCommand( + startIntent(tripWithDestination() + mapOf("routeKey" to 7, "routeName" to "7")), + 0, + 3, + ) + + assertFalse("a different route is a different trip", ride(service).hasBoarded) + assertFalse(ride(service).boardingPromptDue) + } + + @Test + fun keepingTheSameTripDoesNotRebuildTheRide() { + val session = tripWithDestination() + mapOf("routeId" to "TXG1") + service.onStartCommand(startIntent(session), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + + // Every refresh the route screen sends carries the same identity, so a + // rebuild here would wipe the ride the user just confirmed. + service.onStartCommand(startIntent(session), 0, 3) + + assertTrue("an identical update must not restart the trip", ride(service).hasBoarded) + assertEquals(BoardingEvidence.USER_CONFIRMED, ride(service).evidence) + } + + @Test + fun theWakeLockDoesNotOutliveTheTrip() { + val lockOf = { + ReflectionHelpers.getField(service, "wakeLock") + } + + service.onStartCommand(startIntent(), 0, 1) + assertNotNull("tracking must hold a wake lock", lockOf()) + assertTrue("the lock must actually be held", lockOf()!!.isHeld) + + service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 2) + + assertNull("a stopped trip must not keep the CPU awake", lockOf()) + } + + @Test + fun theBoardingStopAndItsLiveEtaMeetOnTheSameKey() { + // 高雄 stop ids are 64-bit, and the two sides of this join used to + // compute the key differently — the session wrapped the low 32 bits + // while the feed hashed the digits — so the live ETA of a 高雄 stop + // could never be found. Both now fold the raw id the same way. + // + // This id is a real one from the 高雄 feed, chosen because its low 32 + // bits are negative: an id that wraps to a positive number would agree + // with the fold by accident and hide the bug. + val rawStopId = "101111144601" + + val session = RouteTripMonitorService.parseSessionPayload( + mapOf( + "provider" to "khh", + "routeKey" to 100, + "routeId" to "KHH100", + "pathId" to 0, + "boardingStopId" to rawStopId.toLong(), + "stops" to listOf( + mapOf( + "stopId" to rawStopId.toLong(), + "stopName" to "測試站", + "sequence" to 1, + "lat" to 22.60612, + "lon" to 120.338401, + ), + ), + ), + ) + // The failure this guards against is a silent one: the folded id stops + // being positive, so the session drops the boarding stop entirely and + // the live ETA is never consulted. Say that, rather than let it surface + // as a bare NPE. + assertNotNull("the session must keep a boarding stop for a 64-bit stop id", session) + val boardingStopId = requireNotNull(session?.boardingStopId) { + "the boarding stop was dropped instead of folded to a positive key" + } + // The live ETA now arrives through the family fetch, so the join is + // checked against the same code the service runs. + val liveStops = TripMonitorFamilyRealtime { path -> + assertEquals("routes/KHH100/realtime", path) + org.json.JSONObject( + """{"paths":[{"pathid":0,"stops":[{"stopid":"$rawStopId","eta":120}]}]}""", + ) + }.fetch(session!!) + + assertTrue("the boarding stop must fold to a positive key", boardingStopId > 0) + assertEquals("the session's stop must keep its id", boardingStopId, session.stops.single().stopId) + assertNotNull( + "the live ETA must be reachable from the session's boarding stop", + liveStops[boardingStopId], + ) + assertEquals(120, liveStops[boardingStopId]!!.seconds) + } + + @Test + fun aCachedFixFromMinutesAgoIsNotTreatedAsWhereTheRiderIs() { + // Robolectric's clock starts at boot; move on so "five minutes ago" exists. + ShadowSystemClock.advanceBy(10, TimeUnit.MINUTES) + service.onStartCommand(startIntent(threeStopSession()), 0, 1) + + // What the fused provider hands back first: its cached last location. + feedFix(stopIndex = 2, ageMinutes = 5) + + assertFalse("a five-minute-old fix is not a usable position", ride(service).hasUsableLocation) + assertEquals( + "and must not decide where the trip starts", + -1, + ride(service).boardingStopIndex, + ) + + feedFix(stopIndex = 1, ageMinutes = 0) + + assertTrue(ride(service).hasUsableLocation) + assertEquals("the trip starts where the rider is now", 1, ride(service).boardingStopIndex) + } + + @Test + fun theSessionsStartingPositionIsAgedByWhenFlutterMeasuredIt() { + ShadowSystemClock.advanceBy(10, TimeUnit.MINUTES) + val (lat, lon) = THREE_STOPS[2] + service.onStartCommand( + startIntent( + threeStopSession() + mapOf( + "initialLatitude" to lat, + "initialLongitude" to lon, + "initialLocationTimestampMs" to System.currentTimeMillis() - 5 * 60_000L, + ), + ), + 0, + 1, + ) + refreshRide() + + assertFalse(ride(service).hasUsableLocation) + assertEquals(-1, ride(service).boardingStopIndex) + } + + @Test + fun boardingWithoutADestinationAsksForOneAndPicksTheRideBackUp() { + val route = threeStopSession() + mapOf("routeId" to "TXG1") + val withBoardingStop = route + mapOf( + "boardingStopId" to 1, + "boardingStopName" to "一站", + ) + // With and without a boarding stop chosen: both used to be reported as + // "still waiting" however the ride had been confirmed. + for (waiting in listOf(withBoardingStop, route)) { + service.onStartCommand(startIntent(waiting), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + assertTrue(ride(service).hasBoarded) + + val paused = awaitPause() + + assertEquals("boarded_no_destination", paused.reason) + val prompt = shadowOf(service.getSystemService(NotificationManager::class.java)) + .getNotification(6022) + assertNotNull("the rider must be asked where they get off", prompt) + assertEquals("已偵測上車", prompt.extras.getString(Notification.EXTRA_TITLE)) + + // The rider answers by choosing a stop. By now they are stops away, + // and the bus that called at the boarding stop half a minute ago + // is someone else's. + val calledAt = nowSeconds() - 30 + backend = { path -> + if (path.endsWith("/realtime/buses")) { + busFeed("NEXT-BUS", stopsOf(waiting).first(), pathId = 0, gpsTimeSeconds = calledAt) + } else { + null + } + } + service.onStartCommand( + startIntent(waiting + mapOf("destinationStopId" to 3, "destinationStopName" to "三站")), + 0, + 3, + ) + + assertTrue("choosing a destination continues the same ride", ride(service).hasBoarded) + assertEquals(BoardingEvidence.USER_CONFIRMED, ride(service).evidence) + assertNull(AppRuntimeStateStore.loadPausedTripMonitor(service)) + + fetchLiveData() + assertTrue(ride(service).hasBoarded) + assertNull( + "a bus at the boarding stop now is not the one boarded back then", + ride(service).trackedVehicleId, + ) + backend = { null } + + service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 4) + } + } + + @Test + fun onlyTheRidePausedForADestinationIsPickedBackUp() { + val waiting = threeStopSession() + mapOf("boardingStopId" to 1) + val withDestination = waiting + mapOf("destinationStopId" to 3) + fun startedAsBoarded( + pausedAs: Map, + reason: String, + resumedAs: Map, + pausedMinutesAgo: Long = 1, + ): Boolean { + AppRuntimeStateStore.savePausedTripMonitor( + service, + RouteTripMonitorService.parseSessionPayload(pausedAs)!!, + reason, + ) + // The store stamps the pause with the real clock, so it is aged here. + service.getSharedPreferences("app_runtime_state", Context.MODE_PRIVATE).edit() + .putLong( + "trip_monitor_paused_at_ms", + System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(pausedMinutesAgo), + ) + .commit() + service.onStartCommand(startIntent(resumedAs), 0, 1) + val boarded = ride(service).hasBoarded + service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 2) + return boarded + } + + assertTrue(startedAsBoarded(waiting, "boarded_no_destination", withDestination)) + assertFalse( + "a pause the rider asked for says nothing about being on board", + startedAsBoarded(waiting, "user", withDestination), + ) + assertFalse( + "another boarding stop is another trip", + startedAsBoarded(waiting, "boarded_no_destination", withDestination + mapOf("boardingStopId" to 2)), + ) + assertFalse( + "an hour on, that ride is over", + startedAsBoarded(waiting, "boarded_no_destination", withDestination, pausedMinutesAgo = 61), + ) } + @Test + fun tappingATripNotificationLaunchesWithTheFullWidthStopId() { + val rawStopId = 101111144601L + val session = RouteTripMonitorService.parseSessionPayload( + mapOf( + "provider" to "khh", + "routeKey" to 100, + "pathId" to 0, + "destinationStopId" to rawStopId, + "stops" to listOf( + mapOf("stopId" to rawStopId, "stopName" to "測試站", "lat" to 22.6, "lon" to 120.3), + ), + ), + )!! + assertEquals("the feed join still uses the folded key", foldStopId(rawStopId), session.destinationStopId) + + val tap = ReflectionHelpers.callInstanceMethod( + service, + "createOpenRoutePendingIntent", + ReflectionHelpers.ClassParameter.from(TrackingSession::class.java, session), + ) + val payload = AppLaunchConstants.extractLaunchPayload(shadowOf(tap).savedIntent)!! + + assertEquals(rawStopId, (payload["stopId"] as Number).toLong()) + } + + @Test + fun theEtaRollingOverToAPlatedLaterBusStillRaisesTheQuestion() { + // The bus at the stop has no position feed, so the ETA rolling over is + // the only sign it left. The next bus is ten minutes out and its ETA + // entry carries a plate, which used to read as "a bus is here". + var stopFeed = stopFeed(eta = 0, plate = "AAA-001") + backend = { path -> if (path.endsWith("/realtime")) stopFeed else "[]" } + val session = threeStopSession() + mapOf("routeId" to "TXG1", "boardingStopId" to 1) + service.onStartCommand(startIntent(session), 0, 1) + fetchLiveData() + assertEquals(0L, ride(service).boardingCheckId) + + stopFeed = stopFeed(eta = 600, plate = "BBB-002") + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + fetchLiveData() + + assertTrue( + "the departure must be registered so the rider can be asked", + ride(service).boardingCheckId != 0L, + ) + } + + @Test + fun onlyTheQuestionsOwnButtonsAnswerTheQuestion() { + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "boardingStopId" to 1, "destinationStopId" to 3, + ) + for (answersTheQuestion in listOf(false, true)) { + // Bus A calls at the boarding stop and leaves. + var busStop = stopsOf(trip).first() + var gpsTime = nowSeconds() + backend = { path -> + if (path.endsWith("/realtime/buses")) { + busFeed("BUS-A", busStop, pathId = 0, gpsTimeSeconds = gpsTime) + } else { + null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + refreshAndDeliverAlerts() + busStop = stopsOf(trip)[1] + gpsTime += 20 + ShadowSystemClock.advanceBy(20, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + val checkId = ride(service).boardingCheckId + assertTrue("A's departure must be pending as a question", checkId != 0L) + val notifications = service.getSystemService(NotificationManager::class.java) + + if (!answersTheQuestion) { + // Not asked yet. The rider is on another bus, one with no + // position feed, and says so on the tracking notification. + assertTrue(notifications.activeNotifications.none { it.id == 6023 }) + service.onStartCommand(buttonIntent(notificationId = 6021, title = "我已上車"), 0, 2) + + assertTrue(ride(service).hasBoarded) + assertNull("an unseen question's bus is not the rider's", ride(service).trackedVehicleId) + } else { + // The question is shown 40 s after the departure. The service + // reads the JVM wall clock, so the departure is aged instead. + ReflectionHelpers.setField( + ReflectionHelpers.getField(service, "detector"), + "pendingCheckSinceMs", + System.currentTimeMillis() - 45_000L, + ) + refreshAndDeliverAlerts() + assertFalse( + "showing the question must not turn the tracking notification's button into an answer", + buttonIntent(notificationId = 6021, title = "我已上車").hasExtra("boarding_check_id"), + ) + service.onStartCommand(buttonIntent(notificationId = 6023, title = "我已上車"), 0, 2) + + assertEquals("yes to the question about A is a ride on A", "BUS-A", ride(service).trackedVehicleId) + } + service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 3) + } + } + + @Test + fun theAnswerAfterTwoBusesLeftCloseTogetherFollowsNeither() { + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "boardingStopId" to 1, "destinationStopId" to 2, + ) + val stops = stopsOf(trip) + // Both stand at the boarding stop; A leaves, then B right behind it. + // The service reads the JVM wall clock, which does not move with the + // test's, so the reports are stamped in the minute leading up to now: + // a vehicle's clock must never run ahead of the phone's. + var busA = stops[0] + var busB = stops[0] + var gpsTime = nowSeconds() - 60 + backend = { path -> + if (path.endsWith("/realtime/buses")) { + busFeeds( + busFeed("BUS-A", busA, pathId = 0, gpsTimeSeconds = gpsTime), + busFeed("BUS-B", busB, pathId = 0, gpsTimeSeconds = gpsTime), + ) + } else { + null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + refreshAndDeliverAlerts() + busA = stops[1] + gpsTime += 20 + ShadowSystemClock.advanceBy(20, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + val asked = ride(service).boardingCheckId + assertTrue("A leaving must raise the question", asked != 0L) + busA = stops[2] + busB = stops[1] + gpsTime += 20 + ShadowSystemClock.advanceBy(20, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + assertEquals("B leaving right behind is part of the same question", asked, ride(service).boardingCheckId) + + // The question is shown 40 s after the first departure. The service + // reads the JVM wall clock, so that is aged instead of waited for. + ReflectionHelpers.setField( + ReflectionHelpers.getField(service, "detector"), + "pendingCheckSinceMs", + System.currentTimeMillis() - 45_000L, + ) + refreshAndDeliverAlerts() + service.onStartCommand(buttonIntent(notificationId = 6023, title = "我已上車"), 0, 2) + refreshAndDeliverAlerts() + + // The rider is on B. A, a stop past their destination, is not their bus. + assertTrue(ride(service).hasBoarded) + assertNull("the answer is as true of either bus", ride(service).trackedVehicleId) + assertNull(ride(service).passedDestinationByStops) + assertEquals(0L, ReflectionHelpers.getField(service, "arrivalDetectedAtMs")) + assertNull(AppRuntimeStateStore.loadPausedTripMonitor(service)) + } + + @Test + fun theBusTheStopFeedPlacesAtTheStopIsTheOneItsRolloverReports() { + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "boardingStopId" to 1, "destinationStopId" to 3, + ) + // A stands at the boarding stop, by both feeds; then it leaves, by both. + var busStop = stopsOf(trip).first() + var gpsTime = nowSeconds() - 60 + var stopFeed = stopFeed(eta = 0, plate = "BUS-A") + backend = { path -> + if (path.endsWith("/realtime/buses")) { + busFeed("BUS-A", busStop, pathId = 0, gpsTimeSeconds = gpsTime) + } else { + stopFeed + } + } + service.onStartCommand(startIntent(trip), 0, 1) + refreshAndDeliverAlerts() + busStop = stopsOf(trip)[1] + gpsTime += 20 + stopFeed = stopFeed(eta = 600, plate = "BBB-002") + ShadowSystemClock.advanceBy(25, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + assertTrue(ride(service).boardingCheckId != 0L) + + ReflectionHelpers.setField( + ReflectionHelpers.getField(service, "detector"), + "pendingCheckSinceMs", + System.currentTimeMillis() - 45_000L, + ) + refreshAndDeliverAlerts() + service.onStartCommand(buttonIntent(notificationId = 6023, title = "我已上車"), 0, 2) + + assertEquals( + "the rollover was tied to A by its plate, so it is no second bus", + "BUS-A", + ride(service).trackedVehicleId, + ) + } + + @Test + fun aFailingStopFeedDoesNotSilenceTheVehicleFeed() { + val requests = java.util.concurrent.CopyOnWriteArrayList() + backend = { path -> + requests += path + if (path.endsWith("/realtime/buses")) { + """[{"id":"AAA-001","direction":0,"lat":24.1,"lon":120.6,"time":1700000000}]""" + } else { + null + } + } + val session = threeStopSession() + mapOf("routeId" to "TXG1", "boardingStopId" to 1) + service.onStartCommand(startIntent(session), 0, 1) + fetchLiveData() + + assertEquals("the bus at the boarding stop must be seen", "AAA-001", ride(service).nextBusVehicleId) + + // And it keeps being fetched while the stop feed sits out its backoff. + val busFetches = requests.count { it.endsWith("/realtime/buses") } + val stopFetches = requests.size - busFetches + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + fetchLiveData() + + assertEquals(busFetches + 1, requests.count { it.endsWith("/realtime/buses") }) + assertEquals(stopFetches, requests.count { !it.endsWith("/realtime/buses") }) + } + + @Test + fun switchingDirectionRebuildsTheRideEvenWithTheSameBoardingStop() { + val outbound = threeStopSession() + mapOf("boardingStopId" to 2, "destinationStopId" to 3) + service.onStartCommand(startIntent(outbound), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + assertTrue(ride(service).hasBoarded) + + val inbound = reversed(outbound) + mapOf("destinationStopId" to 1) + service.onStartCommand(startIntent(inbound), 0, 3) + + assertFalse("the other direction is a different trip", ride(service).hasBoarded) + assertEquals( + "the boarding stop is found on the new direction's stops", + 1, + ride(service).boardingStopIndex, + ) + + // The same stops in a new order are a new line too, whatever the path id. + service.onStartCommand(command("MARK_BOARDED"), 0, 4) + service.onStartCommand(startIntent(inbound + mapOf("stops" to outbound.getValue("stops"))), 0, 5) + assertFalse(ride(service).hasBoarded) + } + + @Test + fun aFetchStillInFlightForTheLastTripDoesNotReachTheNextOne() { + val first = threeStopSession() + mapOf("routeId" to "TXG1", "boardingStopId" to 2) + val anotherRoute = first + mapOf("routeKey" to 2, "routeId" to "TXG2") + val theOtherDirection = reversed(first) + for (next in listOf(anotherRoute, theOtherDirection)) { + val firstStop = stopsOf(next).first() + val oldFetchStarted = CountDownLatch(1) + val releaseOldFetch = CountDownLatch(1) + val answersTheNewTrip = AtomicBoolean(false) + backend = { path -> + val wantsBuses = path.endsWith("/realtime/buses") + when { + // The new trip: its bus is a stop back, ten minutes out. + answersTheNewTrip.get() && wantsBuses -> busFeed("NEW-001", firstStop) + answersTheNewTrip.get() -> stopFeed(eta = 600, plate = "NEW-001", stopId = 2) + // The old one: a bus pulling in at the boarding stop. + wantsBuses -> busFeed("OLD-001", stopsOf(first)[1]) + else -> { + oldFetchStarted.countDown() + releaseOldFetch.await(10, TimeUnit.SECONDS) + stopFeed(eta = 0, plate = "OLD-001", stopId = 2) + } + } + } + service.onStartCommand(startIntent(first), 0, 1) + assertTrue("the first trip's fetch never started", oldFetchStarted.await(10, TimeUnit.SECONDS)) + + // The rider switches while that request is still out. Both trips + // share stops, so everything it brings back would fit the new one. + service.onStartCommand(startIntent(next), 0, 2) + releaseOldFetch.countDown() + awaitQueuedRefreshes() + answersTheNewTrip.set(true) + fetchLiveData() + + assertEquals( + "the new trip must see its own vehicles, at once, and none of the old trip's", + "NEW-001", + ride(service).nextBusVehicleId, + ) + assertEquals( + "the old trip's ETA must not count as a bus arriving, and so later as one leaving", + 0L, + ride(service).boardingCheckId, + ) + service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 3) + } + } + + @Test + fun aRebuiltTripHearsOfTheVehiclesOnItsFirstRefresh() { + // One bus at the middle stop, running the direction about to be chosen. + backend = { path -> + if (path.endsWith("/realtime/buses")) { + """[{"id":"AAA-001","direction":1,"lat":24.1045,"lon":120.6,"time":1700000000}]""" + } else { + null + } + } + val outbound = threeStopSession() + mapOf("routeId" to "TXG1", "boardingStopId" to 2) + service.onStartCommand(startIntent(outbound), 0, 1) + fetchLiveData() + assertNull("it is the other direction's bus", ride(service).nextBusVehicleId) + + // The route's vehicles were fetched a moment ago, but for a detector + // that no longer exists; the new one must not wait out that interval. + service.onStartCommand(startIntent(reversed(outbound)), 0, 2) + fetchLiveData() + + assertEquals("AAA-001", ride(service).nextBusVehicleId) + } + + @Test + fun whatARefreshDecidedForAReplacedTripIsNotActedOn() { + // On board with nowhere to get off: this trip's refresh ends by pausing + // the monitor to ask for a destination. + service.onStartCommand(startIntent(threeStopSession() + mapOf("boardingStopId" to 1)), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + fetchLiveData() + + // Before that is acted on, the rider opens another route. + val next = tripWithDestination() + mapOf("routeKey" to 7, "routeName" to "7") + service.onStartCommand(startIntent(next), 0, 3) + shadowOf(Looper.getMainLooper()).idle() + + assertNull( + "the pause meant for the old trip must not land on the new one", + AppRuntimeStateStore.loadPausedTripMonitor(service), + ) + assertEquals(7, ReflectionHelpers.getField(service, "session").routeKey) + } + + @Test + fun whatARefreshDecidedBeforeTheRiderAnsweredIsNotActedOn() { + // Taken for on board with nowhere to get off, so this refresh too ends + // by pausing the monitor to ask for a destination. + service.onStartCommand(startIntent(threeStopSession() + mapOf("boardingStopId" to 1)), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + fetchLiveData() + + // Before that is acted on, the rider says they are not on the bus. + beforeTheNextRefresh { + service.onStartCommand(command("NOT_BOARDED"), 0, 3) + shadowOf(Looper.getMainLooper()).idle() + } + + assertNull( + "a rider still waiting must not have the monitor paused as though on board", + AppRuntimeStateStore.loadPausedTripMonitor(service), + ) + assertNotNull(ReflectionHelpers.getField(service, "session")) + } + + @Test + fun whatARefreshDecidedBeforeADestinationWasChosenIsNotActedOn() { + val boarded = threeStopSession() + mapOf("boardingStopId" to 1) + service.onStartCommand(startIntent(boarded), 0, 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + fetchLiveData() + + // Before the pause for want of a destination lands, the rider picks one. + beforeTheNextRefresh { + val chosen = mapOf("destinationStopId" to 3, "destinationStopName" to "三站") + service.onStartCommand(startIntent(boarded + chosen), 0, 3) + shadowOf(Looper.getMainLooper()).idle() + } + + assertNull( + "the ride now has somewhere to get off, and must carry on to it", + AppRuntimeStateStore.loadPausedTripMonitor(service), + ) + assertEquals(3, ReflectionHelpers.getField(service, "session").destinationStopId) + } + + @Test + fun theStopFeedTheAppHandsOverWhileOpenReachesTheDetector() { + // With the app open the route screen sends its own copy of the stop feed + // every few seconds, and the monitor does not fetch one of its own. + fun update(eta: Int, plate: String): Intent { + val session = threeStopSession() + val stops = stopsOf(session).map { stop -> + stop + mapOf( + "liveData" to mapOf("eta" to eta, "etas" to listOf(mapOf("plate" to plate, "eta" to eta))), + ) + } + return startIntent(session + mapOf("boardingStopId" to 1, "stops" to stops)) + } + service.onStartCommand(update(eta = 0, plate = "AAA-001"), 0, 1) + refreshRide() + assertEquals(0L, ride(service).boardingCheckId) + + service.onStartCommand(update(eta = 600, plate = "BBB-002"), 0, 2) + refreshRide() + + assertTrue( + "the bus leaving must be noticed from the app's copy of the feed too", + ride(service).boardingCheckId != 0L, + ) + } + + @Test + fun skippingTheDestinationStillRaisesTheOvershootAlert() { + jumpPastDestination() + val arrivalAt = ReflectionHelpers.getField(service, "arrivalDetectedAtMs") + assertTrue("passing the destination must start the arrival clock", arrivalAt > 0L) + + // The service uses the JVM wall clock, not Robolectric's uptime. Age + // its recorded baseline instead of waiting for the real deadline. + val beforeDeadline = System.currentTimeMillis() - 30_000L + ReflectionHelpers.setField(service, "arrivalDetectedAtMs", beforeDeadline) + refreshAndDeliverAlerts() + assertNull(AppRuntimeStateStore.loadPausedTripMonitor(service)) + assertEquals( + "later overshot snapshots must not restart the clock", + beforeDeadline, + ReflectionHelpers.getField(service, "arrivalDetectedAtMs"), + ) + + ReflectionHelpers.setField(service, "arrivalDetectedAtMs", System.currentTimeMillis() - 45_000L) + refreshAndDeliverAlerts() + + assertEquals("overshot", AppRuntimeStateStore.loadPausedTripMonitor(service)?.reason) + val alert = service.getSystemService(NotificationManager::class.java) + .activeNotifications.single { it.id == 6022 }.notification + assertEquals("可能坐過站了", alert.extras.getString(Notification.EXTRA_TITLE)) + } + + @Test + fun skippingTheDestinationStillPausesAfterArrivalGraceWhenOffRoute() { + // A fix 150 m from the line cannot confirm an overshoot, but arrival's + // grace period must still finish rather than leaving the trip running. + jumpPastDestination(longitudeOffset = 0.0015) + assertTrue( + "passing the destination must start the arrival clock even off route", + ReflectionHelpers.getField(service, "arrivalDetectedAtMs") > 0L, + ) + ReflectionHelpers.setField(service, "arrivalDetectedAtMs", System.currentTimeMillis() - 60_000L) + refreshAndDeliverAlerts() + + assertEquals("arrived", AppRuntimeStateStore.loadPausedTripMonitor(service)?.reason) + } + + @Test + fun anOldStopFetchCannotOverwriteANewerAppUpdateForTheSameTrip() { + assertNewerAppStopFeedWins(stopFeed(eta = 600, plate = "OLD-001")) + } + + @Test + fun anOldStopFetchFailureCannotBackOffANewerAppUpdateForTheSameTrip() { + assertNewerAppStopFeedWins(null) + } + + @Test + fun aFamilyVariantCarriesAConfirmedRideWithoutPhoneFixes() { + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", + "familyRouteIds" to listOf("TXG1", "TXG2"), + "boardingStopId" to 1, + "destinationStopId" to 3, + ) + var busStop = stopsOf(trip).first() + var gpsTime = nowSeconds() + backend = { path -> + when (path) { + "/api/v1/routes/TXG1/realtime/buses" -> "[]" + "/api/v1/routes/TXG2/realtime/buses" -> + busFeed("VARIANT-001", busStop, pathId = 0, gpsTimeSeconds = gpsTime) + else -> null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + fetchLiveData() + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + refreshAndDeliverAlerts() + assertFalse(ride(service).hasUsableLocation) + assertEquals("VARIANT-001", ride(service).trackedVehicleId) + assertEquals(2, ride(service).remainingStops) + + busStop = stopsOf(trip)[1] + gpsTime += 30 + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + + assertEquals(1, ride(service).progressStopIndex) + assertEquals(1, ride(service).remainingStops) + val alert = service.getSystemService(NotificationManager::class.java) + .activeNotifications.single { it.id == 6022 }.notification + assertEquals("準備下車", alert.extras.getString(Notification.EXTRA_TITLE)) + } + + @Test + fun familyVehicleFetchesAreThrottledAndFailedRoutesBackOffIndependently() { + val requests = java.util.concurrent.CopyOnWriteArrayList() + backend = { path -> + if (path.endsWith("/realtime/buses")) { + requests += path + if (path.contains("/TXG2/")) null else "[]" + } else { + null + } + } + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", + "familyRouteIds" to listOf("TXG1", "TXG2", "TXG3", "TXG2"), + "boardingStopId" to 1, + ) + fun assertFetchCounts(primary: Int, failed: Int, healthy: Int) { + assertEquals(primary, requests.count { it.contains("/TXG1/") }) + assertEquals(failed, requests.count { it.contains("/TXG2/") }) + assertEquals(healthy, requests.count { it.contains("/TXG3/") }) + } + service.onStartCommand(startIntent(trip), 0, 1) + fetchLiveData() + assertFetchCounts(primary = 1, failed = 1, healthy = 1) + fetchLiveData() + assertFetchCounts(primary = 1, failed = 1, healthy = 1) + + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + fetchLiveData() + assertFetchCounts(primary = 2, failed = 1, healthy = 2) + + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + fetchLiveData() + assertFetchCounts(primary = 3, failed = 2, healthy = 3) + } + + @Test + fun aLateFamilyVariantIsFetchedWithoutRefetchingTheFreshPrimary() { + val requests = java.util.concurrent.CopyOnWriteArrayList() + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "boardingStopId" to 1, "destinationStopId" to 3, + ) + // The variant's bus is at the stop before the rider confirms; the app + // only hears of it once the variant is known. + val reportedAt = nowSeconds() + backend = { path -> + if (path.endsWith("/realtime/buses")) { + requests += path + if (path.contains("/TXG1/")) { + "[]" + } else { + busFeed("VARIANT-001", stopsOf(trip).first(), gpsTimeSeconds = reportedAt) + } + } else { + null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + fetchLiveData() + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + awaitQueuedRefreshes() + val originalDetector = ReflectionHelpers.getField(service, "detector") + service.onStartCommand( + startIntent(trip + mapOf("familyRouteIds" to listOf("TXG1", "TXG2"))), 0, 3, + ) + fetchLiveData() + + assertTrue(originalDetector === ReflectionHelpers.getField(service, "detector")) + assertTrue(ride(service).hasBoarded) + assertEquals("VARIANT-001", ride(service).trackedVehicleId) + assertEquals(1, requests.count { it.contains("/TXG1/") }) + assertEquals(1, requests.count { it.contains("/TXG2/") }) + } + + @Test + fun aRemovedFamilyVariantsInFlightResponseCannotReachTheConfirmedRide() { + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "familyRouteIds" to listOf("TXG2"), + "boardingStopId" to 1, "destinationStopId" to 3, + ) + val variantFetchStarted = CountDownLatch(1) + val releaseVariantFetch = CountDownLatch(1) + backend = { path -> + when (path) { + "/api/v1/routes/TXG1/realtime/buses" -> "[]" + "/api/v1/routes/TXG2/realtime/buses" -> { + variantFetchStarted.countDown() + check(releaseVariantFetch.await(10, TimeUnit.SECONDS)) { "variant request was never released" } + busFeed("REMOVED", stopsOf(trip)[2]) + } + else -> null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + try { + assertTrue(variantFetchStarted.await(10, TimeUnit.SECONDS)) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + val originalDetector = ReflectionHelpers.getField(service, "detector") + service.onStartCommand(startIntent(trip + mapOf("familyRouteIds" to emptyList())), 0, 3) + assertTrue(originalDetector === ReflectionHelpers.getField(service, "detector")) + } finally { + releaseVariantFetch.countDown() + } + awaitQueuedRefreshes() + // A second confirmation would pick any leaked position at the stop. + service.onStartCommand(command("MARK_BOARDED"), 0, 4) + awaitQueuedRefreshes() + assertNull(ride(service).trackedVehicleId) + val refreshState = ReflectionHelpers.getField>(service, "busFeedRefreshByRoute") + assertFalse("an obsolete family response must not repopulate its cache", "TXG2" in refreshState) + } + + @Test + fun aTrackedBusRaisesAnOvershootAlertWithoutPhoneFixes() { + assertBusOnlyOvershoot(stalePhoneFix = false) + } + + @Test + fun aStaleFarAwayPhoneFixDoesNotHideATrackedBusOvershoot() { + assertBusOnlyOvershoot(stalePhoneFix = true) + } + + @Test + fun manualBoardingWaitsForTheActualBusInsteadOfAnEarlierBusPastTheDestination() { + val stops = (0..5).map { index -> + mapOf( + "stopId" to index + 1, "stopName" to "stop-$index", "sequence" to index + 1, + "lat" to 24.1 + index * 0.0045, "lon" to 120.6, + ) + } + val trip = sessionPayload() + mapOf( + "routeId" to "TXG1", "boardingStopId" to 1, "destinationStopId" to 4, "stops" to stops, + ) + var actualStop: Map? = null + var gpsTime = nowSeconds() + var actualGpsTime = gpsTime + backend = { path -> + if (path.endsWith("/realtime/buses")) { + val reports = org.json.JSONArray(busFeed("EARLIER", stops[5], pathId = 0, gpsTimeSeconds = gpsTime)) + actualStop?.let { stop -> + reports.put( + org.json.JSONArray(busFeed("ACTUAL", stop, pathId = 0, gpsTimeSeconds = actualGpsTime)) + .getJSONObject(0), + ) + } + reports.toString() + } else { + null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + fetchLiveData() + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + refreshAndDeliverAlerts() + + assertTrue(ride(service).hasBoarded) + assertNull(ride(service).trackedVehicleId) + assertNull(ride(service).passedDestinationByStops) + assertEquals(0L, ReflectionHelpers.getField(service, "arrivalDetectedAtMs")) + assertNull(AppRuntimeStateStore.loadPausedTripMonitor(service)) + + // The feed catches up half a minute later, with the fix the rider's + // bus made at the stop just before they confirmed. + actualStop = stops.first() + gpsTime += 30 + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + assertEquals("ACTUAL", ride(service).trackedVehicleId) + assertEquals(0, ride(service).progressStopIndex) + + actualStop = stops[1] + gpsTime += 30 + actualGpsTime = gpsTime + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + assertEquals("ACTUAL", ride(service).trackedVehicleId) + assertEquals(2, ride(service).remainingStops) + assertNull(ride(service).passedDestinationByStops) + assertEquals(0L, ReflectionHelpers.getField(service, "arrivalDetectedAtMs")) + assertNull(AppRuntimeStateStore.loadPausedTripMonitor(service)) + val alert = service.getSystemService(NotificationManager::class.java) + .activeNotifications.single { it.id == 6022 }.notification + assertEquals("1 快到了", alert.extras.getString(Notification.EXTRA_TITLE)) + } + + @Test + fun wrongDirectionAndOffRouteVariantsCannotBeConfirmedAsTheRidersBus() { + for ((pathId, longitudeOffset) in listOf(1 to 0.0, 0 to 0.004)) { + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "familyRouteIds" to listOf("TXG2"), + "boardingStopId" to 1, "destinationStopId" to 3, + ) + val busStop = stopsOf(trip).first() + mapOf("lon" to 120.6 + longitudeOffset) + backend = { path -> + when (path) { + "/api/v1/routes/TXG1/realtime/buses" -> "[]" + "/api/v1/routes/TXG2/realtime/buses" -> busFeed("INVALID", busStop, pathId = pathId) + else -> null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + fetchLiveData() + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + awaitQueuedRefreshes() + assertTrue(ride(service).hasBoarded) + assertNull(ride(service).trackedVehicleId) + assertEquals(-1, ride(service).progressStopIndex) + service.onStartCommand(command("STOP_TRIP_MONITOR"), 0, 3) + } + } + + private fun assertBusOnlyOvershoot(stalePhoneFix: Boolean) { + ShadowSystemClock.advanceBy(10, TimeUnit.MINUTES) + val trip = threeStopSession() + mapOf( + "routeId" to "TXG1", "boardingStopId" to 1, "destinationStopId" to 2, + ) + var busStop = stopsOf(trip).first() + var gpsTime = nowSeconds() + backend = { path -> + if (path.endsWith("/realtime/buses")) { + busFeed("TRACKED-001", busStop, pathId = 0, gpsTimeSeconds = gpsTime) + } else { + null + } + } + service.onStartCommand(startIntent(trip), 0, 1) + fetchLiveData() + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + refreshAndDeliverAlerts() + assertEquals("TRACKED-001", ride(service).trackedVehicleId) + assertEquals(0L, ReflectionHelpers.getField(service, "arrivalDetectedAtMs")) + if (stalePhoneFix) { + feedFix(stopIndex = 0, ageMinutes = 5, longitudeOffset = 0.01) + } + + busStop = stopsOf(trip)[2] + gpsTime += 30 + ShadowSystemClock.advanceBy(30, TimeUnit.SECONDS) + refreshAndDeliverAlerts() + assertFalse(ride(service).hasUsableLocation) + assertEquals(1, ride(service).passedDestinationByStops) + assertTrue(ReflectionHelpers.getField(service, "arrivalDetectedAtMs") > 0L) + + ReflectionHelpers.setField(service, "arrivalDetectedAtMs", System.currentTimeMillis() - 60_000L) + refreshAndDeliverAlerts() + assertEquals("overshot", AppRuntimeStateStore.loadPausedTripMonitor(service)?.reason) + val alert = service.getSystemService(NotificationManager::class.java) + .activeNotifications.single { it.id == 6022 }.notification + assertEquals("可能坐過站了", alert.extras.getString(Notification.EXTRA_TITLE)) + } + + private fun assertNewerAppStopFeedWins(oldResponse: String?) { + val initial = threeStopSession() + mapOf("routeId" to "TXG1", "boardingStopId" to 1) + val oldFetchStarted = CountDownLatch(1) + val releaseOldFetch = CountDownLatch(1) + backend = { path -> + if (path.endsWith("/realtime/buses")) { + busFeed("CURRENT-001", stopsOf(initial).first()) + } else { + oldFetchStarted.countDown() + check(releaseOldFetch.await(10, TimeUnit.SECONDS)) { "old stop request was never released" } + oldResponse + } + } + service.onStartCommand(startIntent(initial), 0, 1) + try { + assertTrue("the old stop request never started", oldFetchStarted.await(10, TimeUnit.SECONDS)) + val originalDetector = ReflectionHelpers.getField(service, "detector") + val currentStops = stopsOf(initial).map { stop -> + stop + mapOf("liveData" to mapOf("eta" to 0)) + } + service.onStartCommand(startIntent(initial + mapOf("stops" to currentStops)), 0, 2) + assertTrue( + "an ordinary update must keep the detector", + originalDetector === ReflectionHelpers.getField(service, "detector"), + ) + } finally { + releaseOldFetch.countDown() + } + awaitQueuedRefreshes() + refreshRide() + + val stops = ReflectionHelpers.getField>(service, "cachedLiveStops") + assertEquals("the app's arrival is newer than the pending response", 0, stops.getValue(1).seconds) + assertEquals( + "an obsolete failure must not back off the current stop feed", + 0L, + ReflectionHelpers.getField(service, "liveStopBackoffUntilMs"), + ) + assertEquals( + "an old ETA must not invent a departure after the app reported an arrival", + 0L, + ride(service).boardingCheckId, + ) + assertEquals( + "discarding the stop response must not discard valid vehicle positions", + "CURRENT-001", + ride(service).nextBusVehicleId, + ) + } + + private fun jumpPastDestination(longitudeOffset: Double = 0.0) { + ShadowSystemClock.advanceBy(10, TimeUnit.MINUTES) + val trip = threeStopSession() + mapOf("boardingStopId" to 1, "destinationStopId" to 2) + service.onStartCommand(startIntent(trip), 0, 1) + awaitQueuedRefreshes() + feedFix(stopIndex = 0, ageMinutes = 1) + service.onStartCommand(command("MARK_BOARDED"), 0, 2) + refreshAndDeliverAlerts() + assertTrue(ride(service).hasBoarded) + assertEquals(1, ride(service).remainingStops) + assertEquals(0L, ReflectionHelpers.getField(service, "arrivalDetectedAtMs")) + + // No fix at the destination: progress goes straight from before it to + // the following stop after a gap in location updates. + feedFix(stopIndex = 2, ageMinutes = 0, longitudeOffset = longitudeOffset) + refreshAndDeliverAlerts() + assertEquals( + "the fix must jump past the destination: ${ride(service)}; " + + "pause=${AppRuntimeStateStore.loadPausedTripMonitor(service)}", + 1, + ride(service).passedDestinationByStops, + ) + assertNull(AppRuntimeStateStore.loadPausedTripMonitor(service)) + } + + private fun refreshAndDeliverAlerts() { + // Run exactly this refresh: advancing Android uptime can otherwise make + // an overdue poll race its main-thread alert with another IO snapshot. + awaitQueuedRefreshes() + ReflectionHelpers.getField(service, "mainHandler").removeCallbacksAndMessages(null) + fetchLiveData() + shadowOf(Looper.getMainLooper()).idle() + } + + /** The other direction of [session]: the same stops, called at in the opposite order. */ + private fun reversed(session: Map): Map = session + mapOf( + "pathId" to 1, + "stops" to stopsOf(session).reversed() + .mapIndexed { index, stop -> stop + mapOf("sequence" to index + 1) }, + ) + + @Suppress("UNCHECKED_CAST") + private fun stopsOf(session: Map) = session.getValue("stops") as List> + + private fun stopFeed(eta: Int, plate: String, stopId: Int = 1) = + """{"paths":[{"stops":[{"stopid":"$stopId","eta":$eta,"etas":[{"plate":"$plate","eta":$eta}]}]}]}""" + + /** One vehicle at [stop]; direction is unknown unless supplied. */ + /** What tapping the button called [title] on a posted notification sends the service. */ + private fun buttonIntent(notificationId: Int, title: String): Intent { + val posted = service.getSystemService(NotificationManager::class.java).activeNotifications + val buttons = posted.firstOrNull { it.id == notificationId }?.notification?.actions.orEmpty() + val button = buttons.firstOrNull { it.title.toString() == title } ?: throw AssertionError( + "no \"$title\" on notification $notificationId; posted: " + + posted.joinToString { n -> "${n.id}${n.notification.actions.orEmpty().map { it.title }}" }, + ) + return shadowOf(button.actionIntent).savedIntent + } + + /** Several vehicles in one response. */ + private fun busFeeds(vararg feeds: String): String = + feeds.joinToString(prefix = "[", postfix = "]", separator = ",") { it.removeSurrounding("[", "]") } + + /** Vehicles stamp their reports with the same clock the phone runs on. */ + private fun nowSeconds() = System.currentTimeMillis() / 1000 + + private fun busFeed( + vehicleId: String, + stop: Map, + pathId: Int? = null, + gpsTimeSeconds: Long = nowSeconds(), + ) = """[{"id":"$vehicleId","direction":$pathId,"lat":${stop["lat"]},"lon":${stop["lon"]},"time":$gpsTimeSeconds}]""" + + /** One refresh as the service runs it, after anything it already queued. */ + private fun fetchLiveData() { + ioExecutor().submit { + ReflectionHelpers.callInstanceMethod( + service, + "refreshTrip", + ReflectionHelpers.ClassParameter.from(Boolean::class.javaPrimitiveType, false), + ) + }.get(10, TimeUnit.SECONDS) + } + + private fun awaitQueuedRefreshes() { + ioExecutor().submit {}.get(10, TimeUnit.SECONDS) + } + + /** + * Runs [block] with the refresh it sets off held back, so that what the + * last refresh left for the main thread is all that can happen in it. + */ + private fun beforeTheNextRefresh(block: () -> Unit) { + val held = CountDownLatch(1) + ioExecutor().execute { held.await(10, TimeUnit.SECONDS) } + try { + block() + } finally { + held.countDown() + } + } + + private fun ioExecutor(): ExecutorService = ReflectionHelpers.getField(service, "ioExecutor") + + /** + * A ride confirmed with nowhere to get off is paused within moments to ask + * for a destination, so tests about a ride that carries on give it one. + */ + private fun tripWithDestination(): Map = sessionPayload() + mapOf( + "destinationStopId" to 1, + "destinationStopName" to "測試站", + ) + + private fun threeStopSession(): Map = sessionPayload() + mapOf( + "stops" to THREE_STOPS.mapIndexed { index, (lat, lon) -> + mapOf( + "stopId" to index + 1, + "stopName" to "${"一二三"[index]}站", + "sequence" to index + 1, + "lat" to lat, + "lon" to lon, + ) + }, + ) + + /** Delivers a fix at a stop, as the location callback does, and re-reads the ride. */ + private fun feedFix(stopIndex: Int, ageMinutes: Long, longitudeOffset: Double = 0.0) { + val (lat, lon) = THREE_STOPS[stopIndex] + val fix = Location("fused").apply { + latitude = lat + longitude = lon + longitudeOffset + accuracy = 10f + elapsedRealtimeNanos = + SystemClock.elapsedRealtimeNanos() - TimeUnit.MINUTES.toNanos(ageMinutes) + time = System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(ageMinutes) + } + ReflectionHelpers.setField(service, "latestLocation", fix) + ReflectionHelpers.callInstanceMethod( + service, + "onUserLocation", + ReflectionHelpers.ClassParameter.from(Location::class.java, fix), + ) + refreshRide() + } + + private fun refreshRide() { + ReflectionHelpers.callInstanceMethod( + service, + "refreshRideSnapshot", + ReflectionHelpers.ClassParameter.from(Long::class.javaPrimitiveType, System.currentTimeMillis()), + ) + } + + /** Lets the service's own refresh loop run until it pauses the trip. */ + private fun awaitPause(): TripMonitorPauseState { + repeat(100) { + AppRuntimeStateStore.loadPausedTripMonitor(service)?.let { return it } + shadowOf(Looper.getMainLooper()).idleFor(500, TimeUnit.MILLISECONDS) + Thread.sleep(50) + } + throw AssertionError("the monitor never paused to ask for a destination") + } + + private fun ride(service: RouteTripMonitorService): RideSnapshot = + ReflectionHelpers.getField(service, "latestRide") + private fun assertStoppedWithoutNotification() { assertTrue(shadow.isStoppedBySelf) assertTrue(shadow.isForegroundStopped) @@ -250,6 +1492,9 @@ class RouteTripMonitorServiceTest { ) } +/** Three stops about 500 m apart, running north. */ +private val THREE_STOPS = listOf(24.1000 to 120.6, 24.1045 to 120.6, 24.1090 to 120.6) + @Implements(Service::class) class RecordingTripServiceShadow : ShadowService() { @RealObject diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/StopIdCodecTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/StopIdCodecTest.kt new file mode 100644 index 0000000..8099664 --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/StopIdCodecTest.kt @@ -0,0 +1,85 @@ +package tw.avianjay.taiwanbus.flutter + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Stop ids are 64-bit in the feeds but every stop map in the app is keyed by an + * `Int`. Both sides of that key fold through [foldStopId]; if they ever + * disagree, the live ETA of a 高雄 stop can never be looked up. + */ +class StopIdCodecTest { + @Test + fun leavesIdsThatAlreadyFitAlone() { + // 台中 and 雙北 ids are small; folding them must be a no-op. + assertEquals(13567, foldStopId("13567")) + assertEquals(194687, foldStopId(194687)) + assertEquals(1001001781, foldStopId("1001001781")) + } + + @Test + fun foldsIdsTooLargeForAnInt() { + // 高雄's real ids are 11-12 digits. + assertEquals(1315880413, foldStopId("100100128221")) + assertEquals(1315910904, foldStopId("100100158712")) + } + + @Test + fun neverReturnsSomethingThePositiveFiltersWouldDrop() { + // 172 高雄 routes have ids whose low 32 bits are negative; a plain + // toInt() turns those into ids the `> 0` checks throw away. + val ids = listOf("101111144601", "100100128221", "100100158712", "99999999999") + ids.forEach { raw -> + assertTrue("$raw folded to a non-positive id", foldStopId(raw) > 0) + } + } + + @Test + fun foldingIsIdempotent() { + // A value that has already been folded can be folded again: the session + // is rebuilt from a payload that already carries folded ids. + val once = foldStopId("100100128221") + assertEquals(once, foldStopId(once)) + } + + @Test + fun bothSidesOfTheJoinAgree() { + // The exact bug: the session side truncated while the feed side hashed, + // and the two values could never be equal. + val fromFeed = foldStopId("100100128221") + val fromSession = foldStopId("100100128221") + assertEquals(fromFeed, fromSession) + } + + @Test + fun hashesIdsThatAreNotNumeric() { + // Some providers use ids like "101-11009". + val first = foldStopId("101-11009") + val second = foldStopId("101-11175") + assertTrue("non-numeric ids must still fold to positive keys", first > 0 && second > 0) + assertEquals("the same id must always fold the same way", first, foldStopId("101-11009")) + assertTrue("different ids must not collide", first != second) + } + + @Test + fun missingIdsFoldToZeroSoTheFiltersStillWork() { + assertEquals(0, foldStopId(null)) + assertEquals(0, foldStopId("")) + assertEquals(0, foldStopId(" ")) + // org.json hands back JSONObject.NULL for absent fields. + assertEquals(0, foldStopId(org.json.JSONObject.NULL)) + } + + @Test + fun realIdsDoNotCollide() { + // Verified against the backend's 114,903 distinct ids: masking collides + // on none of them. + val ids = listOf( + "13567", "12251", "194687", "1001001781", "100100128221", + "100100158712", "101111144601", "101111144651", "101111144681", + ) + val folded = ids.map(::foldStopId) + assertEquals("folding must be injective for real ids", ids.size, folded.toSet().size) + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorCoMovementTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorCoMovementTest.kt new file mode 100644 index 0000000..1afe8c6 --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorCoMovementTest.kt @@ -0,0 +1,201 @@ +package tw.avianjay.taiwanbus.flutter + +import kotlin.math.PI +import kotlin.math.cos +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Which vehicles may confirm a ride. + * + * The position feed lists every vehicle of the route: this direction's, the + * other direction's, and ones parked or deadheading beside it. All of them + * project to some position along the trip's stops, so a vehicle that merely + * moves in step with the phone used to be taken for the rider's bus whatever + * direction it reported and however far off the route it was. + * + * The route is a straight northbound line of five stops 500 m apart. Every ride + * here covers 300 m, short of what the rider's own progress would need to count + * by itself, so the vehicle is the only thing that can confirm it. + */ +class TripRideDetectorCoMovementTest { + @Test + fun ridingABusOfThisDirectionIsDetected() { + val ride = rideAlongside { seconds -> report(seconds, along = 5.0 * seconds) } + + assertTrue("the scenario has to be one a real bus would confirm", ride.hasBoarded) + assertEquals(BoardingEvidence.BUS_CO_MOVEMENT, ride.evidence) + assertEquals(BUS, ride.trackedVehicleId) + } + + @Test + fun aBusOfTheOtherDirectionIsNotTheRidersBus() { + val ride = rideAlongside { seconds -> report(seconds, along = 5.0 * seconds, pathId = 1) } + + assertFalse("a bus going the other way cannot be the one the rider is on", ride.hasBoarded) + assertEquals(BoardingEvidence.NONE, ride.evidence) + assertNull(ride.trackedVehicleId) + } + + @Test + fun aVehicleOffTheRouteIsNotTheRidersBus() { + // 350 m to the side: deadheading down a parallel street. + val ride = rideAlongside { seconds -> + report(seconds, along = 5.0 * seconds, offsetMeters = 350.0) + } + + assertFalse("a vehicle that is not on the route cannot be the rider's bus", ride.hasBoarded) + assertNull(ride.trackedVehicleId) + } + + @Test + fun reportsEitherSideOfAStretchOnTheOtherDirectionDoNotAddUp() { + // On this direction, then one report on the other, then back: too few + // reports on either side to confirm anything, and the two sides are not + // one journey. + val ride = rideAlongside { seconds -> + report(seconds, along = 5.0 * seconds, pathId = if (seconds == 40L) 1 else 0) + } + + assertFalse(ride.hasBoarded) + } + + @Test + fun aVehicleParkedBesideTheRouteIsNotPickedAsTheBusBeingRidden() { + val detector = newDetector() + // The parked vehicle is level with the rider; the bus they are on was + // last reported 400 m back, as the feed's delay makes usual. + detector.onBusReports( + listOf( + report(0, along = 1_000.0, offsetMeters = 350.0, vehicleId = "PARKED"), + report(0, along = 600.0), + ), + ) + detector.onUserLocation(fix(0, along = 1_000.0)) + detector.confirmBoardedByUser(at(0)) + + assertEquals(BUS, detector.snapshot(at(0)).trackedVehicleId) + } + + @Test + fun anInvalidReportCannotDriveAnAlreadyConfirmedRide() { + for ((pathId, offsetMeters) in listOf(1 to 0.0, 0 to 350.0)) { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.confirmBoardedByUser(at(0)) + assertEquals(BUS, detector.snapshot(at(0)).trackedVehicleId) + + detector.onBusReports(listOf(report(30, along = 1_500.0, pathId = pathId, offsetMeters = offsetMeters))) + val ride = detector.snapshot(at(30)) + assertTrue(ride.hasBoarded) + assertEquals("keep the ride's identity without trusting the invalid position", BUS, ride.trackedVehicleId) + assertEquals(-1, ride.progressStopIndex) + assertFalse(ride.hasRouteValidProgress) + + detector.onBusReports(listOf(report(60, along = 1_600.0))) + val recovered = detector.snapshot(at(60)) + assertEquals(BUS, recovered.trackedVehicleId) + assertEquals(3, recovered.progressStopIndex) + assertTrue(recovered.hasRouteValidProgress) + } + } + + @Test + fun aVehicleFeedOutageDoesNotLoseTheRideOrSwitchToAnotherBus() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.confirmBoardedByUser(at(0)) + detector.onBusReports(listOf(report(200, along = 50.0, vehicleId = "OTHER"))) + + val outage = detector.snapshot(at(200)) + assertEquals(BUS, outage.trackedVehicleId) + assertEquals(-1, outage.progressStopIndex) + assertFalse(outage.hasRouteValidProgress) + + detector.onBusReports(listOf(report(201, along = 1_600.0))) + val recovered = detector.snapshot(at(201)) + assertEquals(BUS, recovered.trackedVehicleId) + assertEquals(3, recovered.progressStopIndex) + assertTrue(recovered.hasRouteValidProgress) + } + + @Test + fun aStalePhoneFixDoesNotPreventConfirmationFromPickingTheBus() { + val detector = newDetector() + detector.onUserLocation(fix(-300, along = 0.0)) + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.confirmBoardedByUser(at(0)) + + val ride = detector.snapshot(at(0)) + assertFalse(ride.hasUsableLocation) + assertEquals(BUS, ride.trackedVehicleId) + } + + @Test + fun anOffRouteReportCannotAnnounceADeparture() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + assertEquals(0L, detector.snapshot(at(0)).boardingCheckId) + detector.onBusReports(listOf(report(30, along = 600.0, offsetMeters = 350.0))) + + assertEquals( + "leaving the route is not evidence of departing the boarding stop", + 0L, + detector.snapshot(at(30)).boardingCheckId, + ) + } + + /** The rider travels 300 m up the route in a minute, beside whatever [vehicle] reports. */ + private fun rideAlongside(vehicle: (Long) -> RideBusReport): RideSnapshot { + val detector = newDetector() + var snapshot = RideSnapshot() + for (seconds in 0L..60L step 20L) { + detector.onBusReports(listOf(vehicle(seconds))) + detector.onUserLocation(fix(seconds, along = 5.0 * seconds)) + snapshot = detector.snapshot(at(seconds)) + } + return snapshot + } + + private fun newDetector() = TripRideDetector( + stops = (0..4).map { index -> + val (lat, lon) = point(index * 500.0, 0.0) + TrackingStop(stopId = 900 + index, stopName = "stop-$index", sequence = index, lat = lat, lon = lon) + }, + pathId = 0, + boardingStopId = 900, + destinationStopId = 904, + ) + + private fun fix(seconds: Long, along: Double): RideGeoPoint { + val (lat, lon) = point(along, 0.0) + return RideGeoPoint(lat, lon, at(seconds), accuracyMeters = 12f, speedMetersPerSecond = 5f) + } + + private fun report( + seconds: Long, + along: Double, + pathId: Int = 0, + offsetMeters: Double = 0.0, + vehicleId: String = BUS, + ): RideBusReport { + val (lat, lon) = point(along, offsetMeters) + return RideBusReport(vehicleId, pathId, lat, lon, gpsTimeMs = at(seconds), receivedAtMs = at(seconds)) + } + + private fun at(seconds: Long): Long = 1_700_000_000_000L + seconds * 1_000L + + /** [offsetMeters] is measured east of the route, which runs due north. */ + private fun point(alongMeters: Double, offsetMeters: Double): Pair { + val metersPerDegreeLat = 6_371_000.0 * PI / 180.0 + return 24.0 + alongMeters / metersPerDegreeLat to + 120.5 + offsetMeters / (metersPerDegreeLat * cos(24.0 * PI / 180.0)) + } + + private companion object { + const val BUS = "AAA-001" + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorDeclineTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorDeclineTest.kt new file mode 100644 index 0000000..b941b76 --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorDeclineTest.kt @@ -0,0 +1,404 @@ +package tw.avianjay.taiwanbus.flutter + +import kotlin.math.PI +import kotlin.math.cos +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * "還沒上車" is an answer about one bus, not about the trip. + * + * It used to switch automatic detection off until the trip was rebuilt, so the + * bus the rider was actually waiting for — usually the next one along, minutes + * behind the one they turned down — was never detected, no boarding alert + * fired, and the destination reminders never armed. + * + * The route is a straight northbound line of five stops 500 m apart, and the + * rider waits at the first one, wandering off it: a phone that never moves is + * read as "still waiting" and is deliberately never asked anything. + */ +class TripRideDetectorDeclineTest { + @Test + fun decliningOneBusStillDetectsTheNext() { + val detector = newDetector() + var declinedAtMs = 0L + var promptsAfterTheAnswer = 0 + var boardedAtMs = 0L + var boardedVehicleId: String? = null + + detector.feed(firstWaitThenRide(), listOf(declinedBusRun(), nextBusRun()), 0, 560) { nowMs, snapshot -> + if (declinedAtMs == 0L) { + if (snapshot.boardingPromptDue) { + // The question the rider answers with "還沒上車", asked about + // the first bus to leave the stop. + detector.declineBoarding(nowMs) + declinedAtMs = nowMs + } + } else if (boardedAtMs == 0L && snapshot.boardingPromptDue) { + promptsAfterTheAnswer++ + } + if (boardedAtMs == 0L && snapshot.hasBoarded) { + boardedAtMs = nowMs + boardedVehicleId = snapshot.trackedVehicleId + } + } + + assertTrue("expected the rider to be asked about the first bus", declinedAtMs > 0L) + assertEquals( + "the bus that was turned down must not be asked about again", + 0, + promptsAfterTheAnswer, + ) + assertTrue("boarding the next bus must still be detected", boardedAtMs > 0L) + assertEquals("and the ride must latch onto the bus actually ridden", NEXT_BUS, boardedVehicleId) + } + + @Test + fun theBoardingQuestionComesBackForTheNextBus() { + val detector = newDetector() + var declinedAtMs = 0L + var askedAboutTheNextBus = false + var boarded = false + + detector.feed(firstWaitThenWatch(), listOf(declinedBusRun(), nextBusRun()), 0, 520) { nowMs, snapshot -> + if (declinedAtMs == 0L) { + if (snapshot.boardingPromptDue) { + detector.declineBoarding(nowMs) + declinedAtMs = nowMs + } + } else if (snapshot.boardingPromptDue) { + askedAboutTheNextBus = true + } + if (snapshot.hasBoarded) { + boarded = true + } + } + + assertTrue("expected the rider to be asked about the first bus", declinedAtMs > 0L) + assertFalse("the rider never got on", boarded) + assertTrue( + "turning down one bus must not silence the question for the next one", + askedAboutTheNextBus, + ) + } + + @Test + fun aTurnedDownVehicleCanBeDetectedAgainOnceItsRejectionLapses() { + val detector = newDetector() + var declinedAtMs = 0L + var boardedAtMs = 0L + var boardedVehicleId: String? = null + + // The same vehicle on its next run, which begins while it is still + // turned down. Only the rejection lapsing lets the ride be detected, so + // the feed stops the moment it should be — a longer feed would let the + // rider's own progress detect the ride instead and hide the bug. + detector.feed(waitingForItsNextRun(), listOf(declinedBusRun(), returnedRun()), 0, 1440) { nowMs, snapshot -> + if (declinedAtMs == 0L) { + if (snapshot.boardingPromptDue) { + detector.declineBoarding(nowMs) + declinedAtMs = nowMs + } + } + if (boardedAtMs == 0L && snapshot.hasBoarded) { + boardedAtMs = nowMs + boardedVehicleId = snapshot.trackedVehicleId + } + } + + assertTrue("expected the rider to be asked about the first bus", declinedAtMs > 0L) + assertTrue( + "the same vehicle must be detectable once its rejection has lapsed", + boardedAtMs > 0L, + ) + assertEquals(DECLINED_BUS, boardedVehicleId) + assertTrue( + "detected ${(boardedAtMs - declinedAtMs) / 1000}s after the answer, which is inside " + + "the 1200s rejection — this run only proves the lapse if the ride is detected " + + "after it", + boardedAtMs - declinedAtMs > 1_200_000L, + ) + } + + @Test + fun confirmingByHandOutranksATurnedDownVehicle() { + val detector = newDetector() + var declinedAtMs = 0L + var confirmed = false + var boarded = false + + detector.feed(firstWaitThenWatch(), listOf(declinedBusRun(), nextBusRun()), 0, 400) { nowMs, snapshot -> + if (declinedAtMs == 0L) { + if (snapshot.boardingPromptDue) { + detector.declineBoarding(nowMs) + declinedAtMs = nowMs + } + } else if (!confirmed && nowMs >= at(300)) { + // The rider stops waiting on detection and taps the manual + // action, exactly as the notification does. + detector.confirmBoardedByUser(nowMs) + confirmed = true + } + if (snapshot.hasBoarded) { + boarded = true + } + } + + assertTrue("expected the rider to be asked about the first bus", declinedAtMs > 0L) + assertTrue(confirmed) + assertTrue("the manual action has to keep working after a decline", boarded) + } + + @Test + fun theSameDepartureEchoedByTheEtaFeedIsNotAskedAboutTwice() { + // No location at all, so asking is the only tool and nothing else can + // quiet the question. + val detector = newDetector() + detector.leaveTheStop(DECLINED_BUS, fromSeconds = 0) + detector.onBoardingStopEta(etaSeconds = 0, arriving = true, nowMs = at(20)) + assertTrue(detector.snapshot(at(80)).boardingPromptDue) + detector.declineBoarding(at(80)) + + // The ETA rolling over to the next bus is the same departure, late. + detector.onBoardingStopEta(etaSeconds = 600, arriving = false, nowMs = at(100)) + + assertFalse(detector.snapshot(at(140)).boardingPromptDue) + } + + @Test + fun aLateAnswerDoesNotSwallowTheNextDeparture() { + val detector = newDetector() + detector.leaveTheStop(DECLINED_BUS, fromSeconds = 0) + // The rider gets round to the question four minutes later. + assertTrue(detector.snapshot(at(280)).boardingPromptDue) + detector.declineBoarding(at(280)) + + // Moments after the answer the next bus pulls in and leaves; it has no + // position feed, so the ETA is all that reports it. + detector.onBoardingStopEta(etaSeconds = 0, arriving = true, nowMs = at(290)) + detector.onBoardingStopEta(etaSeconds = 600, arriving = false, nowMs = at(310)) + + assertTrue( + "the next bus must be asked about however late the last answer came", + detector.snapshot(at(350)).boardingPromptDue, + ) + } + + /** A bus pulls in at the boarding stop and drives off, 20 s a report. */ + private fun TripRideDetector.leaveTheStop(vehicleId: String, fromSeconds: Long) { + val run = busRun( + vehicleId, + fromSeconds to 10.0, + fromSeconds + 20 to 30.0, + fromSeconds + 40 to 150.0, + ) + for (seconds in fromSeconds..fromSeconds + 40 step 20) { + onBusReports(run.reportsAt(at(seconds))) + snapshot(at(seconds)) + } + } + + private fun newDetector() = TripRideDetector( + stops = routeStops(), + pathId = 0, + boardingStopId = BOARDING_STOP_ID, + destinationStopId = BOARDING_STOP_ID + 4, + ) +} + +private const val BOARDING_STOP_ID = 900 + +/** An arbitrary wall-clock instant, aligned to the 20 s poll grid below. */ +private const val BASE_MS = 1_700_000_000_000L + +private const val DECLINED_BUS = "AAA-001" +private const val NEXT_BUS = "BBB-002" + +private fun at(seconds: Long): Long = BASE_MS + seconds * 1_000L + +private val METERS_PER_DEGREE_LAT = 6_371_000.0 * PI / 180.0 +private val METERS_PER_DEGREE_LON = METERS_PER_DEGREE_LAT * cos(24.0 * PI / 180.0) + +/** [offsetMeters] is measured east of the route, which runs due north. */ +private fun point(alongMeters: Double, offsetMeters: Double): Pair = + 24.0 + alongMeters / METERS_PER_DEGREE_LAT to 120.5 + offsetMeters / METERS_PER_DEGREE_LON + +private fun routeStops(): List = (0..4).map { index -> + val (lat, lon) = point(index * 500.0, 0.0) + TrackingStop( + stopId = BOARDING_STOP_ID + index, + stopName = "stop-$index", + sequence = index, + lat = lat, + lon = lon, + ) +} + +/** The rider's path as waypoints, linearly interpolated by the frame clock. */ +private class UserPath(private val waypoints: List) { + data class Waypoint( + val tMs: Long, + val alongMeters: Double, + val offsetMeters: Double, + val speedMetersPerSecond: Float, + ) + + fun fix(tMs: Long): RideGeoPoint { + val index = waypoints.indexOfLast { it.tMs <= tMs }.coerceAtLeast(0) + val from = waypoints[index] + val to = waypoints[(index + 1).coerceAtMost(waypoints.lastIndex)] + val span = (to.tMs - from.tMs).toDouble() + val fraction = if (span <= 0.0) 0.0 else ((tMs - from.tMs) / span).coerceIn(0.0, 1.0) + val (lat, lon) = point( + from.alongMeters + fraction * (to.alongMeters - from.alongMeters), + from.offsetMeters + fraction * (to.offsetMeters - from.offsetMeters), + ) + return RideGeoPoint( + latitude = lat, + longitude = lon, + timestampMs = tMs, + accuracyMeters = 12f, + speedMetersPerSecond = from.speedMetersPerSecond, + ) + } +} + +private fun path(vararg waypoints: UserPath.Waypoint) = UserPath(waypoints.toList()) + +private fun step(seconds: Long, along: Double, offset: Double, speed: Float) = + UserPath.Waypoint(at(seconds), along, offset, speed) + +/** One bus's positions, keyed by the poll they were first seen in. */ +private class BusRun(val vehicleId: String, positions: List>) { + private val alongByTime = positions.toMap() + + fun reportsAt(tMs: Long): List { + val along = alongByTime[tMs] ?: return emptyList() + val (lat, lon) = point(along, 0.0) + return listOf( + RideBusReport( + vehicleId = vehicleId, + pathId = 0, + latitude = lat, + longitude = lon, + gpsTimeMs = tMs, + receivedAtMs = tMs, + ), + ) + } +} + +private fun busRun(vehicleId: String, vararg positions: Pair) = + BusRun(vehicleId, positions.map { at(it.first) to it.second }) + +/** + * Pulls in at the boarding stop, is turned down, and drives away. It keeps + * reporting while it leaves, which is what the rejection is scoped to. + */ +private fun declinedBusRun() = busRun( + DECLINED_BUS, + 40L to 10.0, + 60L to 30.0, + 80L to 100.0, + 100L to 150.0, + 120L to 250.0, + 140L to 350.0, +) + +/** The bus the rider actually takes, five minutes behind the one they refused. */ +private fun nextBusRun() = busRun( + NEXT_BUS, + 340L to 10.0, + 360L to 25.0, + 380L to 35.0, + 400L to 45.0, + 420L to 130.0, + 440L to 230.0, + 460L to 330.0, + 480L to 430.0, + 500L to 530.0, + 520L to 630.0, + 540L to 730.0, + 560L to 830.0, +) + +/** + * The same vehicle on its next run, which starts 19 minutes after it was + * refused — before the rejection lapses — and leaves the stop after it has. + */ +private fun returnedRun() = busRun( + DECLINED_BUS, + 1300L to 10.0, + 1320L to 15.0, + 1340L to 20.0, + 1360L to 25.0, + 1380L to 30.0, + 1400L to 130.0, + 1420L to 230.0, + 1440L to 330.0, +) + +/** + * Wanders 250 m off the stop — enough that the app cannot call it "still + * waiting" — comes back, and rides the next bus at 5 m/s. + */ +private fun firstWaitThenRide() = path( + step(0, along = 0.0, offset = 0.0, speed = 0.3f), + step(80, along = 0.0, offset = 0.0, speed = 2.5f), + step(180, along = 0.0, offset = 250.0, speed = 2.5f), + step(200, along = 0.0, offset = 240.0, speed = 1.15f), + step(400, along = 0.0, offset = 0.0, speed = 1.15f), + step(420, along = 100.0, offset = 0.0, speed = 5f), + step(560, along = 800.0, offset = 0.0, speed = 5f), +) + +/** The same wait, but the rider wanders away instead of boarding. */ +private fun firstWaitThenWatch() = path( + step(0, along = 0.0, offset = 0.0, speed = 0.3f), + step(80, along = 0.0, offset = 0.0, speed = 2.5f), + step(180, along = 0.0, offset = 250.0, speed = 2.5f), + step(200, along = 0.0, offset = 240.0, speed = 1.15f), + step(400, along = 0.0, offset = 0.0, speed = 1.15f), + step(420, along = 0.0, offset = 0.0, speed = 2.5f), + step(520, along = 0.0, offset = 250.0, speed = 2.5f), +) + +/** + * Waits at the stop for the same vehicle's next run and boards it as it leaves. + * The ride is short on purpose — under 500 m of travel, so that co-movement is + * the only layer that can detect it and the vehicle's rejection is the one + * thing standing in the way. + */ +private fun waitingForItsNextRun() = path( + step(0, along = 0.0, offset = 0.0, speed = 0.3f), + step(80, along = 0.0, offset = 0.0, speed = 2.5f), + step(180, along = 0.0, offset = 250.0, speed = 2.5f), + step(200, along = 0.0, offset = 240.0, speed = 1.15f), + step(400, along = 0.0, offset = 0.0, speed = 1.15f), + step(1380, along = 0.0, offset = 0.0, speed = 0.3f), + step(1400, along = 100.0, offset = 0.0, speed = 5f), + step(1440, along = 300.0, offset = 0.0, speed = 5f), +) + +/** + * Feeds the polls in time order, as the service does: the frame's bus reports + * first, then the rider's fix, then the snapshot the app would take. + */ +private fun TripRideDetector.feed( + userPath: UserPath, + buses: List, + fromSeconds: Long, + toSeconds: Long, + onFrame: (Long, RideSnapshot) -> Unit, +) { + var seconds = fromSeconds + while (seconds <= toSeconds) { + val nowMs = at(seconds) + buses.forEach { onBusReports(it.reportsAt(nowMs)) } + onUserLocation(userPath.fix(nowMs)) + onFrame(nowMs, snapshot(nowMs)) + seconds += 20 + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorManualBoardingTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorManualBoardingTest.kt new file mode 100644 index 0000000..5dbbc2e --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorManualBoardingTest.kt @@ -0,0 +1,717 @@ +package tw.avianjay.taiwanbus.flutter + +import kotlin.math.PI +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +/** Confirming a ride says nothing about which of the route's vehicles it is. */ +class TripRideDetectorManualBoardingTest { + @Test + fun manualConfirmationDoesNotChooseAnEarlierBusSeveralStopsAhead() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 2_500.0, id = "AHEAD-ONLY"))) + detector.confirmBoardedByUser(at(0)) + + assertUnidentified(detector.snapshot(at(0))) + } + + @Test + fun anUnknownRideWaitsForItsActualBoardingBus() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 2_500.0, id = "EARLIER"))) + detector.confirmBoardedByUser(at(0)) + assertUnidentified(detector.snapshot(at(0))) + + // The feed runs behind: the fix the rider's bus made at the stop, just + // before they confirmed, only reaches the app now. + detector.onBusReports(listOf( + report(20, along = 2_500.0, id = "EARLIER"), + report(-5, along = 0.0).copy(receivedAtMs = at(20)), + )) + assertEquals(BUS, detector.snapshot(at(20)).trackedVehicleId) + + // Identification is bounded; travel on the identified vehicle is not. + detector.onBusReports(listOf(report(180, along = 2_000.0))) + val ride = detector.snapshot(at(180)) + assertEquals(BUS, ride.trackedVehicleId) + assertEquals(1, ride.passedDestinationByStops) + assertTrue(ride.hasRouteValidProgress) + } + + @Test + fun aNearbyVehicleNeedsAnActualAtStopObservation() { + for (along in listOf(100.0, 200.0)) { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = along))) + detector.confirmBoardedByUser(at(0)) + + assertUnidentified(detector.snapshot(at(0))) + } + } + + @Test + fun aVehicleBeforeTheFirstStopIsNotAtTheBoardingStop() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = -200.0))) + detector.confirmBoardedByUser(at(0)) + + assertUnidentified(detector.snapshot(at(0))) + } + + @Test + fun aVehicleBesideTheStopIsNotAnAtStopObservation() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0).copy(longitude = 120.6015))) + detector.confirmBoardedByUser(at(0)) + + assertUnidentified(detector.snapshot(at(0))) + } + + @Test + fun aBusStillAtTheStopWhenTheRiderConfirmsIsTheirs() { + // Its next report, already made but not yet delivered, shows it moving + // off with them on board. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.confirmBoardedByUser(at(15)) + assertEquals(BUS, detector.snapshot(at(15)).trackedVehicleId) + + detector.onBusReports(listOf(report(40, along = 300.0))) + assertEquals(BUS, detector.snapshot(at(40)).trackedVehicleId) + } + + @Test + fun aLaterBusCallingAtTheStopIsNotTheConfirmedRide() { + // The rider's own bus has no position feed. Whatever pulls in after + // they said they were on board is the next bus. + for (arrivesAfterSeconds in listOf(1L, 20L, 60L, 110L)) { + val detector = newDetector() + detector.confirmBoardedByUser(at(0)) + assertUnidentified(detector.snapshot(at(0))) + + detector.onBusReports(listOf(report(arrivesAfterSeconds, along = 0.0, id = "LATER-BUS"))) + assertUnidentified(detector.snapshot(at(arrivesAfterSeconds))) + + // It runs on past the rider's destination; the ride is not it. + detector.onBusReports(listOf(report(arrivesAfterSeconds + 60, along = 2_000.0, id = "LATER-BUS"))) + assertUnidentified(detector.snapshot(at(arrivesAfterSeconds + 60))) + } + } + + @Test + fun aLaterBusIsNotTakenEvenAfterTheRidersOwnBusWasSeenLeaving() { + val detector = newDetector() + detector.onBusReports(listOf(report(-10, along = 0.0))) + detector.confirmBoardedByUser(at(0)) + assertEquals(BUS, detector.snapshot(at(0)).trackedVehicleId) + + detector.onBusReports(listOf(report(60, along = 600.0), report(60, along = 0.0, id = "LATER-BUS"))) + assertEquals(BUS, detector.snapshot(at(60)).trackedVehicleId) + } + + @Test + fun answeringTheQuestionAboutANamedBusFollowsThatBus() { + // The question comes up 40 s after the bus has left, by which time it + // is well down the road; "yes" is still about that bus. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0))) + assertTrue(detector.snapshot(at(20)).boardingCheckId != 0L) + detector.onBusReports(listOf(report(60, along = 700.0))) + val question = detector.snapshot(at(60)) + assertTrue(question.boardingPromptDue) + detector.confirmBoardedByUser(at(70), answeredCheckId = question.boardingCheckId) + + val ride = detector.snapshot(at(70)) + assertEquals(BUS, ride.trackedVehicleId) + assertEquals(1, ride.progressStopIndex) + assertTrue(ride.hasRouteValidProgress) + } + + @Test + fun confirmingUnpromptedDoesNotAdoptABusAboutToBeAskedAbout() { + // Bus A leaves and a question about it is pending, but is not shown for + // another 40 s. Meanwhile the rider gets on bus B, which has no + // position feed, and says so with the button that is always there. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + val pending = detector.snapshot(at(20)) + assertTrue(pending.boardingCheckId != 0L) + assertFalse("the rider has not been asked anything yet", pending.boardingPromptDue) + detector.confirmBoardedByUser(at(30)) + + assertUnidentified(detector.snapshot(at(30))) + // A carries on past the rider's destination; the ride is not on it. + detector.onBusReports(listOf(report(150, along = 2_000.0, id = "BUS-A"))) + assertUnidentified(detector.snapshot(at(150))) + } + + @Test + fun confirmingUnpromptedWhileAQuestionIsShowingIsStillNotAnAnswerToIt() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + detector.snapshot(at(20)) + detector.onBusReports(listOf(report(60, along = 700.0, id = "BUS-A"))) + assertTrue(detector.snapshot(at(60)).boardingPromptDue) + detector.confirmBoardedByUser(at(70)) + + assertUnidentified(detector.snapshot(at(70))) + } + + @Test + fun anAnswerToAQuestionSinceReplacedNamesNoBus() { + // The question about bus A has been on screen for a while when bus B + // leaves, and the rider is to be asked afresh. Until that shows, the + // notification they can see and answer is still the old one. + for (boarded in listOf(true, false)) { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + val aboutA = detector.snapshot(at(20)).boardingCheckId + detector.onBusReports(listOf(report(100, along = 0.0, id = "BUS-B"))) + detector.snapshot(at(100)) + detector.onBusReports(listOf(report(120, along = 150.0, id = "BUS-B"))) + val afresh = detector.snapshot(at(120)).boardingCheckId + assertTrue("B leaving this long after must be asked about afresh", afresh != 0L && afresh != aboutA) + + if (boarded) { + detector.confirmBoardedByUser(at(125), answeredCheckId = aboutA) + assertUnidentified(detector.snapshot(at(125))) + } else { + detector.declineBoarding(at(125), answeredCheckId = aboutA) + val ride = detector.snapshot(at(165)) + assertEquals("the new question still stands", afresh, ride.boardingCheckId) + assertTrue(ride.boardingPromptDue) + + // It is about both buses: A left unanswered for, not long ago. + detector.confirmBoardedByUser(at(170), answeredCheckId = afresh) + assertUnidentified(detector.snapshot(at(170))) + } + } + } + + @Test + fun aQuestionThatHasLapsedNamesNoBus() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0))) + val question = detector.snapshot(at(20)).boardingCheckId + assertTrue(question != 0L) + // Another bus is standing at the stop by now. + detector.onBusReports(listOf(report(330, along = 2_400.0), report(325, along = 0.0, id = "OTHER"))) + // Not snapshotted in between, so the question was never retired. + detector.confirmBoardedByUser(at(330), answeredCheckId = question) + + // "Yes" was to a question about a bus long gone, not about this one. + assertUnidentified(detector.snapshot(at(330))) + } + + @Test + fun aVehicleBackAfterALongSilenceDidNotJustLeaveTheStop() { + for ((silenceSeconds, asked) in listOf(180L to true, 181L to false)) { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + assertEquals(0L, detector.snapshot(at(0)).boardingCheckId) + detector.onBusReports(listOf(report(silenceSeconds, along = 2_400.0))) + + assertEquals( + "a bus last seen at the stop $silenceSeconds s ago", + asked, + detector.snapshot(at(silenceSeconds)).boardingCheckId != 0L, + ) + } + } + + @Test + fun aRidePickedBackUpDoesNotAdoptTheBusNowAtTheBoardingStop() { + val detector = newDetector() + detector.onBusReports(listOf(report(-10, along = 0.0, id = "NEXT-BUS"))) + detector.resumeConfirmedRide() + + assertUnidentified(detector.snapshot(at(0))) + detector.onBusReports(listOf(report(20, along = 100.0, id = "NEXT-BUS"))) + assertUnidentified(detector.snapshot(at(20))) + + // The rider's own position is what finds their bus again. + detector.onBusReports(listOf(report(40, along = 1_400.0))) + detector.onUserLocation(RideGeoPoint(latitude(1_500.0), 120.6, at(40), accuracyMeters = 10f)) + assertEquals(BUS, detector.snapshot(at(40)).trackedVehicleId) + } + + @Test + fun atStopEvidenceMustBeFromTheNinetySecondsBeforeConfirming() { + for (seconds in listOf(90L, 91L)) { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(seconds - 5, along = 100.0))) + detector.confirmBoardedByUser(at(seconds)) + + assertEquals(BUS.takeIf { seconds == 90L }, detector.snapshot(at(seconds)).trackedVehicleId) + } + } + + @Test + fun repeatingTheSameFeedFixDoesNotRefreshItsEvidence() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.onBusReports(listOf(report(0, along = 0.0).copy(receivedAtMs = at(100)))) + detector.confirmBoardedByUser(at(100)) + + assertUnidentified(detector.snapshot(at(100))) + } + + @Test + fun lateIdentificationExpiresAndRepeatedConfirmationDoesNotRearmIt() { + for (seconds in listOf(120L, 121L)) { + val detector = newDetector() + detector.confirmBoardedByUser(at(0)) + // Made at the stop before the rider confirmed, delivered late. + detector.onBusReports(listOf(report(-10, along = 0.0).copy(receivedAtMs = at(seconds)))) + val expected = BUS.takeIf { seconds == 120L } + assertEquals(expected, detector.snapshot(at(seconds)).trackedVehicleId) + detector.confirmBoardedByUser(at(seconds + 1)) + + assertEquals(expected, detector.snapshot(at(seconds + 1)).trackedVehicleId) + } + } + + @Test + fun anInvalidStretchBreaksTheAtStopEvidence() { + for ((pathId, longitude) in listOf(1 to 120.6, 0 to 120.61)) { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.onBusReports(listOf(report(10, along = 100.0).copy(pathId = pathId, longitude = longitude))) + detector.onBusReports(listOf(report(20, along = 200.0))) + detector.confirmBoardedByUser(at(20)) + + assertUnidentified(detector.snapshot(at(20))) + } + } + + @Test + fun ambiguousNearbyVehiclesDoNotBecomeIdentifiedJustBecauseOneLeavesTheBound() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0), report(0, along = 0.0, id = "OTHER"))) + detector.confirmBoardedByUser(at(0)) + assertUnidentified(detector.snapshot(at(0))) + detector.onBusReports(listOf(report(20, along = 500.0), report(20, along = 0.0, id = "OTHER"))) + + assertUnidentified(detector.snapshot(at(20))) + detector.onUserLocation(RideGeoPoint(latitude(500.0), 120.6, at(20), accuracyMeters = 10f)) + assertEquals(BUS, detector.snapshot(at(20)).trackedVehicleId) + } + + @Test + fun confirmingAnIdentifiedRideDoesNotSwitchToANearerVehicle() { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.confirmBoardedByUser(at(0)) + assertEquals(BUS, detector.snapshot(at(0)).trackedVehicleId) + detector.onBusReports(listOf(report(60, along = 1_500.0), report(60, along = 0.0, id = "OTHER"))) + detector.confirmBoardedByUser(at(60)) + + assertEquals(BUS, detector.snapshot(at(60)).trackedVehicleId) + } + + @Test + fun twoBusesLeavingCloseTogetherAreNotOneDeparture() { + // A and B stand at the stop together. A leaves, and B forty seconds + // behind it, before the question about A has even been shown. The one + // question the rider then sees is about both. + val detector = newDetector() + detector.reportBoth(0, a = 0.0, b = 0.0) + detector.snapshot(at(0)) + detector.reportBoth(20, a = 150.0, b = 0.0) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue("A leaving must raise the question", asked != 0L) + detector.reportBoth(60, a = 600.0, b = 150.0) + val question = detector.snapshot(at(60)) + assertEquals("B leaving so soon after is asked about in the same breath", asked, question.boardingCheckId) + assertTrue(question.boardingPromptDue) + detector.reportBoth(80, a = 900.0, b = 400.0) + detector.snapshot(at(80)) + + // The rider got on B. "Yes" is as true of A, so it names neither. + detector.confirmBoardedByUser(at(85), answeredCheckId = asked) + assertUnidentified(detector.snapshot(at(85))) + + // A runs on past the rider's stop while B, with the rider, is short of it. + detector.reportBoth(200, a = 2_400.0, b = 1_200.0) + assertUnidentified(detector.snapshot(at(200))) + } + + @Test + fun aSecondBusAlreadyReportedGoneCountsBeforeAnySnapshotIsTaken() { + // The report that shows B gone as well arrives, and the rider answers + // before the detector has been asked to look again. + for (boarded in listOf(true, false)) { + val detector = newDetector() + detector.reportBoth(0, a = 0.0, b = 0.0) + detector.snapshot(at(0)) + detector.reportBoth(20, a = 150.0, b = 0.0) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue(asked != 0L) + detector.reportBoth(60, a = 600.0, b = 400.0) + + if (boarded) { + detector.confirmBoardedByUser(at(61), answeredCheckId = asked) + assertUnidentified(detector.snapshot(at(61))) + } else { + detector.declineBoarding(at(61), answeredCheckId = asked) + assertEquals( + "still waiting means on neither, so B is not asked about next", + 0L, + detector.snapshot(at(61)).boardingCheckId, + ) + } + } + } + + @Test + fun aRiderNoLongerAtTheStopIsNotAskedAfresh() { + // At the stop when A leaves; a few minutes on, some 350 m off to the + // side — not gone, but not where a bus is boarded — when B does. + val detector = newDetector() + detector.onUserLocation(RideGeoPoint(latitude(0.0), 120.6, at(0), accuracyMeters = 10f)) + detector.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue(asked != 0L) + for (seconds in 60L..200L step 20L) { + detector.onUserLocation(RideGeoPoint(latitude(0.0), 120.6034, at(seconds), accuracyMeters = 10f)) + } + detector.onBusReports(listOf(report(180, along = 0.0, id = "BUS-B"))) + detector.snapshot(at(180)) + detector.onBusReports(listOf(report(200, along = 150.0, id = "BUS-B"))) + val ride = detector.snapshot(at(200)) + + assertFalse(ride.hasBoarded) + assertEquals("B leaving is not a reason to ask someone who is not at the stop", asked, ride.boardingCheckId) + } + + @Test + fun aBusSeenLeavingTwiceIsOneDeparture() { + // A fix that wanders back to the stop makes the bus "leave" again. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0))) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue(asked != 0L) + detector.onBusReports(listOf(report(40, along = 30.0))) + detector.snapshot(at(40)) + detector.onBusReports(listOf(report(60, along = 400.0))) + assertTrue(detector.snapshot(at(60)).boardingPromptDue) + detector.confirmBoardedByUser(at(70), answeredCheckId = asked) + + assertEquals(BUS, detector.snapshot(at(70)).trackedVehicleId) + } + + @Test + fun aBusNotHeardFromForThreeMinutesDoesNotLeaveTheAnswerOpen() { + // B was last reported at the boarding stop, 500 m along, and then + // nothing: a bus the feed can no longer place is not one to weigh. + for ((silentForSeconds, namesTheBus) in listOf(180L to false, 181L to true)) { + val detector = newDetector(boardingStopId = 2) + detector.onBusReports(listOf(report(70 - silentForSeconds, along = 500.0, id = "BUS-B"))) + detector.onBusReports(listOf(report(0, along = 500.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 650.0, id = "BUS-A"))) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue(asked != 0L) + detector.onBusReports(listOf(report(60, along = 1_200.0, id = "BUS-A"))) + detector.snapshot(at(60)) + detector.confirmBoardedByUser(at(70), answeredCheckId = asked) + + assertEquals( + "B last heard from $silentForSeconds s before the answer", + "BUS-A".takeIf { namesTheBus }, + detector.snapshot(at(70)).trackedVehicleId, + ) + } + } + + @Test + fun aBusTheRiderCouldHaveBoardedSinceLeavesTheAnswerOpen() { + // The boarding stop is the second one, 500 m along. A has left it and + // the rider is asked; "yes" comes with B at [bAlong]. + fun answerWithBAt(bAlong: Double): RideSnapshot { + val detector = newDetector(boardingStopId = 2) + detector.onBusReports(listOf(report(0, along = 500.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 650.0, id = "BUS-A"))) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue(asked != 0L) + detector.reportBoth(60, a = 1_200.0, b = bAlong) + assertTrue(detector.snapshot(at(60)).boardingPromptDue) + detector.confirmBoardedByUser(at(70), answeredCheckId = asked) + return detector.snapshot(at(70)) + } + // Standing at the stop; near enough to have pulled in since it last + // reported; just gone from the stop without having been seen arriving. + for (bAlong in listOf(500.0, 250.0, 700.0)) { + assertUnidentified(answerWithBAt(bAlong)) + } + // Too far short of the stop to have reached it, or long past it. + for (bAlong in listOf(150.0, 800.0)) { + assertEquals("BUS-A", answerWithBAt(bAlong).trackedVehicleId) + } + } + + @Test + fun unpromptedTheBusAtTheStopIsNotAdoptedWhileOneThatLeftIsUnanswered() { + // A has left and is waiting to be asked about. B pulls in. The rider + // says they are on board without having been asked: on A, already down + // the road, or on B — the words are the same. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + assertTrue(detector.snapshot(at(20)).boardingCheckId != 0L) + detector.reportBoth(40, a = 450.0, b = 0.0) + assertFalse("nothing has been asked yet", detector.snapshot(at(40)).boardingPromptDue) + detector.confirmBoardedByUser(at(45)) + + assertUnidentified(detector.snapshot(at(45))) + // Nor does B become the ride for staying while A draws away. + detector.reportBoth(60, a = 700.0, b = 0.0) + assertUnidentified(detector.snapshot(at(60))) + detector.reportBoth(80, a = 950.0, b = 150.0) + assertUnidentified(detector.snapshot(at(80))) + } + + @Test + fun unpromptedASecondBusNearTheStopLeavesTheRideUnidentified() { + // A stands at the boarding stop, 500 m along; B is at [bAlong]. + fun confirmWithBAt(bAlong: Double): TripRideDetector { + val detector = newDetector(boardingStopId = 2) + detector.reportBoth(0, a = 500.0, b = bAlong) + detector.confirmBoardedByUser(at(5)) + return detector + } + // Coming up to the stop, or just pulling away from it. + for (bAlong in listOf(250.0, 600.0)) { + val detector = confirmWithBAt(bAlong) + assertUnidentified(detector.snapshot(at(5))) + // B moving on does not settle which of the two it was. + detector.reportBoth(30, a = 500.0, b = bAlong + 400.0) + assertUnidentified(detector.snapshot(at(30))) + } + // Too far back to have reached the stop. + assertEquals("BUS-A", confirmWithBAt(150.0).snapshot(at(5)).trackedVehicleId) + } + + @Test + fun unpromptedABusAlreadyTurnedDownDoesNotLeaveTheRideUnidentified() { + // A left and the rider said "not yet". It is held up just past the stop + // when B pulls in; the rider gets on B and says so. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + val asked = detector.snapshot(at(20)).boardingCheckId + detector.onBusReports(listOf(report(60, along = 200.0, id = "BUS-A"))) + assertTrue(detector.snapshot(at(60)).boardingPromptDue) + detector.declineBoarding(at(65), answeredCheckId = asked) + detector.reportBoth(80, a = 210.0, b = 0.0) + detector.snapshot(at(80)) + detector.confirmBoardedByUser(at(85)) + + assertEquals("BUS-B", detector.snapshot(at(85)).trackedVehicleId) + // And its report at the stop may as well arrive after the rider spoke. + val late = newDetector() + late.onBusReports(listOf(report(0, along = 0.0, id = "BUS-A"))) + late.snapshot(at(0)) + late.onBusReports(listOf(report(20, along = 150.0, id = "BUS-A"))) + val askedLate = late.snapshot(at(20)).boardingCheckId + late.onBusReports(listOf(report(60, along = 200.0, id = "BUS-A"))) + late.snapshot(at(60)) + late.declineBoarding(at(65), answeredCheckId = askedLate) + late.confirmBoardedByUser(at(85)) + assertUnidentified(late.snapshot(at(85))) + late.onBusReports( + listOf( + report(100, along = 210.0, id = "BUS-A"), + report(80, along = 0.0, id = "BUS-B").copy(receivedAtMs = at(100)), + ), + ) + assertEquals("BUS-B", late.snapshot(at(100)).trackedVehicleId) + } + + @Test + fun aRolloverBesideABusSeenLeavingIsThatBusOnlyWhenTheFeedSaysSo() { + // A is seen at the stop and leaving. Within the minute the ETA rolls + // over. [atStop] is the bus the stop feed placed there, if it said. + fun answer(atStop: String?, aSecondRollover: Boolean = false): RideSnapshot { + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.onBoardingStopEta(0, arriving = true, nowMs = at(0), arrivingVehicleId = atStop) + detector.snapshot(at(0)) + detector.onBusReports(listOf(report(20, along = 150.0))) + val asked = detector.snapshot(at(20)).boardingCheckId + assertTrue(asked != 0L) + detector.onBoardingStopEta(600, arriving = false, nowMs = at(50)) + if (aSecondRollover) { + detector.onBoardingStopEta(0, arriving = true, nowMs = at(60), arrivingVehicleId = atStop) + detector.onBoardingStopEta(600, arriving = false, nowMs = at(80)) + } + detector.onBusReports(listOf(report(80, along = 900.0))) + val ride = detector.snapshot(at(84)) + assertEquals("the rollover is not asked about separately", asked, ride.boardingCheckId) + detector.confirmBoardedByUser(at(85), answeredCheckId = asked) + return detector.snapshot(at(85)) + } + // The feed named the bus at the stop: the rollover is A leaving. + assertEquals(BUS, answer(atStop = BUS).trackedVehicleId) + assertEquals(BUS, answer(atStop = BUS, aSecondRollover = true).trackedVehicleId) + // It named another: B, with no position feed, left as well. + assertUnidentified(answer(atStop = "BUS-B")) + // It named none. The rider may have boarded a B the app never saw. + assertUnidentified(answer(atStop = null)) + assertUnidentified(answer(atStop = null, aSecondRollover = true)) + } + + @Test + fun aBusSeenLeavingBesideAnUnnamedRolloverIsNotNamedByTheAnswer() { + // The other order: the rollover first, then the position feed. + val detector = newDetector() + detector.onBusReports(listOf(report(0, along = 0.0))) + detector.onBoardingStopEta(0, arriving = true, nowMs = at(0)) + detector.snapshot(at(0)) + detector.onBoardingStopEta(600, arriving = false, nowMs = at(15)) + val asked = detector.snapshot(at(15)).boardingCheckId + assertTrue(asked != 0L) + detector.onBusReports(listOf(report(10, along = 150.0).copy(receivedAtMs = at(30)))) + detector.snapshot(at(30)) + detector.onBusReports(listOf(report(60, along = 800.0))) + assertEquals(asked, detector.snapshot(at(60)).boardingCheckId) + detector.confirmBoardedByUser(at(65), answeredCheckId = asked) + + assertUnidentified(detector.snapshot(at(65))) + } + + @Test + fun eachDeclinedBusIsCaughtUpWithByTheEtaOnce() { + // Two buses with no position feed, a "not yet" between them. + val etaOnly = newDetector() + etaOnly.onBoardingStopEta(0, arriving = true, nowMs = at(0)) + etaOnly.onBoardingStopEta(600, arriving = false, nowMs = at(30)) + val first = etaOnly.snapshot(at(75)) + assertTrue(first.boardingPromptDue) + etaOnly.declineBoarding(at(80), answeredCheckId = first.boardingCheckId) + etaOnly.onBoardingStopEta(0, arriving = true, nowMs = at(90)) + etaOnly.onBoardingStopEta(600, arriving = false, nowMs = at(110)) + assertTrue("the second bus is asked about", etaOnly.snapshot(at(110)).boardingCheckId != 0L) + + // A bus seen leaving and declined: its rollover is swallowed, the next is not. + val seen = newDetector() + seen.onBusReports(listOf(report(0, along = 0.0))) + seen.onBoardingStopEta(0, arriving = true, nowMs = at(0)) + seen.snapshot(at(0)) + seen.onBusReports(listOf(report(20, along = 150.0))) + val asked = seen.snapshot(at(20)).boardingCheckId + seen.onBusReports(listOf(report(60, along = 700.0))) + assertTrue(seen.snapshot(at(60)).boardingPromptDue) + seen.declineBoarding(at(65), answeredCheckId = asked) + seen.onBoardingStopEta(600, arriving = false, nowMs = at(70)) + assertEquals("A's own rollover", 0L, seen.snapshot(at(70)).boardingCheckId) + seen.onBoardingStopEta(0, arriving = true, nowMs = at(80)) + seen.onBoardingStopEta(600, arriving = false, nowMs = at(100)) + assertTrue("the next bus's rollover", seen.snapshot(at(100)).boardingCheckId != 0L) + } + + @Test + fun aBusStillAtTheStopAfterADeclinedRolloverIsTheNextBus() { + for (stillThereAfterwards in listOf(false, true)) { + val detector = newDetector() + detector.onBoardingStopEta(0, arriving = true, nowMs = at(0)) + detector.onBoardingStopEta(600, arriving = false, nowMs = at(30)) + val first = detector.snapshot(at(75)) + assertTrue(first.boardingPromptDue) + detector.declineBoarding(at(80), answeredCheckId = first.boardingCheckId) + // The position feed, late: at the stop either before the rollover + // (the same bus) or after it (the one behind). + val atStopAt = if (stillThereAfterwards) 60L else 10L + detector.onBusReports(listOf(report(atStopAt, along = 0.0).copy(receivedAtMs = at(85)))) + detector.snapshot(at(85)) + detector.onBusReports(listOf(report(95, along = 150.0))) + + assertEquals(stillThereAfterwards, detector.snapshot(at(100)).boardingCheckId != 0L) + } + } + + @Test + fun notYetSetsAsideEveryBusThatHasLeft() { + val detector = newDetector() + detector.reportBoth(0, a = 0.0, b = 0.0) + detector.snapshot(at(0)) + detector.reportBoth(20, a = 150.0, b = 0.0) + val asked = detector.snapshot(at(20)).boardingCheckId + detector.reportBoth(60, a = 600.0, b = 150.0) + assertTrue(detector.snapshot(at(60)).boardingPromptDue) + // Still waiting: on neither. + detector.declineBoarding(at(70), answeredCheckId = asked) + assertEquals(0L, detector.snapshot(at(70)).boardingCheckId) + + // B is held up just past the stop when the next bus comes and goes. + detector.onBusReports(listOf(report(100, along = 200.0, id = "BUS-B"), report(100, along = 0.0))) + detector.snapshot(at(100)) + detector.onBusReports(listOf(report(120, along = 220.0, id = "BUS-B"), report(120, along = 150.0))) + val next = detector.snapshot(at(120)).boardingCheckId + assertTrue("the next bus is asked about in its turn", next != 0L) + detector.onBusReports(listOf(report(160, along = 230.0, id = "BUS-B"), report(160, along = 700.0))) + assertTrue(detector.snapshot(at(160)).boardingPromptDue) + detector.confirmBoardedByUser(at(170), answeredCheckId = next) + + assertEquals( + "B was answered for already, and does not make this answer doubtful", + BUS, + detector.snapshot(at(170)).trackedVehicleId, + ) + } + + private fun assertUnidentified(ride: RideSnapshot) { + assertTrue("the manual action still confirms that the rider boarded", ride.hasBoarded) + assertEquals(BoardingEvidence.USER_CONFIRMED, ride.evidence) + assertNull("confirming a ride does not identify an unrelated vehicle", ride.trackedVehicleId) + assertEquals(-1, ride.progressStopIndex) + assertNull(ride.passedDestinationByStops) + assertFalse(ride.hasRouteValidProgress) + } + + /** Six stops 500 m apart; the rider gets off at the fourth. */ + private fun newDetector(boardingStopId: Int = 1) = TripRideDetector( + stops = (0..5).map { index -> + TrackingStop(index + 1, "stop-$index", index, latitude(index * 500.0), 120.6) + }, + pathId = 0, boardingStopId = boardingStopId, destinationStopId = 4, + ) + + /** Buses A and B, reported together. */ + private fun TripRideDetector.reportBoth(seconds: Long, a: Double, b: Double) = + onBusReports(listOf(report(seconds, along = a, id = "BUS-A"), report(seconds, along = b, id = "BUS-B"))) + + private fun report(seconds: Long, along: Double, id: String = BUS) = RideBusReport( + id, 0, latitude(along), 120.6, gpsTimeMs = at(seconds), receivedAtMs = at(seconds), + ) + + private fun latitude(meters: Double) = 24.1 + meters / (6_371_000.0 * PI / 180.0) + + private fun at(seconds: Long) = 1_700_000_000_000L + seconds * 1_000L + + private companion object { + const val BUS = "ACTUAL" + } +} diff --git a/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorReplayTest.kt b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorReplayTest.kt new file mode 100644 index 0000000..3d4cd14 --- /dev/null +++ b/android/app/src/test/kotlin/tw/avianjay/taiwanbus/flutter/TripRideDetectorReplayTest.kt @@ -0,0 +1,410 @@ +package tw.avianjay.taiwanbus.flutter + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.boolean +import kotlinx.serialization.json.double +import kotlinx.serialization.json.float +import kotlinx.serialization.json.int +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.jsonArray +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import kotlinx.serialization.json.long +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Replays recorded live feeds through [TripRideDetector]. + * + * Each fixture is a real capture of `/realtime/buses` and `/realtime` for one + * route: a synthetic passenger waits at a stop and then rides one of the + * recorded vehicles, while two negative controls are on the same street without + * being on the bus — waiting at the stop (with urban-canyon GPS noise), and + * walking the route. + */ +class TripRideDetectorReplayTest { + @Test + fun detectsRidingATaichungBus() { + val replay = Replay.load("ride_replay_TXG300_0") + val result = replay.detect(replay.riderFixes) + + assertTrue("expected the ride to be detected", result.detected) + assertTrue( + "detected ${(result.detectedAtMs - replay.boardedAtMs) / 1000}s after boarding", + result.detectedAtMs - replay.boardedAtMs <= 180_000L, + ) + } + + @Test + fun detectsRidingAKaohsiungBus() { + val replay = Replay.load("ride_replay_KHH100_0") + val result = replay.detect(replay.riderFixes) + + assertTrue("expected the ride to be detected", result.detected) + assertTrue( + "detected ${(result.detectedAtMs - replay.boardedAtMs) / 1000}s after boarding", + result.detectedAtMs - replay.boardedAtMs <= 180_000L, + ) + } + + @Test + fun detectsRidingATaipeiBusWhoseEtaFeedHasNoPlates() { + val replay = Replay.load("ride_replay_TPE157463_0") + val result = replay.detect(replay.riderFixes) + + assertTrue("expected the ride to be detected", result.detected) + assertTrue( + "detected ${(result.detectedAtMs - replay.boardedAtMs) / 1000}s after boarding", + result.detectedAtMs - replay.boardedAtMs <= 180_000L, + ) + } + + @Test + fun ridesTheRecordedVehicle() { + val replay = Replay.load("ride_replay_TXG300_0") + val result = replay.detect(replay.riderFixes) + + assertTrue(result.detected) + assertEquals(replay.plate, result.trackedVehicleId) + } + + @Test + fun doesNotDetectWaitingAtTheBoardingStop() { + val replay = Replay.load("ride_replay_TXG300_0") + val result = replay.detect(replay.waitingFixes) + + assertFalse("waiting at the stop must never count as boarding", result.detected) + } + + @Test + fun doesNotDetectWaitingAtTheBoardingStopInTaipei() { + val replay = Replay.load("ride_replay_TPE157463_0") + val result = replay.detect(replay.waitingFixes) + + assertFalse("waiting at the stop must never count as boarding", result.detected) + } + + @Test + fun doesNotDetectWalkingAlongTheRoute() { + val replay = Replay.load("ride_replay_TXG300_0") + val result = replay.detect(replay.walkerFixes) + + assertFalse("walking the same street must never count as boarding", result.detected) + } + + @Test + fun doesNotDetectWalkingAlongTheRouteInTaipei() { + val replay = Replay.load("ride_replay_TPE157463_0") + val result = replay.detect(replay.walkerFixes) + + assertFalse("walking the same street must never count as boarding", result.detected) + } + + @Test + fun asksTheUserAfterABusLeavesTheBoardingStop() { + val replay = Replay.load("ride_replay_TXG300_0") + // The user drifts around the stop, as a phone in a pocket does, and + // eventually wanders off down the street — so the app cannot conclude + // from stillness alone that they are still waiting. + val drifting = replay.waitingFixes.mapIndexed { index, fix -> + if (index < replay.waitingFixes.size / 2) fix else replay.walkerFixes[index] + } + var prompted = false + val result = replay.detect(drifting) { _, snapshot -> + if (snapshot.boardingPromptDue) { + prompted = true + } + } + + assertTrue("a passenger who never boarded should be asked", prompted) + assertFalse("walking away from the stop is not a ride", result.detected) + } + + @Test + fun doesNotAskWhileTheUserIsStillRiding() { + val replay = Replay.load("ride_replay_TXG300_0") + var askedWhileRiding = false + val result = replay.detect(replay.riderFixes) { _, snapshot -> + if (snapshot.hasBoarded && snapshot.boardingPromptDue) { + askedWhileRiding = true + } + } + + assertTrue("expected the ride to be detected", result.detected) + assertFalse("a confirmed ride must not be questioned", askedWhileRiding) + } + + @Test + fun asksEvenWhenTheBusPositionFeedIsEmpty() { + val replay = Replay.load("ride_replay_TPE157463_0") + var prompted = false + replay.detect(replay.waitingFixes, withBusReports = false) { _, snapshot -> + if (snapshot.boardingPromptDue) { + prompted = true + } + } + + assertTrue("the ETA feed alone must be able to raise the question", prompted) + } + + @Test + fun userConfirmationWinsOverEverything() { + val replay = Replay.load("ride_replay_TXG300_0") + val detector = replay.detector() + replay.pump(detector, withBusReports = true, withUserFixes = false) { _, _ -> } + + detector.confirmBoardedByUser(replay.lastFrameMs) + + val snapshot = detector.snapshot(replay.lastFrameMs) + assertTrue(snapshot.hasBoarded) + assertEquals(BoardingEvidence.USER_CONFIRMED, snapshot.evidence) + assertFalse(snapshot.boardingPromptDue) + } + + @Test + fun aDeclineIsNotOverruledByTheEvidenceItAnswered() { + val replay = Replay.load("ride_replay_TXG300_0") + val detector = replay.detector() + var declined = false + val looksAfterTheAnswer = ArrayList() + replay.pump(detector, withUserFixes = true) { nowMs, snapshot -> + if (!declined && snapshot.hasBoarded) { + // Everything that detected this ride is still in the detector's + // window when the rider says no. + detector.declineBoarding(nowMs) + declined = true + looksAfterTheAnswer += detector.snapshot(nowMs) + } else if (declined && looksAfterTheAnswer.size < 2) { + looksAfterTheAnswer += snapshot + } + } + + assertTrue("expected the recorded ride to be detected", declined) + assertEquals(2, looksAfterTheAnswer.size) + looksAfterTheAnswer.forEach { snapshot -> + assertFalse("the answer holds at the next look", snapshot.hasBoarded) + assertEquals(BoardingEvidence.NONE, snapshot.evidence) + } + } + + @Test + fun remainingStopsCountDownAndNeverGoNegative() { + val replay = Replay.load("ride_replay_TXG300_0") + var previous = Int.MAX_VALUE + var observed = false + replay.detect(replay.riderFixes) { _, snapshot -> + val remaining = snapshot.remainingStops ?: return@detect + observed = true + assertTrue("remainingStops grew: $previous -> $remaining", remaining <= previous) + assertTrue("remainingStops went negative: $remaining", remaining >= 0) + previous = remaining + } + + assertTrue("expected a destination to be tracked", observed) + } + + @Test + fun changingTheDestinationDoesNotDisturbTheRide() { + val replay = Replay.load("ride_replay_TXG300_0") + val detector = replay.detector() + var changed = false + replay.pump(detector, withUserFixes = true) { _, snapshot -> + if (!changed && snapshot.hasBoarded) { + detector.updateDestination(replay.destinationStopId - 1) + changed = true + } + } + + assertTrue(changed) + val snapshot = detector.snapshot(replay.lastFrameMs) + assertTrue("the ride must survive a new destination", snapshot.hasBoarded) + assertEquals(replay.plate, snapshot.trackedVehicleId) + } +} + +private class Replay( + private val stops: List, + private val pathId: Int, + val plate: String, + val boardedAtMs: Long, + val destinationStopId: Int, + private val boardingStopId: Int, + private val frames: List, + val riderFixes: List, + val waitingFixes: List, + val walkerFixes: List, +) { + /** One poll of the live feeds, at the moment the app would have made it. */ + class Frame( + val nowMs: Long, + val reports: List, + val boardingEta: EtaReading?, + ) + + class EtaReading(val etaSeconds: Int?, val arriving: Boolean) + + class DetectionResult( + val detected: Boolean, + val detectedAtMs: Long, + val trackedVehicleId: String?, + ) + + val lastFrameMs: Long get() = frames.last().nowMs + + fun detector(): TripRideDetector = TripRideDetector( + stops = stops, + pathId = pathId, + boardingStopId = boardingStopId, + destinationStopId = destinationStopId, + ) + + /** + * Feeds the recorded polls and [fixes] in time order, reporting every + * snapshot. The detector's own answer is what decides when a ride has been + * detected, so this never reads state the app could not see. + */ + fun detect( + fixes: List, + withBusReports: Boolean = true, + onSnapshot: (Long, RideSnapshot) -> Unit = { _, _ -> }, + ): DetectionResult { + val detector = detector() + var detectedAtMs = 0L + var trackedVehicleId: String? = null + var fixIndex = 0 + pump(detector, withBusReports = withBusReports, withUserFixes = false) { nowMs, _ -> + while (fixIndex < fixes.size && fixes[fixIndex].timestampMs <= nowMs) { + detector.onUserLocation(fixes[fixIndex]) + fixIndex++ + } + val snapshot = detector.snapshot(nowMs) + if (detectedAtMs == 0L && snapshot.hasBoarded) { + detectedAtMs = nowMs + trackedVehicleId = snapshot.trackedVehicleId + } + onSnapshot(nowMs, snapshot) + } + return DetectionResult(detectedAtMs != 0L, detectedAtMs, trackedVehicleId) + } + + /** Walks the recorded polls without feeding any user fixes. */ + fun pump( + detector: TripRideDetector, + withBusReports: Boolean = true, + withUserFixes: Boolean = false, + onSnapshot: (Long, RideSnapshot) -> Unit, + ) { + frames.forEach { frame -> + if (withBusReports && frame.reports.isNotEmpty()) { + detector.onBusReports(frame.reports) + } + frame.boardingEta?.let { + detector.onBoardingStopEta(it.etaSeconds, it.arriving, frame.nowMs) + } + if (withUserFixes) { + detector.onUserLocation(nearestFix(riderFixes, frame.nowMs)) + } + onSnapshot(frame.nowMs, detector.snapshot(frame.nowMs)) + } + } + + private fun nearestFix(fixes: List, nowMs: Long): RideGeoPoint { + var low = 0 + var high = fixes.size - 1 + while (low < high) { + val mid = (low + high) / 2 + if (fixes[mid].timestampMs < nowMs) low = mid + 1 else high = mid + } + return fixes[low] + } + + companion object { + // org.json is only a stub on the plain JVM test classpath, so fixtures + // are read with kotlinx.serialization instead. + fun load(name: String): Replay { + val stream = Replay::class.java.classLoader?.getResourceAsStream("$name.json") + val text = requireNotNull(stream) { "missing fixture $name.json" } + .bufferedReader(Charsets.UTF_8) + .use { it.readText() } + val root = Json.parseToJsonElement(text).jsonObject + + val stops = root.getArray("stops").map { element -> + val stop = element.jsonObject + TrackingStop( + // Folded exactly as the service folds it, from the raw feed + // value. A fixture that stored pre-folded ids could not tell + // a correct fold from a broken one. + stopId = foldStopId(stop.getValue("stopIdRaw").jsonPrimitive.content), + stopName = "stop-${stop.getInt("sequence")}", + sequence = stop.getInt("sequence"), + lat = stop.getValue("lat").jsonPrimitive.double, + lon = stop.getValue("lon").jsonPrimitive.double, + ) + } + + val reportsByFrame = root.getArray("busReports").groupBy { it.jsonObject.getLong("rx") } + val etaByFrame = root.getArray("boardingEta").associateBy { it.jsonObject.getLong("t") } + val frames = (reportsByFrame.keys + etaByFrame.keys).sorted().map { nowMs -> + Frame( + nowMs = nowMs, + reports = reportsByFrame[nowMs].orEmpty().map { element -> + val report = element.jsonObject + RideBusReport( + vehicleId = report.getValue("id").jsonPrimitive.content, + pathId = report["dir"]?.jsonPrimitive?.content?.toIntOrNull(), + latitude = report.getValue("lat").jsonPrimitive.double, + longitude = report.getValue("lon").jsonPrimitive.double, + gpsTimeMs = report.getValue("t").jsonPrimitive.long, + receivedAtMs = nowMs, + ) + }, + boardingEta = etaByFrame[nowMs]?.jsonObject?.let { eta -> + EtaReading( + etaSeconds = eta["eta"]?.jsonPrimitive?.content?.toIntOrNull(), + arriving = eta["arriving"]?.jsonPrimitive?.boolean ?: false, + ) + }, + ) + } + + return Replay( + stops = stops, + pathId = root.getInt("pathId"), + plate = root.getValue("plate").jsonPrimitive.content, + boardedAtMs = root.getValue("boardedAtMs").jsonPrimitive.long, + destinationStopId = foldStopId( + root.getValue("destinationStopIdRaw").jsonPrimitive.content, + ), + boardingStopId = foldStopId( + root.getValue("boardingStopIdRaw").jsonPrimitive.content, + ), + frames = frames, + riderFixes = root.fixes("riderFixes"), + waitingFixes = root.fixes("waitingFixes"), + walkerFixes = root.fixes("walkerFixes"), + ) + } + + private fun JsonObject.getArray(key: String) = getValue(key).jsonArray + + private fun JsonObject.getInt(key: String) = getValue(key).jsonPrimitive.int + + private fun JsonObject.getLong(key: String) = getValue(key).jsonPrimitive.long + + private fun JsonObject.fixes(key: String): List { + return getArray(key).map { element -> + val fix = element.jsonObject + RideGeoPoint( + latitude = fix.getValue("lat").jsonPrimitive.double, + longitude = fix.getValue("lon").jsonPrimitive.double, + timestampMs = fix.getValue("t").jsonPrimitive.long, + accuracyMeters = fix["acc"]?.jsonPrimitive?.float ?: 0f, + speedMetersPerSecond = fix["spd"]?.jsonPrimitive?.float, + ) + } + } + } +} diff --git a/android/app/src/test/resources/ride_replay_KHH100_0.json b/android/app/src/test/resources/ride_replay_KHH100_0.json new file mode 100644 index 0000000..d9e5aca --- /dev/null +++ b/android/app/src/test/resources/ride_replay_KHH100_0.json @@ -0,0 +1 @@ +{"route":"KHH100","pathId":0,"plate":"051-V3","boardedAtMs":1794376372610,"boardingStopIdRaw":"100100128221","destinationStopIdRaw":"1001001521","stops":[{"stopIdRaw":"1001001781","sequence":1,"lat":22.60612,"lon":120.338401},{"stopIdRaw":"1001003081","sequence":2,"lat":22.605755,"lon":120.334116},{"stopIdRaw":"1001008311","sequence":3,"lat":22.607333,"lon":120.333533},{"stopIdRaw":"1001008321","sequence":4,"lat":22.609866,"lon":120.33315},{"stopIdRaw":"1001008331","sequence":5,"lat":22.612866,"lon":120.332716},{"stopIdRaw":"100100128221","sequence":6,"lat":22.614283,"lon":120.332533},{"stopIdRaw":"1001008371","sequence":7,"lat":22.617133,"lon":120.33225},{"stopIdRaw":"1001008401","sequence":8,"lat":22.619716,"lon":120.331966},{"stopIdRaw":"1001008411","sequence":9,"lat":22.621489,"lon":120.331691},{"stopIdRaw":"10010095351","sequence":10,"lat":22.622119,"lon":120.329212},{"stopIdRaw":"1001001521","sequence":11,"lat":22.621192,"lon":120.326523},{"stopIdRaw":"100100521","sequence":12,"lat":22.620276,"lon":120.323801},{"stopIdRaw":"10010095371","sequence":13,"lat":22.619572,"lon":120.321493},{"stopIdRaw":"10010095391","sequence":14,"lat":22.619,"lon":120.319791},{"stopIdRaw":"10010095401","sequence":15,"lat":22.618223,"lon":120.317459},{"stopIdRaw":"10010095411","sequence":16,"lat":22.617162,"lon":120.313862},{"stopIdRaw":"10010095541","sequence":17,"lat":22.616591,"lon":120.312333},{"stopIdRaw":"10010095551","sequence":18,"lat":22.616015,"lon":120.310558},{"stopIdRaw":"10010095561","sequence":19,"lat":22.615457,"lon":120.308804},{"stopIdRaw":"10010095771","sequence":20,"lat":22.614469,"lon":120.306097},{"stopIdRaw":"1001007571","sequence":21,"lat":22.613541,"lon":120.303198},{"stopIdRaw":"1001009071","sequence":22,"lat":22.61283,"lon":120.300958},{"stopIdRaw":"1001007661","sequence":23,"lat":22.61238,"lon":120.299629},{"stopIdRaw":"1001008591","sequence":24,"lat":22.613843,"lon":120.297885},{"stopIdRaw":"10010095781","sequence":25,"lat":22.616931,"lon":120.296764},{"stopIdRaw":"100100801","sequence":26,"lat":22.619297,"lon":120.295867},{"stopIdRaw":"10010025951","sequence":27,"lat":22.621196,"lon":120.295963},{"stopIdRaw":"100100122871","sequence":28,"lat":22.621859,"lon":120.29761},{"stopIdRaw":"10010095791","sequence":29,"lat":22.622617,"lon":120.299883},{"stopIdRaw":"100100158451","sequence":30,"lat":22.623627,"lon":120.30297},{"stopIdRaw":"100100158491","sequence":31,"lat":22.626727,"lon":120.304377},{"stopIdRaw":"10010095931","sequence":32,"lat":22.630165,"lon":120.301918},{"stopIdRaw":"100100158501","sequence":33,"lat":22.634002,"lon":120.302372},{"stopIdRaw":"100100158511","sequence":34,"lat":22.639645,"lon":120.303662},{"stopIdRaw":"100100158521","sequence":35,"lat":22.64565,"lon":120.301402},{"stopIdRaw":"100100158531","sequence":36,"lat":22.646418,"lon":120.299898},{"stopIdRaw":"10010091321","sequence":37,"lat":22.651493,"lon":120.302197},{"stopIdRaw":"100100158541","sequence":38,"lat":22.654081,"lon":120.303772},{"stopIdRaw":"100100158551","sequence":39,"lat":22.656977,"lon":120.303235},{"stopIdRaw":"100100158581","sequence":40,"lat":22.660755,"lon":120.303238},{"stopIdRaw":"100100158591","sequence":41,"lat":22.66423,"lon":120.30333},{"stopIdRaw":"100100158601","sequence":42,"lat":22.666229,"lon":120.303453},{"stopIdRaw":"100100158631","sequence":43,"lat":22.668385,"lon":120.303541},{"stopIdRaw":"100100158641","sequence":44,"lat":22.670703,"lon":120.301788}],"busReports":[{"id":"038-V3","dir":1,"lat":22.617069,"lon":120.313983,"t":1794376116000,"rx":1794376162068},{"id":"051-V3","dir":0,"lat":22.605685,"lon":120.333899,"t":1794376112000,"rx":1794376162068},{"id":"056-V3","dir":0,"lat":22.61973,"lon":120.322109,"t":1794376115000,"rx":1794376162068},{"id":"059-V3","dir":0,"lat":22.620523,"lon":120.29523,"t":1794376114000,"rx":1794376162068},{"id":"063-V3","dir":0,"lat":22.671093,"lon":120.303856,"t":1794376114000,"rx":1794376162068},{"id":"067-V3","dir":0,"lat":22.627553,"lon":120.302295,"t":1794376116000,"rx":1794376162068},{"id":"068-V3","dir":1,"lat":22.645581,"lon":120.300651,"t":1794376113000,"rx":1794376162068},{"id":"111-V3","dir":0,"lat":22.606338,"lon":120.33839,"t":1794376116000,"rx":1794376162068},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376114000,"rx":1794376162068},{"id":"123-V3","dir":1,"lat":22.654108,"lon":120.303543,"t":1794376125000,"rx":1794376162068},{"id":"185-FT","dir":1,"lat":22.616431,"lon":120.332241,"t":1794376129000,"rx":1794376162068},{"id":"038-V3","dir":1,"lat":22.617373,"lon":120.314958,"t":1794376156000,"rx":1794376182859},{"id":"051-V3","dir":0,"lat":22.605765,"lon":120.333618,"t":1794376152000,"rx":1794376182859},{"id":"056-V3","dir":0,"lat":22.618974,"lon":120.31981,"t":1794376155000,"rx":1794376182859},{"id":"059-V3","dir":0,"lat":22.621458,"lon":120.296393,"t":1794376154000,"rx":1794376182859},{"id":"063-V3","dir":0,"lat":22.672245,"lon":120.301426,"t":1794376154000,"rx":1794376182859},{"id":"067-V3","dir":0,"lat":22.627801,"lon":120.301609,"t":1794376156000,"rx":1794376182859},{"id":"068-V3","dir":1,"lat":22.645541,"lon":120.30141,"t":1794376153000,"rx":1794376182859},{"id":"111-V3","dir":0,"lat":22.606338,"lon":120.33839,"t":1794376156000,"rx":1794376182859},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376154000,"rx":1794376182859},{"id":"123-V3","dir":1,"lat":22.652446,"lon":120.303568,"t":1794376152000,"rx":1794376182859},{"id":"185-FT","dir":1,"lat":22.613478,"lon":120.332579,"t":1794376157000,"rx":1794376182859},{"id":"038-V3","dir":1,"lat":22.61755,"lon":120.315378,"t":1794376176000,"rx":1794376208998},{"id":"051-V3","dir":0,"lat":22.606766,"lon":120.333486,"t":1794376172000,"rx":1794376208998},{"id":"056-V3","dir":0,"lat":22.618475,"lon":120.318088,"t":1794376184000,"rx":1794376208998},{"id":"059-V3","dir":0,"lat":22.622088,"lon":120.29823,"t":1794376186000,"rx":1794376208998},{"id":"063-V3","dir":0,"lat":22.671366,"lon":120.300951,"t":1794376174000,"rx":1794376208998},{"id":"067-V3","dir":0,"lat":22.628879,"lon":120.301763,"t":1794376176000,"rx":1794376208998},{"id":"068-V3","dir":1,"lat":22.645411,"lon":120.30294,"t":1794376173000,"rx":1794376208998},{"id":"111-V3","dir":0,"lat":22.606338,"lon":120.33839,"t":1794376176000,"rx":1794376208998},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376174000,"rx":1794376208998},{"id":"123-V3","dir":1,"lat":22.652446,"lon":120.303568,"t":1794376172000,"rx":1794376208998},{"id":"185-FT","dir":1,"lat":22.61325,"lon":120.332608,"t":1794376182000,"rx":1794376208998},{"id":"038-V3","dir":1,"lat":22.61824,"lon":120.317451,"t":1794376216000,"rx":1794376233211},{"id":"051-V3","dir":0,"lat":22.608035,"lon":120.33334,"t":1794376213000,"rx":1794376233211},{"id":"056-V3","dir":0,"lat":22.61803,"lon":120.316646,"t":1794376218000,"rx":1794376233211},{"id":"059-V3","dir":0,"lat":22.622116,"lon":120.298285,"t":1794376214000,"rx":1794376233211},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376214000,"rx":1794376233211},{"id":"067-V3","dir":0,"lat":22.629539,"lon":120.301841,"t":1794376216000,"rx":1794376233211},{"id":"068-V3","dir":1,"lat":22.644514,"lon":120.302921,"t":1794376213000,"rx":1794376233211},{"id":"111-V3","dir":0,"lat":22.606338,"lon":120.33839,"t":1794376216000,"rx":1794376233211},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376214000,"rx":1794376233211},{"id":"123-V3","dir":1,"lat":22.652378,"lon":120.303558,"t":1794376212000,"rx":1794376233211},{"id":"185-FT","dir":1,"lat":22.61325,"lon":120.332608,"t":1794376202000,"rx":1794376233211},{"id":"038-V3","dir":1,"lat":22.61824,"lon":120.317451,"t":1794376236000,"rx":1794376278940},{"id":"051-V3","dir":0,"lat":22.610648,"lon":120.332988,"t":1794376243000,"rx":1794376278940},{"id":"056-V3","dir":0,"lat":22.617871,"lon":120.316159,"t":1794376235000,"rx":1794376278940},{"id":"059-V3","dir":0,"lat":22.622116,"lon":120.298285,"t":1794376234000,"rx":1794376278940},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376234000,"rx":1794376278940},{"id":"067-V3","dir":0,"lat":22.630346,"lon":120.301981,"t":1794376236000,"rx":1794376278940},{"id":"068-V3","dir":1,"lat":22.643113,"lon":120.302824,"t":1794376233000,"rx":1794376278940},{"id":"111-V3","dir":0,"lat":22.606338,"lon":120.33839,"t":1794376236000,"rx":1794376278940},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376234000,"rx":1794376278940},{"id":"123-V3","dir":1,"lat":22.651758,"lon":120.302764,"t":1794376241000,"rx":1794376278940},{"id":"185-FT","dir":1,"lat":22.611944,"lon":120.332731,"t":1794376248000,"rx":1794376278940},{"id":"038-V3","dir":1,"lat":22.618323,"lon":120.31776,"t":1794376276000,"rx":1794376301877},{"id":"051-V3","dir":0,"lat":22.612151,"lon":120.332776,"t":1794376272000,"rx":1794376301877},{"id":"056-V3","dir":0,"lat":22.617776,"lon":120.315915,"t":1794376275000,"rx":1794376301877},{"id":"059-V3","dir":0,"lat":22.622116,"lon":120.298285,"t":1794376274000,"rx":1794376301877},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376274000,"rx":1794376301877},{"id":"067-V3","dir":0,"lat":22.630524,"lon":120.301969,"t":1794376276000,"rx":1794376301877},{"id":"068-V3","dir":1,"lat":22.640606,"lon":120.301783,"t":1794376273000,"rx":1794376301877},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376276000,"rx":1794376301877},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376274000,"rx":1794376301877},{"id":"123-V3","dir":1,"lat":22.651074,"lon":120.300283,"t":1794376272000,"rx":1794376301877},{"id":"185-FT","dir":1,"lat":22.610523,"lon":120.332873,"t":1794376262000,"rx":1794376301877},{"id":"038-V3","dir":1,"lat":22.618438,"lon":120.318308,"t":1794376296000,"rx":1794376327152},{"id":"051-V3","dir":0,"lat":22.612151,"lon":120.332776,"t":1794376292000,"rx":1794376327152},{"id":"056-V3","dir":0,"lat":22.617321,"lon":120.314496,"t":1794376295000,"rx":1794376327152},{"id":"059-V3","dir":0,"lat":22.622415,"lon":120.299165,"t":1794376308000,"rx":1794376327152},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376294000,"rx":1794376327152},{"id":"067-V3","dir":0,"lat":22.630534,"lon":120.301969,"t":1794376296000,"rx":1794376327152},{"id":"068-V3","dir":1,"lat":22.640603,"lon":120.301784,"t":1794376293000,"rx":1794376327152},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376296000,"rx":1794376327152},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376294000,"rx":1794376327152},{"id":"123-V3","dir":1,"lat":22.650166,"lon":120.300161,"t":1794376292000,"rx":1794376327152},{"id":"185-FT","dir":1,"lat":22.60929,"lon":120.333031,"t":1794376302000,"rx":1794376327152},{"id":"038-V3","dir":1,"lat":22.619225,"lon":120.320779,"t":1794376337000,"rx":1794376351234},{"id":"051-V3","dir":0,"lat":22.612293,"lon":120.332743,"t":1794376332000,"rx":1794376351234},{"id":"056-V3","dir":0,"lat":22.616966,"lon":120.313474,"t":1794376335000,"rx":1794376351234},{"id":"059-V3","dir":0,"lat":22.623171,"lon":120.301339,"t":1794376334000,"rx":1794376351234},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376334000,"rx":1794376351234},{"id":"067-V3","dir":0,"lat":22.630534,"lon":120.301969,"t":1794376336000,"rx":1794376351234},{"id":"068-V3","dir":1,"lat":22.640603,"lon":120.301784,"t":1794376333000,"rx":1794376351234},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376336000,"rx":1794376351234},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376334000,"rx":1794376351234},{"id":"123-V3","dir":1,"lat":22.647991,"lon":120.299946,"t":1794376332000,"rx":1794376351234},{"id":"185-FT","dir":1,"lat":22.609116,"lon":120.333055,"t":1794376331000,"rx":1794376351234},{"id":"038-V3","dir":1,"lat":22.619946,"lon":120.323004,"t":1794376356000,"rx":1794376398309},{"id":"051-V3","dir":0,"lat":22.613546,"lon":120.332603,"t":1794376363000,"rx":1794376398309},{"id":"056-V3","dir":0,"lat":22.616613,"lon":120.312319,"t":1794376355000,"rx":1794376398309},{"id":"059-V3","dir":0,"lat":22.623658,"lon":120.302911,"t":1794376354000,"rx":1794376398309},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376354000,"rx":1794376398309},{"id":"067-V3","dir":0,"lat":22.630946,"lon":120.302019,"t":1794376356000,"rx":1794376398309},{"id":"068-V3","dir":1,"lat":22.640463,"lon":120.301786,"t":1794376353000,"rx":1794376398309},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376356000,"rx":1794376398309},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376354000,"rx":1794376398309},{"id":"123-V3","dir":1,"lat":22.647629,"lon":120.29994,"t":1794376364000,"rx":1794376398309},{"id":"185-FT","dir":1,"lat":22.607341,"lon":120.333264,"t":1794376362000,"rx":1794376398309},{"id":"038-V3","dir":1,"lat":22.620011,"lon":120.323181,"t":1794376396000,"rx":1794376422055},{"id":"051-V3","dir":0,"lat":22.615764,"lon":120.332331,"t":1794376392000,"rx":1794376422055},{"id":"056-V3","dir":0,"lat":22.616516,"lon":120.312053,"t":1794376395000,"rx":1794376422055},{"id":"059-V3","dir":0,"lat":22.62366,"lon":120.302914,"t":1794376394000,"rx":1794376422055},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376394000,"rx":1794376422055},{"id":"067-V3","dir":0,"lat":22.63353,"lon":120.302246,"t":1794376396000,"rx":1794376422055},{"id":"068-V3","dir":1,"lat":22.638343,"lon":120.301603,"t":1794376393000,"rx":1794376422055},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376396000,"rx":1794376422055},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376394000,"rx":1794376422055},{"id":"123-V3","dir":1,"lat":22.645623,"lon":120.300133,"t":1794376394000,"rx":1794376422055},{"id":"185-FT","dir":1,"lat":22.606513,"lon":120.333406,"t":1794376383000,"rx":1794376422055},{"id":"038-V3","dir":1,"lat":22.620051,"lon":120.323301,"t":1794376417000,"rx":1794376445889},{"id":"051-V3","dir":0,"lat":22.615764,"lon":120.332331,"t":1794376412000,"rx":1794376445889},{"id":"056-V3","dir":0,"lat":22.616516,"lon":120.312053,"t":1794376415000,"rx":1794376445889},{"id":"059-V3","dir":0,"lat":22.623905,"lon":120.303443,"t":1794376421000,"rx":1794376445889},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376414000,"rx":1794376445889},{"id":"067-V3","dir":0,"lat":22.634125,"lon":120.3023,"t":1794376421000,"rx":1794376445889},{"id":"068-V3","dir":1,"lat":22.637198,"lon":120.302321,"t":1794376414000,"rx":1794376445889},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376416000,"rx":1794376445889},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376414000,"rx":1794376445889},{"id":"123-V3","dir":1,"lat":22.645586,"lon":120.300771,"t":1794376412000,"rx":1794376445889},{"id":"185-FT","dir":1,"lat":22.605959,"lon":120.333486,"t":1794376422000,"rx":1794376445889},{"id":"038-V3","dir":1,"lat":22.620421,"lon":120.324396,"t":1794376456000,"rx":1794376484384},{"id":"051-V3","dir":0,"lat":22.615925,"lon":120.332324,"t":1794376452000,"rx":1794376484384},{"id":"056-V3","dir":0,"lat":22.615943,"lon":120.31037,"t":1794376455000,"rx":1794376484384},{"id":"059-V3","dir":0,"lat":22.624683,"lon":120.304843,"t":1794376454000,"rx":1794376484384},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376454000,"rx":1794376484384},{"id":"067-V3","dir":0,"lat":22.634206,"lon":120.302301,"t":1794376456000,"rx":1794376484384},{"id":"068-V3","dir":1,"lat":22.636331,"lon":120.302151,"t":1794376453000,"rx":1794376484384},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376456000,"rx":1794376484384},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376454000,"rx":1794376484384},{"id":"123-V3","dir":1,"lat":22.645495,"lon":120.30213,"t":1794376452000,"rx":1794376484384},{"id":"185-FT","dir":1,"lat":22.605696,"lon":120.333574,"t":1794376459000,"rx":1794376484384},{"id":"038-V3","dir":1,"lat":22.620908,"lon":120.325913,"t":1794376489000,"rx":1794376509087},{"id":"051-V3","dir":0,"lat":22.619133,"lon":120.331924,"t":1794376486000,"rx":1794376509087},{"id":"056-V3","dir":0,"lat":22.615636,"lon":120.309528,"t":1794376479000,"rx":1794376509087},{"id":"059-V3","dir":0,"lat":22.626256,"lon":120.304325,"t":1794376475000,"rx":1794376509087},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376474000,"rx":1794376509087},{"id":"067-V3","dir":0,"lat":22.634206,"lon":120.302301,"t":1794376476000,"rx":1794376509087},{"id":"068-V3","dir":1,"lat":22.636331,"lon":120.302151,"t":1794376473000,"rx":1794376509087},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376476000,"rx":1794376509087},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376474000,"rx":1794376509087},{"id":"123-V3","dir":1,"lat":22.645385,"lon":120.302899,"t":1794376472000,"rx":1794376509087},{"id":"185-FT","dir":1,"lat":22.605696,"lon":120.333574,"t":1794376482000,"rx":1794376509087},{"id":"038-V3","dir":1,"lat":22.621421,"lon":120.327498,"t":1794376516000,"rx":1794376532079},{"id":"051-V3","dir":0,"lat":22.620948,"lon":120.331725,"t":1794376512000,"rx":1794376532079},{"id":"056-V3","dir":0,"lat":22.614836,"lon":120.307036,"t":1794376515000,"rx":1794376532079},{"id":"059-V3","dir":0,"lat":22.627228,"lon":120.304304,"t":1794376515000,"rx":1794376532079},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376514000,"rx":1794376532079},{"id":"067-V3","dir":0,"lat":22.63435,"lon":120.302319,"t":1794376516000,"rx":1794376532079},{"id":"068-V3","dir":1,"lat":22.636258,"lon":120.302163,"t":1794376513000,"rx":1794376532079},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376516000,"rx":1794376532079},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376514000,"rx":1794376532079},{"id":"123-V3","dir":1,"lat":22.644196,"lon":120.302893,"t":1794376512000,"rx":1794376532079},{"id":"185-FT","dir":1,"lat":22.605676,"lon":120.334109,"t":1794376502000,"rx":1794376532079},{"id":"038-V3","dir":1,"lat":22.621421,"lon":120.327498,"t":1794376536000,"rx":1794376581176},{"id":"051-V3","dir":0,"lat":22.62112,"lon":120.33171,"t":1794376532000,"rx":1794376581176},{"id":"056-V3","dir":0,"lat":22.614836,"lon":120.307036,"t":1794376535000,"rx":1794376581176},{"id":"059-V3","dir":0,"lat":22.627453,"lon":120.303979,"t":1794376534000,"rx":1794376581176},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376534000,"rx":1794376581176},{"id":"067-V3","dir":0,"lat":22.635729,"lon":120.302424,"t":1794376536000,"rx":1794376581176},{"id":"068-V3","dir":1,"lat":22.634746,"lon":120.302143,"t":1794376543000,"rx":1794376581176},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376536000,"rx":1794376581176},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376534000,"rx":1794376581176},{"id":"123-V3","dir":1,"lat":22.642508,"lon":120.302766,"t":1794376532000,"rx":1794376581176},{"id":"185-FT","dir":1,"lat":22.605796,"lon":120.33534,"t":1794376542000,"rx":1794376581176},{"id":"038-V3","dir":1,"lat":22.621858,"lon":120.328771,"t":1794376578000,"rx":1794376604245},{"id":"051-V3","dir":0,"lat":22.622685,"lon":120.33109,"t":1794376572000,"rx":1794376604245},{"id":"056-V3","dir":0,"lat":22.614761,"lon":120.306881,"t":1794376575000,"rx":1794376604245},{"id":"059-V3","dir":0,"lat":22.627561,"lon":120.302143,"t":1794376574000,"rx":1794376604245},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376574000,"rx":1794376604245},{"id":"067-V3","dir":0,"lat":22.638298,"lon":120.303498,"t":1794376576000,"rx":1794376604245},{"id":"068-V3","dir":1,"lat":22.63173,"lon":120.301869,"t":1794376573000,"rx":1794376604245},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376576000,"rx":1794376604245},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376574000,"rx":1794376604245},{"id":"123-V3","dir":1,"lat":22.639926,"lon":120.301734,"t":1794376572000,"rx":1794376604245},{"id":"185-FT","dir":0,"lat":22.606038,"lon":120.338108,"t":1794376571000,"rx":1794376604245},{"id":"038-V3","dir":1,"lat":22.622424,"lon":120.330439,"t":1794376596000,"rx":1794376629045},{"id":"051-V3","dir":0,"lat":22.622153,"lon":120.329521,"t":1794376592000,"rx":1794376629045},{"id":"056-V3","dir":0,"lat":22.614329,"lon":120.306048,"t":1794376595000,"rx":1794376629045},{"id":"059-V3","dir":0,"lat":22.627561,"lon":120.30154,"t":1794376594000,"rx":1794376629045},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376594000,"rx":1794376629045},{"id":"067-V3","dir":0,"lat":22.639118,"lon":120.303548,"t":1794376596000,"rx":1794376629045},{"id":"068-V3","dir":1,"lat":22.63163,"lon":120.301863,"t":1794376593000,"rx":1794376629045},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376596000,"rx":1794376629045},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376594000,"rx":1794376629045},{"id":"123-V3","dir":1,"lat":22.639604,"lon":120.301625,"t":1794376592000,"rx":1794376629045},{"id":"185-FT","dir":0,"lat":22.606369,"lon":120.338624,"t":1794376602000,"rx":1794376629045},{"id":"038-V3","dir":1,"lat":22.620693,"lon":120.331704,"t":1794376636000,"rx":1794376652937},{"id":"051-V3","dir":0,"lat":22.621949,"lon":120.32881,"t":1794376635000,"rx":1794376652937},{"id":"056-V3","dir":0,"lat":22.614166,"lon":120.305508,"t":1794376639000,"rx":1794376652937},{"id":"059-V3","dir":0,"lat":22.628869,"lon":120.301723,"t":1794376634000,"rx":1794376652937},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376634000,"rx":1794376652937},{"id":"067-V3","dir":0,"lat":22.639118,"lon":120.303548,"t":1794376636000,"rx":1794376652937},{"id":"068-V3","dir":1,"lat":22.63163,"lon":120.301863,"t":1794376633000,"rx":1794376652937},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376636000,"rx":1794376652937},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376634000,"rx":1794376652937},{"id":"123-V3","dir":1,"lat":22.639604,"lon":120.301625,"t":1794376632000,"rx":1794376652937},{"id":"185-FT","dir":0,"lat":22.606176,"lon":120.338686,"t":1794376622000,"rx":1794376652937},{"id":"038-V3","dir":1,"lat":22.620291,"lon":120.331751,"t":1794376656000,"rx":1794376700181},{"id":"051-V3","dir":0,"lat":22.621356,"lon":120.327076,"t":1794376657000,"rx":1794376700181},{"id":"056-V3","dir":0,"lat":22.614119,"lon":120.304946,"t":1794376655000,"rx":1794376700181},{"id":"059-V3","dir":0,"lat":22.629576,"lon":120.301796,"t":1794376669000,"rx":1794376700181},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376654000,"rx":1794376700181},{"id":"067-V3","dir":0,"lat":22.639189,"lon":120.303546,"t":1794376657000,"rx":1794376700181},{"id":"068-V3","dir":1,"lat":22.63163,"lon":120.301863,"t":1794376653000,"rx":1794376700181},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376656000,"rx":1794376700181},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376654000,"rx":1794376700181},{"id":"123-V3","dir":1,"lat":22.639604,"lon":120.301625,"t":1794376652000,"rx":1794376700181},{"id":"185-FT","dir":0,"lat":22.606211,"lon":120.338626,"t":1794376662000,"rx":1794376700181},{"id":"038-V3","dir":1,"lat":22.61984,"lon":120.331819,"t":1794376696000,"rx":1794376724129},{"id":"051-V3","dir":0,"lat":22.621349,"lon":120.327034,"t":1794376692000,"rx":1794376724129},{"id":"056-V3","dir":0,"lat":22.614119,"lon":120.304946,"t":1794376695000,"rx":1794376724129},{"id":"059-V3","dir":0,"lat":22.631969,"lon":120.302066,"t":1794376694000,"rx":1794376724129},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376694000,"rx":1794376724129},{"id":"067-V3","dir":0,"lat":22.641835,"lon":120.302903,"t":1794376696000,"rx":1794376724129},{"id":"068-V3","dir":1,"lat":22.630406,"lon":120.301681,"t":1794376693000,"rx":1794376724129},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376696000,"rx":1794376724129},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376694000,"rx":1794376724129},{"id":"123-V3","dir":1,"lat":22.637188,"lon":120.302308,"t":1794376698000,"rx":1794376724129},{"id":"185-FT","dir":0,"lat":22.606206,"lon":120.338516,"t":1794376690000,"rx":1794376724129},{"id":"038-V3","dir":1,"lat":22.617536,"lon":120.332083,"t":1794376723000,"rx":1794376746588},{"id":"051-V3","dir":0,"lat":22.621349,"lon":120.327034,"t":1794376712000,"rx":1794376746588},{"id":"056-V3","dir":0,"lat":22.614119,"lon":120.304946,"t":1794376715000,"rx":1794376746588},{"id":"059-V3","dir":0,"lat":22.634121,"lon":120.302215,"t":1794376716000,"rx":1794376746588},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376714000,"rx":1794376746588},{"id":"067-V3","dir":0,"lat":22.644121,"lon":120.30306,"t":1794376716000,"rx":1794376746588},{"id":"068-V3","dir":1,"lat":22.630406,"lon":120.301681,"t":1794376713000,"rx":1794376746588},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376716000,"rx":1794376746588},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376714000,"rx":1794376746588},{"id":"123-V3","dir":1,"lat":22.635259,"lon":120.302111,"t":1794376723000,"rx":1794376746588},{"id":"038-V3","dir":1,"lat":22.613483,"lon":120.332594,"t":1794376759000,"rx":1794376771004},{"id":"051-V3","dir":0,"lat":22.620993,"lon":120.325954,"t":1794376752000,"rx":1794376771004},{"id":"056-V3","dir":0,"lat":22.614086,"lon":120.304771,"t":1794376755000,"rx":1794376771004},{"id":"059-V3","dir":0,"lat":22.634266,"lon":120.302216,"t":1794376754000,"rx":1794376771004},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376754000,"rx":1794376771004},{"id":"067-V3","dir":0,"lat":22.645649,"lon":120.301013,"t":1794376756000,"rx":1794376771004},{"id":"068-V3","dir":1,"lat":22.629449,"lon":120.301691,"t":1794376753000,"rx":1794376771004},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376756000,"rx":1794376771004},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376754000,"rx":1794376771004},{"id":"123-V3","dir":1,"lat":22.634946,"lon":120.302098,"t":1794376752000,"rx":1794376771004},{"id":"038-V3","dir":1,"lat":22.613305,"lon":120.332618,"t":1794376776000,"rx":1794376809416},{"id":"051-V3","dir":0,"lat":22.620093,"lon":120.323241,"t":1794376788000,"rx":1794376809416},{"id":"056-V3","dir":0,"lat":22.613475,"lon":120.302981,"t":1794376777000,"rx":1794376809416},{"id":"059-V3","dir":0,"lat":22.634266,"lon":120.302216,"t":1794376774000,"rx":1794376809416},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376774000,"rx":1794376809416},{"id":"067-V3","dir":0,"lat":22.646028,"lon":120.299851,"t":1794376776000,"rx":1794376809416},{"id":"068-V3","dir":1,"lat":22.629449,"lon":120.301691,"t":1794376773000,"rx":1794376809416},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376776000,"rx":1794376809416},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376774000,"rx":1794376809416},{"id":"123-V3","dir":1,"lat":22.634946,"lon":120.302098,"t":1794376772000,"rx":1794376809416},{"id":"038-V3","dir":1,"lat":22.612989,"lon":120.332624,"t":1794376816000,"rx":1794376833257},{"id":"051-V3","dir":0,"lat":22.619791,"lon":120.322298,"t":1794376812000,"rx":1794376833257},{"id":"056-V3","dir":0,"lat":22.612548,"lon":120.300503,"t":1794376815000,"rx":1794376833257},{"id":"058-V3","dir":0,"lat":22.606188,"lon":120.338646,"t":1794376819000,"rx":1794376833257},{"id":"059-V3","dir":0,"lat":22.634464,"lon":120.302238,"t":1794376814000,"rx":1794376833257},{"id":"063-V3","dir":0,"lat":22.671363,"lon":120.30095,"t":1794376814000,"rx":1794376833257},{"id":"067-V3","dir":0,"lat":22.646603,"lon":120.299906,"t":1794376816000,"rx":1794376833257},{"id":"068-V3","dir":1,"lat":22.629331,"lon":120.301678,"t":1794376813000,"rx":1794376833257},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376816000,"rx":1794376833257},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376814000,"rx":1794376833257},{"id":"123-V3","dir":1,"lat":22.634769,"lon":120.302049,"t":1794376816000,"rx":1794376833257},{"id":"038-V3","dir":1,"lat":22.612704,"lon":120.332648,"t":1794376836000,"rx":1794376879594},{"id":"051-V3","dir":0,"lat":22.619791,"lon":120.322298,"t":1794376832000,"rx":1794376879594},{"id":"056-V3","dir":0,"lat":22.612548,"lon":120.300503,"t":1794376835000,"rx":1794376879594},{"id":"058-V3","dir":0,"lat":22.606075,"lon":120.338421,"t":1794376839000,"rx":1794376879594},{"id":"059-V3","dir":0,"lat":22.636296,"lon":120.302374,"t":1794376834000,"rx":1794376879594},{"id":"063-V3","dir":0,"lat":22.670956,"lon":120.301148,"t":1794376834000,"rx":1794376879594},{"id":"067-V3","dir":0,"lat":22.647378,"lon":120.299959,"t":1794376836000,"rx":1794376879594},{"id":"068-V3","dir":1,"lat":22.62809,"lon":120.301543,"t":1794376833000,"rx":1794376879594},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376836000,"rx":1794376879594},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376834000,"rx":1794376879594},{"id":"123-V3","dir":1,"lat":22.633241,"lon":120.301963,"t":1794376834000,"rx":1794376879594},{"id":"038-V3","dir":1,"lat":22.609393,"lon":120.333021,"t":1794376876000,"rx":1794376903189},{"id":"051-V3","dir":0,"lat":22.619263,"lon":120.320745,"t":1794376879000,"rx":1794376903189},{"id":"056-V3","dir":0,"lat":22.612114,"lon":120.299385,"t":1794376875000,"rx":1794376903189},{"id":"058-V3","dir":0,"lat":22.606081,"lon":120.338329,"t":1794376879000,"rx":1794376903189},{"id":"059-V3","dir":0,"lat":22.639413,"lon":120.303476,"t":1794376874000,"rx":1794376903189},{"id":"063-V3","dir":0,"lat":22.670801,"lon":120.301676,"t":1794376874000,"rx":1794376903189},{"id":"067-V3","dir":0,"lat":22.647761,"lon":120.299994,"t":1794376876000,"rx":1794376903189},{"id":"068-V3","dir":1,"lat":22.627388,"lon":120.301493,"t":1794376873000,"rx":1794376903189},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376876000,"rx":1794376903189},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376874000,"rx":1794376903189},{"id":"123-V3","dir":1,"lat":22.628951,"lon":120.301609,"t":1794376872000,"rx":1794376903189},{"id":"038-V3","dir":1,"lat":22.609341,"lon":120.333026,"t":1794376896000,"rx":1794376927482},{"id":"051-V3","dir":0,"lat":22.61897,"lon":120.319834,"t":1794376892000,"rx":1794376927482},{"id":"056-V3","dir":0,"lat":22.612363,"lon":120.298518,"t":1794376895000,"rx":1794376927482},{"id":"058-V3","dir":0,"lat":22.605953,"lon":120.33669,"t":1794376899000,"rx":1794376927482},{"id":"059-V3","dir":0,"lat":22.640216,"lon":120.303561,"t":1794376894000,"rx":1794376927482},{"id":"063-V3","dir":1,"lat":22.670539,"lon":120.302781,"t":1794376894000,"rx":1794376927482},{"id":"067-V3","dir":0,"lat":22.648503,"lon":120.300061,"t":1794376896000,"rx":1794376927482},{"id":"068-V3","dir":1,"lat":22.627191,"lon":120.303886,"t":1794376905000,"rx":1794376927482},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376896000,"rx":1794376927482},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376894000,"rx":1794376927482},{"id":"123-V3","dir":1,"lat":22.627708,"lon":120.301501,"t":1794376892000,"rx":1794376927482},{"id":"038-V3","dir":1,"lat":22.609341,"lon":120.333026,"t":1794376916000,"rx":1794376950347},{"id":"051-V3","dir":0,"lat":22.618743,"lon":120.319194,"t":1794376919000,"rx":1794376950347},{"id":"056-V3","dir":0,"lat":22.612651,"lon":120.298433,"t":1794376915000,"rx":1794376950347},{"id":"058-V3","dir":0,"lat":22.605765,"lon":120.334594,"t":1794376919000,"rx":1794376950347},{"id":"059-V3","dir":0,"lat":22.640216,"lon":120.303561,"t":1794376914000,"rx":1794376950347},{"id":"063-V3","dir":1,"lat":22.67012,"lon":120.303308,"t":1794376914000,"rx":1794376950347},{"id":"067-V3","dir":0,"lat":22.650118,"lon":120.300213,"t":1794376916000,"rx":1794376950347},{"id":"068-V3","dir":1,"lat":22.62631,"lon":120.304315,"t":1794376927000,"rx":1794376950347},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376916000,"rx":1794376950347},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376914000,"rx":1794376950347},{"id":"123-V3","dir":1,"lat":22.627708,"lon":120.301501,"t":1794376912000,"rx":1794376950347},{"id":"038-V3","dir":1,"lat":22.606503,"lon":120.333423,"t":1794376958000,"rx":1794376989075},{"id":"051-V3","dir":0,"lat":22.618294,"lon":120.317258,"t":1794376952000,"rx":1794376989075},{"id":"056-V3","dir":0,"lat":22.614004,"lon":120.297876,"t":1794376955000,"rx":1794376989075},{"id":"058-V3","dir":0,"lat":22.60573,"lon":120.334008,"t":1794376959000,"rx":1794376989075},{"id":"059-V3","dir":0,"lat":22.640216,"lon":120.303561,"t":1794376954000,"rx":1794376989075},{"id":"063-V3","dir":1,"lat":22.668514,"lon":120.303318,"t":1794376954000,"rx":1794376989075},{"id":"067-V3","dir":0,"lat":22.650376,"lon":120.300229,"t":1794376956000,"rx":1794376989075},{"id":"068-V3","dir":1,"lat":22.624714,"lon":120.304791,"t":1794376953000,"rx":1794376989075},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376956000,"rx":1794376989075},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376954000,"rx":1794376989075},{"id":"123-V3","dir":1,"lat":22.627638,"lon":120.30149,"t":1794376952000,"rx":1794376989075},{"id":"038-V3","dir":1,"lat":22.605693,"lon":120.33387,"t":1794376976000,"rx":1794377012158},{"id":"051-V3","dir":0,"lat":22.618023,"lon":120.316633,"t":1794376976000,"rx":1794377012158},{"id":"056-V3","dir":0,"lat":22.614571,"lon":120.297624,"t":1794376980000,"rx":1794377012158},{"id":"058-V3","dir":0,"lat":22.60575,"lon":120.333664,"t":1794376988000,"rx":1794377012158},{"id":"059-V3","dir":0,"lat":22.641038,"lon":120.303235,"t":1794376974000,"rx":1794377012158},{"id":"063-V3","dir":1,"lat":22.667491,"lon":120.303296,"t":1794376974000,"rx":1794377012158},{"id":"067-V3","dir":0,"lat":22.651178,"lon":120.300796,"t":1794376976000,"rx":1794377012158},{"id":"068-V3","dir":1,"lat":22.624481,"lon":120.304853,"t":1794376973000,"rx":1794377012158},{"id":"111-V3","dir":0,"lat":22.606411,"lon":120.338323,"t":1794376976000,"rx":1794377012158},{"id":"120-V3","dir":0,"lat":22.606606,"lon":120.338488,"t":1794376974000,"rx":1794377012158},{"id":"123-V3","dir":1,"lat":22.627516,"lon":120.301484,"t":1794376972000,"rx":1794377012158},{"id":"038-V3","dir":1,"lat":22.605816,"lon":120.335198,"t":1794377016000,"rx":1794377037592},{"id":"051-V3","dir":0,"lat":22.617913,"lon":120.31632,"t":1794377012000,"rx":1794377037592},{"id":"056-V3","dir":0,"lat":22.615626,"lon":120.297168,"t":1794377015000,"rx":1794377037592},{"id":"058-V3","dir":0,"lat":22.607928,"lon":120.333306,"t":1794377019000,"rx":1794377037592},{"id":"059-V3","dir":0,"lat":22.644825,"lon":120.303049,"t":1794377014000,"rx":1794377037592},{"id":"063-V3","dir":1,"lat":22.664918,"lon":120.303264,"t":1794377014000,"rx":1794377037592},{"id":"067-V3","dir":0,"lat":22.651523,"lon":120.302194,"t":1794377016000,"rx":1794377037592},{"id":"068-V3","dir":1,"lat":22.624095,"lon":120.303788,"t":1794377013000,"rx":1794377037592},{"id":"111-V3","dir":0,"lat":22.606321,"lon":120.338626,"t":1794377016000,"rx":1794377037592},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377014000,"rx":1794377037592},{"id":"123-V3","dir":1,"lat":22.627516,"lon":120.301484,"t":1794377012000,"rx":1794377037592},{"id":"038-V3","dir":0,"lat":22.606,"lon":120.338071,"t":1794377041000,"rx":1794377070377},{"id":"051-V3","dir":0,"lat":22.617326,"lon":120.314551,"t":1794377048000,"rx":1794377070377},{"id":"056-V3","dir":0,"lat":22.616173,"lon":120.296936,"t":1794377042000,"rx":1794377070377},{"id":"058-V3","dir":0,"lat":22.608029,"lon":120.333299,"t":1794377048000,"rx":1794377070377},{"id":"059-V3","dir":0,"lat":22.645595,"lon":120.301953,"t":1794377048000,"rx":1794377070377},{"id":"063-V3","dir":1,"lat":22.664425,"lon":120.303206,"t":1794377034000,"rx":1794377070377},{"id":"067-V3","dir":0,"lat":22.651665,"lon":120.302473,"t":1794377036000,"rx":1794377070377},{"id":"068-V3","dir":1,"lat":22.623601,"lon":120.302286,"t":1794377045000,"rx":1794377070377},{"id":"111-V3","dir":0,"lat":22.606353,"lon":120.338634,"t":1794377036000,"rx":1794377070377},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377034000,"rx":1794377070377},{"id":"123-V3","dir":1,"lat":22.627183,"lon":120.303908,"t":1794377049000,"rx":1794377070377},{"id":"038-V3","dir":0,"lat":22.606453,"lon":120.338558,"t":1794377076000,"rx":1794377109397},{"id":"051-V3","dir":0,"lat":22.616291,"lon":120.311496,"t":1794377077000,"rx":1794377109397},{"id":"056-V3","dir":0,"lat":22.618078,"lon":120.296189,"t":1794377075000,"rx":1794377109397},{"id":"058-V3","dir":0,"lat":22.609954,"lon":120.333076,"t":1794377079000,"rx":1794377109397},{"id":"059-V3","dir":0,"lat":22.645633,"lon":120.301355,"t":1794377074000,"rx":1794377109397},{"id":"063-V3","dir":1,"lat":22.663236,"lon":120.303161,"t":1794377074000,"rx":1794377109397},{"id":"067-V3","dir":0,"lat":22.651791,"lon":120.303021,"t":1794377076000,"rx":1794377109397},{"id":"068-V3","dir":1,"lat":22.623235,"lon":120.301109,"t":1794377079000,"rx":1794377109397},{"id":"111-V3","dir":0,"lat":22.606353,"lon":120.338634,"t":1794377076000,"rx":1794377109397},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377074000,"rx":1794377109397},{"id":"123-V3","dir":1,"lat":22.626844,"lon":120.304239,"t":1794377072000,"rx":1794377109397},{"id":"038-V3","dir":0,"lat":22.605723,"lon":120.338506,"t":1794377096000,"rx":1794377133569},{"id":"051-V3","dir":0,"lat":22.615501,"lon":120.309539,"t":1794377095000,"rx":1794377133569},{"id":"056-V3","dir":0,"lat":22.619271,"lon":120.295831,"t":1794377095000,"rx":1794377133569},{"id":"058-V3","dir":0,"lat":22.611026,"lon":120.332916,"t":1794377099000,"rx":1794377133569},{"id":"059-V3","dir":0,"lat":22.645713,"lon":120.300198,"t":1794377094000,"rx":1794377133569},{"id":"063-V3","dir":1,"lat":22.663236,"lon":120.303161,"t":1794377094000,"rx":1794377133569},{"id":"067-V3","dir":0,"lat":22.652103,"lon":120.303758,"t":1794377096000,"rx":1794377133569},{"id":"068-V3","dir":1,"lat":22.62301,"lon":120.300323,"t":1794377093000,"rx":1794377133569},{"id":"111-V3","dir":0,"lat":22.606353,"lon":120.338634,"t":1794377096000,"rx":1794377133569},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377094000,"rx":1794377133569},{"id":"123-V3","dir":1,"lat":22.625686,"lon":120.304445,"t":1794377092000,"rx":1794377133569},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377136000,"rx":1794377155188},{"id":"051-V3","dir":0,"lat":22.615348,"lon":120.308823,"t":1794377132000,"rx":1794377155188},{"id":"056-V3","dir":0,"lat":22.619428,"lon":120.295761,"t":1794377135000,"rx":1794377155188},{"id":"058-V3","dir":0,"lat":22.612188,"lon":120.332759,"t":1794377139000,"rx":1794377155188},{"id":"059-V3","dir":0,"lat":22.645721,"lon":120.299931,"t":1794377134000,"rx":1794377155188},{"id":"063-V3","dir":1,"lat":22.661193,"lon":120.303091,"t":1794377137000,"rx":1794377155188},{"id":"067-V3","dir":0,"lat":22.655143,"lon":120.303608,"t":1794377136000,"rx":1794377155188},{"id":"068-V3","dir":1,"lat":22.622734,"lon":120.299633,"t":1794377133000,"rx":1794377155188},{"id":"111-V3","dir":0,"lat":22.606353,"lon":120.338634,"t":1794377136000,"rx":1794377155188},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377134000,"rx":1794377155188},{"id":"123-V3","dir":1,"lat":22.624265,"lon":120.304351,"t":1794377132000,"rx":1794377155188},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377156000,"rx":1794377180893},{"id":"051-V3","dir":0,"lat":22.615348,"lon":120.308823,"t":1794377152000,"rx":1794377180893},{"id":"056-V3","dir":0,"lat":22.619428,"lon":120.295761,"t":1794377155000,"rx":1794377180893},{"id":"058-V3","dir":0,"lat":22.612188,"lon":120.332759,"t":1794377159000,"rx":1794377180893},{"id":"059-V3","dir":0,"lat":22.645721,"lon":120.299931,"t":1794377154000,"rx":1794377180893},{"id":"063-V3","dir":1,"lat":22.659678,"lon":120.303038,"t":1794377154000,"rx":1794377180893},{"id":"067-V3","dir":0,"lat":22.656539,"lon":120.303314,"t":1794377161000,"rx":1794377180893},{"id":"068-V3","dir":1,"lat":22.62263,"lon":120.29934,"t":1794377153000,"rx":1794377180893},{"id":"111-V3","dir":0,"lat":22.606353,"lon":120.338634,"t":1794377156000,"rx":1794377180893},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377154000,"rx":1794377180893},{"id":"123-V3","dir":1,"lat":22.623864,"lon":120.303228,"t":1794377166000,"rx":1794377180893},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377196000,"rx":1794377229086},{"id":"051-V3","dir":0,"lat":22.614761,"lon":120.306906,"t":1794377197000,"rx":1794377229086},{"id":"056-V3","dir":0,"lat":22.620936,"lon":120.295098,"t":1794377195000,"rx":1794377229086},{"id":"058-V3","dir":0,"lat":22.613433,"lon":120.332636,"t":1794377199000,"rx":1794377229086},{"id":"059-V3","dir":0,"lat":22.646386,"lon":120.299846,"t":1794377194000,"rx":1794377229086},{"id":"063-V3","dir":1,"lat":22.657143,"lon":120.30301,"t":1794377194000,"rx":1794377229086},{"id":"067-V3","dir":0,"lat":22.657103,"lon":120.303274,"t":1794377196000,"rx":1794377229086},{"id":"068-V3","dir":1,"lat":22.622595,"lon":120.299218,"t":1794377193000,"rx":1794377229086},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377196000,"rx":1794377229086},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377194000,"rx":1794377229086},{"id":"123-V3","dir":1,"lat":22.623426,"lon":120.301716,"t":1794377192000,"rx":1794377229086},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377216000,"rx":1794377253027},{"id":"051-V3","dir":0,"lat":22.614431,"lon":120.305994,"t":1794377212000,"rx":1794377253027},{"id":"056-V3","dir":0,"lat":22.621366,"lon":120.296081,"t":1794377215000,"rx":1794377253027},{"id":"058-V3","dir":0,"lat":22.614758,"lon":120.332464,"t":1794377219000,"rx":1794377253027},{"id":"059-V3","dir":0,"lat":22.64701,"lon":120.299896,"t":1794377216000,"rx":1794377253027},{"id":"063-V3","dir":1,"lat":22.656873,"lon":120.303055,"t":1794377214000,"rx":1794377253027},{"id":"067-V3","dir":0,"lat":22.657873,"lon":120.30319,"t":1794377216000,"rx":1794377253027},{"id":"068-V3","dir":1,"lat":22.622206,"lon":120.298033,"t":1794377215000,"rx":1794377253027},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377216000,"rx":1794377253027},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377214000,"rx":1794377253027},{"id":"123-V3","dir":1,"lat":22.623243,"lon":120.301124,"t":1794377226000,"rx":1794377253027},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377256000,"rx":1794377277200},{"id":"051-V3","dir":0,"lat":22.614243,"lon":120.305468,"t":1794377252000,"rx":1794377277200},{"id":"056-V3","dir":0,"lat":22.62179,"lon":120.297241,"t":1794377255000,"rx":1794377277200},{"id":"058-V3","dir":0,"lat":22.615735,"lon":120.332346,"t":1794377259000,"rx":1794377277200},{"id":"059-V3","dir":0,"lat":22.647725,"lon":120.299968,"t":1794377254000,"rx":1794377277200},{"id":"063-V3","dir":1,"lat":22.65659,"lon":120.303129,"t":1794377254000,"rx":1794377277200},{"id":"067-V3","dir":0,"lat":22.657988,"lon":120.3032,"t":1794377256000,"rx":1794377277200},{"id":"068-V3","dir":1,"lat":22.621898,"lon":120.29721,"t":1794377253000,"rx":1794377277200},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377256000,"rx":1794377277200},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377254000,"rx":1794377277200},{"id":"123-V3","dir":1,"lat":22.62262,"lon":120.299303,"t":1794377252000,"rx":1794377277200},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377276000,"rx":1794377323219},{"id":"051-V3","dir":0,"lat":22.6141,"lon":120.304958,"t":1794377272000,"rx":1794377323219},{"id":"056-V3","dir":0,"lat":22.621866,"lon":120.297671,"t":1794377275000,"rx":1794377323219},{"id":"058-V3","dir":0,"lat":22.615735,"lon":120.332346,"t":1794377279000,"rx":1794377323219},{"id":"059-V3","dir":0,"lat":22.648831,"lon":120.300051,"t":1794377274000,"rx":1794377323219},{"id":"063-V3","dir":1,"lat":22.656313,"lon":120.303191,"t":1794377286000,"rx":1794377323219},{"id":"067-V3","dir":0,"lat":22.660203,"lon":120.303188,"t":1794377289000,"rx":1794377323219},{"id":"068-V3","dir":1,"lat":22.62141,"lon":120.295793,"t":1794377273000,"rx":1794377323219},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377276000,"rx":1794377323219},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377274000,"rx":1794377323219},{"id":"123-V3","dir":1,"lat":22.622621,"lon":120.299306,"t":1794377272000,"rx":1794377323219},{"id":"038-V3","dir":0,"lat":22.604651,"lon":120.33668,"t":1794377316000,"rx":1794377347811},{"id":"051-V3","dir":0,"lat":22.6141,"lon":120.304958,"t":1794377312000,"rx":1794377347811},{"id":"056-V3","dir":0,"lat":22.621948,"lon":120.297891,"t":1794377315000,"rx":1794377347811},{"id":"058-V3","dir":0,"lat":22.618728,"lon":120.331946,"t":1794377319000,"rx":1794377347811},{"id":"059-V3","dir":0,"lat":22.650296,"lon":120.300181,"t":1794377314000,"rx":1794377347811},{"id":"063-V3","dir":1,"lat":22.654126,"lon":120.303578,"t":1794377314000,"rx":1794377347811},{"id":"067-V3","dir":0,"lat":22.662831,"lon":120.303283,"t":1794377316000,"rx":1794377347811},{"id":"068-V3","dir":1,"lat":22.62123,"lon":120.295186,"t":1794377313000,"rx":1794377347811},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377316000,"rx":1794377347811},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377314000,"rx":1794377347811},{"id":"123-V3","dir":1,"lat":22.622621,"lon":120.299306,"t":1794377312000,"rx":1794377347811},{"id":"038-V3","dir":0,"lat":22.604668,"lon":120.336581,"t":1794377336000,"rx":1794377362313},{"id":"051-V3","dir":0,"lat":22.6141,"lon":120.304958,"t":1794377332000,"rx":1794377362313},{"id":"056-V3","dir":0,"lat":22.622099,"lon":120.29823,"t":1794377335000,"rx":1794377362313},{"id":"058-V3","dir":0,"lat":22.619806,"lon":120.331833,"t":1794377339000,"rx":1794377362313},{"id":"059-V3","dir":0,"lat":22.650503,"lon":120.300174,"t":1794377334000,"rx":1794377362313},{"id":"063-V3","dir":1,"lat":22.652281,"lon":120.303568,"t":1794377334000,"rx":1794377362313},{"id":"067-V3","dir":0,"lat":22.664511,"lon":120.30334,"t":1794377336000,"rx":1794377362313},{"id":"068-V3","dir":1,"lat":22.62123,"lon":120.295186,"t":1794377333000,"rx":1794377362313},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377336000,"rx":1794377362313},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377334000,"rx":1794377362313},{"id":"123-V3","dir":1,"lat":22.622621,"lon":120.299306,"t":1794377332000,"rx":1794377362313},{"id":"038-V3","dir":0,"lat":22.604668,"lon":120.336581,"t":1794377376000,"rx":1794377407256},{"id":"051-V3","dir":0,"lat":22.613414,"lon":120.302925,"t":1794377375000,"rx":1794377407256},{"id":"056-V3","dir":0,"lat":22.622885,"lon":120.300573,"t":1794377379000,"rx":1794377407256},{"id":"058-V3","dir":0,"lat":22.619876,"lon":120.331824,"t":1794377379000,"rx":1794377407256},{"id":"059-V3","dir":0,"lat":22.651416,"lon":120.30177,"t":1794377379000,"rx":1794377407256},{"id":"063-V3","dir":1,"lat":22.652239,"lon":120.303569,"t":1794377374000,"rx":1794377407256},{"id":"067-V3","dir":0,"lat":22.665206,"lon":120.303375,"t":1794377376000,"rx":1794377407256},{"id":"068-V3","dir":1,"lat":22.621081,"lon":120.294948,"t":1794377373000,"rx":1794377407256},{"id":"111-V3","dir":0,"lat":22.60642,"lon":120.338713,"t":1794377376000,"rx":1794377407256},{"id":"120-V3","dir":0,"lat":22.606629,"lon":120.338581,"t":1794377374000,"rx":1794377407256},{"id":"123-V3","dir":1,"lat":22.621898,"lon":120.297361,"t":1794377372000,"rx":1794377407256}],"boardingEta":[{"t":1794376163693,"eta":150,"arriving":false},{"t":1794376170785,"eta":150,"arriving":false},{"t":1794376185566,"eta":127,"arriving":false},{"t":1794376195395,"eta":127,"arriving":false},{"t":1794376210922,"eta":141,"arriving":false},{"t":1794376218989,"eta":141,"arriving":false},{"t":1794376234963,"eta":161,"arriving":false},{"t":1794376242899,"eta":161,"arriving":false},{"t":1794376257718,"eta":137,"arriving":false},{"t":1794376266920,"eta":137,"arriving":false},{"t":1794376280943,"eta":91,"arriving":false},{"t":1794376293308,"eta":91,"arriving":false},{"t":1794376303386,"eta":70,"arriving":false},{"t":1794376315250,"eta":70,"arriving":false},{"t":1794376329068,"eta":24,"arriving":false},{"t":1794376339679,"eta":24,"arriving":false},{"t":1794376353152,"eta":-1,"arriving":false},{"t":1794376364015,"eta":-1,"arriving":false},{"t":1794376377495,"eta":17,"arriving":false},{"t":1794376387892,"eta":17,"arriving":false},{"t":1794376400173,"eta":-1,"arriving":true},{"t":1794376413923,"eta":-1,"arriving":true},{"t":1794376423944,"eta":-1,"arriving":false},{"t":1794376436583,"eta":-1,"arriving":false},{"t":1794376447758,"eta":null,"arriving":false},{"t":1794376462688,"eta":null,"arriving":false},{"t":1794376470669,"eta":null,"arriving":false},{"t":1794376486042,"eta":null,"arriving":false},{"t":1794376494567,"eta":null,"arriving":false},{"t":1794376510540,"eta":null,"arriving":false},{"t":1794376518657,"eta":null,"arriving":false},{"t":1794376533733,"eta":null,"arriving":false},{"t":1794376542498,"eta":null,"arriving":false},{"t":1794376558066,"eta":null,"arriving":false},{"t":1794376566529,"eta":null,"arriving":false},{"t":1794376583036,"eta":null,"arriving":false},{"t":1794376590852,"eta":null,"arriving":false},{"t":1794376606060,"eta":null,"arriving":false},{"t":1794376614538,"eta":null,"arriving":false},{"t":1794376631050,"eta":null,"arriving":false},{"t":1794376638601,"eta":null,"arriving":false},{"t":1794376654789,"eta":null,"arriving":false},{"t":1794376662715,"eta":null,"arriving":false},{"t":1794376678137,"eta":null,"arriving":false},{"t":1794376686707,"eta":null,"arriving":false},{"t":1794376701601,"eta":null,"arriving":false},{"t":1794376710635,"eta":null,"arriving":false},{"t":1794376725946,"eta":null,"arriving":false},{"t":1794376734640,"eta":null,"arriving":false},{"t":1794376748432,"eta":null,"arriving":false},{"t":1794376760440,"eta":null,"arriving":false},{"t":1794376773065,"eta":null,"arriving":false},{"t":1794376787810,"eta":null,"arriving":false},{"t":1794376794892,"eta":null,"arriving":false},{"t":1794376811581,"eta":null,"arriving":false},{"t":1794376818676,"eta":null,"arriving":false},{"t":1794376835317,"eta":null,"arriving":false},{"t":1794376842926,"eta":null,"arriving":false},{"t":1794376857517,"eta":null,"arriving":false},{"t":1794376867718,"eta":null,"arriving":false},{"t":1794376881564,"eta":null,"arriving":false},{"t":1794376891608,"eta":null,"arriving":false},{"t":1794376904823,"eta":null,"arriving":false},{"t":1794376915664,"eta":null,"arriving":false},{"t":1794376929445,"eta":null,"arriving":false},{"t":1794376939768,"eta":null,"arriving":false},{"t":1794376952162,"eta":null,"arriving":false},{"t":1794376965858,"eta":146,"arriving":false},{"t":1794376974751,"eta":146,"arriving":false},{"t":1794376991088,"eta":123,"arriving":false},{"t":1794376998672,"eta":123,"arriving":false},{"t":1794377014080,"eta":139,"arriving":false},{"t":1794377022957,"eta":139,"arriving":false},{"t":1794377039542,"eta":153,"arriving":false},{"t":1794377047432,"eta":153,"arriving":false},{"t":1794377063068,"eta":132,"arriving":false},{"t":1794377071026,"eta":132,"arriving":false},{"t":1794377087501,"eta":86,"arriving":false},{"t":1794377095040,"eta":86,"arriving":false},{"t":1794377111998,"eta":60,"arriving":false},{"t":1794377119064,"eta":60,"arriving":false},{"t":1794377136046,"eta":79,"arriving":false},{"t":1794377142471,"eta":79,"arriving":false},{"t":1794377157213,"eta":38,"arriving":false},{"t":1794377166632,"eta":38,"arriving":false},{"t":1794377183321,"eta":11,"arriving":false},{"t":1794377191087,"eta":11,"arriving":false},{"t":1794377207416,"eta":27,"arriving":false},{"t":1794377214790,"eta":27,"arriving":false},{"t":1794377230944,"eta":3,"arriving":false},{"t":1794377239022,"eta":3,"arriving":false},{"t":1794377254989,"eta":-1,"arriving":true},{"t":1794377263033,"eta":-1,"arriving":true},{"t":1794377279609,"eta":null,"arriving":false},{"t":1794377287475,"eta":null,"arriving":false},{"t":1794377300197,"eta":null,"arriving":false},{"t":1794377313305,"eta":null,"arriving":false},{"t":1794377324991,"eta":null,"arriving":false},{"t":1794377335180,"eta":null,"arriving":false},{"t":1794377349476,"eta":null,"arriving":false},{"t":1794377364211,"eta":null,"arriving":false},{"t":1794377370793,"eta":null,"arriving":false},{"t":1794377385536,"eta":null,"arriving":false},{"t":1794377395370,"eta":null,"arriving":false},{"t":1794377409111,"eta":null,"arriving":false}],"riderFixes":[{"t":1794376163000,"lat":22.614211,"lon":120.332806,"acc":15.0,"spd":0.2},{"t":1794376168000,"lat":22.614346,"lon":120.332437,"acc":15.0,"spd":0.4},{"t":1794376173000,"lat":22.614289,"lon":120.33256,"acc":15.0,"spd":0.0},{"t":1794376178000,"lat":22.614368,"lon":120.332548,"acc":15.0,"spd":0.4},{"t":1794376183000,"lat":22.614092,"lon":120.33255,"acc":15.0,"spd":0.0},{"t":1794376188000,"lat":22.614397,"lon":120.332507,"acc":15.0,"spd":0.0},{"t":1794376193000,"lat":22.614159,"lon":120.332511,"acc":15.0,"spd":0.1},{"t":1794376198000,"lat":22.614248,"lon":120.332806,"acc":15.0,"spd":0.0},{"t":1794376203000,"lat":22.614253,"lon":120.3325,"acc":15.0,"spd":0.3},{"t":1794376208000,"lat":22.614319,"lon":120.332616,"acc":15.0,"spd":0.0},{"t":1794376213000,"lat":22.614303,"lon":120.332642,"acc":15.0,"spd":0.3},{"t":1794376218000,"lat":22.614321,"lon":120.332747,"acc":15.0,"spd":0.2},{"t":1794376223000,"lat":22.614165,"lon":120.332389,"acc":15.0,"spd":0.3},{"t":1794376228000,"lat":22.614421,"lon":120.332775,"acc":15.0,"spd":0.3},{"t":1794376233000,"lat":22.614478,"lon":120.332421,"acc":15.0,"spd":0.0},{"t":1794376238000,"lat":22.61434,"lon":120.332595,"acc":15.0,"spd":0.4},{"t":1794376243000,"lat":22.614321,"lon":120.332607,"acc":15.0,"spd":0.1},{"t":1794376248000,"lat":22.614237,"lon":120.332579,"acc":15.0,"spd":0.0},{"t":1794376253000,"lat":22.614259,"lon":120.332422,"acc":15.0,"spd":0.0},{"t":1794376258000,"lat":22.61407,"lon":120.332351,"acc":15.0,"spd":0.0},{"t":1794376263000,"lat":22.614245,"lon":120.332624,"acc":15.0,"spd":0.3},{"t":1794376268000,"lat":22.614061,"lon":120.332594,"acc":15.0,"spd":0.2},{"t":1794376273000,"lat":22.614353,"lon":120.332617,"acc":15.0,"spd":0.0},{"t":1794376278000,"lat":22.614295,"lon":120.332633,"acc":15.0,"spd":0.2},{"t":1794376283000,"lat":22.614371,"lon":120.332438,"acc":15.0,"spd":0.4},{"t":1794376288000,"lat":22.614123,"lon":120.332664,"acc":15.0,"spd":0.0},{"t":1794376293000,"lat":22.614065,"lon":120.332619,"acc":15.0,"spd":0.3},{"t":1794376298000,"lat":22.614251,"lon":120.332298,"acc":15.0,"spd":0.2},{"t":1794376303000,"lat":22.614277,"lon":120.332462,"acc":15.0,"spd":0.5},{"t":1794376308000,"lat":22.614322,"lon":120.332572,"acc":15.0,"spd":0.0},{"t":1794376313000,"lat":22.614201,"lon":120.332467,"acc":15.0,"spd":0.0},{"t":1794376318000,"lat":22.614117,"lon":120.332732,"acc":15.0,"spd":0.6},{"t":1794376323000,"lat":22.614326,"lon":120.332466,"acc":15.0,"spd":0.0},{"t":1794376328000,"lat":22.614173,"lon":120.332793,"acc":15.0,"spd":0.0},{"t":1794376333000,"lat":22.614251,"lon":120.332727,"acc":15.0,"spd":0.0},{"t":1794376338000,"lat":22.61437,"lon":120.332472,"acc":15.0,"spd":0.0},{"t":1794376343000,"lat":22.614138,"lon":120.332377,"acc":15.0,"spd":0.0},{"t":1794376348000,"lat":22.61449,"lon":120.332534,"acc":15.0,"spd":0.2},{"t":1794376353000,"lat":22.614362,"lon":120.332504,"acc":15.0,"spd":0.0},{"t":1794376358000,"lat":22.61411,"lon":120.332529,"acc":15.0,"spd":0.0},{"t":1794376363000,"lat":22.614303,"lon":120.332586,"acc":15.0,"spd":0.0},{"t":1794376368000,"lat":22.614227,"lon":120.332537,"acc":15.0,"spd":0.1},{"t":1794376373000,"lat":22.614101,"lon":120.332513,"acc":15.0,"spd":0.9},{"t":1794376378000,"lat":22.614666,"lon":120.332368,"acc":15.0,"spd":8.0},{"t":1794376383000,"lat":22.615043,"lon":120.332597,"acc":15.0,"spd":8.0},{"t":1794376388000,"lat":22.615542,"lon":120.332387,"acc":15.0,"spd":8.4},{"t":1794376393000,"lat":22.615753,"lon":120.332322,"acc":15.0,"spd":6.5},{"t":1794376398000,"lat":22.615951,"lon":120.332462,"acc":15.0,"spd":0.0},{"t":1794376403000,"lat":22.615839,"lon":120.332596,"acc":15.0,"spd":0.0},{"t":1794376408000,"lat":22.615881,"lon":120.332469,"acc":15.0,"spd":0.4},{"t":1794376413000,"lat":22.615726,"lon":120.332174,"acc":15.0,"spd":0.5},{"t":1794376418000,"lat":22.615874,"lon":120.332414,"acc":15.0,"spd":0.1},{"t":1794376423000,"lat":22.615728,"lon":120.332305,"acc":15.0,"spd":0.3},{"t":1794376428000,"lat":22.615773,"lon":120.332462,"acc":15.0,"spd":0.4},{"t":1794376433000,"lat":22.615807,"lon":120.332373,"acc":15.0,"spd":0.2},{"t":1794376438000,"lat":22.616017,"lon":120.332439,"acc":15.0,"spd":0.0},{"t":1794376443000,"lat":22.615991,"lon":120.332354,"acc":15.0,"spd":0.3},{"t":1794376448000,"lat":22.61594,"lon":120.332341,"acc":15.0,"spd":0.0},{"t":1794376453000,"lat":22.616154,"lon":120.332365,"acc":15.0,"spd":2.1},{"t":1794376458000,"lat":22.616466,"lon":120.332055,"acc":15.0,"spd":10.5},{"t":1794376463000,"lat":22.616891,"lon":120.3323,"acc":15.0,"spd":10.5},{"t":1794376468000,"lat":22.617413,"lon":120.332199,"acc":15.0,"spd":10.1},{"t":1794376473000,"lat":22.617881,"lon":120.331964,"acc":15.0,"spd":9.7},{"t":1794376478000,"lat":22.618296,"lon":120.332048,"acc":15.0,"spd":10.6},{"t":1794376483000,"lat":22.618718,"lon":120.331776,"acc":15.0,"spd":10.7},{"t":1794376488000,"lat":22.619491,"lon":120.331825,"acc":15.0,"spd":9.5},{"t":1794376493000,"lat":22.619721,"lon":120.331677,"acc":15.0,"spd":8.3},{"t":1794376498000,"lat":22.619893,"lon":120.331696,"acc":15.0,"spd":7.4},{"t":1794376503000,"lat":22.620108,"lon":120.331726,"acc":15.0,"spd":7.3},{"t":1794376508000,"lat":22.620579,"lon":120.33188,"acc":15.0,"spd":8.1},{"t":1794376513000,"lat":22.620913,"lon":120.331745,"acc":15.0,"spd":7.0},{"t":1794376518000,"lat":22.621029,"lon":120.331711,"acc":15.0,"spd":0.4},{"t":1794376523000,"lat":22.621164,"lon":120.331654,"acc":15.0,"spd":0.5},{"t":1794376528000,"lat":22.621026,"lon":120.331738,"acc":15.0,"spd":1.5},{"t":1794376533000,"lat":22.621058,"lon":120.331718,"acc":15.0,"spd":2.2},{"t":1794376538000,"lat":22.621517,"lon":120.331805,"acc":15.0,"spd":4.7},{"t":1794376543000,"lat":22.621643,"lon":120.331359,"acc":15.0,"spd":4.7},{"t":1794376548000,"lat":22.62175,"lon":120.331443,"acc":15.0,"spd":5.3},{"t":1794376553000,"lat":22.621953,"lon":120.331389,"acc":15.0,"spd":4.2},{"t":1794376558000,"lat":22.622217,"lon":120.331455,"acc":15.0,"spd":4.9},{"t":1794376563000,"lat":22.622274,"lon":120.331429,"acc":15.0,"spd":4.5},{"t":1794376568000,"lat":22.622454,"lon":120.331129,"acc":15.0,"spd":4.5},{"t":1794376573000,"lat":22.622522,"lon":120.331082,"acc":15.0,"spd":4.4},{"t":1794376578000,"lat":22.622386,"lon":120.330804,"acc":15.0,"spd":9.2},{"t":1794376583000,"lat":22.622455,"lon":120.330106,"acc":15.0,"spd":9.1},{"t":1794376588000,"lat":22.622365,"lon":120.329919,"acc":15.0,"spd":8.5},{"t":1794376593000,"lat":22.622013,"lon":120.329418,"acc":15.0,"spd":6.8},{"t":1794376598000,"lat":22.622173,"lon":120.329517,"acc":15.0,"spd":0.9},{"t":1794376603000,"lat":22.622213,"lon":120.329236,"acc":15.0,"spd":1.8},{"t":1794376608000,"lat":22.622094,"lon":120.329195,"acc":15.0,"spd":1.8},{"t":1794376613000,"lat":22.622013,"lon":120.329239,"acc":15.0,"spd":1.8},{"t":1794376618000,"lat":22.622053,"lon":120.329042,"acc":15.0,"spd":2.1},{"t":1794376623000,"lat":22.622124,"lon":120.329041,"acc":15.0,"spd":1.5},{"t":1794376628000,"lat":22.62196,"lon":120.328784,"acc":15.0,"spd":1.2},{"t":1794376633000,"lat":22.621876,"lon":120.32875,"acc":15.0,"spd":1.4},{"t":1794376638000,"lat":22.621837,"lon":120.328598,"acc":15.0,"spd":6.0},{"t":1794376643000,"lat":22.621635,"lon":120.328279,"acc":15.0,"spd":9.4},{"t":1794376648000,"lat":22.621635,"lon":120.327906,"acc":15.0,"spd":8.9},{"t":1794376653000,"lat":22.621539,"lon":120.327332,"acc":15.0,"spd":8.6},{"t":1794376658000,"lat":22.621434,"lon":120.327107,"acc":15.0,"spd":6.2},{"t":1794376663000,"lat":22.621174,"lon":120.327082,"acc":15.0,"spd":0.0},{"t":1794376668000,"lat":22.621306,"lon":120.327117,"acc":15.0,"spd":0.0},{"t":1794376673000,"lat":22.621096,"lon":120.32699,"acc":15.0,"spd":0.2},{"t":1794376678000,"lat":22.621573,"lon":120.32684,"acc":15.0,"spd":0.1},{"t":1794376683000,"lat":22.62126,"lon":120.326798,"acc":15.0,"spd":0.0},{"t":1794376688000,"lat":22.621224,"lon":120.326874,"acc":15.0,"spd":0.0},{"t":1794376693000,"lat":22.621464,"lon":120.327288,"acc":15.0,"spd":0.3},{"t":1794376698000,"lat":22.621496,"lon":120.327048,"acc":15.0,"spd":0.5},{"t":1794376703000,"lat":22.621309,"lon":120.327142,"acc":15.0,"spd":0.6},{"t":1794376708000,"lat":22.621424,"lon":120.32706,"acc":15.0,"spd":0.7},{"t":1794376713000,"lat":22.621309,"lon":120.327102,"acc":15.0,"spd":0.2},{"t":1794376718000,"lat":22.621185,"lon":120.326971,"acc":15.0,"spd":3.0},{"t":1794376723000,"lat":22.621119,"lon":120.326914,"acc":15.0,"spd":2.5},{"t":1794376728000,"lat":22.621136,"lon":120.326661,"acc":15.0,"spd":2.9},{"t":1794376733000,"lat":22.621212,"lon":120.326414,"acc":15.0,"spd":3.4},{"t":1794376738000,"lat":22.621156,"lon":120.326432,"acc":15.0,"spd":3.7},{"t":1794376743000,"lat":22.620855,"lon":120.326018,"acc":15.0,"spd":2.3},{"t":1794376748000,"lat":22.620968,"lon":120.326042,"acc":15.0,"spd":3.3},{"t":1794376753000,"lat":22.620979,"lon":120.326048,"acc":15.0,"spd":4.2},{"t":1794376758000,"lat":22.620875,"lon":120.325463,"acc":15.0,"spd":8.1},{"t":1794376763000,"lat":22.620786,"lon":120.324967,"acc":15.0,"spd":8.3},{"t":1794376768000,"lat":22.620531,"lon":120.324692,"acc":15.0,"spd":8.2},{"t":1794376773000,"lat":22.62038,"lon":120.324508,"acc":15.0,"spd":8.2},{"t":1794376778000,"lat":22.620314,"lon":120.32397,"acc":15.0,"spd":8.2},{"t":1794376783000,"lat":22.620295,"lon":120.323645,"acc":15.0,"spd":8.1},{"t":1794376788000,"lat":22.62012,"lon":120.323379,"acc":15.0,"spd":8.6},{"t":1794376793000,"lat":22.620074,"lon":120.323026,"acc":15.0,"spd":3.7},{"t":1794376798000,"lat":22.620076,"lon":120.322924,"acc":15.0,"spd":4.6},{"t":1794376803000,"lat":22.619962,"lon":120.322696,"acc":15.0,"spd":3.9},{"t":1794376808000,"lat":22.619839,"lon":120.322385,"acc":15.0,"spd":4.3},{"t":1794376813000,"lat":22.619991,"lon":120.322359,"acc":15.0,"spd":3.6},{"t":1794376818000,"lat":22.619902,"lon":120.322105,"acc":15.0,"spd":0.8},{"t":1794376823000,"lat":22.619596,"lon":120.322328,"acc":15.0,"spd":0.0},{"t":1794376828000,"lat":22.619912,"lon":120.322401,"acc":15.0,"spd":0.0},{"t":1794376833000,"lat":22.619725,"lon":120.322339,"acc":15.0,"spd":0.8},{"t":1794376838000,"lat":22.619566,"lon":120.322096,"acc":15.0,"spd":3.0},{"t":1794376843000,"lat":22.619848,"lon":120.321845,"acc":15.0,"spd":3.2},{"t":1794376848000,"lat":22.619635,"lon":120.321861,"acc":15.0,"spd":3.5},{"t":1794376853000,"lat":22.61954,"lon":120.321799,"acc":15.0,"spd":4.2},{"t":1794376858000,"lat":22.619339,"lon":120.321328,"acc":15.0,"spd":3.9},{"t":1794376863000,"lat":22.619627,"lon":120.321268,"acc":15.0,"spd":3.8},{"t":1794376868000,"lat":22.619461,"lon":120.321166,"acc":15.0,"spd":3.6},{"t":1794376873000,"lat":22.619263,"lon":120.32122,"acc":15.0,"spd":4.0},{"t":1794376878000,"lat":22.619306,"lon":120.320831,"acc":15.0,"spd":3.8},{"t":1794376883000,"lat":22.619077,"lon":120.320342,"acc":15.0,"spd":6.5},{"t":1794376888000,"lat":22.61905,"lon":120.319964,"acc":15.0,"spd":7.5},{"t":1794376893000,"lat":22.618858,"lon":120.319666,"acc":15.0,"spd":6.3},{"t":1794376898000,"lat":22.618867,"lon":120.319848,"acc":15.0,"spd":2.2},{"t":1794376903000,"lat":22.61894,"lon":120.31947,"acc":15.0,"spd":2.0},{"t":1794376908000,"lat":22.618891,"lon":120.319598,"acc":15.0,"spd":3.6},{"t":1794376913000,"lat":22.619039,"lon":120.319252,"acc":15.0,"spd":2.7},{"t":1794376918000,"lat":22.618643,"lon":120.319199,"acc":15.0,"spd":2.5},{"t":1794376923000,"lat":22.618648,"lon":120.31894,"acc":15.0,"spd":5.1},{"t":1794376928000,"lat":22.618466,"lon":120.318668,"acc":15.0,"spd":5.6},{"t":1794376933000,"lat":22.618524,"lon":120.318541,"acc":15.0,"spd":6.3},{"t":1794376938000,"lat":22.618493,"lon":120.31817,"acc":15.0,"spd":6.2},{"t":1794376943000,"lat":22.618575,"lon":120.317788,"acc":15.0,"spd":6.1},{"t":1794376948000,"lat":22.618506,"lon":120.31756,"acc":15.0,"spd":6.0},{"t":1794376953000,"lat":22.618134,"lon":120.317082,"acc":15.0,"spd":5.4},{"t":1794376958000,"lat":22.617988,"lon":120.317047,"acc":15.0,"spd":2.5},{"t":1794376963000,"lat":22.618018,"lon":120.316861,"acc":15.0,"spd":2.8},{"t":1794376968000,"lat":22.61812,"lon":120.316938,"acc":15.0,"spd":2.5},{"t":1794376973000,"lat":22.618202,"lon":120.316501,"acc":15.0,"spd":3.0},{"t":1794376978000,"lat":22.618073,"lon":120.316506,"acc":15.0,"spd":2.5},{"t":1794376983000,"lat":22.618086,"lon":120.316591,"acc":15.0,"spd":1.3},{"t":1794376988000,"lat":22.617988,"lon":120.316325,"acc":15.0,"spd":0.9},{"t":1794376993000,"lat":22.618078,"lon":120.316448,"acc":15.0,"spd":1.5},{"t":1794376998000,"lat":22.617971,"lon":120.316496,"acc":15.0,"spd":1.3},{"t":1794377003000,"lat":22.618065,"lon":120.316402,"acc":15.0,"spd":0.9},{"t":1794377008000,"lat":22.618026,"lon":120.316387,"acc":15.0,"spd":0.7},{"t":1794377013000,"lat":22.617973,"lon":120.316279,"acc":15.0,"spd":1.9},{"t":1794377018000,"lat":22.617768,"lon":120.316042,"acc":15.0,"spd":5.8},{"t":1794377023000,"lat":22.61769,"lon":120.316048,"acc":15.0,"spd":6.4},{"t":1794377028000,"lat":22.617595,"lon":120.315612,"acc":15.0,"spd":5.6},{"t":1794377033000,"lat":22.617656,"lon":120.315132,"acc":15.0,"spd":5.8},{"t":1794377038000,"lat":22.617373,"lon":120.315056,"acc":15.0,"spd":5.2},{"t":1794377043000,"lat":22.617357,"lon":120.314804,"acc":15.0,"spd":5.5},{"t":1794377048000,"lat":22.617509,"lon":120.314449,"acc":15.0,"spd":6.3},{"t":1794377053000,"lat":22.617187,"lon":120.314157,"acc":15.0,"spd":11.4},{"t":1794377058000,"lat":22.616898,"lon":120.313475,"acc":15.0,"spd":11.6},{"t":1794377063000,"lat":22.616815,"lon":120.312975,"acc":15.0,"spd":10.6},{"t":1794377068000,"lat":22.616388,"lon":120.31231,"acc":15.0,"spd":11.6},{"t":1794377073000,"lat":22.616405,"lon":120.311858,"acc":15.0,"spd":11.0},{"t":1794377078000,"lat":22.616256,"lon":120.311554,"acc":15.0,"spd":12.5},{"t":1794377083000,"lat":22.615851,"lon":120.310896,"acc":15.0,"spd":12.3},{"t":1794377088000,"lat":22.615759,"lon":120.310239,"acc":15.0,"spd":12.5},{"t":1794377093000,"lat":22.615664,"lon":120.309833,"acc":15.0,"spd":12.1},{"t":1794377098000,"lat":22.615353,"lon":120.309427,"acc":15.0,"spd":6.5},{"t":1794377103000,"lat":22.615493,"lon":120.309349,"acc":15.0,"spd":2.4},{"t":1794377108000,"lat":22.615645,"lon":120.309414,"acc":15.0,"spd":1.8},{"t":1794377113000,"lat":22.615219,"lon":120.309147,"acc":15.0,"spd":2.1},{"t":1794377118000,"lat":22.615562,"lon":120.309021,"acc":15.0,"spd":2.3},{"t":1794377123000,"lat":22.615372,"lon":120.309042,"acc":15.0,"spd":1.7},{"t":1794377128000,"lat":22.615352,"lon":120.308974,"acc":15.0,"spd":2.0},{"t":1794377133000,"lat":22.615403,"lon":120.308827,"acc":15.0,"spd":1.6},{"t":1794377138000,"lat":22.615277,"lon":120.308705,"acc":15.0,"spd":0.0},{"t":1794377143000,"lat":22.615266,"lon":120.308589,"acc":15.0,"spd":0.2},{"t":1794377148000,"lat":22.615432,"lon":120.30883,"acc":15.0,"spd":0.0},{"t":1794377153000,"lat":22.615545,"lon":120.308708,"acc":15.0,"spd":0.3},{"t":1794377158000,"lat":22.615412,"lon":120.308542,"acc":15.0,"spd":4.7},{"t":1794377163000,"lat":22.615275,"lon":120.308281,"acc":15.0,"spd":4.1},{"t":1794377168000,"lat":22.61502,"lon":120.30794,"acc":15.0,"spd":4.3},{"t":1794377173000,"lat":22.615134,"lon":120.307847,"acc":15.0,"spd":5.0},{"t":1794377178000,"lat":22.61503,"lon":120.307535,"acc":15.0,"spd":4.6},{"t":1794377183000,"lat":22.615224,"lon":120.307726,"acc":15.0,"spd":5.1},{"t":1794377188000,"lat":22.614922,"lon":120.307317,"acc":15.0,"spd":4.5},{"t":1794377193000,"lat":22.614946,"lon":120.307223,"acc":15.0,"spd":5.2},{"t":1794377198000,"lat":22.614632,"lon":120.306769,"acc":15.0,"spd":5.3},{"t":1794377203000,"lat":22.614728,"lon":120.306633,"acc":15.0,"spd":6.3},{"t":1794377208000,"lat":22.614438,"lon":120.306167,"acc":15.0,"spd":6.9},{"t":1794377213000,"lat":22.614409,"lon":120.305951,"acc":15.0,"spd":6.2},{"t":1794377218000,"lat":22.614543,"lon":120.306109,"acc":15.0,"spd":1.4},{"t":1794377223000,"lat":22.6145,"lon":120.305692,"acc":15.0,"spd":1.6},{"t":1794377228000,"lat":22.614284,"lon":120.305879,"acc":15.0,"spd":1.1},{"t":1794377233000,"lat":22.614479,"lon":120.305674,"acc":15.0,"spd":2.1},{"t":1794377238000,"lat":22.614096,"lon":120.305833,"acc":15.0,"spd":1.1},{"t":1794377243000,"lat":22.614314,"lon":120.305405,"acc":15.0,"spd":1.5},{"t":1794377248000,"lat":22.61445,"lon":120.305644,"acc":15.0,"spd":1.4},{"t":1794377253000,"lat":22.614283,"lon":120.305381,"acc":15.0,"spd":1.8},{"t":1794377258000,"lat":22.614175,"lon":120.305261,"acc":15.0,"spd":2.8},{"t":1794377263000,"lat":22.614184,"lon":120.305318,"acc":15.0,"spd":1.9},{"t":1794377268000,"lat":22.614384,"lon":120.305203,"acc":15.0,"spd":3.3},{"t":1794377273000,"lat":22.613983,"lon":120.305029,"acc":15.0,"spd":2.2},{"t":1794377278000,"lat":22.614111,"lon":120.305023,"acc":15.0,"spd":0.0},{"t":1794377283000,"lat":22.614139,"lon":120.304938,"acc":15.0,"spd":0.0},{"t":1794377288000,"lat":22.614072,"lon":120.3048,"acc":15.0,"spd":0.8},{"t":1794377293000,"lat":22.614062,"lon":120.305071,"acc":15.0,"spd":0.0},{"t":1794377298000,"lat":22.614044,"lon":120.304692,"acc":15.0,"spd":0.1},{"t":1794377303000,"lat":22.614056,"lon":120.305052,"acc":15.0,"spd":0.5},{"t":1794377308000,"lat":22.613946,"lon":120.304758,"acc":15.0,"spd":0.0},{"t":1794377313000,"lat":22.614107,"lon":120.305051,"acc":15.0,"spd":0.1},{"t":1794377318000,"lat":22.614156,"lon":120.305085,"acc":15.0,"spd":0.0},{"t":1794377323000,"lat":22.614045,"lon":120.304745,"acc":15.0,"spd":0.0},{"t":1794377328000,"lat":22.613956,"lon":120.304879,"acc":15.0,"spd":0.0},{"t":1794377333000,"lat":22.614022,"lon":120.304957,"acc":15.0,"spd":1.6},{"t":1794377338000,"lat":22.613795,"lon":120.304424,"acc":15.0,"spd":5.0},{"t":1794377343000,"lat":22.614056,"lon":120.304242,"acc":15.0,"spd":5.1},{"t":1794377348000,"lat":22.613845,"lon":120.30418,"acc":15.0,"spd":5.4},{"t":1794377353000,"lat":22.613671,"lon":120.304087,"acc":15.0,"spd":5.9},{"t":1794377358000,"lat":22.613767,"lon":120.303792,"acc":15.0,"spd":4.7},{"t":1794377363000,"lat":22.613367,"lon":120.303476,"acc":15.0,"spd":5.1},{"t":1794377368000,"lat":22.613521,"lon":120.303344,"acc":15.0,"spd":5.0},{"t":1794377373000,"lat":22.613417,"lon":120.303057,"acc":15.0,"spd":5.3},{"t":1794377378000,"lat":22.613455,"lon":120.303156,"acc":15.0,"spd":1.1},{"t":1794377383000,"lat":22.613339,"lon":120.303356,"acc":15.0,"spd":0.3},{"t":1794377388000,"lat":22.61351,"lon":120.303118,"acc":15.0,"spd":0.0},{"t":1794377393000,"lat":22.613502,"lon":120.303028,"acc":15.0,"spd":0.2},{"t":1794377398000,"lat":22.613569,"lon":120.303127,"acc":15.0,"spd":0.0},{"t":1794377403000,"lat":22.613693,"lon":120.303075,"acc":15.0,"spd":0.0},{"t":1794377408000,"lat":22.613445,"lon":120.302995,"acc":15.0,"spd":0.5}],"waitingFixes":[{"t":1794376163000,"lat":22.614265,"lon":120.332554,"acc":25.0,"spd":0.4},{"t":1794376168000,"lat":22.613849,"lon":120.332453,"acc":25.0,"spd":0.4},{"t":1794376173000,"lat":22.614412,"lon":120.332561,"acc":25.0,"spd":0.2},{"t":1794376178000,"lat":22.614207,"lon":120.332491,"acc":25.0,"spd":0.4},{"t":1794376183000,"lat":22.614403,"lon":120.33286,"acc":25.0,"spd":0.4},{"t":1794376188000,"lat":22.614506,"lon":120.332349,"acc":25.0,"spd":0.3},{"t":1794376193000,"lat":22.614432,"lon":120.33266,"acc":25.0,"spd":0.6},{"t":1794376198000,"lat":22.614136,"lon":120.33242,"acc":25.0,"spd":0.1},{"t":1794376203000,"lat":22.614364,"lon":120.332321,"acc":25.0,"spd":0.5},{"t":1794376208000,"lat":22.614202,"lon":120.332599,"acc":25.0,"spd":0.2},{"t":1794376213000,"lat":22.614313,"lon":120.332725,"acc":25.0,"spd":0.0},{"t":1794376218000,"lat":22.61431,"lon":120.3325,"acc":25.0,"spd":0.2},{"t":1794376223000,"lat":22.614423,"lon":120.333277,"acc":25.0,"spd":0.1},{"t":1794376228000,"lat":22.614338,"lon":120.333116,"acc":25.0,"spd":0.3},{"t":1794376233000,"lat":22.614172,"lon":120.332793,"acc":25.0,"spd":0.4},{"t":1794376238000,"lat":22.614181,"lon":120.332813,"acc":25.0,"spd":0.2},{"t":1794376243000,"lat":22.614242,"lon":120.332377,"acc":25.0,"spd":0.2},{"t":1794376248000,"lat":22.614312,"lon":120.332795,"acc":25.0,"spd":0.1},{"t":1794376253000,"lat":22.614333,"lon":120.332317,"acc":25.0,"spd":0.3},{"t":1794376258000,"lat":22.614287,"lon":120.332509,"acc":25.0,"spd":0.1},{"t":1794376263000,"lat":22.614358,"lon":120.332429,"acc":25.0,"spd":0.5},{"t":1794376268000,"lat":22.614284,"lon":120.332567,"acc":25.0,"spd":0.2},{"t":1794376273000,"lat":22.614362,"lon":120.332455,"acc":25.0,"spd":0.1},{"t":1794376278000,"lat":22.61429,"lon":120.332491,"acc":25.0,"spd":0.1},{"t":1794376283000,"lat":22.614145,"lon":120.332359,"acc":25.0,"spd":0.2},{"t":1794376288000,"lat":22.614394,"lon":120.332736,"acc":25.0,"spd":0.1},{"t":1794376293000,"lat":22.614297,"lon":120.332227,"acc":25.0,"spd":0.1},{"t":1794376298000,"lat":22.614614,"lon":120.332772,"acc":25.0,"spd":0.3},{"t":1794376303000,"lat":22.614373,"lon":120.332829,"acc":25.0,"spd":0.2},{"t":1794376308000,"lat":22.614311,"lon":120.332495,"acc":25.0,"spd":0.0},{"t":1794376313000,"lat":22.614502,"lon":120.332465,"acc":25.0,"spd":0.3},{"t":1794376318000,"lat":22.614435,"lon":120.332465,"acc":25.0,"spd":0.3},{"t":1794376323000,"lat":22.614416,"lon":120.332472,"acc":25.0,"spd":0.1},{"t":1794376328000,"lat":22.614294,"lon":120.332753,"acc":25.0,"spd":0.4},{"t":1794376333000,"lat":22.614259,"lon":120.332789,"acc":25.0,"spd":0.1},{"t":1794376338000,"lat":22.614201,"lon":120.332431,"acc":25.0,"spd":0.0},{"t":1794376343000,"lat":22.614486,"lon":120.332485,"acc":25.0,"spd":0.0},{"t":1794376348000,"lat":22.614479,"lon":120.332595,"acc":25.0,"spd":0.2},{"t":1794376353000,"lat":22.614209,"lon":120.332654,"acc":25.0,"spd":0.4},{"t":1794376358000,"lat":22.614281,"lon":120.33247,"acc":25.0,"spd":0.7},{"t":1794376363000,"lat":22.614312,"lon":120.332304,"acc":25.0,"spd":0.3},{"t":1794376368000,"lat":22.614277,"lon":120.332635,"acc":25.0,"spd":0.0},{"t":1794376373000,"lat":22.614263,"lon":120.332607,"acc":25.0,"spd":0.4},{"t":1794376378000,"lat":22.614164,"lon":120.332519,"acc":25.0,"spd":0.4},{"t":1794376383000,"lat":22.614227,"lon":120.332492,"acc":25.0,"spd":0.3},{"t":1794376388000,"lat":22.61427,"lon":120.3326,"acc":25.0,"spd":0.1},{"t":1794376393000,"lat":22.614135,"lon":120.332779,"acc":25.0,"spd":0.1},{"t":1794376398000,"lat":22.614383,"lon":120.33262,"acc":25.0,"spd":0.2},{"t":1794376403000,"lat":22.614305,"lon":120.332372,"acc":25.0,"spd":0.1},{"t":1794376408000,"lat":22.614257,"lon":120.332634,"acc":25.0,"spd":0.5},{"t":1794376413000,"lat":22.614146,"lon":120.332627,"acc":25.0,"spd":0.0},{"t":1794376418000,"lat":22.61449,"lon":120.332775,"acc":25.0,"spd":0.2},{"t":1794376423000,"lat":22.614329,"lon":120.333158,"acc":25.0,"spd":0.1},{"t":1794376428000,"lat":22.614243,"lon":120.332981,"acc":25.0,"spd":0.5},{"t":1794376433000,"lat":22.614319,"lon":120.332716,"acc":25.0,"spd":0.1},{"t":1794376438000,"lat":22.614251,"lon":120.332297,"acc":25.0,"spd":0.1},{"t":1794376443000,"lat":22.614079,"lon":120.332726,"acc":25.0,"spd":0.3},{"t":1794376448000,"lat":22.614077,"lon":120.33249,"acc":25.0,"spd":0.0},{"t":1794376453000,"lat":22.614236,"lon":120.332382,"acc":25.0,"spd":0.3},{"t":1794376458000,"lat":22.614403,"lon":120.3327,"acc":25.0,"spd":0.2},{"t":1794376463000,"lat":22.614249,"lon":120.332632,"acc":25.0,"spd":0.2},{"t":1794376468000,"lat":22.614429,"lon":120.332698,"acc":25.0,"spd":0.1},{"t":1794376473000,"lat":22.614312,"lon":120.332481,"acc":25.0,"spd":0.5},{"t":1794376478000,"lat":22.614296,"lon":120.332439,"acc":25.0,"spd":0.4},{"t":1794376483000,"lat":22.614437,"lon":120.332732,"acc":25.0,"spd":0.3},{"t":1794376488000,"lat":22.614393,"lon":120.332484,"acc":25.0,"spd":0.0},{"t":1794376493000,"lat":22.614391,"lon":120.332555,"acc":25.0,"spd":0.3},{"t":1794376498000,"lat":22.614244,"lon":120.332605,"acc":25.0,"spd":0.2},{"t":1794376503000,"lat":22.614292,"lon":120.33241,"acc":25.0,"spd":0.2},{"t":1794376508000,"lat":22.61438,"lon":120.332404,"acc":25.0,"spd":0.2},{"t":1794376513000,"lat":22.614246,"lon":120.33246,"acc":25.0,"spd":0.3},{"t":1794376518000,"lat":22.614188,"lon":120.332523,"acc":25.0,"spd":0.3},{"t":1794376523000,"lat":22.614422,"lon":120.332393,"acc":25.0,"spd":0.2},{"t":1794376528000,"lat":22.614239,"lon":120.332661,"acc":25.0,"spd":0.4},{"t":1794376533000,"lat":22.614412,"lon":120.332466,"acc":25.0,"spd":0.2},{"t":1794376538000,"lat":22.614019,"lon":120.33247,"acc":25.0,"spd":0.3},{"t":1794376543000,"lat":22.614532,"lon":120.332571,"acc":25.0,"spd":0.1},{"t":1794376548000,"lat":22.614453,"lon":120.332271,"acc":25.0,"spd":0.3},{"t":1794376553000,"lat":22.614257,"lon":120.332629,"acc":25.0,"spd":0.1},{"t":1794376558000,"lat":22.614545,"lon":120.332258,"acc":25.0,"spd":0.3},{"t":1794376563000,"lat":22.614371,"lon":120.332463,"acc":25.0,"spd":0.2},{"t":1794376568000,"lat":22.614315,"lon":120.332384,"acc":25.0,"spd":0.0},{"t":1794376573000,"lat":22.614451,"lon":120.332555,"acc":25.0,"spd":0.1},{"t":1794376578000,"lat":22.614135,"lon":120.332437,"acc":25.0,"spd":0.3},{"t":1794376583000,"lat":22.614348,"lon":120.332388,"acc":25.0,"spd":0.0},{"t":1794376588000,"lat":22.614075,"lon":120.332555,"acc":25.0,"spd":0.3},{"t":1794376593000,"lat":22.614332,"lon":120.332626,"acc":25.0,"spd":0.3},{"t":1794376598000,"lat":22.614164,"lon":120.33246,"acc":25.0,"spd":0.3},{"t":1794376603000,"lat":22.614258,"lon":120.332442,"acc":25.0,"spd":0.1},{"t":1794376608000,"lat":22.614366,"lon":120.333139,"acc":25.0,"spd":0.2},{"t":1794376613000,"lat":22.614347,"lon":120.332632,"acc":25.0,"spd":0.3},{"t":1794376618000,"lat":22.614478,"lon":120.332466,"acc":25.0,"spd":0.3},{"t":1794376623000,"lat":22.614297,"lon":120.332659,"acc":25.0,"spd":0.0},{"t":1794376628000,"lat":22.614289,"lon":120.332545,"acc":25.0,"spd":0.1},{"t":1794376633000,"lat":22.614264,"lon":120.332364,"acc":25.0,"spd":0.0},{"t":1794376638000,"lat":22.614503,"lon":120.332727,"acc":25.0,"spd":0.1},{"t":1794376643000,"lat":22.614204,"lon":120.332373,"acc":25.0,"spd":0.6},{"t":1794376648000,"lat":22.61412,"lon":120.332675,"acc":25.0,"spd":0.1},{"t":1794376653000,"lat":22.614321,"lon":120.332468,"acc":25.0,"spd":0.2},{"t":1794376658000,"lat":22.614211,"lon":120.332518,"acc":25.0,"spd":0.2},{"t":1794376663000,"lat":22.614242,"lon":120.332603,"acc":25.0,"spd":0.0},{"t":1794376668000,"lat":22.614463,"lon":120.332673,"acc":25.0,"spd":0.1},{"t":1794376673000,"lat":22.614574,"lon":120.332316,"acc":25.0,"spd":0.7},{"t":1794376678000,"lat":22.614473,"lon":120.332586,"acc":25.0,"spd":0.8},{"t":1794376683000,"lat":22.614519,"lon":120.332521,"acc":25.0,"spd":0.5},{"t":1794376688000,"lat":22.614267,"lon":120.332344,"acc":25.0,"spd":0.1},{"t":1794376693000,"lat":22.614214,"lon":120.332611,"acc":25.0,"spd":0.3},{"t":1794376698000,"lat":22.614425,"lon":120.332542,"acc":25.0,"spd":0.2},{"t":1794376703000,"lat":22.614219,"lon":120.332524,"acc":25.0,"spd":0.0},{"t":1794376708000,"lat":22.614214,"lon":120.332585,"acc":25.0,"spd":0.3},{"t":1794376713000,"lat":22.614197,"lon":120.332467,"acc":25.0,"spd":0.2},{"t":1794376718000,"lat":22.61424,"lon":120.332351,"acc":25.0,"spd":0.0},{"t":1794376723000,"lat":22.61441,"lon":120.332488,"acc":25.0,"spd":0.1},{"t":1794376728000,"lat":22.614032,"lon":120.332645,"acc":25.0,"spd":0.1},{"t":1794376733000,"lat":22.614416,"lon":120.332423,"acc":25.0,"spd":0.1},{"t":1794376738000,"lat":22.614273,"lon":120.332638,"acc":25.0,"spd":0.3},{"t":1794376743000,"lat":22.614111,"lon":120.332515,"acc":25.0,"spd":0.0},{"t":1794376748000,"lat":22.614507,"lon":120.332521,"acc":25.0,"spd":0.0},{"t":1794376753000,"lat":22.613797,"lon":120.332737,"acc":25.0,"spd":0.6},{"t":1794376758000,"lat":22.614311,"lon":120.332554,"acc":25.0,"spd":0.1},{"t":1794376763000,"lat":22.614312,"lon":120.332313,"acc":25.0,"spd":0.1},{"t":1794376768000,"lat":22.614416,"lon":120.332686,"acc":25.0,"spd":0.3},{"t":1794376773000,"lat":22.614381,"lon":120.332551,"acc":25.0,"spd":0.4},{"t":1794376778000,"lat":22.614535,"lon":120.332556,"acc":25.0,"spd":0.4},{"t":1794376783000,"lat":22.614166,"lon":120.332586,"acc":25.0,"spd":0.3},{"t":1794376788000,"lat":22.614214,"lon":120.331676,"acc":25.0,"spd":0.1},{"t":1794376793000,"lat":22.614082,"lon":120.332173,"acc":25.0,"spd":0.6},{"t":1794376798000,"lat":22.614478,"lon":120.332358,"acc":25.0,"spd":0.2},{"t":1794376803000,"lat":22.614323,"lon":120.332585,"acc":25.0,"spd":0.3},{"t":1794376808000,"lat":22.614303,"lon":120.332553,"acc":25.0,"spd":0.1},{"t":1794376813000,"lat":22.614116,"lon":120.332523,"acc":25.0,"spd":0.2},{"t":1794376818000,"lat":22.614234,"lon":120.332549,"acc":25.0,"spd":0.1},{"t":1794376823000,"lat":22.614459,"lon":120.332404,"acc":25.0,"spd":0.2},{"t":1794376828000,"lat":22.614149,"lon":120.33229,"acc":25.0,"spd":0.5},{"t":1794376833000,"lat":22.614332,"lon":120.33224,"acc":25.0,"spd":0.3},{"t":1794376838000,"lat":22.614233,"lon":120.332443,"acc":25.0,"spd":0.2},{"t":1794376843000,"lat":22.614176,"lon":120.332572,"acc":25.0,"spd":0.4},{"t":1794376848000,"lat":22.614259,"lon":120.332387,"acc":25.0,"spd":0.2},{"t":1794376853000,"lat":22.614183,"lon":120.332346,"acc":25.0,"spd":0.6},{"t":1794376858000,"lat":22.614429,"lon":120.33267,"acc":25.0,"spd":0.2},{"t":1794376863000,"lat":22.614408,"lon":120.332689,"acc":25.0,"spd":0.2},{"t":1794376868000,"lat":22.613993,"lon":120.33268,"acc":25.0,"spd":0.1},{"t":1794376873000,"lat":22.614185,"lon":120.332569,"acc":25.0,"spd":0.3},{"t":1794376878000,"lat":22.614418,"lon":120.332645,"acc":25.0,"spd":0.1},{"t":1794376883000,"lat":22.614125,"lon":120.332427,"acc":25.0,"spd":0.2},{"t":1794376888000,"lat":22.61446,"lon":120.332579,"acc":25.0,"spd":0.0},{"t":1794376893000,"lat":22.614183,"lon":120.332449,"acc":25.0,"spd":0.0},{"t":1794376898000,"lat":22.614293,"lon":120.332527,"acc":25.0,"spd":0.3},{"t":1794376903000,"lat":22.614301,"lon":120.332341,"acc":25.0,"spd":0.8},{"t":1794376908000,"lat":22.614205,"lon":120.332641,"acc":25.0,"spd":0.2},{"t":1794376913000,"lat":22.614061,"lon":120.332432,"acc":25.0,"spd":0.2},{"t":1794376918000,"lat":22.614551,"lon":120.332162,"acc":25.0,"spd":0.1},{"t":1794376923000,"lat":22.61425,"lon":120.332616,"acc":25.0,"spd":0.7},{"t":1794376928000,"lat":22.614267,"lon":120.332542,"acc":25.0,"spd":0.1},{"t":1794376933000,"lat":22.614157,"lon":120.332388,"acc":25.0,"spd":0.5},{"t":1794376938000,"lat":22.614393,"lon":120.332065,"acc":25.0,"spd":0.2},{"t":1794376943000,"lat":22.614214,"lon":120.332314,"acc":25.0,"spd":0.0},{"t":1794376948000,"lat":22.6143,"lon":120.332234,"acc":25.0,"spd":0.1},{"t":1794376953000,"lat":22.614491,"lon":120.332389,"acc":25.0,"spd":0.3},{"t":1794376958000,"lat":22.614407,"lon":120.33274,"acc":25.0,"spd":0.2},{"t":1794376963000,"lat":22.614126,"lon":120.332467,"acc":25.0,"spd":0.1},{"t":1794376968000,"lat":22.614273,"lon":120.332406,"acc":25.0,"spd":0.2},{"t":1794376973000,"lat":22.61401,"lon":120.332656,"acc":25.0,"spd":0.2},{"t":1794376978000,"lat":22.61403,"lon":120.332687,"acc":25.0,"spd":0.2},{"t":1794376983000,"lat":22.614474,"lon":120.332703,"acc":25.0,"spd":0.1},{"t":1794376988000,"lat":22.614281,"lon":120.332587,"acc":25.0,"spd":0.1},{"t":1794376993000,"lat":22.614267,"lon":120.333658,"acc":25.0,"spd":0.3},{"t":1794376998000,"lat":22.6145,"lon":120.332601,"acc":25.0,"spd":0.4},{"t":1794377003000,"lat":22.614569,"lon":120.332696,"acc":25.0,"spd":0.3},{"t":1794377008000,"lat":22.61448,"lon":120.332605,"acc":25.0,"spd":0.3},{"t":1794377013000,"lat":22.614215,"lon":120.332357,"acc":25.0,"spd":0.4},{"t":1794377018000,"lat":22.614149,"lon":120.332872,"acc":25.0,"spd":0.1},{"t":1794377023000,"lat":22.614131,"lon":120.332491,"acc":25.0,"spd":0.0},{"t":1794377028000,"lat":22.614377,"lon":120.332492,"acc":25.0,"spd":0.3},{"t":1794377033000,"lat":22.614364,"lon":120.332728,"acc":25.0,"spd":0.5},{"t":1794377038000,"lat":22.614292,"lon":120.332544,"acc":25.0,"spd":0.3},{"t":1794377043000,"lat":22.614153,"lon":120.332627,"acc":25.0,"spd":0.4},{"t":1794377048000,"lat":22.614353,"lon":120.332517,"acc":25.0,"spd":0.0},{"t":1794377053000,"lat":22.614356,"lon":120.332693,"acc":25.0,"spd":0.4},{"t":1794377058000,"lat":22.614223,"lon":120.33251,"acc":25.0,"spd":0.1},{"t":1794377063000,"lat":22.614053,"lon":120.332665,"acc":25.0,"spd":0.1},{"t":1794377068000,"lat":22.614058,"lon":120.332709,"acc":25.0,"spd":0.0},{"t":1794377073000,"lat":22.614301,"lon":120.332313,"acc":25.0,"spd":0.1},{"t":1794377078000,"lat":22.614166,"lon":120.332339,"acc":25.0,"spd":0.4},{"t":1794377083000,"lat":22.614199,"lon":120.332513,"acc":25.0,"spd":0.1},{"t":1794377088000,"lat":22.61415,"lon":120.332792,"acc":25.0,"spd":0.2},{"t":1794377093000,"lat":22.614277,"lon":120.333426,"acc":25.0,"spd":0.7},{"t":1794377098000,"lat":22.614208,"lon":120.332805,"acc":25.0,"spd":0.3},{"t":1794377103000,"lat":22.61428,"lon":120.332422,"acc":25.0,"spd":0.2},{"t":1794377108000,"lat":22.614307,"lon":120.33278,"acc":25.0,"spd":0.1},{"t":1794377113000,"lat":22.614244,"lon":120.332814,"acc":25.0,"spd":0.6},{"t":1794377118000,"lat":22.614203,"lon":120.332604,"acc":25.0,"spd":0.2},{"t":1794377123000,"lat":22.614326,"lon":120.332551,"acc":25.0,"spd":0.0},{"t":1794377128000,"lat":22.614259,"lon":120.332638,"acc":25.0,"spd":0.3},{"t":1794377133000,"lat":22.614344,"lon":120.332515,"acc":25.0,"spd":0.3},{"t":1794377138000,"lat":22.614167,"lon":120.332614,"acc":25.0,"spd":0.4},{"t":1794377143000,"lat":22.614347,"lon":120.33233,"acc":25.0,"spd":0.1},{"t":1794377148000,"lat":22.614481,"lon":120.332483,"acc":25.0,"spd":0.2},{"t":1794377153000,"lat":22.614462,"lon":120.332351,"acc":25.0,"spd":0.3},{"t":1794377158000,"lat":22.6146,"lon":120.332367,"acc":25.0,"spd":0.1},{"t":1794377163000,"lat":22.614036,"lon":120.332456,"acc":25.0,"spd":0.1},{"t":1794377168000,"lat":22.614388,"lon":120.332393,"acc":25.0,"spd":0.2},{"t":1794377173000,"lat":22.614251,"lon":120.332545,"acc":25.0,"spd":0.2},{"t":1794377178000,"lat":22.614488,"lon":120.332469,"acc":25.0,"spd":0.3},{"t":1794377183000,"lat":22.614426,"lon":120.332574,"acc":25.0,"spd":0.4},{"t":1794377188000,"lat":22.614397,"lon":120.332492,"acc":25.0,"spd":0.4},{"t":1794377193000,"lat":22.614138,"lon":120.332553,"acc":25.0,"spd":0.3},{"t":1794377198000,"lat":22.614695,"lon":120.332458,"acc":25.0,"spd":0.0},{"t":1794377203000,"lat":22.61445,"lon":120.332558,"acc":25.0,"spd":0.4},{"t":1794377208000,"lat":22.614297,"lon":120.332356,"acc":25.0,"spd":0.1},{"t":1794377213000,"lat":22.614152,"lon":120.332358,"acc":25.0,"spd":0.2},{"t":1794377218000,"lat":22.614269,"lon":120.332438,"acc":25.0,"spd":0.1},{"t":1794377223000,"lat":22.614467,"lon":120.332311,"acc":25.0,"spd":0.1},{"t":1794377228000,"lat":22.614453,"lon":120.3324,"acc":25.0,"spd":0.1},{"t":1794377233000,"lat":22.614247,"lon":120.332749,"acc":25.0,"spd":0.5},{"t":1794377238000,"lat":22.614164,"lon":120.332081,"acc":25.0,"spd":0.0},{"t":1794377243000,"lat":22.614179,"lon":120.332338,"acc":25.0,"spd":0.4},{"t":1794377248000,"lat":22.614116,"lon":120.332406,"acc":25.0,"spd":0.2},{"t":1794377253000,"lat":22.614292,"lon":120.332382,"acc":25.0,"spd":0.1},{"t":1794377258000,"lat":22.614289,"lon":120.332836,"acc":25.0,"spd":0.2},{"t":1794377263000,"lat":22.614344,"lon":120.332463,"acc":25.0,"spd":0.3},{"t":1794377268000,"lat":22.614274,"lon":120.332718,"acc":25.0,"spd":0.2},{"t":1794377273000,"lat":22.614353,"lon":120.332436,"acc":25.0,"spd":0.2},{"t":1794377278000,"lat":22.614658,"lon":120.332719,"acc":25.0,"spd":0.1},{"t":1794377283000,"lat":22.614394,"lon":120.332329,"acc":25.0,"spd":0.0},{"t":1794377288000,"lat":22.6142,"lon":120.332712,"acc":25.0,"spd":0.3},{"t":1794377293000,"lat":22.614378,"lon":120.332637,"acc":25.0,"spd":0.2},{"t":1794377298000,"lat":22.61417,"lon":120.332619,"acc":25.0,"spd":0.3},{"t":1794377303000,"lat":22.614339,"lon":120.332487,"acc":25.0,"spd":0.2},{"t":1794377308000,"lat":22.614634,"lon":120.332544,"acc":25.0,"spd":0.1},{"t":1794377313000,"lat":22.614253,"lon":120.332501,"acc":25.0,"spd":0.2},{"t":1794377318000,"lat":22.614236,"lon":120.332203,"acc":25.0,"spd":0.0},{"t":1794377323000,"lat":22.614491,"lon":120.33251,"acc":25.0,"spd":0.2},{"t":1794377328000,"lat":22.614609,"lon":120.332667,"acc":25.0,"spd":0.1},{"t":1794377333000,"lat":22.614186,"lon":120.33231,"acc":25.0,"spd":0.5},{"t":1794377338000,"lat":22.614349,"lon":120.332324,"acc":25.0,"spd":0.2},{"t":1794377343000,"lat":22.614346,"lon":120.332529,"acc":25.0,"spd":0.3},{"t":1794377348000,"lat":22.614361,"lon":120.332552,"acc":25.0,"spd":0.1},{"t":1794377353000,"lat":22.61435,"lon":120.333269,"acc":25.0,"spd":0.6},{"t":1794377358000,"lat":22.614233,"lon":120.3327,"acc":25.0,"spd":0.1},{"t":1794377363000,"lat":22.614329,"lon":120.332485,"acc":25.0,"spd":0.5},{"t":1794377368000,"lat":22.614181,"lon":120.332838,"acc":25.0,"spd":0.2},{"t":1794377373000,"lat":22.614316,"lon":120.332501,"acc":25.0,"spd":0.5},{"t":1794377378000,"lat":22.614232,"lon":120.332439,"acc":25.0,"spd":0.6},{"t":1794377383000,"lat":22.614323,"lon":120.332555,"acc":25.0,"spd":0.1},{"t":1794377388000,"lat":22.614181,"lon":120.332561,"acc":25.0,"spd":0.3},{"t":1794377393000,"lat":22.613992,"lon":120.332448,"acc":25.0,"spd":0.3},{"t":1794377398000,"lat":22.614397,"lon":120.332482,"acc":25.0,"spd":0.5},{"t":1794377403000,"lat":22.614415,"lon":120.332656,"acc":25.0,"spd":0.2},{"t":1794377408000,"lat":22.614482,"lon":120.332409,"acc":25.0,"spd":0.1}],"walkerFixes":[{"t":1794376163000,"lat":22.614433,"lon":120.332475,"acc":15.0,"spd":1.2},{"t":1794376168000,"lat":22.614277,"lon":120.33248,"acc":15.0,"spd":1.5},{"t":1794376173000,"lat":22.61439,"lon":120.33244,"acc":15.0,"spd":1.6},{"t":1794376178000,"lat":22.61446,"lon":120.332384,"acc":15.0,"spd":1.0},{"t":1794376183000,"lat":22.614428,"lon":120.332479,"acc":15.0,"spd":1.2},{"t":1794376188000,"lat":22.614386,"lon":120.332424,"acc":15.0,"spd":1.2},{"t":1794376193000,"lat":22.614908,"lon":120.332465,"acc":15.0,"spd":1.2},{"t":1794376198000,"lat":22.614481,"lon":120.332562,"acc":15.0,"spd":1.4},{"t":1794376203000,"lat":22.614853,"lon":120.33264,"acc":15.0,"spd":1.4},{"t":1794376208000,"lat":22.614706,"lon":120.332709,"acc":15.0,"spd":1.5},{"t":1794376213000,"lat":22.614899,"lon":120.332466,"acc":15.0,"spd":1.4},{"t":1794376218000,"lat":22.615081,"lon":120.332459,"acc":15.0,"spd":1.4},{"t":1794376223000,"lat":22.615134,"lon":120.332324,"acc":15.0,"spd":1.7},{"t":1794376228000,"lat":22.615134,"lon":120.332406,"acc":15.0,"spd":1.2},{"t":1794376233000,"lat":22.615131,"lon":120.332613,"acc":15.0,"spd":1.4},{"t":1794376238000,"lat":22.615225,"lon":120.332456,"acc":15.0,"spd":1.3},{"t":1794376243000,"lat":22.615236,"lon":120.332433,"acc":15.0,"spd":1.3},{"t":1794376248000,"lat":22.615303,"lon":120.332442,"acc":15.0,"spd":1.2},{"t":1794376253000,"lat":22.615442,"lon":120.332575,"acc":15.0,"spd":1.7},{"t":1794376258000,"lat":22.615375,"lon":120.332399,"acc":15.0,"spd":1.6},{"t":1794376263000,"lat":22.615372,"lon":120.332505,"acc":15.0,"spd":1.7},{"t":1794376268000,"lat":22.615635,"lon":120.332577,"acc":15.0,"spd":1.6},{"t":1794376273000,"lat":22.615779,"lon":120.332511,"acc":15.0,"spd":1.3},{"t":1794376278000,"lat":22.615795,"lon":120.332296,"acc":15.0,"spd":1.4},{"t":1794376283000,"lat":22.615701,"lon":120.332333,"acc":15.0,"spd":1.5},{"t":1794376288000,"lat":22.615923,"lon":120.332356,"acc":15.0,"spd":1.6},{"t":1794376293000,"lat":22.615907,"lon":120.332485,"acc":15.0,"spd":1.3},{"t":1794376298000,"lat":22.615948,"lon":120.33234,"acc":15.0,"spd":1.5},{"t":1794376303000,"lat":22.615875,"lon":120.332375,"acc":15.0,"spd":1.1},{"t":1794376308000,"lat":22.6161,"lon":120.33244,"acc":15.0,"spd":1.2},{"t":1794376313000,"lat":22.616043,"lon":120.332323,"acc":15.0,"spd":1.5},{"t":1794376318000,"lat":22.616259,"lon":120.332266,"acc":15.0,"spd":1.5},{"t":1794376323000,"lat":22.616231,"lon":120.332136,"acc":15.0,"spd":1.3},{"t":1794376328000,"lat":22.616443,"lon":120.332342,"acc":15.0,"spd":1.7},{"t":1794376333000,"lat":22.616292,"lon":120.332097,"acc":15.0,"spd":1.6},{"t":1794376338000,"lat":22.616572,"lon":120.332039,"acc":15.0,"spd":1.4},{"t":1794376343000,"lat":22.616625,"lon":120.332361,"acc":15.0,"spd":1.4},{"t":1794376348000,"lat":22.616551,"lon":120.332302,"acc":15.0,"spd":1.1},{"t":1794376353000,"lat":22.616649,"lon":120.332034,"acc":15.0,"spd":1.3},{"t":1794376358000,"lat":22.616617,"lon":120.332362,"acc":15.0,"spd":1.3},{"t":1794376363000,"lat":22.616891,"lon":120.332227,"acc":15.0,"spd":1.0},{"t":1794376368000,"lat":22.616958,"lon":120.332345,"acc":15.0,"spd":1.3},{"t":1794376373000,"lat":22.617065,"lon":120.332117,"acc":15.0,"spd":1.4},{"t":1794376378000,"lat":22.616947,"lon":120.332373,"acc":15.0,"spd":1.1},{"t":1794376383000,"lat":22.617286,"lon":120.332245,"acc":15.0,"spd":1.3},{"t":1794376388000,"lat":22.617243,"lon":120.332292,"acc":15.0,"spd":1.2},{"t":1794376393000,"lat":22.617118,"lon":120.332335,"acc":15.0,"spd":1.5},{"t":1794376398000,"lat":22.617195,"lon":120.332278,"acc":15.0,"spd":1.1},{"t":1794376403000,"lat":22.617334,"lon":120.332144,"acc":15.0,"spd":1.3},{"t":1794376408000,"lat":22.617147,"lon":120.332291,"acc":15.0,"spd":1.4},{"t":1794376413000,"lat":22.617367,"lon":120.332192,"acc":15.0,"spd":1.2},{"t":1794376418000,"lat":22.617398,"lon":120.332211,"acc":15.0,"spd":1.5},{"t":1794376423000,"lat":22.617626,"lon":120.332441,"acc":15.0,"spd":1.4},{"t":1794376428000,"lat":22.617454,"lon":120.332091,"acc":15.0,"spd":1.4},{"t":1794376433000,"lat":22.617704,"lon":120.332267,"acc":15.0,"spd":1.2},{"t":1794376438000,"lat":22.617805,"lon":120.33223,"acc":15.0,"spd":1.1},{"t":1794376443000,"lat":22.617675,"lon":120.332233,"acc":15.0,"spd":1.2},{"t":1794376448000,"lat":22.617798,"lon":120.332145,"acc":15.0,"spd":1.4},{"t":1794376453000,"lat":22.617985,"lon":120.332354,"acc":15.0,"spd":1.6},{"t":1794376458000,"lat":22.618281,"lon":120.332135,"acc":15.0,"spd":1.2},{"t":1794376463000,"lat":22.617974,"lon":120.332273,"acc":15.0,"spd":1.2},{"t":1794376468000,"lat":22.61814,"lon":120.332396,"acc":15.0,"spd":1.6},{"t":1794376473000,"lat":22.618065,"lon":120.332164,"acc":15.0,"spd":1.2},{"t":1794376478000,"lat":22.618219,"lon":120.332157,"acc":15.0,"spd":1.5},{"t":1794376483000,"lat":22.618375,"lon":120.332093,"acc":15.0,"spd":1.5},{"t":1794376488000,"lat":22.618304,"lon":120.332301,"acc":15.0,"spd":1.6},{"t":1794376493000,"lat":22.618459,"lon":120.332077,"acc":15.0,"spd":1.0},{"t":1794376498000,"lat":22.618559,"lon":120.332388,"acc":15.0,"spd":1.3},{"t":1794376503000,"lat":22.618617,"lon":120.332253,"acc":15.0,"spd":1.6},{"t":1794376508000,"lat":22.618641,"lon":120.332142,"acc":15.0,"spd":1.6},{"t":1794376513000,"lat":22.618677,"lon":120.332026,"acc":15.0,"spd":1.2},{"t":1794376518000,"lat":22.618785,"lon":120.332094,"acc":15.0,"spd":1.5},{"t":1794376523000,"lat":22.61889,"lon":120.332215,"acc":15.0,"spd":1.3},{"t":1794376528000,"lat":22.618829,"lon":120.332282,"acc":15.0,"spd":1.3},{"t":1794376533000,"lat":22.618927,"lon":120.332078,"acc":15.0,"spd":1.2},{"t":1794376538000,"lat":22.618905,"lon":120.332113,"acc":15.0,"spd":1.3},{"t":1794376543000,"lat":22.618977,"lon":120.331993,"acc":15.0,"spd":1.4},{"t":1794376548000,"lat":22.619192,"lon":120.332133,"acc":15.0,"spd":1.5},{"t":1794376553000,"lat":22.619168,"lon":120.331882,"acc":15.0,"spd":1.0},{"t":1794376558000,"lat":22.619137,"lon":120.331956,"acc":15.0,"spd":1.5},{"t":1794376563000,"lat":22.619332,"lon":120.332191,"acc":15.0,"spd":1.7},{"t":1794376568000,"lat":22.619372,"lon":120.332086,"acc":15.0,"spd":0.9},{"t":1794376573000,"lat":22.619364,"lon":120.331836,"acc":15.0,"spd":1.4},{"t":1794376578000,"lat":22.61967,"lon":120.331962,"acc":15.0,"spd":1.3},{"t":1794376583000,"lat":22.619471,"lon":120.33185,"acc":15.0,"spd":0.9},{"t":1794376588000,"lat":22.619585,"lon":120.332135,"acc":15.0,"spd":1.5},{"t":1794376593000,"lat":22.619609,"lon":120.332099,"acc":15.0,"spd":1.2},{"t":1794376598000,"lat":22.619941,"lon":120.331994,"acc":15.0,"spd":1.7},{"t":1794376603000,"lat":22.619744,"lon":120.331856,"acc":15.0,"spd":1.6},{"t":1794376608000,"lat":22.619995,"lon":120.331811,"acc":15.0,"spd":1.4},{"t":1794376613000,"lat":22.619861,"lon":120.331822,"acc":15.0,"spd":1.4},{"t":1794376618000,"lat":22.619905,"lon":120.331833,"acc":15.0,"spd":1.2},{"t":1794376623000,"lat":22.619881,"lon":120.331934,"acc":15.0,"spd":1.2},{"t":1794376628000,"lat":22.620196,"lon":120.331644,"acc":15.0,"spd":1.4},{"t":1794376633000,"lat":22.620224,"lon":120.331857,"acc":15.0,"spd":1.6},{"t":1794376638000,"lat":22.62019,"lon":120.33163,"acc":15.0,"spd":1.3},{"t":1794376643000,"lat":22.620266,"lon":120.331731,"acc":15.0,"spd":1.8},{"t":1794376648000,"lat":22.620581,"lon":120.331875,"acc":15.0,"spd":1.4},{"t":1794376653000,"lat":22.620509,"lon":120.331814,"acc":15.0,"spd":1.3},{"t":1794376658000,"lat":22.62049,"lon":120.331935,"acc":15.0,"spd":1.2},{"t":1794376663000,"lat":22.620534,"lon":120.33197,"acc":15.0,"spd":1.2},{"t":1794376668000,"lat":22.620761,"lon":120.331828,"acc":15.0,"spd":1.7},{"t":1794376673000,"lat":22.620854,"lon":120.331841,"acc":15.0,"spd":1.4},{"t":1794376678000,"lat":22.620628,"lon":120.331879,"acc":15.0,"spd":1.5},{"t":1794376683000,"lat":22.620952,"lon":120.331919,"acc":15.0,"spd":1.4},{"t":1794376688000,"lat":22.621029,"lon":120.331734,"acc":15.0,"spd":1.4},{"t":1794376693000,"lat":22.620961,"lon":120.331763,"acc":15.0,"spd":1.4},{"t":1794376698000,"lat":22.621112,"lon":120.331787,"acc":15.0,"spd":1.6},{"t":1794376703000,"lat":22.621055,"lon":120.331811,"acc":15.0,"spd":1.6},{"t":1794376708000,"lat":22.620862,"lon":120.331579,"acc":15.0,"spd":1.2},{"t":1794376713000,"lat":22.621277,"lon":120.331606,"acc":15.0,"spd":1.1},{"t":1794376718000,"lat":22.62109,"lon":120.33185,"acc":15.0,"spd":1.2},{"t":1794376723000,"lat":22.621352,"lon":120.331785,"acc":15.0,"spd":1.4},{"t":1794376728000,"lat":22.621367,"lon":120.33183,"acc":15.0,"spd":1.3},{"t":1794376733000,"lat":22.62135,"lon":120.331708,"acc":15.0,"spd":1.5},{"t":1794376738000,"lat":22.621439,"lon":120.331599,"acc":15.0,"spd":1.3},{"t":1794376743000,"lat":22.62144,"lon":120.331799,"acc":15.0,"spd":1.5},{"t":1794376748000,"lat":22.621523,"lon":120.331695,"acc":15.0,"spd":1.7},{"t":1794376753000,"lat":22.621671,"lon":120.331773,"acc":15.0,"spd":1.4},{"t":1794376758000,"lat":22.621433,"lon":120.331598,"acc":15.0,"spd":1.3},{"t":1794376763000,"lat":22.621592,"lon":120.331502,"acc":15.0,"spd":1.8},{"t":1794376768000,"lat":22.621573,"lon":120.331308,"acc":15.0,"spd":1.6},{"t":1794376773000,"lat":22.621692,"lon":120.331012,"acc":15.0,"spd":1.3},{"t":1794376778000,"lat":22.62165,"lon":120.331012,"acc":15.0,"spd":1.5},{"t":1794376783000,"lat":22.621556,"lon":120.331436,"acc":15.0,"spd":1.2},{"t":1794376788000,"lat":22.621689,"lon":120.330738,"acc":15.0,"spd":1.4},{"t":1794376793000,"lat":22.621416,"lon":120.330685,"acc":15.0,"spd":1.1},{"t":1794376798000,"lat":22.621593,"lon":120.330996,"acc":15.0,"spd":1.3},{"t":1794376803000,"lat":22.621762,"lon":120.330811,"acc":15.0,"spd":1.2},{"t":1794376808000,"lat":22.621892,"lon":120.330967,"acc":15.0,"spd":1.3},{"t":1794376813000,"lat":22.621551,"lon":120.330504,"acc":15.0,"spd":1.4},{"t":1794376818000,"lat":22.621673,"lon":120.330499,"acc":15.0,"spd":1.4},{"t":1794376823000,"lat":22.621776,"lon":120.330584,"acc":15.0,"spd":1.6},{"t":1794376828000,"lat":22.62177,"lon":120.330465,"acc":15.0,"spd":1.5},{"t":1794376833000,"lat":22.621631,"lon":120.330567,"acc":15.0,"spd":1.6},{"t":1794376838000,"lat":22.621666,"lon":120.330275,"acc":15.0,"spd":1.4},{"t":1794376843000,"lat":22.621701,"lon":120.330459,"acc":15.0,"spd":1.2},{"t":1794376848000,"lat":22.621857,"lon":120.330308,"acc":15.0,"spd":1.0},{"t":1794376853000,"lat":22.621786,"lon":120.330178,"acc":15.0,"spd":1.3},{"t":1794376858000,"lat":22.621999,"lon":120.33014,"acc":15.0,"spd":1.0},{"t":1794376863000,"lat":22.621839,"lon":120.329829,"acc":15.0,"spd":1.5},{"t":1794376868000,"lat":22.621799,"lon":120.33011,"acc":15.0,"spd":1.4},{"t":1794376873000,"lat":22.621896,"lon":120.329937,"acc":15.0,"spd":1.0},{"t":1794376878000,"lat":22.621861,"lon":120.329901,"acc":15.0,"spd":1.7},{"t":1794376883000,"lat":22.622008,"lon":120.329863,"acc":15.0,"spd":1.0},{"t":1794376888000,"lat":22.622009,"lon":120.329755,"acc":15.0,"spd":1.2},{"t":1794376893000,"lat":22.622205,"lon":120.329634,"acc":15.0,"spd":1.2},{"t":1794376898000,"lat":22.622006,"lon":120.329427,"acc":15.0,"spd":1.0},{"t":1794376903000,"lat":22.622061,"lon":120.329519,"acc":15.0,"spd":1.6},{"t":1794376908000,"lat":22.622182,"lon":120.329402,"acc":15.0,"spd":1.6},{"t":1794376913000,"lat":22.622214,"lon":120.329375,"acc":15.0,"spd":1.1},{"t":1794376918000,"lat":22.621946,"lon":120.329314,"acc":15.0,"spd":1.6},{"t":1794376923000,"lat":22.622088,"lon":120.329265,"acc":15.0,"spd":1.2},{"t":1794376928000,"lat":22.622308,"lon":120.329112,"acc":15.0,"spd":1.5},{"t":1794376933000,"lat":22.622194,"lon":120.329114,"acc":15.0,"spd":1.4},{"t":1794376938000,"lat":22.622092,"lon":120.329163,"acc":15.0,"spd":1.4},{"t":1794376943000,"lat":22.62189,"lon":120.329141,"acc":15.0,"spd":1.3},{"t":1794376948000,"lat":22.622042,"lon":120.329097,"acc":15.0,"spd":1.6},{"t":1794376953000,"lat":22.622025,"lon":120.329013,"acc":15.0,"spd":1.5},{"t":1794376958000,"lat":22.621949,"lon":120.328775,"acc":15.0,"spd":1.6},{"t":1794376963000,"lat":22.622014,"lon":120.32879,"acc":15.0,"spd":1.6},{"t":1794376968000,"lat":22.622076,"lon":120.328783,"acc":15.0,"spd":1.2},{"t":1794376973000,"lat":22.621998,"lon":120.328536,"acc":15.0,"spd":1.6},{"t":1794376978000,"lat":22.621805,"lon":120.328521,"acc":15.0,"spd":1.3},{"t":1794376983000,"lat":22.621944,"lon":120.328646,"acc":15.0,"spd":1.3},{"t":1794376988000,"lat":22.62191,"lon":120.328368,"acc":15.0,"spd":1.6},{"t":1794376993000,"lat":22.621777,"lon":120.328432,"acc":15.0,"spd":1.5},{"t":1794376998000,"lat":22.621851,"lon":120.328398,"acc":15.0,"spd":1.6},{"t":1794377003000,"lat":22.621877,"lon":120.328271,"acc":15.0,"spd":1.0},{"t":1794377008000,"lat":22.621843,"lon":120.328245,"acc":15.0,"spd":1.6},{"t":1794377013000,"lat":22.621799,"lon":120.327947,"acc":15.0,"spd":1.3},{"t":1794377018000,"lat":22.621598,"lon":120.327929,"acc":15.0,"spd":1.6},{"t":1794377023000,"lat":22.621742,"lon":120.32815,"acc":15.0,"spd":1.5},{"t":1794377028000,"lat":22.621536,"lon":120.327754,"acc":15.0,"spd":1.3},{"t":1794377033000,"lat":22.621611,"lon":120.327814,"acc":15.0,"spd":1.5},{"t":1794377038000,"lat":22.621682,"lon":120.327876,"acc":15.0,"spd":1.6},{"t":1794377043000,"lat":22.621716,"lon":120.327651,"acc":15.0,"spd":1.6},{"t":1794377048000,"lat":22.621787,"lon":120.327849,"acc":15.0,"spd":0.9},{"t":1794377053000,"lat":22.621575,"lon":120.32762,"acc":15.0,"spd":1.4},{"t":1794377058000,"lat":22.621435,"lon":120.32754,"acc":15.0,"spd":1.3},{"t":1794377063000,"lat":22.621627,"lon":120.327574,"acc":15.0,"spd":1.0},{"t":1794377068000,"lat":22.621487,"lon":120.3273,"acc":15.0,"spd":1.4},{"t":1794377073000,"lat":22.62134,"lon":120.327175,"acc":15.0,"spd":1.6},{"t":1794377078000,"lat":22.621394,"lon":120.327418,"acc":15.0,"spd":1.7},{"t":1794377083000,"lat":22.621331,"lon":120.327218,"acc":15.0,"spd":1.2},{"t":1794377088000,"lat":22.62139,"lon":120.327195,"acc":15.0,"spd":1.4},{"t":1794377093000,"lat":22.621535,"lon":120.327205,"acc":15.0,"spd":1.4},{"t":1794377098000,"lat":22.621351,"lon":120.327298,"acc":15.0,"spd":1.4},{"t":1794377103000,"lat":22.621292,"lon":120.326982,"acc":15.0,"spd":1.1},{"t":1794377108000,"lat":22.621337,"lon":120.32665,"acc":15.0,"spd":1.5},{"t":1794377113000,"lat":22.621234,"lon":120.326772,"acc":15.0,"spd":1.7},{"t":1794377118000,"lat":22.621324,"lon":120.326658,"acc":15.0,"spd":1.3},{"t":1794377123000,"lat":22.621323,"lon":120.32665,"acc":15.0,"spd":1.1},{"t":1794377128000,"lat":22.621556,"lon":120.326646,"acc":15.0,"spd":1.5},{"t":1794377133000,"lat":22.621288,"lon":120.326628,"acc":15.0,"spd":1.2},{"t":1794377138000,"lat":22.62109,"lon":120.326561,"acc":15.0,"spd":1.8},{"t":1794377143000,"lat":22.621125,"lon":120.326342,"acc":15.0,"spd":1.1},{"t":1794377148000,"lat":22.621346,"lon":120.326187,"acc":15.0,"spd":1.3},{"t":1794377153000,"lat":22.621292,"lon":120.326027,"acc":15.0,"spd":1.4},{"t":1794377158000,"lat":22.621161,"lon":120.326169,"acc":15.0,"spd":1.3},{"t":1794377163000,"lat":22.620966,"lon":120.3261,"acc":15.0,"spd":1.2},{"t":1794377168000,"lat":22.62092,"lon":120.326192,"acc":15.0,"spd":1.2},{"t":1794377173000,"lat":22.62096,"lon":120.326102,"acc":15.0,"spd":1.5},{"t":1794377178000,"lat":22.621118,"lon":120.325969,"acc":15.0,"spd":1.3},{"t":1794377183000,"lat":22.621097,"lon":120.325986,"acc":15.0,"spd":1.2},{"t":1794377188000,"lat":22.620967,"lon":120.325834,"acc":15.0,"spd":1.3},{"t":1794377193000,"lat":22.620928,"lon":120.325764,"acc":15.0,"spd":1.7},{"t":1794377198000,"lat":22.621029,"lon":120.325758,"acc":15.0,"spd":1.2},{"t":1794377203000,"lat":22.620931,"lon":120.325643,"acc":15.0,"spd":1.5},{"t":1794377208000,"lat":22.620898,"lon":120.325711,"acc":15.0,"spd":1.3},{"t":1794377213000,"lat":22.620882,"lon":120.325499,"acc":15.0,"spd":1.8},{"t":1794377218000,"lat":22.620907,"lon":120.325605,"acc":15.0,"spd":1.3},{"t":1794377223000,"lat":22.620801,"lon":120.325437,"acc":15.0,"spd":1.3},{"t":1794377228000,"lat":22.620709,"lon":120.325607,"acc":15.0,"spd":1.3},{"t":1794377233000,"lat":22.620688,"lon":120.325388,"acc":15.0,"spd":1.5},{"t":1794377238000,"lat":22.620714,"lon":120.325226,"acc":15.0,"spd":1.4},{"t":1794377243000,"lat":22.620793,"lon":120.325334,"acc":15.0,"spd":1.4},{"t":1794377248000,"lat":22.620859,"lon":120.325101,"acc":15.0,"spd":1.9},{"t":1794377253000,"lat":22.620678,"lon":120.325169,"acc":15.0,"spd":1.3},{"t":1794377258000,"lat":22.620654,"lon":120.324952,"acc":15.0,"spd":1.5},{"t":1794377263000,"lat":22.620672,"lon":120.324923,"acc":15.0,"spd":1.1},{"t":1794377268000,"lat":22.620705,"lon":120.325029,"acc":15.0,"spd":1.6},{"t":1794377273000,"lat":22.620471,"lon":120.324561,"acc":15.0,"spd":1.5},{"t":1794377278000,"lat":22.620809,"lon":120.324603,"acc":15.0,"spd":1.4},{"t":1794377283000,"lat":22.620494,"lon":120.324811,"acc":15.0,"spd":1.2},{"t":1794377288000,"lat":22.620711,"lon":120.324371,"acc":15.0,"spd":1.4},{"t":1794377293000,"lat":22.620566,"lon":120.324523,"acc":15.0,"spd":1.3},{"t":1794377298000,"lat":22.620341,"lon":120.324477,"acc":15.0,"spd":1.3},{"t":1794377303000,"lat":22.620405,"lon":120.324148,"acc":15.0,"spd":1.6},{"t":1794377308000,"lat":22.62024,"lon":120.324201,"acc":15.0,"spd":1.1},{"t":1794377313000,"lat":22.62046,"lon":120.324357,"acc":15.0,"spd":1.2},{"t":1794377318000,"lat":22.620428,"lon":120.324381,"acc":15.0,"spd":1.6},{"t":1794377323000,"lat":22.620354,"lon":120.324039,"acc":15.0,"spd":1.3},{"t":1794377328000,"lat":22.62044,"lon":120.324012,"acc":15.0,"spd":1.3},{"t":1794377333000,"lat":22.620272,"lon":120.32394,"acc":15.0,"spd":1.5},{"t":1794377338000,"lat":22.620346,"lon":120.324133,"acc":15.0,"spd":1.3},{"t":1794377343000,"lat":22.620198,"lon":120.324045,"acc":15.0,"spd":1.4},{"t":1794377348000,"lat":22.620254,"lon":120.323619,"acc":15.0,"spd":1.2},{"t":1794377353000,"lat":22.62037,"lon":120.323654,"acc":15.0,"spd":1.7},{"t":1794377358000,"lat":22.620089,"lon":120.323623,"acc":15.0,"spd":1.6},{"t":1794377363000,"lat":22.62015,"lon":120.323658,"acc":15.0,"spd":1.2},{"t":1794377368000,"lat":22.620264,"lon":120.323597,"acc":15.0,"spd":1.4},{"t":1794377373000,"lat":22.620052,"lon":120.323512,"acc":15.0,"spd":1.4},{"t":1794377378000,"lat":22.620318,"lon":120.323528,"acc":15.0,"spd":1.2},{"t":1794377383000,"lat":22.620074,"lon":120.323384,"acc":15.0,"spd":1.5},{"t":1794377388000,"lat":22.620089,"lon":120.323235,"acc":15.0,"spd":1.2},{"t":1794377393000,"lat":22.620176,"lon":120.323168,"acc":15.0,"spd":1.3},{"t":1794377398000,"lat":22.620247,"lon":120.323233,"acc":15.0,"spd":1.2},{"t":1794377403000,"lat":22.620046,"lon":120.32316,"acc":15.0,"spd":1.6},{"t":1794377408000,"lat":22.620007,"lon":120.323223,"acc":15.0,"spd":1.7}]} \ No newline at end of file diff --git a/android/app/src/test/resources/ride_replay_TPE157463_0.json b/android/app/src/test/resources/ride_replay_TPE157463_0.json new file mode 100644 index 0000000..6c676f5 --- /dev/null +++ b/android/app/src/test/resources/ride_replay_TPE157463_0.json @@ -0,0 +1 @@ +{"route":"TPE157463","pathId":0,"plate":"EAL-1162","boardedAtMs":1794376456264,"boardingStopIdRaw":"15232","destinationStopIdRaw":"15242","stops":[{"stopIdRaw":"194687","sequence":1,"lat":25.022559,"lon":121.452109},{"stopIdRaw":"194688","sequence":2,"lat":25.026206,"lon":121.452324},{"stopIdRaw":"221716","sequence":3,"lat":25.0258419392865,"lon":121.453808034732},{"stopIdRaw":"15198","sequence":4,"lat":25.023423,"lon":121.454215},{"stopIdRaw":"15199","sequence":5,"lat":25.020666,"lon":121.454666},{"stopIdRaw":"15200","sequence":6,"lat":25.01938238,"lon":121.45531395},{"stopIdRaw":"15201","sequence":7,"lat":25.016976,"lon":121.456421},{"stopIdRaw":"15202","sequence":8,"lat":25.014996,"lon":121.45642},{"stopIdRaw":"15203","sequence":9,"lat":25.01274499,"lon":121.4561617},{"stopIdRaw":"15204","sequence":10,"lat":25.010986,"lon":121.456883},{"stopIdRaw":"15205","sequence":11,"lat":25.009036,"lon":121.458454},{"stopIdRaw":"15206","sequence":12,"lat":25.013298,"lon":121.460871},{"stopIdRaw":"15207","sequence":13,"lat":25.01277,"lon":121.462542},{"stopIdRaw":"153799","sequence":14,"lat":25.0118568,"lon":121.4643742},{"stopIdRaw":"153800","sequence":15,"lat":25.009624,"lon":121.465209},{"stopIdRaw":"15208","sequence":16,"lat":25.00839,"lon":121.46584},{"stopIdRaw":"15209","sequence":17,"lat":25.007531,"lon":121.46822},{"stopIdRaw":"15210","sequence":18,"lat":25.006683,"lon":121.470497},{"stopIdRaw":"15211","sequence":19,"lat":25.00622954,"lon":121.4730341},{"stopIdRaw":"15212","sequence":20,"lat":25.005765,"lon":121.47597},{"stopIdRaw":"15213","sequence":21,"lat":25.005751,"lon":121.478674},{"stopIdRaw":"199119","sequence":22,"lat":25.004871,"lon":121.480278},{"stopIdRaw":"15214","sequence":23,"lat":25.00395,"lon":121.480636},{"stopIdRaw":"15215","sequence":24,"lat":25.000604,"lon":121.481193},{"stopIdRaw":"15216","sequence":25,"lat":24.998038,"lon":121.481182},{"stopIdRaw":"15217","sequence":26,"lat":24.99579873,"lon":121.4809504},{"stopIdRaw":"15218","sequence":27,"lat":24.995553,"lon":121.482957},{"stopIdRaw":"15219","sequence":28,"lat":24.996128,"lon":121.484429},{"stopIdRaw":"15220","sequence":29,"lat":24.99626066,"lon":121.4903845},{"stopIdRaw":"15222","sequence":30,"lat":24.9965568353221,"lon":121.492320399314},{"stopIdRaw":"15224","sequence":31,"lat":24.99819818,"lon":121.4951329},{"stopIdRaw":"15226","sequence":32,"lat":24.9996352583255,"lon":121.496428401096},{"stopIdRaw":"201702","sequence":33,"lat":25.001888,"lon":121.496889},{"stopIdRaw":"15228","sequence":34,"lat":25.003062,"lon":121.496762},{"stopIdRaw":"15230","sequence":35,"lat":25.01987,"lon":121.497706},{"stopIdRaw":"15232","sequence":36,"lat":25.0232906952259,"lon":121.499894100772},{"stopIdRaw":"15234","sequence":37,"lat":25.025045,"lon":121.500485},{"stopIdRaw":"15236","sequence":38,"lat":25.028737,"lon":121.500599},{"stopIdRaw":"15238","sequence":39,"lat":25.0308055,"lon":121.500635},{"stopIdRaw":"15240","sequence":40,"lat":25.03113,"lon":121.50255},{"stopIdRaw":"15242","sequence":41,"lat":25.03268103989999,"lon":121.50499126498305},{"stopIdRaw":"15244","sequence":42,"lat":25.0357,"lon":121.506607},{"stopIdRaw":"15246","sequence":43,"lat":25.041730058378,"lon":121.508285900629},{"stopIdRaw":"15248","sequence":44,"lat":25.0456964941835,"lon":121.509380726049},{"stopIdRaw":"15250","sequence":45,"lat":25.0465635496954,"lon":121.515689438859},{"stopIdRaw":"15252","sequence":46,"lat":25.04483077,"lon":121.5215079},{"stopIdRaw":"15254","sequence":47,"lat":25.0437303889139,"lon":121.522278540137},{"stopIdRaw":"15256","sequence":48,"lat":25.046759,"lon":121.523978},{"stopIdRaw":"15258","sequence":49,"lat":25.04809523,"lon":121.524405},{"stopIdRaw":"15260","sequence":50,"lat":25.05059,"lon":121.52495},{"stopIdRaw":"15262","sequence":51,"lat":25.0520078098529,"lon":121.529855025946},{"stopIdRaw":"15264","sequence":52,"lat":25.0519912439505,"lon":121.532641149751},{"stopIdRaw":"15266","sequence":53,"lat":25.0519389515979,"lon":121.53627250499},{"stopIdRaw":"15268","sequence":54,"lat":25.05189813653983,"lon":121.54002052720857},{"stopIdRaw":"15270","sequence":55,"lat":25.0518040049938,"lon":121.543561439976},{"stopIdRaw":"15272","sequence":56,"lat":25.0517035084006,"lon":121.548353475095},{"stopIdRaw":"15274","sequence":57,"lat":25.051594154729063,"lon":121.55469770362868},{"stopIdRaw":"15276","sequence":58,"lat":25.05148353485096,"lon":121.5602663380149},{"stopIdRaw":"15278","sequence":59,"lat":25.0512559293088,"lon":121.566535683299},{"stopIdRaw":"15280","sequence":60,"lat":25.05111499,"lon":121.5688141},{"stopIdRaw":"15282","sequence":61,"lat":25.05220024,"lon":121.5704299},{"stopIdRaw":"15284","sequence":62,"lat":25.055027,"lon":121.569875},{"stopIdRaw":"15286","sequence":63,"lat":25.0577179708185,"lon":121.569393769344},{"stopIdRaw":"15290","sequence":64,"lat":25.06018904,"lon":121.5676949},{"stopIdRaw":"15292","sequence":65,"lat":25.062445,"lon":121.566269},{"stopIdRaw":"15288","sequence":66,"lat":25.06530387,"lon":121.5657277}],"busReports":[{"id":"EAA-150","dir":0,"lat":25.051362,"lon":121.569912,"t":1794376137000,"rx":1794376159195},{"id":"EAL-0072","dir":0,"lat":24.996637,"lon":121.492255,"t":1794376132000,"rx":1794376159195},{"id":"EAL-0080","dir":0,"lat":25.02065,"lon":121.454847,"t":1794376143000,"rx":1794376159195},{"id":"EAL-0081","dir":0,"lat":25.006282,"lon":121.473548,"t":1794376135000,"rx":1794376159195},{"id":"EAL-0097","dir":0,"lat":25.01152,"lon":121.46478,"t":1794376135000,"rx":1794376159195},{"id":"EAL-0236","dir":0,"lat":25.022667,"lon":121.49937,"t":1794376141000,"rx":1794376159195},{"id":"EAL-1157","dir":0,"lat":25.008973,"lon":121.45889,"t":1794376147000,"rx":1794376159195},{"id":"EAL-1161","dir":0,"lat":25.046613,"lon":121.51605,"t":1794376146000,"rx":1794376159195},{"id":"EAL-1162","dir":0,"lat":25.00514,"lon":121.496128,"t":1794376144000,"rx":1794376159195},{"id":"EAL-1168","dir":0,"lat":25.031327,"lon":121.503733,"t":1794376140000,"rx":1794376159195},{"id":"EAL-1171","dir":0,"lat":25.060372,"lon":121.567445,"t":1794376136000,"rx":1794376159195},{"id":"EAL-1173","dir":0,"lat":25.051192,"lon":121.56933,"t":1794376136000,"rx":1794376159195},{"id":"EAL-1299","dir":0,"lat":25.054982,"lon":121.569928,"t":1794376137000,"rx":1794376159195},{"id":"EAL-2266","dir":0,"lat":25.04567,"lon":121.50954,"t":1794376136000,"rx":1794376159195},{"id":"EAL-2267","dir":0,"lat":25.020692,"lon":121.498098,"t":1794376148000,"rx":1794376159195},{"id":"EAL-0072","dir":0,"lat":24.9971,"lon":121.493345,"t":1794376152000,"rx":1794376168901},{"id":"EAL-0081","dir":0,"lat":25.006292,"lon":121.473568,"t":1794376155000,"rx":1794376168901},{"id":"EAL-0097","dir":0,"lat":25.011502,"lon":121.464805,"t":1794376155000,"rx":1794376168901},{"id":"EAL-2267","dir":0,"lat":25.021033,"lon":121.498368,"t":1794376155000,"rx":1794376168901},{"id":"EAA-150","dir":0,"lat":25.051368,"lon":121.569913,"t":1794376157000,"rx":1794376180724},{"id":"EAL-0080","dir":0,"lat":25.020175,"lon":121.454952,"t":1794376165000,"rx":1794376180724},{"id":"EAL-0236","dir":0,"lat":25.023803,"lon":121.500163,"t":1794376157000,"rx":1794376180724},{"id":"EAL-1157","dir":0,"lat":25.008973,"lon":121.458888,"t":1794376167000,"rx":1794376180724},{"id":"EAL-1161","dir":0,"lat":25.046475,"lon":121.516588,"t":1794376166000,"rx":1794376180724},{"id":"EAL-1162","dir":0,"lat":25.007122,"lon":121.495417,"t":1794376164000,"rx":1794376180724},{"id":"EAL-1168","dir":0,"lat":25.03133,"lon":121.503733,"t":1794376160000,"rx":1794376180724},{"id":"EAL-1171","dir":0,"lat":25.060748,"lon":121.56714,"t":1794376167000,"rx":1794376180724},{"id":"EAL-1173","dir":0,"lat":25.051187,"lon":121.569333,"t":1794376156000,"rx":1794376180724},{"id":"EAL-1299","dir":0,"lat":25.056717,"lon":121.569568,"t":1794376167000,"rx":1794376180724},{"id":"EAL-2266","dir":0,"lat":25.046543,"lon":121.50969,"t":1794376167000,"rx":1794376180724},{"id":"EAL-0072","dir":0,"lat":24.997302,"lon":121.493515,"t":1794376172000,"rx":1794376193174},{"id":"EAL-0080","dir":0,"lat":25.019608,"lon":121.455247,"t":1794376171000,"rx":1794376193174},{"id":"EAL-0081","dir":0,"lat":25.00627,"lon":121.47371,"t":1794376175000,"rx":1794376193174},{"id":"EAL-0097","dir":0,"lat":25.01145,"lon":121.464812,"t":1794376175000,"rx":1794376193174},{"id":"EAL-2267","dir":0,"lat":25.022483,"lon":121.49924,"t":1794376175000,"rx":1794376193174},{"id":"EAA-150","dir":0,"lat":25.051317,"lon":121.569867,"t":1794376197000,"rx":1794376206585},{"id":"EAL-0072","dir":0,"lat":24.997305,"lon":121.493512,"t":1794376192000,"rx":1794376206585},{"id":"EAL-0080","dir":0,"lat":25.019308,"lon":121.455277,"t":1794376183000,"rx":1794376206585},{"id":"EAL-0081","dir":0,"lat":25.006045,"lon":121.475322,"t":1794376196000,"rx":1794376206585},{"id":"EAL-0097","dir":0,"lat":25.010132,"lon":121.465532,"t":1794376195000,"rx":1794376206585},{"id":"EAL-0236","dir":0,"lat":25.025217,"lon":121.500535,"t":1794376197000,"rx":1794376206585},{"id":"EAL-1157","dir":0,"lat":25.008882,"lon":121.459645,"t":1794376194000,"rx":1794376206585},{"id":"EAL-1161","dir":0,"lat":25.046363,"lon":121.516978,"t":1794376186000,"rx":1794376206585},{"id":"EAL-1162","dir":0,"lat":25.00928,"lon":121.495455,"t":1794376184000,"rx":1794376206585},{"id":"EAL-1168","dir":0,"lat":25.031315,"lon":121.503722,"t":1794376180000,"rx":1794376206585},{"id":"EAL-1171","dir":0,"lat":25.062418,"lon":121.566275,"t":1794376196000,"rx":1794376206585},{"id":"EAL-1173","dir":0,"lat":25.051187,"lon":121.569338,"t":1794376196000,"rx":1794376206585},{"id":"EAL-1299","dir":0,"lat":25.058962,"lon":121.569083,"t":1794376197000,"rx":1794376206585},{"id":"EAL-2266","dir":0,"lat":25.048162,"lon":121.510463,"t":1794376196000,"rx":1794376206585},{"id":"EAL-2267","dir":0,"lat":25.023798,"lon":121.500078,"t":1794376195000,"rx":1794376206585},{"id":"EAA-150","dir":0,"lat":25.051927,"lon":121.570378,"t":1794376224000,"rx":1794376231055},{"id":"EAL-0072","dir":0,"lat":24.997362,"lon":121.493583,"t":1794376212000,"rx":1794376231055},{"id":"EAL-0080","dir":0,"lat":25.01755,"lon":121.456193,"t":1794376223000,"rx":1794376231055},{"id":"EAL-0081","dir":0,"lat":25.006402,"lon":121.47662,"t":1794376215000,"rx":1794376231055},{"id":"EAL-0097","dir":0,"lat":25.009588,"lon":121.465322,"t":1794376215000,"rx":1794376231055},{"id":"EAL-0236","dir":0,"lat":25.026015,"lon":121.500582,"t":1794376217000,"rx":1794376231055},{"id":"EAL-1157","dir":0,"lat":25.009062,"lon":121.459785,"t":1794376207000,"rx":1794376231055},{"id":"EAL-1161","dir":0,"lat":25.046302,"lon":121.517203,"t":1794376206000,"rx":1794376231055},{"id":"EAL-1162","dir":0,"lat":25.013623,"lon":121.495552,"t":1794376224000,"rx":1794376231055},{"id":"EAL-1168","dir":0,"lat":25.031295,"lon":121.503828,"t":1794376220000,"rx":1794376231055},{"id":"EAL-1171","dir":0,"lat":25.06249,"lon":121.566233,"t":1794376216000,"rx":1794376231055},{"id":"EAL-1173","dir":0,"lat":25.051167,"lon":121.570018,"t":1794376217000,"rx":1794376231055},{"id":"EAL-1299","dir":0,"lat":25.059078,"lon":121.568983,"t":1794376217000,"rx":1794376231055},{"id":"EAL-2266","dir":0,"lat":25.047532,"lon":121.512113,"t":1794376216000,"rx":1794376231055},{"id":"EAL-2267","dir":0,"lat":25.024523,"lon":121.500352,"t":1794376215000,"rx":1794376231055},{"id":"EAA-150","dir":0,"lat":25.053057,"lon":121.570193,"t":1794376237000,"rx":1794376254335},{"id":"EAL-0072","dir":0,"lat":24.997832,"lon":121.4943,"t":1794376232000,"rx":1794376254335},{"id":"EAL-0081","dir":0,"lat":25.005695,"lon":121.477088,"t":1794376239000,"rx":1794376254335},{"id":"EAL-0097","dir":0,"lat":25.009383,"lon":121.465198,"t":1794376235000,"rx":1794376254335},{"id":"EAL-0236","dir":0,"lat":25.026083,"lon":121.500545,"t":1794376237000,"rx":1794376254335},{"id":"EAL-1157","dir":0,"lat":25.010802,"lon":121.461342,"t":1794376227000,"rx":1794376254335},{"id":"EAL-1161","dir":0,"lat":25.046338,"lon":121.517218,"t":1794376226000,"rx":1794376254335},{"id":"EAL-1168","dir":0,"lat":25.031347,"lon":121.503973,"t":1794376231000,"rx":1794376254335},{"id":"EAL-1171","dir":0,"lat":25.062852,"lon":121.566078,"t":1794376236000,"rx":1794376254335},{"id":"EAL-1173","dir":0,"lat":25.051887,"lon":121.570415,"t":1794376236000,"rx":1794376254335},{"id":"EAL-1299","dir":0,"lat":25.05942,"lon":121.56847,"t":1794376237000,"rx":1794376254335},{"id":"EAL-2266","dir":0,"lat":25.04701,"lon":121.514042,"t":1794376236000,"rx":1794376254335},{"id":"EAL-2267","dir":0,"lat":25.025097,"lon":121.500505,"t":1794376235000,"rx":1794376254335},{"id":"EAA-150","dir":0,"lat":25.054225,"lon":121.570087,"t":1794376257000,"rx":1794376276680},{"id":"EAL-0072","dir":0,"lat":24.998227,"lon":121.494957,"t":1794376252000,"rx":1794376276680},{"id":"EAL-0080","dir":0,"lat":25.016438,"lon":121.456527,"t":1794376263000,"rx":1794376276680},{"id":"EAL-0081","dir":0,"lat":25.00584,"lon":121.478535,"t":1794376255000,"rx":1794376276680},{"id":"EAL-0097","dir":0,"lat":25.009195,"lon":121.465118,"t":1794376255000,"rx":1794376276680},{"id":"EAL-0236","dir":0,"lat":25.026055,"lon":121.500548,"t":1794376257000,"rx":1794376276680},{"id":"EAL-1157","dir":0,"lat":25.011257,"lon":121.46172,"t":1794376267000,"rx":1794376276680},{"id":"EAL-1161","dir":0,"lat":25.046262,"lon":121.517378,"t":1794376266000,"rx":1794376276680},{"id":"EAL-1162","dir":0,"lat":25.017432,"lon":121.496095,"t":1794376264000,"rx":1794376276680},{"id":"EAL-1168","dir":0,"lat":25.032085,"lon":121.504515,"t":1794376260000,"rx":1794376276680},{"id":"EAL-1171","dir":0,"lat":25.062857,"lon":121.566085,"t":1794376256000,"rx":1794376276680},{"id":"EAL-1173","dir":0,"lat":25.05395,"lon":121.57008,"t":1794376256000,"rx":1794376276680},{"id":"EAL-1299","dir":0,"lat":25.059725,"lon":121.568095,"t":1794376257000,"rx":1794376276680},{"id":"EAL-2266","dir":0,"lat":25.046703,"lon":121.515448,"t":1794376256000,"rx":1794376276680},{"id":"EAL-2267","dir":0,"lat":25.025092,"lon":121.500507,"t":1794376255000,"rx":1794376276680},{"id":"EAA-150","dir":0,"lat":25.054278,"lon":121.570082,"t":1794376277000,"rx":1794376300571},{"id":"EAL-0072","dir":0,"lat":24.998387,"lon":121.495215,"t":1794376272000,"rx":1794376300571},{"id":"EAL-0080","dir":0,"lat":25.015747,"lon":121.456413,"t":1794376285000,"rx":1794376300571},{"id":"EAL-0081","dir":0,"lat":25.005848,"lon":121.47865,"t":1794376275000,"rx":1794376300571},{"id":"EAL-0097","dir":0,"lat":25.009188,"lon":121.46512,"t":1794376275000,"rx":1794376300571},{"id":"EAL-0236","dir":0,"lat":25.026525,"lon":121.50053,"t":1794376277000,"rx":1794376300571},{"id":"EAL-1162","dir":0,"lat":25.01939,"lon":121.497292,"t":1794376284000,"rx":1794376300571},{"id":"EAL-1168","dir":0,"lat":25.032603,"lon":121.504913,"t":1794376280000,"rx":1794376300571},{"id":"EAL-1171","dir":0,"lat":25.062862,"lon":121.566085,"t":1794376276000,"rx":1794376300571},{"id":"EAL-1173","dir":0,"lat":25.054045,"lon":121.570148,"t":1794376276000,"rx":1794376300571},{"id":"EAL-1299","dir":0,"lat":25.060237,"lon":121.567668,"t":1794376277000,"rx":1794376300571},{"id":"EAL-2266","dir":0,"lat":25.046533,"lon":121.515635,"t":1794376276000,"rx":1794376300571},{"id":"EAL-2267","dir":0,"lat":25.025222,"lon":121.500507,"t":1794376275000,"rx":1794376300571},{"id":"EAA-150","dir":0,"lat":25.05574,"lon":121.569775,"t":1794376305000,"rx":1794376312722},{"id":"EAA-151","dir":0,"lat":25.023818,"lon":121.450015,"t":1794376305000,"rx":1794376312722},{"id":"EAL-0072","dir":0,"lat":24.999408,"lon":121.496043,"t":1794376299000,"rx":1794376312722},{"id":"EAL-0080","dir":0,"lat":25.0152,"lon":121.456417,"t":1794376303000,"rx":1794376312722},{"id":"EAL-0081","dir":0,"lat":25.006085,"lon":121.47951,"t":1794376295000,"rx":1794376312722},{"id":"EAL-0097","dir":0,"lat":25.009172,"lon":121.465122,"t":1794376295000,"rx":1794376312722},{"id":"EAL-0236","dir":0,"lat":25.027952,"lon":121.500578,"t":1794376297000,"rx":1794376312722},{"id":"EAL-1157","dir":0,"lat":25.011192,"lon":121.461643,"t":1794376287000,"rx":1794376312722},{"id":"EAL-1161","dir":0,"lat":25.045955,"lon":121.518333,"t":1794376286000,"rx":1794376312722},{"id":"EAL-1162","dir":0,"lat":25.019922,"lon":121.497662,"t":1794376304000,"rx":1794376312722},{"id":"EAL-1168","dir":0,"lat":25.033607,"lon":121.505372,"t":1794376300000,"rx":1794376312722},{"id":"EAL-1171","dir":0,"lat":25.062963,"lon":121.566062,"t":1794376296000,"rx":1794376312722},{"id":"EAL-1173","dir":0,"lat":25.054422,"lon":121.570057,"t":1794376296000,"rx":1794376312722},{"id":"EAL-1299","dir":0,"lat":25.06029,"lon":121.567612,"t":1794376297000,"rx":1794376312722},{"id":"EAL-2266","dir":0,"lat":25.046543,"lon":121.515867,"t":1794376297000,"rx":1794376312722},{"id":"EAL-2267","dir":0,"lat":25.026505,"lon":121.500542,"t":1794376305000,"rx":1794376312722},{"id":"EAA-150","dir":0,"lat":25.056743,"lon":121.569603,"t":1794376317000,"rx":1794376336865},{"id":"EAA-151","dir":0,"lat":25.023868,"lon":121.449793,"t":1794376312000,"rx":1794376336865},{"id":"EAL-0072","dir":0,"lat":24.999837,"lon":121.49632,"t":1794376312000,"rx":1794376336865},{"id":"EAL-0080","dir":0,"lat":25.014305,"lon":121.456333,"t":1794376327000,"rx":1794376336865},{"id":"EAL-0081","dir":0,"lat":25.006078,"lon":121.479553,"t":1794376315000,"rx":1794376336865},{"id":"EAL-0097","dir":0,"lat":25.008882,"lon":121.465007,"t":1794376315000,"rx":1794376336865},{"id":"EAL-0236","dir":0,"lat":25.029173,"lon":121.50056,"t":1794376324000,"rx":1794376336865},{"id":"EAL-1157","dir":0,"lat":25.012505,"lon":121.460613,"t":1794376327000,"rx":1794376336865},{"id":"EAL-1161","dir":0,"lat":25.045907,"lon":121.518538,"t":1794376326000,"rx":1794376336865},{"id":"EAL-1162","dir":0,"lat":25.020785,"lon":121.498232,"t":1794376324000,"rx":1794376336865},{"id":"EAL-1168","dir":0,"lat":25.034345,"lon":121.50584,"t":1794376320000,"rx":1794376336865},{"id":"EAL-1171","dir":0,"lat":25.063148,"lon":121.566002,"t":1794376316000,"rx":1794376336865},{"id":"EAL-1173","dir":0,"lat":25.056792,"lon":121.569587,"t":1794376319000,"rx":1794376336865},{"id":"EAL-1299","dir":0,"lat":25.060698,"lon":121.567183,"t":1794376325000,"rx":1794376336865},{"id":"EAL-2266","dir":0,"lat":25.046323,"lon":121.51682,"t":1794376316000,"rx":1794376336865},{"id":"EAL-2267","dir":0,"lat":25.02739,"lon":121.500537,"t":1794376316000,"rx":1794376336865},{"id":"EAA-150","dir":0,"lat":25.05873,"lon":121.569182,"t":1794376337000,"rx":1794376361149},{"id":"EAA-151","dir":0,"lat":25.025178,"lon":121.451098,"t":1794376343000,"rx":1794376361149},{"id":"EAL-0072","dir":0,"lat":24.99987,"lon":121.49633,"t":1794376352000,"rx":1794376361149},{"id":"EAL-0080","dir":0,"lat":25.013507,"lon":121.456248,"t":1794376343000,"rx":1794376361149},{"id":"EAL-0081","dir":0,"lat":25.004965,"lon":121.480252,"t":1794376355000,"rx":1794376361149},{"id":"EAL-0097","dir":0,"lat":25.008358,"lon":121.466158,"t":1794376355000,"rx":1794376361149},{"id":"EAL-0236","dir":0,"lat":25.030125,"lon":121.50058,"t":1794376337000,"rx":1794376361149},{"id":"EAL-1157","dir":0,"lat":25.012538,"lon":121.460575,"t":1794376347000,"rx":1794376361149},{"id":"EAL-1161","dir":0,"lat":25.045912,"lon":121.518578,"t":1794376346000,"rx":1794376361149},{"id":"EAL-1162","dir":0,"lat":25.02134,"lon":121.498572,"t":1794376344000,"rx":1794376361149},{"id":"EAL-1168","dir":0,"lat":25.035133,"lon":121.506237,"t":1794376340000,"rx":1794376361149},{"id":"EAL-1171","dir":0,"lat":25.064713,"lon":121.565745,"t":1794376336000,"rx":1794376361149},{"id":"EAL-1173","dir":0,"lat":25.05836,"lon":121.569163,"t":1794376335000,"rx":1794376361149},{"id":"EAL-1299","dir":0,"lat":25.06159,"lon":121.566513,"t":1794376337000,"rx":1794376361149},{"id":"EAL-2266","dir":0,"lat":25.046218,"lon":121.51726,"t":1794376336000,"rx":1794376361149},{"id":"EAL-2267","dir":0,"lat":25.028607,"lon":121.500607,"t":1794376335000,"rx":1794376361149},{"id":"EAA-150","dir":0,"lat":25.059412,"lon":121.568432,"t":1794376377000,"rx":1794376385057},{"id":"EAA-151","dir":0,"lat":25.026647,"lon":121.45269,"t":1794376376000,"rx":1794376385057},{"id":"EAL-0072","dir":0,"lat":24.99987,"lon":121.496332,"t":1794376372000,"rx":1794376385057},{"id":"EAL-0080","dir":0,"lat":25.013448,"lon":121.456228,"t":1794376363000,"rx":1794376385057},{"id":"EAL-0081","dir":0,"lat":25.004752,"lon":121.480343,"t":1794376375000,"rx":1794376385057},{"id":"EAL-0097","dir":0,"lat":25.008068,"lon":121.467025,"t":1794376375000,"rx":1794376385057},{"id":"EAL-0236","dir":0,"lat":25.030977,"lon":121.50067,"t":1794376377000,"rx":1794376385057},{"id":"EAL-1157","dir":0,"lat":25.012542,"lon":121.460582,"t":1794376367000,"rx":1794376385057},{"id":"EAL-1161","dir":0,"lat":25.045925,"lon":121.51858,"t":1794376366000,"rx":1794376385057},{"id":"EAL-1162","dir":0,"lat":25.021318,"lon":121.498592,"t":1794376364000,"rx":1794376385057},{"id":"EAL-1168","dir":0,"lat":25.035303,"lon":121.506512,"t":1794376360000,"rx":1794376385057},{"id":"EAL-1171","dir":0,"lat":25.066345,"lon":121.565332,"t":1794376376000,"rx":1794376385057},{"id":"EAL-1173","dir":0,"lat":25.059425,"lon":121.568835,"t":1794376376000,"rx":1794376385057},{"id":"EAL-2266","dir":0,"lat":25.04632,"lon":121.51742,"t":1794376376000,"rx":1794376385057},{"id":"EAL-2267","dir":0,"lat":25.029022,"lon":121.500572,"t":1794376375000,"rx":1794376385057},{"id":"EAA-150","dir":0,"lat":25.059633,"lon":121.568157,"t":1794376397000,"rx":1794376411094},{"id":"EAA-151","dir":0,"lat":25.026012,"lon":121.453727,"t":1794376392000,"rx":1794376411094},{"id":"EAL-0072","dir":0,"lat":25.000522,"lon":121.496747,"t":1794376392000,"rx":1794376411094},{"id":"EAL-0080","dir":0,"lat":25.012007,"lon":121.455982,"t":1794376403000,"rx":1794376411094},{"id":"EAL-0081","dir":0,"lat":25.004727,"lon":121.480345,"t":1794376395000,"rx":1794376411094},{"id":"EAL-0097","dir":0,"lat":25.007622,"lon":121.468245,"t":1794376395000,"rx":1794376411094},{"id":"EAL-0236","dir":0,"lat":25.030982,"lon":121.500665,"t":1794376397000,"rx":1794376411094},{"id":"EAL-1157","dir":0,"lat":25.012537,"lon":121.460565,"t":1794376387000,"rx":1794376411094},{"id":"EAL-1161","dir":0,"lat":25.045843,"lon":121.518893,"t":1794376386000,"rx":1794376411094},{"id":"EAL-1162","dir":0,"lat":25.021295,"lon":121.498583,"t":1794376404000,"rx":1794376411094},{"id":"EAL-1168","dir":0,"lat":25.035363,"lon":121.506502,"t":1794376400000,"rx":1794376411094},{"id":"EAL-1171","dir":0,"lat":25.066272,"lon":121.565315,"t":1794376396000,"rx":1794376411094},{"id":"EAL-1173","dir":0,"lat":25.059388,"lon":121.568493,"t":1794376396000,"rx":1794376411094},{"id":"EAL-2266","dir":0,"lat":25.046328,"lon":121.517353,"t":1794376396000,"rx":1794376411094},{"id":"EAL-2267","dir":0,"lat":25.029067,"lon":121.500542,"t":1794376395000,"rx":1794376411094},{"id":"EAA-150","dir":0,"lat":25.059998,"lon":121.567807,"t":1794376417000,"rx":1794376433833},{"id":"EAA-151","dir":0,"lat":25.025987,"lon":121.453792,"t":1794376412000,"rx":1794376433833},{"id":"EAL-0072","dir":0,"lat":25.000717,"lon":121.496833,"t":1794376412000,"rx":1794376433833},{"id":"EAL-0080","dir":0,"lat":25.0117,"lon":121.455297,"t":1794376423000,"rx":1794376433833},{"id":"EAL-0081","dir":0,"lat":25.00411,"lon":121.480668,"t":1794376417000,"rx":1794376433833},{"id":"EAL-0097","dir":0,"lat":25.007517,"lon":121.468357,"t":1794376415000,"rx":1794376433833},{"id":"EAL-0236","dir":0,"lat":25.030985,"lon":121.500662,"t":1794376417000,"rx":1794376433833},{"id":"EAL-1157","dir":0,"lat":25.012532,"lon":121.460598,"t":1794376407000,"rx":1794376433833},{"id":"EAL-1161","dir":0,"lat":25.045408,"lon":121.520623,"t":1794376406000,"rx":1794376433833},{"id":"EAL-1162","dir":0,"lat":25.021303,"lon":121.498553,"t":1794376424000,"rx":1794376433833},{"id":"EAL-1168","dir":0,"lat":25.035762,"lon":121.506623,"t":1794376420000,"rx":1794376433833},{"id":"EAL-1171","dir":0,"lat":25.066225,"lon":121.565328,"t":1794376416000,"rx":1794376433833},{"id":"EAL-1173","dir":0,"lat":25.059442,"lon":121.56833,"t":1794376416000,"rx":1794376433833},{"id":"EAL-2266","dir":0,"lat":25.046207,"lon":121.517463,"t":1794376416000,"rx":1794376433833},{"id":"EAL-2267","dir":0,"lat":25.02907,"lon":121.500542,"t":1794376415000,"rx":1794376433833},{"id":"EAA-150","dir":0,"lat":25.06048,"lon":121.567325,"t":1794376437000,"rx":1794376458813},{"id":"EAA-151","dir":0,"lat":25.024712,"lon":121.454343,"t":1794376439000,"rx":1794376458813},{"id":"EAL-0072","dir":0,"lat":25.000717,"lon":121.49683,"t":1794376432000,"rx":1794376458813},{"id":"EAL-0080","dir":0,"lat":25.011668,"lon":121.455238,"t":1794376443000,"rx":1794376458813},{"id":"EAL-0081","dir":0,"lat":25.003112,"lon":121.48117,"t":1794376448000,"rx":1794376458813},{"id":"EAL-0097","dir":0,"lat":25.00718,"lon":121.469598,"t":1794376443000,"rx":1794376458813},{"id":"EAL-0236","dir":0,"lat":25.030987,"lon":121.50066,"t":1794376437000,"rx":1794376458813},{"id":"EAL-1157","dir":0,"lat":25.01313,"lon":121.460685,"t":1794376447000,"rx":1794376458813},{"id":"EAL-1161","dir":0,"lat":25.04501,"lon":121.521547,"t":1794376446000,"rx":1794376458813},{"id":"EAL-1162","dir":0,"lat":25.022583,"lon":121.499393,"t":1794376444000,"rx":1794376458813},{"id":"EAL-1168","dir":0,"lat":25.035812,"lon":121.506638,"t":1794376440000,"rx":1794376458813},{"id":"EAL-1171","dir":0,"lat":25.066273,"lon":121.565315,"t":1794376436000,"rx":1794376458813},{"id":"EAL-1173","dir":0,"lat":25.060202,"lon":121.567658,"t":1794376436000,"rx":1794376458813},{"id":"EAL-2266","dir":0,"lat":25.04594,"lon":121.518138,"t":1794376436000,"rx":1794376458813},{"id":"EAL-2267","dir":0,"lat":25.029955,"lon":121.50058,"t":1794376448000,"rx":1794376458813},{"id":"EAA-150","dir":0,"lat":25.061615,"lon":121.56652,"t":1794376475000,"rx":1794376482286},{"id":"EAA-151","dir":0,"lat":25.023498,"lon":121.454278,"t":1794376472000,"rx":1794376482286},{"id":"EAL-0072","dir":0,"lat":25.000688,"lon":121.496852,"t":1794376472000,"rx":1794376482286},{"id":"EAL-0080","dir":0,"lat":25.011717,"lon":121.456025,"t":1794376463000,"rx":1794376482286},{"id":"EAL-0081","dir":0,"lat":25.000437,"lon":121.481342,"t":1794376475000,"rx":1794376482286},{"id":"EAL-0097","dir":0,"lat":25.006812,"lon":121.470603,"t":1794376475000,"rx":1794376482286},{"id":"EAL-0236","dir":0,"lat":25.030988,"lon":121.500658,"t":1794376457000,"rx":1794376482286},{"id":"EAL-1157","dir":0,"lat":25.013498,"lon":121.461008,"t":1794376467000,"rx":1794376482286},{"id":"EAL-1161","dir":0,"lat":25.044457,"lon":121.52144,"t":1794376466000,"rx":1794376482286},{"id":"EAL-1162","dir":0,"lat":25.023778,"lon":121.500135,"t":1794376464000,"rx":1794376482286},{"id":"EAL-1168","dir":0,"lat":25.035817,"lon":121.506645,"t":1794376460000,"rx":1794376482286},{"id":"EAL-1171","dir":0,"lat":25.066263,"lon":121.565303,"t":1794376456000,"rx":1794376482286},{"id":"EAL-1173","dir":0,"lat":25.060705,"lon":121.567228,"t":1794376472000,"rx":1794376482286},{"id":"EAL-1299","dir":0,"lat":25.063257,"lon":121.565918,"t":1794376468000,"rx":1794376482286},{"id":"EAL-2266","dir":0,"lat":25.045887,"lon":121.518358,"t":1794376456000,"rx":1794376482286},{"id":"EAL-2267","dir":0,"lat":25.030373,"lon":121.500565,"t":1794376455000,"rx":1794376482286},{"id":"EAA-150","dir":0,"lat":25.063425,"lon":121.565975,"t":1794376497000,"rx":1794376506593},{"id":"EAA-151","dir":0,"lat":25.022485,"lon":121.454247,"t":1794376492000,"rx":1794376506593},{"id":"EAL-0072","dir":0,"lat":25.000972,"lon":121.497045,"t":1794376492000,"rx":1794376506593},{"id":"EAL-0080","dir":0,"lat":25.0112,"lon":121.456898,"t":1794376483000,"rx":1794376506593},{"id":"EAL-0081","dir":0,"lat":25.00042,"lon":121.481342,"t":1794376495000,"rx":1794376506593},{"id":"EAL-0097","dir":0,"lat":25.006827,"lon":121.470642,"t":1794376495000,"rx":1794376506593},{"id":"EAL-0236","dir":0,"lat":25.031235,"lon":121.502107,"t":1794376497000,"rx":1794376506593},{"id":"EAL-1157","dir":0,"lat":25.014158,"lon":121.461093,"t":1794376487000,"rx":1794376506593},{"id":"EAL-1161","dir":0,"lat":25.044252,"lon":121.521363,"t":1794376486000,"rx":1794376506593},{"id":"EAL-1162","dir":0,"lat":25.025157,"lon":121.50049,"t":1794376492000,"rx":1794376506593},{"id":"EAL-1168","dir":0,"lat":25.035868,"lon":121.506652,"t":1794376480000,"rx":1794376506593},{"id":"EAL-1171","dir":0,"lat":25.066225,"lon":121.56533,"t":1794376496000,"rx":1794376506593},{"id":"EAL-1173","dir":0,"lat":25.062175,"lon":121.566303,"t":1794376496000,"rx":1794376506593},{"id":"EAL-2266","dir":0,"lat":25.045893,"lon":121.518372,"t":1794376496000,"rx":1794376506593},{"id":"EAL-2267","dir":0,"lat":25.0312,"lon":121.500628,"t":1794376495000,"rx":1794376506593},{"id":"EAA-150","dir":0,"lat":25.064328,"lon":121.565798,"t":1794376517000,"rx":1794376529420},{"id":"EAA-151","dir":0,"lat":25.020903,"lon":121.454662,"t":1794376512000,"rx":1794376529420},{"id":"EAL-0072","dir":0,"lat":25.00119,"lon":121.49683,"t":1794376512000,"rx":1794376529420},{"id":"EAL-0080","dir":0,"lat":25.01038,"lon":121.457722,"t":1794376513000,"rx":1794376529420},{"id":"EAL-0081","dir":0,"lat":24.999433,"lon":121.481293,"t":1794376515000,"rx":1794376529420},{"id":"EAL-0097","dir":0,"lat":25.006645,"lon":121.471323,"t":1794376518000,"rx":1794376529420},{"id":"EAL-0236","dir":0,"lat":25.031288,"lon":121.502017,"t":1794376517000,"rx":1794376529420},{"id":"EAL-1157","dir":0,"lat":25.014088,"lon":121.461115,"t":1794376507000,"rx":1794376529420},{"id":"EAL-1161","dir":0,"lat":25.043922,"lon":121.522043,"t":1794376514000,"rx":1794376529420},{"id":"EAL-1162","dir":0,"lat":25.025678,"lon":121.50057,"t":1794376504000,"rx":1794376529420},{"id":"EAL-1168","dir":0,"lat":25.036838,"lon":121.506752,"t":1794376504000,"rx":1794376529420},{"id":"EAL-1171","dir":0,"lat":25.066283,"lon":121.565308,"t":1794376516000,"rx":1794376529420},{"id":"EAL-1173","dir":0,"lat":25.06238,"lon":121.56624,"t":1794376516000,"rx":1794376529420},{"id":"EAL-2266","dir":0,"lat":25.045898,"lon":121.518372,"t":1794376516000,"rx":1794376529420},{"id":"EAL-2267","dir":0,"lat":25.031277,"lon":121.501585,"t":1794376515000,"rx":1794376529420},{"id":"EAA-150","dir":0,"lat":25.065298,"lon":121.565663,"t":1794376537000,"rx":1794376554833},{"id":"EAA-151","dir":0,"lat":25.01961,"lon":121.45531,"t":1794376532000,"rx":1794376554833},{"id":"EAL-0072","dir":0,"lat":25.001265,"lon":121.496805,"t":1794376532000,"rx":1794376554833},{"id":"EAL-0080","dir":0,"lat":25.00963,"lon":121.457697,"t":1794376527000,"rx":1794376554833},{"id":"EAL-0081","dir":0,"lat":24.999415,"lon":121.481278,"t":1794376535000,"rx":1794376554833},{"id":"EAL-0097","dir":0,"lat":25.00644,"lon":121.472447,"t":1794376535000,"rx":1794376554833},{"id":"EAL-0236","dir":0,"lat":25.031325,"lon":121.503083,"t":1794376537000,"rx":1794376554833},{"id":"EAL-1157","dir":0,"lat":25.01411,"lon":121.461105,"t":1794376527000,"rx":1794376554833},{"id":"EAL-1161","dir":0,"lat":25.043822,"lon":121.522255,"t":1794376526000,"rx":1794376554833},{"id":"EAL-1162","dir":0,"lat":25.026127,"lon":121.500577,"t":1794376524000,"rx":1794376554833},{"id":"EAL-1168","dir":0,"lat":25.037918,"lon":121.507217,"t":1794376520000,"rx":1794376554833},{"id":"EAL-1171","dir":0,"lat":25.066277,"lon":121.56531,"t":1794376536000,"rx":1794376554833},{"id":"EAL-1173","dir":0,"lat":25.063053,"lon":121.566007,"t":1794376536000,"rx":1794376554833},{"id":"EAL-2266","dir":0,"lat":25.045863,"lon":121.518565,"t":1794376536000,"rx":1794376554833},{"id":"EAL-2267","dir":0,"lat":25.031287,"lon":121.503078,"t":1794376535000,"rx":1794376554833},{"id":"EAA-150","dir":0,"lat":25.067063,"lon":121.565022,"t":1794376557000,"rx":1794376578225},{"id":"EAA-151","dir":0,"lat":25.019537,"lon":121.455328,"t":1794376552000,"rx":1794376578225},{"id":"EAL-0072","dir":0,"lat":25.001813,"lon":121.49667,"t":1794376552000,"rx":1794376578225},{"id":"EAL-0080","dir":0,"lat":25.009573,"lon":121.457743,"t":1794376563000,"rx":1794376578225},{"id":"EAL-0081","dir":0,"lat":24.99886,"lon":121.48127,"t":1794376557000,"rx":1794376578225},{"id":"EAL-0097","dir":0,"lat":25.006345,"lon":121.4731,"t":1794376555000,"rx":1794376578225},{"id":"EAL-0098","dir":0,"lat":25.024512,"lon":121.450347,"t":1794376569000,"rx":1794376578225},{"id":"EAL-0236","dir":0,"lat":25.031905,"lon":121.504515,"t":1794376567000,"rx":1794376578225},{"id":"EAL-1157","dir":0,"lat":25.014067,"lon":121.46137,"t":1794376568000,"rx":1794376578225},{"id":"EAL-1161","dir":0,"lat":25.043773,"lon":121.522475,"t":1794376566000,"rx":1794376578225},{"id":"EAL-1162","dir":0,"lat":25.026132,"lon":121.500558,"t":1794376564000,"rx":1794376578225},{"id":"EAL-1168","dir":0,"lat":25.041127,"lon":121.507905,"t":1794376560000,"rx":1794376578225},{"id":"EAL-1171","dir":0,"lat":25.066277,"lon":121.565318,"t":1794376556000,"rx":1794376578225},{"id":"EAL-1173","dir":0,"lat":25.06307,"lon":121.565962,"t":1794376556000,"rx":1794376578225},{"id":"EAL-2266","dir":0,"lat":25.045288,"lon":121.520593,"t":1794376562000,"rx":1794376578225},{"id":"EAL-2267","dir":0,"lat":25.031967,"lon":121.504482,"t":1794376558000,"rx":1794376578225},{"id":"EAA-150","dir":0,"lat":25.067893,"lon":121.56643,"t":1794376577000,"rx":1794376602158},{"id":"EAA-151","dir":0,"lat":25.017785,"lon":121.456123,"t":1794376592000,"rx":1794376602158},{"id":"EAL-0072","dir":0,"lat":25.002042,"lon":121.496685,"t":1794376592000,"rx":1794376602158},{"id":"EAL-0080","dir":0,"lat":25.009607,"lon":121.457738,"t":1794376583000,"rx":1794376602158},{"id":"EAL-0081","dir":0,"lat":24.997647,"lon":121.48133,"t":1794376595000,"rx":1794376602158},{"id":"EAL-0097","dir":0,"lat":25.006312,"lon":121.47337,"t":1794376595000,"rx":1794376602158},{"id":"EAL-0098","dir":0,"lat":25.025615,"lon":121.451493,"t":1794376589000,"rx":1794376602158},{"id":"EAL-0236","dir":0,"lat":25.032603,"lon":121.50481,"t":1794376577000,"rx":1794376602158},{"id":"EAL-1157","dir":0,"lat":25.013857,"lon":121.462022,"t":1794376588000,"rx":1794376602158},{"id":"EAL-1161","dir":0,"lat":25.043717,"lon":121.522693,"t":1794376586000,"rx":1794376602158},{"id":"EAL-1162","dir":0,"lat":25.027415,"lon":121.500547,"t":1794376585000,"rx":1794376602158},{"id":"EAL-1168","dir":0,"lat":25.042023,"lon":121.508528,"t":1794376580000,"rx":1794376602158},{"id":"EAL-1171","dir":0,"lat":25.066237,"lon":121.565318,"t":1794376576000,"rx":1794376602158},{"id":"EAL-1173","dir":0,"lat":25.063088,"lon":121.56595,"t":1794376576000,"rx":1794376602158},{"id":"EAL-2266","dir":0,"lat":25.045228,"lon":121.521457,"t":1794376576000,"rx":1794376602158},{"id":"EAL-2267","dir":0,"lat":25.032868,"lon":121.504952,"t":1794376576000,"rx":1794376602158},{"id":"EAA-150","dir":0,"lat":25.067718,"lon":121.566972,"t":1794376617000,"rx":1794376626573},{"id":"EAA-151","dir":0,"lat":25.017225,"lon":121.456398,"t":1794376612000,"rx":1794376626573},{"id":"EAL-0072","dir":0,"lat":25.002037,"lon":121.49666,"t":1794376612000,"rx":1794376626573},{"id":"EAL-0080","dir":0,"lat":25.009148,"lon":121.458395,"t":1794376603000,"rx":1794376626573},{"id":"EAL-0081","dir":0,"lat":24.997527,"lon":121.481328,"t":1794376615000,"rx":1794376626573},{"id":"EAL-0097","dir":0,"lat":25.006313,"lon":121.473372,"t":1794376615000,"rx":1794376626573},{"id":"EAL-0098","dir":0,"lat":25.026673,"lon":121.45261,"t":1794376609000,"rx":1794376626573},{"id":"EAL-0236","dir":0,"lat":25.033023,"lon":121.504915,"t":1794376617000,"rx":1794376626573},{"id":"EAL-1157","dir":0,"lat":25.013858,"lon":121.461953,"t":1794376607000,"rx":1794376626573},{"id":"EAL-1161","dir":0,"lat":25.044583,"lon":121.523305,"t":1794376618000,"rx":1794376626573},{"id":"EAL-1162","dir":0,"lat":25.029962,"lon":121.500607,"t":1794376615000,"rx":1794376626573},{"id":"EAL-1168","dir":0,"lat":25.042038,"lon":121.508462,"t":1794376600000,"rx":1794376626573},{"id":"EAL-1171","dir":0,"lat":25.066277,"lon":121.565335,"t":1794376616000,"rx":1794376626573},{"id":"EAL-1173","dir":0,"lat":25.063398,"lon":121.565913,"t":1794376616000,"rx":1794376626573},{"id":"EAL-2266","dir":0,"lat":25.044745,"lon":121.521528,"t":1794376616000,"rx":1794376626573},{"id":"EAL-2267","dir":0,"lat":25.032935,"lon":121.504953,"t":1794376615000,"rx":1794376626573},{"id":"EAA-150","dir":0,"lat":25.067822,"lon":121.567165,"t":1794376637000,"rx":1794376650697},{"id":"EAA-151","dir":0,"lat":25.017152,"lon":121.456422,"t":1794376632000,"rx":1794376650697},{"id":"EAL-0072","dir":0,"lat":25.002037,"lon":121.496663,"t":1794376632000,"rx":1794376650697},{"id":"EAL-0080","dir":0,"lat":25.009238,"lon":121.458897,"t":1794376623000,"rx":1794376650697},{"id":"EAL-0081","dir":0,"lat":24.997243,"lon":121.48131,"t":1794376635000,"rx":1794376650697},{"id":"EAL-0097","dir":0,"lat":25.006258,"lon":121.473587,"t":1794376635000,"rx":1794376650697},{"id":"EAL-0098","dir":0,"lat":25.025808,"lon":121.453918,"t":1794376629000,"rx":1794376650697},{"id":"EAL-0236","dir":0,"lat":25.034347,"lon":121.505817,"t":1794376637000,"rx":1794376650697},{"id":"EAL-1157","dir":0,"lat":25.013212,"lon":121.462778,"t":1794376627000,"rx":1794376650697},{"id":"EAL-1161","dir":0,"lat":25.045758,"lon":121.523625,"t":1794376632000,"rx":1794376650697},{"id":"EAL-1162","dir":0,"lat":25.03072,"lon":121.500622,"t":1794376624000,"rx":1794376650697},{"id":"EAL-1168","dir":0,"lat":25.042047,"lon":121.508465,"t":1794376620000,"rx":1794376650697},{"id":"EAL-1171","dir":0,"lat":25.0663,"lon":121.565328,"t":1794376636000,"rx":1794376650697},{"id":"EAL-1173","dir":0,"lat":25.065032,"lon":121.565705,"t":1794376636000,"rx":1794376650697},{"id":"EAL-2266","dir":0,"lat":25.044308,"lon":121.521395,"t":1794376636000,"rx":1794376650697},{"id":"EAL-2267","dir":0,"lat":25.03422,"lon":121.505698,"t":1794376639000,"rx":1794376650697},{"id":"EAL-0072","dir":0,"lat":25.00226,"lon":121.49668,"t":1794376642000,"rx":1794376660769},{"id":"EAL-0080","dir":0,"lat":25.009105,"lon":121.458972,"t":1794376643000,"rx":1794376660769},{"id":"EAL-0081","dir":0,"lat":24.996662,"lon":121.4812,"t":1794376642000,"rx":1794376660769},{"id":"EAL-0097","dir":0,"lat":25.006208,"lon":121.474172,"t":1794376641000,"rx":1794376660769},{"id":"EAL-1162","dir":0,"lat":25.031212,"lon":121.501358,"t":1794376644000,"rx":1794376660769},{"id":"EAL-1168","dir":0,"lat":25.042043,"lon":121.508465,"t":1794376640000,"rx":1794376660769},{"id":"EAA-151","dir":0,"lat":25.016953,"lon":121.456495,"t":1794376652000,"rx":1794376673569},{"id":"EAL-0072","dir":0,"lat":25.002938,"lon":121.496655,"t":1794376652000,"rx":1794376673569},{"id":"EAL-0080","dir":0,"lat":25.008793,"lon":121.459043,"t":1794376663000,"rx":1794376673569},{"id":"EAL-0081","dir":0,"lat":24.99593,"lon":121.48107,"t":1794376655000,"rx":1794376673569},{"id":"EAL-0097","dir":0,"lat":25.006007,"lon":121.475355,"t":1794376655000,"rx":1794376673569},{"id":"EAL-0098","dir":0,"lat":25.02473,"lon":121.454258,"t":1794376664000,"rx":1794376673569},{"id":"EAL-0236","dir":0,"lat":25.035543,"lon":121.506562,"t":1794376657000,"rx":1794376673569},{"id":"EAL-1157","dir":0,"lat":25.01287,"lon":121.463183,"t":1794376647000,"rx":1794376673569},{"id":"EAL-1161","dir":0,"lat":25.046697,"lon":121.523975,"t":1794376646000,"rx":1794376673569},{"id":"EAL-1162","dir":0,"lat":25.031248,"lon":121.502098,"t":1794376664000,"rx":1794376673569},{"id":"EAL-1168","dir":0,"lat":25.04204,"lon":121.508462,"t":1794376660000,"rx":1794376673569},{"id":"EAL-1171","dir":0,"lat":25.066293,"lon":121.565313,"t":1794376656000,"rx":1794376673569},{"id":"EAL-2266","dir":0,"lat":25.043867,"lon":121.52203,"t":1794376661000,"rx":1794376673569},{"id":"EAL-2267","dir":0,"lat":25.035275,"lon":121.506352,"t":1794376655000,"rx":1794376673569},{"id":"EAA-151","dir":0,"lat":25.016898,"lon":121.456438,"t":1794376672000,"rx":1794376698283},{"id":"EAL-0072","dir":0,"lat":25.003857,"lon":121.496495,"t":1794376685000,"rx":1794376698283},{"id":"EAL-0080","dir":0,"lat":25.008785,"lon":121.459032,"t":1794376683000,"rx":1794376698283},{"id":"EAL-0081","dir":0,"lat":24.995352,"lon":121.481022,"t":1794376677000,"rx":1794376698283},{"id":"EAL-0097","dir":0,"lat":25.005907,"lon":121.475925,"t":1794376675000,"rx":1794376698283},{"id":"EAL-0098","dir":0,"lat":25.024597,"lon":121.454345,"t":1794376689000,"rx":1794376698283},{"id":"EAL-0236","dir":0,"lat":25.0355,"lon":121.506592,"t":1794376677000,"rx":1794376698283},{"id":"EAL-1157","dir":0,"lat":25.01327,"lon":121.46244,"t":1794376687000,"rx":1794376698283},{"id":"EAL-1161","dir":0,"lat":25.04717,"lon":121.524188,"t":1794376686000,"rx":1794376698283},{"id":"EAL-1162","dir":0,"lat":25.031265,"lon":121.503183,"t":1794376684000,"rx":1794376698283},{"id":"EAL-1168","dir":0,"lat":25.042033,"lon":121.508458,"t":1794376680000,"rx":1794376698283},{"id":"EAL-1171","dir":0,"lat":25.066305,"lon":121.565338,"t":1794376676000,"rx":1794376698283},{"id":"EAL-2266","dir":0,"lat":25.043772,"lon":121.522322,"t":1794376676000,"rx":1794376698283},{"id":"EAL-2267","dir":0,"lat":25.035602,"lon":121.50654,"t":1794376675000,"rx":1794376698283},{"id":"EAA-151","dir":0,"lat":25.015892,"lon":121.456662,"t":1794376712000,"rx":1794376721477},{"id":"EAL-0072","dir":0,"lat":25.00683,"lon":121.49545,"t":1794376712000,"rx":1794376721477},{"id":"EAL-0080","dir":0,"lat":25.00871,"lon":121.459063,"t":1794376703000,"rx":1794376721477},{"id":"EAL-0081","dir":0,"lat":24.995688,"lon":121.48295,"t":1794376715000,"rx":1794376721477},{"id":"EAL-0097","dir":0,"lat":25.00577,"lon":121.478402,"t":1794376715000,"rx":1794376721477},{"id":"EAL-0098","dir":0,"lat":25.023482,"lon":121.45426,"t":1794376709000,"rx":1794376721477},{"id":"EAL-0236","dir":0,"lat":25.036837,"lon":121.506855,"t":1794376712000,"rx":1794376721477},{"id":"EAL-1157","dir":0,"lat":25.013172,"lon":121.462725,"t":1794376707000,"rx":1794376721477},{"id":"EAL-1161","dir":0,"lat":25.047862,"lon":121.524337,"t":1794376709000,"rx":1794376721477},{"id":"EAL-1162","dir":0,"lat":25.031315,"lon":121.504045,"t":1794376704000,"rx":1794376721477},{"id":"EAL-1168","dir":0,"lat":25.042283,"lon":121.508502,"t":1794376700000,"rx":1794376721477},{"id":"EAL-1171","dir":0,"lat":25.066243,"lon":121.565313,"t":1794376696000,"rx":1794376721477},{"id":"EAL-2266","dir":0,"lat":25.043608,"lon":121.522773,"t":1794376696000,"rx":1794376721477},{"id":"EAL-2267","dir":0,"lat":25.035668,"lon":121.506603,"t":1794376695000,"rx":1794376721477},{"id":"EAA-151","dir":0,"lat":25.015292,"lon":121.456592,"t":1794376732000,"rx":1794376744644},{"id":"EAL-0072","dir":0,"lat":25.00971,"lon":121.495428,"t":1794376732000,"rx":1794376744644},{"id":"EAL-0080","dir":0,"lat":25.008705,"lon":121.459072,"t":1794376723000,"rx":1794376744644},{"id":"EAL-0081","dir":0,"lat":24.99623,"lon":121.483787,"t":1794376735000,"rx":1794376744644},{"id":"EAL-0097","dir":0,"lat":25.005823,"lon":121.478637,"t":1794376735000,"rx":1794376744644},{"id":"EAL-0098","dir":0,"lat":25.022485,"lon":121.454253,"t":1794376729000,"rx":1794376744644},{"id":"EAL-0236","dir":0,"lat":25.036973,"lon":121.506918,"t":1794376717000,"rx":1794376744644},{"id":"EAL-1157","dir":0,"lat":25.013343,"lon":121.462128,"t":1794376727000,"rx":1794376744644},{"id":"EAL-1161","dir":0,"lat":25.048097,"lon":121.52436,"t":1794376726000,"rx":1794376744644},{"id":"EAL-1162","dir":0,"lat":25.03222,"lon":121.504615,"t":1794376724000,"rx":1794376744644},{"id":"EAL-1168","dir":0,"lat":25.044895,"lon":121.509172,"t":1794376732000,"rx":1794376744644},{"id":"EAL-1171","dir":0,"lat":25.066278,"lon":121.565367,"t":1794376716000,"rx":1794376744644},{"id":"EAL-2266","dir":0,"lat":25.043603,"lon":121.522793,"t":1794376716000,"rx":1794376744644},{"id":"EAL-2267","dir":0,"lat":25.03855,"lon":121.507368,"t":1794376735000,"rx":1794376744644},{"id":"EAA-151","dir":0,"lat":25.01526,"lon":121.456568,"t":1794376752000,"rx":1794376768678},{"id":"EAL-0072","dir":0,"lat":25.012643,"lon":121.495483,"t":1794376752000,"rx":1794376768678},{"id":"EAL-0080","dir":0,"lat":25.008637,"lon":121.459043,"t":1794376743000,"rx":1794376768678},{"id":"EAL-0081","dir":0,"lat":24.996267,"lon":121.484248,"t":1794376755000,"rx":1794376768678},{"id":"EAL-0097","dir":0,"lat":25.00606,"lon":121.479387,"t":1794376755000,"rx":1794376768678},{"id":"EAL-0098","dir":0,"lat":25.020953,"lon":121.454708,"t":1794376749000,"rx":1794376768678},{"id":"EAL-0236","dir":0,"lat":25.041637,"lon":121.508307,"t":1794376757000,"rx":1794376768678},{"id":"EAL-1157","dir":0,"lat":25.013522,"lon":121.462323,"t":1794376747000,"rx":1794376768678},{"id":"EAL-1161","dir":0,"lat":25.048952,"lon":121.524532,"t":1794376746000,"rx":1794376768678},{"id":"EAL-1162","dir":0,"lat":25.033663,"lon":121.505415,"t":1794376755000,"rx":1794376768678},{"id":"EAL-1168","dir":0,"lat":25.04536,"lon":121.509317,"t":1794376740000,"rx":1794376768678},{"id":"EAL-1171","dir":0,"lat":25.067662,"lon":121.565717,"t":1794376756000,"rx":1794376768678},{"id":"EAL-2266","dir":0,"lat":25.044235,"lon":121.523228,"t":1794376756000,"rx":1794376768678},{"id":"EAL-2267","dir":0,"lat":25.040597,"lon":121.507945,"t":1794376755000,"rx":1794376768678},{"id":"EAA-151","dir":0,"lat":25.01336,"lon":121.456252,"t":1794376772000,"rx":1794376783462},{"id":"EAL-0072","dir":0,"lat":25.015227,"lon":121.495545,"t":1794376772000,"rx":1794376783462},{"id":"EAL-0080","dir":0,"lat":25.008532,"lon":121.45917,"t":1794376763000,"rx":1794376783462},{"id":"EAL-0081","dir":0,"lat":24.996195,"lon":121.484835,"t":1794376775000,"rx":1794376783462},{"id":"EAL-0097","dir":0,"lat":25.00607,"lon":121.47939,"t":1794376775000,"rx":1794376783462},{"id":"EAL-0098","dir":0,"lat":25.02051,"lon":121.454665,"t":1794376769000,"rx":1794376783462},{"id":"EAL-1157","dir":0,"lat":25.013225,"lon":121.462832,"t":1794376767000,"rx":1794376783462},{"id":"EAL-1161","dir":0,"lat":25.049682,"lon":121.524707,"t":1794376766000,"rx":1794376783462},{"id":"EAL-1162","dir":0,"lat":25.034203,"lon":121.505765,"t":1794376764000,"rx":1794376783462},{"id":"EAL-1168","dir":0,"lat":25.046507,"lon":121.5097,"t":1794376775000,"rx":1794376783462},{"id":"EAL-2266","dir":0,"lat":25.044625,"lon":121.523322,"t":1794376761000,"rx":1794376783462},{"id":"EAA-151","dir":0,"lat":25.012873,"lon":121.45624,"t":1794376792000,"rx":1794376806394},{"id":"EAL-0072","dir":0,"lat":25.017622,"lon":121.496178,"t":1794376792000,"rx":1794376806394},{"id":"EAL-0080","dir":0,"lat":25.008923,"lon":121.459678,"t":1794376791000,"rx":1794376806394},{"id":"EAL-0081","dir":0,"lat":24.996248,"lon":121.484938,"t":1794376795000,"rx":1794376806394},{"id":"EAL-0097","dir":0,"lat":25.005807,"lon":121.48,"t":1794376795000,"rx":1794376806394},{"id":"EAL-0098","dir":0,"lat":25.020512,"lon":121.454773,"t":1794376789000,"rx":1794376806394},{"id":"EAL-0236","dir":0,"lat":25.042463,"lon":121.508598,"t":1794376797000,"rx":1794376806394},{"id":"EAL-1157","dir":0,"lat":25.012938,"lon":121.463503,"t":1794376793000,"rx":1794376806394},{"id":"EAL-1161","dir":0,"lat":25.050437,"lon":121.524967,"t":1794376786000,"rx":1794376806394},{"id":"EAL-1162","dir":0,"lat":25.035503,"lon":121.506477,"t":1794376784000,"rx":1794376806394},{"id":"EAL-1168","dir":0,"lat":25.046633,"lon":121.509588,"t":1794376780000,"rx":1794376806394},{"id":"EAL-1171","dir":0,"lat":25.067847,"lon":121.56708,"t":1794376796000,"rx":1794376806394},{"id":"EAL-2266","dir":0,"lat":25.047228,"lon":121.524125,"t":1794376796000,"rx":1794376806394},{"id":"EAL-2267","dir":0,"lat":25.042045,"lon":121.508407,"t":1794376796000,"rx":1794376806394},{"id":"EAA-151","dir":0,"lat":25.012428,"lon":121.456042,"t":1794376812000,"rx":1794376830243},{"id":"EAL-0072","dir":0,"lat":25.01949,"lon":121.497315,"t":1794376812000,"rx":1794376830243},{"id":"EAL-0080","dir":0,"lat":25.009838,"lon":121.460445,"t":1794376803000,"rx":1794376830243},{"id":"EAL-0081","dir":0,"lat":24.996217,"lon":121.485213,"t":1794376815000,"rx":1794376830243},{"id":"EAL-0097","dir":0,"lat":25.00491,"lon":121.480318,"t":1794376815000,"rx":1794376830243},{"id":"EAL-0098","dir":0,"lat":25.020047,"lon":121.455073,"t":1794376816000,"rx":1794376830243},{"id":"EAL-0236","dir":0,"lat":25.04432,"lon":121.50898,"t":1794376817000,"rx":1794376830243},{"id":"EAL-1157","dir":0,"lat":25.012087,"lon":121.464358,"t":1794376807000,"rx":1794376830243},{"id":"EAL-1161","dir":0,"lat":25.050458,"lon":121.524938,"t":1794376806000,"rx":1794376830243},{"id":"EAL-1162","dir":0,"lat":25.035742,"lon":121.506548,"t":1794376804000,"rx":1794376830243},{"id":"EAL-1168","dir":0,"lat":25.047685,"lon":121.510152,"t":1794376800000,"rx":1794376830243},{"id":"EAL-1171","dir":0,"lat":25.067768,"lon":121.567097,"t":1794376816000,"rx":1794376830243},{"id":"EAL-2266","dir":0,"lat":25.047517,"lon":121.524198,"t":1794376816000,"rx":1794376830243},{"id":"EAL-2267","dir":0,"lat":25.04215,"lon":121.508318,"t":1794376815000,"rx":1794376830243},{"id":"EAL-0080","dir":0,"lat":25.011427,"lon":121.461805,"t":1794376823000,"rx":1794376841065},{"id":"EAL-0098","dir":0,"lat":25.019512,"lon":121.45536,"t":1794376823000,"rx":1794376841065},{"id":"EAL-1161","dir":0,"lat":25.051232,"lon":121.525203,"t":1794376821000,"rx":1794376841065},{"id":"EAL-1162","dir":0,"lat":25.035818,"lon":121.506558,"t":1794376824000,"rx":1794376841065},{"id":"EAL-1168","dir":0,"lat":25.047695,"lon":121.510178,"t":1794376820000,"rx":1794376841065},{"id":"EAA-151","dir":0,"lat":25.012225,"lon":121.456058,"t":1794376838000,"rx":1794376854284},{"id":"EAL-0072","dir":0,"lat":25.01992,"lon":121.497608,"t":1794376832000,"rx":1794376854284},{"id":"EAL-0081","dir":0,"lat":24.996388,"lon":121.48664,"t":1794376835000,"rx":1794376854284},{"id":"EAL-0097","dir":0,"lat":25.004825,"lon":121.48036,"t":1794376835000,"rx":1794376854284},{"id":"EAL-0098","dir":0,"lat":25.019402,"lon":121.455353,"t":1794376829000,"rx":1794376854284},{"id":"EAL-0236","dir":0,"lat":25.045325,"lon":121.508767,"t":1794376837000,"rx":1794376854284},{"id":"EAL-1157","dir":0,"lat":25.0119,"lon":121.464507,"t":1794376827000,"rx":1794376854284},{"id":"EAL-1161","dir":0,"lat":25.051322,"lon":121.52519,"t":1794376826000,"rx":1794376854284},{"id":"EAL-1171","dir":0,"lat":25.06775,"lon":121.5671,"t":1794376836000,"rx":1794376854284},{"id":"EAL-2266","dir":0,"lat":25.047528,"lon":121.524198,"t":1794376836000,"rx":1794376854284},{"id":"EAL-2267","dir":0,"lat":25.042133,"lon":121.508337,"t":1794376835000,"rx":1794376854284},{"id":"EAA-151","dir":0,"lat":25.012003,"lon":121.45598,"t":1794376852000,"rx":1794376864933},{"id":"EAL-0072","dir":0,"lat":25.019917,"lon":121.49761,"t":1794376852000,"rx":1794376864933},{"id":"EAL-0080","dir":0,"lat":25.011602,"lon":121.462012,"t":1794376843000,"rx":1794376864933},{"id":"EAL-0081","dir":0,"lat":24.996265,"lon":121.488313,"t":1794376855000,"rx":1794376864933},{"id":"EAL-0097","dir":0,"lat":25.004803,"lon":121.480393,"t":1794376855000,"rx":1794376864933},{"id":"EAL-0098","dir":0,"lat":25.019307,"lon":121.455227,"t":1794376849000,"rx":1794376864933},{"id":"EAL-1157","dir":0,"lat":25.011752,"lon":121.464735,"t":1794376847000,"rx":1794376864933},{"id":"EAL-1161","dir":0,"lat":25.051495,"lon":121.525198,"t":1794376846000,"rx":1794376864933},{"id":"EAL-1162","dir":0,"lat":25.03581,"lon":121.506572,"t":1794376844000,"rx":1794376864933},{"id":"EAL-1168","dir":0,"lat":25.047697,"lon":121.510182,"t":1794376840000,"rx":1794376864933},{"id":"EAL-0236","dir":0,"lat":25.04512,"lon":121.509,"t":1794376857000,"rx":1794376878217},{"id":"EAL-1171","dir":0,"lat":25.067725,"lon":121.567098,"t":1794376856000,"rx":1794376878217},{"id":"EAL-2266","dir":0,"lat":25.047523,"lon":121.524198,"t":1794376856000,"rx":1794376878217},{"id":"EAL-2267","dir":0,"lat":25.042098,"lon":121.50834,"t":1794376855000,"rx":1794376878217},{"id":"EAA-151","dir":0,"lat":25.011758,"lon":121.45547,"t":1794376872000,"rx":1794376888910},{"id":"EAL-0072","dir":0,"lat":25.020633,"lon":121.498058,"t":1794376876000,"rx":1794376888910},{"id":"EAL-0080","dir":0,"lat":25.011612,"lon":121.462055,"t":1794376863000,"rx":1794376888910},{"id":"EAL-0081","dir":0,"lat":24.996298,"lon":121.489333,"t":1794376875000,"rx":1794376888910},{"id":"EAL-0097","dir":0,"lat":25.00397,"lon":121.48076,"t":1794376875000,"rx":1794376888910},{"id":"EAL-0098","dir":0,"lat":25.017717,"lon":121.45601,"t":1794376869000,"rx":1794376888910},{"id":"EAL-0236","dir":0,"lat":25.045057,"lon":121.509037,"t":1794376877000,"rx":1794376888910},{"id":"EAL-1157","dir":0,"lat":25.01174,"lon":121.464605,"t":1794376867000,"rx":1794376888910},{"id":"EAL-1161","dir":0,"lat":25.05149,"lon":121.525218,"t":1794376866000,"rx":1794376888910},{"id":"EAL-1162","dir":0,"lat":25.03581,"lon":121.506563,"t":1794376864000,"rx":1794376888910},{"id":"EAL-1168","dir":0,"lat":25.047697,"lon":121.510183,"t":1794376860000,"rx":1794376888910},{"id":"EAL-1171","dir":0,"lat":25.067712,"lon":121.567103,"t":1794376876000,"rx":1794376888910},{"id":"EAL-2266","dir":0,"lat":25.047525,"lon":121.524198,"t":1794376876000,"rx":1794376888910},{"id":"EAL-2267","dir":0,"lat":25.042118,"lon":121.508365,"t":1794376875000,"rx":1794376888910},{"id":"EAA-151","dir":0,"lat":25.011775,"lon":121.455477,"t":1794376892000,"rx":1794376912737},{"id":"EAL-0072","dir":0,"lat":25.020765,"lon":121.49811,"t":1794376892000,"rx":1794376912737},{"id":"EAL-0080","dir":0,"lat":25.012858,"lon":121.4604,"t":1794376903000,"rx":1794376912737},{"id":"EAL-0081","dir":0,"lat":24.99638,"lon":121.48979,"t":1794376895000,"rx":1794376912737},{"id":"EAL-0097","dir":0,"lat":25.003065,"lon":121.481197,"t":1794376898000,"rx":1794376912737},{"id":"EAL-0098","dir":0,"lat":25.017432,"lon":121.45622,"t":1794376889000,"rx":1794376912737},{"id":"EAL-0236","dir":0,"lat":25.044965,"lon":121.509047,"t":1794376897000,"rx":1794376912737},{"id":"EAL-1157","dir":0,"lat":25.011788,"lon":121.464972,"t":1794376887000,"rx":1794376912737},{"id":"EAL-1161","dir":0,"lat":25.051488,"lon":121.52522,"t":1794376886000,"rx":1794376912737},{"id":"EAL-1162","dir":0,"lat":25.037245,"lon":121.506927,"t":1794376904000,"rx":1794376912737},{"id":"EAL-1168","dir":0,"lat":25.047848,"lon":121.511622,"t":1794376900000,"rx":1794376912737},{"id":"EAL-1171","dir":0,"lat":25.067723,"lon":121.5671,"t":1794376896000,"rx":1794376912737},{"id":"EAL-2266","dir":0,"lat":25.047865,"lon":121.524288,"t":1794376898000,"rx":1794376912737},{"id":"EAL-2267","dir":0,"lat":25.042428,"lon":121.508482,"t":1794376895000,"rx":1794376912737},{"id":"EAA-151","dir":0,"lat":25.011483,"lon":121.455388,"t":1794376912000,"rx":1794376936930},{"id":"EAL-0072","dir":0,"lat":25.02247,"lon":121.499233,"t":1794376922000,"rx":1794376936930},{"id":"EAL-0080","dir":0,"lat":25.013412,"lon":121.46082,"t":1794376923000,"rx":1794376936930},{"id":"EAL-0081","dir":0,"lat":24.996728,"lon":121.491063,"t":1794376915000,"rx":1794376936930},{"id":"EAL-0097","dir":0,"lat":25.00149,"lon":121.481273,"t":1794376915000,"rx":1794376936930},{"id":"EAL-0098","dir":0,"lat":25.017293,"lon":121.456365,"t":1794376929000,"rx":1794376936930},{"id":"EAL-0236","dir":0,"lat":25.045575,"lon":121.509358,"t":1794376917000,"rx":1794376936930},{"id":"EAL-1157","dir":0,"lat":25.0117,"lon":121.464947,"t":1794376927000,"rx":1794376936930},{"id":"EAL-1161","dir":0,"lat":25.051498,"lon":121.525212,"t":1794376926000,"rx":1794376936930},{"id":"EAL-1162","dir":0,"lat":25.03929,"lon":121.507508,"t":1794376924000,"rx":1794376936930},{"id":"EAL-1168","dir":0,"lat":25.047407,"lon":121.512373,"t":1794376920000,"rx":1794376936930},{"id":"EAL-1171","dir":0,"lat":25.067733,"lon":121.567108,"t":1794376916000,"rx":1794376936930},{"id":"EAL-2266","dir":0,"lat":25.049628,"lon":121.52474,"t":1794376927000,"rx":1794376936930},{"id":"EAL-2267","dir":0,"lat":25.044948,"lon":121.50913,"t":1794376919000,"rx":1794376936930},{"id":"EAA-151","dir":0,"lat":25.011157,"lon":121.456808,"t":1794376952000,"rx":1794376962156},{"id":"EAL-0072","dir":0,"lat":25.024555,"lon":121.500367,"t":1794376952000,"rx":1794376962156},{"id":"EAL-0080","dir":0,"lat":25.014095,"lon":121.461243,"t":1794376943000,"rx":1794376962156},{"id":"EAL-0081","dir":0,"lat":24.996565,"lon":121.491002,"t":1794376955000,"rx":1794376962156},{"id":"EAL-0097","dir":0,"lat":25.000573,"lon":121.48126,"t":1794376955000,"rx":1794376962156},{"id":"EAL-0098","dir":0,"lat":25.016557,"lon":121.456688,"t":1794376954000,"rx":1794376962156},{"id":"EAL-0236","dir":0,"lat":25.04655,"lon":121.509727,"t":1794376941000,"rx":1794376962156},{"id":"EAL-1157","dir":0,"lat":25.010152,"lon":121.465313,"t":1794376947000,"rx":1794376962156},{"id":"EAL-1161","dir":0,"lat":25.051458,"lon":121.525207,"t":1794376946000,"rx":1794376962156},{"id":"EAL-1162","dir":0,"lat":25.041505,"lon":121.508167,"t":1794376944000,"rx":1794376962156},{"id":"EAL-1168","dir":0,"lat":25.047443,"lon":121.512303,"t":1794376940000,"rx":1794376962156},{"id":"EAL-2266","dir":0,"lat":25.0497,"lon":121.524743,"t":1794376936000,"rx":1794376962156},{"id":"EAL-2267","dir":0,"lat":25.046525,"lon":121.509652,"t":1794376942000,"rx":1794376962156},{"id":"EAA-151","dir":0,"lat":25.01106,"lon":121.456903,"t":1794376972000,"rx":1794376986474},{"id":"EAL-0072","dir":0,"lat":25.024987,"lon":121.500477,"t":1794376972000,"rx":1794376986474},{"id":"EAL-0080","dir":0,"lat":25.014028,"lon":121.4613,"t":1794376963000,"rx":1794376986474},{"id":"EAL-0081","dir":0,"lat":24.996747,"lon":121.492488,"t":1794376975000,"rx":1794376986474},{"id":"EAL-0097","dir":0,"lat":24.999562,"lon":121.481348,"t":1794376975000,"rx":1794376986474},{"id":"EAL-0098","dir":0,"lat":25.015748,"lon":121.456437,"t":1794376971000,"rx":1794376986474},{"id":"EAL-0236","dir":0,"lat":25.047248,"lon":121.510092,"t":1794376977000,"rx":1794376986474},{"id":"EAL-1157","dir":0,"lat":25.009258,"lon":121.465072,"t":1794376967000,"rx":1794376986474},{"id":"EAL-1161","dir":0,"lat":25.05176,"lon":121.525235,"t":1794376966000,"rx":1794376986474},{"id":"EAL-1162","dir":0,"lat":25.042122,"lon":121.508332,"t":1794376967000,"rx":1794376986474},{"id":"EAL-1168","dir":0,"lat":25.047443,"lon":121.5123,"t":1794376960000,"rx":1794376986474},{"id":"EAL-2266","dir":0,"lat":25.050738,"lon":121.525025,"t":1794376976000,"rx":1794376986474},{"id":"EAL-2267","dir":0,"lat":25.047168,"lon":121.510048,"t":1794376975000,"rx":1794376986474},{"id":"EAA-151","dir":0,"lat":25.01035,"lon":121.457828,"t":1794376992000,"rx":1794377009811},{"id":"EAL-0072","dir":0,"lat":25.024988,"lon":121.500477,"t":1794376992000,"rx":1794377009811},{"id":"EAL-0080","dir":0,"lat":25.014015,"lon":121.461297,"t":1794376983000,"rx":1794377009811},{"id":"EAL-0081","dir":0,"lat":24.996767,"lon":121.492605,"t":1794376995000,"rx":1794377009811},{"id":"EAL-0097","dir":0,"lat":24.999413,"lon":121.481272,"t":1794376995000,"rx":1794377009811},{"id":"EAL-0098","dir":0,"lat":25.015185,"lon":121.456338,"t":1794376989000,"rx":1794377009811},{"id":"EAL-0236","dir":0,"lat":25.047735,"lon":121.510238,"t":1794376997000,"rx":1794377009811},{"id":"EAL-1157","dir":0,"lat":25.009225,"lon":121.46503,"t":1794376987000,"rx":1794377009811},{"id":"EAL-1161","dir":0,"lat":25.052158,"lon":121.525362,"t":1794376986000,"rx":1794377009811},{"id":"EAL-1162","dir":0,"lat":25.044885,"lon":121.509107,"t":1794376994000,"rx":1794377009811},{"id":"EAL-1168","dir":0,"lat":25.04741,"lon":121.51231,"t":1794376980000,"rx":1794377009811},{"id":"EAL-2266","dir":0,"lat":25.051387,"lon":121.525213,"t":1794376996000,"rx":1794377009811},{"id":"EAL-2267","dir":0,"lat":25.047618,"lon":121.510218,"t":1794376995000,"rx":1794377009811},{"id":"EAL-0080","dir":0,"lat":25.014007,"lon":121.461308,"t":1794377003000,"rx":1794377021105},{"id":"EAL-0081","dir":0,"lat":24.997068,"lon":121.49335,"t":1794377004000,"rx":1794377021105},{"id":"EAL-1162","dir":0,"lat":25.045003,"lon":121.509205,"t":1794377004000,"rx":1794377021105},{"id":"EAL-1168","dir":0,"lat":25.047253,"lon":121.512707,"t":1794377000000,"rx":1794377021105},{"id":"EAA-151","dir":0,"lat":25.009977,"lon":121.457745,"t":1794377012000,"rx":1794377034840},{"id":"EAL-0072","dir":0,"lat":25.024992,"lon":121.500465,"t":1794377012000,"rx":1794377034840},{"id":"EAL-0080","dir":0,"lat":25.014277,"lon":121.461622,"t":1794377023000,"rx":1794377034840},{"id":"EAL-0081","dir":0,"lat":24.997777,"lon":121.494337,"t":1794377015000,"rx":1794377034840},{"id":"EAL-0097","dir":0,"lat":24.998252,"lon":121.48132,"t":1794377015000,"rx":1794377034840},{"id":"EAL-0098","dir":0,"lat":25.01501,"lon":121.456388,"t":1794377009000,"rx":1794377034840},{"id":"EAL-0236","dir":0,"lat":25.047742,"lon":121.510238,"t":1794377017000,"rx":1794377034840},{"id":"EAL-1157","dir":0,"lat":25.009217,"lon":121.465052,"t":1794377007000,"rx":1794377034840},{"id":"EAL-1161","dir":0,"lat":25.052163,"lon":121.526882,"t":1794377006000,"rx":1794377034840},{"id":"EAL-1162","dir":0,"lat":25.045025,"lon":121.509198,"t":1794377024000,"rx":1794377034840},{"id":"EAL-1168","dir":0,"lat":25.046997,"lon":121.51409,"t":1794377020000,"rx":1794377034840},{"id":"EAL-2266","dir":0,"lat":25.051802,"lon":121.525337,"t":1794377016000,"rx":1794377034840},{"id":"EAL-2267","dir":0,"lat":25.047612,"lon":121.510218,"t":1794377015000,"rx":1794377034840},{"id":"EAL-0098","dir":0,"lat":25.014902,"lon":121.456487,"t":1794377029000,"rx":1794377045519},{"id":"EAL-1157","dir":0,"lat":25.009217,"lon":121.465043,"t":1794377027000,"rx":1794377045519},{"id":"EAL-1161","dir":0,"lat":25.052093,"lon":121.528932,"t":1794377026000,"rx":1794377045519},{"id":"EAA-151","dir":0,"lat":25.009608,"lon":121.45773,"t":1794377043000,"rx":1794377058626},{"id":"EAL-0072","dir":0,"lat":25.025108,"lon":121.500445,"t":1794377040000,"rx":1794377058626},{"id":"EAL-0080","dir":0,"lat":25.013838,"lon":121.46195,"t":1794377043000,"rx":1794377058626},{"id":"EAL-0081","dir":0,"lat":24.998415,"lon":121.495237,"t":1794377035000,"rx":1794377058626},{"id":"EAL-0097","dir":0,"lat":24.997857,"lon":121.48131,"t":1794377035000,"rx":1794377058626},{"id":"EAL-0098","dir":0,"lat":25.013512,"lon":121.45619,"t":1794377049000,"rx":1794377058626},{"id":"EAL-0236","dir":0,"lat":25.047765,"lon":121.510237,"t":1794377037000,"rx":1794377058626},{"id":"EAL-1157","dir":0,"lat":25.009217,"lon":121.465043,"t":1794377047000,"rx":1794377058626},{"id":"EAL-1161","dir":0,"lat":25.052177,"lon":121.529535,"t":1794377046000,"rx":1794377058626},{"id":"EAL-1162","dir":0,"lat":25.045035,"lon":121.50922,"t":1794377044000,"rx":1794377058626},{"id":"EAL-1168","dir":0,"lat":25.046593,"lon":121.515672,"t":1794377040000,"rx":1794377058626},{"id":"EAL-2266","dir":0,"lat":25.051797,"lon":121.525345,"t":1794377036000,"rx":1794377058626},{"id":"EAL-2267","dir":0,"lat":25.047608,"lon":121.510207,"t":1794377035000,"rx":1794377058626},{"id":"EAA-151","dir":0,"lat":25.009133,"lon":121.458438,"t":1794377072000,"rx":1794377082794},{"id":"EAL-0072","dir":0,"lat":25.02746,"lon":121.500542,"t":1794377072000,"rx":1794377082794},{"id":"EAL-0080","dir":0,"lat":25.01368,"lon":121.462198,"t":1794377063000,"rx":1794377082794},{"id":"EAL-0081","dir":0,"lat":24.999707,"lon":121.496258,"t":1794377075000,"rx":1794377082794},{"id":"EAL-0097","dir":0,"lat":24.997445,"lon":121.48129,"t":1794377075000,"rx":1794377082794},{"id":"EAL-0098","dir":0,"lat":25.012775,"lon":121.456167,"t":1794377069000,"rx":1794377082794},{"id":"EAL-0236","dir":0,"lat":25.047778,"lon":121.510238,"t":1794377057000,"rx":1794377082794},{"id":"EAL-1157","dir":0,"lat":25.008922,"lon":121.465087,"t":1794377067000,"rx":1794377082794},{"id":"EAL-1161","dir":0,"lat":25.052105,"lon":121.531358,"t":1794377072000,"rx":1794377082794},{"id":"EAL-1162","dir":0,"lat":25.045053,"lon":121.50922,"t":1794377064000,"rx":1794377082794},{"id":"EAL-1168","dir":0,"lat":25.046592,"lon":121.515688,"t":1794377060000,"rx":1794377082794},{"id":"EAL-2266","dir":0,"lat":25.051797,"lon":121.525347,"t":1794377056000,"rx":1794377082794},{"id":"EAL-2267","dir":0,"lat":25.047687,"lon":121.510162,"t":1794377055000,"rx":1794377082794},{"id":"EAA-151","dir":0,"lat":25.009122,"lon":121.458498,"t":1794377092000,"rx":1794377106781},{"id":"EAL-0072","dir":0,"lat":25.028695,"lon":121.500563,"t":1794377092000,"rx":1794377106781},{"id":"EAL-0080","dir":0,"lat":25.013167,"lon":121.462788,"t":1794377083000,"rx":1794377106781},{"id":"EAL-0081","dir":0,"lat":24.999723,"lon":121.496297,"t":1794377095000,"rx":1794377106781},{"id":"EAL-0097","dir":0,"lat":24.996363,"lon":121.481148,"t":1794377095000,"rx":1794377106781},{"id":"EAL-0098","dir":0,"lat":25.012392,"lon":121.456312,"t":1794377089000,"rx":1794377106781},{"id":"EAL-0236","dir":0,"lat":25.047475,"lon":121.512233,"t":1794377097000,"rx":1794377106781},{"id":"EAL-1157","dir":0,"lat":25.008322,"lon":121.46601,"t":1794377087000,"rx":1794377106781},{"id":"EAL-1161","dir":0,"lat":25.052158,"lon":121.53241,"t":1794377086000,"rx":1794377106781},{"id":"EAL-1162","dir":0,"lat":25.045048,"lon":121.509213,"t":1794377084000,"rx":1794377106781},{"id":"EAL-1168","dir":0,"lat":25.046343,"lon":121.516942,"t":1794377080000,"rx":1794377106781},{"id":"EAL-2266","dir":0,"lat":25.051802,"lon":121.525285,"t":1794377096000,"rx":1794377106781},{"id":"EAL-2267","dir":0,"lat":25.048025,"lon":121.511177,"t":1794377095000,"rx":1794377106781},{"id":"EAA-151","dir":0,"lat":25.008812,"lon":121.459027,"t":1794377112000,"rx":1794377130763},{"id":"EAL-0072","dir":0,"lat":25.02873,"lon":121.500552,"t":1794377112000,"rx":1794377130763},{"id":"EAL-0080","dir":0,"lat":25.012973,"lon":121.463012,"t":1794377103000,"rx":1794377130763},{"id":"EAL-0081","dir":0,"lat":25.0003,"lon":121.496683,"t":1794377115000,"rx":1794377130763},{"id":"EAL-0097","dir":0,"lat":24.995983,"lon":121.481117,"t":1794377115000,"rx":1794377130763},{"id":"EAL-0098","dir":0,"lat":25.012038,"lon":121.455973,"t":1794377109000,"rx":1794377130763},{"id":"EAL-0236","dir":0,"lat":25.047327,"lon":121.512838,"t":1794377117000,"rx":1794377130763},{"id":"EAL-1157","dir":0,"lat":25.00819,"lon":121.466823,"t":1794377118000,"rx":1794377130763},{"id":"EAL-1161","dir":0,"lat":25.05215,"lon":121.532713,"t":1794377106000,"rx":1794377130763},{"id":"EAL-1162","dir":0,"lat":25.045873,"lon":121.509453,"t":1794377104000,"rx":1794377130763},{"id":"EAL-1168","dir":0,"lat":25.045882,"lon":121.518845,"t":1794377100000,"rx":1794377130763},{"id":"EAL-2266","dir":0,"lat":25.051863,"lon":121.525332,"t":1794377116000,"rx":1794377130763},{"id":"EAL-2267","dir":0,"lat":25.047358,"lon":121.512627,"t":1794377115000,"rx":1794377130763},{"id":"EAL-0080","dir":0,"lat":25.01308,"lon":121.462457,"t":1794377123000,"rx":1794377140601},{"id":"EAL-1157","dir":0,"lat":25.007912,"lon":121.467515,"t":1794377124000,"rx":1794377140601},{"id":"EAL-1162","dir":0,"lat":25.046588,"lon":121.509703,"t":1794377124000,"rx":1794377140601},{"id":"EAL-1168","dir":0,"lat":25.04561,"lon":121.519115,"t":1794377120000,"rx":1794377140601},{"id":"EAA-151","dir":0,"lat":25.008815,"lon":121.459057,"t":1794377132000,"rx":1794377153897},{"id":"EAL-0072","dir":0,"lat":25.029177,"lon":121.500545,"t":1794377132000,"rx":1794377153897},{"id":"EAL-0081","dir":0,"lat":25.000455,"lon":121.496788,"t":1794377135000,"rx":1794377153897},{"id":"EAL-0097","dir":0,"lat":24.995695,"lon":121.481105,"t":1794377135000,"rx":1794377153897},{"id":"EAL-0098","dir":0,"lat":25.011538,"lon":121.45512,"t":1794377129000,"rx":1794377153897},{"id":"EAL-0236","dir":0,"lat":25.047335,"lon":121.512858,"t":1794377137000,"rx":1794377153897},{"id":"EAL-1157","dir":0,"lat":25.007368,"lon":121.468962,"t":1794377139000,"rx":1794377153897},{"id":"EAL-1161","dir":0,"lat":25.052072,"lon":121.53541,"t":1794377128000,"rx":1794377153897},{"id":"EAL-2266","dir":0,"lat":25.051932,"lon":121.525295,"t":1794377136000,"rx":1794377153897},{"id":"EAL-2267","dir":0,"lat":25.047365,"lon":121.512707,"t":1794377135000,"rx":1794377153897},{"id":"EAA-151","dir":0,"lat":25.008837,"lon":121.459013,"t":1794377152000,"rx":1794377178598},{"id":"EAL-0072","dir":0,"lat":25.02921,"lon":121.500522,"t":1794377152000,"rx":1794377178598},{"id":"EAL-0080","dir":0,"lat":25.013413,"lon":121.462083,"t":1794377163000,"rx":1794377178598},{"id":"EAL-0081","dir":0,"lat":25.000483,"lon":121.496725,"t":1794377155000,"rx":1794377178598},{"id":"EAL-0097","dir":0,"lat":24.99517,"lon":121.480975,"t":1794377155000,"rx":1794377178598},{"id":"EAL-0098","dir":0,"lat":25.011485,"lon":121.456455,"t":1794377169000,"rx":1794377178598},{"id":"EAL-0236","dir":0,"lat":25.047293,"lon":121.512847,"t":1794377157000,"rx":1794377178598},{"id":"EAL-1157","dir":0,"lat":25.006857,"lon":121.470438,"t":1794377167000,"rx":1794377178598},{"id":"EAL-1161","dir":0,"lat":25.052135,"lon":121.536028,"t":1794377166000,"rx":1794377178598},{"id":"EAL-1162","dir":0,"lat":25.047477,"lon":121.510055,"t":1794377164000,"rx":1794377178598},{"id":"EAL-1168","dir":0,"lat":25.04555,"lon":121.519142,"t":1794377160000,"rx":1794377178598},{"id":"EAL-2266","dir":0,"lat":25.052183,"lon":121.525443,"t":1794377156000,"rx":1794377178598},{"id":"EAL-2267","dir":0,"lat":25.047365,"lon":121.512707,"t":1794377155000,"rx":1794377178598},{"id":"EAA-151","dir":0,"lat":25.008828,"lon":121.459043,"t":1794377192000,"rx":1794377202737},{"id":"EAL-0072","dir":0,"lat":25.030062,"lon":121.500518,"t":1794377192000,"rx":1794377202737},{"id":"EAL-0080","dir":0,"lat":25.013575,"lon":121.462248,"t":1794377183000,"rx":1794377202737},{"id":"EAL-0081","dir":0,"lat":25.000468,"lon":121.496792,"t":1794377195000,"rx":1794377202737},{"id":"EAL-0097","dir":0,"lat":24.995685,"lon":121.482907,"t":1794377195000,"rx":1794377202737},{"id":"EAL-0098","dir":0,"lat":25.011077,"lon":121.45696,"t":1794377189000,"rx":1794377202737},{"id":"EAL-0236","dir":0,"lat":25.046822,"lon":121.513185,"t":1794377177000,"rx":1794377202737},{"id":"EAL-1157","dir":0,"lat":25.006763,"lon":121.470787,"t":1794377187000,"rx":1794377202737},{"id":"EAL-1161","dir":0,"lat":25.052123,"lon":121.53606,"t":1794377186000,"rx":1794377202737},{"id":"EAL-1162","dir":0,"lat":25.047835,"lon":121.510178,"t":1794377184000,"rx":1794377202737},{"id":"EAL-1168","dir":0,"lat":25.045553,"lon":121.51914,"t":1794377180000,"rx":1794377202737},{"id":"EAL-2266","dir":0,"lat":25.0522,"lon":121.525435,"t":1794377176000,"rx":1794377202737},{"id":"EAL-2267","dir":0,"lat":25.047322,"lon":121.512697,"t":1794377175000,"rx":1794377202737},{"id":"EAA-151","dir":0,"lat":25.008827,"lon":121.459047,"t":1794377212000,"rx":1794377226549},{"id":"EAL-0072","dir":0,"lat":25.030912,"lon":121.500572,"t":1794377212000,"rx":1794377226549},{"id":"EAL-0080","dir":0,"lat":25.013577,"lon":121.462577,"t":1794377203000,"rx":1794377226549},{"id":"EAL-0081","dir":0,"lat":25.00101,"lon":121.497087,"t":1794377215000,"rx":1794377226549},{"id":"EAL-0097","dir":0,"lat":24.99569,"lon":121.482952,"t":1794377215000,"rx":1794377226549},{"id":"EAL-0098","dir":0,"lat":25.010553,"lon":121.457693,"t":1794377214000,"rx":1794377226549},{"id":"EAL-0236","dir":0,"lat":25.046637,"lon":121.514732,"t":1794377217000,"rx":1794377226549},{"id":"EAL-1157","dir":0,"lat":25.006763,"lon":121.470783,"t":1794377207000,"rx":1794377226549},{"id":"EAL-1161","dir":0,"lat":25.052113,"lon":121.536057,"t":1794377206000,"rx":1794377226549},{"id":"EAL-1162","dir":0,"lat":25.048052,"lon":121.511307,"t":1794377204000,"rx":1794377226549},{"id":"EAL-1168","dir":0,"lat":25.045382,"lon":121.520728,"t":1794377219000,"rx":1794377226549},{"id":"EAL-2266","dir":0,"lat":25.052073,"lon":121.527795,"t":1794377216000,"rx":1794377226549},{"id":"EAL-2267","dir":0,"lat":25.046932,"lon":121.514193,"t":1794377215000,"rx":1794377226549},{"id":"EAA-151","dir":0,"lat":25.008842,"lon":121.459655,"t":1794377238000,"rx":1794377250371},{"id":"EAL-0072","dir":0,"lat":25.031223,"lon":121.50137,"t":1794377237000,"rx":1794377250371},{"id":"EAL-0080","dir":0,"lat":25.013292,"lon":121.462843,"t":1794377223000,"rx":1794377250371},{"id":"EAL-0081","dir":0,"lat":25.001852,"lon":121.496872,"t":1794377235000,"rx":1794377250371},{"id":"EAL-0097","dir":0,"lat":24.995663,"lon":121.482968,"t":1794377235000,"rx":1794377250371},{"id":"EAL-0098","dir":0,"lat":25.009632,"lon":121.45778,"t":1794377233000,"rx":1794377250371},{"id":"EAL-0236","dir":0,"lat":25.046455,"lon":121.515827,"t":1794377237000,"rx":1794377250371},{"id":"EAL-1157","dir":0,"lat":25.006763,"lon":121.470783,"t":1794377227000,"rx":1794377250371},{"id":"EAL-1159","dir":0,"lat":25.023797,"lon":121.44992,"t":1794377231000,"rx":1794377250371},{"id":"EAL-1161","dir":0,"lat":25.051948,"lon":121.537202,"t":1794377231000,"rx":1794377250371},{"id":"EAL-1162","dir":0,"lat":25.04715,"lon":121.513592,"t":1794377231000,"rx":1794377250371},{"id":"EAL-1168","dir":0,"lat":25.045493,"lon":121.520392,"t":1794377220000,"rx":1794377250371},{"id":"EAL-2266","dir":0,"lat":25.0521,"lon":121.529478,"t":1794377236000,"rx":1794377250371},{"id":"EAL-2267","dir":0,"lat":25.04653,"lon":121.515597,"t":1794377235000,"rx":1794377250371},{"id":"EAL-0080","dir":0,"lat":25.012923,"lon":121.46355,"t":1794377245000,"rx":1794377261145},{"id":"EAL-1162","dir":0,"lat":25.046885,"lon":121.514622,"t":1794377244000,"rx":1794377261145},{"id":"EAL-1168","dir":0,"lat":25.04525,"lon":121.521193,"t":1794377240000,"rx":1794377261145},{"id":"EAA-151","dir":0,"lat":25.009617,"lon":121.460288,"t":1794377252000,"rx":1794377274392},{"id":"EAL-0072","dir":0,"lat":25.031223,"lon":121.502082,"t":1794377252000,"rx":1794377274392},{"id":"EAL-0080","dir":0,"lat":25.012008,"lon":121.464368,"t":1794377263000,"rx":1794377274392},{"id":"EAL-0081","dir":0,"lat":25.002198,"lon":121.496793,"t":1794377259000,"rx":1794377274392},{"id":"EAL-0097","dir":0,"lat":24.995655,"lon":121.48298,"t":1794377255000,"rx":1794377274392},{"id":"EAL-0098","dir":0,"lat":25.009347,"lon":121.457993,"t":1794377249000,"rx":1794377274392},{"id":"EAL-0236","dir":0,"lat":25.046665,"lon":121.515803,"t":1794377257000,"rx":1794377274392},{"id":"EAL-1157","dir":0,"lat":25.006608,"lon":121.47138,"t":1794377262000,"rx":1794377274392},{"id":"EAL-1159","dir":0,"lat":25.023733,"lon":121.449932,"t":1794377251000,"rx":1794377274392},{"id":"EAL-1161","dir":0,"lat":25.051922,"lon":121.53909,"t":1794377250000,"rx":1794377274392},{"id":"EAL-1162","dir":0,"lat":25.046715,"lon":121.515492,"t":1794377264000,"rx":1794377274392},{"id":"EAL-1168","dir":0,"lat":25.045177,"lon":121.521442,"t":1794377260000,"rx":1794377274392},{"id":"EAL-2266","dir":0,"lat":25.052083,"lon":121.52994,"t":1794377256000,"rx":1794377274392},{"id":"EAL-2267","dir":0,"lat":25.046523,"lon":121.515713,"t":1794377255000,"rx":1794377274392},{"id":"EAA-151","dir":0,"lat":25.01074,"lon":121.461242,"t":1794377272000,"rx":1794377296670},{"id":"EAL-0072","dir":0,"lat":25.03122,"lon":121.502237,"t":1794377272000,"rx":1794377296670},{"id":"EAL-0080","dir":0,"lat":25.011838,"lon":121.464503,"t":1794377283000,"rx":1794377296670},{"id":"EAL-0081","dir":0,"lat":25.00307,"lon":121.496697,"t":1794377275000,"rx":1794377296670},{"id":"EAL-0097","dir":0,"lat":24.99626,"lon":121.484788,"t":1794377285000,"rx":1794377296670},{"id":"EAL-0098","dir":0,"lat":25.008942,"lon":121.458855,"t":1794377289000,"rx":1794377296670},{"id":"EAL-0236","dir":0,"lat":25.046048,"lon":121.517443,"t":1794377277000,"rx":1794377296670},{"id":"EAL-1157","dir":0,"lat":25.006388,"lon":121.472953,"t":1794377287000,"rx":1794377296670},{"id":"EAL-1159","dir":0,"lat":25.024333,"lon":121.450128,"t":1794377271000,"rx":1794377296670},{"id":"EAL-1161","dir":0,"lat":25.051888,"lon":121.539772,"t":1794377286000,"rx":1794377296670},{"id":"EAL-1162","dir":0,"lat":25.046563,"lon":121.516262,"t":1794377284000,"rx":1794377296670},{"id":"EAL-1168","dir":0,"lat":25.04493,"lon":121.521417,"t":1794377280000,"rx":1794377296670},{"id":"EAL-2266","dir":0,"lat":25.05217,"lon":121.531352,"t":1794377284000,"rx":1794377296670},{"id":"EAL-2267","dir":0,"lat":25.046357,"lon":121.516352,"t":1794377275000,"rx":1794377296670},{"id":"EAA-151","dir":0,"lat":25.011227,"lon":121.461623,"t":1794377292000,"rx":1794377320722},{"id":"EAL-0072","dir":0,"lat":25.031308,"lon":121.503975,"t":1794377304000,"rx":1794377320722},{"id":"EAL-0080","dir":0,"lat":25.011747,"lon":121.464583,"t":1794377303000,"rx":1794377320722},{"id":"EAL-0081","dir":0,"lat":25.003213,"lon":121.496703,"t":1794377295000,"rx":1794377320722},{"id":"EAL-0097","dir":0,"lat":24.996253,"lon":121.485117,"t":1794377295000,"rx":1794377320722},{"id":"EAL-0098","dir":0,"lat":25.008903,"lon":121.458953,"t":1794377309000,"rx":1794377320722},{"id":"EAL-0236","dir":0,"lat":25.045538,"lon":121.518895,"t":1794377297000,"rx":1794377320722},{"id":"EAL-1157","dir":0,"lat":25.006407,"lon":121.473172,"t":1794377307000,"rx":1794377320722},{"id":"EAL-1159","dir":0,"lat":25.025188,"lon":121.450917,"t":1794377296000,"rx":1794377320722},{"id":"EAL-1161","dir":0,"lat":25.051888,"lon":121.54222,"t":1794377306000,"rx":1794377320722},{"id":"EAL-1162","dir":0,"lat":25.046138,"lon":121.51776,"t":1794377304000,"rx":1794377320722},{"id":"EAL-1168","dir":0,"lat":25.044608,"lon":121.5214,"t":1794377300000,"rx":1794377320722},{"id":"EAL-2266","dir":0,"lat":25.052012,"lon":121.532825,"t":1794377301000,"rx":1794377320722},{"id":"EAL-2267","dir":0,"lat":25.045885,"lon":121.51831,"t":1794377296000,"rx":1794377320722},{"id":"EAA-151","dir":0,"lat":25.011242,"lon":121.4617,"t":1794377312000,"rx":1794377332836},{"id":"EAL-0072","dir":0,"lat":25.031425,"lon":121.50429,"t":1794377312000,"rx":1794377332836},{"id":"EAL-0081","dir":0,"lat":25.004055,"lon":121.496528,"t":1794377315000,"rx":1794377332836},{"id":"EAL-0097","dir":0,"lat":24.996328,"lon":121.486482,"t":1794377315000,"rx":1794377332836},{"id":"EAL-1159","dir":0,"lat":25.026482,"lon":121.452337,"t":1794377314000,"rx":1794377332836},{"id":"EAA-151","dir":0,"lat":25.01134,"lon":121.461823,"t":1794377332000,"rx":1794377344757},{"id":"EAL-0072","dir":0,"lat":25.032222,"lon":121.504662,"t":1794377332000,"rx":1794377344757},{"id":"EAL-0080","dir":0,"lat":25.011707,"lon":121.464585,"t":1794377323000,"rx":1794377344757},{"id":"EAL-0081","dir":0,"lat":25.004587,"lon":121.496333,"t":1794377335000,"rx":1794377344757},{"id":"EAL-0097","dir":0,"lat":24.99638,"lon":121.488632,"t":1794377335000,"rx":1794377344757},{"id":"EAL-0098","dir":0,"lat":25.008838,"lon":121.458847,"t":1794377329000,"rx":1794377344757},{"id":"EAL-0236","dir":0,"lat":25.045618,"lon":121.519118,"t":1794377317000,"rx":1794377344757},{"id":"EAL-1157","dir":0,"lat":25.006283,"lon":121.473552,"t":1794377327000,"rx":1794377344757},{"id":"EAL-1159","dir":0,"lat":25.026732,"lon":121.452795,"t":1794377331000,"rx":1794377344757},{"id":"EAL-1161","dir":0,"lat":25.051845,"lon":121.54331,"t":1794377326000,"rx":1794377344757},{"id":"EAL-1162","dir":0,"lat":25.045837,"lon":121.518527,"t":1794377324000,"rx":1794377344757},{"id":"EAL-1168","dir":0,"lat":25.04451,"lon":121.521457,"t":1794377320000,"rx":1794377344757},{"id":"EAL-2266","dir":0,"lat":25.052018,"lon":121.532888,"t":1794377317000,"rx":1794377344757},{"id":"EAL-2267","dir":0,"lat":25.045795,"lon":121.518838,"t":1794377315000,"rx":1794377344757},{"id":"EAL-0080","dir":0,"lat":25.011565,"lon":121.464713,"t":1794377343000,"rx":1794377359605},{"id":"EAL-0097","dir":0,"lat":24.996352,"lon":121.489297,"t":1794377342000,"rx":1794377359605},{"id":"EAL-0098","dir":0,"lat":25.008908,"lon":121.458858,"t":1794377349000,"rx":1794377359605},{"id":"EAL-0236","dir":0,"lat":25.045757,"lon":121.51944,"t":1794377337000,"rx":1794377359605},{"id":"EAL-1157","dir":0,"lat":25.006263,"lon":121.473545,"t":1794377347000,"rx":1794377359605},{"id":"EAL-1159","dir":0,"lat":25.026042,"lon":121.453728,"t":1794377347000,"rx":1794377359605},{"id":"EAL-1161","dir":0,"lat":25.051893,"lon":121.543368,"t":1794377346000,"rx":1794377359605},{"id":"EAL-1162","dir":0,"lat":25.045947,"lon":121.518598,"t":1794377344000,"rx":1794377359605},{"id":"EAL-1168","dir":0,"lat":25.04425,"lon":121.521362,"t":1794377341000,"rx":1794377359605},{"id":"EAL-2266","dir":0,"lat":25.052095,"lon":121.532792,"t":1794377336000,"rx":1794377359605},{"id":"EAL-2267","dir":0,"lat":25.045803,"lon":121.518848,"t":1794377335000,"rx":1794377359605},{"id":"EAA-151","dir":0,"lat":25.012632,"lon":121.460488,"t":1794377373000,"rx":1794377380931},{"id":"EAL-0072","dir":0,"lat":25.034263,"lon":121.505793,"t":1794377372000,"rx":1794377380931},{"id":"EAL-0080","dir":0,"lat":25.01157,"lon":121.464713,"t":1794377363000,"rx":1794377380931},{"id":"EAL-0081","dir":0,"lat":25.00839,"lon":121.495427,"t":1794377375000,"rx":1794377380931},{"id":"EAL-0097","dir":0,"lat":24.996325,"lon":121.489682,"t":1794377375000,"rx":1794377380931},{"id":"EAL-0098","dir":0,"lat":25.008902,"lon":121.458873,"t":1794377369000,"rx":1794377380931},{"id":"EAL-0236","dir":0,"lat":25.045703,"lon":121.519108,"t":1794377357000,"rx":1794377380931},{"id":"EAL-1157","dir":0,"lat":25.006275,"lon":121.473533,"t":1794377367000,"rx":1794377380931},{"id":"EAL-1159","dir":0,"lat":25.025625,"lon":121.45427,"t":1794377371000,"rx":1794377380931},{"id":"EAL-1161","dir":0,"lat":25.051928,"lon":121.543355,"t":1794377366000,"rx":1794377380931},{"id":"EAL-1162","dir":0,"lat":25.045907,"lon":121.51862,"t":1794377364000,"rx":1794377380931},{"id":"EAL-1168","dir":0,"lat":25.043938,"lon":121.522047,"t":1794377365000,"rx":1794377380931},{"id":"EAL-2266","dir":0,"lat":25.052093,"lon":121.532815,"t":1794377356000,"rx":1794377380931},{"id":"EAL-2267","dir":0,"lat":25.045803,"lon":121.518848,"t":1794377375000,"rx":1794377380931},{"id":"EAA-151","dir":0,"lat":25.012657,"lon":121.460483,"t":1794377392000,"rx":1794377404783},{"id":"EAL-0072","dir":0,"lat":25.035567,"lon":121.506555,"t":1794377392000,"rx":1794377404783},{"id":"EAL-0080","dir":0,"lat":25.010478,"lon":121.465277,"t":1794377391000,"rx":1794377404783},{"id":"EAL-0081","dir":0,"lat":25.010607,"lon":121.495437,"t":1794377395000,"rx":1794377404783},{"id":"EAL-0097","dir":0,"lat":24.99629,"lon":121.489693,"t":1794377395000,"rx":1794377404783},{"id":"EAL-0098","dir":0,"lat":25.008812,"lon":121.459672,"t":1794377392000,"rx":1794377404783},{"id":"EAL-0236","dir":0,"lat":25.04592,"lon":121.519208,"t":1794377377000,"rx":1794377404783},{"id":"EAL-1157","dir":0,"lat":25.00614,"lon":121.474362,"t":1794377387000,"rx":1794377404783},{"id":"EAL-1159","dir":0,"lat":25.024747,"lon":121.454257,"t":1794377392000,"rx":1794377404783},{"id":"EAL-1161","dir":0,"lat":25.05194,"lon":121.543358,"t":1794377386000,"rx":1794377404783},{"id":"EAL-1162","dir":0,"lat":25.045905,"lon":121.518615,"t":1794377384000,"rx":1794377404783},{"id":"EAL-1168","dir":0,"lat":25.043918,"lon":121.522263,"t":1794377380000,"rx":1794377404783},{"id":"EAL-2266","dir":0,"lat":25.052102,"lon":121.532832,"t":1794377376000,"rx":1794377404783}],"boardingEta":[{"t":1794376161163,"eta":-1,"arriving":false},{"t":1794376169519,"eta":-1,"arriving":false},{"t":1794376182180,"eta":186,"arriving":false},{"t":1794376193812,"eta":186,"arriving":false},{"t":1794376208338,"eta":-1,"arriving":false},{"t":1794376217701,"eta":-1,"arriving":false},{"t":1794376232517,"eta":175,"arriving":false},{"t":1794376241661,"eta":175,"arriving":false},{"t":1794376254932,"eta":116,"arriving":false},{"t":1794376265507,"eta":116,"arriving":false},{"t":1794376278192,"eta":115,"arriving":false},{"t":1794376292087,"eta":114,"arriving":false},{"t":1794376301192,"eta":114,"arriving":false},{"t":1794376314034,"eta":103,"arriving":false},{"t":1794376326472,"eta":103,"arriving":false},{"t":1794376338415,"eta":102,"arriving":false},{"t":1794376350524,"eta":102,"arriving":false},{"t":1794376362708,"eta":97,"arriving":false},{"t":1794376374860,"eta":97,"arriving":false},{"t":1794376386614,"eta":20,"arriving":false},{"t":1794376397644,"eta":20,"arriving":false},{"t":1794376412620,"eta":18,"arriving":false},{"t":1794376421370,"eta":18,"arriving":false},{"t":1794376435321,"eta":19,"arriving":false},{"t":1794376445195,"eta":19,"arriving":false},{"t":1794376460328,"eta":-1,"arriving":false},{"t":1794376469440,"eta":-1,"arriving":false},{"t":1794376483762,"eta":580,"arriving":false},{"t":1794376493332,"eta":580,"arriving":false},{"t":1794376508346,"eta":567,"arriving":false},{"t":1794376517400,"eta":567,"arriving":false},{"t":1794376531141,"eta":563,"arriving":false},{"t":1794376541250,"eta":563,"arriving":false},{"t":1794376555485,"eta":566,"arriving":false},{"t":1794376565240,"eta":566,"arriving":false},{"t":1794376580076,"eta":566,"arriving":false},{"t":1794376589613,"eta":566,"arriving":false},{"t":1794376603608,"eta":566,"arriving":false},{"t":1794376613254,"eta":566,"arriving":false},{"t":1794376628378,"eta":458,"arriving":false},{"t":1794376637375,"eta":458,"arriving":false},{"t":1794376652253,"eta":452,"arriving":false},{"t":1794376661383,"eta":452,"arriving":false},{"t":1794376675104,"eta":363,"arriving":false},{"t":1794376685506,"eta":363,"arriving":false},{"t":1794376699548,"eta":366,"arriving":false},{"t":1794376709397,"eta":366,"arriving":false},{"t":1794376723203,"eta":366,"arriving":false},{"t":1794376733347,"eta":366,"arriving":false},{"t":1794376745964,"eta":291,"arriving":false},{"t":1794376759202,"eta":291,"arriving":false},{"t":1794376770255,"eta":294,"arriving":false},{"t":1794376785008,"eta":293,"arriving":false},{"t":1794376793262,"eta":293,"arriving":false},{"t":1794376807605,"eta":237,"arriving":false},{"t":1794376817441,"eta":237,"arriving":false},{"t":1794376831615,"eta":138,"arriving":false},{"t":1794376841689,"eta":138,"arriving":false},{"t":1794376854925,"eta":141,"arriving":false},{"t":1794376866466,"eta":138,"arriving":false},{"t":1794376878846,"eta":138,"arriving":false},{"t":1794376890367,"eta":138,"arriving":false},{"t":1794376902417,"eta":138,"arriving":false},{"t":1794376914080,"eta":47,"arriving":false},{"t":1794376926656,"eta":47,"arriving":false},{"t":1794376938489,"eta":-1,"arriving":false},{"t":1794376949704,"eta":-1,"arriving":false},{"t":1794376963702,"eta":832,"arriving":false},{"t":1794376973445,"eta":832,"arriving":false},{"t":1794376988181,"eta":730,"arriving":false},{"t":1794376997395,"eta":730,"arriving":false},{"t":1794377011382,"eta":725,"arriving":false},{"t":1794377021713,"eta":725,"arriving":false},{"t":1794377036381,"eta":648,"arriving":false},{"t":1794377046142,"eta":648,"arriving":false},{"t":1794377060522,"eta":652,"arriving":false},{"t":1794377069762,"eta":652,"arriving":false},{"t":1794377084355,"eta":595,"arriving":false},{"t":1794377093783,"eta":595,"arriving":false},{"t":1794377108774,"eta":594,"arriving":false},{"t":1794377117786,"eta":594,"arriving":false},{"t":1794377132890,"eta":592,"arriving":false},{"t":1794377141239,"eta":592,"arriving":false},{"t":1794377154508,"eta":593,"arriving":false},{"t":1794377165329,"eta":593,"arriving":false},{"t":1794377180180,"eta":513,"arriving":false},{"t":1794377189844,"eta":513,"arriving":false},{"t":1794377204253,"eta":516,"arriving":false},{"t":1794377213529,"eta":516,"arriving":false},{"t":1794377228410,"eta":461,"arriving":false},{"t":1794377237765,"eta":461,"arriving":false},{"t":1794377251874,"eta":461,"arriving":false},{"t":1794377261751,"eta":461,"arriving":false},{"t":1794377276519,"eta":464,"arriving":false},{"t":1794377285907,"eta":464,"arriving":false},{"t":1794377298048,"eta":263,"arriving":false},{"t":1794377312042,"eta":263,"arriving":false},{"t":1794377322545,"eta":262,"arriving":false},{"t":1794377333740,"eta":262,"arriving":false},{"t":1794377346549,"eta":265,"arriving":false},{"t":1794377361626,"eta":263,"arriving":false},{"t":1794377369592,"eta":263,"arriving":false},{"t":1794377382264,"eta":265,"arriving":false},{"t":1794377394139,"eta":265,"arriving":false},{"t":1794377406609,"eta":149,"arriving":false}],"riderFixes":[{"t":1794376160000,"lat":25.023426,"lon":121.499905,"acc":15.0,"spd":0.0},{"t":1794376165000,"lat":25.023088,"lon":121.500284,"acc":15.0,"spd":0.1},{"t":1794376170000,"lat":25.023256,"lon":121.499874,"acc":15.0,"spd":0.0},{"t":1794376175000,"lat":25.023386,"lon":121.499764,"acc":15.0,"spd":0.0},{"t":1794376180000,"lat":25.023266,"lon":121.500018,"acc":15.0,"spd":0.0},{"t":1794376185000,"lat":25.023484,"lon":121.499792,"acc":15.0,"spd":0.0},{"t":1794376190000,"lat":25.023218,"lon":121.499876,"acc":15.0,"spd":0.3},{"t":1794376195000,"lat":25.023344,"lon":121.499778,"acc":15.0,"spd":0.3},{"t":1794376200000,"lat":25.023083,"lon":121.500139,"acc":15.0,"spd":0.0},{"t":1794376205000,"lat":25.023308,"lon":121.499983,"acc":15.0,"spd":0.1},{"t":1794376210000,"lat":25.023176,"lon":121.499795,"acc":15.0,"spd":0.0},{"t":1794376215000,"lat":25.023155,"lon":121.500206,"acc":15.0,"spd":0.0},{"t":1794376220000,"lat":25.023217,"lon":121.500036,"acc":15.0,"spd":0.0},{"t":1794376225000,"lat":25.023321,"lon":121.500048,"acc":15.0,"spd":0.2},{"t":1794376230000,"lat":25.023238,"lon":121.499869,"acc":15.0,"spd":0.0},{"t":1794376235000,"lat":25.023256,"lon":121.499722,"acc":15.0,"spd":0.0},{"t":1794376240000,"lat":25.023263,"lon":121.499901,"acc":15.0,"spd":0.3},{"t":1794376245000,"lat":25.023195,"lon":121.499979,"acc":15.0,"spd":0.0},{"t":1794376250000,"lat":25.023328,"lon":121.499826,"acc":15.0,"spd":0.0},{"t":1794376255000,"lat":25.023295,"lon":121.499921,"acc":15.0,"spd":0.0},{"t":1794376260000,"lat":25.02332,"lon":121.499895,"acc":15.0,"spd":0.2},{"t":1794376265000,"lat":25.023292,"lon":121.499991,"acc":15.0,"spd":0.1},{"t":1794376270000,"lat":25.023255,"lon":121.49991,"acc":15.0,"spd":0.0},{"t":1794376275000,"lat":25.02315,"lon":121.500088,"acc":15.0,"spd":0.0},{"t":1794376280000,"lat":25.023097,"lon":121.499993,"acc":15.0,"spd":0.0},{"t":1794376285000,"lat":25.023387,"lon":121.499898,"acc":15.0,"spd":0.2},{"t":1794376290000,"lat":25.0231,"lon":121.499858,"acc":15.0,"spd":0.0},{"t":1794376295000,"lat":25.023163,"lon":121.499868,"acc":15.0,"spd":0.0},{"t":1794376300000,"lat":25.023218,"lon":121.499922,"acc":15.0,"spd":0.1},{"t":1794376305000,"lat":25.023266,"lon":121.499632,"acc":15.0,"spd":0.0},{"t":1794376310000,"lat":25.023304,"lon":121.499808,"acc":15.0,"spd":0.0},{"t":1794376315000,"lat":25.023384,"lon":121.499872,"acc":15.0,"spd":0.3},{"t":1794376320000,"lat":25.023428,"lon":121.499905,"acc":15.0,"spd":0.4},{"t":1794376325000,"lat":25.023211,"lon":121.49979,"acc":15.0,"spd":0.0},{"t":1794376330000,"lat":25.023201,"lon":121.500101,"acc":15.0,"spd":0.4},{"t":1794376335000,"lat":25.023357,"lon":121.500039,"acc":15.0,"spd":0.4},{"t":1794376340000,"lat":25.023395,"lon":121.499981,"acc":15.0,"spd":0.7},{"t":1794376345000,"lat":25.023362,"lon":121.499721,"acc":15.0,"spd":0.0},{"t":1794376350000,"lat":25.023373,"lon":121.499734,"acc":15.0,"spd":0.0},{"t":1794376355000,"lat":25.023337,"lon":121.500021,"acc":15.0,"spd":0.0},{"t":1794376360000,"lat":25.023519,"lon":121.499888,"acc":15.0,"spd":0.0},{"t":1794376365000,"lat":25.023454,"lon":121.499915,"acc":15.0,"spd":0.4},{"t":1794376370000,"lat":25.023309,"lon":121.499999,"acc":15.0,"spd":0.0},{"t":1794376375000,"lat":25.023184,"lon":121.499862,"acc":15.0,"spd":0.2},{"t":1794376380000,"lat":25.023153,"lon":121.499802,"acc":15.0,"spd":0.7},{"t":1794376385000,"lat":25.023375,"lon":121.499771,"acc":15.0,"spd":0.1},{"t":1794376390000,"lat":25.023305,"lon":121.49984,"acc":15.0,"spd":0.0},{"t":1794376395000,"lat":25.023276,"lon":121.499917,"acc":15.0,"spd":0.0},{"t":1794376400000,"lat":25.023384,"lon":121.499983,"acc":15.0,"spd":0.0},{"t":1794376405000,"lat":25.023353,"lon":121.499953,"acc":15.0,"spd":0.0},{"t":1794376410000,"lat":25.023196,"lon":121.499837,"acc":15.0,"spd":0.2},{"t":1794376415000,"lat":25.023229,"lon":121.50011,"acc":15.0,"spd":0.2},{"t":1794376420000,"lat":25.023293,"lon":121.499748,"acc":15.0,"spd":0.0},{"t":1794376425000,"lat":25.023361,"lon":121.499807,"acc":15.0,"spd":0.5},{"t":1794376430000,"lat":25.023446,"lon":121.499858,"acc":15.0,"spd":0.3},{"t":1794376435000,"lat":25.023239,"lon":121.499894,"acc":15.0,"spd":0.3},{"t":1794376440000,"lat":25.023277,"lon":121.500311,"acc":15.0,"spd":0.0},{"t":1794376445000,"lat":25.023261,"lon":121.500019,"acc":15.0,"spd":0.0},{"t":1794376450000,"lat":25.023182,"lon":121.499848,"acc":15.0,"spd":0.0},{"t":1794376455000,"lat":25.023285,"lon":121.499853,"acc":15.0,"spd":0.4},{"t":1794376460000,"lat":25.023653,"lon":121.500091,"acc":15.0,"spd":5.6},{"t":1794376465000,"lat":25.02359,"lon":121.500181,"acc":15.0,"spd":7.2},{"t":1794376470000,"lat":25.023963,"lon":121.500032,"acc":15.0,"spd":5.3},{"t":1794376475000,"lat":25.024414,"lon":121.500295,"acc":15.0,"spd":5.6},{"t":1794376480000,"lat":25.024564,"lon":121.500201,"acc":15.0,"spd":5.8},{"t":1794376485000,"lat":25.024678,"lon":121.500541,"acc":15.0,"spd":6.1},{"t":1794376490000,"lat":25.025016,"lon":121.500264,"acc":15.0,"spd":5.0},{"t":1794376495000,"lat":25.025128,"lon":121.500389,"acc":15.0,"spd":5.4},{"t":1794376500000,"lat":25.025522,"lon":121.500614,"acc":15.0,"spd":4.9},{"t":1794376505000,"lat":25.025674,"lon":121.500532,"acc":15.0,"spd":4.3},{"t":1794376510000,"lat":25.025763,"lon":121.500665,"acc":15.0,"spd":2.3},{"t":1794376515000,"lat":25.025993,"lon":121.500844,"acc":15.0,"spd":2.7},{"t":1794376520000,"lat":25.025918,"lon":121.500602,"acc":15.0,"spd":2.4},{"t":1794376525000,"lat":25.025964,"lon":121.500634,"acc":15.0,"spd":2.4},{"t":1794376530000,"lat":25.026039,"lon":121.500591,"acc":15.0,"spd":0.5},{"t":1794376535000,"lat":25.026134,"lon":121.500637,"acc":15.0,"spd":0.0},{"t":1794376540000,"lat":25.026159,"lon":121.50065,"acc":15.0,"spd":0.0},{"t":1794376545000,"lat":25.026078,"lon":121.500435,"acc":15.0,"spd":0.0},{"t":1794376550000,"lat":25.026115,"lon":121.500538,"acc":15.0,"spd":0.1},{"t":1794376555000,"lat":25.026164,"lon":121.500576,"acc":15.0,"spd":0.5},{"t":1794376560000,"lat":25.025963,"lon":121.500575,"acc":15.0,"spd":1.2},{"t":1794376565000,"lat":25.026058,"lon":121.500533,"acc":15.0,"spd":2.0},{"t":1794376570000,"lat":25.026502,"lon":121.500618,"acc":15.0,"spd":6.3},{"t":1794376575000,"lat":25.026833,"lon":121.500525,"acc":15.0,"spd":6.8},{"t":1794376580000,"lat":25.027185,"lon":121.500516,"acc":15.0,"spd":6.9},{"t":1794376585000,"lat":25.027416,"lon":121.500434,"acc":15.0,"spd":7.0},{"t":1794376590000,"lat":25.027856,"lon":121.500727,"acc":15.0,"spd":10.1},{"t":1794376595000,"lat":25.028297,"lon":121.500641,"acc":15.0,"spd":9.7},{"t":1794376600000,"lat":25.028769,"lon":121.500289,"acc":15.0,"spd":9.6},{"t":1794376605000,"lat":25.028991,"lon":121.500568,"acc":15.0,"spd":9.2},{"t":1794376610000,"lat":25.029516,"lon":121.500345,"acc":15.0,"spd":10.0},{"t":1794376615000,"lat":25.03021,"lon":121.500694,"acc":15.0,"spd":9.1},{"t":1794376620000,"lat":25.030455,"lon":121.500625,"acc":15.0,"spd":9.8},{"t":1794376625000,"lat":25.030822,"lon":121.500956,"acc":15.0,"spd":8.2},{"t":1794376630000,"lat":25.03083,"lon":121.500905,"acc":15.0,"spd":5.2},{"t":1794376635000,"lat":25.031015,"lon":121.501232,"acc":15.0,"spd":4.5},{"t":1794376640000,"lat":25.031158,"lon":121.501256,"acc":15.0,"spd":4.9},{"t":1794376645000,"lat":25.031051,"lon":121.501384,"acc":15.0,"spd":5.1},{"t":1794376650000,"lat":25.031172,"lon":121.501459,"acc":15.0,"spd":3.3},{"t":1794376655000,"lat":25.031434,"lon":121.501704,"acc":15.0,"spd":3.6},{"t":1794376660000,"lat":25.031253,"lon":121.501965,"acc":15.0,"spd":4.2},{"t":1794376665000,"lat":25.031338,"lon":121.50212,"acc":15.0,"spd":4.5},{"t":1794376670000,"lat":25.031169,"lon":121.502642,"acc":15.0,"spd":5.6},{"t":1794376675000,"lat":25.0312,"lon":121.502839,"acc":15.0,"spd":6.5},{"t":1794376680000,"lat":25.031234,"lon":121.502933,"acc":15.0,"spd":4.8},{"t":1794376685000,"lat":25.031053,"lon":121.503288,"acc":15.0,"spd":5.0},{"t":1794376690000,"lat":25.031393,"lon":121.503321,"acc":15.0,"spd":4.7},{"t":1794376695000,"lat":25.031287,"lon":121.503708,"acc":15.0,"spd":4.4},{"t":1794376700000,"lat":25.031364,"lon":121.503704,"acc":15.0,"spd":4.9},{"t":1794376705000,"lat":25.031383,"lon":121.50393,"acc":15.0,"spd":4.3},{"t":1794376710000,"lat":25.03165,"lon":121.504063,"acc":15.0,"spd":5.6},{"t":1794376715000,"lat":25.031826,"lon":121.50399,"acc":15.0,"spd":5.8},{"t":1794376720000,"lat":25.03199,"lon":121.504561,"acc":15.0,"spd":5.7},{"t":1794376725000,"lat":25.032056,"lon":121.504685,"acc":15.0,"spd":5.9},{"t":1794376730000,"lat":25.032465,"lon":121.504821,"acc":15.0,"spd":5.2},{"t":1794376735000,"lat":25.032646,"lon":121.504939,"acc":15.0,"spd":5.9},{"t":1794376740000,"lat":25.03301,"lon":121.504962,"acc":15.0,"spd":5.8},{"t":1794376745000,"lat":25.033234,"lon":121.505226,"acc":15.0,"spd":6.1},{"t":1794376750000,"lat":25.033599,"lon":121.505291,"acc":15.0,"spd":5.9},{"t":1794376755000,"lat":25.033629,"lon":121.505319,"acc":15.0,"spd":5.8},{"t":1794376760000,"lat":25.03416,"lon":121.505648,"acc":15.0,"spd":8.1},{"t":1794376765000,"lat":25.034372,"lon":121.505644,"acc":15.0,"spd":8.1},{"t":1794376770000,"lat":25.034528,"lon":121.506014,"acc":15.0,"spd":8.6},{"t":1794376775000,"lat":25.034917,"lon":121.50614,"acc":15.0,"spd":7.7},{"t":1794376780000,"lat":25.035448,"lon":121.506473,"acc":15.0,"spd":7.6},{"t":1794376785000,"lat":25.035596,"lon":121.506366,"acc":15.0,"spd":7.2},{"t":1794376790000,"lat":25.035596,"lon":121.506319,"acc":15.0,"spd":1.4},{"t":1794376795000,"lat":25.035684,"lon":121.506431,"acc":15.0,"spd":0.4},{"t":1794376800000,"lat":25.035728,"lon":121.506567,"acc":15.0,"spd":1.7},{"t":1794376805000,"lat":25.03584,"lon":121.506547,"acc":15.0,"spd":0.9},{"t":1794376810000,"lat":25.035728,"lon":121.506607,"acc":15.0,"spd":0.8},{"t":1794376815000,"lat":25.035848,"lon":121.506485,"acc":15.0,"spd":0.3},{"t":1794376820000,"lat":25.035674,"lon":121.506477,"acc":15.0,"spd":0.5},{"t":1794376825000,"lat":25.035484,"lon":121.506443,"acc":15.0,"spd":0.7},{"t":1794376830000,"lat":25.035842,"lon":121.506703,"acc":15.0,"spd":0.0},{"t":1794376835000,"lat":25.035833,"lon":121.506474,"acc":15.0,"spd":0.5},{"t":1794376840000,"lat":25.03577,"lon":121.506415,"acc":15.0,"spd":0.1},{"t":1794376845000,"lat":25.035678,"lon":121.506523,"acc":15.0,"spd":0.0},{"t":1794376850000,"lat":25.035915,"lon":121.506655,"acc":15.0,"spd":0.0},{"t":1794376855000,"lat":25.035813,"lon":121.506382,"acc":15.0,"spd":0.0},{"t":1794376860000,"lat":25.035618,"lon":121.506603,"acc":15.0,"spd":0.0},{"t":1794376865000,"lat":25.035844,"lon":121.506672,"acc":15.0,"spd":0.2},{"t":1794376870000,"lat":25.036123,"lon":121.506527,"acc":15.0,"spd":4.6},{"t":1794376875000,"lat":25.036079,"lon":121.506785,"acc":15.0,"spd":3.6},{"t":1794376880000,"lat":25.03624,"lon":121.506678,"acc":15.0,"spd":3.4},{"t":1794376885000,"lat":25.03646,"lon":121.506687,"acc":15.0,"spd":4.3},{"t":1794376890000,"lat":25.036826,"lon":121.506687,"acc":15.0,"spd":4.6},{"t":1794376895000,"lat":25.036783,"lon":121.506924,"acc":15.0,"spd":4.1},{"t":1794376900000,"lat":25.036847,"lon":121.506767,"acc":15.0,"spd":4.7},{"t":1794376905000,"lat":25.037426,"lon":121.506883,"acc":15.0,"spd":5.9},{"t":1794376910000,"lat":25.037883,"lon":121.507249,"acc":15.0,"spd":11.4},{"t":1794376915000,"lat":25.038438,"lon":121.507396,"acc":15.0,"spd":11.5},{"t":1794376920000,"lat":25.038883,"lon":121.507563,"acc":15.0,"spd":11.9},{"t":1794376925000,"lat":25.039416,"lon":121.507647,"acc":15.0,"spd":12.2},{"t":1794376930000,"lat":25.039838,"lon":121.507774,"acc":15.0,"spd":12.5},{"t":1794376935000,"lat":25.040522,"lon":121.507672,"acc":15.0,"spd":13.4},{"t":1794376940000,"lat":25.040916,"lon":121.508065,"acc":15.0,"spd":13.5},{"t":1794376945000,"lat":25.041553,"lon":121.508178,"acc":15.0,"spd":10.4},{"t":1794376950000,"lat":25.041415,"lon":121.508178,"acc":15.0,"spd":2.8},{"t":1794376955000,"lat":25.041882,"lon":121.508212,"acc":15.0,"spd":3.6},{"t":1794376960000,"lat":25.041735,"lon":121.50802,"acc":15.0,"spd":3.4},{"t":1794376965000,"lat":25.042065,"lon":121.508387,"acc":15.0,"spd":2.9},{"t":1794376970000,"lat":25.042465,"lon":121.50854,"acc":15.0,"spd":8.9},{"t":1794376975000,"lat":25.043048,"lon":121.508828,"acc":15.0,"spd":11.3},{"t":1794376980000,"lat":25.043518,"lon":121.508592,"acc":15.0,"spd":11.1},{"t":1794376985000,"lat":25.043977,"lon":121.509035,"acc":15.0,"spd":12.6},{"t":1794376990000,"lat":25.044536,"lon":121.508916,"acc":15.0,"spd":10.7},{"t":1794376995000,"lat":25.045043,"lon":121.508989,"acc":15.0,"spd":9.9},{"t":1794377000000,"lat":25.044746,"lon":121.509154,"acc":15.0,"spd":1.8},{"t":1794377005000,"lat":25.045233,"lon":121.509055,"acc":15.0,"spd":1.1},{"t":1794377010000,"lat":25.045062,"lon":121.509151,"acc":15.0,"spd":0.0},{"t":1794377015000,"lat":25.04488,"lon":121.509128,"acc":15.0,"spd":0.3},{"t":1794377020000,"lat":25.045111,"lon":121.509397,"acc":15.0,"spd":0.3},{"t":1794377025000,"lat":25.045161,"lon":121.509191,"acc":15.0,"spd":0.1},{"t":1794377030000,"lat":25.045112,"lon":121.509111,"acc":15.0,"spd":0.0},{"t":1794377035000,"lat":25.04494,"lon":121.509339,"acc":15.0,"spd":0.3},{"t":1794377040000,"lat":25.045008,"lon":121.509286,"acc":15.0,"spd":0.4},{"t":1794377045000,"lat":25.044912,"lon":121.50951,"acc":15.0,"spd":0.5},{"t":1794377050000,"lat":25.045208,"lon":121.509377,"acc":15.0,"spd":0.8},{"t":1794377055000,"lat":25.044941,"lon":121.509272,"acc":15.0,"spd":0.5},{"t":1794377060000,"lat":25.045257,"lon":121.509207,"acc":15.0,"spd":0.0},{"t":1794377065000,"lat":25.044897,"lon":121.50921,"acc":15.0,"spd":0.0},{"t":1794377070000,"lat":25.04527,"lon":121.509243,"acc":15.0,"spd":0.0},{"t":1794377075000,"lat":25.04487,"lon":121.509246,"acc":15.0,"spd":0.4},{"t":1794377080000,"lat":25.045226,"lon":121.509392,"acc":15.0,"spd":0.6},{"t":1794377085000,"lat":25.045019,"lon":121.509117,"acc":15.0,"spd":1.0},{"t":1794377090000,"lat":25.045366,"lon":121.50919,"acc":15.0,"spd":4.6},{"t":1794377095000,"lat":25.045498,"lon":121.509418,"acc":15.0,"spd":4.9},{"t":1794377100000,"lat":25.045682,"lon":121.50943,"acc":15.0,"spd":5.2},{"t":1794377105000,"lat":25.045807,"lon":121.509455,"acc":15.0,"spd":4.9},{"t":1794377110000,"lat":25.046281,"lon":121.509454,"acc":15.0,"spd":4.3},{"t":1794377115000,"lat":25.046227,"lon":121.509761,"acc":15.0,"spd":5.0},{"t":1794377120000,"lat":25.046427,"lon":121.509634,"acc":15.0,"spd":4.7},{"t":1794377125000,"lat":25.046589,"lon":121.509568,"acc":15.0,"spd":3.9},{"t":1794377130000,"lat":25.046673,"lon":121.509822,"acc":15.0,"spd":3.4},{"t":1794377135000,"lat":25.046892,"lon":121.509888,"acc":15.0,"spd":2.8},{"t":1794377140000,"lat":25.046762,"lon":121.509936,"acc":15.0,"spd":3.1},{"t":1794377145000,"lat":25.047143,"lon":121.509864,"acc":15.0,"spd":2.5},{"t":1794377150000,"lat":25.047006,"lon":121.510066,"acc":15.0,"spd":2.5},{"t":1794377155000,"lat":25.047131,"lon":121.510117,"acc":15.0,"spd":2.5},{"t":1794377160000,"lat":25.047352,"lon":121.510179,"acc":15.0,"spd":3.1},{"t":1794377165000,"lat":25.047657,"lon":121.510101,"acc":15.0,"spd":2.1},{"t":1794377170000,"lat":25.047568,"lon":121.510205,"acc":15.0,"spd":1.9},{"t":1794377175000,"lat":25.047639,"lon":121.510025,"acc":15.0,"spd":1.6},{"t":1794377180000,"lat":25.047808,"lon":121.510224,"acc":15.0,"spd":2.4},{"t":1794377185000,"lat":25.047834,"lon":121.510137,"acc":15.0,"spd":1.9},{"t":1794377190000,"lat":25.047882,"lon":121.510354,"acc":15.0,"spd":5.2},{"t":1794377195000,"lat":25.047934,"lon":121.510845,"acc":15.0,"spd":5.6},{"t":1794377200000,"lat":25.047942,"lon":121.511009,"acc":15.0,"spd":5.4},{"t":1794377205000,"lat":25.048014,"lon":121.511387,"acc":15.0,"spd":7.4},{"t":1794377210000,"lat":25.048085,"lon":121.511728,"acc":15.0,"spd":9.5},{"t":1794377215000,"lat":25.047686,"lon":121.512395,"acc":15.0,"spd":9.7},{"t":1794377220000,"lat":25.047585,"lon":121.512664,"acc":15.0,"spd":9.5},{"t":1794377225000,"lat":25.047313,"lon":121.512992,"acc":15.0,"spd":9.5},{"t":1794377230000,"lat":25.047226,"lon":121.513445,"acc":15.0,"spd":9.7},{"t":1794377235000,"lat":25.047021,"lon":121.513749,"acc":15.0,"spd":8.4},{"t":1794377240000,"lat":25.046878,"lon":121.514377,"acc":15.0,"spd":9.0},{"t":1794377245000,"lat":25.046518,"lon":121.514861,"acc":15.0,"spd":8.0},{"t":1794377250000,"lat":25.046796,"lon":121.51479,"acc":15.0,"spd":4.8},{"t":1794377255000,"lat":25.04669,"lon":121.515158,"acc":15.0,"spd":5.1},{"t":1794377260000,"lat":25.046855,"lon":121.515404,"acc":15.0,"spd":4.2},{"t":1794377265000,"lat":25.046632,"lon":121.515648,"acc":15.0,"spd":4.2},{"t":1794377270000,"lat":25.046859,"lon":121.515676,"acc":15.0,"spd":4.0},{"t":1794377275000,"lat":25.046609,"lon":121.51581,"acc":15.0,"spd":4.5},{"t":1794377280000,"lat":25.046829,"lon":121.516099,"acc":15.0,"spd":3.8},{"t":1794377285000,"lat":25.046723,"lon":121.516443,"acc":15.0,"spd":4.6},{"t":1794377290000,"lat":25.046288,"lon":121.516711,"acc":15.0,"spd":7.5},{"t":1794377295000,"lat":25.046333,"lon":121.517337,"acc":15.0,"spd":7.5},{"t":1794377300000,"lat":25.046254,"lon":121.517367,"acc":15.0,"spd":7.4},{"t":1794377305000,"lat":25.045986,"lon":121.518002,"acc":15.0,"spd":7.2},{"t":1794377310000,"lat":25.045948,"lon":121.517953,"acc":15.0,"spd":3.8},{"t":1794377315000,"lat":25.046323,"lon":121.518266,"acc":15.0,"spd":4.8},{"t":1794377320000,"lat":25.045926,"lon":121.518275,"acc":15.0,"spd":3.8},{"t":1794377325000,"lat":25.045711,"lon":121.518586,"acc":15.0,"spd":3.0},{"t":1794377330000,"lat":25.045779,"lon":121.518695,"acc":15.0,"spd":0.6},{"t":1794377335000,"lat":25.04584,"lon":121.518451,"acc":15.0,"spd":1.0},{"t":1794377340000,"lat":25.045934,"lon":121.518602,"acc":15.0,"spd":0.5},{"t":1794377345000,"lat":25.045974,"lon":121.518529,"acc":15.0,"spd":0.5},{"t":1794377350000,"lat":25.045751,"lon":121.518726,"acc":15.0,"spd":0.4},{"t":1794377355000,"lat":25.045981,"lon":121.518677,"acc":15.0,"spd":0.0},{"t":1794377360000,"lat":25.045985,"lon":121.518788,"acc":15.0,"spd":0.2},{"t":1794377365000,"lat":25.045917,"lon":121.518712,"acc":15.0,"spd":0.7},{"t":1794377370000,"lat":25.045852,"lon":121.518786,"acc":15.0,"spd":0.0},{"t":1794377375000,"lat":25.045786,"lon":121.518582,"acc":15.0,"spd":0.0},{"t":1794377380000,"lat":25.045819,"lon":121.518713,"acc":15.0,"spd":0.0},{"t":1794377385000,"lat":25.045848,"lon":121.518831,"acc":15.0,"spd":0.5},{"t":1794377390000,"lat":25.04583,"lon":121.518651,"acc":15.0,"spd":0.1},{"t":1794377395000,"lat":25.04608,"lon":121.51868,"acc":15.0,"spd":0.0},{"t":1794377400000,"lat":25.045776,"lon":121.518562,"acc":15.0,"spd":0.0},{"t":1794377405000,"lat":25.046012,"lon":121.518596,"acc":15.0,"spd":0.0}],"waitingFixes":[{"t":1794376160000,"lat":25.023311,"lon":121.500042,"acc":25.0,"spd":0.0},{"t":1794376165000,"lat":25.023231,"lon":121.49984,"acc":25.0,"spd":0.4},{"t":1794376170000,"lat":25.023557,"lon":121.499695,"acc":25.0,"spd":0.1},{"t":1794376175000,"lat":25.023399,"lon":121.499567,"acc":25.0,"spd":0.1},{"t":1794376180000,"lat":25.023196,"lon":121.499804,"acc":25.0,"spd":0.2},{"t":1794376185000,"lat":25.023219,"lon":121.499852,"acc":25.0,"spd":0.5},{"t":1794376190000,"lat":25.023536,"lon":121.499725,"acc":25.0,"spd":0.2},{"t":1794376195000,"lat":25.023261,"lon":121.499925,"acc":25.0,"spd":0.1},{"t":1794376200000,"lat":25.023453,"lon":121.499876,"acc":25.0,"spd":0.2},{"t":1794376205000,"lat":25.023606,"lon":121.499748,"acc":25.0,"spd":0.2},{"t":1794376210000,"lat":25.023147,"lon":121.499819,"acc":25.0,"spd":0.0},{"t":1794376215000,"lat":25.02335,"lon":121.500639,"acc":25.0,"spd":0.3},{"t":1794376220000,"lat":25.023284,"lon":121.500507,"acc":25.0,"spd":0.0},{"t":1794376225000,"lat":25.023186,"lon":121.50018,"acc":25.0,"spd":0.0},{"t":1794376230000,"lat":25.023186,"lon":121.500005,"acc":25.0,"spd":0.4},{"t":1794376235000,"lat":25.023273,"lon":121.500008,"acc":25.0,"spd":0.0},{"t":1794376240000,"lat":25.023047,"lon":121.499888,"acc":25.0,"spd":0.3},{"t":1794376245000,"lat":25.023167,"lon":121.500006,"acc":25.0,"spd":0.2},{"t":1794376250000,"lat":25.023117,"lon":121.499941,"acc":25.0,"spd":0.0},{"t":1794376255000,"lat":25.023213,"lon":121.499597,"acc":25.0,"spd":0.4},{"t":1794376260000,"lat":25.023398,"lon":121.499727,"acc":25.0,"spd":0.2},{"t":1794376265000,"lat":25.023389,"lon":121.500205,"acc":25.0,"spd":0.1},{"t":1794376270000,"lat":25.023364,"lon":121.499969,"acc":25.0,"spd":0.2},{"t":1794376275000,"lat":25.023188,"lon":121.499899,"acc":25.0,"spd":0.3},{"t":1794376280000,"lat":25.023256,"lon":121.499849,"acc":25.0,"spd":0.0},{"t":1794376285000,"lat":25.023391,"lon":121.499855,"acc":25.0,"spd":0.3},{"t":1794376290000,"lat":25.023191,"lon":121.499876,"acc":25.0,"spd":0.3},{"t":1794376295000,"lat":25.023247,"lon":121.500041,"acc":25.0,"spd":0.2},{"t":1794376300000,"lat":25.023461,"lon":121.49992,"acc":25.0,"spd":0.1},{"t":1794376305000,"lat":25.023336,"lon":121.499843,"acc":25.0,"spd":0.2},{"t":1794376310000,"lat":25.02322,"lon":121.500011,"acc":25.0,"spd":0.2},{"t":1794376315000,"lat":25.023402,"lon":121.499867,"acc":25.0,"spd":0.9},{"t":1794376320000,"lat":25.023349,"lon":121.500005,"acc":25.0,"spd":0.1},{"t":1794376325000,"lat":25.023103,"lon":121.499609,"acc":25.0,"spd":0.7},{"t":1794376330000,"lat":25.023021,"lon":121.499862,"acc":25.0,"spd":0.3},{"t":1794376335000,"lat":25.022975,"lon":121.499778,"acc":25.0,"spd":0.5},{"t":1794376340000,"lat":25.023328,"lon":121.499778,"acc":25.0,"spd":0.4},{"t":1794376345000,"lat":25.023205,"lon":121.499927,"acc":25.0,"spd":0.2},{"t":1794376350000,"lat":25.023452,"lon":121.499798,"acc":25.0,"spd":0.3},{"t":1794376355000,"lat":25.023139,"lon":121.500159,"acc":25.0,"spd":0.3},{"t":1794376360000,"lat":25.023274,"lon":121.499814,"acc":25.0,"spd":0.1},{"t":1794376365000,"lat":25.02359,"lon":121.499865,"acc":25.0,"spd":0.3},{"t":1794376370000,"lat":25.023283,"lon":121.499935,"acc":25.0,"spd":0.3},{"t":1794376375000,"lat":25.023457,"lon":121.499893,"acc":25.0,"spd":0.1},{"t":1794376380000,"lat":25.023407,"lon":121.4998,"acc":25.0,"spd":0.1},{"t":1794376385000,"lat":25.023095,"lon":121.499884,"acc":25.0,"spd":0.0},{"t":1794376390000,"lat":25.023207,"lon":121.499771,"acc":25.0,"spd":0.3},{"t":1794376395000,"lat":25.023232,"lon":121.49971,"acc":25.0,"spd":0.1},{"t":1794376400000,"lat":25.023522,"lon":121.500121,"acc":25.0,"spd":0.5},{"t":1794376405000,"lat":25.023132,"lon":121.499814,"acc":25.0,"spd":0.1},{"t":1794376410000,"lat":25.023445,"lon":121.499886,"acc":25.0,"spd":0.4},{"t":1794376415000,"lat":25.023338,"lon":121.499751,"acc":25.0,"spd":0.1},{"t":1794376420000,"lat":25.023377,"lon":121.499589,"acc":25.0,"spd":0.0},{"t":1794376425000,"lat":25.023199,"lon":121.499915,"acc":25.0,"spd":0.3},{"t":1794376430000,"lat":25.023318,"lon":121.500031,"acc":25.0,"spd":0.0},{"t":1794376435000,"lat":25.023239,"lon":121.499873,"acc":25.0,"spd":0.3},{"t":1794376440000,"lat":25.023261,"lon":121.499946,"acc":25.0,"spd":0.2},{"t":1794376445000,"lat":25.023401,"lon":121.500159,"acc":25.0,"spd":0.1},{"t":1794376450000,"lat":25.023377,"lon":121.499905,"acc":25.0,"spd":0.3},{"t":1794376455000,"lat":25.023274,"lon":121.499809,"acc":25.0,"spd":0.4},{"t":1794376460000,"lat":25.023193,"lon":121.499928,"acc":25.0,"spd":0.2},{"t":1794376465000,"lat":25.023425,"lon":121.499902,"acc":25.0,"spd":0.1},{"t":1794376470000,"lat":25.023375,"lon":121.5003,"acc":25.0,"spd":0.7},{"t":1794376475000,"lat":25.023314,"lon":121.499835,"acc":25.0,"spd":0.1},{"t":1794376480000,"lat":25.023475,"lon":121.500059,"acc":25.0,"spd":0.3},{"t":1794376485000,"lat":25.023494,"lon":121.49961,"acc":25.0,"spd":0.1},{"t":1794376490000,"lat":25.02305,"lon":121.500057,"acc":25.0,"spd":0.5},{"t":1794376495000,"lat":25.023145,"lon":121.49997,"acc":25.0,"spd":0.0},{"t":1794376500000,"lat":25.023219,"lon":121.499617,"acc":25.0,"spd":0.4},{"t":1794376505000,"lat":25.023147,"lon":121.50004,"acc":25.0,"spd":0.0},{"t":1794376510000,"lat":25.023342,"lon":121.500045,"acc":25.0,"spd":0.4},{"t":1794376515000,"lat":25.023159,"lon":121.5,"acc":25.0,"spd":0.7},{"t":1794376520000,"lat":25.023072,"lon":121.499864,"acc":25.0,"spd":0.3},{"t":1794376525000,"lat":25.02339,"lon":121.500027,"acc":25.0,"spd":0.2},{"t":1794376530000,"lat":25.023303,"lon":121.49984,"acc":25.0,"spd":0.0},{"t":1794376535000,"lat":25.023469,"lon":121.499722,"acc":25.0,"spd":0.3},{"t":1794376540000,"lat":25.02329,"lon":121.499936,"acc":25.0,"spd":0.2},{"t":1794376545000,"lat":25.023468,"lon":121.499969,"acc":25.0,"spd":0.5},{"t":1794376550000,"lat":25.023367,"lon":121.499887,"acc":25.0,"spd":0.0},{"t":1794376555000,"lat":25.023442,"lon":121.500642,"acc":25.0,"spd":0.1},{"t":1794376560000,"lat":25.023279,"lon":121.500215,"acc":25.0,"spd":0.1},{"t":1794376565000,"lat":25.023378,"lon":121.499783,"acc":25.0,"spd":0.1},{"t":1794376570000,"lat":25.023389,"lon":121.499954,"acc":25.0,"spd":0.1},{"t":1794376575000,"lat":25.023308,"lon":121.5001,"acc":25.0,"spd":0.1},{"t":1794376580000,"lat":25.023432,"lon":121.499771,"acc":25.0,"spd":0.1},{"t":1794376585000,"lat":25.023135,"lon":121.49995,"acc":25.0,"spd":0.2},{"t":1794376590000,"lat":25.023133,"lon":121.499705,"acc":25.0,"spd":0.6},{"t":1794376595000,"lat":25.023283,"lon":121.500084,"acc":25.0,"spd":0.2},{"t":1794376600000,"lat":25.023066,"lon":121.500003,"acc":25.0,"spd":0.3},{"t":1794376605000,"lat":25.0235,"lon":121.499851,"acc":25.0,"spd":0.1},{"t":1794376610000,"lat":25.023366,"lon":121.499842,"acc":25.0,"spd":0.1},{"t":1794376615000,"lat":25.02336,"lon":121.4998,"acc":25.0,"spd":0.2},{"t":1794376620000,"lat":25.023392,"lon":121.499981,"acc":25.0,"spd":0.4},{"t":1794376625000,"lat":25.02346,"lon":121.499873,"acc":25.0,"spd":0.2},{"t":1794376630000,"lat":25.023319,"lon":121.499884,"acc":25.0,"spd":0.1},{"t":1794376635000,"lat":25.023262,"lon":121.499752,"acc":25.0,"spd":0.2},{"t":1794376640000,"lat":25.023369,"lon":121.499993,"acc":25.0,"spd":0.1},{"t":1794376645000,"lat":25.023178,"lon":121.499857,"acc":25.0,"spd":0.3},{"t":1794376650000,"lat":25.023336,"lon":121.499856,"acc":25.0,"spd":0.2},{"t":1794376655000,"lat":25.023381,"lon":121.499884,"acc":25.0,"spd":0.8},{"t":1794376660000,"lat":25.023225,"lon":121.499701,"acc":25.0,"spd":0.3},{"t":1794376665000,"lat":25.023285,"lon":121.49975,"acc":25.0,"spd":0.1},{"t":1794376670000,"lat":25.023282,"lon":121.499789,"acc":25.0,"spd":0.2},{"t":1794376675000,"lat":25.023314,"lon":121.499713,"acc":25.0,"spd":0.6},{"t":1794376680000,"lat":25.023539,"lon":121.500024,"acc":25.0,"spd":0.2},{"t":1794376685000,"lat":25.023132,"lon":121.500052,"acc":25.0,"spd":0.2},{"t":1794376690000,"lat":25.023315,"lon":121.499755,"acc":25.0,"spd":0.3},{"t":1794376695000,"lat":25.023211,"lon":121.500056,"acc":25.0,"spd":0.0},{"t":1794376700000,"lat":25.023009,"lon":121.500105,"acc":25.0,"spd":0.1},{"t":1794376705000,"lat":25.023303,"lon":121.500175,"acc":25.0,"spd":0.1},{"t":1794376710000,"lat":25.023497,"lon":121.49976,"acc":25.0,"spd":0.1},{"t":1794376715000,"lat":25.023426,"lon":121.499616,"acc":25.0,"spd":0.1},{"t":1794376720000,"lat":25.023604,"lon":121.499934,"acc":25.0,"spd":0.1},{"t":1794376725000,"lat":25.023666,"lon":121.499803,"acc":25.0,"spd":0.2},{"t":1794376730000,"lat":25.023326,"lon":121.499853,"acc":25.0,"spd":0.7},{"t":1794376735000,"lat":25.023222,"lon":121.499966,"acc":25.0,"spd":0.3},{"t":1794376740000,"lat":25.023093,"lon":121.499932,"acc":25.0,"spd":0.4},{"t":1794376745000,"lat":25.023174,"lon":121.499847,"acc":25.0,"spd":0.2},{"t":1794376750000,"lat":25.023392,"lon":121.500111,"acc":25.0,"spd":0.4},{"t":1794376755000,"lat":25.023169,"lon":121.500004,"acc":25.0,"spd":0.2},{"t":1794376760000,"lat":25.023206,"lon":121.499882,"acc":25.0,"spd":0.0},{"t":1794376765000,"lat":25.023314,"lon":121.499711,"acc":25.0,"spd":0.2},{"t":1794376770000,"lat":25.023198,"lon":121.500034,"acc":25.0,"spd":0.6},{"t":1794376775000,"lat":25.023347,"lon":121.500552,"acc":25.0,"spd":0.2},{"t":1794376780000,"lat":25.023411,"lon":121.500214,"acc":25.0,"spd":0.2},{"t":1794376785000,"lat":25.023257,"lon":121.499975,"acc":25.0,"spd":0.3},{"t":1794376790000,"lat":25.023386,"lon":121.500085,"acc":25.0,"spd":0.2},{"t":1794376795000,"lat":25.023393,"lon":121.499934,"acc":25.0,"spd":0.2},{"t":1794376800000,"lat":25.023325,"lon":121.4999,"acc":25.0,"spd":0.1},{"t":1794376805000,"lat":25.023219,"lon":121.499726,"acc":25.0,"spd":0.3},{"t":1794376810000,"lat":25.023581,"lon":121.499278,"acc":25.0,"spd":0.4},{"t":1794376815000,"lat":25.023345,"lon":121.499813,"acc":25.0,"spd":0.5},{"t":1794376820000,"lat":25.023387,"lon":121.499801,"acc":25.0,"spd":0.1},{"t":1794376825000,"lat":25.023521,"lon":121.499794,"acc":25.0,"spd":0.2},{"t":1794376830000,"lat":25.023344,"lon":121.500203,"acc":25.0,"spd":0.3},{"t":1794376835000,"lat":25.023377,"lon":121.499813,"acc":25.0,"spd":0.1},{"t":1794376840000,"lat":25.023081,"lon":121.500434,"acc":25.0,"spd":0.0},{"t":1794376845000,"lat":25.023416,"lon":121.500219,"acc":25.0,"spd":0.2},{"t":1794376850000,"lat":25.023175,"lon":121.500189,"acc":25.0,"spd":0.0},{"t":1794376855000,"lat":25.023296,"lon":121.499444,"acc":25.0,"spd":0.1},{"t":1794376860000,"lat":25.023253,"lon":121.49986,"acc":25.0,"spd":0.1},{"t":1794376865000,"lat":25.023589,"lon":121.499982,"acc":25.0,"spd":0.2},{"t":1794376870000,"lat":25.023321,"lon":121.499954,"acc":25.0,"spd":0.3},{"t":1794376875000,"lat":25.02335,"lon":121.499886,"acc":25.0,"spd":0.0},{"t":1794376880000,"lat":25.023181,"lon":121.499433,"acc":25.0,"spd":0.2},{"t":1794376885000,"lat":25.023466,"lon":121.49969,"acc":25.0,"spd":0.4},{"t":1794376890000,"lat":25.023428,"lon":121.500124,"acc":25.0,"spd":0.1},{"t":1794376895000,"lat":25.023298,"lon":121.499842,"acc":25.0,"spd":0.2},{"t":1794376900000,"lat":25.023224,"lon":121.500051,"acc":25.0,"spd":0.0},{"t":1794376905000,"lat":25.023465,"lon":121.500019,"acc":25.0,"spd":0.2},{"t":1794376910000,"lat":25.023306,"lon":121.49982,"acc":25.0,"spd":0.1},{"t":1794376915000,"lat":25.023385,"lon":121.500053,"acc":25.0,"spd":0.1},{"t":1794376920000,"lat":25.023259,"lon":121.499793,"acc":25.0,"spd":0.4},{"t":1794376925000,"lat":25.023144,"lon":121.499926,"acc":25.0,"spd":0.0},{"t":1794376930000,"lat":25.023411,"lon":121.499925,"acc":25.0,"spd":0.2},{"t":1794376935000,"lat":25.023349,"lon":121.49996,"acc":25.0,"spd":0.2},{"t":1794376940000,"lat":25.023436,"lon":121.500024,"acc":25.0,"spd":0.0},{"t":1794376945000,"lat":25.02355,"lon":121.499843,"acc":25.0,"spd":0.1},{"t":1794376950000,"lat":25.023344,"lon":121.500062,"acc":25.0,"spd":0.0},{"t":1794376955000,"lat":25.023409,"lon":121.499839,"acc":25.0,"spd":0.2},{"t":1794376960000,"lat":25.0232,"lon":121.500077,"acc":25.0,"spd":0.3},{"t":1794376965000,"lat":25.023212,"lon":121.499844,"acc":25.0,"spd":0.1},{"t":1794376970000,"lat":25.023223,"lon":121.500112,"acc":25.0,"spd":0.3},{"t":1794376975000,"lat":25.023312,"lon":121.500142,"acc":25.0,"spd":0.4},{"t":1794376980000,"lat":25.023253,"lon":121.499669,"acc":25.0,"spd":0.1},{"t":1794376985000,"lat":25.023411,"lon":121.499859,"acc":25.0,"spd":0.0},{"t":1794376990000,"lat":25.023423,"lon":121.5,"acc":25.0,"spd":0.6},{"t":1794376995000,"lat":25.023199,"lon":121.499946,"acc":25.0,"spd":0.3},{"t":1794377000000,"lat":25.023382,"lon":121.499893,"acc":25.0,"spd":0.5},{"t":1794377005000,"lat":25.023273,"lon":121.500243,"acc":25.0,"spd":0.2},{"t":1794377010000,"lat":25.023232,"lon":121.499887,"acc":25.0,"spd":0.4},{"t":1794377015000,"lat":25.023275,"lon":121.500062,"acc":25.0,"spd":0.0},{"t":1794377020000,"lat":25.023352,"lon":121.500046,"acc":25.0,"spd":0.3},{"t":1794377025000,"lat":25.023451,"lon":121.499813,"acc":25.0,"spd":0.3},{"t":1794377030000,"lat":25.023267,"lon":121.499761,"acc":25.0,"spd":0.0},{"t":1794377035000,"lat":25.022994,"lon":121.499238,"acc":25.0,"spd":0.0},{"t":1794377040000,"lat":25.023306,"lon":121.499519,"acc":25.0,"spd":0.0},{"t":1794377045000,"lat":25.023358,"lon":121.499821,"acc":25.0,"spd":0.0},{"t":1794377050000,"lat":25.023335,"lon":121.499788,"acc":25.0,"spd":0.3},{"t":1794377055000,"lat":25.023325,"lon":121.499806,"acc":25.0,"spd":0.3},{"t":1794377060000,"lat":25.023297,"lon":121.499672,"acc":25.0,"spd":0.2},{"t":1794377065000,"lat":25.023484,"lon":121.499965,"acc":25.0,"spd":0.4},{"t":1794377070000,"lat":25.023455,"lon":121.500001,"acc":25.0,"spd":0.3},{"t":1794377075000,"lat":25.023139,"lon":121.49998,"acc":25.0,"spd":0.2},{"t":1794377080000,"lat":25.023241,"lon":121.500298,"acc":25.0,"spd":0.2},{"t":1794377085000,"lat":25.023275,"lon":121.500115,"acc":25.0,"spd":0.3},{"t":1794377090000,"lat":25.023384,"lon":121.500094,"acc":25.0,"spd":0.3},{"t":1794377095000,"lat":25.023263,"lon":121.499778,"acc":25.0,"spd":0.1},{"t":1794377100000,"lat":25.023151,"lon":121.499983,"acc":25.0,"spd":0.3},{"t":1794377105000,"lat":25.023048,"lon":121.499125,"acc":25.0,"spd":0.0},{"t":1794377110000,"lat":25.023071,"lon":121.499645,"acc":25.0,"spd":0.5},{"t":1794377115000,"lat":25.023125,"lon":121.499834,"acc":25.0,"spd":0.1},{"t":1794377120000,"lat":25.023437,"lon":121.499959,"acc":25.0,"spd":0.4},{"t":1794377125000,"lat":25.023435,"lon":121.499844,"acc":25.0,"spd":0.6},{"t":1794377130000,"lat":25.023459,"lon":121.499623,"acc":25.0,"spd":0.4},{"t":1794377135000,"lat":25.023231,"lon":121.499879,"acc":25.0,"spd":0.1},{"t":1794377140000,"lat":25.023223,"lon":121.49984,"acc":25.0,"spd":0.5},{"t":1794377145000,"lat":25.023322,"lon":121.499889,"acc":25.0,"spd":0.1},{"t":1794377150000,"lat":25.023386,"lon":121.49975,"acc":25.0,"spd":0.3},{"t":1794377155000,"lat":25.023498,"lon":121.499884,"acc":25.0,"spd":0.1},{"t":1794377160000,"lat":25.023254,"lon":121.499831,"acc":25.0,"spd":0.2},{"t":1794377165000,"lat":25.023457,"lon":121.500038,"acc":25.0,"spd":0.5},{"t":1794377170000,"lat":25.023129,"lon":121.499703,"acc":25.0,"spd":0.1},{"t":1794377175000,"lat":25.023253,"lon":121.499938,"acc":25.0,"spd":0.4},{"t":1794377180000,"lat":25.023325,"lon":121.499694,"acc":25.0,"spd":0.1},{"t":1794377185000,"lat":25.023239,"lon":121.499908,"acc":25.0,"spd":0.2},{"t":1794377190000,"lat":25.023298,"lon":121.500136,"acc":25.0,"spd":0.0},{"t":1794377195000,"lat":25.023177,"lon":121.499833,"acc":25.0,"spd":0.4},{"t":1794377200000,"lat":25.023065,"lon":121.499728,"acc":25.0,"spd":0.0},{"t":1794377205000,"lat":25.023128,"lon":121.499975,"acc":25.0,"spd":0.4},{"t":1794377210000,"lat":25.023422,"lon":121.499846,"acc":25.0,"spd":0.4},{"t":1794377215000,"lat":25.023246,"lon":121.499749,"acc":25.0,"spd":0.3},{"t":1794377220000,"lat":25.023348,"lon":121.499783,"acc":25.0,"spd":0.3},{"t":1794377225000,"lat":25.02324,"lon":121.499949,"acc":25.0,"spd":0.4},{"t":1794377230000,"lat":25.023288,"lon":121.499608,"acc":25.0,"spd":0.7},{"t":1794377235000,"lat":25.023394,"lon":121.49997,"acc":25.0,"spd":0.5},{"t":1794377240000,"lat":25.023433,"lon":121.499912,"acc":25.0,"spd":0.1},{"t":1794377245000,"lat":25.023126,"lon":121.500041,"acc":25.0,"spd":0.2},{"t":1794377250000,"lat":25.023583,"lon":121.500205,"acc":25.0,"spd":0.5},{"t":1794377255000,"lat":25.02323,"lon":121.500002,"acc":25.0,"spd":0.2},{"t":1794377260000,"lat":25.023246,"lon":121.500033,"acc":25.0,"spd":0.2},{"t":1794377265000,"lat":25.023337,"lon":121.499957,"acc":25.0,"spd":0.3},{"t":1794377270000,"lat":25.023432,"lon":121.499907,"acc":25.0,"spd":0.2},{"t":1794377275000,"lat":25.023326,"lon":121.499997,"acc":25.0,"spd":0.3},{"t":1794377280000,"lat":25.023357,"lon":121.499979,"acc":25.0,"spd":0.1},{"t":1794377285000,"lat":25.023433,"lon":121.499812,"acc":25.0,"spd":0.0},{"t":1794377290000,"lat":25.023279,"lon":121.49995,"acc":25.0,"spd":0.1},{"t":1794377295000,"lat":25.023205,"lon":121.500524,"acc":25.0,"spd":0.2},{"t":1794377300000,"lat":25.023188,"lon":121.500237,"acc":25.0,"spd":0.2},{"t":1794377305000,"lat":25.02329,"lon":121.500248,"acc":25.0,"spd":0.2},{"t":1794377310000,"lat":25.023356,"lon":121.500149,"acc":25.0,"spd":0.3},{"t":1794377315000,"lat":25.023182,"lon":121.499947,"acc":25.0,"spd":0.4},{"t":1794377320000,"lat":25.023427,"lon":121.499689,"acc":25.0,"spd":0.3},{"t":1794377325000,"lat":25.023371,"lon":121.499939,"acc":25.0,"spd":0.6},{"t":1794377330000,"lat":25.023317,"lon":121.500015,"acc":25.0,"spd":0.3},{"t":1794377335000,"lat":25.023177,"lon":121.500104,"acc":25.0,"spd":0.2},{"t":1794377340000,"lat":25.023165,"lon":121.499752,"acc":25.0,"spd":0.0},{"t":1794377345000,"lat":25.023436,"lon":121.49986,"acc":25.0,"spd":0.7},{"t":1794377350000,"lat":25.023442,"lon":121.500082,"acc":25.0,"spd":0.2},{"t":1794377355000,"lat":25.02305,"lon":121.499771,"acc":25.0,"spd":0.5},{"t":1794377360000,"lat":25.02346,"lon":121.499731,"acc":25.0,"spd":0.4},{"t":1794377365000,"lat":25.023336,"lon":121.499992,"acc":25.0,"spd":0.0},{"t":1794377370000,"lat":25.023137,"lon":121.499848,"acc":25.0,"spd":0.0},{"t":1794377375000,"lat":25.023208,"lon":121.5001,"acc":25.0,"spd":0.6},{"t":1794377380000,"lat":25.023236,"lon":121.499846,"acc":25.0,"spd":0.0},{"t":1794377385000,"lat":25.02343,"lon":121.499758,"acc":25.0,"spd":0.2},{"t":1794377390000,"lat":25.023232,"lon":121.499838,"acc":25.0,"spd":0.2},{"t":1794377395000,"lat":25.023117,"lon":121.499873,"acc":25.0,"spd":0.3},{"t":1794377400000,"lat":25.023145,"lon":121.499901,"acc":25.0,"spd":0.4},{"t":1794377405000,"lat":25.023442,"lon":121.499928,"acc":25.0,"spd":0.0}],"walkerFixes":[{"t":1794376160000,"lat":25.023229,"lon":121.499946,"acc":15.0,"spd":1.5},{"t":1794376165000,"lat":25.023184,"lon":121.499738,"acc":15.0,"spd":1.4},{"t":1794376170000,"lat":25.02345,"lon":121.499751,"acc":15.0,"spd":1.6},{"t":1794376175000,"lat":25.023531,"lon":121.499921,"acc":15.0,"spd":1.5},{"t":1794376180000,"lat":25.023468,"lon":121.499843,"acc":15.0,"spd":1.6},{"t":1794376185000,"lat":25.023718,"lon":121.499763,"acc":15.0,"spd":1.3},{"t":1794376190000,"lat":25.023659,"lon":121.50007,"acc":15.0,"spd":1.5},{"t":1794376195000,"lat":25.023703,"lon":121.500016,"acc":15.0,"spd":1.2},{"t":1794376200000,"lat":25.023796,"lon":121.500137,"acc":15.0,"spd":1.2},{"t":1794376205000,"lat":25.023715,"lon":121.50001,"acc":15.0,"spd":1.5},{"t":1794376210000,"lat":25.023824,"lon":121.500084,"acc":15.0,"spd":1.2},{"t":1794376215000,"lat":25.023913,"lon":121.500246,"acc":15.0,"spd":1.6},{"t":1794376220000,"lat":25.024135,"lon":121.500027,"acc":15.0,"spd":1.3},{"t":1794376225000,"lat":25.024195,"lon":121.500107,"acc":15.0,"spd":0.9},{"t":1794376230000,"lat":25.024104,"lon":121.50015,"acc":15.0,"spd":1.0},{"t":1794376235000,"lat":25.024125,"lon":121.500118,"acc":15.0,"spd":1.3},{"t":1794376240000,"lat":25.024436,"lon":121.500102,"acc":15.0,"spd":1.6},{"t":1794376245000,"lat":25.024297,"lon":121.500169,"acc":15.0,"spd":1.4},{"t":1794376250000,"lat":25.024369,"lon":121.500451,"acc":15.0,"spd":1.6},{"t":1794376255000,"lat":25.024498,"lon":121.500103,"acc":15.0,"spd":1.4},{"t":1794376260000,"lat":25.024543,"lon":121.500433,"acc":15.0,"spd":1.6},{"t":1794376265000,"lat":25.024698,"lon":121.500364,"acc":15.0,"spd":1.4},{"t":1794376270000,"lat":25.02485,"lon":121.500306,"acc":15.0,"spd":1.7},{"t":1794376275000,"lat":25.024776,"lon":121.500266,"acc":15.0,"spd":1.5},{"t":1794376280000,"lat":25.024618,"lon":121.500351,"acc":15.0,"spd":1.3},{"t":1794376285000,"lat":25.024812,"lon":121.500395,"acc":15.0,"spd":1.4},{"t":1794376290000,"lat":25.024841,"lon":121.500401,"acc":15.0,"spd":1.4},{"t":1794376295000,"lat":25.024908,"lon":121.500454,"acc":15.0,"spd":1.7},{"t":1794376300000,"lat":25.024946,"lon":121.500507,"acc":15.0,"spd":1.2},{"t":1794376305000,"lat":25.025112,"lon":121.500618,"acc":15.0,"spd":1.1},{"t":1794376310000,"lat":25.02502,"lon":121.500533,"acc":15.0,"spd":1.4},{"t":1794376315000,"lat":25.02505,"lon":121.500699,"acc":15.0,"spd":1.3},{"t":1794376320000,"lat":25.025417,"lon":121.500867,"acc":15.0,"spd":1.3},{"t":1794376325000,"lat":25.025242,"lon":121.500385,"acc":15.0,"spd":1.5},{"t":1794376330000,"lat":25.025303,"lon":121.500467,"acc":15.0,"spd":1.1},{"t":1794376335000,"lat":25.025543,"lon":121.500375,"acc":15.0,"spd":1.7},{"t":1794376340000,"lat":25.025372,"lon":121.500597,"acc":15.0,"spd":0.9},{"t":1794376345000,"lat":25.025334,"lon":121.500463,"acc":15.0,"spd":1.4},{"t":1794376350000,"lat":25.025672,"lon":121.500871,"acc":15.0,"spd":1.8},{"t":1794376355000,"lat":25.025745,"lon":121.500339,"acc":15.0,"spd":1.5},{"t":1794376360000,"lat":25.025712,"lon":121.500573,"acc":15.0,"spd":1.5},{"t":1794376365000,"lat":25.025792,"lon":121.500538,"acc":15.0,"spd":1.2},{"t":1794376370000,"lat":25.025932,"lon":121.500584,"acc":15.0,"spd":1.5},{"t":1794376375000,"lat":25.025847,"lon":121.500342,"acc":15.0,"spd":1.6},{"t":1794376380000,"lat":25.025937,"lon":121.500381,"acc":15.0,"spd":1.3},{"t":1794376385000,"lat":25.025886,"lon":121.500449,"acc":15.0,"spd":1.7},{"t":1794376390000,"lat":25.026183,"lon":121.500494,"acc":15.0,"spd":1.1},{"t":1794376395000,"lat":25.026175,"lon":121.500554,"acc":15.0,"spd":1.5},{"t":1794376400000,"lat":25.026305,"lon":121.500317,"acc":15.0,"spd":1.6},{"t":1794376405000,"lat":25.02626,"lon":121.500493,"acc":15.0,"spd":1.2},{"t":1794376410000,"lat":25.02635,"lon":121.500456,"acc":15.0,"spd":1.6},{"t":1794376415000,"lat":25.026509,"lon":121.500433,"acc":15.0,"spd":1.3},{"t":1794376420000,"lat":25.026455,"lon":121.500492,"acc":15.0,"spd":1.4},{"t":1794376425000,"lat":25.026607,"lon":121.500281,"acc":15.0,"spd":1.4},{"t":1794376430000,"lat":25.026532,"lon":121.50043,"acc":15.0,"spd":1.4},{"t":1794376435000,"lat":25.026726,"lon":121.500361,"acc":15.0,"spd":1.4},{"t":1794376440000,"lat":25.02692,"lon":121.500453,"acc":15.0,"spd":1.1},{"t":1794376445000,"lat":25.026927,"lon":121.500673,"acc":15.0,"spd":1.4},{"t":1794376450000,"lat":25.026956,"lon":121.500464,"acc":15.0,"spd":1.8},{"t":1794376455000,"lat":25.02678,"lon":121.500565,"acc":15.0,"spd":1.4},{"t":1794376460000,"lat":25.027033,"lon":121.50073,"acc":15.0,"spd":1.4},{"t":1794376465000,"lat":25.027239,"lon":121.500647,"acc":15.0,"spd":1.7},{"t":1794376470000,"lat":25.027133,"lon":121.500397,"acc":15.0,"spd":1.6},{"t":1794376475000,"lat":25.027177,"lon":121.500513,"acc":15.0,"spd":1.4},{"t":1794376480000,"lat":25.027354,"lon":121.50048,"acc":15.0,"spd":1.2},{"t":1794376485000,"lat":25.027175,"lon":121.500667,"acc":15.0,"spd":1.4},{"t":1794376490000,"lat":25.027428,"lon":121.500509,"acc":15.0,"spd":1.5},{"t":1794376495000,"lat":25.027351,"lon":121.500295,"acc":15.0,"spd":0.9},{"t":1794376500000,"lat":25.027634,"lon":121.500492,"acc":15.0,"spd":1.4},{"t":1794376505000,"lat":25.027629,"lon":121.500563,"acc":15.0,"spd":1.7},{"t":1794376510000,"lat":25.027591,"lon":121.50084,"acc":15.0,"spd":1.8},{"t":1794376515000,"lat":25.027595,"lon":121.500559,"acc":15.0,"spd":1.8},{"t":1794376520000,"lat":25.027907,"lon":121.500798,"acc":15.0,"spd":1.4},{"t":1794376525000,"lat":25.027685,"lon":121.50041,"acc":15.0,"spd":1.6},{"t":1794376530000,"lat":25.028027,"lon":121.50052,"acc":15.0,"spd":1.3},{"t":1794376535000,"lat":25.027941,"lon":121.500633,"acc":15.0,"spd":1.8},{"t":1794376540000,"lat":25.028221,"lon":121.500624,"acc":15.0,"spd":1.4},{"t":1794376545000,"lat":25.028189,"lon":121.500714,"acc":15.0,"spd":1.4},{"t":1794376550000,"lat":25.028129,"lon":121.500538,"acc":15.0,"spd":1.3},{"t":1794376555000,"lat":25.028248,"lon":121.500699,"acc":15.0,"spd":1.5},{"t":1794376560000,"lat":25.028368,"lon":121.500534,"acc":15.0,"spd":1.2},{"t":1794376565000,"lat":25.028328,"lon":121.500493,"acc":15.0,"spd":1.8},{"t":1794376570000,"lat":25.028261,"lon":121.500605,"acc":15.0,"spd":1.7},{"t":1794376575000,"lat":25.028398,"lon":121.500623,"acc":15.0,"spd":1.4},{"t":1794376580000,"lat":25.028391,"lon":121.500468,"acc":15.0,"spd":1.7},{"t":1794376585000,"lat":25.028558,"lon":121.500462,"acc":15.0,"spd":1.5},{"t":1794376590000,"lat":25.028498,"lon":121.500659,"acc":15.0,"spd":1.2},{"t":1794376595000,"lat":25.028509,"lon":121.500658,"acc":15.0,"spd":1.5},{"t":1794376600000,"lat":25.028537,"lon":121.500728,"acc":15.0,"spd":1.4},{"t":1794376605000,"lat":25.028771,"lon":121.500571,"acc":15.0,"spd":1.2},{"t":1794376610000,"lat":25.029022,"lon":121.500436,"acc":15.0,"spd":1.3},{"t":1794376615000,"lat":25.028992,"lon":121.500669,"acc":15.0,"spd":1.4},{"t":1794376620000,"lat":25.029001,"lon":121.500609,"acc":15.0,"spd":1.3},{"t":1794376625000,"lat":25.029132,"lon":121.50063,"acc":15.0,"spd":1.4},{"t":1794376630000,"lat":25.02907,"lon":121.50061,"acc":15.0,"spd":1.5},{"t":1794376635000,"lat":25.029288,"lon":121.500681,"acc":15.0,"spd":1.4},{"t":1794376640000,"lat":25.029126,"lon":121.500983,"acc":15.0,"spd":1.5},{"t":1794376645000,"lat":25.029324,"lon":121.500624,"acc":15.0,"spd":1.1},{"t":1794376650000,"lat":25.029248,"lon":121.500381,"acc":15.0,"spd":1.5},{"t":1794376655000,"lat":25.029266,"lon":121.500719,"acc":15.0,"spd":1.6},{"t":1794376660000,"lat":25.029467,"lon":121.500618,"acc":15.0,"spd":1.2},{"t":1794376665000,"lat":25.029332,"lon":121.500716,"acc":15.0,"spd":1.1},{"t":1794376670000,"lat":25.02964,"lon":121.500422,"acc":15.0,"spd":1.2},{"t":1794376675000,"lat":25.029588,"lon":121.500369,"acc":15.0,"spd":1.4},{"t":1794376680000,"lat":25.02997,"lon":121.500367,"acc":15.0,"spd":1.3},{"t":1794376685000,"lat":25.02972,"lon":121.500695,"acc":15.0,"spd":1.4},{"t":1794376690000,"lat":25.029853,"lon":121.50057,"acc":15.0,"spd":1.6},{"t":1794376695000,"lat":25.030081,"lon":121.500398,"acc":15.0,"spd":1.0},{"t":1794376700000,"lat":25.030087,"lon":121.500604,"acc":15.0,"spd":1.5},{"t":1794376705000,"lat":25.029982,"lon":121.500709,"acc":15.0,"spd":1.6},{"t":1794376710000,"lat":25.030203,"lon":121.500735,"acc":15.0,"spd":1.4},{"t":1794376715000,"lat":25.030082,"lon":121.50053,"acc":15.0,"spd":1.5},{"t":1794376720000,"lat":25.030249,"lon":121.500472,"acc":15.0,"spd":1.5},{"t":1794376725000,"lat":25.03036,"lon":121.500503,"acc":15.0,"spd":1.6},{"t":1794376730000,"lat":25.030257,"lon":121.50075,"acc":15.0,"spd":1.4},{"t":1794376735000,"lat":25.030634,"lon":121.500377,"acc":15.0,"spd":1.7},{"t":1794376740000,"lat":25.03049,"lon":121.50065,"acc":15.0,"spd":1.2},{"t":1794376745000,"lat":25.030454,"lon":121.500685,"acc":15.0,"spd":1.5},{"t":1794376750000,"lat":25.030731,"lon":121.500516,"acc":15.0,"spd":1.2},{"t":1794376755000,"lat":25.030843,"lon":121.500589,"acc":15.0,"spd":1.2},{"t":1794376760000,"lat":25.030677,"lon":121.500612,"acc":15.0,"spd":1.4},{"t":1794376765000,"lat":25.030668,"lon":121.500571,"acc":15.0,"spd":1.4},{"t":1794376770000,"lat":25.030733,"lon":121.500848,"acc":15.0,"spd":1.8},{"t":1794376775000,"lat":25.030812,"lon":121.501014,"acc":15.0,"spd":1.2},{"t":1794376780000,"lat":25.030976,"lon":121.500747,"acc":15.0,"spd":1.2},{"t":1794376785000,"lat":25.030823,"lon":121.501041,"acc":15.0,"spd":1.5},{"t":1794376790000,"lat":25.030952,"lon":121.501079,"acc":15.0,"spd":1.3},{"t":1794376795000,"lat":25.031052,"lon":121.501168,"acc":15.0,"spd":1.7},{"t":1794376800000,"lat":25.030926,"lon":121.50128,"acc":15.0,"spd":1.5},{"t":1794376805000,"lat":25.030879,"lon":121.501198,"acc":15.0,"spd":1.6},{"t":1794376810000,"lat":25.030939,"lon":121.501259,"acc":15.0,"spd":1.1},{"t":1794376815000,"lat":25.031007,"lon":121.50137,"acc":15.0,"spd":1.6},{"t":1794376820000,"lat":25.03094,"lon":121.501469,"acc":15.0,"spd":1.4},{"t":1794376825000,"lat":25.030929,"lon":121.501489,"acc":15.0,"spd":1.3},{"t":1794376830000,"lat":25.030851,"lon":121.5016,"acc":15.0,"spd":1.3},{"t":1794376835000,"lat":25.030916,"lon":121.501652,"acc":15.0,"spd":1.6},{"t":1794376840000,"lat":25.030975,"lon":121.501809,"acc":15.0,"spd":1.5},{"t":1794376845000,"lat":25.031234,"lon":121.501743,"acc":15.0,"spd":1.2},{"t":1794376850000,"lat":25.031026,"lon":121.50187,"acc":15.0,"spd":1.5},{"t":1794376855000,"lat":25.031071,"lon":121.501981,"acc":15.0,"spd":1.2},{"t":1794376860000,"lat":25.030959,"lon":121.501929,"acc":15.0,"spd":1.5},{"t":1794376865000,"lat":25.030916,"lon":121.502256,"acc":15.0,"spd":1.4},{"t":1794376870000,"lat":25.031267,"lon":121.501947,"acc":15.0,"spd":1.3},{"t":1794376875000,"lat":25.030906,"lon":121.502306,"acc":15.0,"spd":1.6},{"t":1794376880000,"lat":25.031221,"lon":121.502166,"acc":15.0,"spd":1.6},{"t":1794376885000,"lat":25.030904,"lon":121.502472,"acc":15.0,"spd":1.7},{"t":1794376890000,"lat":25.030978,"lon":121.502467,"acc":15.0,"spd":1.2},{"t":1794376895000,"lat":25.031251,"lon":121.502418,"acc":15.0,"spd":1.4},{"t":1794376900000,"lat":25.03117,"lon":121.502578,"acc":15.0,"spd":1.6},{"t":1794376905000,"lat":25.031055,"lon":121.502476,"acc":15.0,"spd":1.2},{"t":1794376910000,"lat":25.031252,"lon":121.502658,"acc":15.0,"spd":1.1},{"t":1794376915000,"lat":25.031265,"lon":121.502467,"acc":15.0,"spd":1.7},{"t":1794376920000,"lat":25.031158,"lon":121.502774,"acc":15.0,"spd":1.4},{"t":1794376925000,"lat":25.031178,"lon":121.50295,"acc":15.0,"spd":1.6},{"t":1794376930000,"lat":25.031157,"lon":121.502887,"acc":15.0,"spd":1.5},{"t":1794376935000,"lat":25.031369,"lon":121.503063,"acc":15.0,"spd":1.5},{"t":1794376940000,"lat":25.031026,"lon":121.502861,"acc":15.0,"spd":1.4},{"t":1794376945000,"lat":25.03127,"lon":121.503156,"acc":15.0,"spd":1.5},{"t":1794376950000,"lat":25.031546,"lon":121.503058,"acc":15.0,"spd":1.1},{"t":1794376955000,"lat":25.031498,"lon":121.503147,"acc":15.0,"spd":1.4},{"t":1794376960000,"lat":25.03155,"lon":121.503333,"acc":15.0,"spd":1.1},{"t":1794376965000,"lat":25.031483,"lon":121.503288,"acc":15.0,"spd":1.7},{"t":1794376970000,"lat":25.031604,"lon":121.503415,"acc":15.0,"spd":1.2},{"t":1794376975000,"lat":25.031576,"lon":121.503445,"acc":15.0,"spd":1.6},{"t":1794376980000,"lat":25.031552,"lon":121.503437,"acc":15.0,"spd":1.2},{"t":1794376985000,"lat":25.031676,"lon":121.503413,"acc":15.0,"spd":1.5},{"t":1794376990000,"lat":25.031751,"lon":121.503532,"acc":15.0,"spd":1.4},{"t":1794376995000,"lat":25.03179,"lon":121.503505,"acc":15.0,"spd":1.1},{"t":1794377000000,"lat":25.031873,"lon":121.5036,"acc":15.0,"spd":1.6},{"t":1794377005000,"lat":25.031844,"lon":121.503651,"acc":15.0,"spd":1.3},{"t":1794377010000,"lat":25.031824,"lon":121.503914,"acc":15.0,"spd":1.4},{"t":1794377015000,"lat":25.032159,"lon":121.503907,"acc":15.0,"spd":1.5},{"t":1794377020000,"lat":25.031867,"lon":121.503945,"acc":15.0,"spd":1.2},{"t":1794377025000,"lat":25.032035,"lon":121.5039,"acc":15.0,"spd":1.2},{"t":1794377030000,"lat":25.031901,"lon":121.503874,"acc":15.0,"spd":1.8},{"t":1794377035000,"lat":25.031844,"lon":121.504102,"acc":15.0,"spd":1.6},{"t":1794377040000,"lat":25.031957,"lon":121.503856,"acc":15.0,"spd":1.5},{"t":1794377045000,"lat":25.032205,"lon":121.504079,"acc":15.0,"spd":1.5},{"t":1794377050000,"lat":25.032076,"lon":121.504375,"acc":15.0,"spd":1.3},{"t":1794377055000,"lat":25.032106,"lon":121.504156,"acc":15.0,"spd":1.7},{"t":1794377060000,"lat":25.032302,"lon":121.504313,"acc":15.0,"spd":1.6},{"t":1794377065000,"lat":25.032259,"lon":121.504527,"acc":15.0,"spd":1.3},{"t":1794377070000,"lat":25.032316,"lon":121.504155,"acc":15.0,"spd":1.8},{"t":1794377075000,"lat":25.032264,"lon":121.504471,"acc":15.0,"spd":1.7},{"t":1794377080000,"lat":25.032574,"lon":121.504578,"acc":15.0,"spd":1.1},{"t":1794377085000,"lat":25.032398,"lon":121.504513,"acc":15.0,"spd":1.4},{"t":1794377090000,"lat":25.032638,"lon":121.504742,"acc":15.0,"spd":1.1},{"t":1794377095000,"lat":25.032417,"lon":121.504668,"acc":15.0,"spd":1.0},{"t":1794377100000,"lat":25.032619,"lon":121.504882,"acc":15.0,"spd":1.3},{"t":1794377105000,"lat":25.032552,"lon":121.504887,"acc":15.0,"spd":1.2},{"t":1794377110000,"lat":25.032628,"lon":121.504839,"acc":15.0,"spd":1.4},{"t":1794377115000,"lat":25.032725,"lon":121.504865,"acc":15.0,"spd":1.5},{"t":1794377120000,"lat":25.032636,"lon":121.504779,"acc":15.0,"spd":1.3},{"t":1794377125000,"lat":25.03261,"lon":121.505255,"acc":15.0,"spd":1.2},{"t":1794377130000,"lat":25.0327,"lon":121.505232,"acc":15.0,"spd":1.2},{"t":1794377135000,"lat":25.03296,"lon":121.505086,"acc":15.0,"spd":1.4},{"t":1794377140000,"lat":25.03308,"lon":121.50532,"acc":15.0,"spd":1.3},{"t":1794377145000,"lat":25.033147,"lon":121.505343,"acc":15.0,"spd":1.4},{"t":1794377150000,"lat":25.033222,"lon":121.505245,"acc":15.0,"spd":1.3},{"t":1794377155000,"lat":25.033213,"lon":121.505392,"acc":15.0,"spd":1.4},{"t":1794377160000,"lat":25.033148,"lon":121.505094,"acc":15.0,"spd":1.1},{"t":1794377165000,"lat":25.033288,"lon":121.505079,"acc":15.0,"spd":1.4},{"t":1794377170000,"lat":25.033265,"lon":121.505457,"acc":15.0,"spd":1.4},{"t":1794377175000,"lat":25.033327,"lon":121.505307,"acc":15.0,"spd":1.6},{"t":1794377180000,"lat":25.033478,"lon":121.50527,"acc":15.0,"spd":1.4},{"t":1794377185000,"lat":25.033567,"lon":121.50536,"acc":15.0,"spd":1.5},{"t":1794377190000,"lat":25.033477,"lon":121.505551,"acc":15.0,"spd":1.5},{"t":1794377195000,"lat":25.033602,"lon":121.505224,"acc":15.0,"spd":1.1},{"t":1794377200000,"lat":25.033782,"lon":121.505546,"acc":15.0,"spd":1.5},{"t":1794377205000,"lat":25.033885,"lon":121.505594,"acc":15.0,"spd":1.4},{"t":1794377210000,"lat":25.033755,"lon":121.505592,"acc":15.0,"spd":1.7},{"t":1794377215000,"lat":25.033574,"lon":121.505548,"acc":15.0,"spd":1.5},{"t":1794377220000,"lat":25.033888,"lon":121.505586,"acc":15.0,"spd":1.6},{"t":1794377225000,"lat":25.033774,"lon":121.505588,"acc":15.0,"spd":1.4},{"t":1794377230000,"lat":25.034021,"lon":121.505696,"acc":15.0,"spd":1.1},{"t":1794377235000,"lat":25.034016,"lon":121.505689,"acc":15.0,"spd":1.3},{"t":1794377240000,"lat":25.034014,"lon":121.505822,"acc":15.0,"spd":1.3},{"t":1794377245000,"lat":25.034018,"lon":121.505936,"acc":15.0,"spd":1.3},{"t":1794377250000,"lat":25.034173,"lon":121.505617,"acc":15.0,"spd":1.3},{"t":1794377255000,"lat":25.034187,"lon":121.505781,"acc":15.0,"spd":1.4},{"t":1794377260000,"lat":25.034271,"lon":121.505895,"acc":15.0,"spd":1.1},{"t":1794377265000,"lat":25.03441,"lon":121.5059,"acc":15.0,"spd":1.3},{"t":1794377270000,"lat":25.034527,"lon":121.506016,"acc":15.0,"spd":1.5},{"t":1794377275000,"lat":25.034444,"lon":121.506046,"acc":15.0,"spd":1.7},{"t":1794377280000,"lat":25.034448,"lon":121.506069,"acc":15.0,"spd":0.9},{"t":1794377285000,"lat":25.034599,"lon":121.506118,"acc":15.0,"spd":1.5},{"t":1794377290000,"lat":25.034536,"lon":121.506112,"acc":15.0,"spd":1.1},{"t":1794377295000,"lat":25.034739,"lon":121.506048,"acc":15.0,"spd":1.4},{"t":1794377300000,"lat":25.035077,"lon":121.506157,"acc":15.0,"spd":1.2},{"t":1794377305000,"lat":25.034656,"lon":121.506073,"acc":15.0,"spd":1.2},{"t":1794377310000,"lat":25.034948,"lon":121.506287,"acc":15.0,"spd":1.5},{"t":1794377315000,"lat":25.034941,"lon":121.506134,"acc":15.0,"spd":1.4},{"t":1794377320000,"lat":25.035119,"lon":121.506378,"acc":15.0,"spd":1.4},{"t":1794377325000,"lat":25.035155,"lon":121.506307,"acc":15.0,"spd":1.3},{"t":1794377330000,"lat":25.035019,"lon":121.506129,"acc":15.0,"spd":1.4},{"t":1794377335000,"lat":25.035235,"lon":121.506366,"acc":15.0,"spd":1.2},{"t":1794377340000,"lat":25.035157,"lon":121.506441,"acc":15.0,"spd":1.4},{"t":1794377345000,"lat":25.035259,"lon":121.506434,"acc":15.0,"spd":1.4},{"t":1794377350000,"lat":25.035511,"lon":121.506426,"acc":15.0,"spd":1.2},{"t":1794377355000,"lat":25.035466,"lon":121.506428,"acc":15.0,"spd":2.1},{"t":1794377360000,"lat":25.035361,"lon":121.506561,"acc":15.0,"spd":1.2},{"t":1794377365000,"lat":25.035295,"lon":121.506511,"acc":15.0,"spd":1.5},{"t":1794377370000,"lat":25.035472,"lon":121.50645,"acc":15.0,"spd":1.3},{"t":1794377375000,"lat":25.035546,"lon":121.506466,"acc":15.0,"spd":1.0},{"t":1794377380000,"lat":25.035638,"lon":121.506684,"acc":15.0,"spd":1.2},{"t":1794377385000,"lat":25.035813,"lon":121.506488,"acc":15.0,"spd":1.6},{"t":1794377390000,"lat":25.035589,"lon":121.50661,"acc":15.0,"spd":1.4},{"t":1794377395000,"lat":25.035872,"lon":121.506747,"acc":15.0,"spd":1.4},{"t":1794377400000,"lat":25.035718,"lon":121.506817,"acc":15.0,"spd":1.3},{"t":1794377405000,"lat":25.035817,"lon":121.506564,"acc":15.0,"spd":1.7}]} \ No newline at end of file diff --git a/android/app/src/test/resources/ride_replay_TXG300_0.json b/android/app/src/test/resources/ride_replay_TXG300_0.json new file mode 100644 index 0000000..bcda3e3 --- /dev/null +++ b/android/app/src/test/resources/ride_replay_TXG300_0.json @@ -0,0 +1 @@ +{"route":"TXG300","pathId":0,"plate":"EAL-2225","boardedAtMs":1794376374000,"boardingStopIdRaw":"19437","destinationStopIdRaw":"696","stops":[{"stopIdRaw":"13567","sequence":1,"lat":24.225899,"lon":120.576539},{"stopIdRaw":"19437","sequence":2,"lat":24.221858,"lon":120.579074},{"stopIdRaw":"19435","sequence":3,"lat":24.217481,"lon":120.580258},{"stopIdRaw":"19433","sequence":4,"lat":24.209925,"lon":120.581137},{"stopIdRaw":"11257","sequence":5,"lat":24.186045538196,"lon":120.58566809429},{"stopIdRaw":"16564","sequence":6,"lat":24.182739398451,"lon":120.59351635159},{"stopIdRaw":"696","sequence":7,"lat":24.182835,"lon":120.603479},{"stopIdRaw":"18163","sequence":8,"lat":24.184402,"lon":120.60992},{"stopIdRaw":"10967","sequence":9,"lat":24.182245,"lon":120.616774},{"stopIdRaw":"10730","sequence":10,"lat":24.180888184761,"lon":120.61955176882},{"stopIdRaw":"9993","sequence":11,"lat":24.178688,"lon":120.623955},{"stopIdRaw":"1311","sequence":12,"lat":24.169165,"lon":120.638879},{"stopIdRaw":"1309","sequence":13,"lat":24.165999,"lon":120.643231},{"stopIdRaw":"8503","sequence":14,"lat":24.162766,"lon":120.649195},{"stopIdRaw":"19431","sequence":15,"lat":24.159138,"lon":120.655721},{"stopIdRaw":"19430","sequence":16,"lat":24.157273797065,"lon":120.65910595273},{"stopIdRaw":"6053","sequence":17,"lat":24.155302,"lon":120.662745},{"stopIdRaw":"10724","sequence":18,"lat":24.152634633083,"lon":120.66746262747},{"stopIdRaw":"1301","sequence":19,"lat":24.149206,"lon":120.671613},{"stopIdRaw":"19264","sequence":20,"lat":24.146362,"lon":120.674826},{"stopIdRaw":"19265","sequence":21,"lat":24.144629,"lon":120.676875},{"stopIdRaw":"5386","sequence":22,"lat":24.143504,"lon":120.678188},{"stopIdRaw":"13530","sequence":23,"lat":24.141863,"lon":120.680019},{"stopIdRaw":"2381","sequence":24,"lat":24.139861,"lon":120.682304},{"stopIdRaw":"22398","sequence":25,"lat":24.138804,"lon":120.683527},{"stopIdRaw":"12251","sequence":26,"lat":24.137749,"lon":120.686459}],"busReports":[{"id":"757-U8","dir":0,"lat":24.261023,"lon":120.521953,"t":1794376111000,"rx":1794376155992},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522088,"t":1794376128000,"rx":1794376155992},{"id":"763-U8","dir":0,"lat":24.150223,"lon":120.670439,"t":1794376124000,"rx":1794376155992},{"id":"780-U8","dir":1,"lat":24.24042,"lon":120.564173,"t":1794376116000,"rx":1794376155992},{"id":"EAL-1378","dir":0,"lat":24.182928,"lon":120.603616,"t":1794376122000,"rx":1794376155992},{"id":"EAL-1589","dir":0,"lat":24.275313,"lon":120.547246,"t":1794376112000,"rx":1794376155992},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564141,"t":1794376129000,"rx":1794376155992},{"id":"EAL-2225","dir":0,"lat":24.231181,"lon":120.573191,"t":1794376114000,"rx":1794376155992},{"id":"EAL-2263","dir":1,"lat":24.179173,"lon":120.623493,"t":1794376126000,"rx":1794376155992},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376112000,"rx":1794376155992},{"id":"EAL-2269","dir":1,"lat":24.244975,"lon":120.573271,"t":1794376125000,"rx":1794376155992},{"id":"EAL-5358","dir":1,"lat":24.143899,"lon":120.677993,"t":1794376130000,"rx":1794376155992},{"id":"EAL-5365","dir":1,"lat":24.213966,"lon":120.581001,"t":1794376128000,"rx":1794376155992},{"id":"EAL-5390","dir":1,"lat":24.178801,"lon":120.624426,"t":1794376124000,"rx":1794376155992},{"id":"EAL-6027","dir":0,"lat":24.264823,"lon":120.54102,"t":1794376122000,"rx":1794376155992},{"id":"EAL-6028","dir":1,"lat":24.143521,"lon":120.678668,"t":1794376128000,"rx":1794376155992},{"id":"EAL-6029","dir":1,"lat":24.182651,"lon":120.601461,"t":1794376118000,"rx":1794376155992},{"id":"EAR-1003","dir":0,"lat":24.17912,"lon":120.623225,"t":1794376123000,"rx":1794376155992},{"id":"EAR-1015","dir":1,"lat":24.183343,"lon":120.592771,"t":1794376121000,"rx":1794376155992},{"id":"EAR-1017","dir":0,"lat":24.159829,"lon":120.654406,"t":1794376129000,"rx":1794376155992},{"id":"FAE-800","dir":0,"lat":24.241038,"lon":120.564229,"t":1794376122000,"rx":1794376155992},{"id":"757-U8","dir":0,"lat":24.261026,"lon":120.521974,"t":1794376151000,"rx":1794376179410},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522104,"t":1794376148000,"rx":1794376179410},{"id":"763-U8","dir":0,"lat":24.149229,"lon":120.671613,"t":1794376144000,"rx":1794376179410},{"id":"780-U8","dir":1,"lat":24.240456,"lon":120.564253,"t":1794376156000,"rx":1794376179410},{"id":"EAL-1378","dir":0,"lat":24.18346,"lon":120.605855,"t":1794376154000,"rx":1794376179410},{"id":"EAL-1589","dir":0,"lat":24.275313,"lon":120.547246,"t":1794376152000,"rx":1794376179410},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564143,"t":1794376149000,"rx":1794376179410},{"id":"EAL-2225","dir":0,"lat":24.229793,"lon":120.574021,"t":1794376154000,"rx":1794376179410},{"id":"EAL-2263","dir":1,"lat":24.179651,"lon":120.622573,"t":1794376150000,"rx":1794376179410},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376152000,"rx":1794376179410},{"id":"EAL-2269","dir":1,"lat":24.247418,"lon":120.574936,"t":1794376145000,"rx":1794376179410},{"id":"EAL-5358","dir":1,"lat":24.143914,"lon":120.677999,"t":1794376155000,"rx":1794376179410},{"id":"EAL-5365","dir":1,"lat":24.215949,"lon":120.580738,"t":1794376144000,"rx":1794376179410},{"id":"EAL-5390","dir":1,"lat":24.179273,"lon":120.623526,"t":1794376144000,"rx":1794376179410},{"id":"EAL-6027","dir":0,"lat":24.264833,"lon":120.541016,"t":1794376142000,"rx":1794376179410},{"id":"EAL-6028","dir":1,"lat":24.143853,"lon":120.678298,"t":1794376157000,"rx":1794376179410},{"id":"EAL-6029","dir":1,"lat":24.182303,"lon":120.59596,"t":1794376158000,"rx":1794376179410},{"id":"EAR-1003","dir":0,"lat":24.178871,"lon":120.623828,"t":1794376160000,"rx":1794376179410},{"id":"EAR-1015","dir":1,"lat":24.185193,"lon":120.588419,"t":1794376159000,"rx":1794376179410},{"id":"EAR-1017","dir":0,"lat":24.159251,"lon":120.655535,"t":1794376149000,"rx":1794376179410},{"id":"FAE-800","dir":0,"lat":24.241023,"lon":120.564229,"t":1794376142000,"rx":1794376179410},{"id":"757-U8","dir":0,"lat":24.261026,"lon":120.521958,"t":1794376171000,"rx":1794376203395},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522088,"t":1794376168000,"rx":1794376203395},{"id":"763-U8","dir":0,"lat":24.149176,"lon":120.671666,"t":1794376164000,"rx":1794376203395},{"id":"EAL-1378","dir":0,"lat":24.183696,"lon":120.606886,"t":1794376162000,"rx":1794376203395},{"id":"EAL-1589","dir":0,"lat":24.275878,"lon":120.547798,"t":1794376172000,"rx":1794376203395},{"id":"EAL-2032","dir":0,"lat":24.240143,"lon":120.564143,"t":1794376169000,"rx":1794376203395},{"id":"EAL-2225","dir":0,"lat":24.22858,"lon":120.574791,"t":1794376174000,"rx":1794376203395},{"id":"EAL-2263","dir":1,"lat":24.179878,"lon":120.622141,"t":1794376166000,"rx":1794376203395},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376172000,"rx":1794376203395},{"id":"EAL-2269","dir":1,"lat":24.24818,"lon":120.575533,"t":1794376165000,"rx":1794376203395},{"id":"EAL-5365","dir":1,"lat":24.216768,"lon":120.580629,"t":1794376164000,"rx":1794376203395},{"id":"EAL-5390","dir":1,"lat":24.179823,"lon":120.622528,"t":1794376173000,"rx":1794376203395},{"id":"EAL-6027","dir":0,"lat":24.264833,"lon":120.541018,"t":1794376162000,"rx":1794376203395},{"id":"EAL-6029","dir":1,"lat":24.182826,"lon":120.5939,"t":1794376175000,"rx":1794376203395},{"id":"EAR-1015","dir":1,"lat":24.185781,"lon":120.587029,"t":1794376171000,"rx":1794376203395},{"id":"EAR-1017","dir":0,"lat":24.159275,"lon":120.655581,"t":1794376169000,"rx":1794376203395},{"id":"FAE-800","dir":0,"lat":24.241018,"lon":120.564228,"t":1794376162000,"rx":1794376203395},{"id":"757-U8","dir":0,"lat":24.261026,"lon":120.521974,"t":1794376191000,"rx":1794376227353},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522104,"t":1794376188000,"rx":1794376227353},{"id":"763-U8","dir":0,"lat":24.147903,"lon":120.673199,"t":1794376204000,"rx":1794376227353},{"id":"780-U8","dir":1,"lat":24.240443,"lon":120.564239,"t":1794376196000,"rx":1794376227353},{"id":"EAL-1378","dir":0,"lat":24.184556,"lon":120.61036,"t":1794376202000,"rx":1794376227353},{"id":"EAL-1589","dir":0,"lat":24.276393,"lon":120.549271,"t":1794376192000,"rx":1794376227353},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564143,"t":1794376189000,"rx":1794376227353},{"id":"EAL-2225","dir":0,"lat":24.227433,"lon":120.57555,"t":1794376194000,"rx":1794376227353},{"id":"EAL-2263","dir":1,"lat":24.17996,"lon":120.621999,"t":1794376186000,"rx":1794376227353},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376192000,"rx":1794376227353},{"id":"EAL-2269","dir":1,"lat":24.247933,"lon":120.575059,"t":1794376205000,"rx":1794376227353},{"id":"EAL-5358","dir":1,"lat":24.143913,"lon":120.678004,"t":1794376195000,"rx":1794376227353},{"id":"EAL-5365","dir":1,"lat":24.219484,"lon":120.580311,"t":1794376204000,"rx":1794376227353},{"id":"EAL-5390","dir":1,"lat":24.179938,"lon":120.62227,"t":1794376204000,"rx":1794376227353},{"id":"EAL-6027","dir":0,"lat":24.264826,"lon":120.541023,"t":1794376202000,"rx":1794376227353},{"id":"EAL-6028","dir":1,"lat":24.143943,"lon":120.678196,"t":1794376204000,"rx":1794376227353},{"id":"EAL-6029","dir":1,"lat":24.182976,"lon":120.593533,"t":1794376198000,"rx":1794376227353},{"id":"EAR-1003","dir":0,"lat":24.17884,"lon":120.623841,"t":1794376200000,"rx":1794376227353},{"id":"EAR-1015","dir":1,"lat":24.186153,"lon":120.586065,"t":1794376199000,"rx":1794376227353},{"id":"EAR-1017","dir":0,"lat":24.159146,"lon":120.655726,"t":1794376189000,"rx":1794376227353},{"id":"FAE-800","dir":0,"lat":24.241003,"lon":120.564228,"t":1794376202000,"rx":1794376227353},{"id":"757-U8","dir":0,"lat":24.261026,"lon":120.521974,"t":1794376231000,"rx":1794376251618},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522104,"t":1794376228000,"rx":1794376251618},{"id":"763-U8","dir":0,"lat":24.147813,"lon":120.673306,"t":1794376224000,"rx":1794376251618},{"id":"780-U8","dir":1,"lat":24.240426,"lon":120.564239,"t":1794376216000,"rx":1794376251618},{"id":"EAL-1378","dir":0,"lat":24.184554,"lon":120.611403,"t":1794376230000,"rx":1794376251618},{"id":"EAL-1589","dir":0,"lat":24.276388,"lon":120.549284,"t":1794376232000,"rx":1794376251618},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564161,"t":1794376229000,"rx":1794376251618},{"id":"EAL-2225","dir":0,"lat":24.227276,"lon":120.575654,"t":1794376234000,"rx":1794376251618},{"id":"EAL-2263","dir":1,"lat":24.180138,"lon":120.621594,"t":1794376229000,"rx":1794376251618},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376232000,"rx":1794376251618},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575059,"t":1794376225000,"rx":1794376251618},{"id":"EAL-5358","dir":1,"lat":24.144576,"lon":120.677153,"t":1794376215000,"rx":1794376251618},{"id":"EAL-5365","dir":1,"lat":24.2195,"lon":120.580303,"t":1794376224000,"rx":1794376251618},{"id":"EAL-5390","dir":1,"lat":24.180201,"lon":120.621656,"t":1794376235000,"rx":1794376251618},{"id":"EAL-6027","dir":0,"lat":24.264828,"lon":120.541006,"t":1794376222000,"rx":1794376251618},{"id":"EAL-6028","dir":1,"lat":24.144916,"lon":120.676971,"t":1794376231000,"rx":1794376251618},{"id":"EAL-6029","dir":1,"lat":24.183001,"lon":120.593441,"t":1794376218000,"rx":1794376251618},{"id":"EAR-1003","dir":0,"lat":24.178331,"lon":120.624713,"t":1794376220000,"rx":1794376251618},{"id":"EAR-1015","dir":1,"lat":24.186238,"lon":120.585925,"t":1794376219000,"rx":1794376251618},{"id":"EAR-1017","dir":0,"lat":24.159143,"lon":120.655738,"t":1794376229000,"rx":1794376251618},{"id":"FAE-800","dir":0,"lat":24.241001,"lon":120.564226,"t":1794376222000,"rx":1794376251618},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521956,"t":1794376351000,"rx":1794376395767},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522088,"t":1794376368000,"rx":1794376395767},{"id":"763-U8","dir":0,"lat":24.145483,"lon":120.675946,"t":1794376364000,"rx":1794376395767},{"id":"780-U8","dir":1,"lat":24.240379,"lon":120.56416,"t":1794376356000,"rx":1794376395767},{"id":"EAL-1378","dir":0,"lat":24.181228,"lon":120.618816,"t":1794376362000,"rx":1794376395767},{"id":"EAL-1589","dir":0,"lat":24.272808,"lon":120.559848,"t":1794376352000,"rx":1794376395767},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564183,"t":1794376369000,"rx":1794376395767},{"id":"EAL-2225","dir":0,"lat":24.222293,"lon":120.578826,"t":1794376354000,"rx":1794376395767},{"id":"EAL-2263","dir":1,"lat":24.182625,"lon":120.616613,"t":1794376366000,"rx":1794376395767},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376352000,"rx":1794376395767},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575055,"t":1794376365000,"rx":1794376395767},{"id":"EAL-5358","dir":1,"lat":24.150656,"lon":120.670243,"t":1794376355000,"rx":1794376395767},{"id":"EAL-5365","dir":1,"lat":24.22512,"lon":120.577335,"t":1794376364000,"rx":1794376395767},{"id":"EAL-5390","dir":1,"lat":24.182533,"lon":120.616641,"t":1794376364000,"rx":1794376395767},{"id":"EAL-6027","dir":0,"lat":24.264814,"lon":120.540994,"t":1794376362000,"rx":1794376395767},{"id":"EAL-6028","dir":1,"lat":24.150594,"lon":120.67058,"t":1794376357000,"rx":1794376395767},{"id":"EAL-6029","dir":1,"lat":24.186063,"lon":120.586218,"t":1794376361000,"rx":1794376395767},{"id":"EAR-1003","dir":0,"lat":24.169301,"lon":120.638728,"t":1794376360000,"rx":1794376395767},{"id":"EAR-1015","dir":1,"lat":24.197561,"lon":120.580528,"t":1794376369000,"rx":1794376395767},{"id":"EAR-1017","dir":0,"lat":24.155075,"lon":120.663313,"t":1794376369000,"rx":1794376395767},{"id":"FAE-800","dir":0,"lat":24.240923,"lon":120.56425,"t":1794376362000,"rx":1794376395767},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521973,"t":1794376371000,"rx":1794376407860},{"id":"763-U8","dir":0,"lat":24.144686,"lon":120.676826,"t":1794376384000,"rx":1794376407860},{"id":"780-U8","dir":1,"lat":24.240383,"lon":120.56416,"t":1794376376000,"rx":1794376407860},{"id":"EAL-1378","dir":0,"lat":24.181228,"lon":120.618816,"t":1794376382000,"rx":1794376407860},{"id":"EAL-1589","dir":0,"lat":24.272196,"lon":120.561591,"t":1794376372000,"rx":1794376407860},{"id":"EAL-2225","dir":0,"lat":24.221758,"lon":120.579133,"t":1794376374000,"rx":1794376407860},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376372000,"rx":1794376407860},{"id":"EAL-2269","dir":1,"lat":24.247924,"lon":120.575055,"t":1794376385000,"rx":1794376407860},{"id":"EAL-5358","dir":1,"lat":24.150655,"lon":120.670266,"t":1794376375000,"rx":1794376407860},{"id":"EAL-5365","dir":1,"lat":24.225238,"lon":120.577291,"t":1794376384000,"rx":1794376407860},{"id":"EAL-5390","dir":1,"lat":24.182533,"lon":120.616638,"t":1794376384000,"rx":1794376407860},{"id":"EAL-6027","dir":0,"lat":24.264816,"lon":120.540998,"t":1794376382000,"rx":1794376407860},{"id":"EAL-6028","dir":1,"lat":24.150653,"lon":120.670528,"t":1794376377000,"rx":1794376407860},{"id":"EAL-6029","dir":1,"lat":24.186144,"lon":120.58606,"t":1794376378000,"rx":1794376407860},{"id":"EAR-1003","dir":0,"lat":24.169344,"lon":120.638819,"t":1794376380000,"rx":1794376407860},{"id":"EAR-1015","dir":1,"lat":24.199161,"lon":120.580714,"t":1794376379000,"rx":1794376407860},{"id":"FAE-800","dir":0,"lat":24.240905,"lon":120.564228,"t":1794376382000,"rx":1794376407860},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521973,"t":1794376411000,"rx":1794376431626},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522088,"t":1794376408000,"rx":1794376431626},{"id":"763-U8","dir":0,"lat":24.143623,"lon":120.678053,"t":1794376404000,"rx":1794376431626},{"id":"780-U8","dir":1,"lat":24.24039,"lon":120.56416,"t":1794376396000,"rx":1794376431626},{"id":"EAL-1378","dir":0,"lat":24.180976,"lon":120.619408,"t":1794376402000,"rx":1794376431626},{"id":"EAL-1589","dir":0,"lat":24.272198,"lon":120.561631,"t":1794376412000,"rx":1794376431626},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564184,"t":1794376409000,"rx":1794376431626},{"id":"EAL-2225","dir":0,"lat":24.22171,"lon":120.579241,"t":1794376414000,"rx":1794376431626},{"id":"EAL-2263","dir":1,"lat":24.184123,"lon":120.613479,"t":1794376413000,"rx":1794376431626},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376412000,"rx":1794376431626},{"id":"EAL-2269","dir":1,"lat":24.247926,"lon":120.575055,"t":1794376405000,"rx":1794376431626},{"id":"EAL-5358","dir":1,"lat":24.150656,"lon":120.670266,"t":1794376395000,"rx":1794376431626},{"id":"EAL-5365","dir":1,"lat":24.225394,"lon":120.577146,"t":1794376405000,"rx":1794376431626},{"id":"EAL-5390","dir":1,"lat":24.183463,"lon":120.615003,"t":1794376409000,"rx":1794376431626},{"id":"EAL-6027","dir":0,"lat":24.264818,"lon":120.541003,"t":1794376402000,"rx":1794376431626},{"id":"EAL-6028","dir":1,"lat":24.150653,"lon":120.670524,"t":1794376397000,"rx":1794376431626},{"id":"EAL-6029","dir":1,"lat":24.186208,"lon":120.586029,"t":1794376398000,"rx":1794376431626},{"id":"EAR-1003","dir":0,"lat":24.169318,"lon":120.638828,"t":1794376400000,"rx":1794376431626},{"id":"EAR-1015","dir":1,"lat":24.201438,"lon":120.580961,"t":1794376399000,"rx":1794376431626},{"id":"EAR-1017","dir":0,"lat":24.154926,"lon":120.663621,"t":1794376411000,"rx":1794376431626},{"id":"FAE-800","dir":0,"lat":24.240896,"lon":120.564224,"t":1794376402000,"rx":1794376431626},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522104,"t":1794376428000,"rx":1794376455876},{"id":"763-U8","dir":0,"lat":24.14348,"lon":120.678306,"t":1794376424000,"rx":1794376455876},{"id":"780-U8","dir":1,"lat":24.240393,"lon":120.56416,"t":1794376416000,"rx":1794376455876},{"id":"EAL-1378","dir":0,"lat":24.180958,"lon":120.619429,"t":1794376422000,"rx":1794376455876},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564168,"t":1794376429000,"rx":1794376455876},{"id":"EAL-2263","dir":1,"lat":24.184618,"lon":120.611975,"t":1794376426000,"rx":1794376455876},{"id":"EAL-2269","dir":1,"lat":24.247934,"lon":120.575059,"t":1794376425000,"rx":1794376455876},{"id":"EAL-5358","dir":1,"lat":24.150656,"lon":120.670265,"t":1794376415000,"rx":1794376455876},{"id":"EAL-5365","dir":1,"lat":24.225381,"lon":120.577151,"t":1794376424000,"rx":1794376455876},{"id":"EAL-5390","dir":1,"lat":24.184358,"lon":120.613068,"t":1794376424000,"rx":1794376455876},{"id":"EAL-6027","dir":0,"lat":24.264821,"lon":120.541008,"t":1794376422000,"rx":1794376455876},{"id":"EAL-6028","dir":1,"lat":24.150653,"lon":120.670541,"t":1794376417000,"rx":1794376455876},{"id":"EAL-6029","dir":1,"lat":24.186215,"lon":120.586029,"t":1794376418000,"rx":1794376455876},{"id":"EAR-1003","dir":0,"lat":24.169319,"lon":120.638828,"t":1794376420000,"rx":1794376455876},{"id":"EAR-1015","dir":1,"lat":24.201461,"lon":120.580956,"t":1794376419000,"rx":1794376455876},{"id":"EAR-1017","dir":0,"lat":24.154811,"lon":120.663701,"t":1794376429000,"rx":1794376455876},{"id":"FAE-800","dir":0,"lat":24.240893,"lon":120.564223,"t":1794376422000,"rx":1794376455876},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521973,"t":1794376451000,"rx":1794376479380},{"id":"762-U8","dir":0,"lat":24.259168,"lon":120.522088,"t":1794376448000,"rx":1794376479380},{"id":"763-U8","dir":0,"lat":24.14309,"lon":120.678773,"t":1794376444000,"rx":1794376479380},{"id":"780-U8","dir":0,"lat":24.240383,"lon":120.56416,"t":1794376456000,"rx":1794376479380},{"id":"782-U8","dir":0,"lat":24.349775,"lon":120.616411,"t":1794376460000,"rx":1794376479380},{"id":"EAL-1378","dir":0,"lat":24.179688,"lon":120.622026,"t":1794376459000,"rx":1794376479380},{"id":"EAL-1589","dir":0,"lat":24.27146,"lon":120.563603,"t":1794376452000,"rx":1794376479380},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564184,"t":1794376449000,"rx":1794376479380},{"id":"EAL-2225","dir":0,"lat":24.221121,"lon":120.579503,"t":1794376454000,"rx":1794376479380},{"id":"EAL-2263","dir":1,"lat":24.184666,"lon":120.610126,"t":1794376446000,"rx":1794376479380},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576544,"t":1794376452000,"rx":1794376479380},{"id":"EAL-2269","dir":1,"lat":24.247939,"lon":120.575061,"t":1794376445000,"rx":1794376479380},{"id":"EAL-5358","dir":1,"lat":24.150656,"lon":120.670259,"t":1794376455000,"rx":1794376479380},{"id":"EAL-5365","dir":0,"lat":24.226216,"lon":120.576625,"t":1794376457000,"rx":1794376479380},{"id":"EAL-5390","dir":1,"lat":24.184744,"lon":120.610434,"t":1794376444000,"rx":1794376479380},{"id":"EAL-6027","dir":0,"lat":24.264823,"lon":120.541008,"t":1794376442000,"rx":1794376479380},{"id":"EAL-6028","dir":1,"lat":24.150655,"lon":120.670524,"t":1794376457000,"rx":1794376479380},{"id":"EAL-6029","dir":1,"lat":24.186251,"lon":120.585961,"t":1794376458000,"rx":1794376479380},{"id":"EAR-1003","dir":0,"lat":24.168103,"lon":120.64024,"t":1794376460000,"rx":1794376479380},{"id":"EAR-1015","dir":1,"lat":24.201461,"lon":120.580958,"t":1794376459000,"rx":1794376479380},{"id":"EAR-1017","dir":0,"lat":24.154569,"lon":120.664128,"t":1794376456000,"rx":1794376479380},{"id":"FAE-800","dir":0,"lat":24.240891,"lon":120.564223,"t":1794376442000,"rx":1794376479380},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521955,"t":1794376471000,"rx":1794376504105},{"id":"762-U8","dir":0,"lat":24.25916,"lon":120.522091,"t":1794376468000,"rx":1794376504105},{"id":"763-U8","dir":0,"lat":24.142949,"lon":120.67892,"t":1794376464000,"rx":1794376504105},{"id":"EAL-1378","dir":0,"lat":24.17892,"lon":120.623549,"t":1794376473000,"rx":1794376504105},{"id":"EAL-1589","dir":0,"lat":24.270624,"lon":120.565969,"t":1794376472000,"rx":1794376504105},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.56417,"t":1794376469000,"rx":1794376504105},{"id":"EAL-2225","dir":0,"lat":24.219183,"lon":120.580071,"t":1794376474000,"rx":1794376504105},{"id":"EAL-2263","dir":1,"lat":24.184438,"lon":120.609033,"t":1794376466000,"rx":1794376504105},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376472000,"rx":1794376504105},{"id":"EAL-2269","dir":1,"lat":24.247933,"lon":120.575063,"t":1794376465000,"rx":1794376504105},{"id":"EAL-5365","dir":0,"lat":24.226913,"lon":120.576205,"t":1794376464000,"rx":1794376504105},{"id":"EAL-5390","dir":1,"lat":24.184494,"lon":120.609129,"t":1794376468000,"rx":1794376504105},{"id":"EAL-6027","dir":0,"lat":24.264823,"lon":120.541025,"t":1794376462000,"rx":1794376504105},{"id":"EAL-6028","dir":1,"lat":24.150823,"lon":120.67032,"t":1794376470000,"rx":1794376504105},{"id":"EAL-6029","dir":1,"lat":24.186496,"lon":120.585313,"t":1794376466000,"rx":1794376504105},{"id":"EAR-1003","dir":0,"lat":24.167165,"lon":120.64154,"t":1794376472000,"rx":1794376504105},{"id":"EAR-1017","dir":0,"lat":24.153316,"lon":120.666353,"t":1794376473000,"rx":1794376504105},{"id":"FAE-800","dir":0,"lat":24.240891,"lon":120.564221,"t":1794376462000,"rx":1794376504105},{"id":"757-U8","dir":0,"lat":24.261023,"lon":120.521955,"t":1794376491000,"rx":1794376527389},{"id":"762-U8","dir":0,"lat":24.259151,"lon":120.522093,"t":1794376488000,"rx":1794376527389},{"id":"763-U8","dir":0,"lat":24.14283,"lon":120.679119,"t":1794376504000,"rx":1794376527389},{"id":"780-U8","dir":0,"lat":24.240376,"lon":120.56416,"t":1794376496000,"rx":1794376527389},{"id":"782-U8","dir":0,"lat":24.294138,"lon":120.588976,"t":1794376500000,"rx":1794376527389},{"id":"EAL-1378","dir":0,"lat":24.178689,"lon":120.624036,"t":1794376502000,"rx":1794376527389},{"id":"EAL-1589","dir":0,"lat":24.269716,"lon":120.568491,"t":1794376492000,"rx":1794376527389},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564186,"t":1794376489000,"rx":1794376527389},{"id":"EAL-2225","dir":0,"lat":24.217503,"lon":120.580303,"t":1794376494000,"rx":1794376527389},{"id":"EAL-2263","dir":1,"lat":24.18336,"lon":120.604391,"t":1794376499000,"rx":1794376527389},{"id":"EAL-2265","dir":1,"lat":24.226491,"lon":120.576546,"t":1794376492000,"rx":1794376527389},{"id":"EAL-2269","dir":1,"lat":24.247945,"lon":120.575081,"t":1794376505000,"rx":1794376527389},{"id":"EAL-5358","dir":1,"lat":24.152384,"lon":120.668253,"t":1794376495000,"rx":1794376527389},{"id":"EAL-5365","dir":0,"lat":24.231486,"lon":120.573165,"t":1794376504000,"rx":1794376527389},{"id":"EAL-5390","dir":1,"lat":24.183344,"lon":120.604311,"t":1794376504000,"rx":1794376527389},{"id":"EAL-6027","dir":0,"lat":24.264803,"lon":120.541013,"t":1794376502000,"rx":1794376527389},{"id":"EAL-6028","dir":1,"lat":24.152291,"lon":120.668514,"t":1794376498000,"rx":1794376527389},{"id":"EAL-6029","dir":1,"lat":24.188391,"lon":120.581031,"t":1794376498000,"rx":1794376527389},{"id":"EAR-1003","dir":0,"lat":24.16617,"lon":120.642993,"t":1794376500000,"rx":1794376527389},{"id":"EAR-1015","dir":1,"lat":24.205995,"lon":120.58149,"t":1794376499000,"rx":1794376527389},{"id":"EAR-1017","dir":0,"lat":24.152709,"lon":120.667346,"t":1794376489000,"rx":1794376527389},{"id":"FAE-800","dir":0,"lat":24.240886,"lon":120.564215,"t":1794376502000,"rx":1794376527389},{"id":"757-U8","dir":0,"lat":24.261023,"lon":120.521969,"t":1794376531000,"rx":1794376552110},{"id":"762-U8","dir":0,"lat":24.259151,"lon":120.522093,"t":1794376528000,"rx":1794376552110},{"id":"763-U8","dir":0,"lat":24.142749,"lon":120.679199,"t":1794376524000,"rx":1794376552110},{"id":"780-U8","dir":0,"lat":24.240376,"lon":120.56416,"t":1794376516000,"rx":1794376552110},{"id":"782-U8","dir":0,"lat":24.294138,"lon":120.588975,"t":1794376520000,"rx":1794376552110},{"id":"EAL-1378","dir":0,"lat":24.178648,"lon":120.62411,"t":1794376522000,"rx":1794376552110},{"id":"EAL-1589","dir":0,"lat":24.265689,"lon":120.571356,"t":1794376532000,"rx":1794376552110},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564186,"t":1794376529000,"rx":1794376552110},{"id":"EAL-2225","dir":0,"lat":24.214331,"lon":120.580719,"t":1794376534000,"rx":1794376552110},{"id":"EAL-2263","dir":1,"lat":24.183073,"lon":120.603333,"t":1794376526000,"rx":1794376552110},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376532000,"rx":1794376552110},{"id":"EAL-2269","dir":1,"lat":24.247934,"lon":120.575073,"t":1794376525000,"rx":1794376552110},{"id":"EAL-5358","dir":1,"lat":24.154765,"lon":120.664121,"t":1794376535000,"rx":1794376552110},{"id":"EAL-5365","dir":0,"lat":24.233564,"lon":120.571875,"t":1794376524000,"rx":1794376552110},{"id":"EAL-5390","dir":1,"lat":24.18315,"lon":120.603518,"t":1794376524000,"rx":1794376552110},{"id":"EAL-6027","dir":0,"lat":24.2648,"lon":120.541013,"t":1794376522000,"rx":1794376552110},{"id":"EAL-6028","dir":1,"lat":24.152923,"lon":120.667554,"t":1794376523000,"rx":1794376552110},{"id":"EAL-6029","dir":1,"lat":24.190881,"lon":120.5798,"t":1794376518000,"rx":1794376552110},{"id":"EAR-1003","dir":0,"lat":24.165559,"lon":120.643876,"t":1794376521000,"rx":1794376552110},{"id":"EAR-1015","dir":1,"lat":24.209834,"lon":120.581479,"t":1794376534000,"rx":1794376552110},{"id":"EAR-1017","dir":0,"lat":24.151291,"lon":120.66924,"t":1794376529000,"rx":1794376552110},{"id":"FAE-800","dir":0,"lat":24.240884,"lon":120.564215,"t":1794376522000,"rx":1794376552110},{"id":"762-U8","dir":0,"lat":24.259151,"lon":120.522091,"t":1794376548000,"rx":1794376575379},{"id":"763-U8","dir":0,"lat":24.142536,"lon":120.679413,"t":1794376544000,"rx":1794376575379},{"id":"780-U8","dir":0,"lat":24.240226,"lon":120.564013,"t":1794376536000,"rx":1794376575379},{"id":"782-U8","dir":0,"lat":24.294138,"lon":120.588991,"t":1794376540000,"rx":1794376575379},{"id":"EAL-1378","dir":0,"lat":24.178648,"lon":120.62411,"t":1794376542000,"rx":1794376575379},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.56417,"t":1794376549000,"rx":1794376575379},{"id":"EAL-2225","dir":0,"lat":24.213326,"lon":120.580825,"t":1794376541000,"rx":1794376575379},{"id":"EAL-2263","dir":1,"lat":24.183021,"lon":120.60329,"t":1794376546000,"rx":1794376575379},{"id":"EAL-2269","dir":1,"lat":24.247934,"lon":120.575076,"t":1794376545000,"rx":1794376575379},{"id":"EAL-5365","dir":0,"lat":24.233374,"lon":120.571749,"t":1794376544000,"rx":1794376575379},{"id":"EAL-5390","dir":1,"lat":24.183109,"lon":120.603456,"t":1794376544000,"rx":1794376575379},{"id":"EAL-6027","dir":0,"lat":24.2648,"lon":120.541015,"t":1794376542000,"rx":1794376575379},{"id":"EAL-6028","dir":1,"lat":24.15453,"lon":120.664868,"t":1794376549000,"rx":1794376575379},{"id":"EAL-6029","dir":1,"lat":24.195515,"lon":120.58037,"t":1794376550000,"rx":1794376575379},{"id":"EAR-1003","dir":0,"lat":24.164664,"lon":120.645471,"t":1794376540000,"rx":1794376575379},{"id":"EAR-1015","dir":1,"lat":24.210376,"lon":120.581418,"t":1794376539000,"rx":1794376575379},{"id":"EAR-1017","dir":0,"lat":24.150988,"lon":120.669534,"t":1794376549000,"rx":1794376575379},{"id":"FAE-800","dir":0,"lat":24.240883,"lon":120.564213,"t":1794376542000,"rx":1794376575379},{"id":"757-U8","dir":0,"lat":24.261023,"lon":120.521969,"t":1794376571000,"rx":1794376599376},{"id":"762-U8","dir":0,"lat":24.259151,"lon":120.522108,"t":1794376568000,"rx":1794376599376},{"id":"763-U8","dir":0,"lat":24.142126,"lon":120.679986,"t":1794376564000,"rx":1794376599376},{"id":"780-U8","dir":0,"lat":24.240666,"lon":120.564653,"t":1794376576000,"rx":1794376599376},{"id":"782-U8","dir":0,"lat":24.294136,"lon":120.588991,"t":1794376580000,"rx":1794376599376},{"id":"EAL-1378","dir":0,"lat":24.17847,"lon":120.624465,"t":1794376576000,"rx":1794376599376},{"id":"EAL-1589","dir":0,"lat":24.261341,"lon":120.574443,"t":1794376572000,"rx":1794376599376},{"id":"EAL-2032","dir":0,"lat":24.240136,"lon":120.56417,"t":1794376569000,"rx":1794376599376},{"id":"EAL-2225","dir":0,"lat":24.210106,"lon":120.581183,"t":1794376574000,"rx":1794376599376},{"id":"EAL-2263","dir":1,"lat":24.182874,"lon":120.602479,"t":1794376575000,"rx":1794376599376},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376572000,"rx":1794376599376},{"id":"EAL-2269","dir":1,"lat":24.247933,"lon":120.575076,"t":1794376565000,"rx":1794376599376},{"id":"EAL-5358","dir":1,"lat":24.155515,"lon":120.663061,"t":1794376575000,"rx":1794376599376},{"id":"EAL-5365","dir":0,"lat":24.232616,"lon":120.572214,"t":1794376564000,"rx":1794376599376},{"id":"EAL-5390","dir":1,"lat":24.182905,"lon":120.602431,"t":1794376579000,"rx":1794376599376},{"id":"EAL-6027","dir":0,"lat":24.2648,"lon":120.541031,"t":1794376562000,"rx":1794376599376},{"id":"EAL-6028","dir":1,"lat":24.154689,"lon":120.664535,"t":1794376577000,"rx":1794376599376},{"id":"EAL-6029","dir":1,"lat":24.19939,"lon":120.580804,"t":1794376578000,"rx":1794376599376},{"id":"EAR-1003","dir":0,"lat":24.164548,"lon":120.645736,"t":1794376580000,"rx":1794376599376},{"id":"EAR-1015","dir":1,"lat":24.214528,"lon":120.580918,"t":1794376579000,"rx":1794376599376},{"id":"EAR-1017","dir":0,"lat":24.150986,"lon":120.669536,"t":1794376569000,"rx":1794376599376},{"id":"FAE-800","dir":0,"lat":24.240881,"lon":120.564213,"t":1794376562000,"rx":1794376599376},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521953,"t":1794376591000,"rx":1794376623337},{"id":"762-U8","dir":0,"lat":24.259153,"lon":120.522091,"t":1794376588000,"rx":1794376623337},{"id":"763-U8","dir":0,"lat":24.142116,"lon":120.68008,"t":1794376584000,"rx":1794376623337},{"id":"EAL-1378","dir":0,"lat":24.178218,"lon":120.625034,"t":1794376582000,"rx":1794376623337},{"id":"EAL-1589","dir":0,"lat":24.259119,"lon":120.575426,"t":1794376592000,"rx":1794376623337},{"id":"EAL-2032","dir":0,"lat":24.240136,"lon":120.564186,"t":1794376589000,"rx":1794376623337},{"id":"EAL-2225","dir":0,"lat":24.210039,"lon":120.581108,"t":1794376594000,"rx":1794376623337},{"id":"EAL-2263","dir":1,"lat":24.182671,"lon":120.601563,"t":1794376586000,"rx":1794376623337},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376592000,"rx":1794376623337},{"id":"EAL-2269","dir":1,"lat":24.247933,"lon":120.575093,"t":1794376585000,"rx":1794376623337},{"id":"EAL-5365","dir":0,"lat":24.232538,"lon":120.572271,"t":1794376584000,"rx":1794376623337},{"id":"EAL-5390","dir":1,"lat":24.182801,"lon":120.601953,"t":1794376584000,"rx":1794376623337},{"id":"EAL-6027","dir":0,"lat":24.264801,"lon":120.540993,"t":1794376582000,"rx":1794376623337},{"id":"EAR-1017","dir":0,"lat":24.150986,"lon":120.669539,"t":1794376589000,"rx":1794376623337},{"id":"FAE-800","dir":0,"lat":24.240878,"lon":120.564211,"t":1794376582000,"rx":1794376623337},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521969,"t":1794376611000,"rx":1794376647774},{"id":"762-U8","dir":0,"lat":24.259153,"lon":120.52209,"t":1794376608000,"rx":1794376647774},{"id":"763-U8","dir":0,"lat":24.141809,"lon":120.680373,"t":1794376624000,"rx":1794376647774},{"id":"780-U8","dir":0,"lat":24.23936,"lon":120.564893,"t":1794376616000,"rx":1794376647774},{"id":"782-U8","dir":0,"lat":24.294135,"lon":120.588973,"t":1794376620000,"rx":1794376647774},{"id":"EAL-1378","dir":0,"lat":24.175879,"lon":120.629769,"t":1794376622000,"rx":1794376647774},{"id":"EAL-1589","dir":0,"lat":24.256673,"lon":120.574868,"t":1794376612000,"rx":1794376647774},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.56417,"t":1794376609000,"rx":1794376647774},{"id":"EAL-2225","dir":0,"lat":24.210039,"lon":120.581093,"t":1794376614000,"rx":1794376647774},{"id":"EAL-2263","dir":1,"lat":24.182103,"lon":120.598389,"t":1794376618000,"rx":1794376647774},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376612000,"rx":1794376647774},{"id":"EAL-2269","dir":1,"lat":24.247921,"lon":120.5751,"t":1794376625000,"rx":1794376647774},{"id":"EAL-5358","dir":1,"lat":24.155568,"lon":120.662796,"t":1794376615000,"rx":1794376647774},{"id":"EAL-5365","dir":0,"lat":24.23254,"lon":120.572289,"t":1794376624000,"rx":1794376647774},{"id":"EAL-5390","dir":1,"lat":24.182138,"lon":120.598091,"t":1794376624000,"rx":1794376647774},{"id":"EAL-6027","dir":0,"lat":24.264818,"lon":120.540994,"t":1794376622000,"rx":1794376647774},{"id":"EAL-6028","dir":1,"lat":24.154578,"lon":120.66455,"t":1794376617000,"rx":1794376647774},{"id":"EAL-6029","dir":1,"lat":24.201313,"lon":120.581048,"t":1794376618000,"rx":1794376647774},{"id":"EAR-1003","dir":0,"lat":24.164546,"lon":120.64575,"t":1794376620000,"rx":1794376647774},{"id":"EAR-1015","dir":1,"lat":24.214539,"lon":120.58092,"t":1794376619000,"rx":1794376647774},{"id":"EAR-1017","dir":0,"lat":24.150964,"lon":120.669534,"t":1794376609000,"rx":1794376647774},{"id":"FAE-800","dir":0,"lat":24.240876,"lon":120.564206,"t":1794376622000,"rx":1794376647774},{"id":"757-U8","dir":0,"lat":24.261025,"lon":120.521969,"t":1794376651000,"rx":1794376671340},{"id":"762-U8","dir":0,"lat":24.259153,"lon":120.52209,"t":1794376648000,"rx":1794376671340},{"id":"763-U8","dir":0,"lat":24.141493,"lon":120.680653,"t":1794376644000,"rx":1794376671340},{"id":"780-U8","dir":0,"lat":24.238603,"lon":120.56704,"t":1794376636000,"rx":1794376671340},{"id":"782-U8","dir":0,"lat":24.294135,"lon":120.588989,"t":1794376640000,"rx":1794376671340},{"id":"EAL-1378","dir":0,"lat":24.174275,"lon":120.632216,"t":1794376642000,"rx":1794376671340},{"id":"EAL-1589","dir":0,"lat":24.252289,"lon":120.575175,"t":1794376652000,"rx":1794376671340},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.56417,"t":1794376649000,"rx":1794376671340},{"id":"EAL-2225","dir":0,"lat":24.206936,"lon":120.58126,"t":1794376654000,"rx":1794376671340},{"id":"EAL-2263","dir":1,"lat":24.182358,"lon":120.595511,"t":1794376646000,"rx":1794376671340},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376652000,"rx":1794376671340},{"id":"EAL-2269","dir":1,"lat":24.247918,"lon":120.5751,"t":1794376645000,"rx":1794376671340},{"id":"EAL-5358","dir":1,"lat":24.155598,"lon":120.662869,"t":1794376635000,"rx":1794376671340},{"id":"EAL-5365","dir":0,"lat":24.23254,"lon":120.572275,"t":1794376644000,"rx":1794376671340},{"id":"EAL-5390","dir":1,"lat":24.18232,"lon":120.595984,"t":1794376644000,"rx":1794376671340},{"id":"EAL-6027","dir":0,"lat":24.264818,"lon":120.540996,"t":1794376642000,"rx":1794376671340},{"id":"EAL-6028","dir":1,"lat":24.155373,"lon":120.663214,"t":1794376651000,"rx":1794376671340},{"id":"EAL-6029","dir":1,"lat":24.20137,"lon":120.581056,"t":1794376647000,"rx":1794376671340},{"id":"EAR-1003","dir":0,"lat":24.164186,"lon":120.646543,"t":1794376642000,"rx":1794376671340},{"id":"EAR-1015","dir":1,"lat":24.215931,"lon":120.58073,"t":1794376641000,"rx":1794376671340},{"id":"EAR-1017","dir":0,"lat":24.150628,"lon":120.669958,"t":1794376650000,"rx":1794376671340},{"id":"FAE-800","dir":0,"lat":24.240876,"lon":120.564205,"t":1794376642000,"rx":1794376671340},{"id":"762-U8","dir":0,"lat":24.259153,"lon":120.522106,"t":1794376668000,"rx":1794376695400},{"id":"763-U8","dir":0,"lat":24.140613,"lon":120.681533,"t":1794376664000,"rx":1794376695400},{"id":"780-U8","dir":0,"lat":24.23722,"lon":120.569346,"t":1794376656000,"rx":1794376695400},{"id":"782-U8","dir":0,"lat":24.294125,"lon":120.588968,"t":1794376660000,"rx":1794376695400},{"id":"EAL-1378","dir":0,"lat":24.172444,"lon":120.634598,"t":1794376662000,"rx":1794376695400},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564186,"t":1794376669000,"rx":1794376695400},{"id":"EAL-2263","dir":1,"lat":24.182823,"lon":120.593866,"t":1794376666000,"rx":1794376695400},{"id":"EAL-2269","dir":1,"lat":24.24792,"lon":120.5751,"t":1794376665000,"rx":1794376695400},{"id":"EAL-5358","dir":1,"lat":24.15615,"lon":120.661595,"t":1794376668000,"rx":1794376695400},{"id":"EAL-5365","dir":0,"lat":24.232538,"lon":120.572276,"t":1794376664000,"rx":1794376695400},{"id":"EAL-5390","dir":1,"lat":24.182745,"lon":120.594243,"t":1794376664000,"rx":1794376695400},{"id":"EAL-6027","dir":0,"lat":24.2648,"lon":120.541003,"t":1794376662000,"rx":1794376695400},{"id":"EAL-6028","dir":1,"lat":24.155468,"lon":120.663145,"t":1794376657000,"rx":1794376695400},{"id":"EAL-6029","dir":1,"lat":24.202396,"lon":120.581154,"t":1794376658000,"rx":1794376695400},{"id":"EAR-1003","dir":0,"lat":24.163068,"lon":120.648516,"t":1794376660000,"rx":1794376695400},{"id":"EAR-1015","dir":1,"lat":24.216956,"lon":120.580596,"t":1794376659000,"rx":1794376695400},{"id":"EAR-1017","dir":0,"lat":24.149316,"lon":120.671526,"t":1794376669000,"rx":1794376695400},{"id":"FAE-800","dir":0,"lat":24.240874,"lon":120.564201,"t":1794376662000,"rx":1794376695400},{"id":"757-U8","dir":0,"lat":24.260976,"lon":120.521945,"t":1794376691000,"rx":1794376719385},{"id":"762-U8","dir":0,"lat":24.259164,"lon":120.522069,"t":1794376688000,"rx":1794376719385},{"id":"763-U8","dir":0,"lat":24.140006,"lon":120.682253,"t":1794376684000,"rx":1794376719385},{"id":"780-U8","dir":0,"lat":24.232726,"lon":120.572253,"t":1794376696000,"rx":1794376719385},{"id":"782-U8","dir":0,"lat":24.294131,"lon":120.588988,"t":1794376700000,"rx":1794376719385},{"id":"EAL-1378","dir":0,"lat":24.170911,"lon":120.636578,"t":1794376682000,"rx":1794376719385},{"id":"EAL-1589","dir":0,"lat":24.248513,"lon":120.575446,"t":1794376692000,"rx":1794376719385},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564171,"t":1794376689000,"rx":1794376719385},{"id":"EAL-2225","dir":0,"lat":24.201278,"lon":120.580626,"t":1794376694000,"rx":1794376719385},{"id":"EAL-2263","dir":1,"lat":24.183305,"lon":120.59274,"t":1794376700000,"rx":1794376719385},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576546,"t":1794376694000,"rx":1794376719385},{"id":"EAL-2269","dir":1,"lat":24.247941,"lon":120.575095,"t":1794376685000,"rx":1794376719385},{"id":"EAL-5358","dir":1,"lat":24.157593,"lon":120.659093,"t":1794376695000,"rx":1794376719385},{"id":"EAL-5365","dir":0,"lat":24.23254,"lon":120.572273,"t":1794376684000,"rx":1794376719385},{"id":"EAL-5390","dir":1,"lat":24.182891,"lon":120.593903,"t":1794376684000,"rx":1794376719385},{"id":"EAL-6027","dir":0,"lat":24.264801,"lon":120.541003,"t":1794376682000,"rx":1794376719385},{"id":"EAL-6028","dir":1,"lat":24.156418,"lon":120.661381,"t":1794376697000,"rx":1794376719385},{"id":"EAL-6029","dir":1,"lat":24.207958,"lon":120.581643,"t":1794376699000,"rx":1794376719385},{"id":"EAR-1003","dir":0,"lat":24.162626,"lon":120.649253,"t":1794376700000,"rx":1794376719385},{"id":"EAR-1015","dir":1,"lat":24.220396,"lon":120.580053,"t":1794376699000,"rx":1794376719385},{"id":"EAR-1017","dir":0,"lat":24.149201,"lon":120.671626,"t":1794376689000,"rx":1794376719385},{"id":"FAE-800","dir":0,"lat":24.240874,"lon":120.564218,"t":1794376682000,"rx":1794376719385},{"id":"757-U8","dir":0,"lat":24.260131,"lon":120.521685,"t":1794376711000,"rx":1794376743335},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522068,"t":1794376708000,"rx":1794376743335},{"id":"763-U8","dir":0,"lat":24.139506,"lon":120.682826,"t":1794376710000,"rx":1794376743335},{"id":"EAL-1378","dir":0,"lat":24.169528,"lon":120.638428,"t":1794376702000,"rx":1794376743335},{"id":"EAL-1589","dir":0,"lat":24.248513,"lon":120.575446,"t":1794376712000,"rx":1794376743335},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564188,"t":1794376709000,"rx":1794376743335},{"id":"EAL-2225","dir":0,"lat":24.198254,"lon":120.580343,"t":1794376715000,"rx":1794376743335},{"id":"EAL-2263","dir":1,"lat":24.18353,"lon":120.592218,"t":1794376706000,"rx":1794376743335},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376714000,"rx":1794376743335},{"id":"EAL-2269","dir":1,"lat":24.247946,"lon":120.575101,"t":1794376705000,"rx":1794376743335},{"id":"EAL-5358","dir":1,"lat":24.157881,"lon":120.65858,"t":1794376713000,"rx":1794376743335},{"id":"EAL-5365","dir":0,"lat":24.23254,"lon":120.572275,"t":1794376704000,"rx":1794376743335},{"id":"EAL-5390","dir":1,"lat":24.183008,"lon":120.593548,"t":1794376704000,"rx":1794376743335},{"id":"EAL-6027","dir":0,"lat":24.26479,"lon":120.541006,"t":1794376702000,"rx":1794376743335},{"id":"EAL-6028","dir":1,"lat":24.156861,"lon":120.660589,"t":1794376704000,"rx":1794376743335},{"id":"EAL-6029","dir":1,"lat":24.208956,"lon":120.581563,"t":1794376711000,"rx":1794376743335},{"id":"EAR-1003","dir":0,"lat":24.162331,"lon":120.649703,"t":1794376705000,"rx":1794376743335},{"id":"EAR-1015","dir":1,"lat":24.221788,"lon":120.579483,"t":1794376714000,"rx":1794376743335},{"id":"EAR-1017","dir":0,"lat":24.14833,"lon":120.672634,"t":1794376709000,"rx":1794376743335},{"id":"FAE-800","dir":0,"lat":24.240874,"lon":120.564201,"t":1794376702000,"rx":1794376743335},{"id":"762-U8","dir":0,"lat":24.259171,"lon":120.522068,"t":1794376728000,"rx":1794376755658},{"id":"763-U8","dir":0,"lat":24.139119,"lon":120.683413,"t":1794376724000,"rx":1794376755658},{"id":"780-U8","dir":0,"lat":24.230636,"lon":120.573573,"t":1794376716000,"rx":1794376755658},{"id":"782-U8","dir":0,"lat":24.294146,"lon":120.588956,"t":1794376720000,"rx":1794376755658},{"id":"EAL-1378","dir":0,"lat":24.169181,"lon":120.638906,"t":1794376722000,"rx":1794376755658},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564171,"t":1794376729000,"rx":1794376755658},{"id":"EAL-2225","dir":0,"lat":24.196058,"lon":120.580173,"t":1794376729000,"rx":1794376755658},{"id":"EAL-2263","dir":1,"lat":24.184313,"lon":120.590353,"t":1794376727000,"rx":1794376755658},{"id":"EAL-2269","dir":1,"lat":24.247941,"lon":120.5751,"t":1794376725000,"rx":1794376755658},{"id":"EAL-5358","dir":1,"lat":24.157963,"lon":120.658408,"t":1794376715000,"rx":1794376755658},{"id":"EAL-5365","dir":0,"lat":24.232546,"lon":120.572283,"t":1794376724000,"rx":1794376755658},{"id":"EAL-5390","dir":1,"lat":24.183381,"lon":120.592601,"t":1794376724000,"rx":1794376755658},{"id":"EAL-6027","dir":0,"lat":24.264771,"lon":120.54101,"t":1794376722000,"rx":1794376755658},{"id":"EAL-6028","dir":1,"lat":24.157661,"lon":120.659163,"t":1794376721000,"rx":1794376755658},{"id":"EAL-6029","dir":1,"lat":24.209834,"lon":120.581448,"t":1794376727000,"rx":1794376755658},{"id":"EAR-1003","dir":0,"lat":24.160828,"lon":120.652636,"t":1794376728000,"rx":1794376755658},{"id":"EAR-1015","dir":1,"lat":24.223786,"lon":120.578218,"t":1794376729000,"rx":1794376755658},{"id":"EAR-1017","dir":0,"lat":24.147745,"lon":120.673276,"t":1794376729000,"rx":1794376755658},{"id":"FAE-800","dir":0,"lat":24.240873,"lon":120.564201,"t":1794376722000,"rx":1794376755658},{"id":"757-U8","dir":0,"lat":24.259758,"lon":120.522193,"t":1794376751000,"rx":1794376779667},{"id":"762-U8","dir":0,"lat":24.259176,"lon":120.522075,"t":1794376748000,"rx":1794376779667},{"id":"763-U8","dir":0,"lat":24.13894,"lon":120.683639,"t":1794376744000,"rx":1794376779667},{"id":"780-U8","dir":0,"lat":24.230066,"lon":120.573946,"t":1794376756000,"rx":1794376779667},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.588951,"t":1794376760000,"rx":1794376779667},{"id":"EAL-1378","dir":0,"lat":24.169154,"lon":120.638963,"t":1794376742000,"rx":1794376779667},{"id":"EAL-1589","dir":0,"lat":24.248513,"lon":120.575446,"t":1794376752000,"rx":1794376779667},{"id":"EAL-2032","dir":0,"lat":24.240134,"lon":120.564188,"t":1794376749000,"rx":1794376779667},{"id":"EAL-2225","dir":0,"lat":24.19299,"lon":120.579803,"t":1794376754000,"rx":1794376779667},{"id":"EAL-2263","dir":1,"lat":24.185123,"lon":120.588466,"t":1794376746000,"rx":1794376779667},{"id":"EAL-2265","dir":1,"lat":24.226493,"lon":120.576563,"t":1794376754000,"rx":1794376779667},{"id":"EAL-2269","dir":1,"lat":24.247933,"lon":120.5751,"t":1794376745000,"rx":1794376779667},{"id":"EAL-5358","dir":1,"lat":24.15818,"lon":120.658019,"t":1794376755000,"rx":1794376779667},{"id":"EAL-5365","dir":0,"lat":24.232545,"lon":120.572291,"t":1794376744000,"rx":1794376779667},{"id":"EAL-5390","dir":1,"lat":24.184841,"lon":120.589183,"t":1794376752000,"rx":1794376779667},{"id":"EAL-6027","dir":0,"lat":24.264773,"lon":120.541011,"t":1794376742000,"rx":1794376779667},{"id":"EAL-6028","dir":1,"lat":24.158854,"lon":120.657353,"t":1794376757000,"rx":1794376779667},{"id":"EAL-6029","dir":1,"lat":24.214013,"lon":120.580951,"t":1794376759000,"rx":1794376779667},{"id":"EAR-1003","dir":0,"lat":24.160783,"lon":120.652821,"t":1794376760000,"rx":1794376779667},{"id":"EAR-1015","dir":1,"lat":24.225374,"lon":120.577185,"t":1794376759000,"rx":1794376779667},{"id":"EAR-1017","dir":0,"lat":24.147743,"lon":120.673261,"t":1794376749000,"rx":1794376779667},{"id":"FAE-800","dir":0,"lat":24.240873,"lon":120.564218,"t":1794376742000,"rx":1794376779667},{"id":"757-U8","dir":0,"lat":24.259591,"lon":120.522803,"t":1794376771000,"rx":1794376803458},{"id":"762-U8","dir":0,"lat":24.259174,"lon":120.522064,"t":1794376768000,"rx":1794376803458},{"id":"763-U8","dir":0,"lat":24.138649,"lon":120.683946,"t":1794376764000,"rx":1794376803458},{"id":"EAL-1378","dir":0,"lat":24.169154,"lon":120.638968,"t":1794376762000,"rx":1794376803458},{"id":"EAL-1589","dir":0,"lat":24.2482,"lon":120.575311,"t":1794376772000,"rx":1794376803458},{"id":"EAL-2032","dir":0,"lat":24.240136,"lon":120.564171,"t":1794376769000,"rx":1794376803458},{"id":"EAL-2225","dir":0,"lat":24.192746,"lon":120.579786,"t":1794376774000,"rx":1794376803458},{"id":"EAL-2263","dir":1,"lat":24.185289,"lon":120.588091,"t":1794376766000,"rx":1794376803458},{"id":"EAL-2265","dir":1,"lat":24.226491,"lon":120.576554,"t":1794376774000,"rx":1794376803458},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575093,"t":1794376765000,"rx":1794376803458},{"id":"EAL-5365","dir":0,"lat":24.232533,"lon":120.572306,"t":1794376764000,"rx":1794376803458},{"id":"EAL-5390","dir":1,"lat":24.185218,"lon":120.58831,"t":1794376764000,"rx":1794376803458},{"id":"EAL-6027","dir":0,"lat":24.264773,"lon":120.541013,"t":1794376762000,"rx":1794376803458},{"id":"EAL-6029","dir":1,"lat":24.21415,"lon":120.580936,"t":1794376761000,"rx":1794376803458},{"id":"EAR-1017","dir":0,"lat":24.147741,"lon":120.673259,"t":1794376769000,"rx":1794376803458},{"id":"FAE-800","dir":0,"lat":24.240873,"lon":120.564199,"t":1794376762000,"rx":1794376803458},{"id":"762-U8","dir":0,"lat":24.259174,"lon":120.522081,"t":1794376788000,"rx":1794376827482},{"id":"763-U8","dir":0,"lat":24.138546,"lon":120.684026,"t":1794376804000,"rx":1794376827482},{"id":"780-U8","dir":0,"lat":24.230083,"lon":120.573959,"t":1794376796000,"rx":1794376827482},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.58897,"t":1794376800000,"rx":1794376827482},{"id":"EAL-1378","dir":0,"lat":24.169045,"lon":120.639165,"t":1794376802000,"rx":1794376827482},{"id":"EAL-1589","dir":0,"lat":24.24696,"lon":120.574446,"t":1794376792000,"rx":1794376827482},{"id":"EAL-2032","dir":0,"lat":24.240136,"lon":120.564188,"t":1794376789000,"rx":1794376827482},{"id":"EAL-2225","dir":0,"lat":24.192741,"lon":120.579791,"t":1794376794000,"rx":1794376827482},{"id":"EAL-2263","dir":1,"lat":24.185304,"lon":120.588075,"t":1794376786000,"rx":1794376827482},{"id":"EAL-2265","dir":1,"lat":24.226511,"lon":120.576594,"t":1794376794000,"rx":1794376827482},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575093,"t":1794376805000,"rx":1794376827482},{"id":"EAL-5358","dir":1,"lat":24.158188,"lon":120.658011,"t":1794376795000,"rx":1794376827482},{"id":"EAL-5365","dir":0,"lat":24.232533,"lon":120.572308,"t":1794376804000,"rx":1794376827482},{"id":"EAL-5390","dir":1,"lat":24.185214,"lon":120.588273,"t":1794376804000,"rx":1794376827482},{"id":"EAL-6027","dir":0,"lat":24.264775,"lon":120.541031,"t":1794376802000,"rx":1794376827482},{"id":"EAL-6028","dir":1,"lat":24.158805,"lon":120.657425,"t":1794376797000,"rx":1794376827482},{"id":"EAL-6029","dir":1,"lat":24.214411,"lon":120.580923,"t":1794376798000,"rx":1794376827482},{"id":"EAR-1003","dir":0,"lat":24.160783,"lon":120.652839,"t":1794376800000,"rx":1794376827482},{"id":"EAR-1015","dir":1,"lat":24.225383,"lon":120.577179,"t":1794376799000,"rx":1794376827482},{"id":"EAR-1017","dir":0,"lat":24.147751,"lon":120.673294,"t":1794376789000,"rx":1794376827482},{"id":"FAE-800","dir":0,"lat":24.240871,"lon":120.564198,"t":1794376802000,"rx":1794376827482},{"id":"762-U8","dir":0,"lat":24.259163,"lon":120.522094,"t":1794376828000,"rx":1794376851441},{"id":"763-U8","dir":0,"lat":24.137689,"lon":120.684666,"t":1794376824000,"rx":1794376851441},{"id":"780-U8","dir":0,"lat":24.229423,"lon":120.574359,"t":1794376816000,"rx":1794376851441},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.588953,"t":1794376820000,"rx":1794376851441},{"id":"EAL-1378","dir":0,"lat":24.167016,"lon":120.641748,"t":1794376833000,"rx":1794376851441},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794376832000,"rx":1794376851441},{"id":"EAL-2032","dir":0,"lat":24.240136,"lon":120.564189,"t":1794376829000,"rx":1794376851441},{"id":"EAL-2225","dir":0,"lat":24.189148,"lon":120.579951,"t":1794376834000,"rx":1794376851441},{"id":"EAL-2263","dir":1,"lat":24.185893,"lon":120.586719,"t":1794376826000,"rx":1794376851441},{"id":"EAL-2265","dir":1,"lat":24.226503,"lon":120.576581,"t":1794376834000,"rx":1794376851441},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575109,"t":1794376825000,"rx":1794376851441},{"id":"EAL-5358","dir":1,"lat":24.158725,"lon":120.65693,"t":1794376824000,"rx":1794376851441},{"id":"EAL-5365","dir":0,"lat":24.232533,"lon":120.572325,"t":1794376824000,"rx":1794376851441},{"id":"EAL-5390","dir":1,"lat":24.185928,"lon":120.586759,"t":1794376833000,"rx":1794376851441},{"id":"EAL-6027","dir":0,"lat":24.264776,"lon":120.541008,"t":1794376822000,"rx":1794376851441},{"id":"EAL-6028","dir":1,"lat":24.15894,"lon":120.656323,"t":1794376834000,"rx":1794376851441},{"id":"EAL-6029","dir":1,"lat":24.216801,"lon":120.580606,"t":1794376832000,"rx":1794376851441},{"id":"EAR-1003","dir":0,"lat":24.160783,"lon":120.652823,"t":1794376820000,"rx":1794376851441},{"id":"EAR-1015","dir":1,"lat":24.225455,"lon":120.577134,"t":1794376819000,"rx":1794376851441},{"id":"EAR-1017","dir":0,"lat":24.146336,"lon":120.675018,"t":1794376829000,"rx":1794376851441},{"id":"FAE-800","dir":0,"lat":24.240871,"lon":120.564196,"t":1794376822000,"rx":1794376851441},{"id":"762-U8","dir":0,"lat":24.25916,"lon":120.522083,"t":1794376848000,"rx":1794376875416},{"id":"763-U8","dir":0,"lat":24.137706,"lon":120.686253,"t":1794376844000,"rx":1794376875416},{"id":"780-U8","dir":0,"lat":24.227293,"lon":120.57572,"t":1794376836000,"rx":1794376875416},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.58897,"t":1794376840000,"rx":1794376875416},{"id":"EAL-1378","dir":0,"lat":24.166176,"lon":120.642803,"t":1794376845000,"rx":1794376875416},{"id":"EAL-2032","dir":0,"lat":24.240136,"lon":120.564173,"t":1794376849000,"rx":1794376875416},{"id":"EAL-2263","dir":1,"lat":24.186043,"lon":120.586295,"t":1794376846000,"rx":1794376875416},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575093,"t":1794376845000,"rx":1794376875416},{"id":"EAL-5358","dir":1,"lat":24.159098,"lon":120.6563,"t":1794376835000,"rx":1794376875416},{"id":"EAL-5365","dir":0,"lat":24.232533,"lon":120.572308,"t":1794376844000,"rx":1794376875416},{"id":"EAL-5390","dir":1,"lat":24.186031,"lon":120.586528,"t":1794376844000,"rx":1794376875416},{"id":"EAL-6027","dir":0,"lat":24.26478,"lon":120.54101,"t":1794376842000,"rx":1794376875416},{"id":"EAL-6028","dir":1,"lat":24.159038,"lon":120.656324,"t":1794376848000,"rx":1794376875416},{"id":"EAL-6029","dir":1,"lat":24.216819,"lon":120.580603,"t":1794376838000,"rx":1794376875416},{"id":"EAR-1003","dir":0,"lat":24.159658,"lon":120.654791,"t":1794376840000,"rx":1794376875416},{"id":"EAR-1015","dir":1,"lat":24.226988,"lon":120.576068,"t":1794376839000,"rx":1794376875416},{"id":"EAR-1017","dir":0,"lat":24.146116,"lon":120.675308,"t":1794376849000,"rx":1794376875416},{"id":"FAE-800","dir":0,"lat":24.240871,"lon":120.564213,"t":1794376842000,"rx":1794376875416},{"id":"762-U8","dir":0,"lat":24.25916,"lon":120.522088,"t":1794376868000,"rx":1794376899397},{"id":"763-U8","dir":0,"lat":24.137916,"lon":120.686639,"t":1794376864000,"rx":1794376899397},{"id":"780-U8","dir":0,"lat":24.226716,"lon":120.576053,"t":1794376876000,"rx":1794376899397},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.58897,"t":1794376880000,"rx":1794376899397},{"id":"EAL-1378","dir":0,"lat":24.166006,"lon":120.643081,"t":1794376862000,"rx":1794376899397},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794376872000,"rx":1794376899397},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564175,"t":1794376869000,"rx":1794376899397},{"id":"EAL-2225","dir":0,"lat":24.186375,"lon":120.585104,"t":1794376874000,"rx":1794376899397},{"id":"EAL-2263","dir":1,"lat":24.186724,"lon":120.584766,"t":1794376866000,"rx":1794376899397},{"id":"EAL-2265","dir":1,"lat":24.226498,"lon":120.576606,"t":1794376874000,"rx":1794376899397},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575109,"t":1794376865000,"rx":1794376899397},{"id":"EAL-5358","dir":1,"lat":24.160986,"lon":120.652951,"t":1794376875000,"rx":1794376899397},{"id":"EAL-5365","dir":0,"lat":24.232531,"lon":120.572308,"t":1794376864000,"rx":1794376899397},{"id":"EAL-5390","dir":1,"lat":24.186543,"lon":120.585356,"t":1794376876000,"rx":1794376899397},{"id":"EAL-6027","dir":0,"lat":24.264781,"lon":120.541011,"t":1794376862000,"rx":1794376899397},{"id":"EAL-6028","dir":1,"lat":24.160531,"lon":120.653726,"t":1794376877000,"rx":1794376899397},{"id":"EAL-6029","dir":1,"lat":24.21997,"lon":120.580176,"t":1794376878000,"rx":1794376899397},{"id":"EAR-1003","dir":0,"lat":24.158336,"lon":120.65712,"t":1794376880000,"rx":1794376899397},{"id":"EAR-1015","dir":1,"lat":24.227081,"lon":120.576023,"t":1794376879000,"rx":1794376899397},{"id":"EAR-1017","dir":0,"lat":24.144203,"lon":120.677463,"t":1794376878000,"rx":1794376899397},{"id":"FAE-800","dir":0,"lat":24.240871,"lon":120.564196,"t":1794376862000,"rx":1794376899397},{"id":"762-U8","dir":0,"lat":24.25916,"lon":120.522104,"t":1794376888000,"rx":1794376923520},{"id":"763-U8","dir":0,"lat":24.13792,"lon":120.686746,"t":1794376884000,"rx":1794376923520},{"id":"EAL-1378","dir":0,"lat":24.166006,"lon":120.643081,"t":1794376882000,"rx":1794376923520},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794376892000,"rx":1794376923520},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564191,"t":1794376889000,"rx":1794376923520},{"id":"EAL-2225","dir":0,"lat":24.185933,"lon":120.586074,"t":1794376894000,"rx":1794376923520},{"id":"EAL-2263","dir":1,"lat":24.187731,"lon":120.582343,"t":1794376886000,"rx":1794376923520},{"id":"EAL-2265","dir":1,"lat":24.226494,"lon":120.57661,"t":1794376894000,"rx":1794376923520},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575093,"t":1794376885000,"rx":1794376923520},{"id":"EAL-5358","dir":1,"lat":24.161829,"lon":120.65142,"t":1794376886000,"rx":1794376923520},{"id":"EAL-5365","dir":0,"lat":24.232531,"lon":120.572325,"t":1794376884000,"rx":1794376923520},{"id":"EAL-5390","dir":1,"lat":24.187061,"lon":120.584166,"t":1794376884000,"rx":1794376923520},{"id":"EAL-6027","dir":0,"lat":24.264781,"lon":120.541028,"t":1794376882000,"rx":1794376923520},{"id":"EAL-6028","dir":1,"lat":24.160889,"lon":120.653059,"t":1794376882000,"rx":1794376923520},{"id":"EAL-6029","dir":1,"lat":24.221031,"lon":120.579779,"t":1794376891000,"rx":1794376923520},{"id":"EAR-1003","dir":0,"lat":24.158045,"lon":120.657661,"t":1794376887000,"rx":1794376923520},{"id":"EAR-1017","dir":0,"lat":24.143429,"lon":120.678323,"t":1794376890000,"rx":1794376923520},{"id":"FAE-800","dir":0,"lat":24.24087,"lon":120.564196,"t":1794376882000,"rx":1794376923520},{"id":"762-U8","dir":0,"lat":24.25916,"lon":120.522088,"t":1794376908000,"rx":1794376947815},{"id":"763-U8","dir":1,"lat":24.13813,"lon":120.687386,"t":1794376924000,"rx":1794376947815},{"id":"780-U8","dir":0,"lat":24.226736,"lon":120.57608,"t":1794376916000,"rx":1794376947815},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.58897,"t":1794376920000,"rx":1794376947815},{"id":"EAL-1378","dir":0,"lat":24.16465,"lon":120.6456,"t":1794376922000,"rx":1794376947815},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794376912000,"rx":1794376947815},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564175,"t":1794376909000,"rx":1794376947815},{"id":"EAL-2225","dir":0,"lat":24.184936,"lon":120.588223,"t":1794376914000,"rx":1794376947815},{"id":"EAL-2263","dir":1,"lat":24.188598,"lon":120.580678,"t":1794376906000,"rx":1794376947815},{"id":"EAL-2265","dir":1,"lat":24.226496,"lon":120.576608,"t":1794376914000,"rx":1794376947815},{"id":"EAL-2269","dir":1,"lat":24.247931,"lon":120.575095,"t":1794376925000,"rx":1794376947815},{"id":"EAL-5358","dir":1,"lat":24.162653,"lon":120.649993,"t":1794376915000,"rx":1794376947815},{"id":"EAL-5365","dir":0,"lat":24.232531,"lon":120.572325,"t":1794376924000,"rx":1794376947815},{"id":"EAL-5390","dir":1,"lat":24.188738,"lon":120.580673,"t":1794376924000,"rx":1794376947815},{"id":"EAL-6027","dir":0,"lat":24.264793,"lon":120.541008,"t":1794376922000,"rx":1794376947815},{"id":"EAL-6028","dir":1,"lat":24.162571,"lon":120.650059,"t":1794376917000,"rx":1794376947815},{"id":"EAL-6029","dir":1,"lat":24.222971,"lon":120.578708,"t":1794376918000,"rx":1794376947815},{"id":"EAR-1003","dir":0,"lat":24.157233,"lon":120.659183,"t":1794376920000,"rx":1794376947815},{"id":"EAR-1015","dir":1,"lat":24.227118,"lon":120.57553,"t":1794376919000,"rx":1794376947815},{"id":"EAR-1017","dir":0,"lat":24.143364,"lon":120.678411,"t":1794376909000,"rx":1794376947815},{"id":"FAE-800","dir":0,"lat":24.240868,"lon":120.564193,"t":1794376922000,"rx":1794376947815},{"id":"762-U8","dir":0,"lat":24.25916,"lon":120.522104,"t":1794376928000,"rx":1794376959388},{"id":"780-U8","dir":0,"lat":24.226736,"lon":120.57608,"t":1794376936000,"rx":1794376959388},{"id":"782-U8","dir":0,"lat":24.294143,"lon":120.588953,"t":1794376940000,"rx":1794376959388},{"id":"EAL-1378","dir":0,"lat":24.164538,"lon":120.645826,"t":1794376926000,"rx":1794376959388},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794376932000,"rx":1794376959388},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564191,"t":1794376929000,"rx":1794376959388},{"id":"EAL-2225","dir":0,"lat":24.184311,"lon":120.589801,"t":1794376934000,"rx":1794376959388},{"id":"EAL-2263","dir":1,"lat":24.188826,"lon":120.580483,"t":1794376926000,"rx":1794376959388},{"id":"EAL-2265","dir":1,"lat":24.226496,"lon":120.576625,"t":1794376934000,"rx":1794376959388},{"id":"EAL-5358","dir":1,"lat":24.162659,"lon":120.649979,"t":1794376935000,"rx":1794376959388},{"id":"EAL-6028","dir":1,"lat":24.162576,"lon":120.650028,"t":1794376937000,"rx":1794376959388},{"id":"EAL-6029","dir":1,"lat":24.223224,"lon":120.578523,"t":1794376938000,"rx":1794376959388},{"id":"EAR-1003","dir":0,"lat":24.157223,"lon":120.659226,"t":1794376940000,"rx":1794376959388},{"id":"EAR-1015","dir":1,"lat":24.227053,"lon":120.575463,"t":1794376939000,"rx":1794376959388},{"id":"EAR-1017","dir":0,"lat":24.143364,"lon":120.678415,"t":1794376929000,"rx":1794376959388},{"id":"763-U8","dir":1,"lat":24.138123,"lon":120.687386,"t":1794376944000,"rx":1794376983366},{"id":"EAL-1378","dir":0,"lat":24.164456,"lon":120.645981,"t":1794376942000,"rx":1794376983366},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794376952000,"rx":1794376983366},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564175,"t":1794376949000,"rx":1794376983366},{"id":"EAL-2225","dir":0,"lat":24.183708,"lon":120.591168,"t":1794376954000,"rx":1794376983366},{"id":"EAL-2263","dir":1,"lat":24.188828,"lon":120.580488,"t":1794376946000,"rx":1794376983366},{"id":"EAL-2265","dir":1,"lat":24.226496,"lon":120.576606,"t":1794376954000,"rx":1794376983366},{"id":"EAL-2269","dir":1,"lat":24.247918,"lon":120.575093,"t":1794376945000,"rx":1794376983366},{"id":"EAL-5365","dir":0,"lat":24.232531,"lon":120.572308,"t":1794376944000,"rx":1794376983366},{"id":"EAL-5390","dir":1,"lat":24.188778,"lon":120.580641,"t":1794376944000,"rx":1794376983366},{"id":"EAL-6027","dir":0,"lat":24.264803,"lon":120.541004,"t":1794376942000,"rx":1794376983366},{"id":"EAR-1015","dir":1,"lat":24.226786,"lon":120.574853,"t":1794376953000,"rx":1794376983366},{"id":"EAR-1017","dir":0,"lat":24.143348,"lon":120.678443,"t":1794376949000,"rx":1794376983366},{"id":"FAE-800","dir":0,"lat":24.240845,"lon":120.564183,"t":1794376942000,"rx":1794376983366},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.522088,"t":1794377088000,"rx":1794377115919},{"id":"763-U8","dir":1,"lat":24.140153,"lon":120.687826,"t":1794377084000,"rx":1794377115919},{"id":"780-U8","dir":0,"lat":24.221283,"lon":120.57948,"t":1794377080000,"rx":1794377115919},{"id":"782-U8","dir":0,"lat":24.294131,"lon":120.588994,"t":1794377080000,"rx":1794377115919},{"id":"EAL-1378","dir":0,"lat":24.161255,"lon":120.65192,"t":1794377085000,"rx":1794377115919},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794377072000,"rx":1794377115919},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564176,"t":1794377089000,"rx":1794377115919},{"id":"EAL-2225","dir":0,"lat":24.182573,"lon":120.594026,"t":1794377074000,"rx":1794377115919},{"id":"EAL-2263","dir":1,"lat":24.203465,"lon":120.581251,"t":1794377086000,"rx":1794377115919},{"id":"EAL-2265","dir":1,"lat":24.226513,"lon":120.576589,"t":1794377074000,"rx":1794377115919},{"id":"EAL-2269","dir":1,"lat":24.247913,"lon":120.57509,"t":1794377085000,"rx":1794377115919},{"id":"EAL-5358","dir":1,"lat":24.165793,"lon":120.644154,"t":1794377075000,"rx":1794377115919},{"id":"EAL-5365","dir":0,"lat":24.232495,"lon":120.572294,"t":1794377084000,"rx":1794377115919},{"id":"EAL-5390","dir":1,"lat":24.201066,"lon":120.580955,"t":1794377084000,"rx":1794377115919},{"id":"EAL-6027","dir":0,"lat":24.264828,"lon":120.541008,"t":1794377082000,"rx":1794377115919},{"id":"EAL-6028","dir":1,"lat":24.165735,"lon":120.644303,"t":1794377077000,"rx":1794377115919},{"id":"EAR-1003","dir":0,"lat":24.153278,"lon":120.666368,"t":1794377080000,"rx":1794377115919},{"id":"EAR-1015","dir":0,"lat":24.2251,"lon":120.568934,"t":1794377079000,"rx":1794377115919},{"id":"EAR-1017","dir":0,"lat":24.140276,"lon":120.681988,"t":1794377089000,"rx":1794377115919},{"id":"FAE-800","dir":0,"lat":24.240926,"lon":120.564236,"t":1794377082000,"rx":1794377115919},{"id":"763-U8","dir":1,"lat":24.140326,"lon":120.687773,"t":1794377104000,"rx":1794377127426},{"id":"780-U8","dir":0,"lat":24.219966,"lon":120.579946,"t":1794377096000,"rx":1794377127426},{"id":"782-U8","dir":0,"lat":24.294139,"lon":120.589008,"t":1794377100000,"rx":1794377127426},{"id":"EAL-1378","dir":0,"lat":24.160671,"lon":120.652896,"t":1794377102000,"rx":1794377127426},{"id":"EAL-1589","dir":0,"lat":24.246931,"lon":120.574423,"t":1794377092000,"rx":1794377127426},{"id":"EAL-2225","dir":0,"lat":24.18205,"lon":120.595993,"t":1794377094000,"rx":1794377127426},{"id":"EAL-2265","dir":1,"lat":24.226521,"lon":120.576613,"t":1794377094000,"rx":1794377127426},{"id":"EAL-2269","dir":1,"lat":24.24791,"lon":120.575074,"t":1794377105000,"rx":1794377127426},{"id":"EAL-5358","dir":1,"lat":24.165874,"lon":120.643961,"t":1794377095000,"rx":1794377127426},{"id":"EAL-5365","dir":0,"lat":24.23251,"lon":120.572306,"t":1794377104000,"rx":1794377127426},{"id":"EAL-5390","dir":1,"lat":24.204351,"lon":120.581325,"t":1794377104000,"rx":1794377127426},{"id":"EAL-6027","dir":0,"lat":24.264829,"lon":120.541008,"t":1794377102000,"rx":1794377127426},{"id":"EAL-6028","dir":1,"lat":24.165896,"lon":120.644094,"t":1794377097000,"rx":1794377127426},{"id":"EAR-1003","dir":0,"lat":24.153148,"lon":120.666496,"t":1794377100000,"rx":1794377127426},{"id":"EAR-1015","dir":0,"lat":24.2251,"lon":120.568951,"t":1794377099000,"rx":1794377127426},{"id":"FAE-800","dir":0,"lat":24.240871,"lon":120.56435,"t":1794377102000,"rx":1794377127426},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.522088,"t":1794377128000,"rx":1794377151847},{"id":"763-U8","dir":1,"lat":24.140326,"lon":120.686626,"t":1794377124000,"rx":1794377151847},{"id":"780-U8","dir":0,"lat":24.219886,"lon":120.580013,"t":1794377116000,"rx":1794377151847},{"id":"782-U8","dir":0,"lat":24.294139,"lon":120.589023,"t":1794377120000,"rx":1794377151847},{"id":"EAL-1378","dir":0,"lat":24.160671,"lon":120.652896,"t":1794377122000,"rx":1794377151847},{"id":"EAL-1589","dir":0,"lat":24.246611,"lon":120.574256,"t":1794377132000,"rx":1794377151847},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564176,"t":1794377129000,"rx":1794377151847},{"id":"EAL-2225","dir":0,"lat":24.182289,"lon":120.600741,"t":1794377134000,"rx":1794377151847},{"id":"EAL-2263","dir":1,"lat":24.208778,"lon":120.581586,"t":1794377126000,"rx":1794377151847},{"id":"EAL-2265","dir":1,"lat":24.226514,"lon":120.576594,"t":1794377134000,"rx":1794377151847},{"id":"EAL-2269","dir":1,"lat":24.247896,"lon":120.575069,"t":1794377125000,"rx":1794377151847},{"id":"EAL-5358","dir":1,"lat":24.16588,"lon":120.643938,"t":1794377115000,"rx":1794377151847},{"id":"EAL-5365","dir":0,"lat":24.232508,"lon":120.572306,"t":1794377124000,"rx":1794377151847},{"id":"EAL-5390","dir":1,"lat":24.207943,"lon":120.581646,"t":1794377128000,"rx":1794377151847},{"id":"EAL-6027","dir":0,"lat":24.264816,"lon":120.541003,"t":1794377122000,"rx":1794377151847},{"id":"EAL-6028","dir":1,"lat":24.165923,"lon":120.644144,"t":1794377117000,"rx":1794377151847},{"id":"EAR-1003","dir":0,"lat":24.153146,"lon":120.666503,"t":1794377120000,"rx":1794377151847},{"id":"EAR-1015","dir":0,"lat":24.2251,"lon":120.568934,"t":1794377119000,"rx":1794377151847},{"id":"EAR-1017","dir":0,"lat":24.139836,"lon":120.682601,"t":1794377129000,"rx":1794377151847},{"id":"FAE-800","dir":0,"lat":24.240888,"lon":120.564355,"t":1794377122000,"rx":1794377151847},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.522104,"t":1794377148000,"rx":1794377175464},{"id":"763-U8","dir":1,"lat":24.14111,"lon":120.686,"t":1794377145000,"rx":1794377175464},{"id":"780-U8","dir":0,"lat":24.219896,"lon":120.580039,"t":1794377136000,"rx":1794377175464},{"id":"782-U8","dir":0,"lat":24.294068,"lon":120.588994,"t":1794377140000,"rx":1794377175464},{"id":"EAL-1378","dir":0,"lat":24.160671,"lon":120.652896,"t":1794377142000,"rx":1794377175464},{"id":"EAL-2032","dir":0,"lat":24.240138,"lon":120.564193,"t":1794377149000,"rx":1794377175464},{"id":"EAL-2225","dir":0,"lat":24.182565,"lon":120.60195,"t":1794377145000,"rx":1794377175464},{"id":"EAL-2263","dir":1,"lat":24.209076,"lon":120.581566,"t":1794377146000,"rx":1794377175464},{"id":"EAL-2269","dir":1,"lat":24.2479,"lon":120.575068,"t":1794377145000,"rx":1794377175464},{"id":"EAL-5358","dir":1,"lat":24.165883,"lon":120.64391,"t":1794377135000,"rx":1794377175464},{"id":"EAL-5365","dir":0,"lat":24.232508,"lon":120.572323,"t":1794377144000,"rx":1794377175464},{"id":"EAL-5390","dir":1,"lat":24.208853,"lon":120.581593,"t":1794377144000,"rx":1794377175464},{"id":"EAL-6027","dir":0,"lat":24.264811,"lon":120.541006,"t":1794377142000,"rx":1794377175464},{"id":"EAL-6028","dir":1,"lat":24.165919,"lon":120.64413,"t":1794377137000,"rx":1794377175464},{"id":"EAR-1003","dir":0,"lat":24.153146,"lon":120.66652,"t":1794377140000,"rx":1794377175464},{"id":"EAR-1015","dir":0,"lat":24.2251,"lon":120.568933,"t":1794377139000,"rx":1794377175464},{"id":"EAR-1017","dir":0,"lat":24.138944,"lon":120.683726,"t":1794377149000,"rx":1794377175464},{"id":"FAE-800","dir":0,"lat":24.240909,"lon":120.56434,"t":1794377142000,"rx":1794377175464},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.522088,"t":1794377168000,"rx":1794377199453},{"id":"763-U8","dir":1,"lat":24.142863,"lon":120.685919,"t":1794377164000,"rx":1794377199453},{"id":"780-U8","dir":0,"lat":24.219093,"lon":120.580213,"t":1794377177000,"rx":1794377199453},{"id":"782-U8","dir":0,"lat":24.290214,"lon":120.586794,"t":1794377180000,"rx":1794377199453},{"id":"EAL-1378","dir":0,"lat":24.160671,"lon":120.652896,"t":1794377162000,"rx":1794377199453},{"id":"EAL-1589","dir":0,"lat":24.243673,"lon":120.572356,"t":1794377172000,"rx":1794377199453},{"id":"EAL-2032","dir":0,"lat":24.24014,"lon":120.564176,"t":1794377169000,"rx":1794377199453},{"id":"EAL-2225","dir":0,"lat":24.182876,"lon":120.603341,"t":1794377174000,"rx":1794377199453},{"id":"EAL-2263","dir":1,"lat":24.209773,"lon":120.581495,"t":1794377170000,"rx":1794377199453},{"id":"EAL-2265","dir":1,"lat":24.226514,"lon":120.576594,"t":1794377174000,"rx":1794377199453},{"id":"EAL-2269","dir":1,"lat":24.247904,"lon":120.575079,"t":1794377165000,"rx":1794377199453},{"id":"EAL-5358","dir":1,"lat":24.166163,"lon":120.643469,"t":1794377175000,"rx":1794377199453},{"id":"EAL-5365","dir":0,"lat":24.232528,"lon":120.572313,"t":1794377164000,"rx":1794377199453},{"id":"EAL-5390","dir":1,"lat":24.209843,"lon":120.581481,"t":1794377175000,"rx":1794377199453},{"id":"EAL-6027","dir":0,"lat":24.264813,"lon":120.540994,"t":1794377162000,"rx":1794377199453},{"id":"EAL-6028","dir":1,"lat":24.16609,"lon":120.643808,"t":1794377177000,"rx":1794377199453},{"id":"EAR-1003","dir":0,"lat":24.153151,"lon":120.666516,"t":1794377180000,"rx":1794377199453},{"id":"EAR-1015","dir":0,"lat":24.225098,"lon":120.568931,"t":1794377179000,"rx":1794377199453},{"id":"EAR-1017","dir":0,"lat":24.138648,"lon":120.683904,"t":1794377172000,"rx":1794377199453},{"id":"FAE-800","dir":0,"lat":24.240901,"lon":120.564313,"t":1794377162000,"rx":1794377199453},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.522104,"t":1794377188000,"rx":1794377223419},{"id":"763-U8","dir":1,"lat":24.143103,"lon":120.685253,"t":1794377184000,"rx":1794377223419},{"id":"780-U8","dir":0,"lat":24.217826,"lon":120.580373,"t":1794377190000,"rx":1794377223419},{"id":"EAL-1378","dir":0,"lat":24.160658,"lon":120.652914,"t":1794377182000,"rx":1794377223419},{"id":"EAL-1589","dir":0,"lat":24.243333,"lon":120.572091,"t":1794377192000,"rx":1794377223419},{"id":"EAL-2032","dir":0,"lat":24.24014,"lon":120.564178,"t":1794377189000,"rx":1794377223419},{"id":"EAL-2225","dir":0,"lat":24.182888,"lon":120.603364,"t":1794377194000,"rx":1794377223419},{"id":"EAL-2263","dir":1,"lat":24.212306,"lon":120.581196,"t":1794377193000,"rx":1794377223419},{"id":"EAL-2265","dir":1,"lat":24.226513,"lon":120.576593,"t":1794377194000,"rx":1794377223419},{"id":"EAL-2269","dir":1,"lat":24.247904,"lon":120.575081,"t":1794377185000,"rx":1794377223419},{"id":"EAL-5358","dir":1,"lat":24.167581,"lon":120.641183,"t":1794377195000,"rx":1794377223419},{"id":"EAL-5365","dir":0,"lat":24.232526,"lon":120.572313,"t":1794377184000,"rx":1794377223419},{"id":"EAL-5390","dir":1,"lat":24.212106,"lon":120.581208,"t":1794377195000,"rx":1794377223419},{"id":"EAL-6027","dir":0,"lat":24.264813,"lon":120.541011,"t":1794377182000,"rx":1794377223419},{"id":"EAL-6028","dir":1,"lat":24.166756,"lon":120.642515,"t":1794377190000,"rx":1794377223419},{"id":"EAR-1017","dir":0,"lat":24.138168,"lon":120.68434,"t":1794377189000,"rx":1794377223419},{"id":"FAE-800","dir":0,"lat":24.240898,"lon":120.564313,"t":1794377182000,"rx":1794377223419},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.52208,"t":1794377208000,"rx":1794377247399},{"id":"763-U8","dir":1,"lat":24.141463,"lon":120.683546,"t":1794377224000,"rx":1794377247399},{"id":"780-U8","dir":0,"lat":24.217463,"lon":120.580386,"t":1794377216000,"rx":1794377247399},{"id":"782-U8","dir":0,"lat":24.287908,"lon":120.585496,"t":1794377220000,"rx":1794377247399},{"id":"EAL-1378","dir":0,"lat":24.159203,"lon":120.655598,"t":1794377222000,"rx":1794377247399},{"id":"EAL-1589","dir":0,"lat":24.24161,"lon":120.570949,"t":1794377212000,"rx":1794377247399},{"id":"EAL-2032","dir":0,"lat":24.24014,"lon":120.564194,"t":1794377209000,"rx":1794377247399},{"id":"EAL-2225","dir":0,"lat":24.182958,"lon":120.603605,"t":1794377214000,"rx":1794377247399},{"id":"EAL-2263","dir":1,"lat":24.215816,"lon":120.580791,"t":1794377217000,"rx":1794377247399},{"id":"EAL-2265","dir":1,"lat":24.226514,"lon":120.576593,"t":1794377214000,"rx":1794377247399},{"id":"EAL-2269","dir":1,"lat":24.247896,"lon":120.575093,"t":1794377225000,"rx":1794377247399},{"id":"EAL-5358","dir":1,"lat":24.168911,"lon":120.639558,"t":1794377215000,"rx":1794377247399},{"id":"EAL-5365","dir":0,"lat":24.232516,"lon":120.572328,"t":1794377224000,"rx":1794377247399},{"id":"EAL-5390","dir":1,"lat":24.216246,"lon":120.580699,"t":1794377224000,"rx":1794377247399},{"id":"EAL-6027","dir":0,"lat":24.264849,"lon":120.540998,"t":1794377222000,"rx":1794377247399},{"id":"EAL-6028","dir":1,"lat":24.168841,"lon":120.639849,"t":1794377224000,"rx":1794377247399},{"id":"EAR-1003","dir":0,"lat":24.15212,"lon":120.66803,"t":1794377221000,"rx":1794377247399},{"id":"EAR-1015","dir":0,"lat":24.225111,"lon":120.568906,"t":1794377219000,"rx":1794377247399},{"id":"EAR-1017","dir":0,"lat":24.138161,"lon":120.684306,"t":1794377209000,"rx":1794377247399},{"id":"FAE-800","dir":0,"lat":24.240935,"lon":120.564246,"t":1794377222000,"rx":1794377247399},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522086,"t":1794377248000,"rx":1794377271436},{"id":"763-U8","dir":1,"lat":24.14048,"lon":120.682573,"t":1794377244000,"rx":1794377271436},{"id":"780-U8","dir":0,"lat":24.214376,"lon":120.580746,"t":1794377250000,"rx":1794377271436},{"id":"782-U8","dir":0,"lat":24.287908,"lon":120.585513,"t":1794377240000,"rx":1794377271436},{"id":"EAL-1378","dir":0,"lat":24.158908,"lon":120.65621,"t":1794377253000,"rx":1794377271436},{"id":"EAL-1589","dir":0,"lat":24.238796,"lon":120.569443,"t":1794377252000,"rx":1794377271436},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564178,"t":1794377249000,"rx":1794377271436},{"id":"EAL-2225","dir":0,"lat":24.183603,"lon":120.606413,"t":1794377254000,"rx":1794377271436},{"id":"EAL-2263","dir":1,"lat":24.21692,"lon":120.580624,"t":1794377246000,"rx":1794377271436},{"id":"EAL-2265","dir":1,"lat":24.226516,"lon":120.576596,"t":1794377254000,"rx":1794377271436},{"id":"EAL-2269","dir":1,"lat":24.24789,"lon":120.575096,"t":1794377245000,"rx":1794377271436},{"id":"EAL-5358","dir":1,"lat":24.16927,"lon":120.639328,"t":1794377237000,"rx":1794377271436},{"id":"EAL-5365","dir":0,"lat":24.232514,"lon":120.572328,"t":1794377244000,"rx":1794377271436},{"id":"EAL-5390","dir":1,"lat":24.216725,"lon":120.580606,"t":1794377244000,"rx":1794377271436},{"id":"EAL-6027","dir":0,"lat":24.264836,"lon":120.541008,"t":1794377242000,"rx":1794377271436},{"id":"EAL-6028","dir":1,"lat":24.169251,"lon":120.639259,"t":1794377247000,"rx":1794377271436},{"id":"EAR-1003","dir":0,"lat":24.149999,"lon":120.670756,"t":1794377246000,"rx":1794377271436},{"id":"EAR-1015","dir":0,"lat":24.225111,"lon":120.568905,"t":1794377239000,"rx":1794377271436},{"id":"EAR-1017","dir":0,"lat":24.138161,"lon":120.684325,"t":1794377249000,"rx":1794377271436},{"id":"FAE-800","dir":0,"lat":24.240935,"lon":120.564234,"t":1794377242000,"rx":1794377271436},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522069,"t":1794377268000,"rx":1794377308017},{"id":"763-U8","dir":1,"lat":24.141756,"lon":120.680453,"t":1794377284000,"rx":1794377308017},{"id":"780-U8","dir":0,"lat":24.210399,"lon":120.581239,"t":1794377281000,"rx":1794377308017},{"id":"782-U8","dir":0,"lat":24.286198,"lon":120.584546,"t":1794377280000,"rx":1794377308017},{"id":"EAL-1378","dir":0,"lat":24.15818,"lon":120.657534,"t":1794377282000,"rx":1794377308017},{"id":"EAL-1589","dir":0,"lat":24.238251,"lon":120.56941,"t":1794377272000,"rx":1794377308017},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564179,"t":1794377269000,"rx":1794377308017},{"id":"EAL-2225","dir":0,"lat":24.184181,"lon":120.608883,"t":1794377274000,"rx":1794377308017},{"id":"EAL-2263","dir":1,"lat":24.219401,"lon":120.580326,"t":1794377272000,"rx":1794377308017},{"id":"EAL-2265","dir":1,"lat":24.226516,"lon":120.576613,"t":1794377274000,"rx":1794377308017},{"id":"EAL-2269","dir":1,"lat":24.24789,"lon":120.575098,"t":1794377285000,"rx":1794377308017},{"id":"EAL-5358","dir":1,"lat":24.172628,"lon":120.634491,"t":1794377275000,"rx":1794377308017},{"id":"EAL-5365","dir":0,"lat":24.232506,"lon":120.57231,"t":1794377284000,"rx":1794377308017},{"id":"EAL-5390","dir":1,"lat":24.219456,"lon":120.58028,"t":1794377284000,"rx":1794377308017},{"id":"EAL-6027","dir":0,"lat":24.264851,"lon":120.541004,"t":1794377282000,"rx":1794377308017},{"id":"EAL-6028","dir":1,"lat":24.171426,"lon":120.636343,"t":1794377277000,"rx":1794377308017},{"id":"EAR-1003","dir":0,"lat":24.149161,"lon":120.671756,"t":1794377280000,"rx":1794377308017},{"id":"EAR-1015","dir":0,"lat":24.22511,"lon":120.568921,"t":1794377279000,"rx":1794377308017},{"id":"EAR-1017","dir":0,"lat":24.137543,"lon":120.685136,"t":1794377275000,"rx":1794377308017},{"id":"FAE-800","dir":0,"lat":24.240936,"lon":120.564231,"t":1794377282000,"rx":1794377308017},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522086,"t":1794377288000,"rx":1794377331467},{"id":"780-U8","dir":0,"lat":24.210116,"lon":120.581253,"t":1794377296000,"rx":1794377331467},{"id":"782-U8","dir":0,"lat":24.283401,"lon":120.582939,"t":1794377300000,"rx":1794377331467},{"id":"EAL-1589","dir":0,"lat":24.238251,"lon":120.56941,"t":1794377292000,"rx":1794377331467},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564196,"t":1794377289000,"rx":1794377331467},{"id":"EAL-2225","dir":0,"lat":24.184373,"lon":120.609991,"t":1794377294000,"rx":1794377331467},{"id":"EAL-2263","dir":1,"lat":24.219521,"lon":120.580301,"t":1794377286000,"rx":1794377331467},{"id":"EAL-2265","dir":1,"lat":24.226516,"lon":120.576594,"t":1794377294000,"rx":1794377331467},{"id":"EAL-5358","dir":1,"lat":24.174855,"lon":120.631623,"t":1794377295000,"rx":1794377331467},{"id":"EAL-6028","dir":1,"lat":24.173076,"lon":120.634116,"t":1794377297000,"rx":1794377331467},{"id":"EAR-1003","dir":0,"lat":24.149128,"lon":120.671771,"t":1794377300000,"rx":1794377331467},{"id":"EAR-1015","dir":0,"lat":24.22511,"lon":120.568903,"t":1794377299000,"rx":1794377331467},{"id":"EAR-1017","dir":0,"lat":24.137813,"lon":120.686589,"t":1794377294000,"rx":1794377331467},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522069,"t":1794377308000,"rx":1794377343439},{"id":"763-U8","dir":1,"lat":24.142046,"lon":120.680093,"t":1794377304000,"rx":1794377343439},{"id":"EAL-1378","dir":0,"lat":24.15818,"lon":120.657534,"t":1794377302000,"rx":1794377343439},{"id":"EAL-1589","dir":0,"lat":24.238251,"lon":120.56941,"t":1794377312000,"rx":1794377343439},{"id":"EAL-2032","dir":0,"lat":24.240141,"lon":120.564179,"t":1794377309000,"rx":1794377343439},{"id":"EAL-2225","dir":0,"lat":24.184439,"lon":120.610288,"t":1794377314000,"rx":1794377343439},{"id":"EAL-2263","dir":1,"lat":24.219521,"lon":120.580318,"t":1794377306000,"rx":1794377343439},{"id":"EAL-2265","dir":1,"lat":24.226518,"lon":120.576593,"t":1794377314000,"rx":1794377343439},{"id":"EAL-2269","dir":1,"lat":24.247888,"lon":120.575098,"t":1794377305000,"rx":1794377343439},{"id":"EAL-5358","dir":1,"lat":24.176736,"lon":120.628311,"t":1794377315000,"rx":1794377343439},{"id":"EAL-5365","dir":0,"lat":24.232506,"lon":120.572311,"t":1794377304000,"rx":1794377343439},{"id":"EAL-5390","dir":1,"lat":24.219398,"lon":120.580295,"t":1794377304000,"rx":1794377343439},{"id":"EAL-6027","dir":0,"lat":24.264831,"lon":120.541011,"t":1794377302000,"rx":1794377343439},{"id":"EAL-6028","dir":1,"lat":24.174568,"lon":120.632234,"t":1794377315000,"rx":1794377343439},{"id":"EAR-1017","dir":0,"lat":24.137753,"lon":120.686638,"t":1794377309000,"rx":1794377343439},{"id":"FAE-800","dir":0,"lat":24.240936,"lon":120.564229,"t":1794377302000,"rx":1794377343439},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522086,"t":1794377328000,"rx":1794377356334},{"id":"763-U8","dir":1,"lat":24.142403,"lon":120.679626,"t":1794377324000,"rx":1794377356334},{"id":"780-U8","dir":0,"lat":24.209986,"lon":120.58128,"t":1794377316000,"rx":1794377356334},{"id":"782-U8","dir":0,"lat":24.282313,"lon":120.581811,"t":1794377320000,"rx":1794377356334},{"id":"EAL-1378","dir":0,"lat":24.15818,"lon":120.657534,"t":1794377322000,"rx":1794377356334},{"id":"EAL-2032","dir":0,"lat":24.240143,"lon":120.564179,"t":1794377329000,"rx":1794377356334},{"id":"EAL-2225","dir":0,"lat":24.184525,"lon":120.610723,"t":1794377317000,"rx":1794377356334},{"id":"EAL-2263","dir":1,"lat":24.219521,"lon":120.580301,"t":1794377326000,"rx":1794377356334},{"id":"EAL-2269","dir":1,"lat":24.247888,"lon":120.5751,"t":1794377325000,"rx":1794377356334},{"id":"EAL-5365","dir":0,"lat":24.232518,"lon":120.572315,"t":1794377324000,"rx":1794377356334},{"id":"EAL-5390","dir":1,"lat":24.219398,"lon":120.580293,"t":1794377324000,"rx":1794377356334},{"id":"EAL-6027","dir":0,"lat":24.264831,"lon":120.541028,"t":1794377322000,"rx":1794377356334},{"id":"EAL-6028","dir":1,"lat":24.175726,"lon":120.630628,"t":1794377328000,"rx":1794377356334},{"id":"EAR-1003","dir":0,"lat":24.148818,"lon":120.672139,"t":1794377322000,"rx":1794377356334},{"id":"EAR-1015","dir":0,"lat":24.22511,"lon":120.56892,"t":1794377319000,"rx":1794377356334},{"id":"EAR-1017","dir":0,"lat":24.137754,"lon":120.686639,"t":1794377329000,"rx":1794377356334},{"id":"FAE-800","dir":0,"lat":24.240936,"lon":120.564231,"t":1794377322000,"rx":1794377356334},{"id":"762-U8","dir":0,"lat":24.259166,"lon":120.522069,"t":1794377348000,"rx":1794377379528},{"id":"763-U8","dir":1,"lat":24.143533,"lon":120.67852,"t":1794377348000,"rx":1794377379528},{"id":"780-U8","dir":0,"lat":24.209033,"lon":120.5814,"t":1794377356000,"rx":1794377379528},{"id":"782-U8","dir":0,"lat":24.280985,"lon":120.581123,"t":1794377360000,"rx":1794377379528},{"id":"EAL-1378","dir":0,"lat":24.157924,"lon":120.657989,"t":1794377359000,"rx":1794377379528},{"id":"EAL-1589","dir":0,"lat":24.235643,"lon":120.570413,"t":1794377352000,"rx":1794377379528},{"id":"EAL-2032","dir":0,"lat":24.240143,"lon":120.564181,"t":1794377349000,"rx":1794377379528},{"id":"EAL-2225","dir":0,"lat":24.183336,"lon":120.614663,"t":1794377354000,"rx":1794377379528},{"id":"EAL-2263","dir":1,"lat":24.220478,"lon":120.580044,"t":1794377358000,"rx":1794377379528},{"id":"EAL-2265","dir":1,"lat":24.226518,"lon":120.576593,"t":1794377354000,"rx":1794377379528},{"id":"EAL-2269","dir":1,"lat":24.247888,"lon":120.575116,"t":1794377345000,"rx":1794377379528},{"id":"EAL-5358","dir":1,"lat":24.179288,"lon":120.623405,"t":1794377355000,"rx":1794377379528},{"id":"EAL-5365","dir":0,"lat":24.23252,"lon":120.572315,"t":1794377344000,"rx":1794377379528},{"id":"EAL-5390","dir":1,"lat":24.219386,"lon":120.580303,"t":1794377344000,"rx":1794377379528},{"id":"EAL-6027","dir":0,"lat":24.264818,"lon":120.541013,"t":1794377342000,"rx":1794377379528},{"id":"EAL-6028","dir":1,"lat":24.177468,"lon":120.627006,"t":1794377357000,"rx":1794377379528},{"id":"EAR-1003","dir":0,"lat":24.147438,"lon":120.673696,"t":1794377360000,"rx":1794377379528},{"id":"EAR-1015","dir":0,"lat":24.225108,"lon":120.568903,"t":1794377359000,"rx":1794377379528},{"id":"EAR-1017","dir":0,"lat":24.137761,"lon":120.686639,"t":1794377349000,"rx":1794377379528},{"id":"FAE-800","dir":0,"lat":24.240935,"lon":120.564233,"t":1794377342000,"rx":1794377379528},{"id":"762-U8","dir":0,"lat":24.259161,"lon":120.522075,"t":1794377368000,"rx":1794377392030},{"id":"763-U8","dir":1,"lat":24.143766,"lon":120.678253,"t":1794377364000,"rx":1794377392030},{"id":"EAL-1378","dir":0,"lat":24.157351,"lon":120.659006,"t":1794377373000,"rx":1794377392030},{"id":"EAL-1589","dir":0,"lat":24.233861,"lon":120.571521,"t":1794377372000,"rx":1794377392030},{"id":"EAL-2032","dir":0,"lat":24.240143,"lon":120.564198,"t":1794377369000,"rx":1794377392030},{"id":"EAL-2225","dir":0,"lat":24.182298,"lon":120.61656,"t":1794377374000,"rx":1794377392030},{"id":"EAL-2263","dir":1,"lat":24.220935,"lon":120.579858,"t":1794377366000,"rx":1794377392030},{"id":"EAL-2265","dir":1,"lat":24.226518,"lon":120.57661,"t":1794377374000,"rx":1794377392030},{"id":"EAL-2269","dir":1,"lat":24.247879,"lon":120.575104,"t":1794377365000,"rx":1794377392030},{"id":"EAL-5365","dir":0,"lat":24.23252,"lon":120.572331,"t":1794377364000,"rx":1794377392030},{"id":"EAL-5390","dir":1,"lat":24.220639,"lon":120.579951,"t":1794377364000,"rx":1794377392030},{"id":"EAL-6027","dir":0,"lat":24.264803,"lon":120.541041,"t":1794377362000,"rx":1794377392030},{"id":"EAL-6028","dir":1,"lat":24.178439,"lon":120.625083,"t":1794377372000,"rx":1794377392030},{"id":"EAR-1003","dir":0,"lat":24.146969,"lon":120.674144,"t":1794377373000,"rx":1794377392030},{"id":"EAR-1017","dir":0,"lat":24.138124,"lon":120.687584,"t":1794377372000,"rx":1794377392030},{"id":"FAE-800","dir":0,"lat":24.240933,"lon":120.564233,"t":1794377362000,"rx":1794377392030}],"boardingEta":[{"t":1794376157891,"eta":null,"arriving":false},{"t":1794376168307,"eta":null,"arriving":false},{"t":1794376180028,"eta":null,"arriving":false},{"t":1794376192545,"eta":null,"arriving":false},{"t":1794376205665,"eta":null,"arriving":false},{"t":1794376216460,"eta":null,"arriving":false},{"t":1794376230308,"eta":null,"arriving":false},{"t":1794376240385,"eta":null,"arriving":false},{"t":1794376253472,"eta":null,"arriving":false},{"t":1794376264176,"eta":null,"arriving":false},{"t":1794376275956,"eta":null,"arriving":false},{"t":1794376289363,"eta":null,"arriving":false},{"t":1794376299970,"eta":null,"arriving":false},{"t":1794376312067,"eta":null,"arriving":false},{"t":1794376325176,"eta":null,"arriving":false},{"t":1794376336180,"eta":null,"arriving":false},{"t":1794376348852,"eta":null,"arriving":false},{"t":1794376360375,"eta":null,"arriving":false},{"t":1794376373564,"eta":21,"arriving":false},{"t":1794376384381,"eta":21,"arriving":false},{"t":1794376396416,"eta":22,"arriving":false},{"t":1794376410169,"eta":-1,"arriving":false},{"t":1794376420134,"eta":-1,"arriving":false},{"t":1794376433078,"eta":-1,"arriving":true},{"t":1794376443952,"eta":-1,"arriving":true},{"t":1794376457703,"eta":-1,"arriving":true},{"t":1794376468193,"eta":-1,"arriving":true},{"t":1794376481353,"eta":-1,"arriving":true},{"t":1794376492105,"eta":-1,"arriving":true},{"t":1794376505825,"eta":null,"arriving":false},{"t":1794376516171,"eta":null,"arriving":false},{"t":1794376528793,"eta":null,"arriving":false},{"t":1794376540048,"eta":null,"arriving":false},{"t":1794376554138,"eta":null,"arriving":false},{"t":1794376564028,"eta":null,"arriving":false},{"t":1794376577304,"eta":null,"arriving":false},{"t":1794376588383,"eta":null,"arriving":false},{"t":1794376601020,"eta":null,"arriving":false},{"t":1794376612031,"eta":null,"arriving":false},{"t":1794376625360,"eta":null,"arriving":false},{"t":1794376636183,"eta":null,"arriving":false},{"t":1794376649710,"eta":null,"arriving":false},{"t":1794376660166,"eta":null,"arriving":false},{"t":1794376672882,"eta":null,"arriving":false},{"t":1794376684282,"eta":null,"arriving":false},{"t":1794376697156,"eta":null,"arriving":false},{"t":1794376708165,"eta":null,"arriving":false},{"t":1794376720806,"eta":null,"arriving":false},{"t":1794376732157,"eta":null,"arriving":false},{"t":1794376743961,"eta":null,"arriving":false},{"t":1794376757487,"eta":null,"arriving":false},{"t":1794376767988,"eta":null,"arriving":false},{"t":1794376782505,"eta":null,"arriving":false},{"t":1794376792050,"eta":null,"arriving":false},{"t":1794376805398,"eta":null,"arriving":false},{"t":1794376816206,"eta":null,"arriving":false},{"t":1794376829467,"eta":null,"arriving":false},{"t":1794376840451,"eta":null,"arriving":false},{"t":1794376853444,"eta":null,"arriving":false},{"t":1794376864233,"eta":null,"arriving":false},{"t":1794376877381,"eta":null,"arriving":false},{"t":1794376888188,"eta":null,"arriving":false},{"t":1794376901048,"eta":null,"arriving":false},{"t":1794376912091,"eta":null,"arriving":false},{"t":1794376925179,"eta":null,"arriving":false},{"t":1794376936264,"eta":null,"arriving":false},{"t":1794376948451,"eta":null,"arriving":false},{"t":1794376961266,"eta":null,"arriving":false},{"t":1794376972217,"eta":null,"arriving":false},{"t":1794376985520,"eta":null,"arriving":false},{"t":1794376996187,"eta":null,"arriving":false},{"t":1794377009040,"eta":null,"arriving":false},{"t":1794377020518,"eta":null,"arriving":false},{"t":1794377033597,"eta":null,"arriving":false},{"t":1794377044560,"eta":null,"arriving":false},{"t":1794377057606,"eta":38,"arriving":false},{"t":1794377068529,"eta":38,"arriving":false},{"t":1794377081509,"eta":-1,"arriving":false},{"t":1794377092556,"eta":-1,"arriving":false},{"t":1794377105161,"eta":-1,"arriving":false},{"t":1794377116553,"eta":-1,"arriving":false},{"t":1794377129515,"eta":null,"arriving":false},{"t":1794377140010,"eta":null,"arriving":false},{"t":1794377153037,"eta":null,"arriving":false},{"t":1794377164074,"eta":null,"arriving":false},{"t":1794377177664,"eta":null,"arriving":false},{"t":1794377188632,"eta":null,"arriving":false},{"t":1794377201503,"eta":null,"arriving":false},{"t":1794377212320,"eta":null,"arriving":false},{"t":1794377225545,"eta":null,"arriving":false},{"t":1794377236520,"eta":null,"arriving":false},{"t":1794377249137,"eta":null,"arriving":false},{"t":1794377260505,"eta":null,"arriving":false},{"t":1794377273243,"eta":null,"arriving":false},{"t":1794377284664,"eta":null,"arriving":false},{"t":1794377295967,"eta":null,"arriving":false},{"t":1794377310531,"eta":null,"arriving":false},{"t":1794377319987,"eta":null,"arriving":false},{"t":1794377332085,"eta":null,"arriving":false},{"t":1794377344070,"eta":null,"arriving":false},{"t":1794377358647,"eta":null,"arriving":false},{"t":1794377368335,"eta":null,"arriving":false},{"t":1794377380140,"eta":null,"arriving":false},{"t":1794377392918,"eta":null,"arriving":false},{"t":1794377404002,"eta":null,"arriving":false}],"riderFixes":[{"t":1794376156000,"lat":24.222015,"lon":120.579226,"acc":15.0,"spd":0.0},{"t":1794376161000,"lat":24.22169,"lon":120.579114,"acc":15.0,"spd":0.3},{"t":1794376166000,"lat":24.221746,"lon":120.579206,"acc":15.0,"spd":0.6},{"t":1794376171000,"lat":24.222095,"lon":120.578895,"acc":15.0,"spd":0.0},{"t":1794376176000,"lat":24.221627,"lon":120.579063,"acc":15.0,"spd":0.5},{"t":1794376181000,"lat":24.221959,"lon":120.579051,"acc":15.0,"spd":0.0},{"t":1794376186000,"lat":24.221683,"lon":120.579105,"acc":15.0,"spd":0.5},{"t":1794376191000,"lat":24.221945,"lon":120.579093,"acc":15.0,"spd":0.0},{"t":1794376196000,"lat":24.221825,"lon":120.578959,"acc":15.0,"spd":0.0},{"t":1794376201000,"lat":24.222104,"lon":120.57923,"acc":15.0,"spd":0.0},{"t":1794376206000,"lat":24.221771,"lon":120.578809,"acc":15.0,"spd":0.0},{"t":1794376211000,"lat":24.221767,"lon":120.578949,"acc":15.0,"spd":0.0},{"t":1794376216000,"lat":24.221952,"lon":120.579289,"acc":15.0,"spd":0.0},{"t":1794376221000,"lat":24.22201,"lon":120.579153,"acc":15.0,"spd":0.0},{"t":1794376226000,"lat":24.221778,"lon":120.5791,"acc":15.0,"spd":0.0},{"t":1794376231000,"lat":24.221972,"lon":120.579093,"acc":15.0,"spd":0.0},{"t":1794376236000,"lat":24.221854,"lon":120.579045,"acc":15.0,"spd":0.0},{"t":1794376241000,"lat":24.222073,"lon":120.579087,"acc":15.0,"spd":0.4},{"t":1794376246000,"lat":24.222087,"lon":120.579125,"acc":15.0,"spd":0.0},{"t":1794376251000,"lat":24.221974,"lon":120.579164,"acc":15.0,"spd":0.0},{"t":1794376256000,"lat":24.22192,"lon":120.57906,"acc":15.0,"spd":0.5},{"t":1794376261000,"lat":24.222036,"lon":120.579176,"acc":15.0,"spd":0.0},{"t":1794376266000,"lat":24.221838,"lon":120.578983,"acc":15.0,"spd":0.0},{"t":1794376271000,"lat":24.221715,"lon":120.57889,"acc":15.0,"spd":0.0},{"t":1794376276000,"lat":24.221953,"lon":120.579192,"acc":15.0,"spd":0.1},{"t":1794376281000,"lat":24.221899,"lon":120.579062,"acc":15.0,"spd":0.0},{"t":1794376286000,"lat":24.222034,"lon":120.579153,"acc":15.0,"spd":0.0},{"t":1794376291000,"lat":24.221665,"lon":120.579027,"acc":15.0,"spd":0.0},{"t":1794376296000,"lat":24.221779,"lon":120.579244,"acc":15.0,"spd":0.0},{"t":1794376301000,"lat":24.221898,"lon":120.579281,"acc":15.0,"spd":0.0},{"t":1794376306000,"lat":24.221815,"lon":120.579096,"acc":15.0,"spd":0.0},{"t":1794376311000,"lat":24.221643,"lon":120.579267,"acc":15.0,"spd":0.0},{"t":1794376316000,"lat":24.221908,"lon":120.578998,"acc":15.0,"spd":0.4},{"t":1794376321000,"lat":24.221922,"lon":120.579029,"acc":15.0,"spd":0.0},{"t":1794376326000,"lat":24.222079,"lon":120.579006,"acc":15.0,"spd":0.6},{"t":1794376331000,"lat":24.221825,"lon":120.57917,"acc":15.0,"spd":0.0},{"t":1794376336000,"lat":24.221908,"lon":120.578938,"acc":15.0,"spd":0.8},{"t":1794376341000,"lat":24.22173,"lon":120.579095,"acc":15.0,"spd":0.0},{"t":1794376346000,"lat":24.221943,"lon":120.579119,"acc":15.0,"spd":0.7},{"t":1794376351000,"lat":24.22178,"lon":120.579024,"acc":15.0,"spd":0.5},{"t":1794376356000,"lat":24.221953,"lon":120.578978,"acc":15.0,"spd":0.1},{"t":1794376361000,"lat":24.221964,"lon":120.579085,"acc":15.0,"spd":0.4},{"t":1794376366000,"lat":24.221861,"lon":120.578956,"acc":15.0,"spd":0.0},{"t":1794376371000,"lat":24.221854,"lon":120.579129,"acc":15.0,"spd":0.0},{"t":1794376376000,"lat":24.221967,"lon":120.579151,"acc":15.0,"spd":3.4},{"t":1794376381000,"lat":24.221517,"lon":120.579274,"acc":15.0,"spd":0.1},{"t":1794376386000,"lat":24.2218,"lon":120.579477,"acc":15.0,"spd":0.3},{"t":1794376391000,"lat":24.221875,"lon":120.579067,"acc":15.0,"spd":0.0},{"t":1794376396000,"lat":24.221513,"lon":120.579287,"acc":15.0,"spd":0.8},{"t":1794376401000,"lat":24.221643,"lon":120.579335,"acc":15.0,"spd":0.7},{"t":1794376406000,"lat":24.221813,"lon":120.579108,"acc":15.0,"spd":0.9},{"t":1794376411000,"lat":24.221872,"lon":120.579148,"acc":15.0,"spd":0.1},{"t":1794376416000,"lat":24.221543,"lon":120.578979,"acc":15.0,"spd":0.6},{"t":1794376421000,"lat":24.221601,"lon":120.579596,"acc":15.0,"spd":1.8},{"t":1794376426000,"lat":24.221518,"lon":120.579322,"acc":15.0,"spd":2.4},{"t":1794376431000,"lat":24.221453,"lon":120.579476,"acc":15.0,"spd":1.9},{"t":1794376436000,"lat":24.221471,"lon":120.579525,"acc":15.0,"spd":1.7},{"t":1794376441000,"lat":24.221177,"lon":120.579325,"acc":15.0,"spd":1.9},{"t":1794376446000,"lat":24.221307,"lon":120.579357,"acc":15.0,"spd":1.5},{"t":1794376451000,"lat":24.22124,"lon":120.579288,"acc":15.0,"spd":1.5},{"t":1794376456000,"lat":24.220859,"lon":120.579423,"acc":15.0,"spd":5.7},{"t":1794376461000,"lat":24.220399,"lon":120.579739,"acc":15.0,"spd":11.3},{"t":1794376466000,"lat":24.220013,"lon":120.580051,"acc":15.0,"spd":11.8},{"t":1794376471000,"lat":24.219451,"lon":120.580048,"acc":15.0,"spd":11.7},{"t":1794376476000,"lat":24.219149,"lon":120.580063,"acc":15.0,"spd":10.1},{"t":1794376481000,"lat":24.218369,"lon":120.580153,"acc":15.0,"spd":9.5},{"t":1794376486000,"lat":24.218195,"lon":120.580325,"acc":15.0,"spd":9.8},{"t":1794376491000,"lat":24.217699,"lon":120.580281,"acc":15.0,"spd":9.4},{"t":1794376496000,"lat":24.21728,"lon":120.580355,"acc":15.0,"spd":8.9},{"t":1794376501000,"lat":24.216797,"lon":120.580304,"acc":15.0,"spd":9.1},{"t":1794376506000,"lat":24.216531,"lon":120.580467,"acc":15.0,"spd":8.8},{"t":1794376511000,"lat":24.216303,"lon":120.580599,"acc":15.0,"spd":9.0},{"t":1794376516000,"lat":24.21556,"lon":120.580403,"acc":15.0,"spd":9.0},{"t":1794376521000,"lat":24.215441,"lon":120.580806,"acc":15.0,"spd":9.0},{"t":1794376526000,"lat":24.214964,"lon":120.580556,"acc":15.0,"spd":9.3},{"t":1794376531000,"lat":24.214649,"lon":120.580665,"acc":15.0,"spd":8.4},{"t":1794376536000,"lat":24.214143,"lon":120.580695,"acc":15.0,"spd":11.8},{"t":1794376541000,"lat":24.213312,"lon":120.581019,"acc":15.0,"spd":16.0},{"t":1794376546000,"lat":24.212867,"lon":120.580771,"acc":15.0,"spd":10.9},{"t":1794376551000,"lat":24.212349,"lon":120.580945,"acc":15.0,"spd":11.2},{"t":1794376556000,"lat":24.211971,"lon":120.580912,"acc":15.0,"spd":11.2},{"t":1794376561000,"lat":24.211537,"lon":120.580894,"acc":15.0,"spd":10.8},{"t":1794376566000,"lat":24.211089,"lon":120.581142,"acc":15.0,"spd":10.5},{"t":1794376571000,"lat":24.210463,"lon":120.581282,"acc":15.0,"spd":10.7},{"t":1794376576000,"lat":24.210083,"lon":120.581295,"acc":15.0,"spd":7.2},{"t":1794376581000,"lat":24.20989,"lon":120.581098,"acc":15.0,"spd":1.5},{"t":1794376586000,"lat":24.209792,"lon":120.580907,"acc":15.0,"spd":0.4},{"t":1794376591000,"lat":24.209993,"lon":120.581138,"acc":15.0,"spd":0.6},{"t":1794376596000,"lat":24.210081,"lon":120.581022,"acc":15.0,"spd":0.0},{"t":1794376601000,"lat":24.209977,"lon":120.580967,"acc":15.0,"spd":0.5},{"t":1794376606000,"lat":24.209991,"lon":120.581017,"acc":15.0,"spd":0.4},{"t":1794376611000,"lat":24.20975,"lon":120.581112,"acc":15.0,"spd":0.1},{"t":1794376616000,"lat":24.209918,"lon":120.580812,"acc":15.0,"spd":2.9},{"t":1794376621000,"lat":24.209767,"lon":120.581099,"acc":15.0,"spd":8.3},{"t":1794376626000,"lat":24.209133,"lon":120.580935,"acc":15.0,"spd":9.1},{"t":1794376631000,"lat":24.208879,"lon":120.581151,"acc":15.0,"spd":9.2},{"t":1794376636000,"lat":24.208328,"lon":120.581282,"acc":15.0,"spd":7.8},{"t":1794376641000,"lat":24.207808,"lon":120.581225,"acc":15.0,"spd":7.9},{"t":1794376646000,"lat":24.207725,"lon":120.581198,"acc":15.0,"spd":8.5},{"t":1794376651000,"lat":24.207093,"lon":120.581205,"acc":15.0,"spd":8.8},{"t":1794376656000,"lat":24.206856,"lon":120.58133,"acc":15.0,"spd":11.3},{"t":1794376661000,"lat":24.205849,"lon":120.581084,"acc":15.0,"spd":15.8},{"t":1794376666000,"lat":24.205167,"lon":120.581036,"acc":15.0,"spd":16.0},{"t":1794376671000,"lat":24.204558,"lon":120.580958,"acc":15.0,"spd":16.4},{"t":1794376676000,"lat":24.20401,"lon":120.58091,"acc":15.0,"spd":15.8},{"t":1794376681000,"lat":24.203187,"lon":120.580841,"acc":15.0,"spd":16.1},{"t":1794376686000,"lat":24.202378,"lon":120.580857,"acc":15.0,"spd":16.0},{"t":1794376691000,"lat":24.201579,"lon":120.580574,"acc":15.0,"spd":15.6},{"t":1794376696000,"lat":24.201103,"lon":120.580627,"acc":15.0,"spd":15.6},{"t":1794376701000,"lat":24.200139,"lon":120.580655,"acc":15.0,"spd":15.9},{"t":1794376706000,"lat":24.199519,"lon":120.58052,"acc":15.0,"spd":16.3},{"t":1794376711000,"lat":24.198779,"lon":120.580303,"acc":15.0,"spd":16.1},{"t":1794376716000,"lat":24.198022,"lon":120.580479,"acc":15.0,"spd":16.0},{"t":1794376721000,"lat":24.197285,"lon":120.580221,"acc":15.0,"spd":17.4},{"t":1794376726000,"lat":24.196523,"lon":120.580099,"acc":15.0,"spd":17.3},{"t":1794376731000,"lat":24.195718,"lon":120.580062,"acc":15.0,"spd":15.8},{"t":1794376736000,"lat":24.195194,"lon":120.580273,"acc":15.0,"spd":14.1},{"t":1794376741000,"lat":24.194738,"lon":120.579888,"acc":15.0,"spd":13.9},{"t":1794376746000,"lat":24.194003,"lon":120.580171,"acc":15.0,"spd":13.2},{"t":1794376751000,"lat":24.193239,"lon":120.579675,"acc":15.0,"spd":13.8},{"t":1794376756000,"lat":24.192873,"lon":120.580004,"acc":15.0,"spd":9.0},{"t":1794376761000,"lat":24.192889,"lon":120.579614,"acc":15.0,"spd":1.6},{"t":1794376766000,"lat":24.192773,"lon":120.579918,"acc":15.0,"spd":1.3},{"t":1794376771000,"lat":24.193036,"lon":120.579992,"acc":15.0,"spd":1.0},{"t":1794376776000,"lat":24.192587,"lon":120.579731,"acc":15.0,"spd":0.5},{"t":1794376781000,"lat":24.192447,"lon":120.579725,"acc":15.0,"spd":0.2},{"t":1794376786000,"lat":24.192728,"lon":120.579974,"acc":15.0,"spd":0.0},{"t":1794376791000,"lat":24.192784,"lon":120.579841,"acc":15.0,"spd":0.0},{"t":1794376796000,"lat":24.192626,"lon":120.579856,"acc":15.0,"spd":3.8},{"t":1794376801000,"lat":24.192149,"lon":120.57968,"acc":15.0,"spd":10.4},{"t":1794376806000,"lat":24.191627,"lon":120.579848,"acc":15.0,"spd":10.0},{"t":1794376811000,"lat":24.191178,"lon":120.579975,"acc":15.0,"spd":10.6},{"t":1794376816000,"lat":24.190872,"lon":120.580067,"acc":15.0,"spd":10.1},{"t":1794376821000,"lat":24.190361,"lon":120.57994,"acc":15.0,"spd":9.7},{"t":1794376826000,"lat":24.189991,"lon":120.579956,"acc":15.0,"spd":10.6},{"t":1794376831000,"lat":24.189473,"lon":120.57985,"acc":15.0,"spd":10.0},{"t":1794376836000,"lat":24.188794,"lon":120.580083,"acc":15.0,"spd":9.8},{"t":1794376841000,"lat":24.188552,"lon":120.580798,"acc":15.0,"spd":15.0},{"t":1794376846000,"lat":24.188287,"lon":120.581482,"acc":15.0,"spd":14.8},{"t":1794376851000,"lat":24.18819,"lon":120.582247,"acc":15.0,"spd":14.4},{"t":1794376856000,"lat":24.187574,"lon":120.582866,"acc":15.0,"spd":15.6},{"t":1794376861000,"lat":24.18709,"lon":120.583287,"acc":15.0,"spd":15.7},{"t":1794376866000,"lat":24.186986,"lon":120.584046,"acc":15.0,"spd":14.5},{"t":1794376871000,"lat":24.186536,"lon":120.584716,"acc":15.0,"spd":15.1},{"t":1794376876000,"lat":24.186269,"lon":120.585083,"acc":15.0,"spd":10.8},{"t":1794376881000,"lat":24.186269,"lon":120.585517,"acc":15.0,"spd":5.4},{"t":1794376886000,"lat":24.186119,"lon":120.585728,"acc":15.0,"spd":5.0},{"t":1794376891000,"lat":24.186078,"lon":120.585961,"acc":15.0,"spd":5.4},{"t":1794376896000,"lat":24.185742,"lon":120.586281,"acc":15.0,"spd":9.0},{"t":1794376901000,"lat":24.185466,"lon":120.586921,"acc":15.0,"spd":12.7},{"t":1794376906000,"lat":24.18529,"lon":120.587434,"acc":15.0,"spd":11.3},{"t":1794376911000,"lat":24.185009,"lon":120.587974,"acc":15.0,"spd":12.0},{"t":1794376916000,"lat":24.185099,"lon":120.588412,"acc":15.0,"spd":10.6},{"t":1794376921000,"lat":24.184848,"lon":120.589029,"acc":15.0,"spd":9.0},{"t":1794376926000,"lat":24.184493,"lon":120.589327,"acc":15.0,"spd":8.2},{"t":1794376931000,"lat":24.184196,"lon":120.589698,"acc":15.0,"spd":8.8},{"t":1794376936000,"lat":24.184104,"lon":120.589956,"acc":15.0,"spd":8.5},{"t":1794376941000,"lat":24.184109,"lon":120.5903,"acc":15.0,"spd":7.0},{"t":1794376946000,"lat":24.18412,"lon":120.590766,"acc":15.0,"spd":7.9},{"t":1794376951000,"lat":24.183718,"lon":120.591035,"acc":15.0,"spd":8.5},{"t":1794376956000,"lat":24.1836,"lon":120.591235,"acc":15.0,"spd":5.4},{"t":1794376961000,"lat":24.183722,"lon":120.591194,"acc":15.0,"spd":1.9},{"t":1794376966000,"lat":24.183699,"lon":120.591334,"acc":15.0,"spd":2.6},{"t":1794376971000,"lat":24.183663,"lon":120.591631,"acc":15.0,"spd":2.6},{"t":1794376976000,"lat":24.183402,"lon":120.591627,"acc":15.0,"spd":3.0},{"t":1794376981000,"lat":24.183277,"lon":120.591766,"acc":15.0,"spd":2.9},{"t":1794376986000,"lat":24.183564,"lon":120.591965,"acc":15.0,"spd":2.1},{"t":1794376991000,"lat":24.18328,"lon":120.592143,"acc":15.0,"spd":2.5},{"t":1794376996000,"lat":24.183322,"lon":120.592151,"acc":15.0,"spd":2.9},{"t":1794377001000,"lat":24.183388,"lon":120.592055,"acc":15.0,"spd":3.2},{"t":1794377006000,"lat":24.183169,"lon":120.592376,"acc":15.0,"spd":2.8},{"t":1794377011000,"lat":24.183169,"lon":120.592649,"acc":15.0,"spd":2.7},{"t":1794377016000,"lat":24.183041,"lon":120.592722,"acc":15.0,"spd":2.4},{"t":1794377021000,"lat":24.183175,"lon":120.592856,"acc":15.0,"spd":2.3},{"t":1794377026000,"lat":24.183031,"lon":120.592861,"acc":15.0,"spd":2.8},{"t":1794377031000,"lat":24.183111,"lon":120.592956,"acc":15.0,"spd":2.9},{"t":1794377036000,"lat":24.182774,"lon":120.59332,"acc":15.0,"spd":3.0},{"t":1794377041000,"lat":24.183011,"lon":120.593344,"acc":15.0,"spd":2.5},{"t":1794377046000,"lat":24.182829,"lon":120.593388,"acc":15.0,"spd":2.6},{"t":1794377051000,"lat":24.183037,"lon":120.593542,"acc":15.0,"spd":2.9},{"t":1794377056000,"lat":24.182836,"lon":120.593611,"acc":15.0,"spd":1.8},{"t":1794377061000,"lat":24.182594,"lon":120.593651,"acc":15.0,"spd":2.6},{"t":1794377066000,"lat":24.182675,"lon":120.593811,"acc":15.0,"spd":2.3},{"t":1794377071000,"lat":24.182686,"lon":120.594084,"acc":15.0,"spd":2.1},{"t":1794377076000,"lat":24.182499,"lon":120.594163,"acc":15.0,"spd":6.0},{"t":1794377081000,"lat":24.182354,"lon":120.594655,"acc":15.0,"spd":10.5},{"t":1794377086000,"lat":24.182188,"lon":120.595252,"acc":15.0,"spd":10.2},{"t":1794377091000,"lat":24.18203,"lon":120.595661,"acc":15.0,"spd":10.8},{"t":1794377096000,"lat":24.182042,"lon":120.596079,"acc":15.0,"spd":11.2},{"t":1794377101000,"lat":24.182217,"lon":120.597018,"acc":15.0,"spd":12.2},{"t":1794377106000,"lat":24.182076,"lon":120.597274,"acc":15.0,"spd":12.0},{"t":1794377111000,"lat":24.182136,"lon":120.59785,"acc":15.0,"spd":12.2},{"t":1794377116000,"lat":24.182169,"lon":120.598676,"acc":15.0,"spd":12.0},{"t":1794377121000,"lat":24.182272,"lon":120.599169,"acc":15.0,"spd":12.3},{"t":1794377126000,"lat":24.182294,"lon":120.599674,"acc":15.0,"spd":11.8},{"t":1794377131000,"lat":24.182325,"lon":120.600396,"acc":15.0,"spd":12.0},{"t":1794377136000,"lat":24.182308,"lon":120.600981,"acc":15.0,"spd":11.7},{"t":1794377141000,"lat":24.182582,"lon":120.601528,"acc":15.0,"spd":11.4},{"t":1794377146000,"lat":24.182508,"lon":120.6018,"acc":15.0,"spd":11.0},{"t":1794377151000,"lat":24.182739,"lon":120.602282,"acc":15.0,"spd":4.8},{"t":1794377156000,"lat":24.182732,"lon":120.602517,"acc":15.0,"spd":5.5},{"t":1794377161000,"lat":24.182795,"lon":120.602751,"acc":15.0,"spd":5.1},{"t":1794377166000,"lat":24.182926,"lon":120.602906,"acc":15.0,"spd":4.7},{"t":1794377171000,"lat":24.182964,"lon":120.603174,"acc":15.0,"spd":4.1},{"t":1794377176000,"lat":24.18296,"lon":120.603228,"acc":15.0,"spd":3.1},{"t":1794377181000,"lat":24.182974,"lon":120.603244,"acc":15.0,"spd":0.0},{"t":1794377186000,"lat":24.182991,"lon":120.603374,"acc":15.0,"spd":0.0},{"t":1794377191000,"lat":24.182893,"lon":120.603138,"acc":15.0,"spd":0.3},{"t":1794377196000,"lat":24.182929,"lon":120.603331,"acc":15.0,"spd":0.0},{"t":1794377201000,"lat":24.183168,"lon":120.603309,"acc":15.0,"spd":1.1},{"t":1794377206000,"lat":24.182972,"lon":120.603552,"acc":15.0,"spd":0.9},{"t":1794377211000,"lat":24.182703,"lon":120.603707,"acc":15.0,"spd":1.3},{"t":1794377216000,"lat":24.183023,"lon":120.60372,"acc":15.0,"spd":3.2},{"t":1794377221000,"lat":24.183101,"lon":120.604064,"acc":15.0,"spd":7.1},{"t":1794377226000,"lat":24.183147,"lon":120.604591,"acc":15.0,"spd":7.0},{"t":1794377231000,"lat":24.183243,"lon":120.604908,"acc":15.0,"spd":7.5},{"t":1794377236000,"lat":24.183129,"lon":120.605236,"acc":15.0,"spd":7.8},{"t":1794377241000,"lat":24.183364,"lon":120.605386,"acc":15.0,"spd":7.3},{"t":1794377246000,"lat":24.183385,"lon":120.60577,"acc":15.0,"spd":7.4},{"t":1794377251000,"lat":24.183444,"lon":120.605885,"acc":15.0,"spd":7.5},{"t":1794377256000,"lat":24.183789,"lon":120.60644,"acc":15.0,"spd":9.1},{"t":1794377261000,"lat":24.183991,"lon":120.607316,"acc":15.0,"spd":13.5},{"t":1794377266000,"lat":24.183859,"lon":120.607777,"acc":15.0,"spd":12.6},{"t":1794377271000,"lat":24.183969,"lon":120.608901,"acc":15.0,"spd":13.9},{"t":1794377276000,"lat":24.184318,"lon":120.609052,"acc":15.0,"spd":9.7},{"t":1794377281000,"lat":24.184305,"lon":120.609197,"acc":15.0,"spd":5.8},{"t":1794377286000,"lat":24.184345,"lon":120.609529,"acc":15.0,"spd":7.2},{"t":1794377291000,"lat":24.184163,"lon":120.610054,"acc":15.0,"spd":5.6},{"t":1794377296000,"lat":24.184358,"lon":120.609968,"acc":15.0,"spd":3.8},{"t":1794377301000,"lat":24.184366,"lon":120.610066,"acc":15.0,"spd":1.3},{"t":1794377306000,"lat":24.184589,"lon":120.610308,"acc":15.0,"spd":1.9},{"t":1794377311000,"lat":24.184406,"lon":120.610476,"acc":15.0,"spd":2.5},{"t":1794377316000,"lat":24.184228,"lon":120.610592,"acc":15.0,"spd":6.1},{"t":1794377321000,"lat":24.184208,"lon":120.611165,"acc":15.0,"spd":12.0},{"t":1794377326000,"lat":24.184172,"lon":120.611828,"acc":15.0,"spd":11.1},{"t":1794377331000,"lat":24.1841,"lon":120.612082,"acc":15.0,"spd":11.0},{"t":1794377336000,"lat":24.184008,"lon":120.612779,"acc":15.0,"spd":11.4},{"t":1794377341000,"lat":24.183957,"lon":120.613194,"acc":15.0,"spd":11.2},{"t":1794377346000,"lat":24.183545,"lon":120.613735,"acc":15.0,"spd":11.5},{"t":1794377351000,"lat":24.183306,"lon":120.614328,"acc":15.0,"spd":10.6},{"t":1794377356000,"lat":24.183257,"lon":120.615007,"acc":15.0,"spd":11.7},{"t":1794377361000,"lat":24.183113,"lon":120.615212,"acc":15.0,"spd":11.5},{"t":1794377366000,"lat":24.1828,"lon":120.615766,"acc":15.0,"spd":10.7},{"t":1794377371000,"lat":24.182607,"lon":120.616436,"acc":15.0,"spd":11.2},{"t":1794377376000,"lat":24.18248,"lon":120.616037,"acc":15.0,"spd":0.0},{"t":1794377381000,"lat":24.182546,"lon":120.61619,"acc":15.0,"spd":0.0},{"t":1794377386000,"lat":24.182525,"lon":120.616244,"acc":15.0,"spd":0.0},{"t":1794377391000,"lat":24.182439,"lon":120.616248,"acc":15.0,"spd":0.0},{"t":1794377396000,"lat":24.182408,"lon":120.616353,"acc":15.0,"spd":0.0},{"t":1794377401000,"lat":24.182474,"lon":120.616392,"acc":15.0,"spd":0.1}],"waitingFixes":[{"t":1794376156000,"lat":24.221671,"lon":120.578961,"acc":25.0,"spd":0.1},{"t":1794376161000,"lat":24.221933,"lon":120.579542,"acc":25.0,"spd":0.0},{"t":1794376166000,"lat":24.22194,"lon":120.579291,"acc":25.0,"spd":0.7},{"t":1794376171000,"lat":24.221731,"lon":120.579494,"acc":25.0,"spd":0.3},{"t":1794376176000,"lat":24.221574,"lon":120.579111,"acc":25.0,"spd":0.0},{"t":1794376181000,"lat":24.221893,"lon":120.578865,"acc":25.0,"spd":0.4},{"t":1794376186000,"lat":24.221995,"lon":120.579203,"acc":25.0,"spd":0.5},{"t":1794376191000,"lat":24.221794,"lon":120.579028,"acc":25.0,"spd":0.0},{"t":1794376196000,"lat":24.222027,"lon":120.579316,"acc":25.0,"spd":0.2},{"t":1794376201000,"lat":24.222029,"lon":120.579252,"acc":25.0,"spd":0.2},{"t":1794376206000,"lat":24.222027,"lon":120.579215,"acc":25.0,"spd":0.0},{"t":1794376211000,"lat":24.221952,"lon":120.579231,"acc":25.0,"spd":0.4},{"t":1794376216000,"lat":24.22183,"lon":120.579146,"acc":25.0,"spd":0.5},{"t":1794376221000,"lat":24.222061,"lon":120.579011,"acc":25.0,"spd":0.1},{"t":1794376226000,"lat":24.221999,"lon":120.579313,"acc":25.0,"spd":0.1},{"t":1794376231000,"lat":24.221972,"lon":120.578824,"acc":25.0,"spd":0.4},{"t":1794376236000,"lat":24.221659,"lon":120.578837,"acc":25.0,"spd":0.2},{"t":1794376241000,"lat":24.222005,"lon":120.579178,"acc":25.0,"spd":0.3},{"t":1794376246000,"lat":24.221894,"lon":120.579141,"acc":25.0,"spd":0.4},{"t":1794376251000,"lat":24.221807,"lon":120.57921,"acc":25.0,"spd":0.1},{"t":1794376256000,"lat":24.221953,"lon":120.578864,"acc":25.0,"spd":0.2},{"t":1794376261000,"lat":24.221692,"lon":120.579095,"acc":25.0,"spd":0.1},{"t":1794376266000,"lat":24.221823,"lon":120.579227,"acc":25.0,"spd":0.1},{"t":1794376271000,"lat":24.221983,"lon":120.579009,"acc":25.0,"spd":0.7},{"t":1794376276000,"lat":24.221552,"lon":120.579048,"acc":25.0,"spd":0.3},{"t":1794376281000,"lat":24.222037,"lon":120.579016,"acc":25.0,"spd":0.3},{"t":1794376286000,"lat":24.221824,"lon":120.579028,"acc":25.0,"spd":0.2},{"t":1794376291000,"lat":24.221743,"lon":120.579043,"acc":25.0,"spd":0.1},{"t":1794376296000,"lat":24.221967,"lon":120.579207,"acc":25.0,"spd":0.1},{"t":1794376301000,"lat":24.221883,"lon":120.578938,"acc":25.0,"spd":0.1},{"t":1794376306000,"lat":24.221806,"lon":120.57901,"acc":25.0,"spd":0.2},{"t":1794376311000,"lat":24.221969,"lon":120.579238,"acc":25.0,"spd":0.0},{"t":1794376316000,"lat":24.221694,"lon":120.57836,"acc":25.0,"spd":0.0},{"t":1794376321000,"lat":24.221887,"lon":120.579078,"acc":25.0,"spd":0.1},{"t":1794376326000,"lat":24.221822,"lon":120.580125,"acc":25.0,"spd":0.3},{"t":1794376331000,"lat":24.222116,"lon":120.579478,"acc":25.0,"spd":0.1},{"t":1794376336000,"lat":24.221629,"lon":120.579293,"acc":25.0,"spd":0.0},{"t":1794376341000,"lat":24.221852,"lon":120.579292,"acc":25.0,"spd":0.2},{"t":1794376346000,"lat":24.221799,"lon":120.578917,"acc":25.0,"spd":0.2},{"t":1794376351000,"lat":24.221561,"lon":120.578873,"acc":25.0,"spd":0.1},{"t":1794376356000,"lat":24.221806,"lon":120.57899,"acc":25.0,"spd":0.4},{"t":1794376361000,"lat":24.221913,"lon":120.579019,"acc":25.0,"spd":0.6},{"t":1794376366000,"lat":24.221439,"lon":120.578967,"acc":25.0,"spd":0.3},{"t":1794376371000,"lat":24.221822,"lon":120.579276,"acc":25.0,"spd":0.4},{"t":1794376376000,"lat":24.221981,"lon":120.578957,"acc":25.0,"spd":0.2},{"t":1794376381000,"lat":24.221988,"lon":120.579049,"acc":25.0,"spd":0.7},{"t":1794376386000,"lat":24.221912,"lon":120.579189,"acc":25.0,"spd":0.2},{"t":1794376391000,"lat":24.221969,"lon":120.578978,"acc":25.0,"spd":0.1},{"t":1794376396000,"lat":24.222025,"lon":120.579972,"acc":25.0,"spd":0.0},{"t":1794376401000,"lat":24.221774,"lon":120.579691,"acc":25.0,"spd":0.0},{"t":1794376406000,"lat":24.221815,"lon":120.579441,"acc":25.0,"spd":0.5},{"t":1794376411000,"lat":24.221703,"lon":120.579214,"acc":25.0,"spd":0.1},{"t":1794376416000,"lat":24.22174,"lon":120.579344,"acc":25.0,"spd":0.7},{"t":1794376421000,"lat":24.22205,"lon":120.579264,"acc":25.0,"spd":0.2},{"t":1794376426000,"lat":24.22181,"lon":120.578959,"acc":25.0,"spd":0.2},{"t":1794376431000,"lat":24.221879,"lon":120.579136,"acc":25.0,"spd":0.2},{"t":1794376436000,"lat":24.221867,"lon":120.579204,"acc":25.0,"spd":0.3},{"t":1794376441000,"lat":24.222069,"lon":120.579007,"acc":25.0,"spd":0.4},{"t":1794376446000,"lat":24.221836,"lon":120.579087,"acc":25.0,"spd":0.2},{"t":1794376451000,"lat":24.221816,"lon":120.579177,"acc":25.0,"spd":0.0},{"t":1794376456000,"lat":24.22178,"lon":120.57904,"acc":25.0,"spd":0.0},{"t":1794376461000,"lat":24.221812,"lon":120.579051,"acc":25.0,"spd":0.0},{"t":1794376466000,"lat":24.221692,"lon":120.57911,"acc":25.0,"spd":0.0},{"t":1794376471000,"lat":24.221955,"lon":120.579093,"acc":25.0,"spd":0.5},{"t":1794376476000,"lat":24.221792,"lon":120.579178,"acc":25.0,"spd":0.0},{"t":1794376481000,"lat":24.221945,"lon":120.579051,"acc":25.0,"spd":0.3},{"t":1794376486000,"lat":24.221857,"lon":120.578944,"acc":25.0,"spd":0.1},{"t":1794376491000,"lat":24.221716,"lon":120.579104,"acc":25.0,"spd":0.1},{"t":1794376496000,"lat":24.221846,"lon":120.579117,"acc":25.0,"spd":0.4},{"t":1794376501000,"lat":24.221801,"lon":120.579389,"acc":25.0,"spd":0.4},{"t":1794376506000,"lat":24.221934,"lon":120.579276,"acc":25.0,"spd":0.1},{"t":1794376511000,"lat":24.221845,"lon":120.5789,"acc":25.0,"spd":0.2},{"t":1794376516000,"lat":24.221849,"lon":120.578759,"acc":25.0,"spd":0.2},{"t":1794376521000,"lat":24.22199,"lon":120.579065,"acc":25.0,"spd":0.3},{"t":1794376526000,"lat":24.221981,"lon":120.578992,"acc":25.0,"spd":0.2},{"t":1794376531000,"lat":24.22193,"lon":120.579479,"acc":25.0,"spd":0.5},{"t":1794376536000,"lat":24.221909,"lon":120.579113,"acc":25.0,"spd":0.4},{"t":1794376541000,"lat":24.222057,"lon":120.578977,"acc":25.0,"spd":0.1},{"t":1794376546000,"lat":24.222015,"lon":120.57893,"acc":25.0,"spd":0.2},{"t":1794376551000,"lat":24.22221,"lon":120.578835,"acc":25.0,"spd":0.1},{"t":1794376556000,"lat":24.221902,"lon":120.579094,"acc":25.0,"spd":0.1},{"t":1794376561000,"lat":24.221906,"lon":120.57909,"acc":25.0,"spd":0.4},{"t":1794376566000,"lat":24.221985,"lon":120.579063,"acc":25.0,"spd":0.2},{"t":1794376571000,"lat":24.22208,"lon":120.578855,"acc":25.0,"spd":0.1},{"t":1794376576000,"lat":24.221897,"lon":120.579083,"acc":25.0,"spd":0.1},{"t":1794376581000,"lat":24.221846,"lon":120.579545,"acc":25.0,"spd":1.0},{"t":1794376586000,"lat":24.222031,"lon":120.579591,"acc":25.0,"spd":0.2},{"t":1794376591000,"lat":24.221854,"lon":120.57937,"acc":25.0,"spd":0.3},{"t":1794376596000,"lat":24.221871,"lon":120.578017,"acc":25.0,"spd":0.2},{"t":1794376601000,"lat":24.222032,"lon":120.578556,"acc":25.0,"spd":0.2},{"t":1794376606000,"lat":24.221876,"lon":120.57886,"acc":25.0,"spd":0.5},{"t":1794376611000,"lat":24.221912,"lon":120.579033,"acc":25.0,"spd":0.2},{"t":1794376616000,"lat":24.222024,"lon":120.579161,"acc":25.0,"spd":0.2},{"t":1794376621000,"lat":24.221816,"lon":120.578902,"acc":25.0,"spd":0.1},{"t":1794376626000,"lat":24.221975,"lon":120.578858,"acc":25.0,"spd":0.2},{"t":1794376631000,"lat":24.222052,"lon":120.578825,"acc":25.0,"spd":0.1},{"t":1794376636000,"lat":24.221909,"lon":120.579086,"acc":25.0,"spd":0.5},{"t":1794376641000,"lat":24.222026,"lon":120.579052,"acc":25.0,"spd":0.5},{"t":1794376646000,"lat":24.221688,"lon":120.579114,"acc":25.0,"spd":0.2},{"t":1794376651000,"lat":24.221952,"lon":120.579063,"acc":25.0,"spd":0.2},{"t":1794376656000,"lat":24.221763,"lon":120.578902,"acc":25.0,"spd":0.4},{"t":1794376661000,"lat":24.221939,"lon":120.579146,"acc":25.0,"spd":0.3},{"t":1794376666000,"lat":24.221847,"lon":120.579256,"acc":25.0,"spd":0.0},{"t":1794376671000,"lat":24.222013,"lon":120.578965,"acc":25.0,"spd":0.1},{"t":1794376676000,"lat":24.221848,"lon":120.579035,"acc":25.0,"spd":0.1},{"t":1794376681000,"lat":24.221781,"lon":120.579003,"acc":25.0,"spd":0.1},{"t":1794376686000,"lat":24.221642,"lon":120.578874,"acc":25.0,"spd":0.4},{"t":1794376691000,"lat":24.221743,"lon":120.579242,"acc":25.0,"spd":0.1},{"t":1794376696000,"lat":24.221707,"lon":120.579042,"acc":25.0,"spd":0.1},{"t":1794376701000,"lat":24.221655,"lon":120.579036,"acc":25.0,"spd":0.3},{"t":1794376706000,"lat":24.221929,"lon":120.579003,"acc":25.0,"spd":0.2},{"t":1794376711000,"lat":24.222058,"lon":120.579047,"acc":25.0,"spd":0.1},{"t":1794376716000,"lat":24.22163,"lon":120.579204,"acc":25.0,"spd":0.1},{"t":1794376721000,"lat":24.221907,"lon":120.579298,"acc":25.0,"spd":0.1},{"t":1794376726000,"lat":24.221721,"lon":120.579494,"acc":25.0,"spd":0.4},{"t":1794376731000,"lat":24.22156,"lon":120.579129,"acc":25.0,"spd":0.2},{"t":1794376736000,"lat":24.22179,"lon":120.579128,"acc":25.0,"spd":0.5},{"t":1794376741000,"lat":24.22203,"lon":120.579174,"acc":25.0,"spd":0.3},{"t":1794376746000,"lat":24.221845,"lon":120.579261,"acc":25.0,"spd":0.1},{"t":1794376751000,"lat":24.221643,"lon":120.579108,"acc":25.0,"spd":0.1},{"t":1794376756000,"lat":24.221911,"lon":120.57919,"acc":25.0,"spd":0.0},{"t":1794376761000,"lat":24.221566,"lon":120.579239,"acc":25.0,"spd":0.3},{"t":1794376766000,"lat":24.222152,"lon":120.579084,"acc":25.0,"spd":0.1},{"t":1794376771000,"lat":24.221775,"lon":120.578922,"acc":25.0,"spd":0.5},{"t":1794376776000,"lat":24.221871,"lon":120.578923,"acc":25.0,"spd":0.1},{"t":1794376781000,"lat":24.221884,"lon":120.579441,"acc":25.0,"spd":0.1},{"t":1794376786000,"lat":24.221717,"lon":120.57924,"acc":25.0,"spd":0.0},{"t":1794376791000,"lat":24.221751,"lon":120.578814,"acc":25.0,"spd":0.1},{"t":1794376796000,"lat":24.2219,"lon":120.579171,"acc":25.0,"spd":0.1},{"t":1794376801000,"lat":24.221777,"lon":120.578908,"acc":25.0,"spd":0.2},{"t":1794376806000,"lat":24.221798,"lon":120.578929,"acc":25.0,"spd":0.1},{"t":1794376811000,"lat":24.222061,"lon":120.578898,"acc":25.0,"spd":0.1},{"t":1794376816000,"lat":24.221925,"lon":120.579233,"acc":25.0,"spd":0.4},{"t":1794376821000,"lat":24.221807,"lon":120.579274,"acc":25.0,"spd":0.5},{"t":1794376826000,"lat":24.221914,"lon":120.579296,"acc":25.0,"spd":0.3},{"t":1794376831000,"lat":24.221726,"lon":120.579086,"acc":25.0,"spd":0.4},{"t":1794376836000,"lat":24.221875,"lon":120.579069,"acc":25.0,"spd":0.3},{"t":1794376841000,"lat":24.222104,"lon":120.579135,"acc":25.0,"spd":0.7},{"t":1794376846000,"lat":24.221785,"lon":120.579149,"acc":25.0,"spd":0.1},{"t":1794376851000,"lat":24.221974,"lon":120.579369,"acc":25.0,"spd":0.5},{"t":1794376856000,"lat":24.221928,"lon":120.579121,"acc":25.0,"spd":0.3},{"t":1794376861000,"lat":24.221789,"lon":120.579246,"acc":25.0,"spd":0.3},{"t":1794376866000,"lat":24.222016,"lon":120.578921,"acc":25.0,"spd":0.4},{"t":1794376871000,"lat":24.221884,"lon":120.579391,"acc":25.0,"spd":0.1},{"t":1794376876000,"lat":24.221611,"lon":120.57886,"acc":25.0,"spd":0.0},{"t":1794376881000,"lat":24.222006,"lon":120.579002,"acc":25.0,"spd":0.4},{"t":1794376886000,"lat":24.222026,"lon":120.578874,"acc":25.0,"spd":0.1},{"t":1794376891000,"lat":24.222072,"lon":120.579026,"acc":25.0,"spd":0.0},{"t":1794376896000,"lat":24.221963,"lon":120.578786,"acc":25.0,"spd":0.1},{"t":1794376901000,"lat":24.221662,"lon":120.579457,"acc":25.0,"spd":0.1},{"t":1794376906000,"lat":24.22177,"lon":120.579166,"acc":25.0,"spd":0.3},{"t":1794376911000,"lat":24.22181,"lon":120.579208,"acc":25.0,"spd":0.2},{"t":1794376916000,"lat":24.221888,"lon":120.579135,"acc":25.0,"spd":0.3},{"t":1794376921000,"lat":24.221614,"lon":120.578848,"acc":25.0,"spd":0.4},{"t":1794376926000,"lat":24.221938,"lon":120.578996,"acc":25.0,"spd":0.4},{"t":1794376931000,"lat":24.221584,"lon":120.578896,"acc":25.0,"spd":0.2},{"t":1794376936000,"lat":24.221784,"lon":120.579179,"acc":25.0,"spd":0.5},{"t":1794376941000,"lat":24.221985,"lon":120.578968,"acc":25.0,"spd":0.1},{"t":1794376946000,"lat":24.221948,"lon":120.579099,"acc":25.0,"spd":0.1},{"t":1794376951000,"lat":24.221924,"lon":120.579298,"acc":25.0,"spd":0.2},{"t":1794376956000,"lat":24.221951,"lon":120.57838,"acc":25.0,"spd":0.4},{"t":1794376961000,"lat":24.221958,"lon":120.578601,"acc":25.0,"spd":0.2},{"t":1794376966000,"lat":24.221745,"lon":120.57876,"acc":25.0,"spd":0.4},{"t":1794376971000,"lat":24.22172,"lon":120.579032,"acc":25.0,"spd":0.1},{"t":1794376976000,"lat":24.221936,"lon":120.578819,"acc":25.0,"spd":0.1},{"t":1794376981000,"lat":24.221768,"lon":120.578904,"acc":25.0,"spd":0.1},{"t":1794376986000,"lat":24.221859,"lon":120.578785,"acc":25.0,"spd":0.1},{"t":1794376991000,"lat":24.22214,"lon":120.57895,"acc":25.0,"spd":0.0},{"t":1794376996000,"lat":24.221878,"lon":120.579168,"acc":25.0,"spd":0.2},{"t":1794377001000,"lat":24.22189,"lon":120.579141,"acc":25.0,"spd":0.2},{"t":1794377006000,"lat":24.222096,"lon":120.579283,"acc":25.0,"spd":0.2},{"t":1794377011000,"lat":24.221977,"lon":120.579108,"acc":25.0,"spd":0.6},{"t":1794377016000,"lat":24.222072,"lon":120.579117,"acc":25.0,"spd":0.5},{"t":1794377021000,"lat":24.221762,"lon":120.579296,"acc":25.0,"spd":0.2},{"t":1794377026000,"lat":24.222006,"lon":120.579137,"acc":25.0,"spd":0.3},{"t":1794377031000,"lat":24.221792,"lon":120.579018,"acc":25.0,"spd":0.2},{"t":1794377036000,"lat":24.221923,"lon":120.578888,"acc":25.0,"spd":0.3},{"t":1794377041000,"lat":24.221674,"lon":120.579236,"acc":25.0,"spd":0.3},{"t":1794377046000,"lat":24.221883,"lon":120.579276,"acc":25.0,"spd":0.2},{"t":1794377051000,"lat":24.221861,"lon":120.579141,"acc":25.0,"spd":0.2},{"t":1794377056000,"lat":24.221997,"lon":120.579053,"acc":25.0,"spd":0.2},{"t":1794377061000,"lat":24.221899,"lon":120.579163,"acc":25.0,"spd":0.0},{"t":1794377066000,"lat":24.221897,"lon":120.57923,"acc":25.0,"spd":0.1},{"t":1794377071000,"lat":24.221871,"lon":120.579202,"acc":25.0,"spd":0.1},{"t":1794377076000,"lat":24.221438,"lon":120.579128,"acc":25.0,"spd":0.4},{"t":1794377081000,"lat":24.221633,"lon":120.578915,"acc":25.0,"spd":0.2},{"t":1794377086000,"lat":24.221805,"lon":120.579234,"acc":25.0,"spd":0.4},{"t":1794377091000,"lat":24.221773,"lon":120.578894,"acc":25.0,"spd":0.1},{"t":1794377096000,"lat":24.222037,"lon":120.578968,"acc":25.0,"spd":0.7},{"t":1794377101000,"lat":24.22157,"lon":120.578971,"acc":25.0,"spd":0.1},{"t":1794377106000,"lat":24.222065,"lon":120.578985,"acc":25.0,"spd":0.2},{"t":1794377111000,"lat":24.221921,"lon":120.579019,"acc":25.0,"spd":0.5},{"t":1794377116000,"lat":24.221892,"lon":120.579075,"acc":25.0,"spd":0.1},{"t":1794377121000,"lat":24.221933,"lon":120.579029,"acc":25.0,"spd":0.4},{"t":1794377126000,"lat":24.222033,"lon":120.579045,"acc":25.0,"spd":0.1},{"t":1794377131000,"lat":24.221675,"lon":120.578938,"acc":25.0,"spd":0.2},{"t":1794377136000,"lat":24.221777,"lon":120.579008,"acc":25.0,"spd":0.1},{"t":1794377141000,"lat":24.221736,"lon":120.579292,"acc":25.0,"spd":0.2},{"t":1794377146000,"lat":24.221944,"lon":120.579208,"acc":25.0,"spd":0.3},{"t":1794377151000,"lat":24.222003,"lon":120.579169,"acc":25.0,"spd":0.6},{"t":1794377156000,"lat":24.221893,"lon":120.579038,"acc":25.0,"spd":0.2},{"t":1794377161000,"lat":24.221957,"lon":120.579022,"acc":25.0,"spd":0.3},{"t":1794377166000,"lat":24.221711,"lon":120.579302,"acc":25.0,"spd":0.0},{"t":1794377171000,"lat":24.221694,"lon":120.578918,"acc":25.0,"spd":0.2},{"t":1794377176000,"lat":24.221774,"lon":120.579182,"acc":25.0,"spd":0.1},{"t":1794377181000,"lat":24.221746,"lon":120.578895,"acc":25.0,"spd":0.4},{"t":1794377186000,"lat":24.221634,"lon":120.578865,"acc":25.0,"spd":0.4},{"t":1794377191000,"lat":24.222,"lon":120.5789,"acc":25.0,"spd":0.1},{"t":1794377196000,"lat":24.22186,"lon":120.579024,"acc":25.0,"spd":0.2},{"t":1794377201000,"lat":24.222159,"lon":120.579287,"acc":25.0,"spd":0.4},{"t":1794377206000,"lat":24.221828,"lon":120.579301,"acc":25.0,"spd":0.1},{"t":1794377211000,"lat":24.221822,"lon":120.578887,"acc":25.0,"spd":0.2},{"t":1794377216000,"lat":24.22184,"lon":120.579102,"acc":25.0,"spd":0.2},{"t":1794377221000,"lat":24.221643,"lon":120.579173,"acc":25.0,"spd":0.2},{"t":1794377226000,"lat":24.221771,"lon":120.578895,"acc":25.0,"spd":0.2},{"t":1794377231000,"lat":24.221884,"lon":120.579292,"acc":25.0,"spd":0.4},{"t":1794377236000,"lat":24.221626,"lon":120.579081,"acc":25.0,"spd":0.3},{"t":1794377241000,"lat":24.221705,"lon":120.579006,"acc":25.0,"spd":0.2},{"t":1794377246000,"lat":24.221822,"lon":120.579097,"acc":25.0,"spd":0.3},{"t":1794377251000,"lat":24.221955,"lon":120.579225,"acc":25.0,"spd":0.3},{"t":1794377256000,"lat":24.221811,"lon":120.579049,"acc":25.0,"spd":0.0},{"t":1794377261000,"lat":24.22181,"lon":120.579038,"acc":25.0,"spd":0.7},{"t":1794377266000,"lat":24.221768,"lon":120.579019,"acc":25.0,"spd":0.1},{"t":1794377271000,"lat":24.221835,"lon":120.57896,"acc":25.0,"spd":0.2},{"t":1794377276000,"lat":24.221681,"lon":120.578864,"acc":25.0,"spd":0.2},{"t":1794377281000,"lat":24.221699,"lon":120.57929,"acc":25.0,"spd":0.0},{"t":1794377286000,"lat":24.221651,"lon":120.579066,"acc":25.0,"spd":0.2},{"t":1794377291000,"lat":24.221918,"lon":120.579225,"acc":25.0,"spd":0.2},{"t":1794377296000,"lat":24.22174,"lon":120.578889,"acc":25.0,"spd":0.6},{"t":1794377301000,"lat":24.221885,"lon":120.579107,"acc":25.0,"spd":0.2},{"t":1794377306000,"lat":24.221759,"lon":120.579148,"acc":25.0,"spd":0.1},{"t":1794377311000,"lat":24.221787,"lon":120.579913,"acc":25.0,"spd":0.1},{"t":1794377316000,"lat":24.221895,"lon":120.579658,"acc":25.0,"spd":0.2},{"t":1794377321000,"lat":24.222087,"lon":120.579451,"acc":25.0,"spd":0.1},{"t":1794377326000,"lat":24.221759,"lon":120.579195,"acc":25.0,"spd":0.8},{"t":1794377331000,"lat":24.221871,"lon":120.579231,"acc":25.0,"spd":0.1},{"t":1794377336000,"lat":24.221645,"lon":120.579271,"acc":25.0,"spd":0.0},{"t":1794377341000,"lat":24.221809,"lon":120.580019,"acc":25.0,"spd":0.3},{"t":1794377346000,"lat":24.221916,"lon":120.579442,"acc":25.0,"spd":0.4},{"t":1794377351000,"lat":24.22189,"lon":120.579053,"acc":25.0,"spd":0.1},{"t":1794377356000,"lat":24.221582,"lon":120.579365,"acc":25.0,"spd":0.0},{"t":1794377361000,"lat":24.221944,"lon":120.579303,"acc":25.0,"spd":0.1},{"t":1794377366000,"lat":24.221905,"lon":120.579053,"acc":25.0,"spd":0.1},{"t":1794377371000,"lat":24.22225,"lon":120.578915,"acc":25.0,"spd":0.1},{"t":1794377376000,"lat":24.221913,"lon":120.578999,"acc":25.0,"spd":0.2},{"t":1794377381000,"lat":24.221807,"lon":120.579163,"acc":25.0,"spd":0.2},{"t":1794377386000,"lat":24.221804,"lon":120.578941,"acc":25.0,"spd":0.0},{"t":1794377391000,"lat":24.221902,"lon":120.578897,"acc":25.0,"spd":0.1},{"t":1794377396000,"lat":24.2219,"lon":120.57847,"acc":25.0,"spd":0.1},{"t":1794377401000,"lat":24.22206,"lon":120.578854,"acc":25.0,"spd":0.6}],"walkerFixes":[{"t":1794376156000,"lat":24.221811,"lon":120.579087,"acc":15.0,"spd":1.8},{"t":1794376161000,"lat":24.221788,"lon":120.579113,"acc":15.0,"spd":1.4},{"t":1794376166000,"lat":24.221703,"lon":120.579004,"acc":15.0,"spd":1.2},{"t":1794376171000,"lat":24.22166,"lon":120.579222,"acc":15.0,"spd":1.2},{"t":1794376176000,"lat":24.221696,"lon":120.579176,"acc":15.0,"spd":1.6},{"t":1794376181000,"lat":24.221401,"lon":120.579256,"acc":15.0,"spd":1.7},{"t":1794376186000,"lat":24.221619,"lon":120.579041,"acc":15.0,"spd":1.2},{"t":1794376191000,"lat":24.221506,"lon":120.579208,"acc":15.0,"spd":1.8},{"t":1794376196000,"lat":24.221258,"lon":120.579058,"acc":15.0,"spd":1.1},{"t":1794376201000,"lat":24.221205,"lon":120.579501,"acc":15.0,"spd":1.4},{"t":1794376206000,"lat":24.221152,"lon":120.579263,"acc":15.0,"spd":1.1},{"t":1794376211000,"lat":24.22144,"lon":120.579036,"acc":15.0,"spd":1.5},{"t":1794376216000,"lat":24.221207,"lon":120.579312,"acc":15.0,"spd":1.5},{"t":1794376221000,"lat":24.221026,"lon":120.57929,"acc":15.0,"spd":1.6},{"t":1794376226000,"lat":24.22099,"lon":120.579389,"acc":15.0,"spd":1.4},{"t":1794376231000,"lat":24.220901,"lon":120.579298,"acc":15.0,"spd":1.2},{"t":1794376236000,"lat":24.220933,"lon":120.579396,"acc":15.0,"spd":1.5},{"t":1794376241000,"lat":24.220905,"lon":120.579466,"acc":15.0,"spd":1.2},{"t":1794376246000,"lat":24.220775,"lon":120.579449,"acc":15.0,"spd":1.6},{"t":1794376251000,"lat":24.220613,"lon":120.579388,"acc":15.0,"spd":1.3},{"t":1794376256000,"lat":24.220684,"lon":120.579399,"acc":15.0,"spd":1.2},{"t":1794376261000,"lat":24.220618,"lon":120.579435,"acc":15.0,"spd":1.5},{"t":1794376266000,"lat":24.220773,"lon":120.579464,"acc":15.0,"spd":1.5},{"t":1794376271000,"lat":24.220486,"lon":120.579703,"acc":15.0,"spd":1.5},{"t":1794376276000,"lat":24.220459,"lon":120.579405,"acc":15.0,"spd":1.3},{"t":1794376281000,"lat":24.220418,"lon":120.579499,"acc":15.0,"spd":1.1},{"t":1794376286000,"lat":24.220173,"lon":120.579575,"acc":15.0,"spd":1.8},{"t":1794376291000,"lat":24.220396,"lon":120.57962,"acc":15.0,"spd":1.5},{"t":1794376296000,"lat":24.220075,"lon":120.579432,"acc":15.0,"spd":1.4},{"t":1794376301000,"lat":24.220118,"lon":120.579627,"acc":15.0,"spd":1.6},{"t":1794376306000,"lat":24.220018,"lon":120.579603,"acc":15.0,"spd":1.5},{"t":1794376311000,"lat":24.22,"lon":120.579538,"acc":15.0,"spd":1.8},{"t":1794376316000,"lat":24.220008,"lon":120.579682,"acc":15.0,"spd":1.4},{"t":1794376321000,"lat":24.219598,"lon":120.579671,"acc":15.0,"spd":1.4},{"t":1794376326000,"lat":24.219669,"lon":120.579963,"acc":15.0,"spd":1.4},{"t":1794376331000,"lat":24.219644,"lon":120.579536,"acc":15.0,"spd":1.5},{"t":1794376336000,"lat":24.219536,"lon":120.579964,"acc":15.0,"spd":1.7},{"t":1794376341000,"lat":24.219703,"lon":120.579657,"acc":15.0,"spd":1.5},{"t":1794376346000,"lat":24.219688,"lon":120.579633,"acc":15.0,"spd":1.7},{"t":1794376351000,"lat":24.219487,"lon":120.579981,"acc":15.0,"spd":1.1},{"t":1794376356000,"lat":24.219558,"lon":120.579829,"acc":15.0,"spd":1.3},{"t":1794376361000,"lat":24.219316,"lon":120.579877,"acc":15.0,"spd":1.1},{"t":1794376366000,"lat":24.219284,"lon":120.579718,"acc":15.0,"spd":1.4},{"t":1794376371000,"lat":24.219414,"lon":120.579748,"acc":15.0,"spd":1.6},{"t":1794376376000,"lat":24.219081,"lon":120.579703,"acc":15.0,"spd":1.5},{"t":1794376381000,"lat":24.218967,"lon":120.579709,"acc":15.0,"spd":1.4},{"t":1794376386000,"lat":24.219216,"lon":120.57976,"acc":15.0,"spd":1.5},{"t":1794376391000,"lat":24.219038,"lon":120.579907,"acc":15.0,"spd":1.3},{"t":1794376396000,"lat":24.218924,"lon":120.579714,"acc":15.0,"spd":1.3},{"t":1794376401000,"lat":24.219047,"lon":120.579731,"acc":15.0,"spd":1.7},{"t":1794376406000,"lat":24.219035,"lon":120.579985,"acc":15.0,"spd":1.2},{"t":1794376411000,"lat":24.218579,"lon":120.579849,"acc":15.0,"spd":1.4},{"t":1794376416000,"lat":24.218813,"lon":120.580253,"acc":15.0,"spd":1.4},{"t":1794376421000,"lat":24.218637,"lon":120.580007,"acc":15.0,"spd":1.5},{"t":1794376426000,"lat":24.218577,"lon":120.579878,"acc":15.0,"spd":1.5},{"t":1794376431000,"lat":24.21848,"lon":120.580008,"acc":15.0,"spd":1.3},{"t":1794376436000,"lat":24.218505,"lon":120.580166,"acc":15.0,"spd":1.6},{"t":1794376441000,"lat":24.218264,"lon":120.580013,"acc":15.0,"spd":1.7},{"t":1794376446000,"lat":24.218261,"lon":120.579965,"acc":15.0,"spd":1.2},{"t":1794376451000,"lat":24.218227,"lon":120.580095,"acc":15.0,"spd":1.0},{"t":1794376456000,"lat":24.217964,"lon":120.580111,"acc":15.0,"spd":1.2},{"t":1794376461000,"lat":24.218373,"lon":120.580186,"acc":15.0,"spd":1.2},{"t":1794376466000,"lat":24.217845,"lon":120.580059,"acc":15.0,"spd":1.1},{"t":1794376471000,"lat":24.218086,"lon":120.580177,"acc":15.0,"spd":1.4},{"t":1794376476000,"lat":24.217989,"lon":120.580143,"acc":15.0,"spd":1.4},{"t":1794376481000,"lat":24.217768,"lon":120.580335,"acc":15.0,"spd":1.1},{"t":1794376486000,"lat":24.217749,"lon":120.580107,"acc":15.0,"spd":1.6},{"t":1794376491000,"lat":24.217823,"lon":120.580311,"acc":15.0,"spd":1.5},{"t":1794376496000,"lat":24.217846,"lon":120.580203,"acc":15.0,"spd":1.4},{"t":1794376501000,"lat":24.217608,"lon":120.580172,"acc":15.0,"spd":1.2},{"t":1794376506000,"lat":24.217601,"lon":120.580265,"acc":15.0,"spd":1.5},{"t":1794376511000,"lat":24.217538,"lon":120.580308,"acc":15.0,"spd":1.4},{"t":1794376516000,"lat":24.217553,"lon":120.580261,"acc":15.0,"spd":1.3},{"t":1794376521000,"lat":24.217567,"lon":120.580388,"acc":15.0,"spd":1.6},{"t":1794376526000,"lat":24.217477,"lon":120.580424,"acc":15.0,"spd":1.1},{"t":1794376531000,"lat":24.217302,"lon":120.580272,"acc":15.0,"spd":1.6},{"t":1794376536000,"lat":24.217253,"lon":120.5802,"acc":15.0,"spd":1.4},{"t":1794376541000,"lat":24.21706,"lon":120.580147,"acc":15.0,"spd":1.4},{"t":1794376546000,"lat":24.217093,"lon":120.58034,"acc":15.0,"spd":1.0},{"t":1794376551000,"lat":24.216967,"lon":120.580203,"acc":15.0,"spd":1.7},{"t":1794376556000,"lat":24.2167,"lon":120.580348,"acc":15.0,"spd":1.1},{"t":1794376561000,"lat":24.217012,"lon":120.580253,"acc":15.0,"spd":1.1},{"t":1794376566000,"lat":24.216759,"lon":120.580363,"acc":15.0,"spd":1.5},{"t":1794376571000,"lat":24.216955,"lon":120.580467,"acc":15.0,"spd":1.6},{"t":1794376576000,"lat":24.216619,"lon":120.580243,"acc":15.0,"spd":1.2},{"t":1794376581000,"lat":24.216568,"lon":120.580326,"acc":15.0,"spd":1.7},{"t":1794376586000,"lat":24.216494,"lon":120.580238,"acc":15.0,"spd":1.3},{"t":1794376591000,"lat":24.216502,"lon":120.580378,"acc":15.0,"spd":1.4},{"t":1794376596000,"lat":24.216355,"lon":120.58041,"acc":15.0,"spd":1.4},{"t":1794376601000,"lat":24.216379,"lon":120.580393,"acc":15.0,"spd":1.8},{"t":1794376606000,"lat":24.216406,"lon":120.580205,"acc":15.0,"spd":1.1},{"t":1794376611000,"lat":24.216349,"lon":120.580381,"acc":15.0,"spd":1.4},{"t":1794376616000,"lat":24.216201,"lon":120.580556,"acc":15.0,"spd":1.2},{"t":1794376621000,"lat":24.216134,"lon":120.580361,"acc":15.0,"spd":1.6},{"t":1794376626000,"lat":24.216,"lon":120.580373,"acc":15.0,"spd":1.0},{"t":1794376631000,"lat":24.21582,"lon":120.580565,"acc":15.0,"spd":1.6},{"t":1794376636000,"lat":24.216061,"lon":120.580365,"acc":15.0,"spd":2.0},{"t":1794376641000,"lat":24.215935,"lon":120.580516,"acc":15.0,"spd":1.3},{"t":1794376646000,"lat":24.215796,"lon":120.580591,"acc":15.0,"spd":1.3},{"t":1794376651000,"lat":24.215688,"lon":120.580556,"acc":15.0,"spd":1.6},{"t":1794376656000,"lat":24.215547,"lon":120.580332,"acc":15.0,"spd":1.5},{"t":1794376661000,"lat":24.215601,"lon":120.580549,"acc":15.0,"spd":1.2},{"t":1794376666000,"lat":24.21559,"lon":120.580579,"acc":15.0,"spd":1.3},{"t":1794376671000,"lat":24.215487,"lon":120.580484,"acc":15.0,"spd":1.5},{"t":1794376676000,"lat":24.215532,"lon":120.580456,"acc":15.0,"spd":1.4},{"t":1794376681000,"lat":24.215296,"lon":120.580416,"acc":15.0,"spd":1.4},{"t":1794376686000,"lat":24.21542,"lon":120.580361,"acc":15.0,"spd":1.2},{"t":1794376691000,"lat":24.215198,"lon":120.580504,"acc":15.0,"spd":1.4},{"t":1794376696000,"lat":24.215232,"lon":120.580478,"acc":15.0,"spd":1.2},{"t":1794376701000,"lat":24.215114,"lon":120.58055,"acc":15.0,"spd":1.3},{"t":1794376706000,"lat":24.215037,"lon":120.580591,"acc":15.0,"spd":1.5},{"t":1794376711000,"lat":24.215283,"lon":120.580392,"acc":15.0,"spd":1.4},{"t":1794376716000,"lat":24.214887,"lon":120.580451,"acc":15.0,"spd":1.6},{"t":1794376721000,"lat":24.214936,"lon":120.58056,"acc":15.0,"spd":1.5},{"t":1794376726000,"lat":24.214754,"lon":120.580604,"acc":15.0,"spd":1.5},{"t":1794376731000,"lat":24.214674,"lon":120.580798,"acc":15.0,"spd":1.6},{"t":1794376736000,"lat":24.214763,"lon":120.58041,"acc":15.0,"spd":1.2},{"t":1794376741000,"lat":24.214567,"lon":120.580597,"acc":15.0,"spd":0.9},{"t":1794376746000,"lat":24.214824,"lon":120.580436,"acc":15.0,"spd":1.2},{"t":1794376751000,"lat":24.214378,"lon":120.580723,"acc":15.0,"spd":1.3},{"t":1794376756000,"lat":24.214621,"lon":120.580693,"acc":15.0,"spd":1.2},{"t":1794376761000,"lat":24.214271,"lon":120.580632,"acc":15.0,"spd":1.2},{"t":1794376766000,"lat":24.214242,"lon":120.580568,"acc":15.0,"spd":1.4},{"t":1794376771000,"lat":24.214424,"lon":120.580725,"acc":15.0,"spd":1.3},{"t":1794376776000,"lat":24.214117,"lon":120.580567,"acc":15.0,"spd":1.1},{"t":1794376781000,"lat":24.214013,"lon":120.580735,"acc":15.0,"spd":1.5},{"t":1794376786000,"lat":24.214171,"lon":120.580713,"acc":15.0,"spd":1.3},{"t":1794376791000,"lat":24.214002,"lon":120.5805,"acc":15.0,"spd":1.2},{"t":1794376796000,"lat":24.214023,"lon":120.580667,"acc":15.0,"spd":1.5},{"t":1794376801000,"lat":24.213858,"lon":120.580725,"acc":15.0,"spd":1.3},{"t":1794376806000,"lat":24.213816,"lon":120.580638,"acc":15.0,"spd":1.4},{"t":1794376811000,"lat":24.213686,"lon":120.580874,"acc":15.0,"spd":1.6},{"t":1794376816000,"lat":24.21358,"lon":120.580549,"acc":15.0,"spd":1.4},{"t":1794376821000,"lat":24.213583,"lon":120.580845,"acc":15.0,"spd":1.3},{"t":1794376826000,"lat":24.213548,"lon":120.580768,"acc":15.0,"spd":1.4},{"t":1794376831000,"lat":24.213496,"lon":120.580738,"acc":15.0,"spd":1.7},{"t":1794376836000,"lat":24.213499,"lon":120.58072,"acc":15.0,"spd":1.5},{"t":1794376841000,"lat":24.213428,"lon":120.58067,"acc":15.0,"spd":1.4},{"t":1794376846000,"lat":24.213317,"lon":120.580598,"acc":15.0,"spd":1.5},{"t":1794376851000,"lat":24.21329,"lon":120.580807,"acc":15.0,"spd":1.5},{"t":1794376856000,"lat":24.213191,"lon":120.580967,"acc":15.0,"spd":1.6},{"t":1794376861000,"lat":24.213008,"lon":120.580691,"acc":15.0,"spd":1.5},{"t":1794376866000,"lat":24.212964,"lon":120.58079,"acc":15.0,"spd":1.8},{"t":1794376871000,"lat":24.212865,"lon":120.580875,"acc":15.0,"spd":1.3},{"t":1794376876000,"lat":24.212951,"lon":120.580855,"acc":15.0,"spd":1.3},{"t":1794376881000,"lat":24.212976,"lon":120.580441,"acc":15.0,"spd":1.2},{"t":1794376886000,"lat":24.212757,"lon":120.580653,"acc":15.0,"spd":1.5},{"t":1794376891000,"lat":24.212741,"lon":120.580888,"acc":15.0,"spd":1.6},{"t":1794376896000,"lat":24.212602,"lon":120.581023,"acc":15.0,"spd":1.5},{"t":1794376901000,"lat":24.212568,"lon":120.580663,"acc":15.0,"spd":1.4},{"t":1794376906000,"lat":24.212473,"lon":120.580851,"acc":15.0,"spd":1.0},{"t":1794376911000,"lat":24.212536,"lon":120.580841,"acc":15.0,"spd":1.4},{"t":1794376916000,"lat":24.212577,"lon":120.58085,"acc":15.0,"spd":1.2},{"t":1794376921000,"lat":24.212304,"lon":120.580771,"acc":15.0,"spd":1.6},{"t":1794376926000,"lat":24.212456,"lon":120.580768,"acc":15.0,"spd":1.5},{"t":1794376931000,"lat":24.212244,"lon":120.580739,"acc":15.0,"spd":1.0},{"t":1794376936000,"lat":24.21228,"lon":120.580755,"acc":15.0,"spd":1.4},{"t":1794376941000,"lat":24.211969,"lon":120.58101,"acc":15.0,"spd":1.9},{"t":1794376946000,"lat":24.212064,"lon":120.580849,"acc":15.0,"spd":1.3},{"t":1794376951000,"lat":24.212028,"lon":120.58105,"acc":15.0,"spd":1.3},{"t":1794376956000,"lat":24.212048,"lon":120.580864,"acc":15.0,"spd":1.9},{"t":1794376961000,"lat":24.211912,"lon":120.580932,"acc":15.0,"spd":1.7},{"t":1794376966000,"lat":24.211714,"lon":120.580867,"acc":15.0,"spd":1.5},{"t":1794376971000,"lat":24.211639,"lon":120.580902,"acc":15.0,"spd":1.1},{"t":1794376976000,"lat":24.211601,"lon":120.580797,"acc":15.0,"spd":1.9},{"t":1794376981000,"lat":24.21185,"lon":120.581022,"acc":15.0,"spd":1.5},{"t":1794376986000,"lat":24.211576,"lon":120.581027,"acc":15.0,"spd":1.5},{"t":1794376991000,"lat":24.211416,"lon":120.581103,"acc":15.0,"spd":1.5},{"t":1794376996000,"lat":24.211419,"lon":120.581004,"acc":15.0,"spd":1.5},{"t":1794377001000,"lat":24.211204,"lon":120.581098,"acc":15.0,"spd":1.5},{"t":1794377006000,"lat":24.211409,"lon":120.580993,"acc":15.0,"spd":1.4},{"t":1794377011000,"lat":24.211178,"lon":120.58105,"acc":15.0,"spd":1.2},{"t":1794377016000,"lat":24.211164,"lon":120.580943,"acc":15.0,"spd":1.7},{"t":1794377021000,"lat":24.211069,"lon":120.581091,"acc":15.0,"spd":1.6},{"t":1794377026000,"lat":24.211168,"lon":120.581081,"acc":15.0,"spd":1.3},{"t":1794377031000,"lat":24.210885,"lon":120.580931,"acc":15.0,"spd":1.5},{"t":1794377036000,"lat":24.21093,"lon":120.581208,"acc":15.0,"spd":1.5},{"t":1794377041000,"lat":24.210853,"lon":120.5809,"acc":15.0,"spd":1.5},{"t":1794377046000,"lat":24.210742,"lon":120.581093,"acc":15.0,"spd":1.2},{"t":1794377051000,"lat":24.210805,"lon":120.581125,"acc":15.0,"spd":1.3},{"t":1794377056000,"lat":24.210787,"lon":120.580829,"acc":15.0,"spd":1.2},{"t":1794377061000,"lat":24.21065,"lon":120.580992,"acc":15.0,"spd":1.5},{"t":1794377066000,"lat":24.210514,"lon":120.581014,"acc":15.0,"spd":1.3},{"t":1794377071000,"lat":24.210606,"lon":120.581073,"acc":15.0,"spd":1.5},{"t":1794377076000,"lat":24.210305,"lon":120.58088,"acc":15.0,"spd":1.2},{"t":1794377081000,"lat":24.210339,"lon":120.581093,"acc":15.0,"spd":1.6},{"t":1794377086000,"lat":24.210355,"lon":120.581211,"acc":15.0,"spd":1.5},{"t":1794377091000,"lat":24.210192,"lon":120.581005,"acc":15.0,"spd":1.6},{"t":1794377096000,"lat":24.210371,"lon":120.581114,"acc":15.0,"spd":1.4},{"t":1794377101000,"lat":24.210115,"lon":120.580967,"acc":15.0,"spd":1.4},{"t":1794377106000,"lat":24.209965,"lon":120.581175,"acc":15.0,"spd":1.3},{"t":1794377111000,"lat":24.210142,"lon":120.581207,"acc":15.0,"spd":1.3},{"t":1794377116000,"lat":24.209838,"lon":120.581055,"acc":15.0,"spd":2.0},{"t":1794377121000,"lat":24.209833,"lon":120.581059,"acc":15.0,"spd":1.6},{"t":1794377126000,"lat":24.209734,"lon":120.581179,"acc":15.0,"spd":1.4},{"t":1794377131000,"lat":24.209805,"lon":120.581083,"acc":15.0,"spd":1.2},{"t":1794377136000,"lat":24.209763,"lon":120.580961,"acc":15.0,"spd":1.5},{"t":1794377141000,"lat":24.209714,"lon":120.581334,"acc":15.0,"spd":1.7},{"t":1794377146000,"lat":24.20954,"lon":120.581246,"acc":15.0,"spd":1.6},{"t":1794377151000,"lat":24.209503,"lon":120.581325,"acc":15.0,"spd":1.2},{"t":1794377156000,"lat":24.209511,"lon":120.580995,"acc":15.0,"spd":1.5},{"t":1794377161000,"lat":24.209218,"lon":120.581251,"acc":15.0,"spd":1.5},{"t":1794377166000,"lat":24.209359,"lon":120.581109,"acc":15.0,"spd":1.2},{"t":1794377171000,"lat":24.20914,"lon":120.581406,"acc":15.0,"spd":1.5},{"t":1794377176000,"lat":24.208969,"lon":120.581324,"acc":15.0,"spd":1.4},{"t":1794377181000,"lat":24.209192,"lon":120.581399,"acc":15.0,"spd":1.3},{"t":1794377186000,"lat":24.209039,"lon":120.581454,"acc":15.0,"spd":1.5},{"t":1794377191000,"lat":24.209031,"lon":120.581431,"acc":15.0,"spd":1.3},{"t":1794377196000,"lat":24.209004,"lon":120.581185,"acc":15.0,"spd":1.2},{"t":1794377201000,"lat":24.208995,"lon":120.581596,"acc":15.0,"spd":1.6},{"t":1794377206000,"lat":24.20888,"lon":120.581299,"acc":15.0,"spd":1.4},{"t":1794377211000,"lat":24.208915,"lon":120.581444,"acc":15.0,"spd":1.7},{"t":1794377216000,"lat":24.208698,"lon":120.581379,"acc":15.0,"spd":1.4},{"t":1794377221000,"lat":24.208778,"lon":120.581457,"acc":15.0,"spd":1.6},{"t":1794377226000,"lat":24.208468,"lon":120.581393,"acc":15.0,"spd":1.2},{"t":1794377231000,"lat":24.208485,"lon":120.581448,"acc":15.0,"spd":1.4},{"t":1794377236000,"lat":24.208305,"lon":120.581357,"acc":15.0,"spd":1.7},{"t":1794377241000,"lat":24.208438,"lon":120.581372,"acc":15.0,"spd":1.3},{"t":1794377246000,"lat":24.208338,"lon":120.581483,"acc":15.0,"spd":1.6},{"t":1794377251000,"lat":24.208185,"lon":120.581199,"acc":15.0,"spd":1.2},{"t":1794377256000,"lat":24.208089,"lon":120.581264,"acc":15.0,"spd":1.3},{"t":1794377261000,"lat":24.208155,"lon":120.581454,"acc":15.0,"spd":1.6},{"t":1794377266000,"lat":24.208003,"lon":120.581394,"acc":15.0,"spd":1.3},{"t":1794377271000,"lat":24.207883,"lon":120.581662,"acc":15.0,"spd":1.5},{"t":1794377276000,"lat":24.207889,"lon":120.5814,"acc":15.0,"spd":1.3},{"t":1794377281000,"lat":24.207931,"lon":120.581747,"acc":15.0,"spd":1.2},{"t":1794377286000,"lat":24.207763,"lon":120.581658,"acc":15.0,"spd":1.8},{"t":1794377291000,"lat":24.207935,"lon":120.581552,"acc":15.0,"spd":1.3},{"t":1794377296000,"lat":24.20776,"lon":120.581403,"acc":15.0,"spd":1.1},{"t":1794377301000,"lat":24.207663,"lon":120.581535,"acc":15.0,"spd":1.3},{"t":1794377306000,"lat":24.207694,"lon":120.581683,"acc":15.0,"spd":1.4},{"t":1794377311000,"lat":24.207803,"lon":120.581766,"acc":15.0,"spd":1.2},{"t":1794377316000,"lat":24.207659,"lon":120.581636,"acc":15.0,"spd":1.5},{"t":1794377321000,"lat":24.207227,"lon":120.581505,"acc":15.0,"spd":1.4},{"t":1794377326000,"lat":24.207395,"lon":120.581644,"acc":15.0,"spd":1.2},{"t":1794377331000,"lat":24.207234,"lon":120.581642,"acc":15.0,"spd":1.7},{"t":1794377336000,"lat":24.20716,"lon":120.581808,"acc":15.0,"spd":1.3},{"t":1794377341000,"lat":24.207179,"lon":120.58155,"acc":15.0,"spd":1.6},{"t":1794377346000,"lat":24.206836,"lon":120.581732,"acc":15.0,"spd":1.4},{"t":1794377351000,"lat":24.206789,"lon":120.581895,"acc":15.0,"spd":1.7},{"t":1794377356000,"lat":24.206993,"lon":120.581701,"acc":15.0,"spd":1.5},{"t":1794377361000,"lat":24.206934,"lon":120.581739,"acc":15.0,"spd":1.4},{"t":1794377366000,"lat":24.206683,"lon":120.581554,"acc":15.0,"spd":1.4},{"t":1794377371000,"lat":24.206777,"lon":120.581643,"acc":15.0,"spd":1.4},{"t":1794377376000,"lat":24.206781,"lon":120.581766,"acc":15.0,"spd":1.0},{"t":1794377381000,"lat":24.206699,"lon":120.581777,"acc":15.0,"spd":1.1},{"t":1794377386000,"lat":24.206628,"lon":120.581846,"acc":15.0,"spd":1.1},{"t":1794377391000,"lat":24.206566,"lon":120.581863,"acc":15.0,"spd":1.7},{"t":1794377396000,"lat":24.206617,"lon":120.581723,"acc":15.0,"spd":1.6},{"t":1794377401000,"lat":24.206245,"lon":120.581592,"acc":15.0,"spd":1.5}]} \ No newline at end of file diff --git a/lib/core/android_trip_monitor.dart b/lib/core/android_trip_monitor.dart index 0c97a72..0f83066 100644 --- a/lib/core/android_trip_monitor.dart +++ b/lib/core/android_trip_monitor.dart @@ -105,6 +105,7 @@ class TripMonitorSession { required this.stops, this.initialLatitude, this.initialLongitude, + this.initialLocationTimestampMs, this.boardingStopId, this.boardingStopName, this.destinationStopId, @@ -123,6 +124,11 @@ class TripMonitorSession { final List stops; final double? initialLatitude; final double? initialLongitude; + + /// When the initial position was measured, in epoch milliseconds. The route + /// screen's last fix can be minutes old by the time a trip starts, and the + /// monitor must not mistake where the rider was for where they are. + final int? initialLocationTimestampMs; final int? boardingStopId; final String? boardingStopName; final int? destinationStopId; @@ -141,6 +147,7 @@ class TripMonitorSession { 'backgroundLocationAlwaysGranted': backgroundLocationAlwaysGranted, 'initialLatitude': initialLatitude, 'initialLongitude': initialLongitude, + 'initialLocationTimestampMs': initialLocationTimestampMs, 'boardingStopId': boardingStopId, 'boardingStopName': boardingStopName, 'destinationStopId': destinationStopId, diff --git a/lib/core/bus_repository.dart b/lib/core/bus_repository.dart index 4bae468..8cafe1b 100644 --- a/lib/core/bus_repository.dart +++ b/lib/core/bus_repository.dart @@ -4959,6 +4959,15 @@ class BusRepository { return (normalized == null || normalized.isEmpty) ? null : normalized; } + /// Parses a stop id into the `int` this app keys stops by. + /// + /// Stop ids are 64-bit in the feeds (高雄's look like `100100128221`), so + /// this keeps the full value rather than narrowing it, and every Dart-side + /// map is keyed with the same result. The native side folds whatever it is + /// given into a positive 32-bit key (`foldStopId` in StopIdCodec.kt); the two + /// never need to agree on the same number, only to be self-consistent. + /// + /// Some providers use non-numeric ids (`101-11009`); those hash. int _parseStopId(Object? raw) { if (raw is num) { return raw.toInt(); diff --git a/lib/screens/route_detail_screen.dart b/lib/screens/route_detail_screen.dart index ea9fa24..0496c19 100644 --- a/lib/screens/route_detail_screen.dart +++ b/lib/screens/route_detail_screen.dart @@ -2130,6 +2130,8 @@ class _RouteDetailScreenState extends State destinationStopName: _destinationStopName, initialLatitude: _lastPosition?.latitude, initialLongitude: _lastPosition?.longitude, + initialLocationTimestampMs: + _lastPosition?.timestamp.millisecondsSinceEpoch, stops: resolvedStops.map(TripMonitorStop.fromStop).toList(), ); } @@ -2730,6 +2732,16 @@ class _RouteDetailScreenState extends State _destinationStopId = stop.stopId; _destinationStopName = stop.stopName; }); + if (_isAndroid && _backgroundTripMonitorPaused) { + // The service pauses a trip, not the screen: with a destination this is + // a different trip to it, and holding on to the old answer would leave + // the monitor off. That stranded the "you have boarded, choose where to + // get off" prompt — choosing never started anything again. + await _syncBackgroundTripMonitorPausedState(); + if (!mounted) { + return; + } + } if (boardingStop != null && pathId != null) { final controller = AppControllerScope.read(context); await controller.recordDestinationChoice( diff --git a/test/android_trip_monitor_test.dart b/test/android_trip_monitor_test.dart index f0dc751..7a74af6 100644 --- a/test/android_trip_monitor_test.dart +++ b/test/android_trip_monitor_test.dart @@ -54,7 +54,69 @@ void main() { final saved = (map['stops'] as List).single as Map; expect(saved['rawStopId'], 'MAIN-STOP'); expect((saved['liveData'] as Map)['eta'], 60); - expect((saved['liveData'] as Map)['buses'], [{'id': 'FAMILY-001'}]); + expect((saved['liveData'] as Map)['buses'], [ + {'id': 'FAMILY-001'}, + ]); + }); + + test('session carries when its initial position was measured', () { + // The route screen's last fix can be minutes old when a trip starts. The + // monitor judges staleness from this, so it has to cross the channel. + final measuredAt = DateTime.utc(2026, 11, 12, 8, 30); + final map = TripMonitorSession( + providerName: 'txg', + routeKey: 1, + routeName: '1', + pathId: 0, + pathName: '去程', + appInForeground: true, + backgroundLocationAlwaysGranted: false, + stops: const [], + initialLatitude: 24.1, + initialLongitude: 120.6, + initialLocationTimestampMs: measuredAt.millisecondsSinceEpoch, + ).toMap(); + + expect( + map['initialLocationTimestampMs'], + measuredAt.millisecondsSinceEpoch, + ); + }); + + test('session keeps 64-bit stop ids at full width', () { + // 高雄 stop ids exceed 32 bits. The native side folds them for its own + // feed joins, but what it is handed has to be the id Dart itself holds — + // that is what a notification tap is launched back with. + const stopId = 101111144601; + final map = TripMonitorSession( + providerName: 'khh', + routeKey: 100, + routeId: 'KHH100', + routeName: '100', + pathId: 0, + pathName: '去程', + appInForeground: true, + backgroundLocationAlwaysGranted: false, + boardingStopId: stopId, + destinationStopId: stopId + 30, + stops: [ + TripMonitorStop.fromStop( + const StopInfo( + routeKey: 100, + pathId: 0, + stopId: stopId, + stopName: '測試站', + sequence: 1, + lat: 22.6, + lon: 120.3, + ), + ), + ], + ).toMap(); + + expect(map['boardingStopId'], stopId); + expect(map['destinationStopId'], stopId + 30); + expect(((map['stops'] as List).single as Map)['stopId'], stopId); }); test('lifecycle calls tolerate a detached platform channel', () async { diff --git a/test/app_routes_test.dart b/test/app_routes_test.dart index 31c95f0..7fc1514 100644 --- a/test/app_routes_test.dart +++ b/test/app_routes_test.dart @@ -143,4 +143,26 @@ void main() { expect(action.routeKey, isNull); expect(action.stopId, isNull); }); + + test('native route launch payload keeps 64-bit stop ids', () { + // 高雄 stop ids exceed 32 bits, and the platform channel hands those over + // as a 64-bit integer. A widget or notification tap has to arrive naming + // the same stop the route screen's own models hold, or nothing is selected. + const stopId = 101111144601; + final action = AppLaunchAction.fromMap({ + 'target': 'route_detail', + 'provider': 'khh', + 'routeKey': 100, + 'routeId': 'KHH100', + 'pathId': 0, + 'stopId': stopId, + 'destinationPathId': 0, + 'destinationStopId': stopId + 30, + }); + + expect(action.target, AppLaunchTarget.routeDetail); + expect(action.provider, BusProvider.khh); + expect(action.stopId, stopId); + expect(action.destinationStopId, stopId + 30); + }); } diff --git a/test/route_detail_screen_loading_test.dart b/test/route_detail_screen_loading_test.dart index c988904..4dc2f00 100644 --- a/test/route_detail_screen_loading_test.dart +++ b/test/route_detail_screen_loading_test.dart @@ -830,6 +830,62 @@ void main() { expect(live['buses'], isEmpty); }); + _screenTest('choosing a destination restarts a monitor paused without one', ( + tester, + repository, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + GeolocatorPlatform.instance = _BackgroundLocation(); + const channel = MethodChannel('tw.avianjay.taiwanbus.flutter/trip_monitor'); + final sessions = >[]; + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(channel, ( + call, + ) async { + final session = (call.arguments as Map?)?['session'] as Map?; + if (call.method == 'startOrUpdateTripMonitor') sessions.add(session!); + // The service paused the trip that had no destination, as it does once + // it has detected boarding and asked where the rider gets off. + if (call.method == 'isTripMonitorPaused') { + return session!['destinationStopId'] == null; + } + return null; + }); + addTearDown(() { + tester.binding.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + null, + ); + }); + final controller = AppControllerScope.read( + tester.element(find.byType(RouteDetailScreen)), + ); + await controller.updateEnableRouteBackgroundMonitor(true); + repository.topologyRequests.single.complete(_detail()); + repository.primaryRequests.single.complete(_detail(eta: 300)); + await _frames(tester, 8); + repository.familyRequests.single.completer.complete( + repository.familyRequests.single.selected, + ); + await _frames(tester); + expect(sessions, isNotEmpty); + final sentWhilePaused = sessions.length; + + await tester.tap(find.byTooltip('背景乘車提醒')); + await _frames(tester); + await tester.tap(find.text('設定下車提醒')); + await _frames(tester); + await tester.tap( + find.descendant( + of: find.byType(BottomSheet), + matching: find.text('返程站5'), + ), + ); + await _frames(tester, 8); + + expect(sessions.length, greaterThan(sentWhilePaused)); + expect(sessions.last['destinationStopId'], 5); + }); + _screenTest('a new refresh rejects late family results', ( tester, repository,