use insee codes to generate all cities measures from osmium and osm pbf

This commit is contained in:
Tykayn 2025-08-31 01:33:22 +02:00 committed by tykayn
parent fa346d522f
commit b28f8eac63
3 changed files with 633 additions and 6 deletions

View file

@ -1646,6 +1646,367 @@ final class AdminController extends AbstractController
$this->addFlash('success', "Création des Stats manquantes terminée : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs."); $this->addFlash('success', "Création des Stats manquantes terminée : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs.");
return $this->redirectToRoute('app_admin'); return $this->redirectToRoute('app_admin');
} }
#[Route('/admin/create-stats-from-insee-csv', name: 'app_admin_create_stats_from_insee_csv')]
public function createStatsFromInseeCsv(): Response
{
$this->actionLogger->log('admin/create_stats_from_insee_csv', []);
$csvFile = 'communes_france.csv';
if (!file_exists($csvFile)) {
$this->addFlash('error', 'Le fichier CSV des communes n\'existe pas. Veuillez exécuter le script fetch_communes.py pour le générer.');
return $this->redirectToRoute('app_admin');
}
$statsRepo = $this->entityManager->getRepository(Stats::class);
$createdCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Ouvrir le fichier CSV
$handle = fopen($csvFile, 'r');
if (!$handle) {
$this->addFlash('error', 'Impossible d\'ouvrir le fichier CSV des communes.');
return $this->redirectToRoute('app_admin');
}
// Lire l'en-tête pour déterminer les indices des colonnes
$header = fgetcsv($handle);
$indices = array_flip($header);
// Vérifier que les colonnes nécessaires existent
$requiredColumns = ['code', 'nom'];
foreach ($requiredColumns as $column) {
if (!isset($indices[$column])) {
$this->addFlash('error', "La colonne '$column' est manquante dans le fichier CSV.");
fclose($handle);
return $this->redirectToRoute('app_admin');
}
}
// Traiter chaque ligne du CSV
while (($data = fgetcsv($handle)) !== false) {
try {
$inseeCode = $data[$indices['code']];
// Vérifier si une Stats existe déjà pour ce code INSEE
$existingStat = $statsRepo->findOneBy(['zone' => $inseeCode]);
if ($existingStat) {
$skippedCount++;
continue;
}
// Créer un nouvel objet Stats
$stat = new Stats();
$stat->setZone($inseeCode)
->setDateCreated(new \DateTime())
->setDateModified(new \DateTime())
->setKind('insee_csv'); // Utiliser 'insee_csv' comme source
// Ajouter le nom si disponible
if (isset($indices['nom']) && !empty($data[$indices['nom']])) {
$stat->setName($data[$indices['nom']]);
}
// Ajouter la population si disponible
if (isset($indices['population']) && !empty($data[$indices['population']])) {
$stat->setPopulation((int)$data[$indices['population']]);
}
// Ajouter les codes postaux si disponibles
if (isset($indices['codesPostaux']) && !empty($data[$indices['codesPostaux']])) {
$stat->setCodesPostaux($data[$indices['codesPostaux']]);
}
// Ajouter le SIREN si disponible
if (isset($indices['siren']) && !empty($data[$indices['siren']])) {
$stat->setSiren((int)$data[$indices['siren']]);
}
// Ajouter le code EPCI si disponible
if (isset($indices['codeEpci']) && !empty($data[$indices['codeEpci']])) {
$stat->setCodeEpci((int)$data[$indices['codeEpci']]);
}
// Ne pas faire de labourage des objets avant la sauvegarde
// Persister l'objet Stats
$this->entityManager->persist($stat);
$createdCount++;
// Flush tous les 100 objets pour éviter de surcharger la mémoire
if ($createdCount % 100 === 0) {
$this->entityManager->flush();
$this->entityManager->clear(Stats::class);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_create_stats_from_insee_csv', [
'insee_code' => $inseeCode ?? 'unknown',
'error' => $e->getMessage()
]);
}
}
// Flush les derniers objets
$this->entityManager->flush();
fclose($handle);
$this->addFlash('success', "Création des Stats depuis le CSV INSEE terminée : $createdCount communes ajoutées, $skippedCount déjà existantes, $errorCount erreurs.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/retrieve-city-polygons', name: 'app_admin_retrieve_city_polygons')]
public function retrieveCityPolygons(): Response
{
$this->actionLogger->log('admin/retrieve_city_polygons', []);
// Vérifier que le dossier polygons existe, sinon le créer
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
if (!is_dir($polygonsDir)) {
mkdir($polygonsDir, 0755, true);
}
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$totalCount = count($allStats);
$existingCount = 0;
$createdCount = 0;
$errorCount = 0;
// Pour chaque Stats, récupérer le polygone si nécessaire
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode) {
continue;
}
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
// Vérifier si le polygone existe déjà
if (file_exists($polygonFile)) {
$existingCount++;
continue;
}
try {
// Utiliser le script Python existant pour récupérer le polygone
$command = 'cd ' . __DIR__ . '/../../counting_osm_objects && python3 get_poly.py ' . $inseeCode;
$output = [];
$returnVar = 0;
exec($command, $output, $returnVar);
if ($returnVar === 0 && file_exists($polygonFile)) {
$createdCount++;
} else {
$errorCount++;
$this->actionLogger->log('error_retrieve_city_polygon', [
'insee_code' => $inseeCode,
'error' => 'Failed to retrieve polygon: ' . implode("\n", $output)
]);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_retrieve_city_polygon', [
'insee_code' => $inseeCode,
'error' => $e->getMessage()
]);
}
}
$this->addFlash('success', "Récupération des polygones terminée : $createdCount polygones créés, $existingCount déjà existants, $errorCount erreurs sur un total de $totalCount communes.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/extract-insee-zones', name: 'app_admin_extract_insee_zones')]
public function extractInseeZones(): Response
{
$this->actionLogger->log('admin/extract_insee_zones', []);
// Vérifier que le fichier france-latest.osm.pbf existe
$francePbfFile = __DIR__ . '/../../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');
}
// Vérifier que le dossier polygons existe
$polygonsDir = __DIR__ . '/../../counting_osm_objects/polygons';
if (!is_dir($polygonsDir)) {
$this->addFlash('error', 'Le dossier des polygones n\'existe pas. Veuillez d\'abord exécuter l\'action "Récupérer les polygones des villes".');
return $this->redirectToRoute('app_admin');
}
// Créer le dossier pour les extractions JSON si nécessaire
$extractsDir = __DIR__ . '/../../insee_extracts';
if (!is_dir($extractsDir)) {
mkdir($extractsDir, 0755, true);
}
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$totalCount = count($allStats);
$existingCount = 0;
$createdCount = 0;
$errorCount = 0;
// Pour chaque Stats, extraire les données si nécessaire
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode) {
continue;
}
$polygonFile = $polygonsDir . '/commune_' . $inseeCode . '.poly';
$extractPbfFile = $extractsDir . '/commune_' . $inseeCode . '.osm.pbf';
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
// Vérifier si le polygone existe
if (!file_exists($polygonFile)) {
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => 'Polygon file does not exist'
]);
$errorCount++;
continue;
}
// Vérifier si l'extraction JSON existe déjà
if (file_exists($extractJsonFile)) {
$existingCount++;
continue;
}
try {
// Étape 1: Extraire les données de france-latest.osm.pbf vers un fichier PBF pour la zone
$extractCommand = 'osmium extract -p ' . $polygonFile . ' ' . $francePbfFile . ' -o ' . $extractPbfFile;
$output = [];
$returnVar = 0;
exec($extractCommand, $output, $returnVar);
if ($returnVar !== 0 || !file_exists($extractPbfFile)) {
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => 'Failed to extract PBF: ' . implode("\n", $output)
]);
$errorCount++;
continue;
}
// Étape 2: Convertir le fichier PBF en JSON
$exportCommand = 'osmium export ' . $extractPbfFile . ' -f json -o ' . $extractJsonFile;
$output = [];
$returnVar = 0;
exec($exportCommand, $output, $returnVar);
if ($returnVar === 0 && file_exists($extractJsonFile)) {
$createdCount++;
} else {
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => 'Failed to export to JSON: ' . implode("\n", $output)
]);
$errorCount++;
}
// Supprimer le fichier PBF intermédiaire pour économiser de l'espace
if (file_exists($extractPbfFile)) {
unlink($extractPbfFile);
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_extract_insee_zone', [
'insee_code' => $inseeCode,
'error' => $e->getMessage()
]);
}
}
$this->addFlash('success', "Extraction des zones INSEE terminée : $createdCount extractions créées, $existingCount déjà existantes, $errorCount erreurs sur un total de $totalCount communes.");
return $this->redirectToRoute('app_admin');
}
#[Route('/admin/process-insee-extracts', name: 'app_admin_process_insee_extracts')]
public function processInseeExtracts(): Response
{
$this->actionLogger->log('admin/process_insee_extracts', []);
// Vérifier que le dossier des extractions existe
$extractsDir = __DIR__ . '/../../insee_extracts';
if (!is_dir($extractsDir)) {
$this->addFlash('error', 'Le dossier des extractions n\'existe pas. Veuillez d\'abord exécuter l\'action "Extraire les données des zones INSEE".');
return $this->redirectToRoute('app_admin');
}
// Récupérer toutes les Stats
$statsRepo = $this->entityManager->getRepository(Stats::class);
$allStats = $statsRepo->findAll();
$totalCount = count($allStats);
$processedCount = 0;
$skippedCount = 0;
$errorCount = 0;
// Pour chaque Stats, traiter les données si nécessaire
foreach ($allStats as $stat) {
$inseeCode = $stat->getZone();
if (!$inseeCode) {
continue;
}
$extractJsonFile = $extractsDir . '/commune_' . $inseeCode . '.json';
// Vérifier si l'extraction JSON existe
if (!file_exists($extractJsonFile)) {
$this->actionLogger->log('error_process_insee_extract', [
'insee_code' => $inseeCode,
'error' => 'JSON extract file does not exist'
]);
$errorCount++;
continue;
}
try {
// Utiliser la Motocultrice pour traiter les données
$result = $this->motocultrice->labourer($inseeCode);
if ($result) {
// Mettre à jour la date de labourage
$stat->setDateLabourageDone(new \DateTime());
$this->entityManager->persist($stat);
$processedCount++;
// Flush tous les 10 objets pour éviter de surcharger la mémoire
if ($processedCount % 10 === 0) {
$this->entityManager->flush();
}
} else {
$this->actionLogger->log('error_process_insee_extract', [
'insee_code' => $inseeCode,
'error' => 'Failed to process extract with Motocultrice'
]);
$errorCount++;
}
} catch (\Exception $e) {
$errorCount++;
$this->actionLogger->log('error_process_insee_extract', [
'insee_code' => $inseeCode,
'error' => $e->getMessage()
]);
}
}
// Flush les derniers objets
$this->entityManager->flush();
$this->addFlash('success', "Traitement des extractions INSEE terminé : $processedCount communes traitées, $skippedCount ignorées, $errorCount erreurs sur un total de $totalCount communes.");
return $this->redirectToRoute('app_admin');
}
/** /**
* Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.) * Complète les données manquantes d'un objet Stats (coordonnées, budget, etc.)

View file

@ -5,7 +5,7 @@ jour ou de traductions, et publier des suggestions sur Mastodon pour encourager
## Vue d'ensemble ## Vue d'ensemble
Le projet comprend huit scripts principaux : Le projet comprend neuf scripts principaux :
1. **wiki_compare.py** : Récupère les 10 clés OSM les plus utilisées, compare leurs pages wiki en anglais et en 1. **wiki_compare.py** : Récupère les 10 clés OSM les plus utilisées, compare leurs pages wiki en anglais et en
français, et identifie celles qui ont besoin de mises à jour. français, et identifie celles qui ont besoin de mises à jour.
@ -13,17 +13,19 @@ Le projet comprend huit scripts principaux :
message sur Mastodon pour suggérer sa mise à jour. message sur Mastodon pour suggérer sa mise à jour.
3. **suggest_translation.py** : Identifie les pages wiki anglaises qui n'ont pas de traduction française et publie une 3. **suggest_translation.py** : Identifie les pages wiki anglaises qui n'ont pas de traduction française et publie une
suggestion de traduction sur Mastodon. suggestion de traduction sur Mastodon.
4. **detect_suspicious_deletions.py** : Analyse les changements récents du wiki OSM pour détecter les suppressions 4. **propose_translation.py** : Sélectionne une page wiki (par défaut la première) et utilise Ollama avec le modèle
"mistral:7b" pour proposer une traduction, qui est sauvegardée dans le fichier outdated_pages.json.
5. **detect_suspicious_deletions.py** : Analyse les changements récents du wiki OSM pour détecter les suppressions
suspectes (plus de 20 caractères) et les enregistre dans un fichier JSON pour affichage sur le site web. suspectes (plus de 20 caractères) et les enregistre dans un fichier JSON pour affichage sur le site web.
5. **fetch_proposals.py** : Récupère les propositions de tags OSM en cours de vote et les propositions récemment modifiées, 6. **fetch_proposals.py** : Récupère les propositions de tags OSM en cours de vote et les propositions récemment modifiées,
et les enregistre dans un fichier JSON pour affichage sur le site web. Les données sont mises en cache pendant une heure et les enregistre dans un fichier JSON pour affichage sur le site web. Les données sont mises en cache pendant une heure
pour éviter des requêtes trop fréquentes au serveur wiki. pour éviter des requêtes trop fréquentes au serveur wiki.
6. **find_untranslated_french_pages.py** : Identifie les pages wiki françaises qui n'ont pas de traduction en anglais 7. **find_untranslated_french_pages.py** : Identifie les pages wiki françaises qui n'ont pas de traduction en anglais
et les enregistre dans un fichier JSON pour affichage sur le site web. Les données sont mises en cache pendant une heure. et les enregistre dans un fichier JSON pour affichage sur le site web. Les données sont mises en cache pendant une heure.
7. **find_pages_unavailable_in_french.py** : Scrape la catégorie des pages non disponibles en français, gère la pagination 8. **find_pages_unavailable_in_french.py** : Scrape la catégorie des pages non disponibles en français, gère la pagination
pour récupérer toutes les pages, les groupe par préfixe de langue et priorise les pages commençant par "En:". Les données pour récupérer toutes les pages, les groupe par préfixe de langue et priorise les pages commençant par "En:". Les données
sont mises en cache pendant une heure. sont mises en cache pendant une heure.
8. **fetch_osm_fr_groups.py** : Récupère les informations sur les groupes de travail et les groupes locaux d'OSM-FR 9. **fetch_osm_fr_groups.py** : Récupère les informations sur les groupes de travail et les groupes locaux d'OSM-FR
depuis la section #Pages_des_groupes_locaux et les enregistre dans un fichier JSON pour affichage sur le site web. depuis la section #Pages_des_groupes_locaux et les enregistre dans un fichier JSON pour affichage sur le site web.
Les données sont mises en cache pendant une heure. Les données sont mises en cache pendant une heure.
@ -42,6 +44,15 @@ Installez les dépendances requises :
pip install requests beautifulsoup4 pip install requests beautifulsoup4
``` ```
Pour utiliser le script propose_translation.py, vous devez également installer Ollama :
1. Installez Ollama en suivant les instructions sur [ollama.ai](https://ollama.ai/)
2. Téléchargez le modèle "mistral:7b" :
```bash
ollama pull mistral:7b
```
## Configuration ## Configuration
### Mastodon API ### Mastodon API
@ -111,6 +122,28 @@ Pour simuler la publication sans réellement poster sur Mastodon (mode test) :
./suggest_translation.py --dry-run ./suggest_translation.py --dry-run
``` ```
### Proposer une traduction avec Ollama
Pour sélectionner une page wiki (par défaut la première du fichier outdated_pages.json) et générer une proposition de traduction avec Ollama :
```bash
./propose_translation.py
```
Pour traduire une page spécifique en utilisant sa clé :
```bash
./propose_translation.py --page type
```
Note : Ce script nécessite que Ollama soit installé et exécuté localement avec le modèle "mistral:7b" disponible. Pour installer Ollama, suivez les instructions sur [ollama.ai](https://ollama.ai/). Pour télécharger le modèle "mistral:7b", exécutez :
```bash
ollama pull mistral:7b
```
Le script enregistre la traduction proposée dans la propriété "proposed_translation" de l'entrée correspondante dans le fichier outdated_pages.json.
### Détecter les suppressions suspectes ### Détecter les suppressions suspectes
Pour analyser les changements récents du wiki OSM et détecter les suppressions suspectes : Pour analyser les changements récents du wiki OSM et détecter les suppressions suspectes :

View file

@ -0,0 +1,233 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
propose_translation.py
This script reads the outdated_pages.json file, selects a wiki page (by default the first one),
and uses Ollama with the "mistral:7b" model to propose a translation of the page.
The translation is saved in the "proposed_translation" property of the JSON file.
Usage:
python propose_translation.py [--page KEY]
Options:
--page KEY Specify the key of the page to translate (default: first page in the file)
Output:
- Updated outdated_pages.json file with proposed translations
"""
import json
import argparse
import logging
import requests
import os
import sys
from bs4 import BeautifulSoup
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)
# Constants
OUTDATED_PAGES_FILE = "outdated_pages.json"
OLLAMA_API_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "mistral:7b"
def load_outdated_pages():
"""
Load the outdated pages from the JSON file
Returns:
list: List of dictionaries containing outdated page information
"""
try:
with open(OUTDATED_PAGES_FILE, 'r', encoding='utf-8') as f:
pages = json.load(f)
logger.info(f"Successfully loaded {len(pages)} pages from {OUTDATED_PAGES_FILE}")
return pages
except (IOError, json.JSONDecodeError) as e:
logger.error(f"Error loading pages from {OUTDATED_PAGES_FILE}: {e}")
return []
def save_to_json(data, filename):
"""
Save data to a JSON file
Args:
data: Data to save
filename (str): Name of the file
"""
try:
with open(filename, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
logger.info(f"Data saved to {filename}")
except IOError as e:
logger.error(f"Error saving data to {filename}: {e}")
def fetch_wiki_page_content(url):
"""
Fetch the content of a wiki page
Args:
url (str): URL of the wiki page
Returns:
str: Content of the wiki page
"""
try:
response = requests.get(url)
response.raise_for_status()
soup = BeautifulSoup(response.text, 'html.parser')
# Get the main content
content = soup.select_one('#mw-content-text')
if content:
# Remove script and style elements
for script in content.select('script, style'):
script.extract()
# Remove .languages elements
for languages_elem in content.select('.languages'):
languages_elem.extract()
# Get text
text = content.get_text(separator=' ', strip=True)
return text
else:
logger.warning(f"Could not find content in page: {url}")
return ""
except requests.exceptions.RequestException as e:
logger.error(f"Error fetching wiki page content: {e}")
return ""
def translate_with_ollama(text, model=OLLAMA_MODEL):
"""
Translate text using Ollama
Args:
text (str): Text to translate
model (str): Ollama model to use
Returns:
str: Translated text
"""
prompt = f"""
Tu es un traducteur professionnel spécialisé dans la traduction de documentation technique de l'anglais vers le français.
Traduis le texte suivant de l'anglais vers le français. Conserve le formatage et la structure du texte original.
Ne traduis pas les noms propres, les URLs, et les termes techniques spécifiques à OpenStreetMap.
Texte à traduire:
{text}
"""
try:
logger.info(f"Sending request to Ollama with model {model}")
payload = {
"model": model,
"prompt": prompt,
"stream": False
}
response = requests.post(OLLAMA_API_URL, json=payload)
response.raise_for_status()
result = response.json()
translation = result.get('response', '')
logger.info(f"Successfully received translation from Ollama")
return translation
except requests.exceptions.RequestException as e:
logger.error(f"Error translating with Ollama: {e}")
return ""
def select_page_for_translation(pages, key=None):
"""
Select a page for translation
Args:
pages (list): List of dictionaries containing page information
key (str): Key of the page to select (if None, select the first page)
Returns:
dict: Selected page or None if no suitable page found
"""
if not pages:
logger.warning("No pages found that need translation")
return None
if key:
# Find the page with the specified key
for page in pages:
if page.get('key') == key:
logger.info(f"Selected page for key '{key}' for translation")
return page
logger.warning(f"No page found with key '{key}'")
return None
else:
# Select the first page
selected_page = pages[0]
logger.info(f"Selected first page (key '{selected_page['key']}') for translation")
return selected_page
def main():
"""Main function to execute the script"""
parser = argparse.ArgumentParser(description="Propose a translation for an OSM wiki page using Ollama")
parser.add_argument("--page", help="Key of the page to translate (default: first page in the file)")
args = parser.parse_args()
logger.info("Starting propose_translation.py")
# Load pages
pages = load_outdated_pages()
if not pages:
logger.error("No pages found. Run wiki_compare.py first.")
sys.exit(1)
# Select a page for translation
selected_page = select_page_for_translation(pages, args.page)
if not selected_page:
logger.error("Could not select a page for translation.")
sys.exit(1)
# Get the English page URL
en_url = selected_page.get('en_page', {}).get('url')
if not en_url:
logger.error(f"No English page URL found for key '{selected_page['key']}'")
sys.exit(1)
# Fetch the content of the English page
logger.info(f"Fetching content from {en_url}")
content = fetch_wiki_page_content(en_url)
if not content:
logger.error(f"Could not fetch content from {en_url}")
sys.exit(1)
# Translate the content
logger.info(f"Translating content for key '{selected_page['key']}'")
translation = translate_with_ollama(content)
if not translation:
logger.error("Could not translate content")
sys.exit(1)
# Save the translation in the JSON file
logger.info(f"Saving translation for key '{selected_page['key']}'")
selected_page['proposed_translation'] = translation
# Save the updated data back to the file
save_to_json(pages, OUTDATED_PAGES_FILE)
logger.info("Script completed successfully")
if __name__ == "__main__":
main()