experiment: shade excluded Bezirke on map (faint yellow overlay)

Tints districts the user's Bezirk filter has EXCLUDED in light yellow
(#fde68a, fillOpacity 0.35) so the active selection is obvious from
the map alone — and you can see at a glance whether a "rausgefiltert"
flat fell in a no-go district. When no district filter is set the
overlay stays off entirely (nothing is excluded).

Wiring:
- Berlin Bezirke GeoJSON checked in at web/static/berlin-districts.geojson
  (12 features, name property matches our DISTRICTS list 1:1, props
  stripped down to {name} and minified — 312KB raw, ~80KB gzipped).
- Route exposes the user's selected_districts_csv to the template.
- The flats-map-data <script> carries it on data-selected-districts so
  it flows in alongside csrf and the marker payload.
- map.js fetches the GeoJSON once (cache normally), keeps the layer in
  a module-level reference, and re-styles it via setStyle() on every
  swap (cheap). Marker layer is kicked to the front so pins always
  paint above the shaded polygons. fingerprintOf now also folds in
  the selected-districts CSV so a Bezirk-only filter change still
  triggers a re-render.

Branch only — kept off main while we see if this reads well.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-23 13:30:30 +02:00
parent 85f5f364ed
commit 2ebbf76a80
4 changed files with 92 additions and 12 deletions

View file

@ -128,12 +128,20 @@ def _wohnungen_context(user) -> dict:
"can_apply": allowed and not already_applied,
"is_running": is_running,
})
# Bezirke the user has narrowed to (CSV). Empty = no district filter, in
# which case the map's "excluded districts" overlay stays off entirely.
try:
selected_districts_csv = (filters_row["districts"] if filters_row else "") or ""
except (KeyError, IndexError, TypeError):
selected_districts_csv = ""
return {
"flats": flats_view,
"rejected_flats": rejected_view,
"filtered_out_flats": filtered_out_view,
"partner": partner_info,
"map_points": map_points,
"selected_districts_csv": selected_districts_csv,
"has_filters": _has_filters(filters_row),
"alert_label": alert_label,
"alert_chip": alert_chip,

File diff suppressed because one or more lines are too long

View file

@ -13,31 +13,45 @@
let mapInstance = null;
let markerLayer = null;
let districtLayer = null;
let districtGeoJson = null;
let districtFetchStarted = false;
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 { csrf: "", flats: [] };
if (!script) return { csrf: "", selectedDistricts: [], flats: [] };
let flats = [];
try {
const flats = JSON.parse(script.textContent || "[]");
const parsed = JSON.parse(script.textContent || "[]");
flats = Array.isArray(parsed) ? parsed : [];
} catch (e) {
flats = [];
}
const csv = script.dataset.selectedDistricts || "";
const selectedDistricts = csv
.split(",")
.map((s) => s.trim())
.filter(Boolean);
return {
csrf: script.dataset.csrf || "",
flats: Array.isArray(flats) ? flats : [],
selectedDistricts,
flats,
};
} catch (e) {
return { csrf: "", flats: [] };
}
}
function fingerprintOf(data) {
return data
function fingerprintOf(data, selectedDistricts) {
const flatPart = data
.map((f) =>
[f.id, f.lat, f.lng, (f.status && f.status.label) || "",
f.can_apply ? 1 : 0, f.is_running ? 1 : 0].join(","),
)
.join("|");
// Include selected districts so changing the Bezirk filter re-renders the
// overlay even when no flat-level field changed.
return flatPart + "||" + selectedDistricts.join(",");
}
function escHtml(s) {
@ -116,8 +130,8 @@ function pinIcon(color) {
function renderMarkers(payload) {
if (!mapInstance) return;
const { csrf, flats } = payload;
const fp = fingerprintOf(flats);
const { csrf, flats, selectedDistricts } = payload;
const fp = fingerprintOf(flats, selectedDistricts);
if (fp === currentFingerprint) return;
currentFingerprint = fp;
const geo = flats.filter((f) => typeof f.lat === "number" && typeof f.lng === "number").length;
@ -137,6 +151,60 @@ function renderMarkers(payload) {
.addTo(markerLayer)
.bindPopup(popupHtml(f, csrf), { minWidth: 240, closeButton: true });
});
ensureDistrictLayer(selectedDistricts);
}
// EXPERIMENT: faintly highlight Bezirke the user's filter EXCLUDES so the
// active selection is obvious at a glance. When the filter is empty (no
// district narrowing) the layer is hidden — nothing is excluded.
function districtStyle(selectedSet) {
const showShading = selectedSet.size > 0;
return (feature) => {
const name = feature.properties && feature.properties.name;
const excluded = showShading && !selectedSet.has(name);
return {
stroke: excluded,
color: "#c89318",
weight: 1,
opacity: 0.55,
fill: excluded,
fillColor: "#fde68a",
fillOpacity: 0.35,
};
};
}
function ensureDistrictLayer(selectedDistricts) {
if (!mapInstance) return;
// Lazy-fetch the GeoJSON once. The /static/ asset is small (~80KB gzipped).
if (!districtGeoJson && !districtFetchStarted) {
districtFetchStarted = true;
fetch("/static/berlin-districts.geojson")
.then((r) => r.json())
.then((g) => {
districtGeoJson = g;
ensureDistrictLayer(readMapData().selectedDistricts);
})
.catch((e) => console.warn("[lazyflat.map] district geojson load failed:", e));
return;
}
if (!districtGeoJson) return;
const set = new Set(selectedDistricts);
if (districtLayer) {
districtLayer.setStyle(districtStyle(set));
} else {
districtLayer = L.geoJSON(districtGeoJson, {
style: districtStyle(set),
interactive: false,
});
districtLayer.addTo(mapInstance);
// Markers must paint above the overlay; re-add marker layer to ensure
// it ends up on top regardless of insertion order.
if (markerLayer) {
markerLayer.bringToFront();
}
}
}
function buildMap(el) {
@ -183,6 +251,7 @@ function ensureMap() {
try { mapInstance.remove(); } catch (e) {}
mapInstance = null;
markerLayer = null;
districtLayer = null;
currentFingerprint = "";
buildMap(el);
} else {

View file

@ -103,7 +103,9 @@
<div id="flats-map" hx-preserve="true"></div>
</div>
</section>
<script id="flats-map-data" type="application/json" data-csrf="{{ csrf }}">{{ map_points | tojson }}</script>
<script id="flats-map-data" type="application/json"
data-csrf="{{ csrf }}"
data-selected-districts="{{ selected_districts_csv }}">{{ map_points | tojson }}</script>
<!-- Liste -->
<section class="view-list card">