Add map with gates

This commit is contained in:
Ettore
2026-05-10 17:56:49 +02:00
parent 9f703c1bfa
commit 7e84587788
21 changed files with 865 additions and 16 deletions

View File

@@ -1,5 +1,12 @@
/* app.js - Lagomare Gates frontend */
// ── Leaflet icon fix (assets served from /static/) ───────────────────────────
L.Icon.Default.mergeOptions({
iconUrl: "/static/images/marker-icon.png",
iconRetinaUrl: "/static/images/marker-icon-2x.png",
shadowUrl: "/static/images/marker-shadow.png",
});
// ── Token helpers ─────────────────────────────────────────────────────────────
const TOKEN_KEY = "lg_keypass_token";
@@ -49,6 +56,95 @@ function showGatesView() {
document.getElementById("gates-view").classList.remove("hidden");
}
// ── Map ───────────────────────────────────────────────────────────────────────
let _map = null;
let _mapMarkers = [];
let _homeMarker = null;
const _homeIcon = L.divIcon({
className: "",
html: '<div style="font-size:1.6rem;line-height:1;filter:drop-shadow(0 1px 3px rgba(0,0,0,.5))">🏠</div>',
iconSize: [28, 28],
iconAnchor: [14, 24],
popupAnchor: [0, -24],
});
const _gateIcon = () => L.icon({
iconUrl: "/static/images/gate.svg",
iconSize: [36, 36],
iconAnchor: [18, 36],
popupAnchor: [0, -38],
});
async function _ensureMapReady() {
if (_map) return;
let siteConfig = null;
try { siteConfig = await fetch("/api/site-config").then(r => r.json()); } catch { /* ignore */ }
_map = L.map("map", { zoomControl: true });
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 19,
}).addTo(_map);
if (siteConfig && siteConfig.home) {
_homeMarker = L.marker([siteConfig.home.lat, siteConfig.home.lon], { icon: _homeIcon })
.bindPopup(`<div style="min-width:140px">
<strong>${siteConfig.home.name}</strong><br>
<a href="https://www.google.com/maps/dir/?api=1&destination=${siteConfig.home.lat},${siteConfig.home.lon}"
target="_blank" rel="noopener"
style="display:inline-block;margin-top:.5em;font-size:.85em;text-decoration:underline">
Get directions
</a>
</div>`)
.addTo(_map);
}
}
function _fitMap() {
if (!_map) return;
const bounds = [];
if (_homeMarker) { const ll = _homeMarker.getLatLng(); bounds.push([ll.lat, ll.lng]); }
_mapMarkers.forEach(m => { const ll = m.getLatLng(); bounds.push([ll.lat, ll.lng]); });
if (bounds.length === 1) _map.setView(bounds[0], 16);
else if (bounds.length > 1) _map.fitBounds(bounds, { padding: [32, 32], maxZoom: 17 });
}
async function updateMap(gates) {
await _ensureMapReady();
const gatesWithCoords = gates.filter(g => g.lat != null && g.lon != null);
if (!_homeMarker && gatesWithCoords.length === 0) return; // nothing to put on the map
document.getElementById("map-btn").classList.remove("hidden");
_mapMarkers.forEach(m => m.remove());
_mapMarkers = [];
for (const gate of gatesWithCoords) {
const popup = L.popup().setContent(
`<div style="min-width:140px">
<strong>${gate.name}</strong><br>
<em style="font-size:.85em;color:#666">${gate.gate_type === "car" ? "Car gate" : "Pedestrian gate"}</em><br>
<a href="https://www.google.com/maps/dir/?api=1&destination=${gate.lat},${gate.lon}"
target="_blank" rel="noopener"
style="display:inline-block;margin-top:.5em;font-size:.85em;text-decoration:underline">
Get directions
</a>
</div>`
);
const marker = L.marker([gate.lat, gate.lon], { icon: _gateIcon(gate.gate_type) })
.bindPopup(popup)
.addTo(_map);
_mapMarkers.push(marker);
}
_fitMap();
}
document.getElementById("map-btn").addEventListener("click", () => {
document.getElementById("map-modal").classList.remove("hidden");
if (_map) setTimeout(() => { _map.invalidateSize(); _fitMap(); }, 50);
});
document.getElementById("map-close").addEventListener("click", () => {
document.getElementById("map-modal").classList.add("hidden");
});
document.getElementById("map-modal").addEventListener("click", e => {
if (e.target === e.currentTarget) document.getElementById("map-modal").classList.add("hidden");
});
// ── Gate rendering ────────────────────────────────────────────────────────────
function renderGates(gates) {
const grid = document.getElementById("gates-grid");
@@ -111,6 +207,7 @@ async function loadGates() {
try {
const gates = await apiFetch("GET", "/api/gates");
renderGates(gates);
updateMap(gates);
} catch (e) {
document.getElementById("loading-gates").textContent = e.message;
document.getElementById("loading-gates").classList.remove("hidden");