Files
RoadTripsGenerator/rtg/interface/static/editeur.js
T
FabriceandClaude Opus 5.5 4366db94ed Administration depuis le site : bouton Connexion sur l'accueil
site.toml [administration] adresse : l'accueil affiche un bouton
« Connexion » vers l'interface. Celle-ci peut tourner derrière le serveur
web du site (rtg interface --proxy --prefixe /admin) : adresses
préfixées, mot de passe obligatoire, visiteur identifié par
X-Forwarded-For, cookie Secure limité au préfixe quand le site est en
HTTPS. Exemples nginx et Apache dans le README. Le bouton Arrêter ne
remplace plus le bloc de la tâche en même temps que la relève.

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

284 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 base = document.body.dataset.base || ""; // « /admin » derrière le serveur web
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(base + 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(base + "/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);
})();