SamplingPolicy.kt
| 1 | package org.opentracker |
| 2 | |
| 3 | import kotlin.math.cos |
| 4 | import kotlin.math.max |
| 5 | import kotlin.math.sqrt |
| 6 | |
| 7 | /** A position, without Android types, so the policy runs in JVM tests. */ |
| 8 | data class Fix( |
| 9 | val timeMs: Long, |
| 10 | val lat: Double, |
| 11 | val lon: Double, |
| 12 | val accuracy: Float? = null, |
| 13 | /** m/s, from GNSS. Network fixes have none. */ |
| 14 | val speed: Float? = null, |
| 15 | ) |
| 16 | |
| 17 | enum class Mode(val label: String) { |
| 18 | VEHICLE("Driving"), |
| 19 | WALK("Moving"), |
| 20 | DWELL("Stopped"), |
| 21 | STATIONARY("Still"), |
| 22 | } |
| 23 | |
| 24 | /** What to ask LocationManager for. */ |
| 25 | data class Request(val gnss: Boolean, val intervalMs: Long, val maxDelayMs: Long) |
| 26 | |
| 27 | /** |
| 28 | * Spends power in proportion to how fast the position changes. |
| 29 | * |
| 30 | * A faster mode starts with the first fix that shows the speed, so the start of a trip is not lost. |
| 31 | * A slower mode waits: DWELL bridges red lights and queues, and only then STATIONARY turns GNSS off. |
| 32 | * In STATIONARY, the significant-motion sensor ([wake]) or a network fix far away brings GNSS back. |
| 33 | * |
| 34 | * Not thread-safe. The tracker thread owns it. |
| 35 | */ |
| 36 | class SamplingPolicy { |
| 37 | var mode = Mode.WALK |
| 38 | private set |
| 39 | |
| 40 | private var lastKept: Fix? = null |
| 41 | private var lastFix: Fix? = null |
| 42 | private var stillSinceMs = 0L |
| 43 | private var slowSinceMs: Long? = null |
| 44 | |
| 45 | /** Updates the mode and returns whether the fix is worth uploading. */ |
| 46 | fun onFix(fix: Fix): Boolean { |
| 47 | tick(fix.timeMs) |
| 48 | updateMode(fix, speedOf(fix)) |
| 49 | lastFix = fix |
| 50 | return keep(fix) |
| 51 | } |
| 52 | |
| 53 | /** Time-based transitions. Call it on every fix and from the watchdog. */ |
| 54 | fun tick(nowMs: Long) { |
| 55 | if (mode == Mode.DWELL && nowMs - stillSinceMs >= DWELL_MS) mode = Mode.STATIONARY |
| 56 | } |
| 57 | |
| 58 | /** The significant-motion sensor fired. */ |
| 59 | fun wake(nowMs: Long) { |
| 60 | if (mode == Mode.STATIONARY || mode == Mode.DWELL) { |
| 61 | mode = Mode.WALK |
| 62 | stillSinceMs = nowMs |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | fun request(lowBattery: Boolean, charging: Boolean): Request { |
| 67 | val walk = Request(gnss = true, intervalMs = 30_000, maxDelayMs = 120_000) |
| 68 | val base = when (mode) { |
| 69 | Mode.VEHICLE -> Request(gnss = true, intervalMs = 5_000, maxDelayMs = 60_000) |
| 70 | Mode.WALK, Mode.DWELL -> walk |
| 71 | Mode.STATIONARY -> if (charging) walk else Request(gnss = false, intervalMs = HEARTBEAT_MS, maxDelayMs = 0) |
| 72 | } |
| 73 | return if (lowBattery && !charging) { |
| 74 | base.copy(intervalMs = base.intervalMs * 2, maxDelayMs = base.maxDelayMs * 2) |
| 75 | } else { |
| 76 | base |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | private fun updateMode(fix: Fix, speed: Float) { |
| 81 | val now = fix.timeMs |
| 82 | when { |
| 83 | speed >= VEHICLE_SPEED -> { |
| 84 | mode = Mode.VEHICLE |
| 85 | slowSinceMs = null |
| 86 | } |
| 87 | speed >= WALK_SPEED -> when (mode) { |
| 88 | Mode.VEHICLE -> { |
| 89 | val since = slowSinceMs ?: now.also { slowSinceMs = it } |
| 90 | if (now - since >= SLOWDOWN_MS) mode = Mode.WALK |
| 91 | } |
| 92 | else -> mode = Mode.WALK |
| 93 | } |
| 94 | mode == Mode.STATIONARY -> { |
| 95 | val last = lastKept |
| 96 | val accurate = (fix.accuracy ?: Float.MAX_VALUE) < DEPARTURE_M |
| 97 | if (last != null && accurate && distance(last, fix) > DEPARTURE_M) { |
| 98 | mode = Mode.WALK |
| 99 | stillSinceMs = now |
| 100 | } |
| 101 | } |
| 102 | mode != Mode.DWELL -> { |
| 103 | mode = Mode.DWELL |
| 104 | stillSinceMs = now |
| 105 | slowSinceMs = null |
| 106 | } |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | /** The higher of the GNSS speed and a cautious estimate from the previous fix. Some chips report 0 while moving. */ |
| 111 | private fun speedOf(fix: Fix): Float = max(fix.speed ?: 0f, estimatedSpeed(fix)) |
| 112 | |
| 113 | private fun estimatedSpeed(fix: Fix): Float { |
| 114 | val prev = lastFix ?: return 0f |
| 115 | val dt = (fix.timeMs - prev.timeMs) / 1000.0 |
| 116 | val acc = (fix.accuracy ?: return 0f) + (prev.accuracy ?: return 0f) |
| 117 | if (dt < 10 || acc > 2 * MAX_ACCURACY) return 0f |
| 118 | // Subtracting both accuracies keeps position noise from looking like movement. |
| 119 | return (max(0.0, distance(prev, fix) - acc) / dt).toFloat() |
| 120 | } |
| 121 | |
| 122 | private fun keep(fix: Fix): Boolean { |
| 123 | val last = lastKept |
| 124 | val age = if (last == null) Long.MAX_VALUE else fix.timeMs - last.timeMs |
| 125 | val ok = when { |
| 126 | last == null -> true |
| 127 | // The server keeps one point per second. |
| 128 | fix.timeMs / 1000 <= last.timeMs / 1000 -> false |
| 129 | // A vague position beats none after a long gap, for example indoors. |
| 130 | (fix.accuracy ?: 0f) > MAX_ACCURACY -> age >= STALE_MS |
| 131 | age >= HEARTBEAT_MS -> true |
| 132 | mode == Mode.STATIONARY -> distance(last, fix) > DEPARTURE_M |
| 133 | mode == Mode.VEHICLE -> distance(last, fix) >= 20 |
| 134 | else -> distance(last, fix) >= 10 |
| 135 | } |
| 136 | if (ok) lastKept = fix |
| 137 | return ok |
| 138 | } |
| 139 | |
| 140 | companion object { |
| 141 | /** About 25 km/h. */ |
| 142 | const val VEHICLE_SPEED = 7f |
| 143 | /** GNSS speed noise on a still phone stays below this. */ |
| 144 | const val WALK_SPEED = 0.8f |
| 145 | const val DWELL_MS = 5 * 60_000L |
| 146 | const val SLOWDOWN_MS = 2 * 60_000L |
| 147 | const val HEARTBEAT_MS = 15 * 60_000L |
| 148 | const val STALE_MS = 10 * 60_000L |
| 149 | const val MAX_ACCURACY = 50f |
| 150 | const val DEPARTURE_M = 200.0 |
| 151 | |
| 152 | /** Metres. Equirectangular, which is exact enough at these distances. */ |
| 153 | fun distance(a: Fix, b: Fix): Double { |
| 154 | val k = 111_320.0 |
| 155 | val dy = (b.lat - a.lat) * k |
| 156 | val dx = (b.lon - a.lon) * k * cos(Math.toRadians((a.lat + b.lat) / 2)) |
| 157 | return sqrt(dx * dx + dy * dy) |
| 158 | } |
| 159 | } |
| 160 | } |
| 161 |