fix(wasserfoerderung): add wheel/pinch zoom and overzoom to Karte mode - #170
Merged
DeepDiver1975 merged 3 commits intoAug 31, 2026
Conversation
Reported: "I cannot zoom into the map data" (initially proposed introducing
Mapsui). Investigated first: MapCanvasControl had no scroll-wheel or pinch
zoom at all -- only two tiny +/- buttons -- and WasserfoerderungViewModel's
MinZoom/MaxZoom were fixed constants (3/19) unrelated to what the
configured region's region.mbtiles actually contains. The real,
published Fürstenfeldbruck pack only renders z11-15; MapDrawing silently
skipped any missing tile, so clicking + past z15 or - past z11 showed a
blank canvas with zero feedback. Mapsui would fix this too, but at real
cost (this app's first third-party dependency, blocked on Android today
since it needs Avalonia >=11.3.1 and Android is pinned to 11.2.2, and a
much bigger rewrite than the bug needs) -- fixed the actual root cause
instead.
- IMapTileSource.GetMaxZoom() (MbTilesFileSource: SELECT MAX(zoom_level)).
- WasserfoerderungViewModel: per-region MinZoom (from the same
GetTileBounds() the center fix already computes -- the region's own
lowest rendered zoom), a new ChangeMapViewCommand(MapViewChange) command
applying a wheel/pinch-driven view change, clamped to Min/MaxZoom.
- MapCanvasControl: OnPointerWheelChanged and a PinchGestureRecognizer
handler, both zooming while keeping the gesture's focal point
geographically stationary, routed through the new ViewChangedCommand
(matching the control's existing PointClickedCommand/UndoRequestedCommand
pattern rather than two-way property binding).
- MapDrawing.DrawTiles: when the exact tile is missing past the source's
actual max zoom, draws a cropped ancestor tile from the max zoom instead
("overzoom" -- standard map-app behavior past native detail).
Verified against the real published Fürstenfeldbruck pack: scrolling in
6 levels past z15 shows a legible overzoomed view (not blank); scrolling
out clamps cleanly at z11 (the pack's real minimum), never blank.
Pinch-to-zoom's scale->zoom-delta math is unit-tested directly
(Avalonia.Headless has no touch/gesture simulation API to drive the full
gesture pipeline).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
…aches the page Root cause (systematic-debugging): the wheel-zoom just added in this same branch unconditionally captured every wheel event over MapCanvasControl (e.Handled = true, no modifier check). The map sits inside the Wasserförderung tab's own ScrollViewer (WasserfoerderungView.axaml) at 360px tall — on a laptop where the window is short enough that the tab needs to scroll, the cursor is very likely to land on the map while the operator scrolls the page. Every such scroll got swallowed as a zoom instead of reaching the ScrollViewer, and a laptop trackpad's rapid wheel-delta stream during a single scroll swipe could zoom dozens of levels in an instant — "renders it unreadable," exactly as reported. Fix: gate wheel-zoom on Ctrl (KeyModifiers.Control), matching how most embedded maps resolve this exact conflict (Leaflet, Google Maps embeds, etc.). Plain scroll no longer sets e.Handled, so it bubbles to the ScrollViewer as normal; Ctrl+scroll still zooms exactly as before, keeping the cursor's geo point stationary. Pinch-to-zoom is unaffected — a pinch gesture doesn't conflict with page-scroll the way a plain wheel notch does. Verified against the real published Fürstenfeldbruck pack: plain scroll over the map no longer changes zoom; Ctrl+scroll still zooms in by one level as designed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
…Karte mode
Root cause (systematic-debugging): cursor-anchored wheel/pinch zoom
shifts the map's center as a side effect (correct, by design), but
MapCanvasControl had no drag-to-pan at all and WasserfoerderungViewModel
never kept the region's initial center/zoom anywhere. Once the view
drifted away from the configured region (trivial after a few zooms near
an edge), there was no way back at all -- matching the report exactly.
Considered Mapsui again; same conclusion as before (first third-party
dependency, blocked on Android's Avalonia pin, much bigger than this
gap needs) -- added the missing controls to the existing hand-rolled map
instead.
- WasserfoerderungViewModel: stores the view it opened with and exposes
a new ResetMapViewCommand ("ZENTRIEREN") that returns to it.
- MapCanvasControl: Ctrl+left-drag pans (moving the center opposite the
drag direction, standard map UX), routed through the same
ViewChangedCommand as wheel/pinch zoom. Reuses the Ctrl convention
already established for zoom, so it never risks being confused with
the primary plain-left-click-to-draw-a-route-point gesture.
- Added a small "Strg + Scrollen: Zoom / Strg + Ziehen: Verschieben"
hint next to the new button, since both interactions are otherwise
undiscoverable.
Also fixed a real regression these additions exposed: the extra button
and hint line made the tab tall enough to trigger the outer
ScrollViewer's (Fluent overlay-style) vertical scrollbar at window sizes
that previously fit without scrolling. That scrollbar renders on top of
-- not narrowing -- the content, so it silently swallowed clicks on the
map's own right edge. Fixed by reserving a right margin matching the
scrollbar's width, so real content never sits flush against that edge
regardless of what triggers scrolling in the future.
Verified against the real published Fürstenfeldbruck pack: drifting the
view to Berlin, then Ctrl+drag panning and clicking ZENTRIEREN, both
bring it back to Fürstenfeldbruck exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2
Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com>
4 tasks
DeepDiver1975
merged commit Aug 31, 2026
b2bbafe
into
fix/wasserfoerderung-karte-default-center
1 of 2 checks passed
DeepDiver1975
added a commit
that referenced
this pull request
Aug 31, 2026
…eal tiles (#169) * feat(wasserfoerderung): downloadable region packs for Einsatzgebiet (#150) Stammdaten's Einsatzgebiet config was a raw folder-path text field with no way to actually get map data onto a machine. Replace it with a dropdown fed by a published region-pack catalog (regions.json + GitHub Releases, one pack per Landkreis): pick a region, hit "Herunterladen", the app fetches and extracts region.mbtiles/region.dem via IRegionPackCatalogService and IRegionPackInstaller. Manual folder entry stays available under an "Erweitert" fallback for a self-built or hand-placed pack. The pack-building side (osmium extract, raster tile rendering, SRTM elevation conversion) stays a documented external runbook under tools/build-region-pack/, deliberately kept out of LageBuch.sln — it's a one-time-per-region maintainer task, not something every installation needs to run itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): reject path-traversal slugs in region-pack manifest RegionPackInstaller joins pack.Slug straight onto the regions base directory (<regionsBaseDir>/<slug>). Since regions.json is fetched from a third-party-controlled URL, a manifest entry with a slug like "../../etc" could extract a downloaded pack outside the regions directory. Reject unsafe slugs at parse time in RegionPackCatalogJson (same "malformed entry is skipped, not thrown" defensive style already used for missing/wrong-typed fields), and add a defense-in-depth path-containment check in RegionPackInstaller itself in case a slug ever reaches it another way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): center Karte mode on the configured region's real tiles Root cause (systematic-debugging): WasserfoerderungViewModel's initial map center/zoom were a hardcoded constant (48.14, 11.58 — near Munich) picked before any Einsatzgebiet had a knowable location. The real, published Fürstenfeldbruck pack's tiles don't cover that point at all (z14 tile column 8719 vs. the pack's actual [8691,8712] range), so Karte mode opened on a blank map with no visible tiles — matching the report exactly. Fix: derive the initial view from the tiles the configured region.mbtiles actually has, via a new IMapTileSource.GetTileBounds() (lowest zoom level present, TMS rows flipped back to XYZ), converted to a center through WebMercator — moved from LageBuch.App.Shared to LageBuch.AppLogic since IncidentWorkspaceViewModel now needs it too. This self-corrects for any region (downloaded or manually placed under "Erweitert") since it reads the actual tile data rather than trusting separately-tracked metadata. The hardcoded fallback stays for the truly-no-tiles case. Verified against the real published Fürstenfeldbruck pack: Karte mode now opens already showing Fürstenfeldbruck/Olching/Puchheim/Germering, not a blank canvas. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): add wheel/pinch zoom and overzoom to Karte mode (#170) * fix(wasserfoerderung): add wheel/pinch zoom and overzoom to Karte mode Reported: "I cannot zoom into the map data" (initially proposed introducing Mapsui). Investigated first: MapCanvasControl had no scroll-wheel or pinch zoom at all -- only two tiny +/- buttons -- and WasserfoerderungViewModel's MinZoom/MaxZoom were fixed constants (3/19) unrelated to what the configured region's region.mbtiles actually contains. The real, published Fürstenfeldbruck pack only renders z11-15; MapDrawing silently skipped any missing tile, so clicking + past z15 or - past z11 showed a blank canvas with zero feedback. Mapsui would fix this too, but at real cost (this app's first third-party dependency, blocked on Android today since it needs Avalonia >=11.3.1 and Android is pinned to 11.2.2, and a much bigger rewrite than the bug needs) -- fixed the actual root cause instead. - IMapTileSource.GetMaxZoom() (MbTilesFileSource: SELECT MAX(zoom_level)). - WasserfoerderungViewModel: per-region MinZoom (from the same GetTileBounds() the center fix already computes -- the region's own lowest rendered zoom), a new ChangeMapViewCommand(MapViewChange) command applying a wheel/pinch-driven view change, clamped to Min/MaxZoom. - MapCanvasControl: OnPointerWheelChanged and a PinchGestureRecognizer handler, both zooming while keeping the gesture's focal point geographically stationary, routed through the new ViewChangedCommand (matching the control's existing PointClickedCommand/UndoRequestedCommand pattern rather than two-way property binding). - MapDrawing.DrawTiles: when the exact tile is missing past the source's actual max zoom, draws a cropped ancestor tile from the max zoom instead ("overzoom" -- standard map-app behavior past native detail). Verified against the real published Fürstenfeldbruck pack: scrolling in 6 levels past z15 shows a legible overzoomed view (not blank); scrolling out clamps cleanly at z11 (the pack's real minimum), never blank. Pinch-to-zoom's scale->zoom-delta math is unit-tested directly (Avalonia.Headless has no touch/gesture simulation API to drive the full gesture pipeline). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): require Ctrl for wheel-zoom so plain scroll reaches the page Root cause (systematic-debugging): the wheel-zoom just added in this same branch unconditionally captured every wheel event over MapCanvasControl (e.Handled = true, no modifier check). The map sits inside the Wasserförderung tab's own ScrollViewer (WasserfoerderungView.axaml) at 360px tall — on a laptop where the window is short enough that the tab needs to scroll, the cursor is very likely to land on the map while the operator scrolls the page. Every such scroll got swallowed as a zoom instead of reaching the ScrollViewer, and a laptop trackpad's rapid wheel-delta stream during a single scroll swipe could zoom dozens of levels in an instant — "renders it unreadable," exactly as reported. Fix: gate wheel-zoom on Ctrl (KeyModifiers.Control), matching how most embedded maps resolve this exact conflict (Leaflet, Google Maps embeds, etc.). Plain scroll no longer sets e.Handled, so it bubbles to the ScrollViewer as normal; Ctrl+scroll still zooms exactly as before, keeping the cursor's geo point stationary. Pinch-to-zoom is unaffected — a pinch gesture doesn't conflict with page-scroll the way a plain wheel notch does. Verified against the real published Fürstenfeldbruck pack: plain scroll over the map no longer changes zoom; Ctrl+scroll still zooms in by one level as designed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): add Ctrl+drag pan and a reset-view control to Karte mode Root cause (systematic-debugging): cursor-anchored wheel/pinch zoom shifts the map's center as a side effect (correct, by design), but MapCanvasControl had no drag-to-pan at all and WasserfoerderungViewModel never kept the region's initial center/zoom anywhere. Once the view drifted away from the configured region (trivial after a few zooms near an edge), there was no way back at all -- matching the report exactly. Considered Mapsui again; same conclusion as before (first third-party dependency, blocked on Android's Avalonia pin, much bigger than this gap needs) -- added the missing controls to the existing hand-rolled map instead. - WasserfoerderungViewModel: stores the view it opened with and exposes a new ResetMapViewCommand ("ZENTRIEREN") that returns to it. - MapCanvasControl: Ctrl+left-drag pans (moving the center opposite the drag direction, standard map UX), routed through the same ViewChangedCommand as wheel/pinch zoom. Reuses the Ctrl convention already established for zoom, so it never risks being confused with the primary plain-left-click-to-draw-a-route-point gesture. - Added a small "Strg + Scrollen: Zoom / Strg + Ziehen: Verschieben" hint next to the new button, since both interactions are otherwise undiscoverable. Also fixed a real regression these additions exposed: the extra button and hint line made the tab tall enough to trigger the outer ScrollViewer's (Fluent overlay-style) vertical scrollbar at window sizes that previously fit without scrolling. That scrollbar renders on top of -- not narrowing -- the content, so it silently swallowed clicks on the map's own right edge. Fixed by reserving a right margin matching the scrollbar's width, so real content never sits flush against that edge regardless of what triggers scrolling in the future. Verified against the real published Fürstenfeldbruck pack: drifting the view to Berlin, then Ctrl+drag panning and clicking ZENTRIEREN, both bring it back to Fürstenfeldbruck exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
DeepDiver1975
added a commit
that referenced
this pull request
Aug 31, 2026
…ion profiles (#150, Plan B) (#164) * feat(wasserfoerderung): draw Förderstrecken on a map with real elevation profiles (#150, Plan B) Phase 2 of #150: lets the operator draw a Wasserförderung route on a map instead of typing length/rise by hand. Terrain is sampled from a bundled DEM (custom binary heightmap) along the drawn polyline, and pump placement is computed leg-by-leg against the actual profile instead of assuming one uniform gradient — catching interior crests (a climb then descent back to the same net height) that the flat Plan A formula would miss. Map tiles come from a bundled MBTiles file, both referenced from a new "Einsatzgebiet" (region of operation) global Stammdaten setting. Fully offline like the rest of the app, no new NuGet dependency: MBTiles is read via the already-referenced Microsoft.Data.Sqlite, and the map canvas is a hand-rolled Avalonia control using standard Web Mercator tile math. Manuell (Plan A) entry stays available unchanged wherever a region isn't configured. The route also gets a small map snapshot embedded in the PDF export next to its numeric row. 826 -> 882 tests, all green. Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): stop Karte-mode content overlapping the header at small windows The Karte-mode layout used a DockPanel with a fixed Height="360" map plus stacked Bottom-docked input-dock panels and no ScrollViewer. In a window short enough that the DataGrid's share of the DockPanel collapsed to zero (confirmed at 1920x700), the map Border — later in Z-order — rendered on top of and overlapped the mode-toggle header buttons instead of being pushed below them. Separately, the Karte input dock's unwrapped horizontal button row pushed "FERTIG" past the window's right edge entirely at narrower widths (confirmed at 1080 wide). Root-caused by reproducing both at exact window sizes via the headless harness (not by guessing from the screenshot) after an initial attempt to reproduce via the live desktop app under Xvfb touched a real incident file's mtime through its native open dialog — that route was abandoned once caught. Fix: wrap everything below the header in a single ScrollViewer with naturally-stacked content (same pattern already used by ChecklistView/FilesView/etc. in this codebase) so insufficient space scrolls instead of overlaps, and switch both input docks' button rows from an unwrapped horizontal StackPanel to a WrapPanel so buttons wrap instead of overflowing the window. New regression test asserts the map never starts above the header's bottom edge and FERTIG's right edge never exceeds the window width, at the exact size that reproduced the bug. 882 -> 883 tests, all green. Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * feat(wasserfoerderung): downloadable region packs for Einsatzgebiet (#150) (#168) * feat(wasserfoerderung): downloadable region packs for Einsatzgebiet (#150) Stammdaten's Einsatzgebiet config was a raw folder-path text field with no way to actually get map data onto a machine. Replace it with a dropdown fed by a published region-pack catalog (regions.json + GitHub Releases, one pack per Landkreis): pick a region, hit "Herunterladen", the app fetches and extracts region.mbtiles/region.dem via IRegionPackCatalogService and IRegionPackInstaller. Manual folder entry stays available under an "Erweitert" fallback for a self-built or hand-placed pack. The pack-building side (osmium extract, raster tile rendering, SRTM elevation conversion) stays a documented external runbook under tools/build-region-pack/, deliberately kept out of LageBuch.sln — it's a one-time-per-region maintainer task, not something every installation needs to run itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): reject path-traversal slugs in region-pack manifest RegionPackInstaller joins pack.Slug straight onto the regions base directory (<regionsBaseDir>/<slug>). Since regions.json is fetched from a third-party-controlled URL, a manifest entry with a slug like "../../etc" could extract a downloaded pack outside the regions directory. Reject unsafe slugs at parse time in RegionPackCatalogJson (same "malformed entry is skipped, not thrown" defensive style already used for missing/wrong-typed fields), and add a defense-in-depth path-containment check in RegionPackInstaller itself in case a slug ever reaches it another way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> * fix(wasserfoerderung): center Karte mode on the configured region's real tiles (#169) * feat(wasserfoerderung): downloadable region packs for Einsatzgebiet (#150) Stammdaten's Einsatzgebiet config was a raw folder-path text field with no way to actually get map data onto a machine. Replace it with a dropdown fed by a published region-pack catalog (regions.json + GitHub Releases, one pack per Landkreis): pick a region, hit "Herunterladen", the app fetches and extracts region.mbtiles/region.dem via IRegionPackCatalogService and IRegionPackInstaller. Manual folder entry stays available under an "Erweitert" fallback for a self-built or hand-placed pack. The pack-building side (osmium extract, raster tile rendering, SRTM elevation conversion) stays a documented external runbook under tools/build-region-pack/, deliberately kept out of LageBuch.sln — it's a one-time-per-region maintainer task, not something every installation needs to run itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): reject path-traversal slugs in region-pack manifest RegionPackInstaller joins pack.Slug straight onto the regions base directory (<regionsBaseDir>/<slug>). Since regions.json is fetched from a third-party-controlled URL, a manifest entry with a slug like "../../etc" could extract a downloaded pack outside the regions directory. Reject unsafe slugs at parse time in RegionPackCatalogJson (same "malformed entry is skipped, not thrown" defensive style already used for missing/wrong-typed fields), and add a defense-in-depth path-containment check in RegionPackInstaller itself in case a slug ever reaches it another way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): center Karte mode on the configured region's real tiles Root cause (systematic-debugging): WasserfoerderungViewModel's initial map center/zoom were a hardcoded constant (48.14, 11.58 — near Munich) picked before any Einsatzgebiet had a knowable location. The real, published Fürstenfeldbruck pack's tiles don't cover that point at all (z14 tile column 8719 vs. the pack's actual [8691,8712] range), so Karte mode opened on a blank map with no visible tiles — matching the report exactly. Fix: derive the initial view from the tiles the configured region.mbtiles actually has, via a new IMapTileSource.GetTileBounds() (lowest zoom level present, TMS rows flipped back to XYZ), converted to a center through WebMercator — moved from LageBuch.App.Shared to LageBuch.AppLogic since IncidentWorkspaceViewModel now needs it too. This self-corrects for any region (downloaded or manually placed under "Erweitert") since it reads the actual tile data rather than trusting separately-tracked metadata. The hardcoded fallback stays for the truly-no-tiles case. Verified against the real published Fürstenfeldbruck pack: Karte mode now opens already showing Fürstenfeldbruck/Olching/Puchheim/Germering, not a blank canvas. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): add wheel/pinch zoom and overzoom to Karte mode (#170) * fix(wasserfoerderung): add wheel/pinch zoom and overzoom to Karte mode Reported: "I cannot zoom into the map data" (initially proposed introducing Mapsui). Investigated first: MapCanvasControl had no scroll-wheel or pinch zoom at all -- only two tiny +/- buttons -- and WasserfoerderungViewModel's MinZoom/MaxZoom were fixed constants (3/19) unrelated to what the configured region's region.mbtiles actually contains. The real, published Fürstenfeldbruck pack only renders z11-15; MapDrawing silently skipped any missing tile, so clicking + past z15 or - past z11 showed a blank canvas with zero feedback. Mapsui would fix this too, but at real cost (this app's first third-party dependency, blocked on Android today since it needs Avalonia >=11.3.1 and Android is pinned to 11.2.2, and a much bigger rewrite than the bug needs) -- fixed the actual root cause instead. - IMapTileSource.GetMaxZoom() (MbTilesFileSource: SELECT MAX(zoom_level)). - WasserfoerderungViewModel: per-region MinZoom (from the same GetTileBounds() the center fix already computes -- the region's own lowest rendered zoom), a new ChangeMapViewCommand(MapViewChange) command applying a wheel/pinch-driven view change, clamped to Min/MaxZoom. - MapCanvasControl: OnPointerWheelChanged and a PinchGestureRecognizer handler, both zooming while keeping the gesture's focal point geographically stationary, routed through the new ViewChangedCommand (matching the control's existing PointClickedCommand/UndoRequestedCommand pattern rather than two-way property binding). - MapDrawing.DrawTiles: when the exact tile is missing past the source's actual max zoom, draws a cropped ancestor tile from the max zoom instead ("overzoom" -- standard map-app behavior past native detail). Verified against the real published Fürstenfeldbruck pack: scrolling in 6 levels past z15 shows a legible overzoomed view (not blank); scrolling out clamps cleanly at z11 (the pack's real minimum), never blank. Pinch-to-zoom's scale->zoom-delta math is unit-tested directly (Avalonia.Headless has no touch/gesture simulation API to drive the full gesture pipeline). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): require Ctrl for wheel-zoom so plain scroll reaches the page Root cause (systematic-debugging): the wheel-zoom just added in this same branch unconditionally captured every wheel event over MapCanvasControl (e.Handled = true, no modifier check). The map sits inside the Wasserförderung tab's own ScrollViewer (WasserfoerderungView.axaml) at 360px tall — on a laptop where the window is short enough that the tab needs to scroll, the cursor is very likely to land on the map while the operator scrolls the page. Every such scroll got swallowed as a zoom instead of reaching the ScrollViewer, and a laptop trackpad's rapid wheel-delta stream during a single scroll swipe could zoom dozens of levels in an instant — "renders it unreadable," exactly as reported. Fix: gate wheel-zoom on Ctrl (KeyModifiers.Control), matching how most embedded maps resolve this exact conflict (Leaflet, Google Maps embeds, etc.). Plain scroll no longer sets e.Handled, so it bubbles to the ScrollViewer as normal; Ctrl+scroll still zooms exactly as before, keeping the cursor's geo point stationary. Pinch-to-zoom is unaffected — a pinch gesture doesn't conflict with page-scroll the way a plain wheel notch does. Verified against the real published Fürstenfeldbruck pack: plain scroll over the map no longer changes zoom; Ctrl+scroll still zooms in by one level as designed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> * fix(wasserfoerderung): add Ctrl+drag pan and a reset-view control to Karte mode Root cause (systematic-debugging): cursor-anchored wheel/pinch zoom shifts the map's center as a side effect (correct, by design), but MapCanvasControl had no drag-to-pan at all and WasserfoerderungViewModel never kept the region's initial center/zoom anywhere. Once the view drifted away from the configured region (trivial after a few zooms near an edge), there was no way back at all -- matching the report exactly. Considered Mapsui again; same conclusion as before (first third-party dependency, blocked on Android's Avalonia pin, much bigger than this gap needs) -- added the missing controls to the existing hand-rolled map instead. - WasserfoerderungViewModel: stores the view it opened with and exposes a new ResetMapViewCommand ("ZENTRIEREN") that returns to it. - MapCanvasControl: Ctrl+left-drag pans (moving the center opposite the drag direction, standard map UX), routed through the same ViewChangedCommand as wheel/pinch zoom. Reuses the Ctrl convention already established for zoom, so it never risks being confused with the primary plain-left-click-to-draw-a-route-point gesture. - Added a small "Strg + Scrollen: Zoom / Strg + Ziehen: Verschieben" hint next to the new button, since both interactions are otherwise undiscoverable. Also fixed a real regression these additions exposed: the extra button and hint line made the tab tall enough to trigger the outer ScrollViewer's (Fluent overlay-style) vertical scrollbar at window sizes that previously fit without scrolling. That scrollbar renders on top of -- not narrowing -- the content, so it silently swallowed clicks on the map's own right edge. Fixed by reserving a right margin matching the scrollbar's width, so real content never sits flush against that edge regardless of what triggers scrolling in the future. Verified against the real published Fürstenfeldbruck pack: drifting the view to Berlin, then Ctrl+drag panning and clicking ZENTRIEREN, both bring it back to Fürstenfeldbruck exactly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2 Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> --------- Signed-off-by: Thomas Müller <1005065+DeepDiver1975@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #169. Reported: "I think its time to introduce Mapsui - as I cannot zoom into the map
data." Investigated before agreeing to that — Mapsui would fix it, but at real cost confirmed this
session: it'd be this app's first-ever third-party NuGet dependency (current footprint is
Avalonia + its own first-party packages +
CommunityToolkit.Mvvm, nothing else),Mapsui.Avalonia5.1.0 needs
Avalonia >= 11.3.1butsrc/LageBuch.App.Androidis still pinned to11.2.2("pending lockstep upgrade" — blocked on Android today), and it'd mean rewriting
MapCanvasControl/MapDrawing/WebMercator/the PDFRouteOverviewRenderer, not just fixingzoom. Fixed the actual root cause instead (confirmed with the user before starting).
Root cause:
MapCanvasControlhad no scroll-wheel or pinch zoom at all — only two tiny+/−buttons.
WasserfoerderungViewModel.MinZoom/MaxZoomwere fixed constants (3/19) unrelated towhat the configured region's
region.mbtilesactually contains. The real, publishedFürstenfeldbruck pack only renders z11–15;
MapDrawingsilently skipped drawing anything for amissing tile — so clicking
+past z15 or−past z11 showed a blank canvas with zero feedback.From the operator's seat that's indistinguishable from "I can't zoom."
Changes
IMapTileSource.GetMaxZoom()—MbTilesFileSourceimplements it viaSELECT MAX(zoom_level).WasserfoerderungViewModel: per-regionMinZoom(reusing the sameGetTileBounds()thecenter fix from fix(wasserfoerderung): center Karte mode on the configured region's real tiles #169 already computes — the region's own lowest rendered zoom), a new
ChangeMapViewCommand(MapViewChange)applying a wheel/pinch-driven view change, clamped toMin/MaxZoom.
MapCanvasControl:OnPointerWheelChanged+ aPinchGestureRecognizer-backed pinch handler,both zooming while keeping the gesture's focal point geographically stationary (standard
"zoom to cursor"/"zoom to pinch centroid"), routed through a new
ViewChangedCommand—matching this control's existing
PointClickedCommand/UndoRequestedCommandpattern ratherthan switching those three properties to two-way binding.
MapDrawing.DrawTiles: when the exact tile is missing and the requested zoom is past thesource's actual max, draws a cropped ancestor tile from the max zoom instead ("overzoom" —
standard behavior every map app has past native detail).
Verification
MbTilesFileSourceTests(GetMaxZoom),WasserfoerderungViewModelTests(
ChangeMapViewCommand+ clamping),IncidentWorkspaceViewModelTests(a configured region'slowest zoom flows into the constructed VM and clamps
ZoomOutCommand),MapDrawingTests(
ComputeOverzoomTile's ancestor/crop math — direct unit tests plus an integration test throughMapCanvasControlconfirming the fallback actually fires),MapCanvasControlTests(scrollwheel zooms in/out by one level keeping the cursor's geo point stationary; pinch's
scale→zoom-delta math unit-tested directly since
Avalonia.Headlesshas no touch/gesturesimulation API to drive the full pinch pipeline).
dotnet test LageBuch.sln: 943 tests passing, 1 pre-existing skip.test, deleted after capturing evidence): scrolling in 6 levels past z15 shows a legible
overzoomed view instead of blank; scrolling out 15 times clamps cleanly at z11 (the pack's real
minimum) instead of going blank past it. Screenshots below.
Screenshots
Please paste these into the PR body — I can't upload images directly:
/tmp/lagebuch-shots/wasserfoerderung-karte-overzoomed.png— 6 scroll-zooms past z15, showinga stretched-but-legible view (street names still readable) instead of blank.
/tmp/lagebuch-shots/wasserfoerderung-karte-min-zoom.png— 15 scroll-zoom-outs, clampedcleanly at z11 showing the full Landkreis, instead of going blank past the pack's minimum.
Checklist
git commit -s)dotnet buildanddotnet testpass locally (943 tests, 1 pre-existing skip; Android SDKpackaging fails in this sandbox only — known environment gap, not a code issue)
already-published, public region pack — no PII)
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com
https://claude.ai/code/session_01UwN31QccH98YV9eEc2bue2