Skip to content

Commit fef8ae8

Browse files
committed
Day 27: Deterministic polygon sync via incident-based derivation (CRDT-safe)
1 parent 6243fe8 commit fef8ae8

File tree

10 files changed

+489
-20
lines changed

10 files changed

+489
-20
lines changed

mobile_app/lib/controllers/route_controller.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class RouteController {
3535

3636
bool _isRouteSafe(List<LatLng> geometry, List<Incident> incidents) {
3737
if (!_avoidanceService.isRouteSafe(geometry, incidents)) return false;
38-
if (!_polygonAvoidanceService.isRouteSafeWithPolygons(geometry, incidents)) return false;
38+
if (!_polygonAvoidanceService.isRouteSafeWithPolygons(geometry)) return false;
3939
return true;
4040
}
4141

mobile_app/lib/data/repositories/incident_repository.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import '../database.dart' as db;
66
import '../../core/api_client.dart';
77
import '../mappers/incident_mapper.dart';
88
import '../../services/p2p_service.dart';
9+
import '../../services/polygon_cache_service.dart';
910

1011
class IncidentRepository {
1112
final db.AppDatabase _db;
1213
final ApiClient _apiClient;
1314
final P2PService _p2pService;
15+
final PolygonCacheService _polygonCache;
1416

15-
IncidentRepository(this._db, this._apiClient, this._p2pService) {
17+
IncidentRepository(this._db, this._apiClient, this._p2pService, this._polygonCache) {
1618
// Day-16: Listen for incoming incident_create messages
1719
_p2pService.incomingIncidents.listen((dto) {
1820
_handleIncomingP2PIncident(dto);
@@ -66,6 +68,9 @@ class IncidentRepository {
6668
);
6769
_p2pService.broadcastIncident(incidentToBroadcast);
6870

71+
// Day 27: Generate and cache polygon for newly created incident
72+
_polygonCache.updateFromIncident(incidentToBroadcast);
73+
6974
await _db.transaction(() async {
7075
await _db.into(_db.incidents).insert(
7176
db.IncidentsCompanion.insert(
@@ -214,11 +219,17 @@ class IncidentRepository {
214219
clientId: Value(dto.clientId),
215220
),
216221
);
222+
223+
// Day 27: Refresh polygon cache after CRDT merge update
224+
debugPrint('[IncidentRepo] CRDT_MERGE_COMPLETED: $incidentId');
225+
final updatedIncident = await getIncident(incidentId);
226+
_polygonCache.updateFromIncident(updatedIncident);
217227
} else {
218228
debugPrint(
219229
'[IncidentRepo] CRDT_MERGE_APPLIED: Local state kept for $incidentId');
220230
debugPrint('[IncidentRepo] CONFLICT_RESOLVED: local wins');
221231
}
232+
debugPrint('[IncidentRepo] P2P_INCIDENT_RECEIVED: $incidentId');
222233
return;
223234
}
224235

@@ -240,6 +251,11 @@ class IncidentRepository {
240251

241252
debugPrint(
242253
'[IncidentRepo] P2P incident inserted: $incidentId');
254+
debugPrint('[IncidentRepo] P2P_INCIDENT_RECEIVED: $incidentId');
255+
256+
// Day 27: Generate and cache polygon after P2P insert
257+
final insertedIncident = await getIncident(incidentId);
258+
_polygonCache.updateFromIncident(insertedIncident);
243259
}
244260

245261
// ─── Day-17: State Synchronization Handlers ────────────────────────────

mobile_app/lib/main.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'controllers/route_controller.dart';
2626
import 'services/route_avoidance_service.dart';
2727
import 'services/polygon_generator.dart';
2828
import 'services/polygon_avoidance_service.dart';
29+
import 'services/polygon_cache_service.dart';
2930
import 'services/p2p_service.dart';
3031

3132
void main() async {
@@ -42,7 +43,6 @@ void main() async {
4243
final p2pService = P2PService(hostUrl: baseUrl);
4344
// Start the background connection to the local node
4445
p2pService.connect();
45-
final incidentRepo = IncidentRepository(db, apiClient, p2pService);
4646
final wsService = WsService(baseUrl, authService, db);
4747
final mapService = MapService();
4848

@@ -73,7 +73,9 @@ void main() async {
7373
final routeCacheService = RouteCacheService();
7474
final routeAvoidanceService = RouteAvoidanceService();
7575
final polygonGenerator = PolygonGenerator();
76-
final polygonAvoidanceService = PolygonAvoidanceService(polygonGenerator);
76+
final polygonCacheService = PolygonCacheService(polygonGenerator);
77+
final polygonAvoidanceService = PolygonAvoidanceService(polygonCacheService);
78+
final incidentRepo = IncidentRepository(db, apiClient, p2pService, polygonCacheService);
7779
final routeController = RouteController(routingService, routeCacheService, routeAvoidanceService, polygonAvoidanceService, incidentRepo);
7880
final responderRegistry = MockResponderRegistry();
7981

@@ -103,6 +105,7 @@ void main() async {
103105
Provider<RouteCacheService>.value(value: routeCacheService),
104106
Provider<RouteAvoidanceService>.value(value: routeAvoidanceService),
105107
Provider<PolygonAvoidanceService>.value(value: polygonAvoidanceService),
108+
Provider<PolygonCacheService>.value(value: polygonCacheService),
106109
Provider<RouteController>.value(value: routeController),
107110
Provider<P2PService>.value(value: p2pService),
108111
],
Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,31 @@
11
import 'package:flutter/foundation.dart';
22
import 'package:latlong2/latlong.dart';
3-
import '../models/models.dart';
43
import '../models/polygon_model.dart';
5-
import 'polygon_generator.dart';
4+
import 'polygon_cache_service.dart';
65
import '../utils/geo_spatial_utils.dart';
76

7+
/// Day 27: Evaluates route safety against cached danger polygons.
8+
///
9+
/// Reads pre-computed polygons from [PolygonCacheService] instead of
10+
/// regenerating on every call. This guarantees consistency with the
11+
/// deterministic polygon pipeline.
812
class PolygonAvoidanceService {
9-
final PolygonGenerator _generator;
13+
final PolygonCacheService _cacheService;
1014

11-
PolygonAvoidanceService(this._generator);
15+
PolygonAvoidanceService(this._cacheService);
1216

13-
bool isRouteSafeWithPolygons(List<LatLng> route, List<Incident> incidents) {
14-
if (incidents.isEmpty) return true;
17+
bool isRouteSafeWithPolygons(List<LatLng> route) {
18+
final List<DangerPolygon> polygons = _cacheService.getAllPolygons();
1519

16-
final List<DangerPolygon> polygons = incidents.map((i) {
17-
debugPrint('POLYGON_GENERATED');
18-
return _generator.generatePolygonFromIncident(i);
19-
}).toList();
20+
if (polygons.isEmpty) return true;
2021

2122
for (final polygon in polygons) {
2223
if (doesPolylineIntersectPolygon(route, polygon.points)) {
23-
debugPrint('POLYGON_INTERSECTION_DETECTED');
24+
debugPrint('POLYGON_INTERSECTION_DETECTED: ${polygon.incidentId}');
2425
return false;
2526
}
2627
}
2728
return true;
2829
}
2930
}
31+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:flutter/foundation.dart';
2+
import '../models/models.dart';
3+
import '../models/polygon_model.dart';
4+
import 'polygon_generator.dart';
5+
6+
/// Caches deterministically-generated danger polygons keyed by incident ID.
7+
///
8+
/// Day 27: Polygon geometry is never transmitted over the network. Every device
9+
/// derives identical polygons from the same incident data (id, lat, lon, type)
10+
/// using [PolygonGenerator]. This service caches the results so polygons are
11+
/// computed only once per incident create/update.
12+
class PolygonCacheService {
13+
final PolygonGenerator _generator;
14+
final Map<String, DangerPolygon> _cache = {};
15+
16+
PolygonCacheService(this._generator);
17+
18+
/// Generates a polygon from the incident and stores it in the cache.
19+
/// If the incident already has a cached polygon, it is replaced.
20+
void updateFromIncident(Incident incident) {
21+
final polygon = _generator.generatePolygonFromIncident(incident);
22+
_cache[incident.id] = polygon;
23+
debugPrint('[PolygonCache] POLYGON_CACHE_UPDATED: ${incident.id}');
24+
}
25+
26+
/// Batch-updates the cache from a list of incidents.
27+
void updateFromIncidents(List<Incident> incidents) {
28+
for (final incident in incidents) {
29+
updateFromIncident(incident);
30+
}
31+
}
32+
33+
/// Returns the cached polygon for the given incident ID, or null.
34+
DangerPolygon? getPolygon(String incidentId) {
35+
return _cache[incidentId];
36+
}
37+
38+
/// Returns all currently cached polygons.
39+
List<DangerPolygon> getAllPolygons() {
40+
return _cache.values.toList();
41+
}
42+
43+
/// Removes a polygon from the cache.
44+
void removePolygon(String incidentId) {
45+
_cache.remove(incidentId);
46+
debugPrint('[PolygonCache] POLYGON_CACHE_REMOVED: $incidentId');
47+
}
48+
49+
/// Number of cached polygons.
50+
int get length => _cache.length;
51+
}

mobile_app/lib/services/polygon_generator.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import 'dart:math' as math;
2+
import 'package:flutter/foundation.dart';
23
import 'package:latlong2/latlong.dart';
34
import '../models/models.dart';
45
import '../models/polygon_model.dart';
56

7+
/// Generates deterministic danger polygons from incident data.
8+
///
9+
/// Day 27: This function is PURE and deterministic:
10+
/// - Radius derived ONLY from incident.type
11+
/// - Fixed 12 polygon points
12+
/// - Constant angle step: 360 / 12
13+
/// - No random values, no timestamps, no device-specific inputs
614
class PolygonGenerator {
715
static const double _earthRadiusMeters = 6378137.0; // WGS84
816

@@ -39,6 +47,8 @@ class PolygonGenerator {
3947
points.add(LatLng(newLat, newLon));
4048
}
4149

50+
debugPrint('[PolygonGenerator] POLYGON_GENERATED_FROM_INCIDENT: ${incident.id} type=${incident.type} radius=$radius points=$numPoints');
51+
4252
return DangerPolygon(
4353
id: '${incident.id}_poly',
4454
points: points,

mobile_app/test/crdt_sync_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'package:flutter_test/flutter_test.dart';
44
import 'package:mobile_app/data/repositories/incident_repository.dart';
55
import 'package:mobile_app/data/database.dart';
66
import 'package:mobile_app/services/p2p_service.dart';
7+
import 'package:mobile_app/services/polygon_generator.dart';
8+
import 'package:mobile_app/services/polygon_cache_service.dart';
79
import 'package:mobile_app/core/api_client.dart';
810
import 'package:mobile_app/models/models.dart';
911
import 'package:mobile_app/models/network_envelope.dart';
@@ -44,7 +46,8 @@ void main() {
4446

4547
p2pService = P2PService(hostUrl: 'http://127.0.0.1', port: mockDaemon.port);
4648
db = AppDatabase.memory();
47-
repository = IncidentRepository(db, MockApiClient(), p2pService);
49+
final polygonCache = PolygonCacheService(PolygonGenerator());
50+
repository = IncidentRepository(db, MockApiClient(), p2pService, polygonCache);
4851

4952
p2pService.connect();
5053
await Future.delayed(const Duration(milliseconds: 100)); // wait for ws

mobile_app/test/final_crdt_observability_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import 'package:flutter_test/flutter_test.dart';
44
import 'package:mobile_app/data/repositories/incident_repository.dart';
55
import 'package:mobile_app/data/database.dart';
66
import 'package:mobile_app/services/p2p_service.dart';
7+
import 'package:mobile_app/services/polygon_generator.dart';
8+
import 'package:mobile_app/services/polygon_cache_service.dart';
79
import 'package:mobile_app/core/api_client.dart';
810
import 'package:mobile_app/models/network_envelope.dart';
911

@@ -43,7 +45,8 @@ void main() {
4345

4446
p2pService = P2PService(hostUrl: 'http://127.0.0.1', port: mockDaemon.port);
4547
db = AppDatabase.memory();
46-
repository = IncidentRepository(db, MockApiClient(), p2pService);
48+
final polygonCache = PolygonCacheService(PolygonGenerator());
49+
repository = IncidentRepository(db, MockApiClient(), p2pService, polygonCache);
4750

4851
p2pService.connect();
4952
await Future.delayed(const Duration(milliseconds: 100)); // wait for ws binding

mobile_app/test/map_screen_widget_test.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import 'package:mobile_app/controllers/route_controller.dart';
2121
import 'package:mobile_app/services/route_avoidance_service.dart';
2222
import 'package:mobile_app/services/polygon_generator.dart';
2323
import 'package:mobile_app/services/polygon_avoidance_service.dart';
24+
import 'package:mobile_app/services/polygon_cache_service.dart';
2425
import 'package:shared_preferences/shared_preferences.dart';
2526
import 'package:mobile_app/controllers/responder_controller.dart';
2627
import 'package:mobile_app/data/db/prefetch_database.dart';
@@ -39,7 +40,9 @@ void main() {
3940
final authService = AuthService();
4041
final apiClient = ApiClient(baseUrl: 'http://test', authService: authService);
4142
p2pService = P2PService(hostUrl: 'http://test');
42-
final incidentRepo = IncidentRepository(db, apiClient, p2pService);
43+
final polygonGenerator = PolygonGenerator();
44+
final polygonCacheService = PolygonCacheService(polygonGenerator);
45+
final incidentRepo = IncidentRepository(db, apiClient, p2pService, polygonCacheService);
4346
final config = AppConfig();
4447
final wsService = WsService('http://test', authService, db);
4548
final mapService = MapService();
@@ -50,8 +53,7 @@ void main() {
5053
final routingService = OsrmRoutingService(osrmService: osrmService, config: routingConfig);
5154
final routeCacheService = RouteCacheService();
5255
final routeAvoidanceService = RouteAvoidanceService();
53-
final polygonGenerator = PolygonGenerator();
54-
final polygonAvoidanceService = PolygonAvoidanceService(polygonGenerator);
56+
final polygonAvoidanceService = PolygonAvoidanceService(polygonCacheService);
5557
routeController = RouteController(routingService, routeCacheService, routeAvoidanceService, polygonAvoidanceService, incidentRepo);
5658
final responderRegistry = MockResponderRegistry();
5759

0 commit comments

Comments
 (0)