366 lines
11 KiB
Dart

// 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));
});
});
}