- 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>
148 lines
5 KiB
JavaScript
148 lines
5 KiB
JavaScript
// 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) + " m²" : "";
|
||
const meta = [rooms, size, rent].filter(Boolean).join(" · ");
|
||
const safeAddr = (f.address || "").replace(/</g, "<");
|
||
const safeLink = (f.link || "#").replace(/"/g, """);
|
||
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);
|