db = Database::getInstance(); } /** * Géocode une adresse via api-adresse.data.gouv.fr * * @param string $num Numéro de rue * @param string $bis Complément (bis, ter, etc.) * @param string $rue Nom de la rue * @param string $ville Nom de la ville * @param string $cp Code postal (pour vérifier le département) * @return array|null [lat, lng] ou null si non trouvé ou score trop faible */ public function geocodeAddress(string $num, string $bis, string $rue, string $ville, string $cp): ?array { try { // Construire l'URL de l'API $query = trim($num . $bis) . ' ' . $rue . ' ' . $ville; $url = 'https://api-adresse.data.gouv.fr/search/?q=' . urlencode($query); LogService::info('[SectorService] Géocodage adresse', [ 'url' => $url, 'adresse' => $query ]); // Appel à l'API $ch = curl_init(); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); $json = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode !== 200 || empty($json)) { LogService::warning('[SectorService] Erreur API géocodage', [ 'http_code' => $httpCode, 'adresse' => $query ]); return null; } $data = json_decode($json); if (empty($data->features)) { LogService::info('[SectorService] Aucun résultat de géocodage', [ 'adresse' => $query ]); return null; } $score = $data->features[0]->properties->score ?? 0; // Vérifier le score (> 0.7 = 70% de confiance) if (floatval($score) <= 0.7) { LogService::info('[SectorService] Score géocodage trop faible', [ 'score' => $score, 'adresse' => $query ]); return null; } // Vérifier le département $cpTrouve = $data->features[0]->properties->postcode ?? ''; $deptTrouve = substr($cpTrouve, 0, 2); $cpAmicale = $cp; if (strlen($cpAmicale) == 4) { $cpAmicale = '0' . $cpAmicale; } $deptAmicale = substr($cpAmicale, 0, 2); if ($deptTrouve !== $deptAmicale) { LogService::warning('[SectorService] Département différent', [ 'dept_trouve' => $deptTrouve, 'dept_attendu' => $deptAmicale, 'adresse' => $query ]); return null; } // Extraire les coordonnées [lng, lat] -> [lat, lng] $coordinates = $data->features[0]->geometry->coordinates; $lat = (float)$coordinates[1]; $lng = (float)$coordinates[0]; LogService::info('[SectorService] Géocodage réussi', [ 'lat' => $lat, 'lng' => $lng, 'score' => $score, 'adresse' => $query ]); return ['lat' => $lat, 'lng' => $lng]; } catch (\Exception $e) { LogService::error('[SectorService] Erreur géocodage', [ 'error' => $e->getMessage(), 'adresse' => "$num$bis $rue $ville" ]); return null; } } /** * Trouve le secteur contenant une position GPS pour une opération donnée * * @param int $operationId ID de l'opération * @param float $lat Latitude * @param float $lng Longitude * @return int|null ID du secteur trouvé ou null */ public function findSectorByGps(int $operationId, float $lat, float $lng): ?int { try { // Récupérer tous les secteurs de l'opération avec leur polygone $query = "SELECT id, sector FROM ope_sectors WHERE fk_operation = :operation_id AND chk_active = 1 AND sector IS NOT NULL AND sector != ''"; $stmt = $this->db->prepare($query); $stmt->execute(['operation_id' => $operationId]); $sectors = $stmt->fetchAll(PDO::FETCH_ASSOC); if (empty($sectors)) { LogService::info('[SectorService] Aucun secteur trouvé pour l\'opération', [ 'operation_id' => $operationId ]); return null; } // Tester chaque secteur foreach ($sectors as $sector) { $polygon = $this->parseSectorPolygon($sector['sector']); if (empty($polygon)) { continue; } if ($this->isPointInsidePolygon($lat, $lng, $polygon)) { LogService::info('[SectorService] Secteur trouvé par GPS', [ 'sector_id' => $sector['id'], 'operation_id' => $operationId, 'lat' => $lat, 'lng' => $lng ]); return (int)$sector['id']; } } LogService::info('[SectorService] Aucun secteur ne contient ce point GPS', [ 'operation_id' => $operationId, 'lat' => $lat, 'lng' => $lng, 'nb_sectors_tested' => count($sectors) ]); return null; } catch (\Exception $e) { LogService::error('[SectorService] Erreur findSectorByGps', [ 'error' => $e->getMessage(), 'operation_id' => $operationId, 'lat' => $lat, 'lng' => $lng ]); return null; } } /** * Trouve le secteur pour une adresse (géocodage + recherche GPS) * * @param int $operationId ID de l'opération * @param string $num Numéro de rue * @param string $bis Complément * @param string $rue Nom de la rue * @param string $ville Nom de la ville * @param string $cp Code postal * @return array|null ['sector_id' => int, 'gps_lat' => float, 'gps_lng' => float] ou null */ public function findSectorByAddress(int $operationId, string $num, string $bis, string $rue, string $ville, string $cp): ?array { // Étape 1 : Géocoder l'adresse $coords = $this->geocodeAddress($num, $bis, $rue, $ville, $cp); if (!$coords) { return null; } // Étape 2 : Chercher le secteur avec les coordonnées obtenues $sectorId = $this->findSectorByGps($operationId, $coords['lat'], $coords['lng']); if (!$sectorId) { // Retourner quand même les coordonnées GPS trouvées (utiles pour mettre à jour le passage) return [ 'sector_id' => null, 'gps_lat' => $coords['lat'], 'gps_lng' => $coords['lng'] ]; } return [ 'sector_id' => $sectorId, 'gps_lat' => $coords['lat'], 'gps_lng' => $coords['lng'] ]; } /** * Parse le format de polygone stocké en base (lat/lng#lat/lng#...) * * @param string $sectorString Format "lat/lng#lat/lng#..." * @return array Array de ['lat' => float, 'lng' => float] */ private function parseSectorPolygon(string $sectorString): array { $polygon = []; $points = explode('#', rtrim($sectorString, '#')); foreach ($points as $point) { if (!empty($point) && strpos($point, '/') !== false) { list($lat, $lng) = explode('/', $point); $polygon[] = [ 'lat' => (float)$lat, 'lng' => (float)$lng ]; } } return $polygon; } /** * Vérifie si un point est à l'intérieur d'un polygone * Utilise l'algorithme de ray casting * * @param float $lat Latitude du point * @param float $lng Longitude du point * @param array $polygon Array de ['lat' => float, 'lng' => float] * @return bool */ private function isPointInsidePolygon(float $lat, float $lng, array $polygon): bool { $x = $lat; $y = $lng; $inside = false; $count = count($polygon); for ($i = 0, $j = $count - 1; $i < $count; $j = $i++) { $xi = $polygon[$i]['lat']; $yi = $polygon[$i]['lng']; $xj = $polygon[$j]['lat']; $yj = $polygon[$j]['lng']; $intersect = (($yi > $y) != ($yj > $y)) && ($x < ($xj - $xi) * ($y - $yi) / ($yj - $yi) + $xi); if ($intersect) { $inside = !$inside; } } return $inside; } }