documentation wiki osm, ajout dashboard issues osmose

This commit is contained in:
Tykayn 2025-08-31 11:06:54 +02:00 committed by tykayn
parent b28f8eac63
commit 7665f1d99c
12 changed files with 1758 additions and 76 deletions

View file

@ -1826,11 +1826,48 @@ final class AdminController extends AbstractController
{
$this->actionLogger->log('admin/extract_insee_zones', []);
// Créer le dossier oss_data s'il n'existe pas
$ossDataDir = __DIR__ . '/../../oss_data';
if (!is_dir($ossDataDir)) {
mkdir($ossDataDir, 0755, true);
}
// Vérifier que le fichier france-latest.osm.pbf existe
$francePbfFile = __DIR__ . '/../../france-latest.osm.pbf';
$francePbfFile = $ossDataDir . '/france-latest.osm.pbf';
if (!file_exists($francePbfFile)) {
$this->addFlash('error', 'Le fichier france-latest.osm.pbf n\'existe pas. Veuillez le télécharger depuis https://download.geofabrik.de/europe/france.html');
return $this->redirectToRoute('app_admin');
$this->actionLogger->log('admin/download_france_pbf', []);
$this->addFlash('info', 'Le fichier france-latest.osm.pbf n\'existe pas. Téléchargement en cours depuis Geofabrik...');
// URL de téléchargement
$downloadUrl = 'https://download.geofabrik.de/europe/france-latest.osm.pbf';
// Télécharger le fichier
try {
$context = stream_context_create([
'http' => [
'header' => "User-Agent: OSM-Commerces/1.0\r\n"
]
]);
// Utiliser file_get_contents pour télécharger le fichier
$fileContent = file_get_contents($downloadUrl, false, $context);
if ($fileContent === false) {
throw new \Exception('Échec du téléchargement');
}
// Sauvegarder le fichier
if (file_put_contents($francePbfFile, $fileContent) === false) {
throw new \Exception('Échec de l\'écriture du fichier');
}
$this->addFlash('success', 'Le fichier france-latest.osm.pbf a été téléchargé avec succès.');
} catch (\Exception $e) {
$this->actionLogger->log('error_download_france_pbf', [
'error' => $e->getMessage()
]);
$this->addFlash('error', 'Erreur lors du téléchargement du fichier france-latest.osm.pbf: ' . $e->getMessage());
return $this->redirectToRoute('app_admin');
}
}
// Vérifier que le dossier polygons existe
@ -2008,6 +2045,571 @@ final class AdminController extends AbstractController
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/completion-statistics', name: 'app_admin_completion_statistics')]
public function completionStatistics(Request $request): Response
{
$this->actionLogger->log('admin/completion_statistics', []);
// Récupérer le thème sélectionné (par défaut: 'places')
$theme = $request->query->get('theme', 'places');
// Récupérer le niveau géographique sélectionné (par défaut: 'department')
$level = $request->query->get('level', 'department');
// Récupérer tous les thèmes disponibles
$themes = $this->followUpService->getFollowUpThemes();
// Récupérer les données de complétion selon le niveau géographique
$completionData = [];
$chartLabels = [];
$chartData = [];
switch ($level) {
case 'department':
// Agréger les données par département
$completionData = $this->aggregateCompletionByDepartment($theme);
break;
case 'region':
// Agréger les données par région
$completionData = $this->aggregateCompletionByRegion($theme);
break;
case 'country':
// Agréger les données pour la France entière
$completionData = $this->aggregateCompletionForCountry($theme);
break;
case 'city':
// Récupérer les données pour les villes (limité aux 50 plus grandes)
$completionData = $this->aggregateCompletionForTopCities($theme, 50);
break;
}
// Préparer les données pour les graphiques
foreach ($completionData as $key => $value) {
$chartLabels[] = $key;
$chartData[] = $value;
}
return $this->render('admin/completion_statistics.html.twig', [
'theme' => $theme,
'level' => $level,
'themes' => $themes,
'chartLabels' => json_encode($chartLabels),
'chartData' => json_encode($chartData),
'completionData' => $completionData
]);
}
/**
* Agrège les données de complétion par département
*/
private function aggregateCompletionByDepartment(string $theme): array
{
$result = [];
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
// Grouper par département (2 premiers chiffres du code INSEE)
$departmentData = [];
$departmentCounts = [];
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode || strlen($inseeCode) < 2) {
continue;
}
// Extraire le code département (2 premiers chiffres du code INSEE)
$departmentCode = substr($inseeCode, 0, 2);
// Cas particuliers pour la Corse
if ($departmentCode === '2A' || $departmentCode === '2B') {
$departmentCode = substr($inseeCode, 0, 3);
}
// Récupérer les mesures pour le thème spécifié
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
if ($latestMeasure !== null) {
if (!isset($departmentData[$departmentCode])) {
$departmentData[$departmentCode] = 0;
$departmentCounts[$departmentCode] = 0;
}
$departmentData[$departmentCode] += $latestMeasure;
$departmentCounts[$departmentCode]++;
}
}
// Calculer les moyennes par département
foreach ($departmentData as $departmentCode => $total) {
if ($departmentCounts[$departmentCode] > 0) {
$result[$departmentCode] = round($total / $departmentCounts[$departmentCode], 2);
}
}
// Trier par code département
ksort($result);
return $result;
}
/**
* Agrège les données de complétion par région
*/
private function aggregateCompletionByRegion(string $theme): array
{
$result = [];
// Mapping des départements vers les régions
$departmentToRegion = $this->getDepartmentToRegionMapping();
// Récupérer les données par département
$departmentData = $this->aggregateCompletionByDepartment($theme);
// Grouper par région
$regionData = [];
$regionCounts = [];
foreach ($departmentData as $departmentCode => $value) {
// Trouver la région correspondante
$region = $departmentToRegion[$departmentCode] ?? 'Autre';
if (!isset($regionData[$region])) {
$regionData[$region] = 0;
$regionCounts[$region] = 0;
}
$regionData[$region] += $value;
$regionCounts[$region]++;
}
// Calculer les moyennes par région
foreach ($regionData as $region => $total) {
if ($regionCounts[$region] > 0) {
$result[$region] = round($total / $regionCounts[$region], 2);
}
}
// Trier par nom de région
ksort($result);
return $result;
}
/**
* Agrège les données de complétion pour la France entière
*/
private function aggregateCompletionForCountry(string $theme): array
{
$result = [];
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
// Récupérer les mesures pour le thème spécifié
$totalMeasure = 0;
$count = 0;
foreach ($allStats as $stat) {
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
if ($latestMeasure !== null) {
$totalMeasure += $latestMeasure;
$count++;
}
}
// Calculer la moyenne nationale
if ($count > 0) {
$result['France'] = round($totalMeasure / $count, 2);
}
return $result;
}
/**
* Agrège les données de complétion pour les principales villes
*/
private function aggregateCompletionForTopCities(string $theme, int $limit = 50): array
{
$result = [];
// Récupérer les Stats triées par population (descendant)
$statsRepo = $this->entityManager->getRepository(Stats::class);
$topCities = $statsRepo->findBy(
['population' => null], // Critère (tous)
['population' => 'DESC'], // Tri par population descendante
$limit // Limite
);
// Récupérer les mesures pour le thème spécifié
foreach ($topCities as $stat) {
$cityName = $stat->getName() ?? $stat->getZone();
$latestMeasure = $this->getLatestMeasureForTheme($stat, $theme);
if ($latestMeasure !== null) {
$result[$cityName] = $latestMeasure;
}
}
// Trier par valeur décroissante
arsort($result);
return $result;
}
/**
* Récupère la dernière mesure pour un thème donné
*/
private function getLatestMeasureForTheme(Stats $stat, string $theme): ?float
{
$cityFollowUps = $stat->getCityFollowUps();
// Filtrer les CityFollowUp par thème et trier par date (descendant)
$filteredFollowUps = $cityFollowUps->filter(function(CityFollowUp $followUp) use ($theme) {
return $followUp->getName() === $theme;
});
if ($filteredFollowUps->isEmpty()) {
return null;
}
// Trier par date (du plus récent au plus ancien)
$iterator = $filteredFollowUps->getIterator();
$iterator->uasort(function(CityFollowUp $a, CityFollowUp $b) {
return $b->getDate() <=> $a->getDate();
});
// Récupérer la mesure la plus récente
$latestFollowUp = $iterator->current();
return $latestFollowUp->getMeasure();
}
#[Route('/admin/osmose-issues-map/{inseeCode}', name: 'app_admin_osmose_issues_map')]
public function osmoseIssuesMap(Request $request, string $inseeCode = null): Response
{
$this->actionLogger->log('admin/osmose_issues_map', [
'insee_code' => $inseeCode
]);
// Si aucun code INSEE n'est fourni, rediriger vers la liste des villes
if (!$inseeCode) {
$this->addFlash('info', 'Veuillez sélectionner une ville pour afficher les problèmes Osmose.');
return $this->redirectToRoute('app_admin');
}
// Récupérer la ville correspondante
$statsRepo = $this->entityManager->getRepository(Stats::class);
$city = $statsRepo->findOneBy(['zone' => $inseeCode]);
if (!$city) {
$this->addFlash('error', 'Ville non trouvée pour le code INSEE ' . $inseeCode);
return $this->redirectToRoute('app_admin');
}
// Récupérer le thème sélectionné (par défaut: tous)
$theme = $request->query->get('theme', 'all');
// Récupérer tous les thèmes disponibles
$themes = $this->followUpService->getFollowUpThemes();
// Récupérer les problèmes Osmose pour cette ville
$osmoseIssues = $this->getOsmoseIssuesForCity($city, $theme);
return $this->render('admin/osmose_issues_map.html.twig', [
'city' => $city,
'theme' => $theme,
'themes' => $themes,
'osmoseIssues' => $osmoseIssues
]);
}
/**
* Récupère les problèmes Osmose pour une ville donnée
*/
private function getOsmoseIssuesForCity(Stats $city, string $theme = 'all'): array
{
$issues = [];
// Coordonnées de la ville
$lat = $city->getLat();
$lon = $city->getLon();
if (!$lat || !$lon) {
return $issues;
}
// Construire l'URL de l'API Osmose
$bbox = $this->calculateBoundingBox($lat, $lon, 5); // 5km autour du centre de la ville
$osmoseApiUrl = sprintf(
'https://osmose.openstreetmap.fr/api/0.3/issues?bbox=%f,%f,%f,%f&item=xxxx&limit=500',
$bbox['min_lon'],
$bbox['min_lat'],
$bbox['max_lon'],
$bbox['max_lat']
);
// Récupérer les items Osmose correspondant aux thèmes
$itemIds = $this->getOsmoseItemIdsForTheme($theme);
if (!empty($itemIds)) {
$osmoseApiUrl = str_replace('xxxx', implode(',', $itemIds), $osmoseApiUrl);
} else {
$osmoseApiUrl = str_replace('&item=xxxx', '', $osmoseApiUrl);
}
try {
// Appeler l'API Osmose
$response = file_get_contents($osmoseApiUrl);
if ($response === false) {
throw new \Exception('Échec de la récupération des données Osmose');
}
$data = json_decode($response, true);
if (isset($data['issues'])) {
foreach ($data['issues'] as $issue) {
// Vérifier si l'issue est dans les limites de la ville (approximativement)
if ($this->isPointInCity($issue['lat'], $issue['lon'], $lat, $lon, 5)) {
$issues[] = [
'id' => $issue['id'],
'title' => $issue['title'] ?? 'Problème sans titre',
'subtitle' => $issue['subtitle'] ?? '',
'lat' => $issue['lat'],
'lon' => $issue['lon'],
'item' => $issue['item'],
'class' => $issue['class'],
'level' => $issue['level'] ?? 2,
'update_timestamp' => $issue['update_timestamp'] ?? null,
'url' => sprintf('https://osmose.openstreetmap.fr/fr/issue/%s', $issue['uuid'])
];
}
}
}
} catch (\Exception $e) {
$this->actionLogger->log('error_osmose_api', [
'insee_code' => $city->getZone(),
'error' => $e->getMessage()
]);
}
return $issues;
}
/**
* Calcule une bounding box autour d'un point
*/
private function calculateBoundingBox(string $lat, string $lon, int $distanceKm = 5): array
{
$lat = (float)$lat;
$lon = (float)$lon;
// Approximation: 1 degré de latitude = 111 km
$latDelta = $distanceKm / 111.0;
// Approximation: 1 degré de longitude = 111 * cos(latitude) km
$lonDelta = $distanceKm / (111.0 * cos(deg2rad($lat)));
return [
'min_lat' => $lat - $latDelta,
'max_lat' => $lat + $latDelta,
'min_lon' => $lon - $lonDelta,
'max_lon' => $lon + $lonDelta
];
}
/**
* Vérifie si un point est dans un rayon donné autour d'une ville
*/
private function isPointInCity(float $pointLat, float $pointLon, string $cityLat, string $cityLon, int $radiusKm = 5): bool
{
$cityLat = (float)$cityLat;
$cityLon = (float)$cityLon;
// Calcul de la distance en km (formule de Haversine)
$earthRadius = 6371; // Rayon de la Terre en km
$dLat = deg2rad($pointLat - $cityLat);
$dLon = deg2rad($pointLon - $cityLon);
$a = sin($dLat/2) * sin($dLat/2) +
cos(deg2rad($cityLat)) * cos(deg2rad($pointLat)) *
sin($dLon/2) * sin($dLon/2);
$c = 2 * atan2(sqrt($a), sqrt(1-$a));
$distance = $earthRadius * $c;
return $distance <= $radiusKm;
}
/**
* Retourne les IDs des items Osmose correspondant à un thème
*/
private function getOsmoseItemIdsForTheme(string $theme): array
{
// Mapping des thèmes vers les items Osmose
$themeToItemsMapping = [
'places' => [8230, 8240, 8250, 8260], // Commerces et services
'restaurants' => [8030, 8031, 8032], // Restaurants et cafés
'hotels' => [8040, 8041, 8042], // Hébergements
'tourism' => [8010, 8011, 8012, 8013], // Tourisme
'leisure' => [8050, 8051, 8052], // Loisirs
'healthcare' => [8060, 8061, 8062], // Santé
'education' => [8070, 8071, 8072], // Éducation
'transportation' => [4010, 4020, 4030, 4040], // Transport
'amenities' => [8080, 8081, 8082], // Équipements
// Si d'autres thèmes sont nécessaires, ajoutez-les ici
];
// Si le thème est 'all' ou n'existe pas dans le mapping, retourner un tableau vide
if ($theme === 'all' || !isset($themeToItemsMapping[$theme])) {
return [];
}
return $themeToItemsMapping[$theme];
}
/**
* Retourne un mapping des départements vers les régions
*/
private function getDepartmentToRegionMapping(): array
{
return [
// Auvergne-Rhône-Alpes
'01' => 'Auvergne-Rhône-Alpes', // Ain
'03' => 'Auvergne-Rhône-Alpes', // Allier
'07' => 'Auvergne-Rhône-Alpes', // Ardèche
'15' => 'Auvergne-Rhône-Alpes', // Cantal
'26' => 'Auvergne-Rhône-Alpes', // Drôme
'38' => 'Auvergne-Rhône-Alpes', // Isère
'42' => 'Auvergne-Rhône-Alpes', // Loire
'43' => 'Auvergne-Rhône-Alpes', // Haute-Loire
'63' => 'Auvergne-Rhône-Alpes', // Puy-de-Dôme
'69' => 'Auvergne-Rhône-Alpes', // Rhône
'73' => 'Auvergne-Rhône-Alpes', // Savoie
'74' => 'Auvergne-Rhône-Alpes', // Haute-Savoie
// Bourgogne-Franche-Comté
'21' => 'Bourgogne-Franche-Comté', // Côte-d'Or
'25' => 'Bourgogne-Franche-Comté', // Doubs
'39' => 'Bourgogne-Franche-Comté', // Jura
'58' => 'Bourgogne-Franche-Comté', // Nièvre
'70' => 'Bourgogne-Franche-Comté', // Haute-Saône
'71' => 'Bourgogne-Franche-Comté', // Saône-et-Loire
'89' => 'Bourgogne-Franche-Comté', // Yonne
'90' => 'Bourgogne-Franche-Comté', // Territoire de Belfort
// Bretagne
'22' => 'Bretagne', // Côtes-d'Armor
'29' => 'Bretagne', // Finistère
'35' => 'Bretagne', // Ille-et-Vilaine
'56' => 'Bretagne', // Morbihan
// Centre-Val de Loire
'18' => 'Centre-Val de Loire', // Cher
'28' => 'Centre-Val de Loire', // Eure-et-Loir
'36' => 'Centre-Val de Loire', // Indre
'37' => 'Centre-Val de Loire', // Indre-et-Loire
'41' => 'Centre-Val de Loire', // Loir-et-Cher
'45' => 'Centre-Val de Loire', // Loiret
// Corse
'2A' => 'Corse', // Corse-du-Sud
'2B' => 'Corse', // Haute-Corse
// Grand Est
'08' => 'Grand Est', // Ardennes
'10' => 'Grand Est', // Aube
'51' => 'Grand Est', // Marne
'52' => 'Grand Est', // Haute-Marne
'54' => 'Grand Est', // Meurthe-et-Moselle
'55' => 'Grand Est', // Meuse
'57' => 'Grand Est', // Moselle
'67' => 'Grand Est', // Bas-Rhin
'68' => 'Grand Est', // Haut-Rhin
'88' => 'Grand Est', // Vosges
// Hauts-de-France
'02' => 'Hauts-de-France', // Aisne
'59' => 'Hauts-de-France', // Nord
'60' => 'Hauts-de-France', // Oise
'62' => 'Hauts-de-France', // Pas-de-Calais
'80' => 'Hauts-de-France', // Somme
// Île-de-France
'75' => 'Île-de-France', // Paris
'77' => 'Île-de-France', // Seine-et-Marne
'78' => 'Île-de-France', // Yvelines
'91' => 'Île-de-France', // Essonne
'92' => 'Île-de-France', // Hauts-de-Seine
'93' => 'Île-de-France', // Seine-Saint-Denis
'94' => 'Île-de-France', // Val-de-Marne
'95' => 'Île-de-France', // Val-d'Oise
// Normandie
'14' => 'Normandie', // Calvados
'27' => 'Normandie', // Eure
'50' => 'Normandie', // Manche
'61' => 'Normandie', // Orne
'76' => 'Normandie', // Seine-Maritime
// Nouvelle-Aquitaine
'16' => 'Nouvelle-Aquitaine', // Charente
'17' => 'Nouvelle-Aquitaine', // Charente-Maritime
'19' => 'Nouvelle-Aquitaine', // Corrèze
'23' => 'Nouvelle-Aquitaine', // Creuse
'24' => 'Nouvelle-Aquitaine', // Dordogne
'33' => 'Nouvelle-Aquitaine', // Gironde
'40' => 'Nouvelle-Aquitaine', // Landes
'47' => 'Nouvelle-Aquitaine', // Lot-et-Garonne
'64' => 'Nouvelle-Aquitaine', // Pyrénées-Atlantiques
'79' => 'Nouvelle-Aquitaine', // Deux-Sèvres
'86' => 'Nouvelle-Aquitaine', // Vienne
'87' => 'Nouvelle-Aquitaine', // Haute-Vienne
// Occitanie
'09' => 'Occitanie', // Ariège
'11' => 'Occitanie', // Aude
'12' => 'Occitanie', // Aveyron
'30' => 'Occitanie', // Gard
'31' => 'Occitanie', // Haute-Garonne
'32' => 'Occitanie', // Gers
'34' => 'Occitanie', // Hérault
'46' => 'Occitanie', // Lot
'48' => 'Occitanie', // Lozère
'65' => 'Occitanie', // Hautes-Pyrénées
'66' => 'Occitanie', // Pyrénées-Orientales
'81' => 'Occitanie', // Tarn
'82' => 'Occitanie', // Tarn-et-Garonne
// Pays de la Loire
'44' => 'Pays de la Loire', // Loire-Atlantique
'49' => 'Pays de la Loire', // Maine-et-Loire
'53' => 'Pays de la Loire', // Mayenne
'72' => 'Pays de la Loire', // Sarthe
'85' => 'Pays de la Loire', // Vendée
// Provence-Alpes-Côte d'Azur
'04' => 'Provence-Alpes-Côte d\'Azur', // Alpes-de-Haute-Provence
'05' => 'Provence-Alpes-Côte d\'Azur', // Hautes-Alpes
'06' => 'Provence-Alpes-Côte d\'Azur', // Alpes-Maritimes
'13' => 'Provence-Alpes-Côte d\'Azur', // Bouches-du-Rhône
'83' => 'Provence-Alpes-Côte d\'Azur', // Var
'84' => 'Provence-Alpes-Côte d\'Azur', // Vaucluse
// Départements d'outre-mer
'971' => 'Guadeloupe',
'972' => 'Martinique',
'973' => 'Guyane',
'974' => 'La Réunion',
'976' => 'Mayotte'
];
}
/**
* Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.)
*/