Add map with gates
This commit is contained in:
@@ -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: '© <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");
|
||||
|
||||
Reference in New Issue
Block a user