Files
geo/api/src/Services/AddressService.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

417 lines
15 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services;
use Database;
use PDO;
use PDOException;
use InvalidArgumentException;
use RuntimeException;
/**
* Service de gestion des adresses
*
* Ce service interroge la base de données externe 'adresses' pour récupérer
* les adresses géographiques dans des secteurs définis.
*/
class AddressService
{
private ?PDO $addressesDb = null;
private PDO $mainDb;
private $buildingService;
public function __construct()
{
try {
$this->addressesDb = \AddressesDatabase::getInstance();
LogService::info('[AddressService] Connexion à la base d\'adresses réussie');
} catch (\Exception $e) {
// Si la connexion échoue, on continue sans la base d'adresses
LogService::error('[AddressService] Connexion à la base d\'adresses impossible', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$this->addressesDb = null;
}
$this->mainDb = \Database::getInstance();
$this->buildingService = new BuildingService();
}
/**
* Vérifie si la connexion à la base d'adresses est active
*
* @return bool
*/
public function isConnected(): bool
{
return $this->addressesDb !== null;
}
/**
* Détermine le département de l'entité courante
*
* @param int|null $entityId ID de l'entité
* @return string|null Code département (ex: "22", "23")
*/
private function getDepartmentForEntity(?int $entityId = null): ?string
{
if (!$entityId) {
$entityId = $_SESSION['entity_id'] ?? null;
}
if (!$entityId) {
return null;
}
try {
$query = "SELECT departement FROM entites WHERE id = :entity_id";
$stmt = $this->mainDb->prepare($query);
$stmt->execute(['entity_id' => $entityId]);
$result = $stmt->fetch();
return $result ? $result['departement'] : null;
} catch (\Exception $e) {
return null;
}
}
/**
* Récupère toutes les adresses contenues dans un polygone défini par des coordonnées
* Gère automatiquement les secteurs multi-départements
*
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
* @return array Array des adresses trouvées
*/
public function getAddressesInPolygon(array $coordinates, ?int $entityId = null): array
{
// Si pas de connexion à la base d'adresses, retourner un tableau vide
if (!$this->addressesDb) {
LogService::error('[AddressService] Pas de connexion à la base d\'adresses externe', [
'entity_id' => $entityId
]);
return [];
}
LogService::info('[AddressService] Début recherche adresses', [
'entity_id' => $entityId,
'nb_coordinates' => count($coordinates)
]);
if (count($coordinates) < 3) {
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
}
// D'abord, déterminer tous les départements touchés par ce secteur
$boundaryService = new DepartmentBoundaryService();
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
if (empty($departmentsTouched)) {
// Si aucun département n'est trouvé par analyse spatiale,
// chercher d'abord dans le département de l'entité et ses limitrophes
$entityDept = $this->getDepartmentForEntity($entityId);
LogService::info('[AddressService] Département de l\'entité', [
'departement' => $entityDept
]);
if (!$entityDept) {
LogService::error('[AddressService] Impossible de déterminer le département de l\'entité', [
'entity_id' => $entityId
]);
throw new RuntimeException("Impossible de déterminer le département");
}
// Obtenir les départements prioritaires (entité + limitrophes)
$priorityDepts = $boundaryService->getPriorityDepartments($entityDept);
// Log pour debug
LogService::warning('[AddressService] Aucun département trouvé par analyse spatiale', [
'departements_prioritaires' => implode(', ', $priorityDepts)
]);
// Utiliser les départements prioritaires pour la recherche
$departmentsTouched = [];
foreach ($priorityDepts as $deptCode) {
$departmentsTouched[] = ['code_dept' => $deptCode];
}
}
// Créer le polygone SQL à partir des coordonnées
$polygonPoints = [];
foreach ($coordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Collecter les adresses de tous les départements touchés
$allAddresses = [];
foreach ($departmentsTouched as $dept) {
$deptCode = $dept['code_dept'];
$tableName = "cp" . $deptCode;
try {
// Requête pour récupérer les adresses dans le polygone pour ce département
$sql = "SELECT
id,
numero,
rue as voie,
cp as code_postal,
ville as commune,
gps_lat as latitude,
gps_lng as longitude,
x,
y,
code_insee,
nom_ld,
ville_acheminement,
rue_afnor,
source,
certification,
:dept_code as departement
FROM `$tableName`
WHERE ST_Contains(
ST_GeomFromText(:polygon, 4326),
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
)
AND gps_lat != ''
AND gps_lng != ''";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute([
'polygon' => $polygonString,
'dept_code' => $deptCode
]);
$addresses = $stmt->fetchAll();
// Ajouter les adresses à la collection globale
foreach ($addresses as $address) {
$allAddresses[] = $address;
}
// Log pour debug
LogService::info('[AddressService] Recherche dans table', [
'table' => $tableName,
'departement' => $deptCode,
'nb_adresses' => count($addresses)
]);
} catch (PDOException $e) {
// Log l'erreur mais continue avec les autres départements
LogService::error('[AddressService] Erreur SQL', [
'table' => $tableName,
'departement' => $deptCode,
'error' => $e->getMessage(),
'code' => $e->getCode()
]);
}
}
LogService::info('[AddressService] Fin recherche adresses', [
'total_adresses' => count($allAddresses)
]);
return $allAddresses;
}
/**
* Enrichit les adresses avec les données bâtiments depuis la base 'batiments'
*
* Pour chaque adresse trouvée, cette méthode cherche si un bâtiment existe
* et ajoute les métadonnées (nb_log, residence, fk_habitat, etc.)
*
* @param array $addresses Liste d'adresses depuis getAddressesInPolygon()
* @param int|null $entityId ID de l'entité (pour logs)
* @return array Adresses enrichies avec données bâtiment
*/
public function enrichAddressesWithBuildings(array $addresses, ?int $entityId = null): array
{
if (empty($addresses)) {
return [];
}
LogService::info('[AddressService] Début enrichissement avec bâtiments', [
'entity_id' => $entityId,
'nb_addresses' => count($addresses)
]);
try {
$enrichedAddresses = $this->buildingService->enrichAddresses($addresses);
// Compter les immeubles vs maisons
$nbImmeubles = 0;
$nbMaisons = 0;
foreach ($enrichedAddresses as $addr) {
if (isset($addr['fk_habitat']) && $addr['fk_habitat'] == 2) {
$nbImmeubles++;
} else {
$nbMaisons++;
}
}
LogService::info('[AddressService] Fin enrichissement avec bâtiments', [
'total_adresses' => count($enrichedAddresses),
'nb_immeubles' => $nbImmeubles,
'nb_maisons' => $nbMaisons
]);
return $enrichedAddresses;
} catch (\Exception $e) {
LogService::error('[AddressService] Erreur lors de l\'enrichissement', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
// En cas d'erreur, retourner les adresses sans enrichissement
return $addresses;
}
}
/**
* Récupère les adresses dans un rayon autour d'un point
*
* @param float $latitude Latitude du centre
* @param float $longitude Longitude du centre
* @param float $radiusMeters Rayon en mètres
* @param int|null $entityId ID de l'entité (pour déterminer le département)
* @return array Array des adresses trouvées
*/
public function getAddressesInRadius(float $latitude, float $longitude, float $radiusMeters, ?int $entityId = null): array
{
// Déterminer le département
$dept = $this->getDepartmentForEntity($entityId);
if (!$dept) {
throw new RuntimeException("Impossible de déterminer le département de l'entité");
}
// Nom de la table selon le département
$tableName = "cp" . $dept;
try {
// Utiliser ST_Distance_Sphere pour calculer la distance en mètres
$sql = "SELECT
id,
numero,
rue as voie,
cp as code_postal,
ville as commune,
gps_lat as latitude,
gps_lng as longitude,
ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
ST_GeomFromText(:point, 4326)
) as distance
FROM `$tableName`
WHERE ST_Distance_Sphere(
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8))),
ST_GeomFromText(:point, 4326)
) <= :radius
AND gps_lat != ''
AND gps_lng != ''
ORDER BY distance";
$point = "POINT($longitude $latitude)";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute([
'point' => $point,
'radius' => $radiusMeters
]);
return $stmt->fetchAll();
} catch (PDOException $e) {
throw new RuntimeException("Erreur lors de la récupération des adresses dans la table $tableName : " . $e->getMessage());
}
}
/**
* Compte le nombre d'adresses dans un polygone
* Gère automatiquement les secteurs multi-départements
*
* @param array $coordinates Array de coordonnées [[lat, lng], [lat, lng], ...]
* @param int|null $entityId ID de l'entité (pour déterminer le département principal)
* @return int Nombre d'adresses
*/
public function countAddressesInPolygon(array $coordinates, ?int $entityId = null): int
{
// Si pas de connexion à la base d'adresses, retourner 0
if (!$this->addressesDb) {
return 0;
}
if (count($coordinates) < 3) {
throw new InvalidArgumentException("Un polygone doit avoir au moins 3 points");
}
// D'abord, déterminer tous les départements touchés par ce secteur
$boundaryService = new DepartmentBoundaryService();
$departmentsTouched = $boundaryService->getDepartmentsForSector($coordinates);
if (empty($departmentsTouched)) {
// Si aucun département n'est trouvé, utiliser le département de l'entité
$dept = $this->getDepartmentForEntity($entityId);
if (!$dept) {
throw new RuntimeException("Impossible de déterminer le département");
}
$departmentsTouched = [['code_dept' => $dept]];
}
// Créer le polygone SQL à partir des coordonnées
$polygonPoints = [];
foreach ($coordinates as $coord) {
if (!isset($coord[0]) || !isset($coord[1])) {
throw new InvalidArgumentException("Chaque coordonnée doit avoir une latitude et une longitude");
}
$polygonPoints[] = $coord[1] . ' ' . $coord[0]; // MySQL attend longitude latitude
}
// Fermer le polygone
$polygonPoints[] = $polygonPoints[0];
$polygonString = 'POLYGON((' . implode(',', $polygonPoints) . '))';
// Compter les adresses dans tous les départements touchés
$totalCount = 0;
foreach ($departmentsTouched as $dept) {
$deptCode = $dept['code_dept'];
$tableName = "cp" . $deptCode;
try {
$sql = "SELECT COUNT(*) as count
FROM `$tableName`
WHERE ST_Contains(
ST_GeomFromText(:polygon, 4326),
POINT(CAST(gps_lng AS DECIMAL(10,8)), CAST(gps_lat AS DECIMAL(10,8)))
)
AND gps_lat != ''
AND gps_lng != ''";
$stmt = $this->addressesDb->prepare($sql);
$stmt->execute(['polygon' => $polygonString]);
$result = $stmt->fetch();
$deptCount = (int)$result['count'];
$totalCount += $deptCount;
} catch (PDOException $e) {
// Log l'erreur mais continue avec les autres départements
error_log("Erreur de comptage pour le département $deptCode : " . $e->getMessage());
}
}
return $totalCount;
}
}