Files
projet_gestion_commande/backend/gestion/handlers/traffic.go
T

625 lines
17 KiB
Go

// ============================================
// handlers/traffic_handlers.go - COMPLET
// ============================================
package handlers
import (
"encoding/json"
"fmt"
"gestion/db"
"gestion/models"
"gestion/services"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
)
// ============================================
// INCIDENTS TRAFFIC TOMTOM
// ============================================
// GetIncidentsAroundDeliveryPerson récupère les incidents autour d'un livreur
// GET /api/v2/admin/traffic/delivery/:username/incidents
func GetIncidentsAroundDeliveryPerson(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" && userRole != "livreur" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
username := c.Param("username")
if username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Username requis"})
return
}
// Récupérer position du livreur
lat, lon, err := database.GetDeliveryPersonLocation(username)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Position livreur non trouvée",
"details": err.Error(),
})
return
}
// Rayon de recherche par défaut: 5 km
radius := 5000 // mètres
// Récupérer incidents TomTom
incidents, err := fetchIncidents(lat, lon, radius)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération incidents",
"details": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"username": username,
"location": gin.H{"latitude": lat, "longitude": lon},
"radius_km": radius / 1000,
"incidents": incidents,
"count": len(incidents),
})
}
// GetIncidentsForAllDeliveries récupère incidents + routes pour tous livreurs actifs
// GET /api/v2/admin/traffic/incidents/all
func GetIncidentsForAllDeliveries(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
// Récupérer tous les livreurs disponibles depuis Redis
livreurs, err := database.GetAvailableDeliveryPersonsRedis()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur récupération livreurs",
"details": err.Error(),
})
return
}
results := []gin.H{}
for _, livreur := range livreurs {
username := livreur.Username
status := livreur.Status
// Sauter les livreurs offline
if status == "offline" {
continue
}
// Position livreur
lat, lon, err := database.GetDeliveryPersonLocation(username)
if err != nil {
log.Printf("⚠️ Position non trouvée pour %s", username)
continue
}
// Vérifier s'il a une commande en cours
commandID := livreur.CurrentCommand
if commandID == 0 {
// Pas de livraison en cours
results = append(results, gin.H{
"username": username,
"status": status,
"location": gin.H{"latitude": lat, "longitude": lon},
"has_delivery": false,
"incidents": []gin.H{},
"route": nil,
})
continue
}
// Récupérer la commande
command, err := database.GetCommandByID(commandID)
if err != nil {
log.Printf("⚠️ Commande %d non trouvée", commandID)
continue
}
// Coordonnées destination
var destLat, destLon float64
if dLat, ok := getFloatFromMap(command, "dest_latitude"); ok && dLat != 0 {
destLat = dLat
}
if dLon, ok := getFloatFromMap(command, "dest_longitude"); ok && dLon != 0 {
destLon = dLon
}
// Si pas de coordonnées, géocoder
if destLat == 0 || destLon == 0 {
address, ok := command["adresse"].(string)
if !ok || address == "" || address == "Adresse non spécifiée" {
continue
}
geoService := c.MustGet("geoService").(*services.GeoService)
location, err := geoService.GeocodeAddress(address)
if err != nil {
log.Printf("⚠️ Géocodage échoué pour %s", address)
continue
}
destLat = location.Latitude
destLon = location.Longitude
}
// Récupérer incidents sur le trajet
incidents, _ := fetchIncidentsOnRoute(lat, lon, destLat, destLon)
// Convertir incidents en gin.H pour JSON
incidentsJSON := make([]gin.H, len(incidents))
for i, inc := range incidents {
incidentsJSON[i] = gin.H{
"type": inc.Type,
"icon": inc.Icon,
"description": inc.Description,
}
}
// Calculer route avec trafic
routeSummary, err := fetchRouteSummary(lat, lon, destLat, destLon)
if err != nil {
log.Printf("⚠️ Erreur calcul route pour %s", username)
routeSummary = models.RouteSummary{}
}
results = append(results, gin.H{
"username": username,
"status": status,
"location": gin.H{"latitude": lat, "longitude": lon},
"destination": gin.H{"latitude": destLat, "longitude": destLon},
"has_delivery": true,
"command_id": commandID,
"incidents": incidentsJSON,
"route": routeSummary,
})
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"deliveries": results,
"count": len(results),
})
}
// ============================================
// MISE À JOUR ETA AVEC TRAFIC
// ============================================
func UpdateETAWithRealTraffic(c *gin.Context) {
database := c.MustGet("database").(*db.Database)
userRole := c.GetString("role")
if userRole != "admin" && userRole != "livreur" {
c.JSON(http.StatusForbidden, gin.H{"error": "Accès refusé"})
return
}
// Récupérer l'ID de la commande
commandID, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID invalide"})
return
}
// Récupérer la commande
command, err := database.GetCommandByID(commandID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Commande non trouvée",
"command_id": commandID,
})
return
}
// Vérifier qu'un livreur est assigné
livreurAssign, ok := command["livreur_assign"].(string)
if !ok || livreurAssign == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Aucun livreur assigné",
"command_id": commandID,
})
return
}
// Position actuelle du livreur
lat, lon, err := database.GetDeliveryPersonLocation(livreurAssign)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "Position livreur introuvable",
"livreur": livreurAssign,
"details": err.Error(),
})
return
}
var destLat, destLon float64
// 🔹 1. Tenter de récupérer depuis le cache Redis (clé spécifique pour destination)
destCacheKey := fmt.Sprintf("command:destination:%d", commandID)
destData, err := db.Redis.Get(db.RedisCtx, destCacheKey).Result()
if err == nil && destData != "" {
var coords struct {
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
if err := json.Unmarshal([]byte(destData), &coords); err == nil && coords.Lat != 0 && coords.Lon != 0 {
destLat = coords.Lat
destLon = coords.Lon
log.Printf("📍 Destination trouvée dans cache Redis pour commande %d", commandID)
}
}
// 🔹 2. Fallback: récupérer depuis la DB
if destLat == 0 || destLon == 0 {
if dLat, okLat := getFloatFromMap(command, "dest_latitude"); okLat && dLat != 0 {
destLat = dLat
}
if dLon, okLon := getFloatFromMap(command, "dest_longitude"); okLon && dLon != 0 {
destLon = dLon
}
}
// 🔹 3. Si toujours pas de coordonnées, géocoder l'adresse
if destLat == 0 || destLon == 0 {
address, ok := command["adresse"].(string)
if !ok || address == "" || address == "Adresse non spécifiée" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "Adresse de destination manquante ou invalide",
"command_id": commandID,
})
return
}
geoService := c.MustGet("geoService").(*services.GeoService)
location, err := geoService.GeocodeAddress(address)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Impossible de géocoder l'adresse",
"address": address,
"command_id": commandID,
"details": err.Error(),
})
return
}
destLat = location.Latitude
destLon = location.Longitude
log.Printf("📍 Adresse géocodée pour commande %d: %s -> (%.6f, %.6f)",
commandID, address, destLat, destLon)
}
// 🔹 4. Sauvegarder les coordonnées destination dans le cache Redis
coordsJSON, _ := json.Marshal(map[string]float64{
"lat": destLat,
"lon": destLon,
})
if err := db.Redis.Set(db.RedisCtx, destCacheKey, coordsJSON, 4*time.Hour).Err(); err != nil {
log.Printf("⚠️ Impossible de sauvegarder destination dans Redis: %v", err)
}
// 🔹 5. Calculer le temps réel avec TomTom Routing API
routeSummary, err := fetchRouteSummary(lat, lon, destLat, destLon)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Impossible de calculer l'itinéraire",
"command_id": commandID,
"from": gin.H{"lat": lat, "lon": lon},
"to": gin.H{"lat": destLat, "lon": destLon},
"details": err.Error(),
})
return
}
// 🔹 6. Mettre à jour l'ETA dans Redis
err = database.SetCommandETA(commandID, routeSummary.TravelTimeInMinutes)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "Erreur mise à jour ETA",
"command_id": commandID,
"details": err.Error(),
})
return
}
log.Printf("✅ ETA mis à jour pour commande %d: %d min (trafic réel inclus)",
commandID, routeSummary.TravelTimeInMinutes)
c.JSON(http.StatusOK, gin.H{
"success": true,
"command_id": commandID,
"eta_minutes": routeSummary.TravelTimeInMinutes,
"distance_km": routeSummary.LengthInKm,
"with_traffic": true,
"route_summary": routeSummary,
})
}
// ============================================
// FONCTIONS HELPERS - TOMTOM API
// ============================================
// fetchIncidents récupère les incidents de trafic autour d'une position
func fetchIncidents(lat, lon float64, radius int) ([]models.Incident, error) {
apiKey := os.Getenv("TOMTOM_API_KEY")
if apiKey == "" {
return nil, fmt.Errorf("TOMTOM_API_KEY non configurée")
}
// API TomTom Traffic Incidents
url := fmt.Sprintf(
"https://api.tomtom.com/traffic/services/5/incidentDetails?key=%s&bbox=%f,%f,%f,%f&fields={incidents{type,geometry{type,coordinates},properties{iconCategory,magnitudeOfDelay,events{description,code,iconCategory}}}}",
apiKey,
lon-0.05, lat-0.05, // Southwest corner
lon+0.05, lat+0.05, // Northeast corner
)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return nil, fmt.Errorf("erreur requête incidents: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("API incidents error %d: %s", resp.StatusCode, string(body))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("erreur lecture réponse: %w", err)
}
var incidentResponse models.IncidentResponse
err = json.Unmarshal(body, &incidentResponse)
if err != nil {
return nil, fmt.Errorf("erreur parsing incidents: %w", err)
}
// Convertir en []models.Incident
incidents := make([]models.Incident, len(incidentResponse.Incidents))
for i, inc := range incidentResponse.Incidents {
incidents[i] = models.Incident{
Type: inc.Type,
Icon: inc.Icon,
Description: inc.Description,
}
}
return incidents, nil
}
// fetchIncidentsOnRoute récupère les incidents sur un trajet
func fetchIncidentsOnRoute(startLat, startLon, destLat, destLon float64) ([]models.Incident, error) {
// Calculer la bounding box du trajet
minLat := min(startLat, destLat) - 0.02
maxLat := max(startLat, destLat) + 0.02
minLon := min(startLon, destLon) - 0.02
maxLon := max(startLon, destLon) + 0.02
apiKey := os.Getenv("TOMTOM_API_KEY")
if apiKey == "" {
return []models.Incident{}, nil
}
url := fmt.Sprintf(
"https://api.tomtom.com/traffic/services/5/incidentDetails?key=%s&bbox=%f,%f,%f,%f&fields={incidents{type,geometry{type,coordinates},properties{iconCategory,magnitudeOfDelay,events{description,code,iconCategory}}}}",
apiKey,
minLon, minLat,
maxLon, maxLat,
)
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get(url)
if err != nil {
return []models.Incident{}, nil
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []models.Incident{}, nil
}
body, _ := io.ReadAll(resp.Body)
var incidentResponse models.IncidentResponse
if err := json.Unmarshal(body, &incidentResponse); err != nil {
return []models.Incident{}, nil
}
// Convertir en []models.Incident
incidents := make([]models.Incident, len(incidentResponse.Incidents))
for i, inc := range incidentResponse.Incidents {
incidents[i] = models.Incident{
Type: inc.Type,
Icon: inc.Icon,
Description: inc.Description,
}
}
return incidents, nil
}
// récupère le temps de trajet réel via l'API TomTom Routing
// fetchRouteSummary récupère le temps de trajet réel via l'API TomTom Routing
func fetchRouteSummary(startLat, startLon, destLat, destLon float64) (models.RouteSummary, error) {
apiKey := os.Getenv("TOMTOM_API_KEY")
if apiKey == "" {
return models.RouteSummary{}, fmt.Errorf("TOMTOM_API_KEY non configurée")
}
// Validation des coordonnées
if startLat < -90 || startLat > 90 || destLat < -90 || destLat > 90 {
return models.RouteSummary{}, fmt.Errorf("latitude invalide: start=%.6f, dest=%.6f", startLat, destLat)
}
if startLon < -180 || startLon > 180 || destLon < -180 || destLon > 180 {
return models.RouteSummary{}, fmt.Errorf("longitude invalide: start=%.6f, dest=%.6f", startLon, destLon)
}
// API TomTom Routing: Calculate Route
url := fmt.Sprintf(
"https://api.tomtom.com/routing/1/calculateRoute/%f,%f:%f,%f/json?key=%s&traffic=true&travelMode=car",
startLat, startLon, destLat, destLon, apiKey,
)
log.Printf("🛣️ Appel TomTom: (%.6f,%.6f) -> (%.6f,%.6f)", startLat, startLon, destLat, destLon)
// Timeout réduit à 8 secondes
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Get(url)
if err != nil {
// Fallback: estimation basée sur distance Haversine
distance := haversineDistance(startLat, startLon, destLat, destLon)
estimatedMinutes := int(distance/25*60) + 3 // ~25 km/h en ville + 3 min marge
if estimatedMinutes < 5 {
estimatedMinutes = 5
}
log.Printf("⚠️ TomTom timeout/erreur, fallback: %.2f km -> %d min estimé", distance, estimatedMinutes)
return models.RouteSummary{
TravelTimeInMinutes: estimatedMinutes,
LengthInKm: distance,
}, nil // Pas d'erreur, on retourne l'estimation
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
io.ReadAll(resp.Body) // Lire et ignorer le body pour fermer proprement
// Fallback en cas d'erreur API
distance := haversineDistance(startLat, startLon, destLat, destLon)
estimatedMinutes := int(distance/25*60) + 3
if estimatedMinutes < 5 {
estimatedMinutes = 5
}
log.Printf("⚠️ TomTom API error %d, fallback: %.2f km -> %d min", resp.StatusCode, distance, estimatedMinutes)
return models.RouteSummary{
TravelTimeInMinutes: estimatedMinutes,
LengthInKm: distance,
}, nil
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return models.RouteSummary{}, fmt.Errorf("erreur lecture réponse: %w", err)
}
var routeResponse models.RouteResponse
err = json.Unmarshal(body, &routeResponse)
if err != nil {
return models.RouteSummary{}, fmt.Errorf("erreur parsing routing: %w", err)
}
if len(routeResponse.Routes) == 0 {
return models.RouteSummary{}, fmt.Errorf("aucun itinéraire trouvé")
}
summary := routeResponse.Routes[0].Summary
summary.TravelTimeInMinutes = (summary.TravelTimeInSeconds + 59) / 60
summary.LengthInKm = float64(summary.LengthInMeters) / 1000.0
log.Printf("🛣️ Route calculée: %.2f km, %d min (trafic inclus)", summary.LengthInKm, summary.TravelTimeInMinutes)
return summary, nil
}
// haversineDistance calcule la distance en km entre deux points GPS
func haversineDistance(lat1, lon1, lat2, lon2 float64) float64 {
const R = 6371.0 // Rayon Terre en km
const toRad = math.Pi / 180.0
dLat := (lat2 - lat1) * toRad
dLon := (lon2 - lon1) * toRad
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*toRad)*math.Cos(lat2*toRad)*
math.Sin(dLon/2)*math.Sin(dLon/2)
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
return R * c
}
// getFloatFromMap récupère un float64 depuis une map avec différents types
func getFloatFromMap(m map[string]interface{}, key string) (float64, bool) {
value, exists := m[key]
if !exists || value == nil {
return 0, false
}
switch v := value.(type) {
case float64:
return v, true
case float32:
return float64(v), true
case int:
return float64(v), true
case int64:
return float64(v), true
case int32:
return float64(v), true
case json.Number:
f, err := v.Float64()
if err != nil {
return 0, false
}
return f, true
case string:
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return 0, false
}
return f, true
case []byte:
s := string(v)
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0, false
}
return f, true
default:
return 0, false
}
}
// min retourne le minimum entre deux float64
func min(a, b float64) float64 {
if a < b {
return a
}
return b
}
// max retourne le maximum entre deux float64
func max(a, b float64) float64 {
if a > b {
return a
}
return b
}