Auto-update: 2025-10-23 15:04:00
This commit is contained in:
parent
7698240a79
commit
6170c48371
28 changed files with 19938 additions and 0 deletions
693
activite1/generate_interactive_map.py
Normal file
693
activite1/generate_interactive_map.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue