// test/unit/geofence_calculation_test.dart // // Unit test untuk GeofenceCalculator — Haversine formula & zone check. // Jalankan: flutter test test/unit/geofence_calculation_test.dart // // Pure Dart test — tidak ada dependency eksternal. import 'dart:math' as math; import 'package:flutter_test/flutter_test.dart'; // ---------- GeofenceCalculator (mirror logika dari project) ---------- class GeofenceCalculator { static const double _earthRadiusMeters = 6371000.0; /// Hitung jarak (meter) antara dua koordinat GPS menggunakan Haversine formula. double haversineDistance( double lat1, double lng1, double lat2, double lng2, ) { final dLat = _toRad(lat2 - lat1); final dLng = _toRad(lng2 - lng1); final a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(_toRad(lat1)) * math.cos(_toRad(lat2)) * math.sin(dLng / 2) * math.sin(dLng / 2); final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); return _earthRadiusMeters * c; } /// Return true jika koordinat berada di dalam radius dari center. bool isInsideGeofence({ required double centerLat, required double centerLng, required double radiusMeters, required double currentLat, required double currentLng, }) { final dist = haversineDistance(centerLat, centerLng, currentLat, currentLng); return dist <= radiusMeters; } /// Return true jika user baru saja keluar dari geofence. bool hasExitedGeofence({ required double centerLat, required double centerLng, required double radiusMeters, required double previousLat, required double previousLng, required double currentLat, required double currentLng, }) { final wasInside = isInsideGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radiusMeters, currentLat: previousLat, currentLng: previousLng, ); final isInside = isInsideGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radiusMeters, currentLat: currentLat, currentLng: currentLng, ); return wasInside && !isInside; } /// Return true jika user baru saja masuk ke geofence. bool hasEnteredGeofence({ required double centerLat, required double centerLng, required double radiusMeters, required double previousLat, required double previousLng, required double currentLat, required double currentLng, }) { final wasInside = isInsideGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radiusMeters, currentLat: previousLat, currentLng: previousLng, ); final isInside = isInsideGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radiusMeters, currentLat: currentLat, currentLng: currentLng, ); return !wasInside && isInside; } double _toRad(double deg) => deg * math.pi / 180.0; } // ---------- Koordinat referensi ---------- // ITS (Institut Teknologi Sepuluh Nopember) Surabaya const double _itsCenterLat = -7.2754; const double _itsCenterLng = 112.7920; // ---------- Tests ---------- void main() { late GeofenceCalculator calc; setUp(() { calc = GeofenceCalculator(); }); // ── HAVERSINE DISTANCE TESTS ───────────────────────────────────── group('haversineDistance — akurasi jarak', () { test('jarak titik yang sama → 0 meter', () { final dist = calc.haversineDistance( _itsCenterLat, _itsCenterLng, _itsCenterLat, _itsCenterLng); expect(dist, closeTo(0.0, 0.001)); }); test('jarak ~111 km per derajat lintang (ekuator)', () { // 1 derajat lintang ≈ 111 km final dist = calc.haversineDistance(0.0, 0.0, 1.0, 0.0); // 0° → 1°N di ekuator expect(dist, closeTo(111_195, 500)); // ±500 meter toleransi }); test('jarak ~111 km per derajat bujur (di ekuator)', () { final dist = calc.haversineDistance(0.0, 0.0, 0.0, 1.0); expect(dist, closeTo(111_195, 500)); }); test('jarak ITS ke Tunjungan Plaza ≈ 7.6 km', () { // Tunjungan Plaza Surabaya: -7.2611, 112.7380 const tpLat = -7.2611; const tpLng = 112.7380; final dist = calc.haversineDistance( _itsCenterLat, _itsCenterLng, tpLat, tpLng); // Jarak darat sekitar 7-8 km, Haversine (garis lurus) ≈ 5-6 km expect(dist, greaterThan(4000)); expect(dist, lessThan(9000)); }); test('jarak simetris: A→B == B→A', () { const lat1 = -7.2754; const lng1 = 112.7920; const lat2 = -7.2611; const lng2 = 112.7380; final d1 = calc.haversineDistance(lat1, lng1, lat2, lng2); final d2 = calc.haversineDistance(lat2, lng2, lat1, lng1); expect(d1, closeTo(d2, 0.001)); }); test('jarak ≥ 0 untuk semua koordinat valid', () { final dist = calc.haversineDistance(-90.0, -180.0, 90.0, 180.0); expect(dist, greaterThanOrEqualTo(0)); }); }); // ── IS INSIDE GEOFENCE ─────────────────────────────────────────── group('isInsideGeofence — pengecekan zona', () { // Radius 500 meter dari ITS const radius = 500.0; test('titik tepat di center → inside', () { expect( calc.isInsideGeofence( centerLat: _itsCenterLat, centerLng: _itsCenterLng, radiusMeters: radius, currentLat: _itsCenterLat, currentLng: _itsCenterLng, ), isTrue, ); }); test('titik 100 meter dari center → inside radius 500m', () { // ±0.001° ≈ 100-111 meter expect( calc.isInsideGeofence( centerLat: _itsCenterLat, centerLng: _itsCenterLng, radiusMeters: radius, currentLat: _itsCenterLat + 0.0009, currentLng: _itsCenterLng, ), isTrue, ); }); test('titik 600 meter dari center → outside radius 500m', () { // ±0.006° ≈ 600+ meter expect( calc.isInsideGeofence( centerLat: _itsCenterLat, centerLng: _itsCenterLng, radiusMeters: radius, currentLat: _itsCenterLat + 0.006, currentLng: _itsCenterLng, ), isFalse, ); }); test('radius sangat kecil (10m) — hanya titik sangat dekat yang inside', () { // ±0.0001° ≈ 11 meter expect( calc.isInsideGeofence( centerLat: _itsCenterLat, centerLng: _itsCenterLng, radiusMeters: 10, currentLat: _itsCenterLat + 0.0001, currentLng: _itsCenterLng, ), isFalse, ); }); test('radius sangat besar (10km) — titik jauh masih inside', () { expect( calc.isInsideGeofence( centerLat: _itsCenterLat, centerLng: _itsCenterLng, radiusMeters: 10000, currentLat: _itsCenterLat + 0.05, currentLng: _itsCenterLng, ), isTrue, ); }); }); // ── GEOFENCE EXIT DETECTION ─────────────────────────────────────── group('hasExitedGeofence', () { const centerLat = _itsCenterLat; const centerLng = _itsCenterLng; const radius = 500.0; // Koordinat dalam radius const insideLat = _itsCenterLat + 0.001; // ≈ 111m const insideLng = _itsCenterLng; // Koordinat di luar radius const outsideLat = _itsCenterLat + 0.01; // ≈ 1110m const outsideLng = _itsCenterLng; test('pindah dari dalam ke luar → hasExited = true', () { expect( calc.hasExitedGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radius, previousLat: insideLat, previousLng: insideLng, currentLat: outsideLat, currentLng: outsideLng, ), isTrue, ); }); test('masih di dalam → hasExited = false', () { expect( calc.hasExitedGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radius, previousLat: insideLat, previousLng: insideLng, currentLat: insideLat + 0.0005, currentLng: insideLng, ), isFalse, ); }); test('sudah di luar dan makin jauh → hasExited = false', () { expect( calc.hasExitedGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radius, previousLat: outsideLat, previousLng: outsideLng, currentLat: outsideLat + 0.005, currentLng: outsideLng, ), isFalse, ); }); }); // ── GEOFENCE ENTER DETECTION ───────────────────────────────────── group('hasEnteredGeofence', () { const centerLat = _itsCenterLat; const centerLng = _itsCenterLng; const radius = 500.0; const insideLat = _itsCenterLat + 0.001; const insideLng = _itsCenterLng; const outsideLat = _itsCenterLat + 0.01; const outsideLng = _itsCenterLng; test('pindah dari luar ke dalam → hasEntered = true', () { expect( calc.hasEnteredGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radius, previousLat: outsideLat, previousLng: outsideLng, currentLat: insideLat, currentLng: insideLng, ), isTrue, ); }); test('sudah di dalam → hasEntered = false', () { expect( calc.hasEnteredGeofence( centerLat: centerLat, centerLng: centerLng, radiusMeters: radius, previousLat: insideLat, previousLng: insideLng, currentLat: insideLat + 0.0001, currentLng: insideLng, ), isFalse, ); }); }); // ── REAL WORLD REFERENCE TESTS ──────────────────────────────────── group('real-world reference distances', () { test('jarak ITS ke Surabaya Gubeng station ≈ 6-8 km', () { // Stasiun Gubeng: -7.2651, 112.7509 final dist = calc.haversineDistance( _itsCenterLat, _itsCenterLng, -7.2651, 112.7509); expect(dist, greaterThan(4000)); expect(dist, lessThan(10000)); }); test('jarak Jakarta ke Surabaya ≈ 700-800 km', () { // Jakarta: -6.2088, 106.8456 // Surabaya: -7.2575, 112.7521 final dist = calc.haversineDistance(-6.2088, 106.8456, -7.2575, 112.7521); expect(dist, greaterThan(650_000)); expect(dist, lessThan(850_000)); }); }); }