feat(map-rework): fit-to-results, marker clustering, map-filter toggle, shape/opacity encoding (v0.7.2)

PR #3 of the v0.7.x GUI rework arc. Makes the /events Leaflet map readable and
intentional. Production code; central-gui restart only (no adapter change).

- Fit-to-results default: the map now fits the actual event distribution on
  load (previously disabled -> fixed global zoom-4). Empty result set falls
  back to the CONUS setView (no crash). Re-fits after each HTMX swap, but only
  when the map-filter toggle is OFF (when ON the viewport drives the bbox, so
  re-fitting would fight/loop the filter).
- leaflet.markercluster (1.5.3, via CDN): point markers cluster into numbered
  badges (disableClusteringAtZoom=9, showCoverageOnHover=false,
  spiderfyOnMaxZoom=true). markercluster supports point markers only, so
  polygons/lines render in a separate un-clustered featureGroup; fit unions both.
- Map-filter toggle ("Filter table by map view"), default OFF. When off the
  table shows all filter-matching events regardless of map zoom; the backend
  ignores region_* unless map_filter is set (guards bookmarked URLs too). URL
  carries map_filter=1 only when on (hidden input disabled otherwise).
- Per-event_type marker shape (derived event_type = first category segment):
  circle = quake/hydro/space (points), square = fire (areas),
  triangle = wx (NWS alerts/warnings), star = disaster (GDACS/EONET).
  Rendered as divIcon + CSS clip-path; point markers switched from circleMarker
  to L.marker(divIcon) (also required for markercluster compatibility).
- Per-severity opacity: critical(4)=1.0, high(3)=0.85, moderate(2)=0.7,
  low(1)=0.5, unknown(0/NULL)=0.4. Needed adding severity to the _fetch_events
  SELECT + event dict (row.get for mock-tolerance) + a data-severity row attr.

Adds 4 tests (map_filter gating on/off, bbox reaches query only when on,
severity in SELECT); updates test_events_bbox_guard for the new toggle contract.

Full suite: 662 passed, 1 skipped (central and unprivileged zvx). Vanilla JS +
HTMX + Leaflet/markercluster; CSS functional-only (polish deferred).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Johnson 2026-05-25 01:20:04 +00:00
commit ed9b6b53be
5 changed files with 196 additions and 56 deletions

View file

@ -27,9 +27,10 @@ def test_out_of_range_bbox_is_dropped_not_errored():
def test_valid_bbox_is_preserved():
# v0.7.2: a bbox is only honored when the map-filter toggle is on.
parsed, error = _parse_events_params(_params(
region_north="42.0", region_south="31.0",
region_east="-102.0", region_west="-124.5",
region_east="-102.0", region_west="-124.5", map_filter="1",
))
assert error is None
assert parsed["bbox"] == {

View file

@ -208,3 +208,46 @@ def test_url_round_trip():
reparsed, _ = routes._parse_events_params(requery)
for k in ("q", "adapters", "categories", "event_types", "severities", "time_token"):
assert reparsed[k] == parsed[k]
# --- map-filter toggle + severity column (v0.7.2) ---------------------------
_REGION = {
"region_north": "42", "region_south": "31", "region_east": "-102", "region_west": "-124.5",
"time": "all",
}
def test_map_filter_off_ignores_bbox():
"""Default (no map_filter): region params are present but the bbox must be
dropped, so the table is not constrained by the map view."""
parsed, err = routes._parse_events_params(dict(_REGION))
assert err is None
assert parsed["map_filter"] is False
assert parsed["bbox"] is None
def test_map_filter_on_applies_bbox():
"""map_filter=1: the region bbox is honored."""
parsed, err = routes._parse_events_params(dict(_REGION, map_filter="1"))
assert err is None
assert parsed["map_filter"] is True
assert parsed["bbox"] == {"north": 42.0, "south": 31.0, "east": -102.0, "west": -124.5}
@pytest.mark.asyncio
async def test_bbox_reaches_query_only_when_map_filter_on():
on, _ = routes._parse_events_params(dict(_REGION, map_filter="1"))
off, _ = routes._parse_events_params(dict(_REGION))
cap_on = await _capture(on)
cap_off = await _capture(off)
assert "ST_Intersects" in cap_on["query"]
assert "ST_Intersects" not in cap_off["query"]
@pytest.mark.asyncio
async def test_select_includes_severity_column():
"""v0.7.2 marker opacity needs severity in the row -> SELECT must fetch it."""
parsed, _ = routes._parse_events_params({"time": "all"})
cap = await _capture(parsed)
assert "severity" in cap["query"]