Skip to content

fix: prevent RangeError in Proj4Crs.zoom for out-of-range scales#2209

Draft
AlexLaroche wants to merge 1 commit into
fleaflet:masterfrom
AlexLaroche:fix/proj4crs-zoom-out-of-range
Draft

fix: prevent RangeError in Proj4Crs.zoom for out-of-range scales#2209
AlexLaroche wants to merge 1 commit into
fleaflet:masterfrom
AlexLaroche:fix/proj4crs-zoom-out-of-range

Conversation

@AlexLaroche
Copy link
Copy Markdown

Summary

Fixes a RangeError thrown by Proj4Crs.zoom when the requested scale is finer than the deepest level defined in the CRS's resolutions list. Closes #1223 (which has documented this exact crash since 2022).

Root cause

In lib/src/geo/crs.dart:

final downScale = _closestElement(_scales, scale);     // returns _scales.last when scale > all entries
final downZoom = _scales.indexOf(downScale);           // = _scales.length - 1
// ...
final nextZoom = downZoom + 1;                         // = _scales.length
final nextScale = _scales[nextZoom];                   // RangeError

When the caller asks for a scale beyond the largest defined level, _closestElement returns _scales.last, so downZoom is the last index. The interpolation block then indexes _scales[downZoom + 1] and throws.

The most common path that hits this is FitBounds._getBoundsZoom, which calls camera.getScaleZoom(scale) before clamping the result to maxZoom (camera_fit.dart:162 vs :176). So setting maxZoom on MapOptions / CameraFit.bounds does not protect against the crash — it crashes inside Proj4Crs.zoom before the clamp can run. This is also why issue #1223's reproduction is fitBounds with a custom CRS.

Fix

When downZoom == _scales.length - 1 and the requested scale is strictly larger than downScale, return double.infinity instead of falling through to the out-of-bounds index access.

This mirrors the existing lower-bound behavior in the same function: when the requested scale is smaller than every entry in _scales, the function already returns double.negativeInfinity. The fix makes the upper bound symmetric.

Callers like FitBounds._getBoundsZoom already .clamp(min, max) the returned zoom, so +∞ becomes maxZoom and the camera settles correctly. The whole frame no longer throws.

Why double.infinity rather than clamping to _scales.length - 1?

  • Symmetric with the existing double.negativeInfinity for the lower bound.
  • Lets the caller decide the clamp policy — FitBounds already has its own maxZoom that may be lower than the deepest CRS level.
  • Unambiguous sentinel: distinguishes "scale exactly at deepest level" from "scale beyond all defined levels".

Happy to switch to a finite clamp if reviewers prefer.

Test plan

  • New regression test in test/geo/crs_test.dart covers exact match, interpolation, lower-bound -∞, and upper-bound +∞.
  • Verified the new +∞ test reproduces the original RangeError at crs.dart:356:30 when the fix is reverted.
  • flutter test test/geo/ test/flutter_map_test.dart — all 20 tests pass.
  • flutter analyze lib/src/geo/crs.dart test/geo/crs_test.dart — no issues.
  • dart format clean on changed files.

When the requested scale is larger than the largest entry in the CRS's
`_scales`, `_closestElement` returns the last element, so `downZoom` is
`_scales.length - 1`. Falling through to the interpolation block then
indexes `_scales[downZoom + 1]` and throws `RangeError`.

Return `double.infinity` in that case, mirroring the
`double.negativeInfinity` already returned when the requested scale is
smaller than every defined level. Callers such as
`FitBounds._getBoundsZoom` then clamp via `.clamp(min, max)` and the
camera ends up at `maxZoom`, instead of the whole frame throwing.

Closes fleaflet#1223.
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.

[BUG] fitBounds surpasses maxZoom param when using custom CRS

1 participant