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:
parent
85f5f364ed
commit
2ebbf76a80
4 changed files with 92 additions and 12 deletions
|
|
@ -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,
|
||||
|
|
|
|||
1
web/static/berlin-districts.geojson
Normal file
1
web/static/berlin-districts.geojson
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue