diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index 252080994..a8ddc43b3 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -647,10 +647,13 @@ class MapInteractiveViewerState extends State var newZoom = _camera.zoom; // Handle pinch zoom. - if (hasPinchZoom && details.scale > 0.0) { + // After a gesture-race win, _scaleCorrector is 1 - lastScale, so this + // sum can be <= 0 even when details.scale itself is positive. + final scale = details.scale + _scaleCorrector; + if (hasPinchZoom && scale > 0.0) { newZoom = _getZoomForScale( _mapZoomStart, - details.scale + _scaleCorrector, + scale, ); // Handle starting of pinch zoom. @@ -1401,10 +1404,19 @@ class MapInteractiveViewerState extends State // Utilities + /// Converts a pinch [scale] into a zoom level starting from [startZoom]. + /// + /// Returns [startZoom] when [scale] is not a positive finite number. + /// `math.log` is undefined for those inputs and would produce `NaN` or + /// `-Infinity`, which then poison the camera. + @visibleForTesting + static double zoomForScale(double startZoom, double scale) { + if (scale <= 0.0 || !scale.isFinite) return startZoom; + return scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; + } + double _getZoomForScale(double startZoom, double scale) { - final resultZoom = - scale == 1.0 ? startZoom : startZoom + math.log(scale) / math.ln2; - return _camera.clampZoom(resultZoom); + return _camera.clampZoom(zoomForScale(startZoom, scale)); } Offset _rotateOffset(Offset offset) { diff --git a/test/gestures/map_interactive_viewer_test.dart b/test/gestures/map_interactive_viewer_test.dart index b7ee28728..1dce29355 100644 --- a/test/gestures/map_interactive_viewer_test.dart +++ b/test/gestures/map_interactive_viewer_test.dart @@ -46,4 +46,42 @@ void main() { }, ); }); + + group('MapInteractiveViewerState.zoomForScale', () { + test('adds log2(scale) to the start zoom', () { + expect(MapInteractiveViewerState.zoomForScale(13, 2), 14); + expect(MapInteractiveViewerState.zoomForScale(13, 1), 13); + expect(MapInteractiveViewerState.zoomForScale(13, 0.5), 12); + }); + + test( + 'returns the start zoom when the corrected pinch scale is not ' + 'positive (regression: details.scale + _scaleCorrector can be ' + '<= 0 after a gesture-race win, and math.log of that is NaN - ' + 'https://github.com/fleaflet/flutter_map/issues/2237)', + () { + // _scaleCorrector = 1.0 - _lastScale, with _lastScale == 2.5 + const scaleCorrector = 1.0 - 2.5; + const detailsScale = 1; + final scale = detailsScale + scaleCorrector; + + expect(scale, lessThanOrEqualTo(0)); + + final zoom = MapInteractiveViewerState.zoomForScale(13, scale); + + expect(zoom, 13); + expect(zoom.isFinite, isTrue); + expect(zoom.isNaN, isFalse); + }, + ); + + test('returns the start zoom for zero, NaN, and infinite scale', () { + expect(MapInteractiveViewerState.zoomForScale(13, 0), 13); + expect(MapInteractiveViewerState.zoomForScale(13, double.nan), 13); + expect( + MapInteractiveViewerState.zoomForScale(13, double.negativeInfinity), + 13, + ); + }); + }); }