// ============================================ // 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 }