Files
RoadTripsGenerator/Scripts/genere_carte.py

239 lines
15 KiB
Python

#!/usr/bin/env python3
import pandas as pd
import folium
from folium import plugins
from folium.plugins import MarkerCluster
import os
import gpxpy
import re
import sys
import markdown2
import numpy as np
# --- 1. CONFIGURATION ---
base_dir = sys.argv[1] if len(sys.argv) > 1 else "./"
base_dir = os.path.abspath(base_dir)
routes_dir = os.path.join(base_dir, "routes")
html_output_dir = os.path.join(base_dir, "html")
csv_videos = os.path.join(base_dir, "export_videos.csv")
csv_photos = os.path.join(base_dir, "export_photos.csv")
file_notes = os.path.join(base_dir, "voyage.md")
if not os.path.exists(html_output_dir):
os.makedirs(html_output_dir)
COULEURS_JOURS = ['#FF5733', '#2ECC71', '#3498DB', '#9B59B6', '#F1C40F', '#E67E22', '#1ABC9C', '#34495E']
noms_colonnes = ['Fichier', 'Date_Heure', 'Latitude', 'Longitude', 'Altitude', 'Vitesse']
# --- FONCTIONS UTILES ---
def nettoyer_et_trier(df):
if df is None or df.empty:
return pd.DataFrame(columns=noms_colonnes + ['DT', 'Jour'])
df = df[(df['Latitude'].notnull()) & (df['Longitude'].notnull())]
df = df[(df['Latitude'] != 0) & (df['Longitude'] != 0)]
df['DT'] = pd.to_datetime(df['Date_Heure'].str.split('.').str[0], format='%Y:%m:%d %H:%M:%S', errors='coerce')
df = df.dropna(subset=['DT'])
df['Jour'] = df['DT'].dt.date
return df.sort_values(by='DT')
def create_pin(color, icon_name):
html_code = f"""<div style="position: relative; height: 40px; width: 24px;">
<div style="background-color: {color}; width: 24px; height: 24px; border-radius: 50%; border: 2px solid white;
display: flex; align-items: center; justify-content: center; color: white; font-size: 12px;
position: absolute; top: 0; z-index: 2; box-shadow: 0 2px 4px rgba(0,0,0,0.4);">
<i class="fa fa-{icon_name}"></i>
</div>
<div style="width: 3px; height: 18px; background-color: {color}; position: absolute; top: 22px; left: 10.5px; z-index: 1;"></div>
</div>"""
return folium.DivIcon(html=html_code, icon_anchor=(12, 40))
def inject_common_assets(m):
custom_js = """
<script>
document.addEventListener('click', function (e) {
setTimeout(function() {
var videos = document.querySelectorAll('video');
videos.forEach(function(video) {
if (video.hasAttribute('autoplay')) {
video.play().catch(function(error) { console.log("Autoplay bloqué"); });
}
});
}, 300);
}, true);
function toggleJournal(open) {
var panel = document.getElementById('journal-panel');
if (open) {
panel.classList.add('open');
localStorage.setItem('journalOpen', 'true');
} else {
panel.classList.remove('open');
localStorage.setItem('journalOpen', 'false');
}
}
// Persistance du journal sur toutes les pages
window.addEventListener('DOMContentLoaded', function() {
if (localStorage.getItem('journalOpen') === 'true') {
var panel = document.getElementById('journal-panel');
if (panel) panel.classList.add('open');
}
});
function moveSlide(btn, step) {
var container = btn.parentElement.querySelector('.slides');
var slides = container.querySelectorAll('.slide');
var activeIndex = Array.from(slides).findIndex(s => s.style.display !== 'none');
if (activeIndex === -1) activeIndex = 0;
slides[activeIndex].style.display = 'none';
var nextIndex = (activeIndex + step + slides.length) % slides.length;
slides[nextIndex].style.display = 'block';
btn.parentElement.querySelector('.slide-counter').innerText = (nextIndex + 1) + '/' + slides.length;
}
</script>
"""
custom_css = """
<style>
.leaflet-top.leaflet-left { top: auto !important; bottom: 20px !important; left: 15px !important; }
.leaflet-popup-content-wrapper { border-radius: 12px; max-width: 95vw !important; }
#journal-panel {
position: fixed; top: 0; right: -100%; width: 400px; height: 100%;
background: white; z-index: 10005; transition: 0.4s;
box-shadow: -5px 0 15px rgba(0,0,0,0.2); padding: 25px;
overflow-y: auto; font-family: sans-serif;
}
#journal-panel.open { right: 0; }
@media (max-width: 600px) { #journal-panel { width: 85%; } }
.close-btn { position: absolute; top: 15px; right: 15px; font-size: 24px; cursor: pointer; border: none; background: none; }
.file-header { background: #f0f0f0; padding: 4px 8px; border-radius: 4px; margin-bottom: 8px; font-size: 11px; font-family: monospace; display: block; overflow: hidden; text-overflow: ellipsis; }
.slider-container { position: relative; width: calc(85vw - 60px); max-width: 800px; text-align: center; min-height: 200px; }
.slide img { max-width: 100%; max-height: 65vh; border-radius: 8px; display: block; margin: auto; }
.nav-btn { position: absolute; top: 50%; transform: translateY(-50%); background: rgba(0,0,0,0.6); color: white; border: 2px solid white; padding: 12px; cursor: pointer; border-radius: 50%; z-index: 10; font-size: 18px; }
.prev { left: -10px; } .next { right: -10px; }
.slide-counter { margin-top: 8px; font-size: 13px; font-weight: bold; color: #333; font-family: sans-serif; }
</style>
"""
m.get_root().header.add_child(folium.Element(custom_css + custom_js))
def get_journal_ui():
journal_html = ""
if os.path.exists(file_notes):
with open(file_notes, 'r', encoding='utf-8') as f:
journal_html = markdown2.markdown(f.read())
return f"""
<div id="journal-panel"><button class="close-btn" onclick="toggleJournal(false)">&times;</button><div class="journal-content">{journal_html}</div></div>
<div style="position: fixed; bottom: 25px; right: 15px; z-index: 10000; display: flex; flex-direction: column; align-items: flex-end; gap: 10px;">
<button onclick="toggleJournal(true)" style="background: #FFD700; border: 2px solid white; padding: 12px 20px; border-radius: 50px; font-weight: bold; font-size: 14px; cursor: pointer; box-shadow: 0 4px 15px rgba(0,0,0,0.3); font-family:sans-serif;">📖 Journal</button>
</div>
"""
# --- 2. CHARGEMENT ---
try:
df_v = pd.read_csv(csv_videos, names=noms_colonnes, header=None) if os.path.exists(csv_videos) else pd.DataFrame()
df_p = pd.read_csv(csv_photos, names=noms_colonnes, header=None) if os.path.exists(csv_photos) else pd.DataFrame()
df_v, df_p = nettoyer_et_trier(df_v), nettoyer_et_trier(df_p)
jours_gpx = set()
if os.path.exists(routes_dir):
for f in os.listdir(routes_dir):
match = re.search(r'(\d{4}-\d{2}-\d{2})', f)
if match: jours_gpx.add(pd.to_datetime(match.group(1)).date())
tous_les_jours = sorted(list(set(df_v['Jour'].unique()) | set(df_p['Jour'].unique()) | jours_gpx))
except Exception as e:
print(f"❌ Erreur : {e}"); sys.exit()
# --- 3. GÉNÉRATION ---
m_global = folium.Map(tiles=None)
folium.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite').add_to(m_global)
folium.TileLayer('OpenStreetMap', name='Plan').add_to(m_global)
inject_common_assets(m_global)
m_mini = folium.Map(tiles='OpenStreetMap', zoom_control=False, control_scale=False, attribution_control=False)
all_global_coords, sidebar_links_html = [], ""
for i, jour in enumerate(tous_les_jours):
day_str = jour.strftime('%Y-%m-%d')
file_name = f'carte_{day_str}.html'
color_day = COULEURS_JOURS[i % len(COULEURS_JOURS)]
sidebar_links_html += f'<a href="html/{file_name}" target="_top" style="display:block; margin-bottom:8px; padding:12px; background:{color_day}; color:white; text-decoration:none; border-radius:8px; font-weight:bold; text-align:center; font-family:sans-serif;">{day_str}</a>'
m_day = folium.Map(tiles=None)
folium.TileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', attr='Esri', name='Satellite').add_to(m_day)
folium.TileLayer('OpenStreetMap', name='Plan').add_to(m_day)
inject_common_assets(m_day)
marker_cluster = MarkerCluster(name="Médias").add_to(m_day)
day_coords = []
current_gpx_filename = None
if os.path.exists(routes_dir):
for gpx_file in os.listdir(routes_dir):
if gpx_file.startswith(day_str) and gpx_file.endswith('.gpx'):
current_gpx_filename = gpx_file
try:
with open(os.path.join(routes_dir, gpx_file), 'r') as f:
gpx = gpxpy.parse(f)
for track in gpx.tracks:
pts = []
for segment in track.segments:
# OPTIMISATION : On ne garde qu'un point sur 5
# pts.extend([[p.latitude, p.longitude] for i, p in enumerate(segment.points) if i % 5 == 0])
pts.extend([[p.latitude, p.longitude] for p in segment.points])
if pts:
day_coords.extend(pts); all_global_coords.extend(pts)
popup_html = f"<div style='text-align:center; font-family:sans-serif;'><b>📅 {day_str}</b><br><br><a href='html/{file_name}' target='_top' style='background:{color_day}; color:white; padding:8px 12px; border-radius:4px; text-decoration:none; font-weight:bold;'>Détails</a></div>"
folium.PolyLine(pts, color=color_day, weight=7, opacity=0.8, popup=folium.Popup(popup_html, max_width=200)).add_to(m_global)
folium.PolyLine(pts, color=color_day, weight=3, opacity=0.8).add_to(m_mini)
folium.PolyLine(pts, color="#FF0000", weight=4, opacity=0.8).add_to(m_day)
except: pass
m_day.get_root().html.add_child(folium.Element('<div style="position: fixed; top: 15px; left: 15px; z-index: 10001;"><a href="../index.html" target="_top" style="text-decoration: none; background: #333; color: white; padding: 12px 20px; border-radius: 10px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 10px rgba(0,0,0,0.5); border: 2px solid white; display: block;">🏠 Accueil</a></div>' + get_journal_ui()))
if current_gpx_filename:
m_day.get_root().html.add_child(folium.Element(f'<div style="position: fixed; bottom: 85px; right: 15px; z-index: 10001;"><a href="../routes/{current_gpx_filename}" download style="text-decoration: none; background: #28a745; color: white; padding: 12px 20px; border-radius: 50px; font-family: sans-serif; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border: 2px solid white; display: block;">📥 GPX</a></div>'))
# REGROUPEMENT PHOTOS
day_p = df_p[df_p['Jour'] == jour].copy()
if not day_p.empty:
tol = 0.0002
while not day_p.empty:
ref = day_p.iloc[0]
masque = (np.abs(day_p['Latitude'] - ref['Latitude']) < tol) & (np.abs(day_p['Longitude'] - ref['Longitude']) < tol)
groupe = day_p[masque]
fichiers = groupe['Fichier'].tolist()
nb = len(fichiers)
# slides = "".join([f'<div class="slide" style="display:{"block" if idx == 0 else "none"};"><div class="file-header">{p.strip()}</div><a href="../photos/{p.strip()}" target="_blank"><img src="../photos/{p.strip()}"></a></div>' for idx, p in enumerate(fichiers)])
# OPTIMISATION : Ajout de loading="lazy" dans la balise img
slides = "".join([f'<div class="slide" style="display:{"block" if idx == 0 else "none"};"><div class="file-header">{p.strip()}</div><a href="../photos/{p.strip()}" target="_blank"><img src="../photos/{p.strip()}" loading="lazy"></a></div>' for idx, p in enumerate(fichiers)])
btns = f'<button class="nav-btn prev" onclick="moveSlide(this, -1)">&#10094;</button><button class="nav-btn next" onclick="moveSlide(this, 1)">&#10095;</button><div class="slide-counter">1/{nb}</div>' if nb > 1 else ""
folium.Marker(location=[ref['Latitude'], ref['Longitude']], popup=folium.Popup(f'<div class="slider-container"><div class="slides">{slides}</div>{btns}</div>', max_width="100%"), icon=create_pin("#FF3B30", "camera" if nb==1 else "images")).add_to(marker_cluster)
day_p = day_p[~masque]
# Vidéos
day_v = df_v[df_v['Jour'] == jour]
for v_name, group in day_v.groupby('Fichier'):
pts_v = group[['Latitude', 'Longitude']].values.tolist()
# v_pop = f'<div style="width:calc(85vw - 40px); max-width:1000px; text-align:center;"><div class="file-header">{v_name.strip()}</div><video style="width:100%; max-height:70vh; border-radius:8px; background:black;" controls playsinline webkit-playsinline autoplay muted><source src="../videos/{v_name.strip()}" type="video/mp4"></video></div>'
v_pop = f'<div style="width:calc(85vw - 40px); max-width:1000px; text-align:center;"><div class="file-header">{v_name.strip()}</div><video style="width:100%; max-height:70vh; border-radius:8px; background:black;" controls playsinline webkit-playsinline autoplay muted preload="auto"><source src="../videos/{v_name.strip()}" type="video/mp4"></video></div>'
folium.Marker(location=pts_v[0], popup=folium.Popup(v_pop, max_width="100%"), icon=create_pin("#007AFF", "play")).add_to(marker_cluster)
if day_coords: m_day.fit_bounds(day_coords, padding=(50, 50))
folium.LayerControl(position='topright').add_to(m_day)
plugins.Fullscreen(position='topright').add_to(m_day)
m_day.save(os.path.join(html_output_dir, file_name))
# --- FINALISATION ---
if all_global_coords:
m_global.fit_bounds(all_global_coords, padding=(50, 50))
m_mini.fit_bounds(all_global_coords, padding=(20, 20))
sidebar_html = f'<div style="position: fixed; top: 15px; left: 15px; width: 160px; max-height: 85vh; overflow-y: auto; background: rgba(255,255,255,0.9); z-index: 9999; padding: 12px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.2);"><a href="../" target="_top" style="display:block; margin-bottom:15px; padding:12px; background:#444; color:white; text-decoration:none; border-radius:8px; font-weight:bold; text-align:center; border: 2px solid #666; font-family:sans-serif;">⬅ Retour</a><div style="border-top: 1px solid #ccc; margin-bottom: 10px; padding-top: 10px; font-family:sans-serif; font-size:12px; font-weight:bold; color:#666; text-align:center;">PAR JOURS</div>{sidebar_links_html}</div>'
m_global.get_root().html.add_child(folium.Element(sidebar_html + get_journal_ui()))
folium.LayerControl().add_to(m_global)
m_global.save(os.path.join(base_dir, "index.html"))
m_mini.save(os.path.join(base_dir, "mini.html"))