import com.rippr.data.TrackPoint import com.rippr.data.Segment import com.rippr.geo.Geo import com.rippr.geo.LatLon import com.rippr.stats.RideStatistics // A deterministic LCG implemented identically in Kotlin and Dart, so both languages // see the SAME noise sequence. Neither language's built-in Random can do this. class Lcg(private var s: Long) { fun nextDouble(): Double { s = s * 6364136223846793005L + 1442695040888963407L val bits = (s ushr 11) and ((1L shl 53) - 1L) return bits.toDouble() / (1L shl 53).toDouble() } } fun p(seg: Long, ts: Long, lat: Double, lon: Double, sp: Float, alt: Double, id: Long) = TrackPoint(id = id, tripId = 1, segmentId = seg, timestamp = ts, latitude = lat, longitude = lon, speedKmh = sp, altitudeM = alt) fun main() { fun out(k: String, v: Any) = println("$k=$v") out("haversine_calgary_edmonton", Geo.haversineMeters(51.0447, -114.0719, 53.5461, -113.4938)) out("haversine_short_hop", Geo.haversineMeters(51.0447, -114.0719, 51.04480, -114.0719)) out("perp_beyond_end", Geo.perpendicularDistanceMeters( LatLon(51.003, -114.0), LatLon(51.000, -114.0), LatLon(51.002, -114.0))) val ride = (0 until 21_600).map { LatLon(51.0 + it * 0.00001, -114.0 + Math.sin(it / 100.0) * 0.001) } val simplified = Geo.simplify(ride, 5.0) out("simplify_count", simplified.size) out("simplify_last_lat", simplified.last().lat) out("path_length_ride", Geo.pathLengthMeters(ride)) // The disputed fixture: 600 stationary fixes with shared LCG +/-8 m altitude noise. val rng = Lcg(42L) val noisy = (0 until 600).map { p(1, it * 1000L, 51.0, -114.0, 0f, 1000.0 + (rng.nextDouble() * 16.0 - 8.0), (it + 1).toLong()) } out("noisy_gain", RideStatistics.compute(noisy).elevationGainM) out("noisy_loss", RideStatistics.compute(noisy).elevationLossM) val climb = (0 until 101).map { p(1, it * 1000L, 51.0 + it * 0.0001, -114.0, 50f, 1000.0 + it, (it + 1).toLong()) } out("climb_gain", RideStatistics.compute(climb).elevationGainM) val run = (0 until 100).map { p(1, it * 1000L, 51.0 + it * 0.0001, -114.0, 40f, 1000.0, (it + 1).toLong()) } val s = RideStatistics.compute(run) out("run_distance", s.distanceM) out("run_moving_millis", s.movingMillis) out("run_avg_speed", s.avgMovingSpeedKmh) val prof = RideStatistics.elevationProfile(run, 200) out("profile_size", prof.size) out("profile_last_distance", prof.last().distanceM) val hist = RideStatistics.speedHistogram(run, 10) out("hist_buckets", hist.size) out("hist_first_millis", hist.first().millis) }