Skip to content

fix: sanitise non-finite zoom in MapCamera.clampZoom#2228

Closed
Pistacchione wants to merge 1 commit into
fleaflet:masterfrom
Pistacchione:fix/nan-zoom-clamp
Closed

fix: sanitise non-finite zoom in MapCamera.clampZoom#2228
Pistacchione wants to merge 1 commit into
fleaflet:masterfrom
Pistacchione:fix/nan-zoom-clamp

Conversation

@Pistacchione

Copy link
Copy Markdown

What & why

During a rapid pinch zoom-out, the gesture handler calls _getZoomForScale,
which computes startZoom + math.log(scale) / math.ln2. When
details.scale + _scaleCorrector momentarily becomes non-positive (the scale
corrector can be negative), math.log returns NaN or -Infinity.

clampZoom relied on num.clamp, which returns NaN unchanged for a NaN
input and therefore cannot sanitise it. The invalid zoom was committed to the
camera and propagated into:

  • TileRangeCalculatorhalfSize = size / (scale * 2) becomes non-finite
    and DiscreteTileRange.fromPixelBounds throws
    Unsupported operation: Infinity or NaN toInt from .floor().
  • MarkerClusterLayer — the visible bounds become NaN, tripping the
    north <= maxLatitude assertion.

The crash is transient/self-healing (the next finite scale update recomputes a
valid zoom), but each bad frame surfaces as an uncaught framework error.

Fix

clampZoom now treats any non-finite input as an invalid update and returns the
current (finite) zoom, keeping the camera in a valid state. Being the single
chokepoint every zoom mutation passes through (gestures and moveRaw), this
covers the whole class of NaN-zoom bugs. It also fixes the minZoom == null
case, where a -Infinity zoom crashed as well.

Tests

Added test/map/camera/camera_test.dart covering the non-finite guard
(NaN/±Infinity), normal min/max clamping, and the no-min/max case. The
existing camera/ tests still pass.

Notes

AI disclosure

This change was written with the assistance of AI (Claude).

During a rapid pinch zoom-out the gesture handler calls
`_getZoomForScale`, which computes `startZoom + math.log(scale) / ln2`.
When `details.scale + _scaleCorrector` momentarily becomes non-positive
(the scale corrector can be negative), `math.log` returns `NaN` or
`-Infinity`. `clampZoom` used `num.clamp`, which returns `NaN` unchanged
for a `NaN` input and cannot sanitise it, so the invalid zoom was
committed to the camera. It then propagated into:

- `TileRangeCalculator`, where `halfSize = size / (scale * 2)` becomes
  non-finite and `DiscreteTileRange.fromPixelBounds` throws
  `Unsupported operation: Infinity or NaN toInt` from `.floor()`;
- `MarkerClusterLayer`, whose visible bounds become `NaN`, tripping the
  `north <= maxLatitude` assertion.

`clampZoom` now treats any non-finite input as an invalid update and
returns the current (finite) zoom, keeping the camera in a valid state.
This also covers the `minZoom == null` case, where a `-Infinity` zoom
previously crashed as well.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JaffaKetchup

Copy link
Copy Markdown
Member

Please open a bug report with an MRE. Ensure you test to see whether your original issue is resolved on the latest master.

As is, this PR does not fix anything and instead just buries the problem, if there is one.

@Pistacchione

Copy link
Copy Markdown
Author

Hi @JaffaKetchup

You were right, thanks for the pointer to test against master.
That's already fixed on master by #2220 (issue #2199) via the velocityDirection fallback in flingDirection, and I've confirmed the crash no longer occurs when pinning to master, so this was never a clampZoom problem.
My PR was masking an already-solved issue, as you said.

Closing the loop here; the PR can stay closed. Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants