lazyflat/web/static/map.js
EiSiMo ceb2486f35 map: fixed Berlin view, CartoDB Voyager tiles, OpenRailwayMap transit overlay
- Drop the fitBounds-to-markers logic so the map always opens at the same
  Berlin-wide view (center 52.52/13.405, zoom 11), regardless of pin count
- Swap OSM standard tiles for CartoDB Voyager — cleaner, more
  Google-Maps-like base style
- Add OpenRailwayMap overlay (opacity .75) so S-/U-Bahn/Tram lines are
  highlighted on top of the base
- CSP img-src widened to cover the new tile hosts

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:48:46 +02:00

148 lines
5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lazyflat — Leaflet flat map.
//
// Two important properties:
// 1. The map must only be *built* once the container has a real size, because
// Leaflet initialised on a hidden 0×0 element never loads its tiles.
// 2. The `#wohnungen-body` partial is re-swapped by HTMX every few seconds.
// To avoid rebuilding Leaflet (and all its tile/marker state) on every
// poll — which caused the whitescreen + out-of-frame glitches — the map
// container itself is preserved across swaps via `hx-preserve`, and the
// marker data is pushed in through a sibling <script id="flats-map-data">
// element that DOES get swapped. On each swap we diff markers against the
// fresh data and update in place.
let mapInstance = null;
let markerLayer = null;
let currentFingerprint = "";
const BERLIN_CENTER = [52.52, 13.405];
const BERLIN_ZOOM = 11;
function readMapData() {
const script = document.getElementById("flats-map-data");
if (!script) return [];
try {
const data = JSON.parse(script.textContent || "[]");
return Array.isArray(data) ? data : [];
} catch (e) {
return [];
}
}
function fingerprintOf(data) {
return data
.map((f) => `${f.lat},${f.lng},${f.address || ""},${f.rent || ""}`)
.join("|");
}
function renderMarkers(data) {
if (!mapInstance) return;
const fp = fingerprintOf(data);
if (fp === currentFingerprint) return;
currentFingerprint = fp;
const geo = data.filter((f) => typeof f.lat === "number" && typeof f.lng === "number").length;
console.log(`[lazyflat.map] rendering ${geo}/${data.length} markers`);
if (markerLayer) {
markerLayer.clearLayers();
} else {
markerLayer = L.layerGroup().addTo(mapInstance);
}
data.forEach((f) => {
if (typeof f.lat !== "number" || typeof f.lng !== "number") return;
const rent = f.rent ? Math.round(f.rent) + " " : "";
const rooms = f.rooms ? f.rooms + " Zi" : "";
const size = f.size ? Math.round(f.size) + " " : "";
const meta = [rooms, size, rent].filter(Boolean).join(" · ");
const safeAddr = (f.address || "").replace(/</g, "&lt;");
const safeLink = (f.link || "#").replace(/"/g, "&quot;");
L.marker([f.lat, f.lng]).addTo(markerLayer).bindPopup(
`<b>${safeAddr}</b>` + (meta ? `<br>${meta}` : "") +
`<br><a href="${safeLink}" target="_blank" rel="noopener">Zur Anzeige →</a>`,
);
});
}
function buildMap(el) {
console.log(`[lazyflat.map] building Leaflet instance (container ${el.clientWidth}×${el.clientHeight})`);
mapInstance = L.map(el, {
zoomControl: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
touchZoom: false,
keyboard: false,
}).setView(BERLIN_CENTER, BERLIN_ZOOM);
// CartoDB Voyager — clean, Google-Maps-ish base style.
L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png", {
attribution: "© OpenStreetMap · © CARTO",
subdomains: "abcd",
maxZoom: 19,
}).addTo(mapInstance);
// Transit overlay — OpenRailwayMap highlights S-/U-/Tram-Linien.
L.tileLayer("https://{s}.tiles.openrailwaymap.org/standard/{z}/{x}/{y}.png", {
attribution: "© OpenRailwayMap",
subdomains: "abc",
maxZoom: 19,
opacity: 0.75,
}).addTo(mapInstance);
markerLayer = L.layerGroup().addTo(mapInstance);
}
function ensureMap() {
const el = document.getElementById("flats-map");
if (!el || typeof L === "undefined") return;
// Hidden (0×0) container → tiles would never load; retry after view toggle.
if (el.clientHeight < 10) return;
if (!mapInstance) {
buildMap(el);
} else if (mapInstance._container !== el) {
// Container node changed (shouldn't happen thanks to hx-preserve, but
// defensive: rebuild so Leaflet rebinds to the live element).
try { mapInstance.remove(); } catch (e) {}
mapInstance = null;
markerLayer = null;
currentFingerprint = "";
buildMap(el);
} else {
try { mapInstance.invalidateSize(); } catch (e) {}
}
renderMarkers(readMapData());
}
function wireViewToggle() {
document.querySelectorAll('input[name="view_mode"]').forEach((r) => {
if (r.dataset.wired === "1") return;
r.dataset.wired = "1";
r.addEventListener("change", (e) => {
try { localStorage.setItem("lazyflat_view_mode", e.target.value); } catch (err) {}
requestAnimationFrame(() => requestAnimationFrame(ensureMap));
setTimeout(ensureMap, 120);
setTimeout(ensureMap, 400);
});
});
}
function restoreView() {
let stored = null;
try { stored = localStorage.getItem("lazyflat_view_mode"); } catch (e) {}
if (!stored) return;
const el = document.querySelector(`input[name="view_mode"][value="${stored}"]`);
if (el && !el.checked) {
el.checked = true;
requestAnimationFrame(() => requestAnimationFrame(ensureMap));
setTimeout(ensureMap, 120);
setTimeout(ensureMap, 400);
}
}
function onReady() {
wireViewToggle();
restoreView();
ensureMap();
}
document.addEventListener("DOMContentLoaded", onReady);
document.body && document.body.addEventListener("htmx:afterSwap", onReady);