Files
roadwave/docs/domains/content/rules/contenus-geolocalises.md
jpgiannetti 35aaa105d0 docs: améliorer rendu markdown et navigation mkdocs
- Ajouter ADR-018 (librairies Go) dans TECHNICAL.md
- Transformer Shared en menu dépliable dans mkdocs (cohérence avec autres domaines)
- Corriger listes markdown (ajout lignes vides avant listes)
- Corriger line breaks dans génération BDD (étapes "Et" sur nouvelles lignes)
- Ajouter script fix-markdown-lists.sh pour corrections futures

Impacte 86 fichiers de documentation et 164 fichiers BDD générés.
2026-02-09 20:49:52 +01:00

790 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 17. Contenus géolocalisés en mode voiture
### 17.1 Principe général
**Objectif** : Proposer des contenus audio au moment précis où l'utilisateur passe devant un point d'intérêt géographique, pour enrichir son trajet avec des informations contextuelles liées au paysage.
**Contrainte principale** : **Sécurité routière**
- Aucune distraction visuelle (pas de texte à lire)
- Notification sonore uniquement + icône minimale
- Validation par un seul bouton physique au volant ("Suivant")
- App doit être ouverte (premier plan)
**Distinction avec audio-guides** :
| Type | Description | Use case |
|------|-------------|----------|
| **Contenu géolocalisé simple** (cette section) | 1 point GPS unique, 1 contenu audio | Monument, panneau historique, point de vue |
| **Audio-guide multi-séquences** (section 16) | Plusieurs points GPS, séquences enchaînées | Safari-parc, circuit touristique, parcours urbain |
---
### 17.2 Détection et notification
#### 17.2.1 Calcul ETA (Estimated Time of Arrival)
**Méthode** : API GPS native iOS/Android
**Principe** :
- Calcul temps d'arrivée au point GPS basé sur vitesse actuelle et distance
- Notification déclenchée **7 secondes avant** d'atteindre le point
- Permet au conducteur d'avoir le temps de réagir (2s) + décompte (5s)
**Implémentation iOS (Swift)** :
```swift
import CoreLocation
class GeoContentDetector {
let locationManager = CLLocationManager()
var geoContents: [GeoContent] = [] // Points GPS proches
func calculateETA(to targetLocation: CLLocation) -> TimeInterval? {
guard let currentLocation = locationManager.location,
let currentSpeed = currentLocation.speed, // m/s
currentSpeed > 0 else { return nil }
let distance = currentLocation.distance(from: targetLocation) // mètres
let eta = distance / currentSpeed // secondes
return eta
}
func checkTriggers() {
// Appelé toutes les 1 seconde
for geoContent in geoContents {
let targetLocation = CLLocation(
latitude: geoContent.latitude,
longitude: geoContent.longitude
)
// Cas particulier : vitesse très faible
if let speed = locationManager.location?.speed,
speed < 1.4, // < 5 km/h
let distance = locationManager.location?.distance(from: targetLocation),
distance < 50 {
triggerNotification(for: geoContent)
continue
}
// Cas normal : calcul ETA
if let eta = calculateETA(to: targetLocation),
eta <= 7.0 && eta > 0 {
triggerNotification(for: geoContent)
}
}
}
}
```
**Implémentation Android (Kotlin)** :
```kotlin
import android.location.Location
import com.google.android.gms.location.FusedLocationProviderClient
class GeoContentDetector(private val fusedLocationClient: FusedLocationProviderClient) {
private var geoContents: List<GeoContent> = emptyList()
fun calculateETA(targetLocation: Location, currentLocation: Location): Double? {
val speed = currentLocation.speed // m/s
if (speed <= 0) return null
val distance = currentLocation.distanceTo(targetLocation) // mètres
return distance / speed // secondes
}
fun checkTriggers(currentLocation: Location) {
for (geoContent in geoContents) {
val targetLocation = Location("").apply {
latitude = geoContent.latitude
longitude = geoContent.longitude
}
// Cas particulier : vitesse très faible (< 5 km/h)
if (currentLocation.speed < 1.4 &&
currentLocation.distanceTo(targetLocation) < 50) {
triggerNotification(geoContent)
continue
}
// Cas normal : calcul ETA
val eta = calculateETA(targetLocation, currentLocation)
if (eta != null && eta <= 7.0 && eta > 0) {
triggerNotification(geoContent)
}
}
}
}
```
**Cas particulier : vitesse nulle ou très faible** :
- Si vitesse < 5 km/h (1.4 m/s) ET distance < 50m → notification immédiate
- Exemple : user arrêté à un feu rouge à 30m du point
- Évite notification trop tardive quand user redémarre
**Fréquence de vérification** :
- GPS updates : toutes les 1 seconde (balance batterie/précision)
- Calcul ETA : à chaque update GPS
- Notification : déclenchée immédiatement quand ETA ≤ 7s
---
#### 17.2.2 Format de la notification
**Philosophie** : **Minimalisme absolu** pour sécurité routière
**Pas de** :
- Titre texte à lire
- Description longue
- Bouton "Annuler" ou "Plus tard"
- Popup bloquante
- Image/cover du contenu
**Uniquement** :
- Son bref (notification)
- Icône selon tag du contenu
- Compteur chiffres (7→1)
**Icônes par tag** :
| Tag | Icône | Couleur |
|-----|-------|---------|
| Culture générale | 🏛️ | Bleu |
| Histoire | 📜 | Marron |
| Voyage | ✈️ | Vert |
| Famille | 👨‍👩‍👧 | Orange |
| Musique | 🎵 | Rose |
| Sport | ⚽ | Rouge |
| Technologie | 💻 | Gris |
| Automobile | 🚗 | Noir |
| Politique | 🏛️ | Bleu foncé |
| Économie | 📈 | Vert foncé |
| Cryptomonnaie | ₿ | Or |
| Santé | 🏥 | Rouge clair |
| Amour | ❤️ | Rose vif |
**Interface visuelle (minimaliste)** :
```
┌────────────────────────────────────────┐
│ [Player actuel] │
│ ▶️ Podcast en cours... │
│ 0:00 ────●──────────── 12:34 │
│ │
│ [|◀] [⏸️] [▶|] │
├────────────────────────────────────────┤
│ 🏛️ │
│ 7 │
└────────────────────────────────────────┘
```
**Évolution du compteur** :
- Affichage pendant 7 secondes
- Compteur décrémente : 7 → 6 → 5 → 4 → 3 → 2 → 1 → disparaît
- Police grande (72pt), bold, couleur blanche
- Background semi-transparent (noir 50% opacity)
**Notification sonore** :
- **Son** : bip court (0.5s) ou "ding" doux personnalisé RoadWave
- **Volume** : suit le volume système notification (indépendant du volume media)
- **Pas de vibration** : inutile en voiture (téléphone sur support)
- **Configurable** : user peut choisir dans settings :
- Bip système
- Son RoadWave personnalisé
- Muet (visuel uniquement)
---
#### 17.2.2b Conformité CarPlay / Android Auto
**Décision** : Notification sonore uniquement en mode CarPlay/Android Auto
**Contexte** :
- [CarPlay Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/carplay) interdisent les overlays qui "takeover" l'écran
- [Android Auto Media Apps Guidelines](https://developer.android.com/training/cars/media) imposent des interactions minimales
- Sécurité routière maximale = pas de distraction visuelle
**Comportement en mode CarPlay/Android Auto** :
**Désactivé** :
- Icône overlay
- Compteur visuel (7...6...5...)
- Tout élément graphique supplémentaire
**Activé uniquement** :
- Notification sonore (bip ou ding)
- Bouton "Suivant" standard (déjà présent)
**Détection automatique** :
```swift
// iOS - Détection CarPlay
import MediaPlayer
class NotificationManager {
var isCarPlayActive: Bool {
if #available(iOS 14.0, *) {
return MPNowPlayingInfoCenter.default().playbackState == .playing &&
MPPlayableContentManager.shared().context != nil
}
return false
}
func showGeoNotification(content: GeoContent) {
if isCarPlayActive {
// CarPlay : sonore uniquement
AudioServicesPlaySystemSound(1052) // Notification beep
// Pas d'overlay visuel
} else {
// Mode normal : sonore + icône + compteur
showFullNotification(content)
}
}
}
```
```kotlin
// Android - Détection Android Auto
import android.car.Car
class NotificationManager(private val context: Context) {
private fun isAndroidAutoActive(): Boolean {
val carManager = context.getSystemService(Context.CAR_SERVICE) as? Car
return carManager?.isConnected == true
}
fun showGeoNotification(content: GeoContent) {
if (isAndroidAutoActive()) {
// Android Auto : sonore uniquement
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setSound(defaultSoundUri)
.setCategory(NotificationCompat.CATEGORY_NAVIGATION)
.build()
notificationManager.notify(NOTIFICATION_ID, notification)
} else {
// Mode normal : sonore + overlay visuel
showFullNotification(content)
}
}
}
```
**Expérience utilisateur en CarPlay/Android Auto** :
1. User roule, app connectée à CarPlay/Android Auto
2. ETA 7s atteint → notification sonore uniquement (bip)
3. User entend le bip, sait qu'un contenu géolocalisé est disponible
4. User appuie sur bouton "Suivant" physique au volant
5. Décompte 5s démarre (audio continue normalement)
6. Contenu géolocalisé démarre après 5s
**Justification** :
- ✅ Conformité maximale CarPlay/Android Auto guidelines
- ✅ Sécurité routière (pas de distraction visuelle)
- ✅ User peut toujours valider via bouton "Suivant" standard
- ✅ Apps comparables (Waze, Apple Maps) utilisent alertes sonores similaires
**Alternative envisagée** : Mini-badge sur bouton "Suivant"
- Moins invasif qu'un compteur
- Mais toujours considéré comme overlay (zone grise)
- **Décision** : Privilégier conformité maximale (sonore uniquement)
---
#### 17.2.3 Décompte après validation
**User appuie sur "Suivant"** :
**Étape 1 : Transition visuelle (0.3s)**
- Icône + compteur "7" disparaissent avec fade out
- Nouveau compteur "5" apparaît (plus grand, centré)
**Étape 2 : Décompte 5 secondes**
- Compteur décrémente : 5 → 4 → 3 → 2 → 1
- Contenu actuel continue de jouer **normalement** (pas de baisse volume)
- User entend le contenu en cours pendant le décompte
**Étape 3 : Fin du décompte**
- Compteur atteint "0"
- Fade out 0.3s du contenu actuel
- Fade in 0.3s du contenu géolocalisé
- Transition audio fluide (pas de silence)
**Cas particulier : contenu actuel se termine pendant le décompte**
Exemple : User clique "Suivant", décompte démarre à 5s, mais contenu actuel se termine après 2s.
```
Timeline :
T+0s : User clique "Suivant", décompte démarre (5...4...3...)
T+2s : Contenu actuel se termine
→ Contenu suivant du buffer démarre immédiatement
→ Décompte continue (2...1...)
T+5s : Décompte atteint 0
→ Fade out du contenu buffer (celui qui a démarré à T+2s)
→ Fade in du contenu géolocalisé
```
**Justification** :
- Évite silence inconfortable pendant décompte
- User continue d'écouter du contenu intéressant
- Transition naturelle
**Interface pendant décompte** :
```
┌────────────────────────────────────────┐
│ [Player actuel] │
│ ▶️ Podcast en cours... │
│ 0:00 ────●──────────── 12:34 │
│ │
│ [|◀] [⏸️] [▶|] │
├────────────────────────────────────────┤
│ │
│ 3 │
│ │
│ Contenu géolocalisé arrive │
└────────────────────────────────────────┘
```
---
### 17.3 Limitation anti-spam
**Problème identifié** :
- Routes riches en points d'intérêt (parcours touristiques)
- Risque de notifier toutes les 30 secondes
- Fatigue utilisateur + distraction conducteur
**Solution : quota horaire + cooldown**
#### 17.3.1 Quota : 6 contenus géolocalisés par heure
**Règle** :
- Maximum **6 contenus géolocalisés** notifiés par heure
- Fenêtre glissante : calcul sur les 60 dernières minutes (pas réinitialisation à 0h)
- Si quota atteint : notifications suivantes ignorées silencieusement
**Exemple** :
```
10h05 : Contenu 1 notifié ✅ (quota : 1/6)
10h12 : Contenu 2 notifié ✅ (quota : 2/6)
10h18 : Contenu 3 notifié ✅ (quota : 3/6)
10h25 : Contenu 4 notifié ✅ (quota : 4/6)
10h33 : Contenu 5 notifié ✅ (quota : 5/6)
10h41 : Contenu 6 notifié ✅ (quota : 6/6)
10h52 : Contenu 7 détecté mais ignoré ❌ (quota atteint)
11h06 : Contenu 1 expire (>60 min) → quota libéré (5/6)
11h08 : Contenu 8 notifié ✅ (quota : 6/6)
```
**Exception : audio-guides multi-séquences** :
- Un audio-guide avec N séquences compte comme **1 seul contenu** dans le quota
- Une fois démarré, toutes ses séquences sont jouables (pas de limite)
- Exemple : audio-guide 8 séquences = 1 quota, contenus simples restants = 5
**Implémentation backend (Redis)** :
```go
// Structure quota (sorted set avec timestamps)
// Clé : user:{user_id}:geo_quota
// Valeur : ZADD avec timestamp
func CanNotify(userID string) bool {
key := fmt.Sprintf("user:%s:geo_quota", userID)
now := time.Now().Unix()
oneHourAgo := now - 3600
// Supprimer entrées > 1h
redis.ZRemRangeByScore(key, "-inf", oneHourAgo)
// Compter entrées restantes
count := redis.ZCard(key)
return count < 6
}
func RecordNotification(userID string, contentID string) {
key := fmt.Sprintf("user:%s:geo_quota", userID)
now := time.Now().Unix()
// Ajouter entrée avec timestamp actuel
redis.ZAdd(key, now, contentID)
// TTL 1h (sécurité)
redis.Expire(key, 3600)
}
```
---
#### 17.3.2 Cooldown : 10 minutes après notification ignorée
**Règle** :
- Si user ne clique pas sur "Suivant" pendant les 7 secondes
- → Cooldown de **10 minutes** activé
- → Aucune nouvelle notification pendant ce délai
- → Même si quota non atteint
**Justification** :
- User a probablement une raison de ne pas vouloir de contenu géolocalisé maintenant
- Évite harcèlement (notifications répétées ignorées)
- Respecte choix implicite de l'utilisateur
**Timeline exemple** :
```
10h00 : Notification contenu A (7s)
10h00+7s : User n'a pas cliqué → cooldown activé
10h05 : Contenu B détecté → ignoré (cooldown actif)
10h08 : Contenu C détecté → ignoré (cooldown actif)
10h10 : Cooldown expire
10h11 : Contenu D détecté → notification affichée ✅
```
**Implémentation (Redis)** :
```go
func IsInCooldown(userID string) bool {
key := fmt.Sprintf("user:%s:geo_cooldown", userID)
exists := redis.Exists(key)
return exists == 1
}
func ActivateCooldown(userID string) {
key := fmt.Sprintf("user:%s:geo_cooldown", userID)
redis.Set(key, "1", 10*time.Minute)
}
```
**Exception : notification validée (user a cliqué)** :
- Pas de cooldown si user a cliqué sur "Suivant"
- Même si user skip le contenu ensuite (pendant le décompte ou après)
- Cooldown = pénalité uniquement pour notification complètement ignorée
---
### 17.4 Navigation avec contenus géolocalisés
#### 17.4.1 Historique de navigation
**Structure** (voir [../../recommendation/rules/interactions-navigation.md](../../recommendation/rules/interactions-navigation.md#52-commande-précédent)) :
- **10 contenus maximum** en mémoire (Redis List)
- Structure : `[{content_id, position_seconds, listened_at, type}, ...]`
- FIFO : au-delà de 10, suppression du plus ancien
**Comportement avec contenus géolocalisés** :
Les contenus géolocalisés s'intègrent dans l'historique comme des contenus normaux :
```
Historique :
[
{id: "buffer_1", position: 180, type: "contextuel"},
{id: "geo_tour_eiffel", position: 42, type: "geo_anchored"},
{id: "buffer_2", position: 90, type: "neutre"},
{id: "buffer_3", position: 0, type: "contextuel"}
]
```
---
#### 17.4.2 Commande "Précédent" avec contenu géolocalisé
**Cas A : User écoute contenu géolocalisé, puis skip**
Timeline :
1. Contenu buffer_1 joue (position 3:00)
2. Notification contenu géolocalisé
3. User clique "Suivant" → décompte 5s → contenu géolocalisé démarre
4. User écoute 42 secondes du contenu géolocalisé
5. User appuie "Suivant" (skip) → contenu buffer_2 démarre
6. User appuie "Précédent" → **retour au contenu géolocalisé à 42s**
**Règle** : Comme décrit dans [../../recommendation/rules/interactions-navigation.md](../../recommendation/rules/interactions-navigation.md#52-commande-précédent) :
- Si temps écouté ≥ 10 secondes → replay contenu actuel depuis début
- Si temps écouté < 10 secondes → retour contenu précédent (position exacte)
Dans ce cas : 42s ≥ 10s → le contenu géolocalisé reprend à 42s (où user l'avait arrêté)
---
**Cas B : User ne clique pas pendant notification (ignore)**
Timeline :
1. Contenu buffer_1 joue
2. Notification contenu géolocalisé (7s)
3. User ne clique pas → notification disparaît
4. Contenu buffer_1 continue
5. User appuie "Précédent" → **retour contenu avant buffer_1**
**Règle** : Contenu géolocalisé ignoré n'entre PAS dans l'historique.
---
**Cas C : User skip pendant le décompte**
Timeline :
1. Notification contenu géolocalisé
2. User clique "Suivant" → décompte 5s démarre (5...4...3...)
3. User re-clique "Suivant" pendant le décompte → **annule le décompte**
4. Contenu suivant du buffer démarre
5. Contenu géolocalisé n'entre PAS dans l'historique
**Justification** : User a explicitement annulé, pas d'intérêt pour ce contenu.
---
### 17.5 Basculement automatique voiture ↔ piéton
**Principe** : Détection automatique du mode selon vitesse GPS, sans action utilisateur.
#### 17.5.1 Calcul vitesse moyenne
**Méthode** : Moyenne glissante sur 30 secondes
```javascript
// Historique GPS : 30 derniers points (1 point/seconde)
const gpsHistory = [
{lat: 48.8584, lon: 2.2945, speed: 8.3, timestamp: 1642861200},
{lat: 48.8585, lon: 2.2946, speed: 8.5, timestamp: 1642861201},
// ... 28 autres points
];
// Calcul vitesse moyenne
const avgSpeed = gpsHistory
.reduce((sum, point) => sum + point.speed, 0) / gpsHistory.length;
// km/h = m/s × 3.6
const avgSpeedKmh = avgSpeed * 3.6;
if (avgSpeedKmh < 5) {
mode = "piéton";
} else {
mode = "voiture";
}
```
**Hysteresis (éviter basculements intempestifs)** :
- Nouvelle vitesse doit être stable pendant **10 secondes** avant basculement
- Exemple : user passe de 20 km/h à 3 km/h (arrêt feu rouge)
- Si vitesse remonte à 20 km/h après 8s → pas de basculement
- Si vitesse reste à 3 km/h pendant 10s → basculement piéton
---
#### 17.5.2 Effets du basculement
**Passage voiture → piéton** :
| Paramètre | Avant (voiture) | Après (piéton) |
|-----------|----------------|----------------|
| **App requise** | Premier plan | Arrière-plan OK |
| **Notification** | Sonore + icône + compteur | Push système standard |
| **Rayon détection** | ETA 7s (distance variable) | 200 mètres fixes |
| **Type contenu** | Tous contenus géolocalisés | Audio-guides uniquement |
**Passage piéton → voiture** :
| Paramètre | Avant (piéton) | Après (voiture) |
|-----------|---------------|----------------|
| **App requise** | Arrière-plan OK | Premier plan |
| **Notification** | Push système standard | Sonore + icône + compteur |
| **Rayon détection** | 200 mètres fixes | ETA 7s (distance variable) |
| **Type contenu** | Audio-guides uniquement | Tous contenus géolocalisés |
**Transition fluide** :
- Pas de popup ou message à l'utilisateur
- Basculement invisible et automatique
- Permissions ajustées automatiquement (si déjà accordées)
---
### 17.6 Edge cases
#### 17.6.1 User passe trop vite devant le point (>130 km/h)
**Scénario** : Autoroute A6, vitesse 130 km/h, contenu géolocalisé détecté.
**Calcul** :
- Vitesse : 130 km/h = 36.1 m/s
- ETA 7s → distance notification : 7 × 36.1 = **252 mètres** avant le point
- User a 7s pour cliquer "Suivant"
- Décompte 5s démarre
- À 130 km/h, user parcourt : 5 × 36.1 = 180m pendant décompte
- Distance restante au démarrage contenu : 252 - 180 = **72 mètres avant le point**
**Conclusion** : Le système fonctionne même à très haute vitesse ✅
**Cas extrême : 180 km/h** (illégal mais théoriquement possible) :
- Vitesse : 180 km/h = 50 m/s
- ETA 7s → distance notification : 350m avant le point
- Décompte 5s : user parcourt 250m
- Distance restante : 350 - 250 = **100m avant le point**
- Contenu démarre encore avant le point ✅
---
#### 17.6.2 Multiples points géolocalisés proches
**Scénario** : Route départementale avec 3 châteaux espacés de 800m chacun.
```
Château A ----800m---- Château B ----800m---- Château C
```
À 50 km/h (13.9 m/s), user met 57 secondes pour parcourir 800m.
**Timeline** :
```
T+0s : Notification Château A (7s avant)
T+7s : User clique "Suivant" → décompte 5s
T+12s : Contenu Château A démarre
T+30s : Contenu Château A se termine (durée 18s)
T+30s : Retour buffer normal (contenu contextuel)
T+50s : Notification Château B devrait se déclencher
MAIS : quota 1/6 utilisé, pas de cooldown (user a validé A)
→ Notification affichée ✅
T+57s : User clique "Suivant" → contenu Château B démarre
...
```
**Problème** : Si user ignore une notification, cooldown 10 min bloque les suivantes.
**Solution proposée** : **Ajuster le cooldown selon validation précédente**
Nouvelle règle :
- Notification validée (user a cliqué) : pas de cooldown
- Notification ignorée (user n'a pas cliqué) : cooldown 10 min
- Exception : si 2+ notifications validées consécutives, cooldown réduit à 5 min
**Implémentation** :
```go
func CalculateCooldown(userID string) time.Duration {
// Récupérer historique dernières notifications
history := getNotificationHistory(userID, 3) // 3 dernières
validatedCount := 0
for _, notif := range history {
if notif.Validated {
validatedCount++
}
}
if validatedCount >= 2 {
return 5 * time.Minute // Cooldown réduit
}
return 10 * time.Minute // Cooldown standard
}
```
---
#### 17.6.3 User arrêté longtemps près d'un point (parking)
**Scénario** : User se gare à 30m d'un château, sort de voiture, visite 1h, revient.
**Problème** :
- Vitesse < 5 km/h + distance < 50m → notification immédiate
- Mais user en mode "stationnement", pas en mode "conduite"
**Solution** : **Détection mode stationnement**
Règle :
- Si vitesse < 1 km/h pendant **2 minutes** consécutives
- → Mode "stationnement" activé
- → Pas de notification de contenus géolocalisés
- → Basculement automatique en mode piéton (push arrière-plan)
**Reprise conduite** :
- Vitesse > 5 km/h pendant 10s
- → Mode "voiture" réactivé
- → Notifications reprennent (si quota non atteint)
---
### 17.7 Récapitulatif
| Point | Décision | Justification |
|-------|----------|---------------|
| **Détection** | ETA 7s avant point (API GPS native) | Précis, fiable, adapté à toutes vitesses |
| **Notification** | Sonore + icône + compteur (pas de texte) | Sécurité routière, minimalisme |
| **Validation** | Bouton "Suivant" uniquement | Un seul geste, simple, pas de distraction |
| **Décompte** | 5s avec contenu actuel qui continue | Évite silence, fluidité |
| **Quota** | 6 contenus/heure, fenêtre glissante | Anti-spam efficace |
| **Cooldown** | 10 min après notification ignorée | Respecte choix utilisateur |
| **Exception audio-guides** | Toutes séquences comptent comme 1 | Encourage création audio-guides |
| **Basculement auto** | Vitesse < 5 km/h = piéton, ≥ 5 km/h = voiture | Transparent, pas de friction |
| **Mode stationnement** | Vitesse < 1 km/h pendant 2 min → piéton | Évite notifications inutiles |
---
## Récapitulatif Section 17
| Point | Décision | Coût | Complexité |
|-------|----------|------|------------|
| **17.1** Principe général | Notification 7s avant, app ouverte requise | 0€ | Faible |
| **17.2.1** Calcul ETA | API GPS native iOS/Android | 0€ | Faible |
| **17.2.2** Format notification | Sonore + icône + compteur (minimaliste) | 0€ | Faible |
| **17.2.3** Décompte 5s | Contenu actuel continue, transition fluide | 0€ | Faible |
| **17.3.1** Quota 6/h | Redis sorted set, fenêtre glissante | 0€ | Moyenne |
| **17.3.2** Cooldown 10 min | Redis TTL après ignorance | 0€ | Faible |
| **17.4** Historique navigation | 10 contenus max, FIFO, comme contenus normaux | 0€ | Faible |
| **17.5** Basculement auto | Vitesse moyenne 30s, hysteresis 10s | 0€ | Moyenne |
| **17.6** Edge cases | Haute vitesse, points multiples, stationnement | 0€ | Moyenne |
**Coût total MVP : 0€** (GPS natif, Redis existant)
---
## Points d'attention pour Gherkin
- Tester calcul ETA à différentes vitesses (10 km/h, 50 km/h, 130 km/h)
- Tester cas vitesse < 5 km/h ET distance < 50m (notification immédiate)
- Tester affichage notification (icône + compteur, pas de texte)
- Tester validation "Suivant" → décompte 5s → contenu démarre
- Tester contenu actuel se termine pendant décompte → buffer démarre
- Tester quota 6/h : 7e notification ignorée
- Tester cooldown 10 min après notification ignorée
- Tester exception audio-guides (toutes séquences = 1 quota)
- Tester navigation "Précédent" après skip contenu géolocalisé (position sauvegardée)
- Tester basculement voiture → piéton (vitesse < 5 km/h stable 10s)
- Tester basculement piéton → voiture (vitesse ≥ 5 km/h stable 10s)
- Tester mode stationnement (vitesse < 1 km/h pendant 2 min)
- Tester multiples points proches (notifications successives si quota OK)
- Tester haute vitesse (130 km/h) : notification 252m avant, contenu démarre au bon moment