Files
geo/api/src/Services/SectorService.php
Pierre 232940b1eb feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques
- #18: Optimisation services API et logs
- #19: Corrections Flutter widgets et repositories
- #20: Fix création passage - détection automatique ope_users.id vs users.id

Suppression dossier web/ (migration vers app Flutter)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:11:15 +01:00

293 lines
9.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use Database;
use PDO;
/**
* Service global pour la gestion des secteurs
*
* Fournit des fonctions réutilisables pour :
* - Géocoder une adresse via api-adresse.data.gouv.fr
* - Trouver un secteur à partir de coordonnées GPS
* - Trouver un secteur à partir d'une adresse
*/
class SectorService
{
private PDO $db;
public function __construct()
{
$this->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;
}
}