Files
FabriceandClaude Opus 5.5 71779f6f53 Interface : corrections à la souris et accès protégé
Onglet « Carte et corrections » : glisser une photo ou une vidéo, placer
les médias sans position, masquer des zones de trace (pour un voyage, ou
zone privée pour tous). Corrections notées dans voyage.toml ([positions],
[corrections]) ou site.toml ([confidentialite]), appliquées par les pages ;
les originaux ne changent pas. L'extraction garde les médias datés sans
position pour qu'on puisse les placer.

Accès : clé d'accès aléatoire à chaque lancement, session par cookie
HttpOnly/SameSite=Strict, mot de passe facultatif (rtg mot-de-passe,
empreinte scrypt hors du site, blocage après 5 échecs), accès réseau
seulement avec mot de passe (--distant), HTTPS possible (--certificat,
--cle), Content-Security-Policy et interdiction des cadres.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mf2kATMtqVABHSYdxJMnim
2026-10-09 14:35:43 +00:00

108 lines
3.7 KiB
Python

"""Corrections faites à la main (interface, onglet « Carte ») et appliquées à
la génération des pages ; les fichiers d'origine ne sont jamais modifiés.
voyage.toml
[positions] média déplacé ou placé à la main
"IMG_4831.jpeg" = [48.85, 2.35]
[corrections]
masques = [[48.85, 2.35, 300]] zones (centre, rayon en m) retirées des traces
site.toml
[confidentialite]
zones = [[48.80, 2.10, 500]] zones retirées des traces de tous les voyages
(autour de chez soi, par exemple)
"""
import csv
import math
import os
import gpxpy.gpx
def masques(cfg):
"""[(lat, lon, rayon_m)] : zones du voyage et zones privées du site."""
res = []
for liste in (cfg.get("corrections", {}).get("masques", []),
cfg.get("confidentialite", {}).get("zones", [])):
for m in liste:
try:
res.append((float(m[0]), float(m[1]), float(m[2])))
except (TypeError, ValueError, IndexError):
pass
return res
def positions(cfg):
"""{nom de fichier: (lat, lon)} des médias placés à la main."""
res = {}
for nom, pos in cfg.get("positions", {}).items():
try:
res[nom] = (float(pos[0]), float(pos[1]))
except (TypeError, ValueError, IndexError):
pass
return res
def distance_m(lat1, lon1, lat2, lon2):
x = math.radians(lon2 - lon1) * math.cos(math.radians((lat1 + lat2) / 2))
y = math.radians(lat2 - lat1)
return 6371000 * math.hypot(x, y)
def dans_un_masque(lat, lon, zones):
return any(distance_m(lat, lon, m[0], m[1]) <= m[2] for m in zones)
def filtrer_piste(piste, zones):
"""Retire d'une piste GPX les points situés dans les zones ; un segment
traversé par une zone est coupé en deux. Renvoie le nombre de points retirés."""
if not zones:
return 0
retires = 0
nouveaux = []
for seg in piste.segments:
courant = gpxpy.gpx.GPXTrackSegment()
for p in seg.points:
if dans_un_masque(p.latitude, p.longitude, zones):
retires += 1
if courant.points:
nouveaux.append(courant)
courant = gpxpy.gpx.GPXTrackSegment()
else:
courant.points.append(p)
if courant.points:
nouveaux.append(courant)
piste.segments = [s for s in nouveaux if len(s.points) > 1]
return retires
def lire_medias(voyage, cfg):
"""Photos et vidéos des exports CSV, positions corrigées comprises.
Renvoie une liste de dicts {fichier, type, date, lat, lon, deplace} ; lat et
lon valent None pour un média sans position."""
corr = positions(cfg)
res = []
for nom_csv, type_ in (("export_photos.csv", "photo"), ("export_videos.csv", "video")):
chemin = os.path.join(voyage, nom_csv)
if not os.path.exists(chemin):
continue
vus = set()
with open(chemin, encoding="utf-8", errors="replace") as f:
for ligne in csv.reader(f):
if len(ligne) < 4 or ligne[0] in vus:
continue
vus.add(ligne[0])
try:
lat, lon = float(ligne[2]), float(ligne[3])
if lat == 0 and lon == 0:
lat = lon = None
except ValueError:
lat = lon = None
m = {"fichier": ligne[0], "type": type_, "date": ligne[1], "lat": lat, "lon": lon,
"deplace": ligne[0] in corr}
if m["deplace"]:
m["lat"], m["lon"] = corr[ligne[0]]
res.append(m)
return res