Add Rippr source snapshot and full-history bundle
Two copies for two jobs. rippr-src/ is a browsable git archive export of the tracked tree at 2f76983 - no build outputs, no local.properties, no nested .git - which is convenient to read in gitea but carries no history and will drift. rippr-full-history.bundle is the real backup: all 18 commits, verified as "records a complete history" and test-cloned before committing. This matters because ~/dojo/rippr has no git remote and otherwise exists only on one machine. rippr-src/SNAPSHOT.md explains the difference and how to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
92
rippr-src/docs/v2/04-geo.md
Normal file
92
rippr-src/docs/v2/04-geo.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# T04 — Geo utilities
|
||||
|
||||
**Phase** 2 · **Depends on** T01 · **Status** Done
|
||||
|
||||
## Goal
|
||||
|
||||
Pure geographic maths with no Android dependencies: distance between fixes, polyline
|
||||
simplification for rendering, and bounding boxes for map auto-fit. Done when
|
||||
`geo/Geo.kt` exists, imports nothing from `android.*`, and is covered by JVM unit tests
|
||||
that run without a device.
|
||||
|
||||
## Context
|
||||
|
||||
This is the pattern `Telemetry.kt` already establishes and the reason its logic is
|
||||
cheaply testable — the conversion and sanitisation functions there are covered by
|
||||
`TelemetryTest` with no emulator involved. `Geo` follows the same rule.
|
||||
|
||||
Three consumers depend on this task: T05 (statistics), T14 (path rendering), T15
|
||||
(export). It is on nobody's blocking path from the UI side, so it can be built in
|
||||
parallel with the whole data-layer track.
|
||||
|
||||
## Design
|
||||
|
||||
```kotlin
|
||||
object Geo {
|
||||
const val EARTH_RADIUS_M = 6_371_008.8 // IUGG mean radius
|
||||
|
||||
fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double
|
||||
|
||||
/** Douglas–Peucker. Always preserves first and last points. */
|
||||
fun simplify(points: List<LatLon>, epsilonMeters: Double): List<LatLon>
|
||||
|
||||
fun bounds(points: List<LatLon>): Bounds? // null for empty input
|
||||
}
|
||||
|
||||
data class LatLon(val lat: Double, val lon: Double)
|
||||
data class Bounds(val minLat: Double, val minLon: Double,
|
||||
val maxLat: Double, val maxLon: Double)
|
||||
```
|
||||
|
||||
**Haversine, not Vincenty.** Haversine assumes a sphere and is accurate to roughly 0.5%
|
||||
— a few metres per kilometre. For motorcycle ride distances that is far below GPS noise,
|
||||
and Vincenty's iterative solution would be false precision at real cost.
|
||||
|
||||
**Douglas–Peucker with a metre epsilon**, not a degree epsilon. A degree of longitude is
|
||||
~111 km at the equator and ~0 at the poles, so a degree-based tolerance behaves
|
||||
differently depending where you ride. Perpendicular distance is computed with a local
|
||||
equirectangular approximation — valid over the short spans between consecutive fixes and
|
||||
far cheaper than a full geodesic.
|
||||
|
||||
**Recursion depth.** A naive recursive Douglas–Peucker on 20,000+ points can blow the
|
||||
stack in a pathological case. Implement iteratively with an explicit work stack.
|
||||
|
||||
## Implementation
|
||||
|
||||
1. Create `geo/Geo.kt` with `LatLon`, `Bounds`, and the three functions.
|
||||
2. Haversine in double precision throughout — float loses metres over a long ride.
|
||||
3. Iterative Douglas–Peucker with an explicit stack.
|
||||
4. `bounds()` returns null on empty rather than a degenerate zero box, so callers must
|
||||
handle "no path" explicitly instead of silently centring on Null Island.
|
||||
5. Add an extension to map `TrackPoint` → `LatLon` so callers stay tidy.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] No `android.*` imports in `geo/`
|
||||
- [x] Haversine matches known reference distances within 0.5%
|
||||
- [x] `simplify()` always returns first and last points unchanged
|
||||
- [x] `simplify()` with epsilon 0 returns the input unchanged
|
||||
- [x] `simplify()` on 25,000 synthetic points completes quickly and does not stack-overflow
|
||||
- [x] `bounds()` returns null for an empty list
|
||||
|
||||
## Tests
|
||||
|
||||
JVM unit tests (`app/src/test/…`), no device needed:
|
||||
- Known pairs: Calgary→Edmonton ≈ 280.9 km (great-circle, not road); a 1° latitude step ≈ 111.2 km; identical points → 0
|
||||
- Antimeridian and equator crossings behave sanely
|
||||
- Douglas–Peucker: a straight line collapses to two points; a zigzag above epsilon is
|
||||
preserved; endpoints always survive
|
||||
- 25,000-point performance and stack-safety check
|
||||
- Bounds across a mixed-sign coordinate set
|
||||
|
||||
## Risks / gotchas
|
||||
|
||||
- **Float vs double.** `TrackPoint.latitude/longitude` are already `Double`; keep them
|
||||
that way through every calculation. `speedKmh` is `Float`, which is fine — it is
|
||||
display data, not accumulated.
|
||||
- **Epsilon tuning is a T14 concern**, not this one. Expose it as a parameter and let the
|
||||
render layer choose.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Anything that consumes these functions — statistics (T05), rendering (T14), export (T15).
|
||||
Reference in New Issue
Block a user