Surveillance géographique du moteur V2 : 9 types de lieux répertoriés, détection d'entrée/sortie par rayon, séquence de mission forward-only, corridor comme polygone GeoJSON, déviation avec métriques et arrêts. 22 cas d'usage.
Étape 7 du pipeline (detectWaypoint). Chaque position acceptée est testée contre tous les waypoints du tenant. Distance haversine vers le centre ; comparaison au radius_m.
haversine ≤ radius_m et pas de visite active pour ce véhicule+waypoint → WaypointEntered, visite ouverte dans waypoint_visits avec state='inside'.
haversine > radius_m pour un véhicule avec visite active → WaypointExited, duration_seconds calculé, state='exited', exited_at enregistré.
Pour les waypoints de mission : seuls les sequence_order > currentSequencePosition sont testés. Empêche les faux matchs sur les allers-retours.
Si le véhicule entre à la séquence 30 alors que currentSequencePosition est 10 : les positions intermédiaires sont sautées. Événement waypoint_missed différé (Phase 2).
WaypointEntered + role='destination' à la bonne position dans la séquence → événement destination_reached (info). Portail de tracking notifié.
Waypoint type=border → border_crossing (info). GSM MCC en confirmation secondaire : 624=Cameroun, 622=Tchad. Pas de signal primaire (oscillation d'antenne).
Station-service (500m) à l'intérieur d'une ville (3km) → 2 visites actives. Le waypoint le plus proche détermine l'autorisation de l'arrêt.
Chaque type a un comportement spécifique : 6 enregistrent les arrêts, 2 non, 1 (border) est sensible à la durée. Voir tableau ci-dessous.
1 position à l'intérieur du rayon → visite courte mais valide. Rare à 500m+ de rayon. Le journal affiche la durée réelle. Durée minimale de séjour reportée à Phase 2.
La mission Douala→Garoua→N'Djamena→Garoua→Douala : mêmes waypoints physiques à des positions de séquence différentes. Le moteur ne matche que vers l'avant.
| Type | Enregistre les arrêts ? | Cross-flotte ? | Gere par | Cas d'usage |
|---|---|---|---|---|
| depot | Oui | Non (par tenant) | Propriétaire flotte | Base du transporteur, lieu de stationnement |
| warehouse | Oui | Non (par tenant) | Propriétaire flotte | Point de chargement/déchargement |
| fuel_station | Oui | Non (par tenant) | Propriétaire flotte | Station-service autorisee, conformite carburant |
| rest_area | Oui | Oui (système) | Admin KORIDO | Aire de repos nocturne désignée |
| port | Oui | Oui (système) | Admin KORIDO | Origine/destination, chargement/déchargement |
| custom | Oui | Non (par tenant) | Propriétaire flotte | Lieu significatif défini par le tenant |
| checkpoint | Non | Oui (système) | Admin KORIDO | Police, gendarmerie, douanes, forêt — passage uniquement |
| city | Non | Oui (système) | Admin KORIDO | Rayon trop large — classifier les arrêts comme enregistrés serait absurde |
| border | Oui (durée) | Oui (système) | Admin KORIDO | Enregistre avec surveillance de durée. Seuil 6h pour border_delay_excessive (Phase 2). Douanes : 6-14h normales. |
tenant_id IS NULL. Visibles par tous les tenants via RLS : USING (tenant_id = ... OR tenant_id IS NULL). Seule table avec tenant_id nullable.
stops.is_authorized + nearest_waypoint_id.Étape 8 du pipeline (detectCorridorDeviation). Le corridor est un polygone GeoJSON chargé dans BatchContext.corridorPolygons. Test de containment via @turf/boolean-point-in-polygon (ray-casting JS, ~0.01ms/position). Distance maximale via @turf/point-to-line-distance.
3 positions acceptées consecutives hors polygone (turf.booleanPointInPolygon = false). Filtre le bruit GPS : 1-2 positions hors corridor = fluctuation, pas déviation.
consecutiveOutsideCount sur la table vehicles. Survit entre batches. Chargé dans VehicleTrackingState, remis à 0 à l'ouverture ou au retour.
deltaMeters de l'étape 5 ajouté à ActiveDeviation.distanceM à chaque position acceptée hors corridor. Écrit dans corridor_deviations.distance_m au retour.
turf/point-to-line-distance à chaque position hors corridor. Le max est conservé dans ActiveDeviation et écrit dans max_distance_from_corridor_m.
StopOpened pendant déviation active → ActiveDeviation.stopCount++. Chaque arrêt = un événement deviation_with_stop distinct avec sa propre notification.
Position re-entre dans le polygone → state='returned', toutes les métriques écrites : distance, durée, stop_count, max_distance, exit/return positions.
Mission terminée avec déviation active → cron cascade, state='abandoned'. Pas d'événement moteur — transition uniquement par cron SQL.
Un arrêt ferme le trip mais PAS la déviation. Plusieurs trips dans une seule déviation. Un episode hors route = un dossier d'investigation.
deviation_with_stop : par arrêt (pas juste le premier), lié au parent via related_event_id. Ne s'auto-resolve JAMAIS — nécessite action humaine.
Événement warning émis à l'ouverture, s'auto-resolve au retour. Mais les deviation_with_stop ont leur propre cycle — ils ne s'auto-resolvent PAS.
Déviation reste active mais métriques GELÉES (distance, durée, stop_count). Alertes supprimées. À la reprise : ferme si sur corridor, continue sinon.
Quand DeviationOpened et StopOpened dans le meme batch : crypto.randomUUID() pré-généré l'ID de l'événement déviation. Le stop référence cet UUID via parentDeviationEventId.
Pas de mission = pas de polygone corridor = detectCorridorDeviation entièrement sauté. La détection de déviation n'existe que dans un contexte de mission.
CREATE TABLE waypoints (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID REFERENCES tenants(id), -- NULL = waypoint système
name TEXT NOT NULL,
position GEOGRAPHY(Point, 4326) NOT NULL,
radius_m INTEGER NOT NULL,
waypoint_type TEXT NOT NULL CHECK (waypoint_type IN (
'city', 'rest_area', 'border', 'port', 'fuel_station',
'warehouse', 'checkpoint', 'custom', 'depot'
)),
source TEXT NOT NULL DEFAULT 'manual'
CHECK (source IN ('manual', 'discovered')),
confirmed BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index spatial pour requetes ST_DWithin
CREATE INDEX waypoints_position ON waypoints USING GIST (position);
CREATE TABLE waypoint_visits (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
vehicle_id UUID NOT NULL REFERENCES vehicles(id),
waypoint_id UUID NOT NULL REFERENCES waypoints(id),
mission_id UUID REFERENCES missions(id),
trip_id UUID REFERENCES trips(id),
mission_waypoint_id UUID REFERENCES mission_waypoints(id),
state TEXT NOT NULL CHECK (state IN ('inside', 'exited')),
entered_at TIMESTAMPTZ NOT NULL,
exited_at TIMESTAMPTZ,
entry_position GEOGRAPHY(Point, 4326),
exit_position GEOGRAPHY(Point, 4326),
duration_seconds INTEGER,
stop_count INTEGER,
total_stop_seconds INTEGER,
distance_within_m NUMERIC(10,1),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Une visite active par waypoint par véhicule
CREATE UNIQUE INDEX waypoint_visits_one_active
ON waypoint_visits (vehicle_id, waypoint_id)
WHERE state = 'inside';
CREATE TABLE corridor_deviations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id),
vehicle_id UUID NOT NULL REFERENCES vehicles(id),
mission_id UUID NOT NULL REFERENCES missions(id),
state TEXT NOT NULL CHECK (state IN
('active', 'returned', 'abandoned')),
started_at TIMESTAMPTZ NOT NULL,
ended_at TIMESTAMPTZ,
exit_position GEOGRAPHY(Point, 4326),
return_position GEOGRAPHY(Point, 4326),
max_distance_from_corridor_m NUMERIC(10,1),
distance_m NUMERIC(10,1),
duration_seconds INTEGER,
stop_count INTEGER,
exit_speed_kmh NUMERIC(5,1),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
detectWaypoint) et 8 (detectCorridorDeviation) ne traitent que les positions avec quality_flag = 'accepted'. Les positions jitter, spike, filtered et late sont ignorées.
corridor_deviations.mission_id est NOT NULL. La déviation est un fait lié à une mission, pas à un véhicule.
deviation_with_stop. Le dossier d'investigation est complet.
| Operation | Outil | Coût | Budget pour 100 pos/batch |
|---|---|---|---|
| Point-in-polygon (containment) | @turf/boolean-point-in-polygon |
~0.01 ms/position | ~1 ms total |
| Distance au corridor | @turf/point-to-line-distance |
Au DeviationReturned uniquement | ~0.1 ms (rare) |
| Polygone en memoire | BatchContext.corridorPolygons | ~2-8 KB par mission | 200-400 vertices Douala-N'Djamena |
| Bundle turf.js (tree-shaken) | 2 modules turf | ~10 KB | Sous la gate CI de 8 MB |
| Paramètre | Défaut | Description |
|---|---|---|
déviationMinPositions |
3 | Positions consecutives hors polygone pour ouvrir une déviation |
stopAlertDuringDeviation |
true | Alerte sur tout arrêt pendant une déviation active |
stopAlertNightEnabled |
true | Alerte sur arrêts non enregistrés de nuit |
stopAlertNightMinDurationSeconds |
600 (10 min) | Durée minimale d'arrêt non enregistré de nuit avant alerte |
stopAlertNightStartHour |
22 | Début de la fenetre "nuit" pour alertes contextuelles |
stopAlertNightEndHour |
5 | Fin de la fenetre "nuit" |
Source : pipeline.md steps 7-8, state-machines.md 3.4-3.5, config.md 11.2, schemas.md 2.4-2.7