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
283 lines
11 KiB
JavaScript
283 lines
11 KiB
JavaScript
// Onglet « Carte et corrections » : déplacer ou placer les photos et vidéos,
|
|
// masquer des zones de trace. Chaque correction est enregistrée aussitôt dans
|
|
// voyage.toml (ou site.toml pour une zone privée) ; les pages du site la
|
|
// prennent en compte à la génération suivante.
|
|
(function () {
|
|
"use strict";
|
|
const racine = document.getElementById("editeur");
|
|
if (!racine || !window.L) return;
|
|
const voyage = racine.dataset.voyage;
|
|
const $ = id => document.getElementById(id);
|
|
let carte = null, donnees = null, coucheTraces, coucheMedias, coucheZones;
|
|
let brouillon = null, modeZone = false, aPlacer = null, cadree = false;
|
|
|
|
// --- Échanges avec le serveur -------------------------------------------
|
|
function message(html) {
|
|
const m = $("messages");
|
|
m.innerHTML = html;
|
|
clearTimeout(window._rtgMessage);
|
|
window._rtgMessage = setTimeout(() => { m.innerHTML = ""; }, 6000);
|
|
}
|
|
|
|
function lireReponse(r) {
|
|
const redirection = r.headers.get("HX-Redirect");
|
|
if (redirection) { location.href = redirection; throw new Error("session"); }
|
|
if (!(r.headers.get("Content-Type") || "").includes("json")) {
|
|
return r.text().then(t => { message(t); throw new Error("refus"); });
|
|
}
|
|
return r.json();
|
|
}
|
|
|
|
function poster(url, params) {
|
|
return fetch(url, {
|
|
method: "POST",
|
|
headers: { "HX-Request": "true", "Content-Type": "application/x-www-form-urlencoded" },
|
|
body: new URLSearchParams(Object.assign({ v: voyage }, params))
|
|
}).then(lireReponse);
|
|
}
|
|
|
|
function etat(texte) {
|
|
$("ed-etat").textContent = texte;
|
|
}
|
|
|
|
function charger() {
|
|
return fetch("/voyage/carte.json?v=" + encodeURIComponent(voyage), { headers: { "HX-Request": "true" } })
|
|
.then(lireReponse)
|
|
.then(d => { donnees = d; remplirJours(); dessinerTout(); });
|
|
}
|
|
|
|
// --- Carte ----------------------------------------------------------------
|
|
function initialiser() {
|
|
$("ed-carte").innerHTML = "";
|
|
carte = L.map("ed-carte");
|
|
const plan = L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
|
{ maxZoom: 19, attribution: "© OpenStreetMap" }).addTo(carte);
|
|
const satellite = L.tileLayer(
|
|
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
|
{ maxZoom: 19, attribution: "Esri World Imagery" });
|
|
L.control.layers({ "Plan": plan, "Satellite": satellite }).addTo(carte);
|
|
carte.setView([46.5, 2.5], 5);
|
|
coucheTraces = L.featureGroup().addTo(carte);
|
|
coucheZones = L.featureGroup().addTo(carte);
|
|
coucheMedias = L.featureGroup().addTo(carte);
|
|
carte.on("click", clicCarte);
|
|
$("ed-jour").addEventListener("change", () => { cadree = false; dessinerTout(); });
|
|
$("ed-photos").addEventListener("change", dessinerMedias);
|
|
$("ed-videos").addEventListener("change", dessinerMedias);
|
|
$("ed-nouvelle-zone").addEventListener("click", commencerZone);
|
|
$("ed-zone-annuler").addEventListener("click", finirZone);
|
|
$("ed-zone-ok").addEventListener("click", enregistrerZone);
|
|
$("ed-rayon").addEventListener("input", () => {
|
|
$("ed-rayon-texte").textContent = $("ed-rayon").value + " m";
|
|
if (brouillon) brouillon.setRadius(+$("ed-rayon").value);
|
|
});
|
|
document.addEventListener("keydown", e => {
|
|
if (e.key === "Escape") { aPlacer = null; finirZone(); mode(""); }
|
|
});
|
|
document.body.addEventListener("tache-finie", () => charger());
|
|
charger();
|
|
}
|
|
|
|
function remplirJours() {
|
|
const choix = $("ed-jour"), actuel = choix.value;
|
|
choix.length = 1;
|
|
donnees.jours.forEach(j => choix.add(new Option(j.nom, j.jour)));
|
|
choix.value = actuel;
|
|
}
|
|
|
|
function jourChoisi() { return $("ed-jour").value; }
|
|
|
|
function dessinerTout() {
|
|
dessinerTraces();
|
|
dessinerZones();
|
|
dessinerMedias();
|
|
if (!cadree) {
|
|
const b = coucheTraces.getLayers().length ? coucheTraces.getBounds()
|
|
: (coucheMedias.getLayers().length ? coucheMedias.getBounds() : null);
|
|
if (b && b.isValid()) carte.fitBounds(b, { padding: [30, 30] });
|
|
cadree = true;
|
|
}
|
|
}
|
|
|
|
function dessinerTraces() {
|
|
coucheTraces.clearLayers();
|
|
const jour = jourChoisi();
|
|
donnees.couches.forEach(c => {
|
|
if (jour && c.jour !== jour) return;
|
|
c.segments.forEach(s => L.polyline(s, { color: c.couleur, weight: 4, opacity: 0.85, interactive: false })
|
|
.addTo(coucheTraces));
|
|
});
|
|
}
|
|
|
|
function icone(m) {
|
|
return L.divIcon({ className: "", iconSize: [16, 16], iconAnchor: [8, 8],
|
|
html: '<span class="mk ' + m.type + (m.deplace ? " deplace" : "") + '"></span>' });
|
|
}
|
|
|
|
function popup(m) {
|
|
const d = document.createElement("div");
|
|
d.className = "ed-popup";
|
|
if (m.vignette) {
|
|
const img = document.createElement("img");
|
|
img.src = m.vignette; img.alt = ""; img.loading = "lazy";
|
|
d.append(img);
|
|
}
|
|
const t = document.createElement("div");
|
|
t.textContent = m.fichier + " · " + m.jour + " " + m.heure;
|
|
d.append(t);
|
|
if (m.deplace) {
|
|
const b = document.createElement("button");
|
|
b.className = "petit";
|
|
b.textContent = "Revenir à la position d'origine";
|
|
b.addEventListener("click", () => { carte.closePopup(); enregistrerPosition(m.fichier, "", ""); });
|
|
d.append(b);
|
|
}
|
|
return d;
|
|
}
|
|
|
|
function dessinerMedias() {
|
|
coucheMedias.clearLayers();
|
|
const jour = jourChoisi(), photos = $("ed-photos").checked, videos = $("ed-videos").checked;
|
|
const sans = [];
|
|
donnees.medias.forEach(m => {
|
|
if (jour && m.jour !== jour) return;
|
|
if ((m.type === "photo" && !photos) || (m.type === "video" && !videos)) return;
|
|
if (m.lat === null) { sans.push(m); return; }
|
|
L.marker([m.lat, m.lon], { icon: icone(m), draggable: true, title: m.fichier })
|
|
.bindPopup(() => popup(m), { minWidth: 200 })
|
|
.on("dragend", e => {
|
|
const p = e.target.getLatLng();
|
|
enregistrerPosition(m.fichier, p.lat.toFixed(6), p.lng.toFixed(6));
|
|
})
|
|
.addTo(coucheMedias);
|
|
});
|
|
const liste = $("ed-sans");
|
|
liste.innerHTML = "";
|
|
$("ed-nb-sans").textContent = sans.length;
|
|
sans.forEach(m => {
|
|
const li = document.createElement("li");
|
|
const a = document.createElement("a");
|
|
a.href = "#carte";
|
|
a.textContent = (m.type === "photo" ? "📷 " : "🎬 ") + m.fichier;
|
|
a.title = m.jour + " " + m.heure;
|
|
a.addEventListener("click", e => {
|
|
e.preventDefault();
|
|
finirZone();
|
|
aPlacer = m.fichier;
|
|
mode("Cliquez sur la carte pour placer " + m.fichier + " (Échap pour annuler)");
|
|
});
|
|
const quand = document.createElement("span");
|
|
quand.className = "discret";
|
|
quand.textContent = " " + m.jour + " " + m.heure;
|
|
li.append(a, quand);
|
|
liste.append(li);
|
|
});
|
|
if (!sans.length) liste.innerHTML = '<li class="vide">Aucun</li>';
|
|
}
|
|
|
|
function enregistrerPosition(fichier, lat, lon) {
|
|
poster("/voyage/position", { fichier: fichier, lat: lat, lon: lon }).then(r => {
|
|
if (r.media) {
|
|
const i = donnees.medias.findIndex(m => m.fichier === fichier);
|
|
if (i >= 0) donnees.medias[i] = r.media;
|
|
}
|
|
dessinerMedias();
|
|
etat("✓ " + fichier + (lat ? " placé" : " remis à sa position d'origine")
|
|
+ ". Régénérez le voyage pour mettre le site à jour.");
|
|
}).catch(() => dessinerMedias());
|
|
}
|
|
|
|
// --- Zones masquées -------------------------------------------------------
|
|
function dessinerZones() {
|
|
coucheZones.clearLayers();
|
|
const liste = $("ed-zones");
|
|
liste.innerHTML = "";
|
|
const ajouter = (z, i, portee) => {
|
|
const site = portee === "site";
|
|
const cercle = L.circle([z[0], z[1]], {
|
|
radius: z[2], color: site ? "#7c3aed" : "#dc2626", weight: 2, fillOpacity: 0.15,
|
|
dashArray: site ? "6 6" : null
|
|
}).addTo(coucheZones);
|
|
const li = document.createElement("li");
|
|
const a = document.createElement("a");
|
|
a.href = "#carte";
|
|
a.textContent = (site ? "Zone privée" : "Zone") + " · " + z[2] + " m";
|
|
a.addEventListener("click", e => { e.preventDefault(); carte.fitBounds(cercle.getBounds(), { maxZoom: 16 }); });
|
|
const b = document.createElement("button");
|
|
b.className = "petit";
|
|
b.textContent = "Supprimer";
|
|
b.addEventListener("click", () => {
|
|
poster("/voyage/masque", { action: "supprimer", portee: portee, index: i }).then(r => {
|
|
donnees.masques = r.masques; donnees.zones = r.zones; dessinerZones();
|
|
etat("✓ Zone supprimée. Régénérez " + (site ? "les voyages" : "le voyage") + " pour mettre le site à jour.");
|
|
});
|
|
});
|
|
li.append(a, " ", b);
|
|
if (site) li.title = "Retirée de tous les voyages (site.toml)";
|
|
liste.append(li);
|
|
};
|
|
donnees.masques.forEach((z, i) => ajouter(z, i, "voyage"));
|
|
donnees.zones.forEach((z, i) => ajouter(z, i, "site"));
|
|
}
|
|
|
|
function commencerZone() {
|
|
aPlacer = null;
|
|
modeZone = true;
|
|
mode("Cliquez au centre de la zone à masquer ; un nouveau clic la déplace.");
|
|
}
|
|
|
|
function finirZone() {
|
|
modeZone = false;
|
|
if (brouillon) { brouillon.remove(); brouillon = null; }
|
|
$("ed-brouillon").hidden = true;
|
|
if (!aPlacer) mode("");
|
|
}
|
|
|
|
function enregistrerZone() {
|
|
if (!brouillon) return;
|
|
const c = brouillon.getLatLng(), site = $("ed-portee-site").checked;
|
|
poster("/voyage/masque", {
|
|
action: "ajouter", portee: site ? "site" : "voyage",
|
|
lat: c.lat.toFixed(6), lon: c.lng.toFixed(6), rayon: $("ed-rayon").value
|
|
}).then(r => {
|
|
donnees.masques = r.masques; donnees.zones = r.zones;
|
|
finirZone();
|
|
dessinerZones();
|
|
etat("✓ Zone enregistrée. Régénérez " + (site ? "les voyages" : "le voyage") + " pour mettre le site à jour.");
|
|
});
|
|
}
|
|
|
|
function clicCarte(e) {
|
|
if (aPlacer) {
|
|
const nom = aPlacer;
|
|
aPlacer = null;
|
|
mode("");
|
|
enregistrerPosition(nom, e.latlng.lat.toFixed(6), e.latlng.lng.toFixed(6));
|
|
} else if (modeZone) {
|
|
if (!brouillon) {
|
|
brouillon = L.circle(e.latlng, { radius: +$("ed-rayon").value, color: "#dc2626", weight: 2,
|
|
dashArray: "4 4", fillOpacity: 0.2 }).addTo(carte);
|
|
} else {
|
|
brouillon.setLatLng(e.latlng);
|
|
}
|
|
$("ed-brouillon").hidden = false;
|
|
mode("Ajustez le rayon puis enregistrez ; un clic sur la carte déplace la zone.");
|
|
}
|
|
}
|
|
|
|
function mode(texte) {
|
|
const m = $("ed-mode");
|
|
m.textContent = texte;
|
|
m.hidden = !texte;
|
|
$("ed-carte").classList.toggle("en-mode", !!texte);
|
|
}
|
|
|
|
// L'onglet peut être caché au chargement : on prépare la carte à sa première ouverture.
|
|
document.addEventListener("rtg:onglet", e => {
|
|
if (e.detail !== "carte") return;
|
|
if (!carte) initialiser();
|
|
else carte.invalidateSize();
|
|
});
|
|
if (location.hash === "#carte") setTimeout(() => { if (!carte) initialiser(); }, 0);
|
|
})();
|