suivi des suppressions d'objets par ville
This commit is contained in:
parent
62e086cd64
commit
8e43908cef
13 changed files with 1185 additions and 2 deletions
35
migrations/Version20251125223837.php
Normal file
35
migrations/Version20251125223837.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251125223837 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE zone_places (id INT AUTO_INCREMENT NOT NULL, disappeared_list JSON DEFAULT NULL, current_list JSON DEFAULT NULL, theme VARCHAR(255) NOT NULL, stats_id INT NOT NULL, INDEX IDX_C1BC442770AA3482 (stats_id), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8');
|
||||
$this->addSql('ALTER TABLE zone_places ADD CONSTRAINT FK_C1BC442770AA3482 FOREIGN KEY (stats_id) REFERENCES stats (id)');
|
||||
$this->addSql('ALTER TABLE place CHANGE email email VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note note LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE name name VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE note_content note_content VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE street street VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE housenumber housenumber VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE siret siret VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE osm_user osm_user VARCHAR(255) CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`, CHANGE email_content email_content LONGTEXT CHARACTER SET utf8mb4 DEFAULT NULL COLLATE `utf8mb4_0900_ai_ci`');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE zone_places DROP FOREIGN KEY FK_C1BC442770AA3482');
|
||||
$this->addSql('DROP TABLE zone_places');
|
||||
$this->addSql('ALTER TABLE place CHANGE email email VARCHAR(255) DEFAULT NULL, CHANGE note note LONGTEXT DEFAULT NULL, CHANGE name name VARCHAR(255) DEFAULT NULL, CHANGE note_content note_content VARCHAR(255) DEFAULT NULL, CHANGE street street VARCHAR(255) DEFAULT NULL, CHANGE housenumber housenumber VARCHAR(255) DEFAULT NULL, CHANGE siret siret VARCHAR(255) DEFAULT NULL, CHANGE osm_user osm_user VARCHAR(255) DEFAULT NULL, CHANGE email_content email_content LONGTEXT DEFAULT NULL');
|
||||
}
|
||||
}
|
||||
|
|
@ -950,6 +950,82 @@ final class AdminController extends AbstractController
|
|||
];
|
||||
}
|
||||
|
||||
// Récupérer les ZonePlaces pour ce thème
|
||||
$zonePlacesRepository = $this->entityManager->getRepository(\App\Entity\ZonePlaces::class);
|
||||
$zonePlaces = $zonePlacesRepository->findOneByStatsAndTheme($stats, $theme);
|
||||
|
||||
// Si le ZonePlaces n'existe pas, l'initialiser avec les données Overpass du thème courant
|
||||
if (!$zonePlaces && $theme !== 'places') {
|
||||
// Récupérer les données Overpass pour ce thème
|
||||
$elements = $motocultrice->followUpCity($insee_code) ?? [];
|
||||
|
||||
// Filtrer les objets selon le thème (même logique que dans FollowUpService)
|
||||
$themeObjects = $this->filterObjectsByTheme($elements, $theme);
|
||||
|
||||
if (!empty($themeObjects)) {
|
||||
// Créer le ZonePlaces et le remplir
|
||||
$zonePlaces = new \App\Entity\ZonePlaces();
|
||||
$zonePlaces->setStats($stats);
|
||||
$zonePlaces->setTheme($theme);
|
||||
|
||||
// Extraire les objets actuels d'Overpass (type, id, user, timestamp, changeset)
|
||||
$currentObjectsMap = [];
|
||||
foreach ($themeObjects as $obj) {
|
||||
if (isset($obj['type']) && isset($obj['id'])) {
|
||||
$currentObjectsMap[] = [
|
||||
'type' => $obj['type'],
|
||||
'id' => $obj['id'],
|
||||
'user' => $obj['user'] ?? null,
|
||||
'timestamp' => $obj['timestamp'] ?? null,
|
||||
'changeset' => $obj['changeset'] ?? null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$zonePlaces->setCurrentList($currentObjectsMap);
|
||||
$zonePlaces->setDisappearedList([]);
|
||||
|
||||
$this->entityManager->persist($zonePlaces);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les objets supprimés, triés par date de détection (plus récent en premier)
|
||||
$disappearedObjects = [];
|
||||
$currentObjectsStats = [
|
||||
'node' => 0,
|
||||
'way' => 0,
|
||||
'relation' => 0,
|
||||
'total' => 0
|
||||
];
|
||||
|
||||
if ($zonePlaces) {
|
||||
// Récupérer les objets supprimés
|
||||
if ($zonePlaces->getDisappearedList()) {
|
||||
$disappearedObjects = $zonePlaces->getDisappearedList();
|
||||
// Trier par noticed_deleted_date décroissant (plus récent en premier)
|
||||
usort($disappearedObjects, function($a, $b) {
|
||||
$dateA = $a['noticed_deleted_date'] ?? '';
|
||||
$dateB = $b['noticed_deleted_date'] ?? '';
|
||||
return strcmp($dateB, $dateA); // Décroissant
|
||||
});
|
||||
}
|
||||
|
||||
// Compter les objets actuels par type
|
||||
$currentList = $zonePlaces->getCurrentList() ?? [];
|
||||
if (is_array($currentList)) {
|
||||
foreach ($currentList as $obj) {
|
||||
if (isset($obj['type'])) {
|
||||
$type = strtolower($obj['type']);
|
||||
if (in_array($type, ['node', 'way', 'relation'])) {
|
||||
$currentObjectsStats[$type]++;
|
||||
$currentObjectsStats['total']++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('admin/followup_theme_graph.html.twig', [
|
||||
'stats' => $stats,
|
||||
'theme' => $theme,
|
||||
|
|
@ -962,6 +1038,9 @@ final class AdminController extends AbstractController
|
|||
'followup_labels' => $themes,
|
||||
'geojson' => json_encode($geojson),
|
||||
'overpass_query' => $overpass_query,
|
||||
'disappeared_objects' => $disappearedObjects,
|
||||
'current_objects_stats' => $currentObjectsStats,
|
||||
'has_zone_places' => $zonePlaces !== null,
|
||||
'josm_url' => $josm_url,
|
||||
'center' => $center,
|
||||
'maptiler_token' => $_ENV['MAPTILER_TOKEN'] ?? null,
|
||||
|
|
@ -3860,4 +3939,73 @@ out meta;';
|
|||
$this->entityManager->persist($stats);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filtre les objets Overpass selon le thème (même logique que dans FollowUpService)
|
||||
*/
|
||||
private function filterObjectsByTheme(array $elements, string $theme): array
|
||||
{
|
||||
if ($theme === 'fire_hydrant') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'fire_hydrant') ?? [];
|
||||
} elseif ($theme === 'charging_station') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'charging_station') ?? [];
|
||||
} elseif ($theme === 'toilets') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'toilets') ?? [];
|
||||
} elseif ($theme === 'bus_stop') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['highway'] ?? null) === 'bus_stop') ?? [];
|
||||
} elseif ($theme === 'defibrillator') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['emergency'] ?? null) === 'defibrillator') ?? [];
|
||||
} elseif ($theme === 'camera') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['man_made'] ?? null) === 'surveillance') ?? [];
|
||||
} elseif ($theme === 'recycling') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'recycling') ?? [];
|
||||
} elseif ($theme === 'substation') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'substation') ?? [];
|
||||
} elseif ($theme === 'laboratory') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['healthcare'] ?? null) === 'laboratory') ?? [];
|
||||
} elseif ($theme === 'school') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'school') ?? [];
|
||||
} elseif ($theme === 'police') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'police') ?? [];
|
||||
} elseif ($theme === 'healthcare') {
|
||||
return array_filter($elements, function ($el) {
|
||||
return isset($el['tags']['healthcare'])
|
||||
|| ($el['tags']['amenity'] ?? null) === 'doctors'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'pharmacy'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'hospital'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'clinic'
|
||||
|| ($el['tags']['amenity'] ?? null) === 'social_facility';
|
||||
}) ?? [];
|
||||
} elseif ($theme === 'bicycle_parking') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'bicycle_parking') ?? [];
|
||||
} elseif ($theme === 'advertising_board') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['advertising'] ?? null) === 'board' && ($el['tags']['message'] ?? null) === 'political') ?? [];
|
||||
} elseif ($theme === 'building') {
|
||||
return array_filter($elements, fn($el) => ($el['type'] ?? null) === 'way' && !empty($el['tags']['building'])) ?? [];
|
||||
} elseif ($theme === 'email') {
|
||||
return array_filter($elements, fn($el) => !empty($el['tags']['email'] ?? null) || !empty($el['tags']['contact:email'] ?? null)) ?? [];
|
||||
} elseif ($theme === 'bench') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'bench') ?? [];
|
||||
} elseif ($theme === 'waste_basket') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'waste_basket') ?? [];
|
||||
} elseif ($theme === 'street_lamp') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['highway'] ?? null) === 'street_lamp') ?? [];
|
||||
} elseif ($theme === 'drinking_water') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'drinking_water') ?? [];
|
||||
} elseif ($theme === 'tree') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['natural'] ?? null) === 'tree') ?? [];
|
||||
} elseif ($theme === 'power_pole') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['power'] ?? null) === 'pole') ?? [];
|
||||
} elseif ($theme === 'manhole') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['manhole'] ?? null) === 'manhole') ?? [];
|
||||
} elseif ($theme === 'little_free_library') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'public_bookcase') ?? [];
|
||||
} elseif ($theme === 'playground') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['leisure'] ?? null) === 'playground') ?? [];
|
||||
} elseif ($theme === 'restaurant') {
|
||||
return array_filter($elements, fn($el) => ($el['tags']['amenity'] ?? null) === 'restaurant') ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1361,6 +1361,184 @@ class PublicController extends AbstractController
|
|||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/atom/city/{insee_code}/deletions', name: 'app_public_atom_city_deletions', requirements: ['insee_code' => '\d+'])]
|
||||
public function atomCityDeletions(string $insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Ville non trouvée');
|
||||
}
|
||||
|
||||
// Récupérer tous les ZonePlaces de cette ville
|
||||
$zonePlaces = $stats->getZonePlaces();
|
||||
$deletionsByDate = [];
|
||||
$followupLabels = \App\Service\FollowUpService::getFollowUpThemes();
|
||||
|
||||
foreach ($zonePlaces as $zp) {
|
||||
$theme = $zp->getTheme();
|
||||
$disappearedList = $zp->getDisappearedList() ?? [];
|
||||
|
||||
if (is_array($disappearedList)) {
|
||||
foreach ($disappearedList as $obj) {
|
||||
if (isset($obj['noticed_deleted_date'])) {
|
||||
$date = substr($obj['noticed_deleted_date'], 0, 10); // YYYY-MM-DD
|
||||
if (!isset($deletionsByDate[$date])) {
|
||||
$deletionsByDate[$date] = [];
|
||||
}
|
||||
if (!isset($deletionsByDate[$date][$theme])) {
|
||||
$deletionsByDate[$date][$theme] = [];
|
||||
}
|
||||
$deletionsByDate[$date][$theme][] = $obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date décroissante
|
||||
krsort($deletionsByDate);
|
||||
|
||||
$content = $this->renderView('public/atom/city_deletions.xml.twig', [
|
||||
'stats' => $stats,
|
||||
'deletionsByDate' => $deletionsByDate,
|
||||
'followup_labels' => $followupLabels,
|
||||
'base_url' => $this->getParameter('router.request_context.host'),
|
||||
]);
|
||||
|
||||
$response = new Response($content);
|
||||
$response->headers->set('Content-Type', 'application/atom+xml');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/atom/city/{insee_code}/creations', name: 'app_public_atom_city_creations', requirements: ['insee_code' => '\d+'])]
|
||||
public function atomCityCreations(string $insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Ville non trouvée');
|
||||
}
|
||||
|
||||
// Récupérer tous les ZonePlaces de cette ville
|
||||
$zonePlaces = $stats->getZonePlaces();
|
||||
$creationsByDate = [];
|
||||
$followupLabels = \App\Service\FollowUpService::getFollowUpThemes();
|
||||
$thirtyDaysAgo = new \DateTime('-30 days');
|
||||
|
||||
foreach ($zonePlaces as $zp) {
|
||||
$theme = $zp->getTheme();
|
||||
$currentList = $zp->getCurrentList() ?? [];
|
||||
|
||||
if (is_array($currentList)) {
|
||||
foreach ($currentList as $obj) {
|
||||
if (isset($obj['timestamp'])) {
|
||||
try {
|
||||
$timestamp = new \DateTime($obj['timestamp']);
|
||||
// Ne garder que les objets créés/modifiés récemment (30 derniers jours)
|
||||
if ($timestamp >= $thirtyDaysAgo) {
|
||||
$date = $timestamp->format('Y-m-d');
|
||||
if (!isset($creationsByDate[$date])) {
|
||||
$creationsByDate[$date] = [];
|
||||
}
|
||||
if (!isset($creationsByDate[$date][$theme])) {
|
||||
$creationsByDate[$date][$theme] = [];
|
||||
}
|
||||
$creationsByDate[$date][$theme][] = $obj;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignorer les timestamps invalides
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date décroissante
|
||||
krsort($creationsByDate);
|
||||
|
||||
$content = $this->renderView('public/atom/city_creations.xml.twig', [
|
||||
'stats' => $stats,
|
||||
'creationsByDate' => $creationsByDate,
|
||||
'followup_labels' => $followupLabels,
|
||||
'base_url' => $this->getParameter('router.request_context.host'),
|
||||
]);
|
||||
|
||||
$response = new Response($content);
|
||||
$response->headers->set('Content-Type', 'application/atom+xml');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/city/{insee_code}/zone-places-history', name: 'app_public_zone_places_history', requirements: ['insee_code' => '\d+'])]
|
||||
public function zonePlacesHistory(string $insee_code): Response
|
||||
{
|
||||
$stats = $this->entityManager->getRepository(Stats::class)->findOneBy(['zone' => $insee_code]);
|
||||
if (!$stats) {
|
||||
throw $this->createNotFoundException('Ville non trouvée');
|
||||
}
|
||||
|
||||
// Récupérer tous les ZonePlaces de cette ville
|
||||
$zonePlaces = $stats->getZonePlaces();
|
||||
$changesByDate = [];
|
||||
$followupLabels = \App\Service\FollowUpService::getFollowUpThemes();
|
||||
$followupIcons = \App\Service\FollowUpService::getFollowUpIcons();
|
||||
$thirtyDaysAgo = new \DateTime('-30 days');
|
||||
|
||||
foreach ($zonePlaces as $zp) {
|
||||
$theme = $zp->getTheme();
|
||||
|
||||
// Traiter les suppressions
|
||||
$disappearedList = $zp->getDisappearedList() ?? [];
|
||||
if (is_array($disappearedList)) {
|
||||
foreach ($disappearedList as $obj) {
|
||||
if (isset($obj['noticed_deleted_date'])) {
|
||||
$date = substr($obj['noticed_deleted_date'], 0, 10);
|
||||
if (!isset($changesByDate[$date])) {
|
||||
$changesByDate[$date] = ['deletions' => [], 'creations' => []];
|
||||
}
|
||||
if (!isset($changesByDate[$date]['deletions'][$theme])) {
|
||||
$changesByDate[$date]['deletions'][$theme] = [];
|
||||
}
|
||||
$changesByDate[$date]['deletions'][$theme][] = $obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter les créations/modifications récentes
|
||||
$currentList = $zp->getCurrentList() ?? [];
|
||||
if (is_array($currentList)) {
|
||||
foreach ($currentList as $obj) {
|
||||
if (isset($obj['timestamp'])) {
|
||||
try {
|
||||
$timestamp = new \DateTime($obj['timestamp']);
|
||||
if ($timestamp >= $thirtyDaysAgo) {
|
||||
$date = $timestamp->format('Y-m-d');
|
||||
if (!isset($changesByDate[$date])) {
|
||||
$changesByDate[$date] = ['deletions' => [], 'creations' => []];
|
||||
}
|
||||
if (!isset($changesByDate[$date]['creations'][$theme])) {
|
||||
$changesByDate[$date]['creations'][$theme] = [];
|
||||
}
|
||||
$changesByDate[$date]['creations'][$theme][] = $obj;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Ignorer les timestamps invalides
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trier par date décroissante
|
||||
krsort($changesByDate);
|
||||
|
||||
return $this->render('public/zone_places_history.html.twig', [
|
||||
'stats' => $stats,
|
||||
'changesByDate' => $changesByDate,
|
||||
'followup_labels' => $followupLabels,
|
||||
'followup_icons' => $followupIcons,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/city/{insee_code}/demandes', name: 'app_public_city_demandes')]
|
||||
public function cityDemandes(string $insee_code): Response
|
||||
{
|
||||
|
|
|
|||
|
|
@ -127,6 +127,13 @@ class Stats
|
|||
#[ORM\Column(length: 20, nullable: true)]
|
||||
private ?string $kind = 'command';
|
||||
|
||||
|
||||
/**
|
||||
* @var Collection<int, ZonePlaces>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ZonePlaces::class, mappedBy: 'stats', orphanRemoval: true)]
|
||||
private Collection $ZonePlaces;
|
||||
|
||||
public function getCTCurlBase(): ?string
|
||||
{
|
||||
$base = 'https://complete-tes-commerces.fr/';
|
||||
|
|
@ -277,6 +284,7 @@ class Stats
|
|||
$this->places = new ArrayCollection();
|
||||
$this->statsHistories = new ArrayCollection();
|
||||
$this->cityFollowUps = new ArrayCollection();
|
||||
$this->ZonePlaces = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
|
|
@ -676,4 +684,35 @@ class Stats
|
|||
$this->kind = $kind;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Collection<int, ZonePlaces>
|
||||
*/
|
||||
public function getZonePlaces(): Collection
|
||||
{
|
||||
return $this->ZonePlaces;
|
||||
}
|
||||
|
||||
public function addZonePlace(ZonePlaces $zonePlace): static
|
||||
{
|
||||
if (!$this->ZonePlaces->contains($zonePlace)) {
|
||||
$this->ZonePlaces->add($zonePlace);
|
||||
$zonePlace->setStats($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeZonePlace(ZonePlaces $zonePlace): static
|
||||
{
|
||||
if ($this->ZonePlaces->removeElement($zonePlace)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($zonePlace->getStats() === $this) {
|
||||
$zonePlace->setStats(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
98
src/Entity/ZonePlaces.php
Normal file
98
src/Entity/ZonePlaces.php
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\ZonePlacesRepository;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ZonePlacesRepository::class)]
|
||||
class ZonePlaces
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private ?int $id = null;
|
||||
|
||||
|
||||
/**
|
||||
* liste d'objets disparus dans OSM.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
#[ORM\Column(type: Types::JSONB, nullable: true)]
|
||||
private mixed $disappearedList = null;
|
||||
|
||||
/**
|
||||
* liste d'objets courants dans OSM.
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
#[ORM\Column(type: Types::JSONB, nullable: true)]
|
||||
private mixed $currentList = null;
|
||||
|
||||
/**
|
||||
* objet Stats, une ville reliée à une liste d'objets courants et une liste d'objets disparus dans OSM.
|
||||
*
|
||||
* @var Stats|null
|
||||
*/
|
||||
#[ORM\ManyToOne(inversedBy: 'ZonePlaces')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?Stats $stats = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $theme = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getStats(): ?Stats
|
||||
{
|
||||
return $this->stats;
|
||||
}
|
||||
|
||||
public function setStats(?Stats $stats): static
|
||||
{
|
||||
$this->stats = $stats;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDisappearedList(): mixed
|
||||
{
|
||||
return $this->disappearedList;
|
||||
}
|
||||
|
||||
public function setDisappearedList(mixed $disappearedList): static
|
||||
{
|
||||
$this->disappearedList = $disappearedList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCurrentList(): mixed
|
||||
{
|
||||
return $this->currentList;
|
||||
}
|
||||
|
||||
public function setCurrentList(mixed $currentList): static
|
||||
{
|
||||
$this->currentList = $currentList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTheme(): ?string
|
||||
{
|
||||
return $this->theme;
|
||||
}
|
||||
|
||||
public function setTheme(string $theme): static
|
||||
{
|
||||
$this->theme = $theme;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
58
src/Repository/ZonePlacesRepository.php
Normal file
58
src/Repository/ZonePlacesRepository.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\ZonePlaces;
|
||||
use App\Entity\Stats;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<ZonePlaces>
|
||||
*/
|
||||
class ZonePlacesRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, ZonePlaces::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouve un ZonePlaces pour un Stats et un thème donnés
|
||||
*/
|
||||
public function findOneByStatsAndTheme(Stats $stats, string $theme): ?ZonePlaces
|
||||
{
|
||||
return $this->createQueryBuilder('z')
|
||||
->andWhere('z.stats = :stats')
|
||||
->andWhere('z.theme = :theme')
|
||||
->setParameter('stats', $stats)
|
||||
->setParameter('theme', $theme)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @return ZonePlaces[] Returns an array of ZonePlaces objects
|
||||
// */
|
||||
// public function findByExampleField($value): array
|
||||
// {
|
||||
// return $this->createQueryBuilder('z')
|
||||
// ->andWhere('z.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->orderBy('z.id', 'ASC')
|
||||
// ->setMaxResults(10)
|
||||
// ->getQuery()
|
||||
// ->getResult()
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?ZonePlaces
|
||||
// {
|
||||
// return $this->createQueryBuilder('z')
|
||||
// ->andWhere('z.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ namespace App\Service;
|
|||
|
||||
use App\Entity\CityFollowUp;
|
||||
use App\Entity\Stats;
|
||||
use App\Entity\ZonePlaces;
|
||||
use App\Repository\ZonePlacesRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
class FollowUpService
|
||||
|
|
@ -86,6 +88,17 @@ class FollowUpService
|
|||
$now = new \DateTime();
|
||||
$persisted = 0;
|
||||
|
||||
// Gestion des ZonePlaces pour chaque thème (sauf 'places')
|
||||
$zonePlacesRepository = $em->getRepository(ZonePlaces::class);
|
||||
foreach ($types as $type => $data) {
|
||||
// On ne gère pas les ZonePlaces pour le thème 'places'
|
||||
if ($type !== 'places' && !empty($data['objects'])) {
|
||||
$this->updateZonePlacesForTheme($stats, $type, $data['objects'], $zonePlacesRepository, $em, $now);
|
||||
}
|
||||
}
|
||||
// Flush des ZonePlaces avant de continuer avec les CityFollowUp
|
||||
$em->flush();
|
||||
|
||||
foreach ($types as $type => $data) {
|
||||
// Suivi du nombre
|
||||
$measureCount = $type === 'places' ? $stats->getPlacesCount() : count($data['objects']);
|
||||
|
|
@ -517,4 +530,102 @@ class FollowUpService
|
|||
'restaurant' => ['contact:phone', 'phone', 'contact:email', 'email', 'contact:website', 'website', 'cuisine', 'ref:FR:SIRET'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour ou crée un ZonePlaces pour un thème donné
|
||||
* Compare les objets actuels avec ceux stockés pour détecter les disparus
|
||||
*/
|
||||
private function updateZonePlacesForTheme(
|
||||
Stats $stats,
|
||||
string $theme,
|
||||
array $currentObjects,
|
||||
ZonePlacesRepository $repository,
|
||||
EntityManagerInterface $em,
|
||||
\DateTime $now
|
||||
): void {
|
||||
// Récupérer ou créer le ZonePlaces pour ce thème
|
||||
$zonePlaces = $repository->findOneByStatsAndTheme($stats, $theme);
|
||||
|
||||
if (!$zonePlaces) {
|
||||
// Créer un nouveau ZonePlaces
|
||||
$zonePlaces = new ZonePlaces();
|
||||
$zonePlaces->setStats($stats);
|
||||
$zonePlaces->setTheme($theme);
|
||||
$zonePlaces->setCurrentList([]);
|
||||
$zonePlaces->setDisappearedList([]);
|
||||
}
|
||||
|
||||
// Extraire les objets actuels d'Overpass (type, id, user, timestamp, changeset)
|
||||
$currentObjectsMap = [];
|
||||
foreach ($currentObjects as $obj) {
|
||||
if (isset($obj['type']) && isset($obj['id'])) {
|
||||
$key = $obj['type'] . '_' . $obj['id'];
|
||||
$currentObjectsMap[$key] = [
|
||||
'type' => $obj['type'],
|
||||
'id' => $obj['id'],
|
||||
'user' => $obj['user'] ?? null,
|
||||
'timestamp' => $obj['timestamp'] ?? null,
|
||||
'changeset' => $obj['changeset'] ?? null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Récupérer les objets précédemment stockés
|
||||
$previousCurrentList = $zonePlaces->getCurrentList() ?? [];
|
||||
$previousDisappearedList = $zonePlaces->getDisappearedList() ?? [];
|
||||
|
||||
// Convertir previousCurrentList en map pour faciliter la comparaison
|
||||
$previousCurrentMap = [];
|
||||
if (is_array($previousCurrentList)) {
|
||||
foreach ($previousCurrentList as $obj) {
|
||||
if (isset($obj['type']) && isset($obj['id'])) {
|
||||
$key = $obj['type'] . '_' . $obj['id'];
|
||||
$previousCurrentMap[$key] = $obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Détecter les objets disparus (présents dans previousCurrentMap mais pas dans currentObjectsMap)
|
||||
$disappearedObjects = [];
|
||||
foreach ($previousCurrentMap as $key => $obj) {
|
||||
if (!isset($currentObjectsMap[$key])) {
|
||||
// L'objet a disparu, on l'ajoute à la liste des disparus avec la date de détection
|
||||
// On conserve seulement les infos essentielles : type, id, user, timestamp, changeset
|
||||
$disappearedObj = [
|
||||
'type' => $obj['type'] ?? null,
|
||||
'id' => $obj['id'] ?? null,
|
||||
'user' => $obj['user'] ?? null,
|
||||
'timestamp' => $obj['timestamp'] ?? null,
|
||||
'changeset' => $obj['changeset'] ?? null,
|
||||
'noticed_deleted_date' => $now->format('Y-m-d H:i:s')
|
||||
];
|
||||
$disappearedObjects[] = $disappearedObj;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour la liste des disparus (ajouter les nouveaux disparus)
|
||||
$updatedDisappearedList = is_array($previousDisappearedList) ? $previousDisappearedList : [];
|
||||
foreach ($disappearedObjects as $disappearedObj) {
|
||||
// Vérifier si l'objet n'est pas déjà dans la liste des disparus
|
||||
$key = $disappearedObj['type'] . '_' . $disappearedObj['id'];
|
||||
$alreadyDisappeared = false;
|
||||
foreach ($updatedDisappearedList as $existing) {
|
||||
if (isset($existing['type']) && isset($existing['id'])
|
||||
&& $existing['type'] === $disappearedObj['type']
|
||||
&& $existing['id'] === $disappearedObj['id']) {
|
||||
$alreadyDisappeared = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$alreadyDisappeared) {
|
||||
$updatedDisappearedList[] = $disappearedObj;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour currentList avec les objets actuels
|
||||
$zonePlaces->setCurrentList(array_values($currentObjectsMap));
|
||||
$zonePlaces->setDisappearedList($updatedDisappearedList);
|
||||
|
||||
$em->persist($zonePlaces);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@
|
|||
<a class="nav-link {% if active_menu == 'stats_evolutions' %}active{% endif %}" href="{{ path('app_public_stats_evolutions', {'insee_code': stats.zone}) }}">
|
||||
<i class="bi bi-activity"></i> Évolutions des objets
|
||||
</a>
|
||||
<a class="nav-link {% if active_menu == 'zone_places_history' %}active{% endif %}" href="{{ path('app_public_zone_places_history', {'insee_code': stats.zone}) }}">
|
||||
<i class="bi bi-clock-history"></i> Historique ZonePlaces
|
||||
</a>
|
||||
<a class="nav-link {% if active_menu == 'street_completion' %}active{% endif %}" href="{{ path('admin_street_completion', {'insee_code': stats.zone}) }}">
|
||||
<i class="bi bi-signpost"></i> Complétion des rues
|
||||
</a>
|
||||
|
|
@ -74,8 +77,8 @@
|
|||
{# </a>#}
|
||||
</nav>
|
||||
|
||||
<!-- Flux RSS -->
|
||||
<div class="sidebar-heading">Flux RSS</div>
|
||||
<!-- Flux RSS/Atom -->
|
||||
<div class="sidebar-heading">Flux RSS/Atom</div>
|
||||
<nav class="nav flex-column">
|
||||
{# <a class="nav-link" href="{{ path('app_public_rss_city_demandes', {'insee_code': stats.zone}) }}" target="_blank">#}
|
||||
{# <i class="bi bi-rss"></i> Demandes#}
|
||||
|
|
@ -83,6 +86,12 @@
|
|||
{# <a class="nav-link" href="{{ path('app_public_rss_city_themes', {'insee_code': stats.zone}) }}" target="_blank">#}
|
||||
{# <i class="bi bi-rss"></i> Changements thématiques#}
|
||||
{# </a>#}
|
||||
<a class="nav-link" href="{{ path('app_public_atom_city_deletions', {'insee_code': stats.zone}) }}" target="_blank">
|
||||
<i class="bi bi-trash"></i> Suppressions (Atom)
|
||||
</a>
|
||||
<a class="nav-link" href="{{ path('app_public_atom_city_creations', {'insee_code': stats.zone}) }}" target="_blank">
|
||||
<i class="bi bi-plus-circle"></i> Créations (Atom)
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<!-- Actions -->
|
||||
|
|
|
|||
|
|
@ -8,6 +8,12 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<style>
|
||||
table {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar de navigation -->
|
||||
|
|
|
|||
|
|
@ -281,6 +281,152 @@
|
|||
{# </div> #}
|
||||
</div>
|
||||
|
||||
{# Section des objets supprimés et statistiques #}
|
||||
{% if has_zone_places is defined and has_zone_places %}
|
||||
<div class="card mt-4 mb-4">
|
||||
<div class="card-header">
|
||||
<h4><i class="bi bi-info-circle"></i> Suivi des objets OSM</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{# Statistiques des objets actuels #}
|
||||
{% if current_objects_stats is defined %}
|
||||
<div class="mb-4">
|
||||
<h5><i class="bi bi-list-ul"></i> Objets actuels suivis</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<div class="h3 mb-0">{{ current_objects_stats.node|default(0) }}</div>
|
||||
<div class="text-muted">Nodes</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<div class="h3 mb-0">{{ current_objects_stats.way|default(0) }}</div>
|
||||
<div class="text-muted">Ways</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-light">
|
||||
<div class="card-body text-center">
|
||||
<div class="h3 mb-0">{{ current_objects_stats.relation|default(0) }}</div>
|
||||
<div class="text-muted">Relations</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body text-center">
|
||||
<div class="h3 mb-0">{{ current_objects_stats.total|default(0) }}</div>
|
||||
<div>Total</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Section des objets supprimés #}
|
||||
<div class="mt-4">
|
||||
<h5><i class="bi bi-trash"></i> Objets supprimés récemment</h5>
|
||||
{% if disappeared_objects is defined and disappeared_objects|length > 0 %}
|
||||
<p class="text-muted">Liste des objets OSM qui ont été supprimés, triés du plus récent au plus ancien.</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>ID</th>
|
||||
<th>Créateur</th>
|
||||
<th>Dernière modification</th>
|
||||
<th>Changeset</th>
|
||||
<th>Date de suppression détectée</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in disappeared_objects %}
|
||||
<tr>
|
||||
<td><span class="badge bg-secondary">{{ obj.type|upper }}</span></td>
|
||||
<td><code>{{ obj.id }}</code></td>
|
||||
<td>{{ obj.user|default('Inconnu') }}</td>
|
||||
<td>
|
||||
{% if obj.timestamp %}
|
||||
{% if obj.timestamp matches '/^\\d{4}-\\d{2}-\\d{2}/' %}
|
||||
{{ obj.timestamp }}
|
||||
{% else %}
|
||||
{{ obj.timestamp|date('Y-m-d H:i:s') }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if obj.changeset %}
|
||||
<a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}"
|
||||
target="_blank"
|
||||
title="Voir le changeset dans achavi"
|
||||
class="text-decoration-none">
|
||||
<code>{{ obj.changeset }}</code>
|
||||
<i class="bi bi-box-arrow-up-right ms-1"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if obj.noticed_deleted_date %}
|
||||
<span class="text-danger">{{ obj.noticed_deleted_date }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% set josm_type = obj.type == 'node' ? 'n' : (obj.type == 'way' ? 'w' : 'r') %}
|
||||
<a href="http://127.0.0.1:8111/load_object?objects={{ josm_type }}{{ obj.id }}"
|
||||
class="btn btn-primary"
|
||||
title="Ouvrir dans JOSM"
|
||||
target="_blank">
|
||||
<i class="bi bi-tools"></i> JOSM
|
||||
</a>
|
||||
<a href="https://osmlab.github.io/osm-deep-history/#/{{ obj.type }}/{{ obj.id }}"
|
||||
class="btn btn-info"
|
||||
title="Voir l'historique dans OSM Deep History"
|
||||
target="_blank">
|
||||
<i class="bi bi-clock-history"></i> Historique
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-check-circle"></i> Aucune suppression d'objet n'a été détectée pour ce thème.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card mt-4 mb-4">
|
||||
<div class="card-header">
|
||||
<h4><i class="bi bi-info-circle"></i> Suivi des objets OSM</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i> Le suivi des objets OSM n'a pas encore été initialisé pour ce thème.
|
||||
Il sera créé lors de la prochaine mise à jour du followup.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card mt-4 mb-4">
|
||||
<div class="card-header">
|
||||
|
|
|
|||
66
templates/public/atom/city_creations.xml.twig
Normal file
66
templates/public/atom/city_creations.xml.twig
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Créations d'objets OSM - {{ stats.name }}</title>
|
||||
<link href="https://{{ base_url }}{{ path('app_public_atom_city_creations', {'insee_code': stats.zone}) }}" rel="self" />
|
||||
<link href="https://{{ base_url }}{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" />
|
||||
<id>https://{{ base_url }}/atom/city/{{ stats.zone }}/creations</id>
|
||||
<updated>{{ "now"|date("Y-m-d\\TH:i:s\\Z") }}</updated>
|
||||
<author>
|
||||
<name>OSM Commerces</name>
|
||||
</author>
|
||||
<subtitle>Flux Atom des créations/modifications récentes d'objets OSM pour {{ stats.name }} ({{ stats.zone }})</subtitle>
|
||||
|
||||
{% for date, themes in creationsByDate %}
|
||||
{% for theme, objects in themes %}
|
||||
{% set themeLabel = followup_labels[theme]|default(theme|capitalize) %}
|
||||
{% for obj in objects %}
|
||||
<entry>
|
||||
<title>Création/Modification : {{ themeLabel }} - {{ obj.type|upper }} {{ obj.id }}</title>
|
||||
<link href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}" />
|
||||
<id>https://{{ base_url }}/atom/city/{{ stats.zone }}/creations/{{ date }}/{{ theme }}/{{ obj.type }}/{{ obj.id }}</id>
|
||||
<updated>{% if obj.timestamp %}{{ obj.timestamp|replace({' ': 'T'}) }}Z{% else %}{{ "now"|date("Y-m-d\\TH:i:s\\Z") }}{% endif %}</updated>
|
||||
<summary type="html">
|
||||
<![CDATA[
|
||||
<p><strong>Thème:</strong> {{ themeLabel }}</p>
|
||||
<p><strong>Type:</strong> {{ obj.type|upper }}</p>
|
||||
<p><strong>ID OSM:</strong> {{ obj.id }}</p>
|
||||
{% if obj.user %}
|
||||
<p><strong>Contributeur:</strong> {{ obj.user }}</p>
|
||||
{% endif %}
|
||||
{% if obj.timestamp %}
|
||||
<p><strong>Date de modification:</strong> {{ obj.timestamp }}</p>
|
||||
{% endif %}
|
||||
{% if obj.changeset %}
|
||||
<p><strong>Changeset:</strong> <a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}">{{ obj.changeset }}</a></p>
|
||||
{% endif %}
|
||||
]]>
|
||||
</summary>
|
||||
<content type="html">
|
||||
<![CDATA[
|
||||
<p><strong>Thème:</strong> {{ themeLabel }}</p>
|
||||
<p><strong>Type:</strong> {{ obj.type|upper }}</p>
|
||||
<p><strong>ID OSM:</strong> <a href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}">{{ obj.id }}</a></p>
|
||||
{% if obj.user %}
|
||||
<p><strong>Contributeur:</strong> <a href="https://www.openstreetmap.org/user/{{ obj.user|url_encode }}">{{ obj.user }}</a></p>
|
||||
{% endif %}
|
||||
{% if obj.timestamp %}
|
||||
<p><strong>Date de modification:</strong> {{ obj.timestamp }}</p>
|
||||
{% endif %}
|
||||
{% if obj.changeset %}
|
||||
<p><strong>Changeset:</strong> <a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}">{{ obj.changeset }}</a></p>
|
||||
{% endif %}
|
||||
<p>
|
||||
<a href="https://osmlab.github.io/osm-deep-history/#/{{ obj.type }}/{{ obj.id }}">Historique OSM Deep History</a> |
|
||||
{% if obj.changeset %}
|
||||
<a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}">Voir dans achavi</a> |
|
||||
{% endif %}
|
||||
<a href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}">Voir sur OSM</a>
|
||||
</p>
|
||||
]]>
|
||||
</content>
|
||||
</entry>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</feed>
|
||||
|
||||
68
templates/public/atom/city_deletions.xml.twig
Normal file
68
templates/public/atom/city_deletions.xml.twig
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
<title>Suppressions d'objets OSM - {{ stats.name }}</title>
|
||||
<link href="https://{{ base_url }}{{ path('app_public_atom_city_deletions', {'insee_code': stats.zone}) }}" rel="self" />
|
||||
<link href="https://{{ base_url }}{{ path('app_admin_stats', {'insee_code': stats.zone}) }}" />
|
||||
<id>https://{{ base_url }}/atom/city/{{ stats.zone }}/deletions</id>
|
||||
<updated>{{ "now"|date("Y-m-d\\TH:i:s\\Z") }}</updated>
|
||||
<author>
|
||||
<name>OSM Commerces</name>
|
||||
</author>
|
||||
<subtitle>Flux Atom des suppressions d'objets OSM pour {{ stats.name }} ({{ stats.zone }})</subtitle>
|
||||
|
||||
{% for date, themes in deletionsByDate %}
|
||||
{% for theme, objects in themes %}
|
||||
{% set themeLabel = followup_labels[theme]|default(theme|capitalize) %}
|
||||
{% for obj in objects %}
|
||||
<entry>
|
||||
<title>Suppression : {{ themeLabel }} - {{ obj.type|upper }} {{ obj.id }}</title>
|
||||
<link href="https://osmlab.github.io/osm-deep-history/#/{{ obj.type }}/{{ obj.id }}" />
|
||||
<id>https://{{ base_url }}/atom/city/{{ stats.zone }}/deletions/{{ date }}/{{ theme }}/{{ obj.type }}/{{ obj.id }}</id>
|
||||
<updated>{% if obj.noticed_deleted_date %}{{ obj.noticed_deleted_date|replace({' ': 'T', '/': '-'}) }}Z{% else %}{{ "now"|date("Y-m-d\\TH:i:s\\Z") }}{% endif %}</updated>
|
||||
<summary type="html">
|
||||
<![CDATA[
|
||||
<p><strong>Thème:</strong> {{ themeLabel }}</p>
|
||||
<p><strong>Type:</strong> {{ obj.type|upper }}</p>
|
||||
<p><strong>ID OSM:</strong> {{ obj.id }}</p>
|
||||
{% if obj.user %}
|
||||
<p><strong>Dernier contributeur:</strong> {{ obj.user }}</p>
|
||||
{% endif %}
|
||||
{% if obj.timestamp %}
|
||||
<p><strong>Dernière modification:</strong> {{ obj.timestamp }}</p>
|
||||
{% endif %}
|
||||
{% if obj.changeset %}
|
||||
<p><strong>Changeset:</strong> <a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}">{{ obj.changeset }}</a></p>
|
||||
{% endif %}
|
||||
<p><strong>Date de suppression détectée:</strong> {{ obj.noticed_deleted_date }}</p>
|
||||
]]>
|
||||
</summary>
|
||||
<content type="html">
|
||||
<![CDATA[
|
||||
<p><strong>Thème:</strong> {{ themeLabel }}</p>
|
||||
<p><strong>Type:</strong> {{ obj.type|upper }}</p>
|
||||
<p><strong>ID OSM:</strong> <a href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}">{{ obj.id }}</a></p>
|
||||
{% if obj.user %}
|
||||
<p><strong>Dernier contributeur:</strong> <a href="https://www.openstreetmap.org/user/{{ obj.user|url_encode }}">{{ obj.user }}</a></p>
|
||||
{% endif %}
|
||||
{% if obj.timestamp %}
|
||||
<p><strong>Dernière modification:</strong> {{ obj.timestamp }}</p>
|
||||
{% endif %}
|
||||
{% if obj.changeset %}
|
||||
<p><strong>Changeset:</strong> <a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}">{{ obj.changeset }}</a></p>
|
||||
{% endif %}
|
||||
<p><strong>Date de suppression détectée:</strong> {{ obj.noticed_deleted_date }}</p>
|
||||
<p>
|
||||
<a href="https://osmlab.github.io/osm-deep-history/#/{{ obj.type }}/{{ obj.id }}">Historique OSM Deep History</a> |
|
||||
{% if obj.changeset %}
|
||||
<a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}">Voir dans achavi</a> |
|
||||
{% endif %}
|
||||
<a href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}">Voir sur OSM</a>
|
||||
</p>
|
||||
]]>
|
||||
</content>
|
||||
</entry>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</feed>
|
||||
|
||||
221
templates/public/zone_places_history.html.twig
Normal file
221
templates/public/zone_places_history.html.twig
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Historique ZonePlaces - {{ stats.name }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link href='{{ asset('css/city-sidebar.css') }}' rel='stylesheet'/>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar de navigation -->
|
||||
<div class="col-12">
|
||||
{% include 'admin/_city_sidebar.html.twig' with {'stats': stats, 'active_menu': 'zone_places_history'} %}
|
||||
</div>
|
||||
|
||||
<!-- Contenu principal -->
|
||||
<div class="col-md-9 col-lg-10 main-content">
|
||||
<div class="p-4">
|
||||
<h1>Historique ZonePlaces - {{ stats.name }} ({{ stats.zone }})</h1>
|
||||
<p class="text-muted">Historique combiné des suppressions et créations d'objets OSM par thème, groupé par date.</p>
|
||||
|
||||
{% if changesByDate is empty %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Aucun changement enregistré pour cette ville.
|
||||
</div>
|
||||
{% else %}
|
||||
{% for date, changes in changesByDate %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h3><i class="bi bi-calendar"></i> {{ date|date('d/m/Y') }}</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{# Suppressions #}
|
||||
{% if changes.deletions is not empty %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-danger"><i class="bi bi-trash"></i> Suppressions</h4>
|
||||
{% for theme, objects in changes.deletions %}
|
||||
{% set themeLabel = followup_labels[theme]|default(theme|capitalize) %}
|
||||
{% set themeIcon = followup_icons[theme]|default('bi-question-circle') %}
|
||||
<div class="mb-3">
|
||||
<h5><i class="bi {{ themeIcon }}"></i> {{ themeLabel }} ({{ objects|length }} suppression{{ objects|length > 1 ? 's' : '' }})</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Thème</th>
|
||||
<th>Type</th>
|
||||
<th>ID</th>
|
||||
<th>Contributeur</th>
|
||||
<th>Dernière modification</th>
|
||||
<th>Changeset</th>
|
||||
<th>Date suppression</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in objects %}
|
||||
<tr>
|
||||
<td><span class="badge bg-info">{{ themeLabel }}</span></td>
|
||||
<td><span class="badge bg-secondary">{{ obj.type|upper }}</span></td>
|
||||
<td><code>{{ obj.id }}</code></td>
|
||||
<td>
|
||||
{% if obj.user %}
|
||||
<a href="https://www.openstreetmap.org/user/{{ obj.user|url_encode }}" target="_blank">
|
||||
{{ obj.user }}
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Inconnu</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if obj.timestamp %}
|
||||
{{ obj.timestamp }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if obj.changeset %}
|
||||
<a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}" target="_blank">
|
||||
<code>{{ obj.changeset }}</code>
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><span class="text-danger">{{ obj.noticed_deleted_date }}</span></td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% set josm_type = obj.type == 'node' ? 'n' : (obj.type == 'way' ? 'w' : 'r') %}
|
||||
<a href="http://127.0.0.1:8111/load_object?objects={{ josm_type }}{{ obj.id }}"
|
||||
class="btn btn-primary btn-sm"
|
||||
title="Ouvrir dans JOSM"
|
||||
target="_blank">
|
||||
<i class="bi bi-tools"></i> JOSM
|
||||
</a>
|
||||
<a href="https://osmlab.github.io/osm-deep-history/#/{{ obj.type }}/{{ obj.id }}"
|
||||
class="btn btn-info btn-sm"
|
||||
title="Voir l'historique dans OSM Deep History"
|
||||
target="_blank">
|
||||
<i class="bi bi-clock-history"></i> Historique
|
||||
</a>
|
||||
<a href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="Voir sur OSM"
|
||||
target="_blank">
|
||||
<i class="bi bi-geo-alt"></i> OSM
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Créations #}
|
||||
{% if changes.creations is not empty %}
|
||||
<div>
|
||||
<h4 class="text-success"><i class="bi bi-plus-circle"></i> Créations/Modifications</h4>
|
||||
{% for theme, objects in changes.creations %}
|
||||
{% set themeLabel = followup_labels[theme]|default(theme|capitalize) %}
|
||||
{% set themeIcon = followup_icons[theme]|default('bi-question-circle') %}
|
||||
<div class="mb-3">
|
||||
<h5><i class="bi {{ themeIcon }}"></i> {{ themeLabel }} ({{ objects|length }} objet{{ objects|length > 1 ? 's' : '' }})</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Thème</th>
|
||||
<th>Type</th>
|
||||
<th>ID</th>
|
||||
<th>Contributeur</th>
|
||||
<th>Date modification</th>
|
||||
<th>Changeset</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for obj in objects %}
|
||||
<tr>
|
||||
<td><span class="badge bg-info">{{ theme }}</span></td>
|
||||
<td><span class="badge bg-success">{{ obj.type|upper }}</span></td>
|
||||
<td><code>{{ obj.id }}</code></td>
|
||||
<td>
|
||||
{% if obj.user %}
|
||||
<a href="https://www.openstreetmap.org/user/{{ obj.user|url_encode }}" target="_blank">
|
||||
{{ obj.user }}
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">Inconnu</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if obj.timestamp %}
|
||||
{{ obj.timestamp }}
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if obj.changeset %}
|
||||
<a href="https://overpass-api.de/achavi/?changeset={{ obj.changeset }}" target="_blank">
|
||||
<code>{{ obj.changeset }}</code>
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">N/A</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% set josm_type = obj.type == 'node' ? 'n' : (obj.type == 'way' ? 'w' : 'r') %}
|
||||
<a href="http://127.0.0.1:8111/load_object?objects={{ josm_type }}{{ obj.id }}"
|
||||
class="btn btn-primary btn-sm"
|
||||
title="Ouvrir dans JOSM"
|
||||
target="_blank">
|
||||
<i class="bi bi-tools"></i> JOSM
|
||||
</a>
|
||||
<a href="https://osmlab.github.io/osm-deep-history/#/{{ obj.type }}/{{ obj.id }}"
|
||||
class="btn btn-info btn-sm"
|
||||
title="Voir l'historique dans OSM Deep History"
|
||||
target="_blank">
|
||||
<i class="bi bi-clock-history"></i> Historique
|
||||
</a>
|
||||
<a href="https://www.openstreetmap.org/{{ obj.type }}/{{ obj.id }}"
|
||||
class="btn btn-secondary btn-sm"
|
||||
title="Voir sur OSM"
|
||||
target="_blank">
|
||||
<i class="bi bi-geo-alt"></i> OSM
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue