← Retour à l'index

Waypoints, Corridor et Déviation

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.

A. Detection de Waypoint 9 cas

É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.

Diagramme 1 GAROUA radius_m = 3000 3 km ENTRÉE WaypointEntered visite ouverte SORTIE WaypointExited durée calculée duration_seconds approche départ haversine(position, waypoint.position) ≤ radius_m → inside | > radius_m → exited
1

Detection d'entrée

haversine ≤ radius_m et pas de visite active pour ce véhicule+waypoint → WaypointEntered, visite ouverte dans waypoint_visits avec state='inside'.

2

Detection de sortie

haversine > radius_m pour un véhicule avec visite active → WaypointExited, duration_seconds calculé, state='exited', exited_at enregistré.

3

Forward-only matching

Pour les waypoints de mission : seuls les sequence_order > currentSequencePosition sont testés. Empêche les faux matchs sur les allers-retours.

4

Waypoints sautés

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).

5

Destination atteinte

WaypointEntered + role='destination' à la bonne position dans la séquence → événement destination_reached (info). Portail de tracking notifié.

6

Passage de frontière

Waypoint type=borderborder_crossing (info). GSM MCC en confirmation secondaire : 624=Cameroun, 622=Tchad. Pas de signal primaire (oscillation d'antenne).

7

Visites simultanées

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.

8

9 types de waypoint

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.

9

Visite GPS éphémère

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.

Matching forward-only sur un aller-retour

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.

Diagramme 2 Douala seq 10 MATCH origin Garoua seq 20 MATCH waypoint N'Djamena seq 30 MATCH destination Garoua seq 40 MATCH waypoint Douala seq 50 MATCH destination Règle forward-only : sequence_order > currentSequencePosition Même waypoint physique (Douala, Garoua) → UUID différents dans mission_waypoints Au départ (seq 10), Douala seq 50 n'est pas teste. Au retour (seq 50), Douala seq 10 est deja passe.

Les 9 types de waypoint

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 nullable (Decision T1-8) Waypoints système (city, rest_area, border, port, checkpoint) ont 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.

Arbre de décision : autorisation d'arrêt

Diagramme 3 Arrêt détecté (StopOpened) Waypoint le plus proche dans BatchContext ? NON Non enregistré is_authorized = false OUI waypoint_type du plus proche ? depot, warehouse, fuel_station, rest_area, port, custom Enregistre is_authorized = true checkpoint, city Non enregistré is_authorized = false border Enregistre (durée) is_authorized = true Seuil 6h (Phase 2) Couche d'alerte (contextuelle, apres classification) Arrêt pendant déviation corridor → alerte | Arrêt nuit + non enregistré + durée > 10 min → alerte Arrêt court de jour sur corridor en lieu inconnu → journal uniquement, pas de notification Zone explicitement restreinte → alerte
2 couches indépendantes Journal : chaque arrêt est classifie "enregistré" ou "non enregistré" — sans jugement. Stocke sur le record stops.is_authorized + nearest_waypoint_id.
Alerte : les notifications ne se déclenchent que sur des conditions contextuelles (déviation, nuit, zone interdite). Un arrêt non enregistré de jour sur le corridor = journal uniquement.

B. Corridor et Déviation 13 cas

É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.

Diagramme 4 CORRIDOR (polygone GeoJSON) 1/3 2/3 3/3 OPEN DeviationOpened started_at = pos 1/3 STOP stopCount++ deviation_with_stop STOP stopCount++ deviation_with_stop max_distance_from_corridor_m RETOUR DeviationReturned Résultat déviation 1. corridor_déviation (warning, auto-resolve au retour) 2. deviation_with_stop #1 (immediate, PAS auto-resolve) 3. deviation_with_stop #2 (immediate, PAS auto-resolve) Sur corridor Hors corridor (déviation) Positions consecutives avant ouverture Limite du corridor
10

Ouverture sur 3 positions

3 positions acceptées consecutives hors polygone (turf.booleanPointInPolygon = false). Filtre le bruit GPS : 1-2 positions hors corridor = fluctuation, pas déviation.

11

Compteur persiste

consecutiveOutsideCount sur la table vehicles. Survit entre batches. Chargé dans VehicleTrackingState, remis à 0 à l'ouverture ou au retour.

12

Distance accumulée

deltaMeters de l'étape 5 ajouté à ActiveDeviation.distanceM à chaque position acceptée hors corridor. Écrit dans corridor_deviations.distance_m au retour.

13

Distance maximale

turf/point-to-line-distance à chaque position hors corridor. Le max est conservé dans ActiveDeviation et écrit dans max_distance_from_corridor_m.

14

Compteur d'arrêts

StopOpened pendant déviation active → ActiveDeviation.stopCount++. Chaque arrêt = un événement deviation_with_stop distinct avec sa propre notification.

15

Retour au corridor

Position re-entre dans le polygone → state='returned', toutes les métriques écrites : distance, durée, stop_count, max_distance, exit/return positions.

16

Déviation abandonnée

Mission terminée avec déviation active → cron cascade, state='abandoned'. Pas d'événement moteur — transition uniquement par cron SQL.

17

Traverse les trips

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.

18

Arrêt + déviation = critique

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.

19

corridor_déviation event

É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.

20

Pause mission + déviation

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.

21

UUID pré-généré

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.

22

Mission obligatoire

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.


Schémas de données

waypoints

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

waypoint_visits

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';

corridor_deviations

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()
);

Automates d'état

Visite de waypoint

Diagramme 5 (aucune) inside exited haversine ≤ radius haversine > radius toujours inside WaypointEntered WaypointExited visite ouverte, mission_waypoint_id lié duration_seconds calculé

Déviation de corridor

Diagramme 6 (aucune) active returned abandoned 3 pos hors corridor re-entre corridor mission terminée (cron cascade) toujours hors corridor distanceM++, métriques DeviationOpened started_at = 1ere pos hors corridor DeviationReturned métriques finales écrites abandoned : cron uniquement, pas d'événement moteur

Règles cles

Positions acceptées uniquement Les étapes 7 (detectWaypoint) et 8 (detectCorridorDeviation) ne traitent que les positions avec quality_flag = 'accepted'. Les positions jitter, spike, filtered et late sont ignorées.
Déviation mission-scoped Pas de mission = pas de polygone corridor = étape 8 entièrement sautée. corridor_deviations.mission_id est NOT NULL. La déviation est un fait lié à une mission, pas à un véhicule.
Waypoints tous les tenants Tous les waypoints du tenant (y compris système) sont testés à chaque position. La détection de waypoint est indépendante de la mission — un arrêt dans une station-service est détecté même sans mission active.
Déviation ne ferme PAS sur arrêt Un arrêt ferme le trip (étape 4) mais PAS la déviation (étape 8). Un episode hors route avec 3 arrêts = 1 déviation + 3 deviation_with_stop. Le dossier d'investigation est complet.

Performance (Decision D-corridor-pure-engine)

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

Configuration (tenant_event_config)

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