Skip to content

Investigate NaN being produced internally during CRS work #2219

Open

Description

@JaffaKetchup

During the timeline below, it was hinted at in #2208 by @maheshj01, then investigated by @mootw, that NaN was being produced internally by the following method in MapCamera:

double getZoomScale(double toZoom, double fromZoom) =>
crs.scale(toZoom) / crs.scale(fromZoom);

It appears that when this method is called with identical arguments, the result is NaN? However, for some reason, before #2182, this did not cause issues. This needs to be investigated.

The method itself also needs to be investigated - why is it the shape that it is? See comments in #2199 for more information. Does it make more sense for the method to return 1 instead of NaN or 0 when the inputs are the same?

Internal link to start of discussion for @fleaflet/maintainers only: https://discord.com/channels/951867686378409984/951879268227485707/1521558461819916458

Timeline:

  1. LatLng with double.nan in Marker/MarkerLayer triggers memory leak #2178 was opened, reporting that Markers with NaN points caused memory leaks and crashes
  2. fix: throw when LatLng is non-finite #2182 was opened and merged (resolving LatLng with double.nan in Marker/MarkerLayer triggers memory leak #2178), which introduced a check for NaN during all projections from LatLngs - this aimed to uncover the memory leak and throw instead, which was deemed an acceptable solution
  3. Many interactions throw "LatLng is not finite" error #2199 was opened, reporting that the new check triggered unexpectedly during many typical and normal interactions and configurations
  4. fix: LatLng is not finite exception #2208 was opened, which fixed some cases of the NaN being produced internally, partially fixing Many interactions throw "LatLng is not finite" error #2199
  5. perf: cache zoom-independent marker projections in MarkerLayer #2213 was merged, which (in addition to unrelated issues) introduced a check for NaN on the MarkerLayer only, double fixing LatLng with double.nan in Marker/MarkerLayer triggers memory leak #2178 with a tighter scope
  6. revert: fix: throw when LatLng is non-finite #2218 was opened and merged, to revert fix: throw when LatLng is non-finite #2182, close fix: LatLng is not finite exception #2208, and fix Many interactions throw "LatLng is not finite" error #2199 - this would've reopened LatLng with double.nan in Marker/MarkerLayer triggers memory leak #2178, but perf: cache zoom-independent marker projections in MarkerLayer #2213 fixed it anyway

Activity

  1. added theissue type on Jun 30, 2026
  2. aki1770-del commented on Jul 3, 2026

    @aki1770-del

    Hi @JaffaKetchup — took a look since this is squarely in an area I work in (a snow-driving nav stack on flutter_map).

    Root cause: getZoomScale(to, from) is crs.scale(to) / crs.scale(from). The default Epsg3857 extends CrsWithStaticTransformation, which doesn't override scale(), so it uses the base Crs.scale = 256 * math.pow(2, zoom). For normal zooms that's a finite number, so getZoomScale(z, z) is a clean 1.0 — the common case is fine. It only degenerates to NaN when scale(z) itself is 0, ±∞, or NaN (i.e. x/x with a degenerate x). Reproduced against the current code:

    scale(1017)  = Infinity   →  getZoomScale(1017, 1017)   = NaN   (∞/∞)
    scale(-1100) = 0.0        →  getZoomScale(-1100, -1100) = NaN   (0/0)
    scale(NaN)   = NaN        →  getZoomScale(NaN, NaN)     = NaN
    getZoomScale(14, 14)      = 1.0                                (normal — fine)
    

    On your suggestion (return 1 when the inputs are the same): it fixes the finite-extreme case and also catches ±infinity (since inf == inf is true) — but it misses the NaN case, because NaN == NaN is false in Dart, so the to == from guard is skipped and the NaN falls through. A complete guard would also short-circuit non-finite inputs (if (!toZoom.isFinite || !fromZoom.isFinite) return 1;).

    That said, the more interesting question is where a NaN/extreme zoom comes from — getZoomScale(z, z) can't be NaN for a normal z (it's always 1.0), so the NaN-during-interaction reports (e.g. #2199) suggest a NaN or runaway zoom is entering the camera upstream, probably from gesture/animation math, and getZoomScale is just where it first surfaces. A guard here stops the crash; tracing the upstream NaN would stop it at the source.

    Happy to put up a small PR with the guard + a regression test (I've got a local repro covering the identical-args finite/∞/NaN cases) if that'd be useful — or leave it to you. Whatever's easiest.

  3. updated Priority Medium and Effort High on Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Fields

Priority

Medium

Effort

High

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions