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:
2026-08-11 08:30:25 -05:00
parent 4debdfa518
commit 280fd7f988
106 changed files with 10377 additions and 0 deletions

View 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).