Auto-update: 2025-10-23 15:04:00

This commit is contained in:
divingeek 2025-10-23 15:04:00 +02:00
parent 7698240a79
commit 6170c48371
28 changed files with 19938 additions and 0 deletions

View file

@ -0,0 +1,693 @@
"""
Script pour générer une page HTML interactive avec les données des villages corses embarquées.
Usage: python generate_interactive_map.py villages_corse.csv
"""
import csv
import json
import sys
def parse_coordinates(point_geo_str):
"""Parse la colonne Point_Geo"""
try:
parts = point_geo_str.split(',')
lat = float(parts[0].strip())
lon = float(parts[1].strip())
return lat, lon
except:
return None, None
def load_villages_from_csv(csv_file):
"""Charge les villages depuis le CSV"""
villages = []
with open(csv_file, 'r', encoding='utf-8') as f:
reader = csv.reader(f, delimiter=';')
next(reader) # Skip header
for row in reader:
if len(row) >= 18:
lat, lon = parse_coordinates(row[17])
if lat and lon:
villages.append({
'name': row[0],
'nameCorse': row[1],
'lat': lat,
'lon': lon,
'dept': row[9],
'altitude': float(row[15]) if row[15] else 0
})
return villages
def generate_html(villages, output_file='knn_interactive_full.html'):
"""Génère le fichier HTML avec les données embarquées"""
villages_json = json.dumps(villages, ensure_ascii=False)
html_content = f'''<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Classification k-NN Interactive - Corse</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
* {{
margin: 0;
padding: 0;
box-sizing: border-box;
}}
body {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}}
.header {{
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}}
.header h1 {{
font-size: 2.5em;
margin-bottom: 10px;
}}
.header p {{
font-size: 1.2em;
opacity: 0.9;
}}
.main-content {{
display: grid;
grid-template-columns: 350px 1fr;
gap: 0;
}}
.sidebar {{
background: #f5f5f5;
padding: 30px;
border-right: 2px solid #ddd;
max-height: 900px;
overflow-y: auto;
}}
.controls {{
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.controls h3 {{
margin-bottom: 15px;
color: #333;
font-size: 1.2em;
}}
.control-group {{
margin-bottom: 20px;
}}
.control-group label {{
display: block;
font-weight: bold;
color: #555;
margin-bottom: 8px;
}}
.slider-container {{
display: flex;
align-items: center;
gap: 10px;
}}
input[type="range"] {{
flex: 1;
height: 8px;
border-radius: 5px;
background: #ddd;
outline: none;
}}
input[type="range"]::-webkit-slider-thumb {{
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
}}
input[type="range"]::-moz-range-thumb {{
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
}}
.k-value {{
background: #667eea;
color: white;
padding: 5px 15px;
border-radius: 20px;
font-weight: bold;
min-width: 40px;
text-align: center;
}}
.result-box {{
background: white;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.result-box h3 {{
margin-bottom: 15px;
color: #333;
}}
.prediction {{
font-size: 1.3em;
font-weight: bold;
padding: 15px;
border-radius: 8px;
text-align: center;
margin-bottom: 15px;
}}
.prediction.corse-sud {{
background: #ffebee;
color: #c62828;
}}
.prediction.haute-corse {{
background: #e3f2fd;
color: #1565c0;
}}
.prediction.no-result {{
background: #f5f5f5;
color: #666;
}}
.coords {{
font-size: 0.9em;
color: #666;
text-align: center;
margin-bottom: 10px;
}}
.votes {{
display: flex;
justify-content: space-around;
margin-bottom: 15px;
}}
.vote-item {{
text-align: center;
}}
.vote-count {{
font-size: 2em;
font-weight: bold;
}}
.vote-count.red {{
color: #c62828;
}}
.vote-count.blue {{
color: #1565c0;
}}
.neighbors-list {{
max-height: 300px;
overflow-y: auto;
background: #fafafa;
padding: 10px;
border-radius: 5px;
}}
.neighbor-item {{
padding: 10px;
margin-bottom: 8px;
background: white;
border-radius: 5px;
border-left: 4px solid #ddd;
}}
.neighbor-item.dept-2a {{
border-left-color: #c62828;
}}
.neighbor-item.dept-2b {{
border-left-color: #1565c0;
}}
.neighbor-name {{
font-weight: bold;
margin-bottom: 3px;
}}
.neighbor-corse {{
font-style: italic;
color: #666;
font-size: 0.9em;
}}
.neighbor-distance {{
font-size: 0.9em;
color: #666;
}}
.instructions {{
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
}}
.instructions h4 {{
margin-bottom: 10px;
color: #856404;
}}
.instructions ul {{
margin-left: 20px;
color: #856404;
}}
.instructions li {{
margin-bottom: 5px;
}}
.legend {{
background: white;
padding: 15px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}}
.legend h4 {{
margin-bottom: 10px;
color: #333;
}}
.legend-item {{
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}}
.legend-circle {{
width: 20px;
height: 20px;
border-radius: 50%;
border: 2px solid #333;
}}
.legend-circle.red {{
background: #e74c3c;
}}
.legend-circle.blue {{
background: #3498db;
}}
.legend-circle.marker {{
background: #95a5a6;
border: 3px solid #000;
}}
#map {{
height: 900px;
cursor: crosshair;
}}
.leaflet-popup-content {{
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}}
button {{
padding: 10px 20px;
border: none;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin-bottom: 10px;
}}
button:hover {{
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}}
.btn-reset {{
background: #f44336;
color: white;
}}
.btn-random {{
background: #4CAF50;
color: white;
}}
.stats {{
background: #e3f2fd;
padding: 10px;
border-radius: 5px;
font-size: 0.9em;
text-align: center;
margin-top: 15px;
color: #1565c0;
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🗺 Classification k-NN Interactive</h1>
<p>Haute-Corse ou Corse du Sud ?</p>
<div class="stats">
<strong>{len(villages)}</strong> villages chargés
</div>
</div>
<div class="main-content">
<div class="sidebar">
<div class="instructions">
<h4>📋 Instructions</h4>
<ul>
<li><strong>Cliquez</strong> n'importe où sur la carte</li>
<li>Ajustez la valeur de <strong>k</strong> avec le curseur</li>
<li>Observez les <strong>k plus proches villages</strong></li>
<li>La <strong>classification</strong> se fait par vote majoritaire</li>
</ul>
</div>
<div class="controls">
<h3> Paramètres</h3>
<div class="control-group">
<label>Nombre de voisins (k) :</label>
<div class="slider-container">
<input type="range" id="kSlider" min="1" max="21" value="5" step="2">
<span class="k-value" id="kValue">5</span>
</div>
</div>
<button class="btn-random" onclick="placeRandomPoint()">🎲 Point Aléatoire</button>
<button class="btn-reset" onclick="resetClassification()">🔄 Réinitialiser</button>
</div>
<div class="result-box" id="resultBox">
<h3>🎯 Résultat</h3>
<div class="prediction no-result" id="prediction">
Cliquez sur la carte
</div>
<div class="coords" id="coords"></div>
<div id="votesContainer" style="display: none;">
<div class="votes">
<div class="vote-item">
<div class="vote-count red" id="votes2A">0</div>
<div>Corse du Sud</div>
</div>
<div class="vote-item">
<div class="vote-count blue" id="votes2B">0</div>
<div>Haute-Corse</div>
</div>
</div>
<h4>🏘 Plus proches voisins :</h4>
<div class="neighbors-list" id="neighborsList"></div>
</div>
</div>
<div class="legend">
<h4>Légende</h4>
<div class="legend-item">
<div class="legend-circle red"></div>
<span>Corse du Sud (2A)</span>
</div>
<div class="legend-item">
<div class="legend-circle blue"></div>
<span>Haute-Corse (2B)</span>
</div>
<div class="legend-item">
<div class="legend-circle marker"></div>
<span>Point à classifier</span>
</div>
</div>
</div>
<div id="map"></div>
</div>
</div>
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Données des villages (embarquées depuis le CSV)
const villages = {villages_json};
let map, testMarker, neighborMarkers = [], neighborLines = [];
let currentK = 5;
// Initialiser la carte
function initMap() {{
map = L.map('map').setView([42.15, 9.05], 9);
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
attribution: '© OpenStreetMap contributors'
}}).addTo(map);
// Ajouter tous les villages
villages.forEach(village => {{
const color = village.dept === '2A' ? '#e74c3c' : '#3498db';
L.circleMarker([village.lat, village.lon], {{
radius: 3,
fillColor: color,
color: '#333',
weight: 1,
opacity: 1,
fillOpacity: 0.5
}}).bindPopup(`<b>${{village.name}}</b><br>${{village.nameCorse}}<br>${{village.dept}}`)
.addTo(map);
}});
// Ajouter le gestionnaire de clic
map.on('click', onMapClick);
}}
// Calcul de distance Haversine
function haversineDistance(lat1, lon1, lat2, lon2) {{
const R = 6371; // Rayon de la Terre en km
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}}
// Classification k-NN
function knnClassify(lat, lon, k) {{
// Calculer distances
const distances = villages.map(village => ({{
...village,
distance: haversineDistance(lat, lon, village.lat, village.lon)
}}));
// Trier par distance
distances.sort((a, b) => a.distance - b.distance);
// Prendre les k plus proches
const neighbors = distances.slice(0, k);
// Voter
const votes = {{ '2A': 0, '2B': 0 }};
neighbors.forEach(n => votes[n.dept]++);
const prediction = votes['2A'] > votes['2B'] ? '2A' : '2B';
return {{ prediction, neighbors, votes }};
}}
// Gestionnaire de clic sur la carte
function onMapClick(e) {{
const lat = e.latlng.lat;
const lon = e.latlng.lng;
classifyPoint(lat, lon);
}}
// Classifier un point
function classifyPoint(lat, lon) {{
// Supprimer les anciens marqueurs
if (testMarker) map.removeLayer(testMarker);
neighborMarkers.forEach(m => map.removeLayer(m));
neighborLines.forEach(l => map.removeLayer(l));
neighborMarkers = [];
neighborLines = [];
// Classifier
const result = knnClassify(lat, lon, currentK);
// Ajouter le marqueur du point test
testMarker = L.circleMarker([lat, lon], {{
radius: 12,
fillColor: '#95a5a6',
color: '#000',
weight: 3,
opacity: 1,
fillOpacity: 0.8
}}).addTo(map);
// Ajouter les marqueurs et lignes des voisins
result.neighbors.forEach(neighbor => {{
const nColor = neighbor.dept === '2A' ? '#e74c3c' : '#3498db';
// Ligne
const line = L.polyline(
[[lat, lon], [neighbor.lat, neighbor.lon]],
{{color: nColor, weight: 2, opacity: 0.5}}
).addTo(map);
neighborLines.push(line);
// Marqueur
const marker = L.circleMarker([neighbor.lat, neighbor.lon], {{
radius: 8,
fillColor: nColor,
color: '#333',
weight: 2,
opacity: 1,
fillOpacity: 0.8
}}).bindPopup(`<b>${{neighbor.name}}</b><br>${{neighbor.nameCorse}}<br>${{neighbor.dept}}<br>Distance: ${{neighbor.distance.toFixed(2)}} km`)
.addTo(map);
neighborMarkers.push(marker);
}});
// Afficher les résultats
displayResults(lat, lon, result);
}}
// Afficher les résultats
function displayResults(lat, lon, result) {{
const predictionDiv = document.getElementById('prediction');
const coordsDiv = document.getElementById('coords');
const votesContainer = document.getElementById('votesContainer');
const votes2A = document.getElementById('votes2A');
const votes2B = document.getElementById('votes2B');
const neighborsList = document.getElementById('neighborsList');
// Coordonnées
coordsDiv.textContent = `(${{lat.toFixed(4)}}, ${{lon.toFixed(4)}})`;
// Prédiction
const deptName = result.prediction === '2A' ? 'Corse du Sud (2A)' : 'Haute-Corse (2B)';
const cssClass = result.prediction === '2A' ? 'corse-sud' : 'haute-corse';
predictionDiv.textContent = deptName;
predictionDiv.className = 'prediction ' + cssClass;
// Votes
votes2A.textContent = result.votes['2A'];
votes2B.textContent = result.votes['2B'];
votesContainer.style.display = 'block';
// Liste des voisins
neighborsList.innerHTML = '';
result.neighbors.forEach(neighbor => {{
const div = document.createElement('div');
div.className = `neighbor-item dept-${{neighbor.dept.toLowerCase()}}`;
div.innerHTML = `
<div class="neighbor-name">${{neighbor.name}} (${{neighbor.dept}})</div>
<div class="neighbor-corse">${{neighbor.nameCorse}}</div>
<div class="neighbor-distance">Distance: ${{neighbor.distance.toFixed(2)}} km</div>
`;
neighborsList.appendChild(div);
}});
}}
// Point aléatoire
function placeRandomPoint() {{
const lat = 41.3 + Math.random() * (43.0 - 41.3);
const lon = 8.5 + Math.random() * (9.6 - 8.5);
classifyPoint(lat, lon);
map.setView([lat, lon], 10);
}}
// Réinitialiser
function resetClassification() {{
if (testMarker) map.removeLayer(testMarker);
neighborMarkers.forEach(m => map.removeLayer(m));
neighborLines.forEach(l => map.removeLayer(l));
neighborMarkers = [];
neighborLines = [];
testMarker = null;
document.getElementById('prediction').textContent = 'Cliquez sur la carte';
document.getElementById('prediction').className = 'prediction no-result';
document.getElementById('coords').textContent = '';
document.getElementById('votesContainer').style.display = 'none';
map.setView([42.15, 9.05], 9);
}}
// Gestionnaire du slider k
document.getElementById('kSlider').addEventListener('input', function(e) {{
currentK = parseInt(e.target.value);
document.getElementById('kValue').textContent = currentK;
// Reclassifier si un point existe
if (testMarker) {{
const latlng = testMarker.getLatLng();
classifyPoint(latlng.lat, latlng.lng);
}}
}});
// Initialisation
initMap();
</script>
</body>
</html>'''
with open(output_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"✅ Fichier HTML généré : {output_file}")
print(f"📊 {len(villages)} villages inclus")
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Usage: python generate_interactive_map.py villages_corse.csv")
sys.exit(1)
csv_file = sys.argv[1]
villages = load_villages_from_csv(csv_file)
generate_html(villages)