From 1535cf8ee3e249a2042de5364b7e51cd37daabb9 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sun, 31 Aug 2025 17:57:28 +0200 Subject: [PATCH] up wiki compare --- find_largest_commit.sh | 222 ++++++++++++++ src/Controller/AdminController.php | 170 ++++++++++- .../admin/followup_theme_graph.html.twig | 194 +++++++++++-- templates/admin/osmose_issues_map.html.twig | 271 +++++++++++++++--- templates/admin/wiki.html.twig | 95 +++++- .../admin/wiki_archived_proposals.html.twig | 96 +++++++ wiki_compare/staleness_histogram.png | Bin 0 -> 56236 bytes wiki_compare/wiki_compare.py | 67 +++++ 8 files changed, 1036 insertions(+), 79 deletions(-) create mode 100755 find_largest_commit.sh create mode 100644 wiki_compare/staleness_histogram.png diff --git a/find_largest_commit.sh b/find_largest_commit.sh new file mode 100755 index 00000000..519c595c --- /dev/null +++ b/find_largest_commit.sh @@ -0,0 +1,222 @@ +#!/bin/bash + +# Script to find which commit made the biggest change in repository size +# +# This script analyzes git commit history to determine which commit caused +# the largest change in the repository's size. It checks out each commit, +# measures the repository size, and identifies the commit with the biggest +# size difference. +# +# Usage: ./find_largest_commit.sh [number_of_commits_to_check] +# +# Arguments: +# number_of_commits_to_check: Optional. Number of recent commits to analyze. +# Defaults to 100 if not specified. +# +# Output: +# - Detailed information about the commit with the largest size change +# - A CSV file with data for all analyzed commits +# +# Requirements: +# - git +# - bc (for floating-point calculations) +# - du (for measuring directory sizes) +# +# Author: Junie (JetBrains AI) +# Date: 2025-08-31 + +# Exit on error +set -e + +# Trap for cleanup in case of unexpected exit +trap cleanup EXIT + +cleanup() { + # Make sure we return to the original branch + if [ -n "$CURRENT_BRANCH" ]; then + git checkout -q "$CURRENT_BRANCH" 2>/dev/null || true + + # Restore stashed changes if needed + if [ "$STASH_NEEDED" = true ]; then + echo "Restoring stashed changes..." + git stash pop -q 2>/dev/null || true + fi + fi +} + +# Default to checking the last 100 commits if not specified +NUM_COMMITS=${1:-100} + +# Validate input +if ! [[ "$NUM_COMMITS" =~ ^[0-9]+$ ]]; then + echo "Error: Number of commits must be a positive integer." + echo "Usage: $0 [number_of_commits_to_check]" + exit 1 +fi + +if [ "$NUM_COMMITS" -lt 1 ]; then + echo "Error: Number of commits must be at least 1." + echo "Usage: $0 [number_of_commits_to_check]" + exit 1 +fi + +echo "Analyzing the last $NUM_COMMITS commits to find the largest size change..." +echo "This may take some time depending on repository size and history." +echo + +# Get the list of commit hashes +COMMITS=$(git log --pretty=format:"%H" -n "$NUM_COMMITS") + +# Initialize variables to track the largest change +LARGEST_CHANGE=0 +LARGEST_COMMIT="" +LARGEST_SIZE_BEFORE=0 +LARGEST_SIZE_AFTER=0 + +# Function to get repository size at a specific commit +get_repo_size() { + local commit=$1 + # Checkout the commit + git checkout -q "$commit" + # Calculate size in bytes (excluding .git directory) + local size=$(du -sb --exclude=.git . | cut -f1) + echo "$size" +} + +# Store current branch to return to it later +CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Check if there are uncommitted changes +if [[ -n $(git status -s) ]]; then + echo "Stashing uncommitted changes before proceeding..." + STASH_NEEDED=true + git stash push -m "Temporary stash by find_largest_commit.sh script" +else + STASH_NEEDED=false +fi + +# Temporary file to store results +TEMP_FILE=$(mktemp) + +echo "Commit Hash,Author,Date,Size Before (bytes),Size After (bytes),Change (bytes),Change (%),Message" > "$TEMP_FILE" + +# Counter for progress display +COUNTER=0 +TOTAL_COMMITS=$(echo "$COMMITS" | wc -l) + +# Process each commit +PREV_SIZE="" +for COMMIT in $COMMITS; do + COUNTER=$((COUNTER + 1)) + echo -ne "Processing commit $COUNTER/$TOTAL_COMMITS...\r" + + # Get commit details + AUTHOR=$(git show -s --format="%an" "$COMMIT") + DATE=$(git show -s --format="%cd" --date=format:"%Y-%m-%d %H:%M:%S" "$COMMIT") + MESSAGE=$(git show -s --format="%s" "$COMMIT" | sed 's/,/;/g') # Replace commas with semicolons + + # Get size after this commit + SIZE_AFTER=$(get_repo_size "$COMMIT") + + # If this is the first commit we're checking, we don't have a previous size + if [ -z "$PREV_SIZE" ]; then + PREV_SIZE="$SIZE_AFTER" + continue + fi + + # Calculate size before (which is the size after the previous commit) + SIZE_BEFORE="$PREV_SIZE" + PREV_SIZE="$SIZE_AFTER" + + # Calculate change + CHANGE=$((SIZE_AFTER - SIZE_BEFORE)) + ABS_CHANGE=${CHANGE#-} # Absolute value + + # Calculate percentage change + if [ "$SIZE_BEFORE" -ne 0 ]; then + PERCENT_CHANGE=$(echo "scale=2; 100 * $CHANGE / $SIZE_BEFORE" | bc) + else + PERCENT_CHANGE="N/A" + fi + + # Record the data + echo "$COMMIT,$AUTHOR,$DATE,$SIZE_BEFORE,$SIZE_AFTER,$CHANGE,$PERCENT_CHANGE%,$MESSAGE" >> "$TEMP_FILE" + + # Check if this is the largest change so far + if [ "$ABS_CHANGE" -gt "$LARGEST_CHANGE" ]; then + LARGEST_CHANGE="$ABS_CHANGE" + LARGEST_COMMIT="$COMMIT" + LARGEST_SIZE_BEFORE="$SIZE_BEFORE" + LARGEST_SIZE_AFTER="$SIZE_AFTER" + fi +done + +# Return to the original branch +# (Cleanup function will handle restoring stashed changes) +git checkout -q "$CURRENT_BRANCH" + +echo -e "\nAnalysis complete!" + +# Function to format size in human-readable format +format_size() { + local size=$1 + if [ "$size" -ge 1073741824 ]; then + echo "$(echo "scale=2; $size / 1073741824" | bc) GB" + elif [ "$size" -ge 1048576 ]; then + echo "$(echo "scale=2; $size / 1048576" | bc) MB" + elif [ "$size" -ge 1024 ]; then + echo "$(echo "scale=2; $size / 1024" | bc) KB" + else + echo "$size bytes" + fi +} + +# Display the result +if [ -n "$LARGEST_COMMIT" ]; then + echo + echo "Commit with the largest size change:" + echo "-----------------------------------" + echo "Commit: $LARGEST_COMMIT" + echo "Author: $(git show -s --format="%an" "$LARGEST_COMMIT")" + echo "Date: $(git show -s --format="%cd" --date=format:"%Y-%m-%d %H:%M:%S" "$LARGEST_COMMIT")" + echo "Message: $(git show -s --format="%s" "$LARGEST_COMMIT")" + echo + echo "Size before: $(format_size "$LARGEST_SIZE_BEFORE")" + echo "Size after: $(format_size "$LARGEST_SIZE_AFTER")" + + CHANGE=$((LARGEST_SIZE_AFTER - LARGEST_SIZE_BEFORE)) + if [ "$CHANGE" -ge 0 ]; then + echo "Change: +$(format_size "${CHANGE#-}") (increased)" + else + echo "Change: -$(format_size "${CHANGE#-}") (decreased)" + fi + + if [ "$LARGEST_SIZE_BEFORE" -ne 0 ]; then + PERCENT_CHANGE=$(echo "scale=2; 100 * $CHANGE / $LARGEST_SIZE_BEFORE" | bc) + echo "Percentage change: $PERCENT_CHANGE%" + fi + + echo + echo "Files changed in this commit:" + + # Get the list of changed files + CHANGED_FILES=$(git show --stat "$LARGEST_COMMIT" | grep '|' | sort -rn -k3) + TOTAL_FILES=$(echo "$CHANGED_FILES" | wc -l) + + # If there are too many files, show only the top 10 with the most changes + if [ "$TOTAL_FILES" -gt 10 ]; then + echo "$CHANGED_FILES" | head -n 10 + echo "... and $(($TOTAL_FILES - 10)) more files (total: $TOTAL_FILES files changed)" + else + echo "$CHANGED_FILES" + fi +else + echo "No commits analyzed." +fi + +echo +echo "Full results saved to: $TEMP_FILE" +echo "You can import this CSV file into a spreadsheet for further analysis." + +# Make the script executable +chmod +x "$0" \ No newline at end of file diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 79164936..90c4736f 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -2334,8 +2334,74 @@ final class AdminController extends AbstractController $city = $statsRepo->findOneBy(['zone' => $inseeCode]); if (!$city) { - $this->addFlash('error', 'Ville non trouvée pour le code INSEE ' . $inseeCode); - return $this->redirectToRoute('app_admin'); + // Si aucune stats n'existe, rechercher dans l'API geo.api.gouv.fr + $apiUrl = "https://geo.api.gouv.fr/communes/{$inseeCode}"; + $response = @file_get_contents($apiUrl); + + if ($response === false) { + $this->addFlash('error', 'Ville non trouvée pour le code INSEE ' . $inseeCode . ' et impossible de récupérer les informations depuis l\'API geo.api.gouv.fr.'); + return $this->redirectToRoute('app_admin'); + } + + $communeData = json_decode($response, true); + if (!$communeData || !isset($communeData['nom'])) { + $this->addFlash('error', 'Aucune commune trouvée avec ce code INSEE dans l\'API geo.api.gouv.fr.'); + return $this->redirectToRoute('app_admin'); + } + + // Créer un nouvel objet Stats avec les données de l'API + $city = new Stats(); + $city->setZone($inseeCode) + ->setName($communeData['nom']) + ->setDateCreated(new \DateTime()) + ->setDateModified(new \DateTime()) + ->setKind('osmose_request'); + + // Ajouter la population si disponible + if (isset($communeData['population'])) { + $city->setPopulation($communeData['population']); + } + + // Ajouter les coordonnées si disponibles + if (isset($communeData['centre']) && isset($communeData['centre']['coordinates'])) { + $city->setLon((string)$communeData['centre']['coordinates'][0]); + $city->setLat((string)$communeData['centre']['coordinates'][1]); + } else { + // Si les coordonnées ne sont pas dans la réponse initiale, faire une requête spécifique + try { + $apiUrl = 'https://geo.api.gouv.fr/communes/' . $inseeCode . '?fields=centre'; + $response = @file_get_contents($apiUrl); + if ($response !== false) { + $data = json_decode($response, true); + if (isset($data['centre']['coordinates']) && count($data['centre']['coordinates']) === 2) { + $city->setLon((string)$data['centre']['coordinates'][0]); + $city->setLat((string)$data['centre']['coordinates'][1]); + } + } + } catch (\Exception $e) { + // Ignorer les erreurs lors de la récupération des coordonnées + } + } + + // Ajouter les codes postaux si disponibles + if (isset($communeData['codesPostaux']) && !empty($communeData['codesPostaux'])) { + $city->setCodesPostaux(implode(',', $communeData['codesPostaux'])); + } + + // Ajouter le code EPCI si disponible + if (isset($communeData['codeEpci'])) { + $city->setCodeEpci((int)$communeData['codeEpci']); + } + + // Ajouter le SIREN si disponible + if (isset($communeData['siren'])) { + $city->setSiren((int)$communeData['siren']); + } + + // Ne pas faire de labourage des Places pour cette ville + // Persister l'objet Stats + $this->entityManager->persist($city); + $this->entityManager->flush(); } // Récupérer le thème sélectionné (par défaut: tous) @@ -2347,11 +2413,86 @@ final class AdminController extends AbstractController // Récupérer les problèmes Osmose pour cette ville $osmoseIssues = $this->getOsmoseIssuesForCity($city, $theme); + // Créer un mapping inverse des items Osmose vers les thèmes + $itemToThemeMapping = []; + $themeToItemsMapping = [ + 'charging_station' => [8410, 8411], + 'school' => [8031], + 'healthcare' => [8211, 7220, 8331], + 'laboratory' => [7240, 8351], + 'police' => [8190, 8191], + 'defibrillator' => [8370], + 'places' => [7240, 8351, 8211, 7220, 8331, 8031], + 'restaurants' => [8030, 8031, 8032], + 'hotels' => [8040, 8041, 8042], + 'tourism' => [8010, 8011, 8012, 8013], + 'leisure' => [8050, 8051, 8052], + 'transportation' => [4010, 4020, 4030, 4040], + 'amenities' => [8080, 8081, 8082], + ]; + + foreach ($themeToItemsMapping as $themeName => $itemIds) { + foreach ($itemIds as $itemId) { + if (!isset($itemToThemeMapping[$itemId])) { + $itemToThemeMapping[$itemId] = []; + } + $itemToThemeMapping[$itemId][] = $themeName; + } + } + + // Compter les problèmes par thème + $issuesByTheme = []; + foreach ($themes as $themeKey => $themeLabel) { + $issuesByTheme[$themeKey] = 0; + } + + // Ajouter un compteur pour "Autres" (problèmes qui ne correspondent à aucun thème) + $issuesByTheme['other'] = 0; + + // Compter les problèmes par niveau de sévérité + $issuesByLevel = [ + 1 => 0, // Critique + 2 => 0, // Important + 3 => 0, // Avertissement + ]; + + foreach ($osmoseIssues as $issue) { + // Compter par niveau de sévérité + $level = (int)$issue['level']; + if (isset($issuesByLevel[$level])) { + $issuesByLevel[$level]++; + } + + // Compter par thème + $itemId = (int)$issue['item']; + $counted = false; + + if (isset($itemToThemeMapping[$itemId])) { + foreach ($itemToThemeMapping[$itemId] as $themeName) { + if (isset($issuesByTheme[$themeName])) { + $issuesByTheme[$themeName]++; + $counted = true; + } + } + } + + // Si le problème n'a été compté dans aucun thème, l'ajouter à "Autres" + if (!$counted) { + $issuesByTheme['other']++; + } + } + + // Ajouter le libellé pour "Autres" + $themes['other'] = 'Autres problèmes'; + return $this->render('admin/osmose_issues_map.html.twig', [ 'city' => $city, 'theme' => $theme, 'themes' => $themes, - 'osmoseIssues' => $osmoseIssues + 'osmoseIssues' => $osmoseIssues, + 'issuesByTheme' => $issuesByTheme, + 'issuesByLevel' => $issuesByLevel, + 'osmoseApiUrl' => 'https://osmose.openstreetmap.fr/fr/map/#zoom=14&lat=' . $city->getLat() . '&lon=' . $city->getLon() ]); } @@ -2478,20 +2619,33 @@ final class AdminController extends AbstractController { // Mapping des thèmes vers les items Osmose $themeToItemsMapping = [ - 'places' => [8230, 8240, 8250, 8260], // Commerces et services + 'charging_station' => [8410, 8411], + 'school' => [8031], + 'healthcare' => [8211, 7220, 8331], + 'laboratory' => [7240, 8351], + 'police' => [8190, 8191], + 'defibrillator' => [8370], + 'places' => [7240, 8351, 8211, 7220, 8331, 8031], 'restaurants' => [8030, 8031, 8032], // Restaurants et cafés 'hotels' => [8040, 8041, 8042], // Hébergements 'tourism' => [8010, 8011, 8012, 8013], // Tourisme 'leisure' => [8050, 8051, 8052], // Loisirs - 'healthcare' => [8060, 8061, 8062], // Santé - 'education' => [8070, 8071, 8072], // Éducation 'transportation' => [4010, 4020, 4030, 4040], // Transport 'amenities' => [8080, 8081, 8082], // Équipements // Si d'autres thèmes sont nécessaires, ajoutez-les ici ]; - // Si le thème est 'all' ou n'existe pas dans le mapping, retourner un tableau vide - if ($theme === 'all' || !isset($themeToItemsMapping[$theme])) { + // Si le thème est 'all', retourner tous les items uniques de tous les thèmes + if ($theme === 'all') { + $allItems = []; + foreach ($themeToItemsMapping as $items) { + $allItems = array_merge($allItems, $items); + } + return array_unique($allItems); + } + + // Si le thème n'existe pas dans le mapping, retourner un tableau vide + if (!isset($themeToItemsMapping[$theme])) { return []; } diff --git a/templates/admin/followup_theme_graph.html.twig b/templates/admin/followup_theme_graph.html.twig index eb92abc1..4d004d36 100644 --- a/templates/admin/followup_theme_graph.html.twig +++ b/templates/admin/followup_theme_graph.html.twig @@ -282,7 +282,35 @@ -
+
+
+

Alertes Osmose

+
+
+
Chargement des alertes...
+ + +
+
@@ -885,38 +913,70 @@ .then(data => { if (!data.issues || data.issues.length === 0) { console.log('Aucune analyse Osmose trouvée pour ce thème dans cette zone.'); + document.querySelector('#alertes_osmose').innerHTML = '
Aucune alerte Osmose trouvée pour ce thème dans cette zone.
'; return; } - const divOsmose = document.querySelector(('#alertes_osmose')) + // Stocker les données Osmose globalement pour pouvoir les utiliser ailleurs + window.osmoseData = data.issues; + + // Mettre à jour le résumé des alertes + const divOsmose = document.querySelector('#alertes_osmose'); if(divOsmose){ if (data.issues.length === 1) { // Si un seul objet, rendre tout le texte cliquable const issueId = data.issues[0].id; - divOsmose.innerHTML = ` - ${data.issues.length} objet à ajouter selon Osmose - `; + divOsmose.innerHTML = `
+ + ${data.issues.length} objet à ajouter selon Osmose + +
`; } else { - // Si plusieurs objets, lister chaque objet avec son numéro - let content = `${data.issues.length} objets à ajouter selon Osmose : `; - - // Limiter à 5 objets affichés pour éviter de surcharger l'interface - const displayLimit = 5; - const displayCount = Math.min(data.issues.length, displayLimit); - - for (let i = 0; i < displayCount; i++) { - const issueId = data.issues[i].id; - content += `${i}`; - } - - // Indiquer s'il y a plus d'objets que ceux affichés - if (data.issues.length > displayLimit) { - content += `(et ${data.issues.length - displayLimit} autres)`; - } - - divOsmose.innerHTML = content; + // Si plusieurs objets, afficher un résumé + divOsmose.innerHTML = `
+ ${data.issues.length} objets à ajouter selon Osmose +
`; } } + + // Remplir la table des alertes + const alertesTableBody = document.querySelector('#alertes_table_body'); + if (alertesTableBody) { + let tableContent = ''; + + data.issues.forEach((issue, index) => { + const issueId = issue.id; + const element = issue.elems && issue.elems.length > 0 ? + `${issue.elems[0].type} ${issue.elems[0].id}` : 'Non spécifié'; + const position = issue.lat && issue.lon ? + `${issue.lat.toFixed(5)}, ${issue.lon.toFixed(5)}` : 'Non spécifié'; + + tableContent += ` + + ${issueId} + ${element} + ${position} + + + + + + + + + + `; + }); + + alertesTableBody.innerHTML = tableContent; + document.querySelector('#alertes_liste').style.display = 'block'; + } + + // Créer la distribution des alertes par thème + createAlertesDistribution(data.issues); + + // Afficher la section de distribution + document.querySelector('#alertes_distribution').style.display = 'block'; console.log(`[Osmose] ${data.issues.length} analyses trouvées pour le thème ${theme}`); @@ -954,6 +1014,94 @@ }); } + // Fonction pour créer le graphique de distribution des alertes par thème + function createAlertesDistribution(issues) { + if (!issues || issues.length === 0) return; + + // Compter les alertes par item + const itemCounts = {}; + const itemLabels = { + 8410: 'Borne de recharge (manquante)', + 8411: 'Borne de recharge (à compléter)', + 8031: 'École (manquante)', + 8211: 'Pharmacie (manquante)', + 7220: 'Cabinet médical (manquant)', + 8331: 'Hôpital (manquant)', + 7240: 'Laboratoire (manquant)', + 8351: 'Laboratoire (à compléter)', + 8190: 'Police (manquante)', + 8191: 'Police (à compléter)', + 8370: 'Défibrillateur (manquant)' + }; + + issues.forEach(issue => { + if (issue.item) { + if (!itemCounts[issue.item]) { + itemCounts[issue.item] = 0; + } + itemCounts[issue.item]++; + } + }); + + // Préparer les données pour le graphique + const labels = []; + const data = []; + const backgroundColor = []; + + // Générer des couleurs aléatoires pour chaque thème + function getRandomColor() { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; + } + + // Trier les items par nombre d'alertes (décroissant) + const sortedItems = Object.keys(itemCounts).sort((a, b) => itemCounts[b] - itemCounts[a]); + + sortedItems.forEach(item => { + // Utiliser le label s'il existe, sinon utiliser l'ID de l'item + labels.push(itemLabels[item] || `Item ${item}`); + data.push(itemCounts[item]); + backgroundColor.push(getRandomColor()); + }); + + // Créer le graphique + const ctx = document.getElementById('alertesChart').getContext('2d'); + new Chart(ctx, { + type: 'pie', + data: { + labels: labels, + datasets: [{ + data: data, + backgroundColor: backgroundColor, + borderWidth: 1 + }] + }, + options: { + responsive: true, + plugins: { + legend: { + position: 'right', + }, + tooltip: { + callbacks: { + label: function(context) { + const label = context.label || ''; + const value = context.raw; + const total = context.dataset.data.reduce((a, b) => a + b, 0); + const percentage = Math.round((value / total) * 100); + return `${label}: ${value} (${percentage}%)`; + } + } + } + } + } + }); + } + // Fonction pour charger les détails d'une analyse Osmose function loadOsmoseIssueDetails(issueId) { const detailsUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}?langs=auto`; diff --git a/templates/admin/osmose_issues_map.html.twig b/templates/admin/osmose_issues_map.html.twig index b1a0ab74..2446c653 100644 --- a/templates/admin/osmose_issues_map.html.twig +++ b/templates/admin/osmose_issues_map.html.twig @@ -93,41 +93,169 @@ +
+ + Voir sur Osmose + +
-
- -
-
-

Liste des problèmes ({{ osmoseIssues|length }})

- - {% if osmoseIssues|length > 0 %} -
- {% for issue in osmoseIssues %} -
-
{{ issue.title }}
- {% if issue.subtitle %} -

{{ issue.subtitle }}

- {% endif %} -
- Item: {{ issue.item }} - Voir sur Osmose + +
+
+
+
+
Répartition par thème
+
+
+ {% if osmoseIssues|length > 0 %} +
+ {% for themeKey, count in issuesByTheme %} + {% if count > 0 %} +
+
+ {{ themes[themeKey] }} + {{ count }} +
+
+
+
+
+
+ {% endif %} + {% endfor %} +
+ {% else %} +

Aucun problème trouvé

+ {% endif %} +
+
+
+
+
+
+
Répartition par niveau de sévérité
+
+
+ {% if osmoseIssues|length > 0 %} +
+
+
+ Critique + {{ issuesByLevel[1] }} +
+
+
+
+
+
+
+
+ Important + {{ issuesByLevel[2] }} +
+
+
+
+
+
+
+
+ Avertissement + {{ issuesByLevel[3] }} +
+
+
+
+
- {% endfor %} + {% else %} +

Aucun problème trouvé

+ {% endif %}
- {% else %} -
-

Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.

+
+
+
+ +
+ +
+
+
+
+

Liste des problèmes ({{ osmoseIssues|length }})

+
+ + +
- {% endif %} +
+ {% if osmoseIssues|length > 0 %} +
+ {% for issue in osmoseIssues %} +
+
+
+
{{ issue.title }}
+ {% if issue.subtitle %} +

{{ issue.subtitle }}

+ {% endif %} +
+ Item: {{ issue.item }} + {% if issue.level == 1 %} + Critique + {% elseif issue.level == 2 %} + Important + {% elseif issue.level == 3 %} + Avertissement + {% endif %} +
+
+
+ + Voir sur Osmose + + +
+
+
+ {% endfor %} +
+ {% else %} +
+

Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.

+
+ {% endif %} +
+
@@ -311,29 +439,47 @@ map.getCanvas().style.cursor = ''; }); - // Ajouter un événement de clic sur les éléments de la liste - {% for issue in osmoseIssues %} - document.querySelector(`.issue-item[data-lat="{{ issue.lat }}"][data-lon="{{ issue.lon }}"]`)?.addEventListener('click', function() { - map.flyTo({ - center: [{{ issue.lon }}, {{ issue.lat }}], - zoom: 18 - }); - - // Simuler un clic sur le point pour ouvrir la popup - const features = map.queryRenderedFeatures( - map.project([{{ issue.lon }}, {{ issue.lat }}]), - { layers: ['unclustered-point'] } - ); - - if (features.length > 0) { - map.fire('click', { - lngLat: { lng: {{ issue.lon }}, lat: {{ issue.lat }} }, - point: map.project([{{ issue.lon }}, {{ issue.lat }}]), - features: [features[0]] - }); - } + // Fonction pour localiser un problème sur la carte + function locateIssueOnMap(lat, lon) { + map.flyTo({ + center: [lon, lat], + zoom: 18 }); - {% endfor %} + + // Simuler un clic sur le point pour ouvrir la popup + const features = map.queryRenderedFeatures( + map.project([lon, lat]), + { layers: ['unclustered-point'] } + ); + + if (features.length > 0) { + map.fire('click', { + lngLat: { lng: lon, lat: lat }, + point: map.project([lon, lat]), + features: [features[0]] + }); + } + } + + // Ajouter un événement de clic sur les boutons "Localiser sur la carte" + document.querySelectorAll('.locate-on-map').forEach(function(button) { + button.addEventListener('click', function(e) { + e.stopPropagation(); // Empêcher la propagation au parent + const issueItem = this.closest('.issue-item'); + const lat = parseFloat(issueItem.dataset.lat); + const lon = parseFloat(issueItem.dataset.lon); + locateIssueOnMap(lat, lon); + }); + }); + + // Ajouter un événement de clic sur les éléments de la liste + document.querySelectorAll('.issue-item').forEach(function(item) { + item.addEventListener('click', function() { + const lat = parseFloat(this.dataset.lat); + const lon = parseFloat(this.dataset.lon); + locateIssueOnMap(lat, lon); + }); + }); // Ajuster la vue pour montrer tous les marqueurs si nécessaire if (features.length > 0) { @@ -347,6 +493,37 @@ padding: 50 }); } + + // Fonctions de tri pour les problèmes + function sortIssuesByLevel() { + const issueList = document.querySelector('.issue-list'); + const issues = Array.from(issueList.querySelectorAll('.issue-item')); + + issues.sort(function(a, b) { + return parseInt(a.dataset.level) - parseInt(b.dataset.level); + }); + + issues.forEach(function(issue) { + issueList.appendChild(issue); + }); + } + + function sortIssuesByItem() { + const issueList = document.querySelector('.issue-list'); + const issues = Array.from(issueList.querySelectorAll('.issue-item')); + + issues.sort(function(a, b) { + return parseInt(a.dataset.item) - parseInt(b.dataset.item); + }); + + issues.forEach(function(issue) { + issueList.appendChild(issue); + }); + } + + // Ajouter des événements de clic sur les boutons de tri + document.getElementById('sort-by-level').addEventListener('click', sortIssuesByLevel); + document.getElementById('sort-by-item').addEventListener('click', sortIssuesByItem); }); }); diff --git a/templates/admin/wiki.html.twig b/templates/admin/wiki.html.twig index e33b8765..6e596ba0 100644 --- a/templates/admin/wiki.html.twig +++ b/templates/admin/wiki.html.twig @@ -171,7 +171,100 @@ On compte aussi le nombre de sections et de liens.

- +
+
+

Graphe de décrépitude

+
+
+ +
+
+{% endblock %} + +{% block javascripts %} + {{ parent() }} + + {% endblock %} \ No newline at end of file diff --git a/templates/admin/wiki_archived_proposals.html.twig b/templates/admin/wiki_archived_proposals.html.twig index 74b8d7ee..b24aad85 100644 --- a/templates/admin/wiki_archived_proposals.html.twig +++ b/templates/admin/wiki_archived_proposals.html.twig @@ -257,6 +257,15 @@
{% endif %} + +
+
+

Répartition des années des propositions

+
+ +
+
+
@@ -578,6 +587,93 @@ }); } } + + // Initialize year distribution chart + const yearChartCanvas = document.getElementById('yearDistributionChart'); + if (yearChartCanvas) { + // Get proposals data from the template + const proposals = {{ proposals|json_encode|raw }}; + + if (proposals && proposals.length > 0) { + // Extract years from last_modified dates + const yearCounts = {}; + + proposals.forEach(proposal => { + if (proposal.last_modified) { + // Extract year from the date string (format: "DD Month YYYY") + const yearMatch = proposal.last_modified.match(/\d{4}$/); + if (yearMatch) { + const year = yearMatch[0]; + yearCounts[year] = (yearCounts[year] || 0) + 1; + } + } + }); + + // Sort years chronologically + const sortedYears = Object.keys(yearCounts).sort(); + const counts = sortedYears.map(year => yearCounts[year]); + + // Generate a color gradient for the bars + const colors = sortedYears.map((year, index) => { + // Create a gradient from blue to green + const ratio = index / (sortedYears.length - 1 || 1); + return `rgba(${Math.round(33 + (20 * ratio))}, ${Math.round(150 + (50 * ratio))}, ${Math.round(243 - (100 * ratio))}, 0.7)`; + }); + + // Create the chart + new Chart(yearChartCanvas, { + type: 'bar', + data: { + labels: sortedYears, + datasets: [{ + label: 'Nombre de propositions', + data: counts, + backgroundColor: colors, + borderColor: colors.map(color => color.replace('0.7', '1')), + borderWidth: 1 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + ticks: { + precision: 0 + }, + title: { + display: true, + text: 'Nombre de propositions' + } + }, + x: { + title: { + display: true, + text: 'Année' + } + } + }, + plugins: { + legend: { + display: false + }, + tooltip: { + callbacks: { + title: function(context) { + return `Année ${context[0].label}`; + }, + label: function(context) { + const count = context.raw; + return count > 1 ? `${count} propositions` : `${count} proposition`; + } + } + } + } + } + }); + } + } }); {% endblock %} \ No newline at end of file diff --git a/wiki_compare/staleness_histogram.png b/wiki_compare/staleness_histogram.png new file mode 100644 index 0000000000000000000000000000000000000000..d07a2a84e9df3fe075bb2fae73ec1446b84a37a1 GIT binary patch literal 56236 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 Q83jL{Za(T9mqoGv0{gk&>;M1& literal 0 HcmV?d00001 diff --git a/wiki_compare/wiki_compare.py b/wiki_compare/wiki_compare.py index bf27b74a..3355492c 100755 --- a/wiki_compare/wiki_compare.py +++ b/wiki_compare/wiki_compare.py @@ -26,6 +26,8 @@ import os from datetime import datetime from bs4 import BeautifulSoup import logging +import matplotlib.pyplot as plt +import numpy as np # Configure logging logging.basicConfig( @@ -42,6 +44,7 @@ WIKI_BASE_URL_FR = "https://wiki.openstreetmap.org/wiki/FR:Key:" TOP_KEYS_FILE = "top_keys.json" WIKI_PAGES_CSV = "wiki_pages.csv" OUTDATED_PAGES_FILE = "outdated_pages.json" +STALENESS_HISTOGRAM_FILE = "staleness_histogram.png" # Number of wiki pages to examine NUM_WIKI_PAGES = 100 @@ -255,6 +258,67 @@ def fetch_wiki_page(key, language='en'): logger.error(f"Error fetching wiki page for key '{key}' in {language}: {e}") return None +def generate_staleness_histogram(wiki_pages): + """ + Generate a histogram of staleness scores by 10% ranges + + Args: + wiki_pages (list): List of dictionaries containing page information with staleness scores + + Returns: + None: Saves the histogram to a file + """ + logger.info("Generating histogram of staleness scores by 10% ranges...") + + # Extract staleness scores + staleness_scores = [] + for page in wiki_pages: + if page and 'staleness_score' in page: + staleness_scores.append(page['staleness_score']) + + if not staleness_scores: + logger.warning("No staleness scores found. Cannot generate histogram.") + return + + # Determine the maximum score for binning + max_score = max(staleness_scores) + # Round up to the nearest 10 to ensure all scores are included + max_bin_edge = np.ceil(max_score / 10) * 10 + + # Create bins for 10% ranges + bins = np.arange(0, max_bin_edge + 10, 10) + + # Count scores in each bin + hist, bin_edges = np.histogram(staleness_scores, bins=bins) + + # Create histogram + plt.figure(figsize=(12, 6)) + + # Create bar chart + plt.bar(range(len(hist)), hist, align='center') + + # Set x-axis labels for each bin + bin_labels = [f"{int(bin_edges[i])}-{int(bin_edges[i+1])}%" for i in range(len(bin_edges)-1)] + plt.xticks(range(len(hist)), bin_labels, rotation=45) + + # Set labels and title + plt.xlabel('Tranches de score de décrépitude (en %)') + plt.ylabel('Nombre de pages') + plt.title('Répartition du score de décrépitude par tranches de 10%') + + # Add grid for better readability + plt.grid(axis='y', linestyle='--', alpha=0.7) + + # Adjust layout + plt.tight_layout() + + # Save figure + plt.savefig(STALENESS_HISTOGRAM_FILE) + logger.info(f"Histogram saved to {STALENESS_HISTOGRAM_FILE}") + + # Close the figure to free memory + plt.close() + def analyze_wiki_pages(pages): """ Analyze wiki pages to determine which ones need updating @@ -621,6 +685,9 @@ def main(): fr_page['staleness_score'] = 0 processed_wiki_pages.append(fr_page) + # Generate histogram of staleness scores + generate_staleness_histogram(processed_wiki_pages) + # Save processed wiki pages to CSV try: with open(WIKI_PAGES_CSV, 'w', newline='', encoding='utf-8') as f: