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,
|
"can_apply": allowed and not already_applied,
|
||||||
"is_running": is_running,
|
"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 {
|
return {
|
||||||
"flats": flats_view,
|
"flats": flats_view,
|
||||||
"rejected_flats": rejected_view,
|
"rejected_flats": rejected_view,
|
||||||
"filtered_out_flats": filtered_out_view,
|
"filtered_out_flats": filtered_out_view,
|
||||||
"partner": partner_info,
|
"partner": partner_info,
|
||||||
"map_points": map_points,
|
"map_points": map_points,
|
||||||
|
"selected_districts_csv": selected_districts_csv,
|
||||||
"has_filters": _has_filters(filters_row),
|
"has_filters": _has_filters(filters_row),
|
||||||
"alert_label": alert_label,
|
"alert_label": alert_label,
|
||||||
"alert_chip": alert_chip,
|
"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 mapInstance = null;
|
||||||
let markerLayer = null;
|
let markerLayer = null;
|
||||||
|
let districtLayer = null;
|
||||||
|
let districtGeoJson = null;
|
||||||
|
let districtFetchStarted = false;
|
||||||
let currentFingerprint = "";
|
let currentFingerprint = "";
|
||||||
const BERLIN_CENTER = [52.52, 13.405];
|
const BERLIN_CENTER = [52.52, 13.405];
|
||||||
const BERLIN_ZOOM = 11;
|
const BERLIN_ZOOM = 11;
|
||||||
|
|
||||||
function readMapData() {
|
function readMapData() {
|
||||||
const script = document.getElementById("flats-map-data");
|
const script = document.getElementById("flats-map-data");
|
||||||
if (!script) return { csrf: "", flats: [] };
|
if (!script) return { csrf: "", selectedDistricts: [], flats: [] };
|
||||||
|
let flats = [];
|
||||||
try {
|
try {
|
||||||
const flats = JSON.parse(script.textContent || "[]");
|
const parsed = JSON.parse(script.textContent || "[]");
|
||||||
return {
|
flats = Array.isArray(parsed) ? parsed : [];
|
||||||
csrf: script.dataset.csrf || "",
|
|
||||||
flats: Array.isArray(flats) ? flats : [],
|
|
||||||
};
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { csrf: "", flats: [] };
|
flats = [];
|
||||||
}
|
}
|
||||||
|
const csv = script.dataset.selectedDistricts || "";
|
||||||
|
const selectedDistricts = csv
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
return {
|
||||||
|
csrf: script.dataset.csrf || "",
|
||||||
|
selectedDistricts,
|
||||||
|
flats,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function fingerprintOf(data) {
|
function fingerprintOf(data, selectedDistricts) {
|
||||||
return data
|
const flatPart = data
|
||||||
.map((f) =>
|
.map((f) =>
|
||||||
[f.id, f.lat, f.lng, (f.status && f.status.label) || "",
|
[f.id, f.lat, f.lng, (f.status && f.status.label) || "",
|
||||||
f.can_apply ? 1 : 0, f.is_running ? 1 : 0].join(","),
|
f.can_apply ? 1 : 0, f.is_running ? 1 : 0].join(","),
|
||||||
)
|
)
|
||||||
.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) {
|
function escHtml(s) {
|
||||||
|
|
@ -116,8 +130,8 @@ function pinIcon(color) {
|
||||||
|
|
||||||
function renderMarkers(payload) {
|
function renderMarkers(payload) {
|
||||||
if (!mapInstance) return;
|
if (!mapInstance) return;
|
||||||
const { csrf, flats } = payload;
|
const { csrf, flats, selectedDistricts } = payload;
|
||||||
const fp = fingerprintOf(flats);
|
const fp = fingerprintOf(flats, selectedDistricts);
|
||||||
if (fp === currentFingerprint) return;
|
if (fp === currentFingerprint) return;
|
||||||
currentFingerprint = fp;
|
currentFingerprint = fp;
|
||||||
const geo = flats.filter((f) => typeof f.lat === "number" && typeof f.lng === "number").length;
|
const geo = flats.filter((f) => typeof f.lat === "number" && typeof f.lng === "number").length;
|
||||||
|
|
@ -137,6 +151,60 @@ function renderMarkers(payload) {
|
||||||
.addTo(markerLayer)
|
.addTo(markerLayer)
|
||||||
.bindPopup(popupHtml(f, csrf), { minWidth: 240, closeButton: true });
|
.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) {
|
function buildMap(el) {
|
||||||
|
|
@ -183,6 +251,7 @@ function ensureMap() {
|
||||||
try { mapInstance.remove(); } catch (e) {}
|
try { mapInstance.remove(); } catch (e) {}
|
||||||
mapInstance = null;
|
mapInstance = null;
|
||||||
markerLayer = null;
|
markerLayer = null;
|
||||||
|
districtLayer = null;
|
||||||
currentFingerprint = "";
|
currentFingerprint = "";
|
||||||
buildMap(el);
|
buildMap(el);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,9 @@
|
||||||
<div id="flats-map" hx-preserve="true"></div>
|
<div id="flats-map" hx-preserve="true"></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 -->
|
<!-- Liste -->
|
||||||
<section class="view-list card">
|
<section class="view-list card">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue