up wiki compare
This commit is contained in:
parent
d2936d5730
commit
1535cf8ee3
8 changed files with 1036 additions and 79 deletions
222
find_largest_commit.sh
Executable file
222
find_largest_commit.sh
Executable file
|
@ -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"
|
|
@ -2334,8 +2334,74 @@ final class AdminController extends AbstractController
|
||||||
$city = $statsRepo->findOneBy(['zone' => $inseeCode]);
|
$city = $statsRepo->findOneBy(['zone' => $inseeCode]);
|
||||||
|
|
||||||
if (!$city) {
|
if (!$city) {
|
||||||
$this->addFlash('error', 'Ville non trouvée pour le code INSEE ' . $inseeCode);
|
// Si aucune stats n'existe, rechercher dans l'API geo.api.gouv.fr
|
||||||
return $this->redirectToRoute('app_admin');
|
$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)
|
// 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
|
// Récupérer les problèmes Osmose pour cette ville
|
||||||
$osmoseIssues = $this->getOsmoseIssuesForCity($city, $theme);
|
$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', [
|
return $this->render('admin/osmose_issues_map.html.twig', [
|
||||||
'city' => $city,
|
'city' => $city,
|
||||||
'theme' => $theme,
|
'theme' => $theme,
|
||||||
'themes' => $themes,
|
'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
|
// Mapping des thèmes vers les items Osmose
|
||||||
$themeToItemsMapping = [
|
$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
|
'restaurants' => [8030, 8031, 8032], // Restaurants et cafés
|
||||||
'hotels' => [8040, 8041, 8042], // Hébergements
|
'hotels' => [8040, 8041, 8042], // Hébergements
|
||||||
'tourism' => [8010, 8011, 8012, 8013], // Tourisme
|
'tourism' => [8010, 8011, 8012, 8013], // Tourisme
|
||||||
'leisure' => [8050, 8051, 8052], // Loisirs
|
'leisure' => [8050, 8051, 8052], // Loisirs
|
||||||
'healthcare' => [8060, 8061, 8062], // Santé
|
|
||||||
'education' => [8070, 8071, 8072], // Éducation
|
|
||||||
'transportation' => [4010, 4020, 4030, 4040], // Transport
|
'transportation' => [4010, 4020, 4030, 4040], // Transport
|
||||||
'amenities' => [8080, 8081, 8082], // Équipements
|
'amenities' => [8080, 8081, 8082], // Équipements
|
||||||
// Si d'autres thèmes sont nécessaires, ajoutez-les ici
|
// 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
|
// Si le thème est 'all', retourner tous les items uniques de tous les thèmes
|
||||||
if ($theme === 'all' || !isset($themeToItemsMapping[$theme])) {
|
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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -282,7 +282,35 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="alertes_osmose"></div>
|
<div class="card mt-4 mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h4><i class="bi bi-exclamation-triangle"></i> Alertes Osmose</h4>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="alertes_osmose">Chargement des alertes...</div>
|
||||||
|
<div id="alertes_liste" class="mt-3" style="display: none;">
|
||||||
|
<h5>Liste complète des alertes</h5>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Élément</th>
|
||||||
|
<th>Position</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="alertes_table_body">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="alertes_distribution" class="mt-4" style="display: none;">
|
||||||
|
<h5>Répartition des alertes par thème</h5>
|
||||||
|
<canvas id="alertesChart" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<canvas id="themeChart"></canvas>
|
<canvas id="themeChart"></canvas>
|
||||||
</div>
|
</div>
|
||||||
|
@ -885,38 +913,70 @@
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (!data.issues || data.issues.length === 0) {
|
if (!data.issues || data.issues.length === 0) {
|
||||||
console.log('Aucune analyse Osmose trouvée pour ce thème dans cette zone.');
|
console.log('Aucune analyse Osmose trouvée pour ce thème dans cette zone.');
|
||||||
|
document.querySelector('#alertes_osmose').innerHTML = '<div class="alert alert-info">Aucune alerte Osmose trouvée pour ce thème dans cette zone.</div>';
|
||||||
return;
|
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(divOsmose){
|
||||||
if (data.issues.length === 1) {
|
if (data.issues.length === 1) {
|
||||||
// Si un seul objet, rendre tout le texte cliquable
|
// Si un seul objet, rendre tout le texte cliquable
|
||||||
const issueId = data.issues[0].id;
|
const issueId = data.issues[0].id;
|
||||||
divOsmose.innerHTML = `<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" style="text-decoration: none; color: inherit;">
|
divOsmose.innerHTML = `<div class="alert alert-warning">
|
||||||
<span class="counter">${data.issues.length}</span> objet à ajouter selon Osmose
|
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" style="text-decoration: none; color: inherit;">
|
||||||
</a>`;
|
<span class="counter">${data.issues.length}</span> objet à ajouter selon Osmose
|
||||||
|
</a>
|
||||||
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
// Si plusieurs objets, lister chaque objet avec son numéro
|
// Si plusieurs objets, afficher un résumé
|
||||||
let content = `<span class="counter">${data.issues.length}</span> objets à ajouter selon Osmose : `;
|
divOsmose.innerHTML = `<div class="alert alert-warning">
|
||||||
|
<span class="counter">${data.issues.length}</span> objets à ajouter selon Osmose
|
||||||
// Limiter à 5 objets affichés pour éviter de surcharger l'interface
|
</div>`;
|
||||||
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 += `<a href="http://localhost:8111/import?url=https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}/fix/0" target="_blank" class="badge bg-purple mx-1">${i}</a>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Indiquer s'il y a plus d'objets que ceux affichés
|
|
||||||
if (data.issues.length > displayLimit) {
|
|
||||||
content += `<span class="text-muted">(et ${data.issues.length - displayLimit} autres)</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
divOsmose.innerHTML = content;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 += `
|
||||||
|
<tr>
|
||||||
|
<td>${issueId}</td>
|
||||||
|
<td>${element}</td>
|
||||||
|
<td>${position}</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://osmose.openstreetmap.fr/fr/error/${issueId}" target="_blank" class="btn btn-sm btn-info" title="Voir sur Osmose">
|
||||||
|
<i class="bi bi-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="http://localhost:8111/import?url=https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}/fix/0" target="_blank" class="btn btn-sm btn-success" title="Corriger dans JOSM">
|
||||||
|
<i class="bi bi-tools"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
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}`);
|
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
|
// Fonction pour charger les détails d'une analyse Osmose
|
||||||
function loadOsmoseIssueDetails(issueId) {
|
function loadOsmoseIssueDetails(issueId) {
|
||||||
const detailsUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}?langs=auto`;
|
const detailsUrl = `https://osmose.openstreetmap.fr/api/0.3/issue/${issueId}?langs=auto`;
|
||||||
|
|
|
@ -93,41 +93,169 @@
|
||||||
<select name="theme" id="theme" class="form-select" onchange="this.form.submit()">
|
<select name="theme" id="theme" class="form-select" onchange="this.form.submit()">
|
||||||
<option value="all" {{ theme == 'all' ? 'selected' : '' }}>Tous les thèmes</option>
|
<option value="all" {{ theme == 'all' ? 'selected' : '' }}>Tous les thèmes</option>
|
||||||
{% for themeKey, themeLabel in themes %}
|
{% for themeKey, themeLabel in themes %}
|
||||||
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
{% if themeKey != 'other' or issuesByTheme['other'] > 0 %}
|
||||||
{{ themeLabel }}
|
<option value="{{ themeKey }}" {{ theme == themeKey ? 'selected' : '' }}>
|
||||||
</option>
|
{{ themeLabel }}{% if issuesByTheme[themeKey] > 0 %} ({{ issuesByTheme[themeKey] }}){% endif %}
|
||||||
|
</option>
|
||||||
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6 text-end">
|
||||||
|
<a href="{{ osmoseApiUrl }}" target="_blank" class="btn btn-primary">
|
||||||
|
<i class="fas fa-external-link-alt"></i> Voir sur Osmose
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="map"></div>
|
<!-- Statistiques des alertes -->
|
||||||
|
<div class="row mb-4">
|
||||||
<div class="row">
|
<div class="col-md-6">
|
||||||
<div class="col-md-12">
|
<div class="card">
|
||||||
<h3>Liste des problèmes ({{ osmoseIssues|length }})</h3>
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">Répartition par thème</h5>
|
||||||
{% if osmoseIssues|length > 0 %}
|
</div>
|
||||||
<div class="issue-list">
|
<div class="card-body">
|
||||||
{% for issue in osmoseIssues %}
|
{% if osmoseIssues|length > 0 %}
|
||||||
<div class="issue-item level-{{ issue.level }}" data-lat="{{ issue.lat }}" data-lon="{{ issue.lon }}">
|
<div class="row">
|
||||||
<h5>{{ issue.title }}</h5>
|
{% for themeKey, count in issuesByTheme %}
|
||||||
{% if issue.subtitle %}
|
{% if count > 0 %}
|
||||||
<p>{{ issue.subtitle }}</p>
|
<div class="col-md-6 mb-2">
|
||||||
{% endif %}
|
<div class="d-flex justify-content-between">
|
||||||
<div class="d-flex justify-content-between">
|
<span>{{ themes[themeKey] }}</span>
|
||||||
<span class="badge bg-secondary">Item: {{ issue.item }}</span>
|
<span class="badge bg-primary">{{ count }}</span>
|
||||||
<a href="{{ issue.url }}" target="_blank" class="btn btn-sm btn-primary">Voir sur Osmose</a>
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar" role="progressbar"
|
||||||
|
style="width: {{ (count / osmoseIssues|length * 100)|round }}%;"
|
||||||
|
aria-valuenow="{{ count }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="{{ osmoseIssues|length }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="text-center">Aucun problème trouvé</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">Répartition par niveau de sévérité</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if osmoseIssues|length > 0 %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12 mb-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Critique</span>
|
||||||
|
<span class="badge bg-danger">{{ issuesByLevel[1] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-danger" role="progressbar"
|
||||||
|
style="width: {{ (issuesByLevel[1] / osmoseIssues|length * 100)|round }}%;"
|
||||||
|
aria-valuenow="{{ issuesByLevel[1] }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="{{ osmoseIssues|length }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Important</span>
|
||||||
|
<span class="badge bg-warning text-dark">{{ issuesByLevel[2] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-warning" role="progressbar"
|
||||||
|
style="width: {{ (issuesByLevel[2] / osmoseIssues|length * 100)|round }}%;"
|
||||||
|
aria-valuenow="{{ issuesByLevel[2] }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="{{ osmoseIssues|length }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-12 mb-2">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<span>Avertissement</span>
|
||||||
|
<span class="badge bg-info">{{ issuesByLevel[3] }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-info" role="progressbar"
|
||||||
|
style="width: {{ (issuesByLevel[3] / osmoseIssues|length * 100)|round }}%;"
|
||||||
|
aria-valuenow="{{ issuesByLevel[3] }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="{{ osmoseIssues|length }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% else %}
|
||||||
|
<p class="text-center">Aucun problème trouvé</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="no-issues">
|
</div>
|
||||||
<p>Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</p>
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="mb-0">Liste des problèmes ({{ osmoseIssues|length }})</h3>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-sm btn-light" id="sort-by-level">Trier par sévérité</button>
|
||||||
|
<button class="btn btn-sm btn-light" id="sort-by-item">Trier par type</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="card-body">
|
||||||
|
{% if osmoseIssues|length > 0 %}
|
||||||
|
<div class="issue-list">
|
||||||
|
{% for issue in osmoseIssues %}
|
||||||
|
<div class="issue-item level-{{ issue.level }}" data-lat="{{ issue.lat }}" data-lon="{{ issue.lon }}" data-level="{{ issue.level }}" data-item="{{ issue.item }}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-9">
|
||||||
|
<h5>{{ issue.title }}</h5>
|
||||||
|
{% if issue.subtitle %}
|
||||||
|
<p class="text-muted">{{ issue.subtitle }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<div class="d-flex gap-2 mt-2">
|
||||||
|
<span class="badge bg-secondary">Item: {{ issue.item }}</span>
|
||||||
|
{% if issue.level == 1 %}
|
||||||
|
<span class="badge bg-danger">Critique</span>
|
||||||
|
{% elseif issue.level == 2 %}
|
||||||
|
<span class="badge bg-warning text-dark">Important</span>
|
||||||
|
{% elseif issue.level == 3 %}
|
||||||
|
<span class="badge bg-info">Avertissement</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 text-end d-flex flex-column justify-content-between">
|
||||||
|
<a href="{{ issue.url }}" target="_blank" class="btn btn-sm btn-primary mb-2">
|
||||||
|
<i class="fas fa-external-link-alt"></i> Voir sur Osmose
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary locate-on-map">
|
||||||
|
<i class="fas fa-map-marker-alt"></i> Localiser sur la carte
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-issues">
|
||||||
|
<p class="text-center">Aucun problème Osmose trouvé pour cette ville avec le filtre actuel.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -311,29 +439,47 @@
|
||||||
map.getCanvas().style.cursor = '';
|
map.getCanvas().style.cursor = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ajouter un événement de clic sur les éléments de la liste
|
// Fonction pour localiser un problème sur la carte
|
||||||
{% for issue in osmoseIssues %}
|
function locateIssueOnMap(lat, lon) {
|
||||||
document.querySelector(`.issue-item[data-lat="{{ issue.lat }}"][data-lon="{{ issue.lon }}"]`)?.addEventListener('click', function() {
|
map.flyTo({
|
||||||
map.flyTo({
|
center: [lon, lat],
|
||||||
center: [{{ issue.lon }}, {{ issue.lat }}],
|
zoom: 18
|
||||||
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]]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
{% 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
|
// Ajuster la vue pour montrer tous les marqueurs si nécessaire
|
||||||
if (features.length > 0) {
|
if (features.length > 0) {
|
||||||
|
@ -347,6 +493,37 @@
|
||||||
padding: 50
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -171,7 +171,100 @@
|
||||||
On compte aussi le nombre de sections et de liens.
|
On compte aussi le nombre de sections et de liens.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2>Graphe de décrépitude</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="decrepitudeChart" height="300"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{{ parent() }}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Collect data from the table
|
||||||
|
const labels = [];
|
||||||
|
const scores = [];
|
||||||
|
const colors = [];
|
||||||
|
|
||||||
|
{% for key, languages in wiki_pages %}
|
||||||
|
{% if languages['en'] is defined and languages['fr'] is defined %}
|
||||||
|
labels.push("{{ key }}");
|
||||||
|
{% set score = languages['en'].staleness_score|default(0) %}
|
||||||
|
scores.push({{ score }});
|
||||||
|
|
||||||
|
// Set color based on score
|
||||||
|
{% if score > 50 %}
|
||||||
|
colors.push('rgba(220, 53, 69, 0.7)'); // danger
|
||||||
|
{% elseif score > 20 %}
|
||||||
|
colors.push('rgba(255, 193, 7, 0.7)'); // warning
|
||||||
|
{% else %}
|
||||||
|
colors.push('rgba(25, 135, 84, 0.7)'); // success
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
// Sort data by score (descending)
|
||||||
|
const indices = Array.from(Array(scores.length).keys())
|
||||||
|
.sort((a, b) => scores[b] - scores[a]);
|
||||||
|
|
||||||
|
const sortedLabels = indices.map(i => labels[i]);
|
||||||
|
const sortedScores = indices.map(i => scores[i]);
|
||||||
|
const sortedColors = indices.map(i => colors[i]);
|
||||||
|
|
||||||
|
// Limit to top 20 pages for readability
|
||||||
|
const displayLimit = 20;
|
||||||
|
const displayLabels = sortedLabels.slice(0, displayLimit);
|
||||||
|
const displayScores = sortedScores.slice(0, displayLimit);
|
||||||
|
const displayColors = sortedColors.slice(0, displayLimit);
|
||||||
|
|
||||||
|
// Create the chart
|
||||||
|
const ctx = document.getElementById('decrepitudeChart').getContext('2d');
|
||||||
|
new Chart(ctx, {
|
||||||
|
type: 'bar',
|
||||||
|
data: {
|
||||||
|
labels: displayLabels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Score de décrépitude',
|
||||||
|
data: displayScores,
|
||||||
|
backgroundColor: displayColors,
|
||||||
|
borderColor: displayColors.map(c => c.replace('0.7', '1')),
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
indexAxis: 'y',
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
return `Score: ${context.raw}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
beginAtZero: true,
|
||||||
|
max: 100,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Score de décrépitude (0-100)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -257,6 +257,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row mt-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<h4>Répartition des années des propositions</h4>
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="yearDistributionChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -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`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
BIN
wiki_compare/staleness_histogram.png
Normal file
BIN
wiki_compare/staleness_histogram.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 55 KiB |
|
@ -26,6 +26,8 @@ import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import logging
|
import logging
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
|
@ -42,6 +44,7 @@ WIKI_BASE_URL_FR = "https://wiki.openstreetmap.org/wiki/FR:Key:"
|
||||||
TOP_KEYS_FILE = "top_keys.json"
|
TOP_KEYS_FILE = "top_keys.json"
|
||||||
WIKI_PAGES_CSV = "wiki_pages.csv"
|
WIKI_PAGES_CSV = "wiki_pages.csv"
|
||||||
OUTDATED_PAGES_FILE = "outdated_pages.json"
|
OUTDATED_PAGES_FILE = "outdated_pages.json"
|
||||||
|
STALENESS_HISTOGRAM_FILE = "staleness_histogram.png"
|
||||||
# Number of wiki pages to examine
|
# Number of wiki pages to examine
|
||||||
NUM_WIKI_PAGES = 100
|
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}")
|
logger.error(f"Error fetching wiki page for key '{key}' in {language}: {e}")
|
||||||
return None
|
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):
|
def analyze_wiki_pages(pages):
|
||||||
"""
|
"""
|
||||||
Analyze wiki pages to determine which ones need updating
|
Analyze wiki pages to determine which ones need updating
|
||||||
|
@ -621,6 +685,9 @@ def main():
|
||||||
fr_page['staleness_score'] = 0
|
fr_page['staleness_score'] = 0
|
||||||
processed_wiki_pages.append(fr_page)
|
processed_wiki_pages.append(fr_page)
|
||||||
|
|
||||||
|
# Generate histogram of staleness scores
|
||||||
|
generate_staleness_histogram(processed_wiki_pages)
|
||||||
|
|
||||||
# Save processed wiki pages to CSV
|
# Save processed wiki pages to CSV
|
||||||
try:
|
try:
|
||||||
with open(WIKI_PAGES_CSV, 'w', newline='', encoding='utf-8') as f:
|
with open(WIKI_PAGES_CSV, 'w', newline='', encoding='utf-8') as f:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue