mirror of
https://forge.chapril.org/tykayn/osm-commerces
synced 2025-10-09 17:02:46 +02:00
menu latéral ville
This commit is contained in:
parent
f4c5e048ff
commit
2e459122b5
11 changed files with 1008 additions and 236 deletions
103
src/Command/CreateStatsFromDemandesCommand.php
Normal file
103
src/Command/CreateStatsFromDemandesCommand.php
Normal file
|
@ -0,0 +1,103 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Demande;
|
||||
use App\Entity\Stats;
|
||||
use App\Repository\DemandeRepository;
|
||||
use App\Repository\StatsRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:create-stats-from-demandes',
|
||||
description: 'Create Stats objects for cities with Demandes but no Stats',
|
||||
)]
|
||||
class CreateStatsFromDemandesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private DemandeRepository $demandeRepository;
|
||||
private StatsRepository $statsRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
DemandeRepository $demandeRepository,
|
||||
StatsRepository $statsRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->demandeRepository = $demandeRepository;
|
||||
$this->statsRepository = $statsRepository;
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Creating Stats objects for cities with Demandes but no Stats');
|
||||
|
||||
// Find all Demandes with INSEE codes
|
||||
$demandesWithInsee = $this->demandeRepository->createQueryBuilder('d')
|
||||
->where('d.insee IS NOT NULL')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
if (empty($demandesWithInsee)) {
|
||||
$io->warning('No Demandes with INSEE codes found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d Demandes with INSEE codes.', count($demandesWithInsee)));
|
||||
|
||||
// Group Demandes by INSEE code
|
||||
$demandesByInsee = [];
|
||||
/** @var Demande $demande */
|
||||
foreach ($demandesWithInsee as $demande) {
|
||||
$insee = $demande->getInsee();
|
||||
if (!isset($demandesByInsee[$insee])) {
|
||||
$demandesByInsee[$insee] = [];
|
||||
}
|
||||
$demandesByInsee[$insee][] = $demande;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d unique INSEE codes.', count($demandesByInsee)));
|
||||
|
||||
// Check which INSEE codes don't have Stats objects
|
||||
$newStatsCount = 0;
|
||||
foreach ($demandesByInsee as $insee => $demandes) {
|
||||
$stats = $this->statsRepository->findOneBy(['zone' => $insee]);
|
||||
if ($stats === null) {
|
||||
// Create a new Stats object for this INSEE code
|
||||
$stats = new Stats();
|
||||
$stats->setZone((string) $insee);
|
||||
|
||||
// Try to set the city name from the first Demande
|
||||
$firstDemande = $demandes[0];
|
||||
if ($firstDemande->getQuery()) {
|
||||
// Use the query as a fallback name (will be updated during labourage)
|
||||
$stats->setName($firstDemande->getQuery());
|
||||
}
|
||||
|
||||
$stats->setDateCreated(new \DateTime());
|
||||
$stats->setDateLabourageRequested(new \DateTime());
|
||||
|
||||
$this->entityManager->persist($stats);
|
||||
$newStatsCount++;
|
||||
|
||||
$io->text(sprintf('Created Stats for INSEE code %s', $insee));
|
||||
}
|
||||
}
|
||||
|
||||
if ($newStatsCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Created %d new Stats objects.', $newStatsCount));
|
||||
} else {
|
||||
$io->info('No new Stats objects needed to be created.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
202
src/Command/LinkDemandesPlacesCommand.php
Normal file
202
src/Command/LinkDemandesPlacesCommand.php
Normal file
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Entity\Demande;
|
||||
use App\Entity\Place;
|
||||
use App\Repository\DemandeRepository;
|
||||
use App\Repository\PlaceRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:link-demandes-places',
|
||||
description: 'Link Demandes to Places based on name similarity',
|
||||
)]
|
||||
class LinkDemandesPlacesCommand extends Command
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private DemandeRepository $demandeRepository;
|
||||
private PlaceRepository $placeRepository;
|
||||
|
||||
public function __construct(
|
||||
EntityManagerInterface $entityManager,
|
||||
DemandeRepository $demandeRepository,
|
||||
PlaceRepository $placeRepository
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->entityManager = $entityManager;
|
||||
$this->demandeRepository = $demandeRepository;
|
||||
$this->placeRepository = $placeRepository;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('threshold', null, InputOption::VALUE_REQUIRED, 'Similarity threshold (0-100)', 70)
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show matches without linking');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('Linking Demandes to Places based on name similarity');
|
||||
|
||||
$threshold = (int) $input->getOption('threshold');
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
if ($threshold < 0 || $threshold > 100) {
|
||||
$io->error('Threshold must be between 0 and 100');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// Find all Demandes without linked Places
|
||||
$demandesWithoutPlace = $this->demandeRepository->createQueryBuilder('d')
|
||||
->where('d.place IS NULL')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
if (empty($demandesWithoutPlace)) {
|
||||
$io->warning('No Demandes without linked Places found.');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->info(sprintf('Found %d Demandes without linked Places.', count($demandesWithoutPlace)));
|
||||
|
||||
// Process each Demande
|
||||
$linkedCount = 0;
|
||||
/** @var Demande $demande */
|
||||
foreach ($demandesWithoutPlace as $demande) {
|
||||
$query = $demande->getQuery();
|
||||
$insee = $demande->getInsee();
|
||||
|
||||
if (empty($query)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find Places with similar names
|
||||
$places = $this->findSimilarPlaces($query, $insee);
|
||||
if (empty($places)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the best match
|
||||
$bestMatch = null;
|
||||
$bestSimilarity = 0;
|
||||
foreach ($places as $place) {
|
||||
$similarity = $this->calculateSimilarity($query, $place->getName());
|
||||
if ($similarity > $bestSimilarity) {
|
||||
$bestSimilarity = $similarity;
|
||||
$bestMatch = $place;
|
||||
}
|
||||
}
|
||||
|
||||
// If similarity is above threshold, link the Demande to the Place
|
||||
if ($bestMatch && $bestSimilarity >= $threshold) {
|
||||
$io->text(sprintf(
|
||||
'Match found: "%s" (Demande) -> "%s" (Place) with similarity %d%%',
|
||||
$query,
|
||||
$bestMatch->getName(),
|
||||
$bestSimilarity
|
||||
));
|
||||
|
||||
if (!$dryRun) {
|
||||
$demande->setPlace($bestMatch);
|
||||
$demande->setStatus('linked_to_place');
|
||||
$this->entityManager->persist($demande);
|
||||
$linkedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$dryRun && $linkedCount > 0) {
|
||||
$this->entityManager->flush();
|
||||
$io->success(sprintf('Linked %d Demandes to Places.', $linkedCount));
|
||||
} elseif ($dryRun) {
|
||||
$io->info('Dry run completed. No changes were made.');
|
||||
} else {
|
||||
$io->info('No Demandes were linked to Places.');
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Places with names similar to the query
|
||||
*/
|
||||
private function findSimilarPlaces(string $query, ?int $insee): array
|
||||
{
|
||||
$queryBuilder = $this->placeRepository->createQueryBuilder('p')
|
||||
->where('p.name IS NOT NULL');
|
||||
|
||||
// If INSEE code is available, filter by Stats zone
|
||||
if ($insee !== null) {
|
||||
$queryBuilder
|
||||
->join('p.stats', 's')
|
||||
->andWhere('s.zone = :insee')
|
||||
->setParameter('insee', (string) $insee);
|
||||
}
|
||||
|
||||
// Use LIKE for initial filtering to reduce the number of results
|
||||
$queryBuilder
|
||||
->andWhere('p.name LIKE :query')
|
||||
->setParameter('query', '%' . $this->sanitizeForLike($query) . '%');
|
||||
|
||||
return $queryBuilder->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate similarity between two strings (0-100)
|
||||
*/
|
||||
private function calculateSimilarity(string $str1, string $str2): int
|
||||
{
|
||||
// Normalize strings for comparison
|
||||
$str1 = $this->normalizeString($str1);
|
||||
$str2 = $this->normalizeString($str2);
|
||||
|
||||
// If either string is empty after normalization, return 0
|
||||
if (empty($str1) || empty($str2)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate Levenshtein distance
|
||||
$levenshtein = levenshtein($str1, $str2);
|
||||
$maxLength = max(strlen($str1), strlen($str2));
|
||||
|
||||
// Convert to similarity percentage (0-100)
|
||||
$similarity = (1 - $levenshtein / $maxLength) * 100;
|
||||
|
||||
return (int) $similarity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a string for comparison
|
||||
*/
|
||||
private function normalizeString(string $str): string
|
||||
{
|
||||
// Convert to lowercase
|
||||
$str = mb_strtolower($str);
|
||||
|
||||
// Remove accents
|
||||
$str = transliterator_transliterate('Any-Latin; Latin-ASCII', $str);
|
||||
|
||||
// Remove special characters and extra spaces
|
||||
$str = preg_replace('/[^a-z0-9\s]/', '', $str);
|
||||
$str = preg_replace('/\s+/', ' ', $str);
|
||||
|
||||
return trim($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a string for use in LIKE queries
|
||||
*/
|
||||
private function sanitizeForLike(string $str): string
|
||||
{
|
||||
return str_replace(['%', '_'], ['\%', '\_'], $str);
|
||||
}
|
||||
}
|
|
@ -1007,6 +1007,101 @@ class PublicController extends AbstractController
|
|||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/rss/city/{insee_code}/demandes', name: 'app_public_rss_city_demandes')]
|
||||
public function rssCityDemandes(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 les demandes pour cette ville
|
||||
$demandes = $this->entityManager->getRepository(Demande::class)
|
||||
->createQueryBuilder('d')
|
||||
->where('d.insee = :insee')
|
||||
->setParameter('insee', $insee_code)
|
||||
->orderBy('d.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
$content = $this->renderView('public/rss/city_demandes.xml.twig', [
|
||||
'demandes' => $demandes,
|
||||
'city' => $stats,
|
||||
'base_url' => $this->getParameter('router.request_context.host'),
|
||||
]);
|
||||
|
||||
$response = new Response($content);
|
||||
$response->headers->set('Content-Type', 'application/rss+xml');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/rss/city/{insee_code}/themes', name: 'app_public_rss_city_themes')]
|
||||
public function rssCityThemes(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 les changements thématiques pour cette ville
|
||||
$followups = $stats->getCityFollowUps();
|
||||
$themeChanges = [];
|
||||
|
||||
foreach ($followups as $followup) {
|
||||
$name = $followup->getName();
|
||||
if (str_ends_with($name, '_count')) {
|
||||
$type = substr($name, 0, -6);
|
||||
if (!isset($themeChanges[$type])) {
|
||||
$themeChanges[$type] = [];
|
||||
}
|
||||
$themeChanges[$type][] = $followup;
|
||||
}
|
||||
}
|
||||
|
||||
// Trier les changements par date pour chaque thème
|
||||
foreach ($themeChanges as &$changes) {
|
||||
usort($changes, function($a, $b) {
|
||||
return $b->getDate() <=> $a->getDate();
|
||||
});
|
||||
}
|
||||
|
||||
$content = $this->renderView('public/rss/city_themes.xml.twig', [
|
||||
'themeChanges' => $themeChanges,
|
||||
'city' => $stats,
|
||||
'base_url' => $this->getParameter('router.request_context.host'),
|
||||
'followup_labels' => \App\Service\FollowUpService::getFollowUpThemes(),
|
||||
]);
|
||||
|
||||
$response = new Response($content);
|
||||
$response->headers->set('Content-Type', 'application/rss+xml');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/city/{insee_code}/demandes', name: 'app_public_city_demandes')]
|
||||
public function cityDemandes(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 les demandes pour cette ville
|
||||
$demandes = $this->entityManager->getRepository(Demande::class)
|
||||
->createQueryBuilder('d')
|
||||
->where('d.insee = :insee')
|
||||
->setParameter('insee', $insee_code)
|
||||
->orderBy('d.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
return $this->render('public/city_demandes.html.twig', [
|
||||
'demandes' => $demandes,
|
||||
'city' => $stats,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/cities', name: 'app_public_cities')]
|
||||
public function cities(): Response
|
||||
{
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue