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 00000000..d07a2a84 Binary files /dev/null and b/wiki_compare/staleness_histogram.png differ 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: