From c9f298223bdc736f2cb8336bca5eb9c793b9c545 Mon Sep 17 00:00:00 2001 From: Markus Zimmerberger Date: Fri, 11 Sep 2026 14:54:40 +0200 Subject: [PATCH] Add route-maps --- static/map.js | 146 +++++++++++++++++++++++++++++++++++++++ templates/add_entry.html | 14 ++++ templates/base.html | 33 ++++++++- templates/dashboard.html | 29 ++++++++ 4 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 static/map.js diff --git a/static/map.js b/static/map.js new file mode 100644 index 0000000..3068474 --- /dev/null +++ b/static/map.js @@ -0,0 +1,146 @@ +/* Shared Leaflet helper: a small OSM route preview (origin/destination + markers + a straight line between them - we don't have real route + geometry, just stop coordinates, so this is a "as the crow flies" hint, + not a turn-by-turn path). + + Exposes window.PbtMap.createRouteMap(containerId) and self-wires: + - the "Von"/"Nach" live preview on the trip form (#route-map) + - the map-pin buttons + dialog on the "Meine Fahrten" list + + No-ops safely wherever these elements/Leaflet aren't present. */ +(function () { + "use strict"; + if (typeof window.L === "undefined") return; + + var ORIGIN_COLOR = "#3fb950"; + var DEST_COLOR = "#f85149"; + var AUSTRIA_CENTER = [47.6, 14.0]; + + function dot(color) { + return L.divIcon({ + className: "", + html: '', + iconSize: [14, 14], + iconAnchor: [7, 7], + }); + } + + function createRouteMap(containerId) { + var map = L.map(containerId).setView(AUSTRIA_CENTER, 7); + L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { + maxZoom: 19, + attribution: '© OpenStreetMap-Mitwirkende', + }).addTo(map); + + var layers = []; + function clear() { + layers.forEach(function (l) { map.removeLayer(l); }); + layers = []; + } + + function render(origin, destination) { + clear(); + var points = []; + if (origin) { + var o = L.marker([origin.lat, origin.lon], { icon: dot(ORIGIN_COLOR) }).addTo(map); + if (origin.label) o.bindTooltip(origin.label); + layers.push(o); + points.push([origin.lat, origin.lon]); + } + if (destination) { + var d = L.marker([destination.lat, destination.lon], { icon: dot(DEST_COLOR) }).addTo(map); + if (destination.label) d.bindTooltip(destination.label); + layers.push(d); + points.push([destination.lat, destination.lon]); + } + if (points.length === 2) { + layers.push( + L.polyline(points, { color: "#4a9eff", weight: 3, dashArray: "6 8", opacity: 0.85 }).addTo(map) + ); + map.fitBounds(L.latLngBounds(points), { padding: [28, 28], maxZoom: 15 }); + } else if (points.length === 1) { + map.setView(points[0], 14); + } else { + map.setView(AUSTRIA_CENTER, 7); + } + return points.length > 0; + } + + return { map: map, render: render }; + } + + window.PbtMap = { createRouteMap: createRouteMap }; + + // --- live preview on the trip form ------------------------------------- + function setupFormPreview(form) { + var container = form.querySelector("#route-map"); + var placeholder = form.querySelector("[data-route-map-placeholder]"); + var originText = form.querySelector("#origin"); + var destText = form.querySelector("#destination"); + var originHidden = form.querySelector("#origin_stop_id"); + var destHidden = form.querySelector("#destination_stop_id"); + if (!container || !originText || !destText || !originHidden || !destHidden) return; + + var routeMap = null; + + function point(hidden, text) { + var lat = hidden.dataset.stopLat, lon = hidden.dataset.stopLon; + if (!lat || !lon) return null; + return { lat: parseFloat(lat), lon: parseFloat(lon), label: text.value.trim() }; + } + + function apply() { + var origin = point(originHidden, originText); + var destination = point(destHidden, destText); + if (!origin && !destination) { + container.hidden = true; + if (placeholder) placeholder.hidden = false; + return; + } + container.hidden = false; + if (placeholder) placeholder.hidden = true; + if (!routeMap) routeMap = createRouteMap("route-map"); + routeMap.render(origin, destination); + setTimeout(function () { routeMap.map.invalidateSize(); }, 0); + } + + form.addEventListener("stopchange", apply); + originText.addEventListener("input", apply); + destText.addEventListener("input", apply); + apply(); + } + + document.querySelectorAll("form").forEach(setupFormPreview); + + // --- "Meine Fahrten" map-pin buttons + shared dialog -------------------- + var dialog = document.getElementById("route-map-dialog"); + if (dialog) { + var dialogTitle = document.getElementById("route-map-dialog-title"); + var closeBtn = dialog.querySelector("[data-close-map-dialog]"); + var dialogMap = null; + + function coordsOf(btn, prefix) { + var lat = btn.dataset[prefix + "Lat"], lon = btn.dataset[prefix + "Lon"]; + if (!lat || !lon) return null; + return { lat: parseFloat(lat), lon: parseFloat(lon), label: btn.dataset[prefix + "Label"] || "" }; + } + + document.querySelectorAll("[data-show-map]").forEach(function (btn) { + btn.addEventListener("click", function () { + var origin = coordsOf(btn, "origin"); + var destination = coordsOf(btn, "destination"); + dialogTitle.textContent = (btn.dataset.originLabel || "?") + " → " + (btn.dataset.destinationLabel || "?"); + dialog.showModal(); + if (!dialogMap) dialogMap = createRouteMap("dialog-route-map"); + dialogMap.render(origin, destination); + setTimeout(function () { dialogMap.map.invalidateSize(); }, 0); + }); + }); + + if (closeBtn) closeBtn.addEventListener("click", function () { dialog.close(); }); + dialog.addEventListener("click", function (e) { + if (e.target === dialog) dialog.close(); // click on the backdrop + }); + } +})(); diff --git a/templates/add_entry.html b/templates/add_entry.html index 771b7cc..15ac8e6 100644 --- a/templates/add_entry.html +++ b/templates/add_entry.html @@ -1,5 +1,9 @@ {% extends "base.html" %} {% block title %}{{ "Fahrt bearbeiten" if fahrt else "Neue Fahrt" }} – PBT{% endblock %} +{% block head %} + +{% endblock %} {% block content %}

{{ "Fahrt bearbeiten" if fahrt else "Neue Fahrt erfassen" }}

@@ -50,6 +54,13 @@ if fahrt and fahrt.destination_stop else "" }}

+
+ +

Wähle bekannte Haltestellen bei Von/Nach für eine Kartenvorschau.

+ +

Luftlinie zwischen den Halten – keine echte Streckenführung.

+
+
{% endblock %} {% block scripts %} + + {% endblock %} diff --git a/templates/base.html b/templates/base.html index 9cf623a..26cca12 100644 --- a/templates/base.html +++ b/templates/base.html @@ -161,9 +161,39 @@ cursor: pointer; } .icon-btn svg { width: 16px; height: 16px; } - .icon-btn.edit:hover { color: var(--accent); border-color: var(--accent); background: rgba(74, 158, 255, 0.1); } + .icon-btn.edit:hover, .icon-btn.map:hover { color: var(--accent); border-color: var(--accent); background: rgba(74, 158, 255, 0.1); } .icon-btn.delete { color: var(--danger); } .icon-btn.delete:hover { background: rgba(248, 81, 73, 0.12); border-color: var(--danger); } + .icon-btn:disabled { opacity: 0.35; cursor: not-allowed; } + .icon-btn:disabled:hover { color: var(--text-muted); border-color: var(--panel-border); background: transparent; } + + .route-map { + height: 220px; + border-radius: 8px; + border: 1px solid var(--panel-border); + background: var(--field-bg); + } + /* Leaflet's own UI (zoom buttons, attribution) is deliberately left in its + standard light styling - it sits on map tiles, not our page background. */ + + dialog.map-dialog { + border: 1px solid var(--panel-border); + border-radius: 10px; + background: var(--panel); + color: var(--text); + padding: 1rem; + width: min(560px, 92vw); + } + dialog.map-dialog::backdrop { background: rgba(0, 0, 0, 0.6); } + .map-dialog-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + margin-bottom: 0.75rem; + font-weight: 600; + font-size: 0.95rem; + } .combo { position: relative; display: flex; flex-direction: column; } .combo-list { @@ -194,6 +224,7 @@ .field-hint { font-size: 0.75rem; color: var(--text-muted); margin-top: 0.3rem; min-height: 1em; } .field-hint.matched { color: var(--success); } + {% block head %}{% endblock %}
diff --git a/templates/dashboard.html b/templates/dashboard.html index ccf2451..53bc29b 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -1,5 +1,9 @@ {% extends "base.html" %} {% block title %}Meine Fahrten – PBT{% endblock %} +{% block head %} + +{% endblock %} {% block content %}

Meine Fahrten

@@ -36,6 +40,18 @@ {{ "★" * f.rating }}{{ "☆" * (5 - f.rating) }}