import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; import 'package:rippr/src/geo/geo.dart'; /// Ported from `com.rippr.geo.GeoTest` (native Android v2.0.1). /// /// Every case and every tolerance is carried over unchanged. Where the Kotlin /// suite encoded a hard-won correction — notably the Calgary–Edmonton /// great-circle figure, which was verified by hand after the *test* turned out to /// be wrong rather than the code — that correction travels with it. void main() { const calgary = LatLon(51.0447, -114.0719); const edmonton = LatLon(53.5461, -113.4938); List line(int n) => List.generate(n, (i) => LatLon(51.0 + i * 0.001, -114.0)); group('haversine', () { test('matches a known long-distance reference', () { // Calgary to Edmonton, great-circle. Cross-checked by hand: 2.5014° of // latitude is ~278.1 km, and 0.5781° of longitude at ~52.3° is ~39.3 km, // giving sqrt(278.1² + 39.3²) ≈ 280.9 km. (Road distance is ~300 km — not // the same thing.) final d = haversineBetween(calgary, edmonton); expect(d, closeTo(280900.0, 280900.0 * 0.005)); }); test('one degree of latitude is about 111 km anywhere', () { final atEquator = haversineMeters(0.0, 0.0, 1.0, 0.0); final atLatitude60 = haversineMeters(60.0, 0.0, 61.0, 0.0); expect(atEquator, closeTo(111195.0, 200.0)); expect(atLatitude60, closeTo(atEquator, 200.0), reason: 'latitude spacing does not vary with longitude'); }); test('a degree of longitude shrinks with latitude', () { final atEquator = haversineMeters(0.0, 0.0, 0.0, 1.0); final atLatitude60 = haversineMeters(60.0, 0.0, 60.0, 1.0); // cos(60°) = 0.5, so it should be about half. expect(atLatitude60, closeTo(atEquator / 2, 500.0)); }); test('identical points are zero distance', () { expect(haversineBetween(calgary, calgary), closeTo(0.0, 1e-9)); }); test('short hops between consecutive fixes stay accurate', () { // ~11 m north, the scale of a 2 Hz fix at road speed. final d = haversineMeters(51.0447, -114.0719, 51.04480, -114.0719); expect(d, closeTo(11.1, 0.5)); }); test('distance is symmetric', () { expect( haversineBetween(calgary, edmonton), closeTo(haversineBetween(edmonton, calgary), 1e-6), ); }); test('handles an antimeridian crossing without exploding', () { // 0.02° apart in longitude, straddling +/-180. final d = haversineMeters(0.0, 179.99, 0.0, -179.99); expect(d, lessThan(3000), reason: 'expected a short hop, got $d m'); }); }); group('douglas-peucker', () { test('a straight line collapses to its endpoints', () { final simplified = simplify(line(50), 5.0); expect(simplified.length, 2); expect(simplified.first, line(50).first); expect(simplified.last, line(50).last); }); test('endpoints always survive', () { final zigzag = List.generate( 30, (i) => LatLon(51.0 + i * 0.001, -114.0 + (i % 2 == 0 ? 0.0 : 0.002)), ); final simplified = simplify(zigzag, 5.0); expect(simplified.first, zigzag.first); expect(simplified.last, zigzag.last); }); test('deviation above epsilon is preserved', () { // Middle point sits ~110 m off the line between its neighbours. const points = [ LatLon(51.000, -114.0), LatLon(51.001, -113.999), LatLon(51.002, -114.0), ]; expect(simplify(points, 5.0).length, 3); }); test('deviation below epsilon is dropped', () { const points = [ LatLon(51.000, -114.0), LatLon(51.001, -114.00001), // under a metre off the line LatLon(51.002, -114.0), ]; expect(simplify(points, 5.0).length, 2); }); test('epsilon of zero returns the input untouched', () { final input = line(20); expect(simplify(input, 0.0), input); }); test('inputs of two or fewer are returned as-is', () { expect(simplify(const [], 5.0).length, 0); expect(simplify(line(1), 5.0).length, 1); expect(simplify(line(2), 5.0).length, 2); }); test('handles a full ride without stack overflow and stays fast', () { // Three hours at 2 Hz, the real scale this has to survive. final ride = List.generate( 21600, (i) => LatLon(51.0 + i * 0.00001, -114.0 + math.sin(i / 100.0) * 0.001), ); final started = DateTime.now(); final simplified = simplify(ride, 5.0); final elapsedMs = DateTime.now().difference(started).inMilliseconds; expect(simplified.length, lessThan(ride.length), reason: 'simplification should reduce the point count'); expect(simplified.first, ride.first); expect(simplified.last, ride.last); expect(elapsedMs, lessThan(1000), reason: 'took ${elapsedMs}ms, expected well under a second'); }); }); group('perpendicular distance', () { test('a point on the segment has zero perpendicular distance', () { final d = perpendicularDistanceMeters( const LatLon(51.001, -114.0), const LatLon(51.000, -114.0), const LatLon(51.002, -114.0), ); expect(d.abs(), lessThan(0.01), reason: 'expected ~0, got $d'); }); test('a point beyond the end measures to the endpoint, not the infinite line', () { // Directly past the segment's end along the same bearing. final d = perpendicularDistanceMeters( const LatLon(51.003, -114.0), const LatLon(51.000, -114.0), const LatLon(51.002, -114.0), ); expect(d, closeTo(111.0, 5.0), reason: 'should be the ~111 m to the endpoint'); }); test('a degenerate segment falls back to point distance', () { const a = LatLon(51.0, -114.0); final d = perpendicularDistanceMeters(const LatLon(51.001, -114.0), a, a); expect(d, closeTo(111.0, 5.0)); }); }); group('bounds', () { test('bounds is null for an empty path', () { expect(bounds(const []), isNull); }); test('bounds spans mixed-sign coordinates', () { final b = bounds(const [ LatLon(-10.0, -20.0), LatLon(30.0, 40.0), LatLon(5.0, 0.0), ])!; expect(b.minLat, closeTo(-10.0, 1e-9)); expect(b.maxLat, closeTo(30.0, 1e-9)); expect(b.minLon, closeTo(-20.0, 1e-9)); expect(b.maxLon, closeTo(40.0, 1e-9)); expect(b.centerLat, closeTo(10.0, 1e-9)); }); test('a stationary ride reports degenerate bounds', () { final b = bounds(List.filled(10, calgary))!; expect(b.isDegenerate, isTrue, reason: 'map auto-fit must special-case this'); }); test('a real path is not degenerate', () { expect(bounds(line(10))!.isDegenerate, isFalse); }); }); group('path length', () { test('path length sums consecutive hops', () { final points = line(11); // ten hops of 0.001 degrees latitude expect(pathLengthMeters(points), closeTo(10 * 111.19, 20.0)); }); test('path length of fewer than two points is zero', () { expect(pathLengthMeters(const []), closeTo(0.0, 1e-9)); expect(pathLengthMeters(const [calgary]), closeTo(0.0, 1e-9)); }); }); }