motocultrice->get_osm_object_data($type, $id); // Récupérer le code postal depuis les tags, sinon mettre -1 $zipCode = isset($data['tags_converted']['addr:postcode']) ? (int)$data['tags_converted']['addr:postcode'] : -1; $place_name = $data['tags_converted']['name']; // Vérifier si une Place existe déjà avec le même osm_kind et osmId $existingPlace = $this->entityManager->getRepository(Place::class)->findOneBy([ 'osm_kind' => $type, 'osmId' => $id ]); if ($existingPlace) { // Mettre à jour l'email de la Place existante $existingPlace->setEmail($email)->setLastContactAttemptDate(new \DateTime()); if ($zipCode != -1) { $existingPlace->setZipCode($zipCode); } $this->entityManager->persist($existingPlace); $this->entityManager->flush(); $debug = ''; if ($this->getParameter('kernel.environment') !== 'prod') { $debug = 'Bonjour, nous sommes des bénévoles d\'OpenStreetMap France et nous vous proposons de modifier les informations de votre commerce. Voici votre lien unique de modification: ' . $this->generateUrl('app_public_edit', [ 'zipcode' => $zipCode, 'name' => $place_name, 'uuid' => $existingPlace->getUuidForUrl() ], true); } $this->addFlash('success', 'L\'email a été mis à jour. Un email vous sera envoyé avec le lien de modification. ' . $debug); } else { // Créer une nouvelle entité Place $place = new Place(); $place->setEmail($email) ->setOsmId($id) ->setOsmKind($type) ->setAskedHumainsSupport(false) ->setOptedOut(false) ->setDead(false) ->setNote('') ->setModifiedDate(new \DateTime()) ->setZipCode($zipCode) ->setPlaceCount(0) ->setMainTag($this->motocultrice->find_main_tag($data['tags_converted']) ?? '') ->setStreet($this->motocultrice->find_street($data['tags_converted']) ?? '') ->setHousenumber($this->motocultrice->find_housenumber($data['tags_converted']) ?? '') ->setLastContactAttemptDate(new \DateTime()) ->setUuidForUrl(uniqid()); $this->entityManager->persist($place); $this->entityManager->flush(); $debug = ''; if ($this->getParameter('kernel.environment') !== 'prod') { $debug = 'Bonjour, nous sommes des bénévoles d\'OpenStreetMap France et nous vous proposons de modifier les informations de votre commerce. Voici votre lien unique de modification: ' . $this->generateUrl('app_public_edit', [ 'zipcode' => $zipCode, 'name' => $place_name, 'uuid' => $place->getUuidForUrl() ], true); } $this->addFlash('success', 'Un email vous sera envoyé avec le lien de modification. ' . $debug); } // Envoyer l'email $destinataire = $this->getParameter('kernel.environment') === 'prod' ? $email : 'contact+essai_osm_commerce@cipherbliss.com'; $message = (new Email()) ->from('contact@osm-commerce.fr') ->to($destinataire) ->subject('Votre lien de modification OpenStreetMap') ->text('Bonjour, nous sommes des bénévoles d\'OpenStreetMap France et nous vous proposons de modifier les informations de votre commerce. Voici votre lien unique de modification: ' . $this->generateUrl('app_public_edit', [ 'zipcode' => $zipCode, 'name' => $place_name, 'uuid' => $existingPlace ? $existingPlace->getUuidForUrl() : $place->getUuidForUrl() ], true)); $this->mailer->send($message); return $this->redirectToRoute('app_public_index'); } #[Route('/', name: 'app_public_index')] public function index(): Response { $stats = $this->entityManager->getRepository(Stats::class)->findAll(); // Préparer les données pour la carte $citiesForMap = []; foreach ($stats as $stat) { if ($stat->getZone() && $stat->getZone() !== 'undefined' && preg_match('/^\d+$/', $stat->getZone())) { // Récupérer les coordonnées de la ville via l'API Nominatim $cityName = $stat->getName() ?: $stat->getZone(); $coordinates = $this->getCityCoordinates($cityName, $stat->getZone()); if ($coordinates) { $citiesForMap[] = [ 'name' => $cityName, 'zone' => $stat->getZone(), 'coordinates' => $coordinates, 'placesCount' => $stat->getPlacesCount(), 'completionPercent' => $stat->getCompletionPercent(), 'population' => $stat->getPopulation(), 'url' => $this->generateUrl('app_admin_stats', ['insee_code' => $stat->getZone()]) ]; } } } return $this->render('public/home.html.twig', [ 'controller_name' => 'PublicController', 'stats' => $stats, 'citiesForMap' => $citiesForMap, 'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null ]); } /** * Récupère les coordonnées d'une ville via l'API Nominatim */ private function getCityCoordinates(string $cityName, string $inseeCode): ?array { // Cache simple pour éviter trop d'appels API $cacheKey = 'city_coords_' . $inseeCode; // Vérifier le cache (ici on utilise une approche simple) // En production, vous pourriez utiliser le cache Symfony $query = urlencode($cityName . ', France'); $url = "https://nominatim.openstreetmap.org/search?q={$query}&format=json&limit=1&countrycodes=fr"; try { $response = file_get_contents($url); $data = json_decode($response, true); if (!empty($data) && isset($data[0]['lat']) && isset($data[0]['lon'])) { return [ 'lat' => (float) $data[0]['lat'], 'lon' => (float) $data[0]['lon'] ]; } } catch (\Exception $e) { // En cas d'erreur, on retourne null } return null; } #[Route('/edit/{zipcode}/{name}/{uuid}', name: 'app_public_edit')] public function edit_with_uuid($zipcode, $name, $uuid): Response { $this->actionLogger->log('edit_place', [ 'zipcode' => $zipcode, 'name' => $name, 'uuid' => $uuid, ]); $place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $uuid]); if (!$place) { $this->addFlash('warning', 'Ce lien de modification n\'existe pas.' . $uuid); return $this->redirectToRoute('app_public_index'); } if ($place->getOsmKind() === 'relation') { $this->addFlash('warning', 'Les objets OSM de type "relation" ne sont pas gérés dans cet outil.'); return $this->redirectToRoute('app_public_index'); } // récupérer les tags de base $base_tags = $this->motocultrice->base_tags; $base_tags = array_fill_keys($base_tags, ''); $commerce_overpass = $this->motocultrice->get_osm_object_data($place->getOsmKind(), $place->getOsmId()); // Fusionner les tags de base avec les tags existants $commerce_overpass['tags_converted'] = array_merge($base_tags, $commerce_overpass['tags_converted']); // Trier les tags par ordre alphabétique des clés ksort($commerce_overpass['tags_converted']); $place->setDisplayedDate(new \DateTime()); $this->entityManager->persist($place); $this->entityManager->flush(); return $this->render('public/edit.html.twig', [ 'commerce_overpass' => $commerce_overpass, 'name' => $name, 'commerce' => $place, 'zone' => $zipcode, 'completion_percentage' => $place->getCompletionPercentage(), 'hide_filled_inputs' => $this->hide_filled_inputs, 'excluded_tags_to_render' => $this->motocultrice->excluded_tags_to_render, 'osm_kind' => $place->getOsmKind(), "mapbox_token" => $_ENV['MAPBOX_TOKEN'], "maptiler_token" => $_ENV['MAPTILER_TOKEN'], ]); } #[Route('/dashboard', name: 'app_public_dashboard')] public function dashboard(): Response { $this->actionLogger->log('dashboard', []); $stats_repo = $this->entityManager->getRepository(Stats::class)->findAll(); $stats_for_chart = []; foreach ($stats_repo as $stat) { if ($stat->getPlacesCount() > 0 && $stat->getName() !== null && $stat->getPopulation() > 0) { $stats_for_chart[] = [ 'name' => $stat->getName(), 'placesCount' => $stat->getPlacesCount(), 'completionPercent' => $stat->getCompletionPercent(), 'population' => $stat->getPopulation(), 'zone' => $stat->getZone(), 'osmDataDateAvg' => $stat->getOsmDataDateAvg() ? $stat->getOsmDataDateAvg()->format('Y-m-d') : null, ]; } } // Compter le nombre total de lieux $placesCount = $this->entityManager->getRepository(Place::class)->count([]); return $this->render('public/dashboard.html.twig', [ 'controller_name' => 'PublicController', 'mapbox_token' => $_ENV['MAPBOX_TOKEN'] ?? null, 'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null, 'stats' => json_encode($stats_for_chart), 'stats_list' => $stats_repo, 'places_count' => $placesCount, ]); } #[Route('/modify/{osm_object_id}/{version}/{changesetID}', name: 'app_public_submit')] public function submit($osm_object_id, $version, $changesetID): Response { $this->actionLogger->log('submit_object', [ 'osm_id' => $osm_object_id, 'version' => $version, 'changesetID' => $changesetID ]); $place = $this->entityManager->getRepository(Place::class)->findOneBy(['osmId' => $osm_object_id]); if (!$place) { $this->addFlash('warning', 'Ce commerce n\'existe pas.'); return $this->redirectToRoute('app_public_index'); } // Récupérer les données POST $request = Request::createFromGlobals(); $status = null; $exception = false; $exception_message = null; $osm_kind = 'node'; // Vérifier si des données ont été soumises if ($request->isMethod('POST')) { $status = "non modifié"; $osm_kind = $request->request->get('osm_kind', 'node'); // Récupérer tous les tags du formulaire $tags = []; $request_post = $request->request->all(); $request_post = $this->motocultrice->map_post_values($request_post); foreach ($request_post as $key => $value) { if (strpos($key, 'commerce_tag_value__') === 0) { $tagKey = str_replace('commerce_tag_value__', '', $key); if (!empty($value)) { $tags[$tagKey] = trim($value); } } } var_dump('POST brut', $_POST); var_dump('request->request->all()', $request->request->all()); var_dump('tags extraits', $tags); // DEBUG : enlever ce die après analyse die('DEBUG avant envoi OSM'); // Récupérer le token OSM depuis les variables d'environnement $osm_api_token = $_ENV['APP_OSM_BEARER']; try { $client = new Client(); // 1. Créer un nouveau changeset $changesetXml = new \SimpleXMLElement(''); $changeset = $changesetXml->addChild('changeset'); $tag = $changeset->addChild('tag'); $tag->addAttribute('k', 'created_by'); $tag->addAttribute('v', 'OSM Mon Commerce Web Editor'); $tag = $changeset->addChild('tag'); $tag->addAttribute('k', 'comment'); $tag->addAttribute('v', 'Modification dans #MonCommerceOSM'); $changesetResponse = $client->put('https://api.openstreetmap.org/api/0.6/changeset/create', [ 'body' => $changesetXml->asXML(), 'headers' => [ 'Authorization' => 'Bearer ' . $osm_api_token, 'Content-Type' => 'application/xml' ] ]); $newChangesetId = $changesetResponse->getBody()->getContents(); // Récupérer les données actuelles de l'objet $currentObjectData = $this->motocultrice->get_osm_object_data($osm_kind, $osm_object_id); // 2. Modifier l'objet avec le nouveau changeset $xml = new \SimpleXMLElement(''); $object = $xml->addChild($osm_kind); $object->addAttribute('id', $osm_object_id); $object->addAttribute('version', $version); $object->addAttribute('changeset', $newChangesetId); // Ajouter les coordonnées pour les nodes if ($osm_kind === 'node') { if (!isset($currentObjectData['@attributes']['lat']) || !isset($currentObjectData['@attributes']['lon'])) { throw new \Exception("Impossible de récupérer les coordonnées du nœud"); } $object->addAttribute('lat', $currentObjectData['@attributes']['lat']); $object->addAttribute('lon', $currentObjectData['@attributes']['lon']); } // Ajouter les tags foreach ($tags as $key => $value) { if (!empty($key) && !empty($value)) { $tag = $object->addChild('tag'); $tag->addAttribute('k', htmlspecialchars($key, ENT_XML1)); $tag->addAttribute('v', htmlspecialchars($value, ENT_XML1)); } } // Debug du XML généré $xmlString = $xml->asXML(); $response = $client->put("https://api.openstreetmap.org/api/0.6/{$osm_kind}/" . $osm_object_id, [ 'body' => $xmlString, 'headers' => [ 'Authorization' => 'Bearer ' . $osm_api_token, 'Content-Type' => 'application/xml' ] ]); // 3. Fermer le changeset $client->put('https://api.openstreetmap.org/api/0.6/changeset/' . $newChangesetId . '/close', [ 'headers' => [ 'Authorization' => 'Bearer ' . $osm_api_token ] ]); if ($response->getStatusCode() === 200) { $status = "Les tags ont été mis à jour avec succès"; } else { $status = "Erreur lors de la mise à jour des tags"; $this->actionLogger->log('ERROR_submit_object', [ 'osm_id' => $osm_object_id, 'version' => $version, 'changesetID' => $changesetID, 'body_sent' => $xmlString, 'response' => $response->getBody()->getContents(), ]); } } catch (\Exception $e) { $status = "Erreur lors de la communication avec l'API OSM: " . $e->getMessage(); $exception = true; $exception_message = $e->getMessage(); // Debug de la réponse en cas d'erreur if (method_exists($e, 'getResponse')) { var_dump($e->getResponse()->getBody()->getContents()); // Faire un log d'action lors d'une erreur de soumission $this->actionLogger->log('ERROR_submit_object_exception', [ 'osm_id' => $osm_object_id ?? null, 'version' => $version ?? null, 'changesetID' => $changesetID ?? null, 'body_sent' => $xmlString ?? null, 'exception_message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); } } } // après envoi on récupère les données $commerce_overpass = $this->motocultrice->get_osm_object_data($osm_kind, $osm_object_id); $place->update_place_from_overpass_data($commerce_overpass); $this->entityManager->persist($place); $this->entityManager->flush(); $this->entityManager->clear(); $stats = $place->getStats(); if (!$stats) { $stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zip_code' => $place->getZipCode()]); } if (!$stats) { $stats = new Stats(); $stats->setZone($place->getZipCode()); } $stats->addPlace($place); $place->setStats($stats); $place->setModifiedDate(new \DateTime()); $stats->computeCompletionPercent(); $this->entityManager->persist($stats); $this->entityManager->persist($place); $this->entityManager->flush(); $this->entityManager->clear(); return $this->render('public/view.html.twig', [ 'controller_name' => 'PublicController', 'commerce' => $commerce_overpass, 'commerce_overpass' => $commerce_overpass, 'place' => $place, 'status' => $status, 'exception' => $exception, 'exception_message' => $exception_message, 'mapbox_token' => $_ENV['MAPBOX_TOKEN'], 'maptiler_token' => $_ENV['MAPTILER_TOKEN'], 'hide_filled_inputs' => false, 'excluded_tags_to_render' => $this->motocultrice->excluded_tags_to_render, ]); } #[Route('/request_email_to_modify/{osm_object_id}', name: 'app_public_request_email')] public function request_email($osm_object_id, Request $request): Response { $this->actionLogger->log('request_email_to_modify', [ 'osm_id' => $osm_object_id, ]); if ($request->isMethod('POST')) { $email = $request->request->get('email'); try { // TODO: Implémenter l'envoi réel du mail $this->addFlash( 'success', 'Un email vous a été envoyé avec les instructions pour modifier ce lieu.' ); } catch (\Exception $e) { $this->actionLogger->log('ERROR_request_email_to_modify', [ 'osm_id' => $osm_object_id, 'exception_message' => $e->getMessage(), ]); $this->addFlash( 'error', 'Une erreur est survenue lors de l\'envoi de l\'email. Veuillez réessayer plus tard.' ); } return $this->redirectToRoute('app_public_index'); } // TODO envoyer un email return $this->render('public/request_email.html.twig', [ 'controller_name' => 'PublicController', 'commerce_id' => $osm_object_id, ]); } #route pour signaler que le commerce est fermé #[Route('/closed_commerce/{osm_object_id}', name: 'app_public_closed_commerce')] public function closed_commerce($osm_object_id): Response { $this->actionLogger->log('closed_commerce', [ 'osm_id' => $osm_object_id, ]); $place = $this->entityManager->getRepository(Place::class)->findOneBy(['osm_id' => $osm_object_id]); if (!$place) { $this->addFlash('warning', 'Ce commerce n\'existe pas.'); return $this->redirectToRoute('app_public_index'); } $place->setClosed(true); $place->setDead(true); $this->entityManager->flush(); return $this->render('public/closed_commerce.html.twig', [ 'controller_name' => 'PublicController', ]); } #[Route('/closed_commerces', name: 'app_public_closed_commerces')] public function closedCommerces(): Response { // Récupérer tous les commerces marqués comme fermés $closedPlaces = $this->entityManager->getRepository(Place::class)->findBy(['dead' => true]); return $this->render('public/closed_commerces.html.twig', [ 'controller_name' => 'PublicController', 'closed_places' => $closedPlaces ]); } #[Route('/places_with_note', name: 'app_public_places_with_note')] public function places_with_note(): Response { // Récupérer tous les commerces ayant une note $places = $this->entityManager->getRepository(Place::class)->findBy(['has_note' => true]); return $this->render('public/places_with_note.html.twig', [ 'controller_name' => 'PublicController', 'places' => $places ]); } #[Route('/latest_changes', name: 'app_public_latest_changes')] public function latestChanges(): Response { // Récupérer les commerces modifiés, triés par date de modification décroissante $places_modified = $this->entityManager->getRepository(Place::class) ->createQueryBuilder('p') ->where('p.modified_date IS NOT NULL') ->orderBy('p.modified_date', 'DESC') ->setMaxResults(20) ->getQuery() ->getResult(); // Récupérer les commerces modifiés, triés par date de modification décroissante $places_displayed = $this->entityManager->getRepository(Place::class) ->createQueryBuilder('p') ->where('p.displayed_date IS NOT NULL') ->orderBy('p.displayed_date', 'DESC') ->setMaxResults(20) ->getQuery() ->getResult(); return $this->render('public/latest_changes.html.twig', [ 'places_modified' => $places_modified, 'places_displayed' => $places_displayed ]); } #[Route('/set_opted_out_place/{uuid}', name: 'app_public_set_opted_out_place')] public function set_opted_out_place($uuid) { $place = $this->entityManager->getRepository(Place::class)->findOneBy(['uuid_for_url' => $uuid]); $this->actionLogger->log('set_place_opted_out', [ 'uuid' => $uuid, ]); if (!$place) { $this->addFlash('warning', 'Ce commerce n\'existe pas.'); return $this->redirectToRoute('app_public_index'); } $place->setOptedOut(true); } #[Route('/ask-for-help', name: 'app_public_ask_for_help')] public function askForHelp(Request $request): Response { $this->actionLogger->log('ask_for_help', []); return $this->redirect('https://www.openstreetmap.fr/contact'); } #[Route('/logs/actions', name: 'app_public_action_logs')] public function listActionLogs(): Response { $logs = $this->actionLogger->getLastLogs(100); return $this->render('public/action_logs.html.twig', [ 'logs' => $logs ]); } }