diff --git a/.gitignore b/.gitignore index a0ba06750..bd908840f 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,6 @@ pubspec.lock !**/ios/**/default.mode2v3 !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 + +# Coverage files +coverage/ diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index e5e3be000..4dbdce2c5 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -26,9 +26,9 @@ class AnimatedMapControllerPageState extends State // See https://github.com/flutter/flutter/issues/14317#issuecomment-361085869 // This project didn't require that change, so YMMV. - static LatLng london = LatLng(51.5, -0.09); - static LatLng paris = LatLng(48.8566, 2.3522); - static LatLng dublin = LatLng(53.3498, -6.2603); + static final london = LatLng(51.5, -0.09); + static final paris = LatLng(48.8566, 2.3522); + static final dublin = LatLng(53.3498, -6.2603); late final MapController mapController; @@ -142,10 +142,12 @@ class AnimatedMapControllerPageState extends State children: [ MaterialButton( onPressed: () { - final bounds = LatLngBounds(); - bounds.extend(dublin); - bounds.extend(paris); - bounds.extend(london); + final bounds = LatLngBounds.fromPoints([ + dublin, + paris, + london, + ]); + mapController.fitBounds( bounds, options: const FitBoundsOptions( @@ -157,10 +159,11 @@ class AnimatedMapControllerPageState extends State ), MaterialButton( onPressed: () { - final bounds = LatLngBounds(); - bounds.extend(dublin); - bounds.extend(paris); - bounds.extend(london); + final bounds = LatLngBounds.fromPoints([ + dublin, + paris, + london, + ]); final centerZoom = mapController.centerZoomFitBounds(bounds); diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index f16e288bd..7a3b63853 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -102,10 +102,12 @@ class MapControllerPageState extends State { children: [ MaterialButton( onPressed: () { - final bounds = LatLngBounds(); - bounds.extend(dublin); - bounds.extend(paris); - bounds.extend(london); + final bounds = LatLngBounds.fromPoints([ + dublin, + paris, + london, + ]); + _mapController.fitBounds( bounds, options: const FitBoundsOptions( diff --git a/lib/src/geo/latlng_bounds.dart b/lib/src/geo/latlng_bounds.dart index 4d0174868..7b228c5bb 100644 --- a/lib/src/geo/latlng_bounds.dart +++ b/lib/src/geo/latlng_bounds.dart @@ -5,19 +5,15 @@ import 'package:latlong2/latlong.dart'; /// Data structure representing rectangular bounding box constrained by its /// northwest and southeast corners class LatLngBounds { - LatLng? _sw; - LatLng? _ne; + late final LatLng _sw; + late final LatLng _ne; - LatLngBounds([LatLng? corner1, LatLng? corner2]) { - extend(corner1); - extend(corner2); - } - - LatLngBounds.fromPoints(List points) { - if (points.isEmpty) { - return; - } + LatLngBounds( + LatLng corner1, + LatLng corner2, + ) : this.fromPoints([corner1, corner2]); + LatLngBounds.fromPoints(List points) : assert(points.isNotEmpty) { double minX = 180; double maxX = -180; double minY = 90; @@ -50,10 +46,7 @@ class LatLngBounds { /// Expands bounding box by [latlng] coordinate point. This method mutates /// the bounds object on which it is called. - void extend(LatLng? latlng) { - if (latlng == null) { - return; - } + void extend(LatLng latlng) { _extend(latlng, latlng); } @@ -64,35 +57,30 @@ class LatLngBounds { _extend(bounds._sw, bounds._ne); } - void _extend(LatLng? sw2, LatLng? ne2) { - if (_sw == null && _ne == null) { - _sw = LatLng(sw2!.latitude, sw2.longitude); - _ne = LatLng(ne2!.latitude, ne2.longitude); - } else { - _sw!.latitude = math.min(sw2!.latitude, _sw!.latitude); - _sw!.longitude = math.min(sw2.longitude, _sw!.longitude); - _ne!.latitude = math.max(ne2!.latitude, _ne!.latitude); - _ne!.longitude = math.max(ne2.longitude, _ne!.longitude); - } + void _extend(LatLng sw2, LatLng ne2) { + _sw.latitude = math.min(sw2.latitude, _sw.latitude); + _sw.longitude = math.min(sw2.longitude, _sw.longitude); + _ne.latitude = math.max(ne2.latitude, _ne.latitude); + _ne.longitude = math.max(ne2.longitude, _ne.longitude); } /// Obtain west edge of the bounds - double get west => southWest!.longitude; + double get west => southWest.longitude; /// Obtain south edge of the bounds - double get south => southWest!.latitude; + double get south => southWest.latitude; /// Obtain east edge of the bounds - double get east => northEast!.longitude; + double get east => northEast.longitude; /// Obtain north edge of the bounds - double get north => northEast!.latitude; + double get north => northEast.latitude; /// Obtain coordinates of southwest corner of the bounds - LatLng? get southWest => _sw; + LatLng get southWest => _sw; /// Obtain coordinates of northeast corner of the bounds - LatLng? get northEast => _ne; + LatLng get northEast => _ne; /// Obtain coordinates of northwest corner of the bounds LatLng get northWest => LatLng(north, west); @@ -112,12 +100,12 @@ class LatLngBounds { lambda: lng */ - final phi1 = southWest!.latitudeInRad; - final lambda1 = southWest!.longitudeInRad; - final phi2 = northEast!.latitudeInRad; + final phi1 = southWest.latitudeInRad; + final lambda1 = southWest.longitudeInRad; + final phi2 = northEast.latitudeInRad; - final dLambda = degToRadian(northEast!.longitude - - southWest!.longitude); // delta lambda = lambda2-lambda1 + final dLambda = degToRadian(northEast.longitude - + southWest.longitude); // delta lambda = lambda2-lambda1 final bx = math.cos(phi2) * math.cos(dLambda); final by = math.cos(phi2) * math.sin(dLambda); @@ -130,15 +118,12 @@ class LatLngBounds { } /// Checks whether bound object is valid - bool get isValid { - return _sw != null && _ne != null; - } + /// TODO: remove this property in the next major release. + @Deprecated('This method is unnecessary and will be removed in the future.') + bool get isValid => true; /// Checks whether [point] is inside bounds - bool contains(LatLng? point) { - if (!isValid) { - return false; - } + bool contains(LatLng point) { final sw2 = point; final ne2 = point; return containsBounds(LatLngBounds(sw2, ne2)); @@ -146,34 +131,31 @@ class LatLngBounds { /// Checks whether [bounds] is contained inside bounds bool containsBounds(LatLngBounds bounds) { - final sw2 = bounds._sw!; + final sw2 = bounds._sw; final ne2 = bounds._ne; - return (sw2.latitude >= _sw!.latitude) && - (ne2!.latitude <= _ne!.latitude) && - (sw2.longitude >= _sw!.longitude) && - (ne2.longitude <= _ne!.longitude); + return (sw2.latitude >= _sw.latitude) && + (ne2.latitude <= _ne.latitude) && + (sw2.longitude >= _sw.longitude) && + (ne2.longitude <= _ne.longitude); } /// Checks whether at least one edge of [bounds] is overlapping with some /// other edge of bounds - bool isOverlapping(LatLngBounds? bounds) { - if (!isValid) { - return false; - } + bool isOverlapping(LatLngBounds bounds) { /* check if bounding box rectangle is outside the other, if it is then it's considered not overlapping */ - if (_sw!.latitude > bounds!._ne!.latitude || - _ne!.latitude < bounds._sw!.latitude || - _ne!.longitude < bounds._sw!.longitude || - _sw!.longitude > bounds._ne!.longitude) { + if (_sw.latitude > bounds._ne.latitude || + _ne.latitude < bounds._sw.latitude || + _ne.longitude < bounds._sw.longitude || + _sw.longitude > bounds._ne.longitude) { return false; } return true; } - + @override - int get hashCode => _sw.hashCode + _ne.hashCode; + int get hashCode => Object.hash(_sw, _ne); @override bool operator ==(Object other) => diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index bdb822b18..3fd7b044a 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -700,8 +700,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { Bounds _latLngBoundsToPixelBounds( FlutterMapState map, LatLngBounds bounds, double thisZoom) { - final swPixel = map.project(bounds.southWest!, thisZoom).floor(); - final nePixel = map.project(bounds.northEast!, thisZoom).ceil(); + final swPixel = map.project(bounds.southWest, thisZoom).floor(); + final nePixel = map.project(bounds.northEast, thisZoom).ceil(); final pxBounds = Bounds(swPixel, nePixel); return pxBounds; } diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index ef1e73135..005676335 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -421,7 +421,7 @@ class FlutterMapState extends MapGestureMixin newZoom = fitZoomToBounds(newZoom); final mapMoved = newCenter != _center || newZoom != _zoom; - if (!mapMoved || !_calculateBounds().isValid) { + if (!mapMoved) { return false; } @@ -483,18 +483,12 @@ class FlutterMapState extends MapGestureMixin } void fitBounds(LatLngBounds bounds, FitBoundsOptions options) { - if (!bounds.isValid) { - throw Exception('Bounds are not valid.'); - } final target = getBoundsCenterZoom(bounds, options); move(target.center, target.zoom, source: MapEventSource.fitBounds); } CenterZoom centerZoomFitBounds( LatLngBounds bounds, FitBoundsOptions options) { - if (!bounds.isValid) { - throw Exception('Bounds are not valid.'); - } return getBoundsCenterZoom(bounds, options); } @@ -523,8 +517,8 @@ class FlutterMapState extends MapGestureMixin zoom = math.min(options.maxZoom, zoom); final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = project(bounds.southWest!, zoom); - final nePoint = project(bounds.northEast!, zoom); + final swPoint = project(bounds.southWest, zoom); + final nePoint = project(bounds.northEast, zoom); final center = unproject((swPoint + nePoint) / 2 + paddingOffset, zoom); return CenterZoom( center: center, @@ -608,8 +602,8 @@ class FlutterMapState extends MapGestureMixin LatLng testCenter, double testZoom, LatLngBounds maxBounds) { LatLng? newCenter; - final swPixel = project(maxBounds.southWest!, testZoom); - final nePixel = project(maxBounds.northEast!, testZoom); + final swPixel = project(maxBounds.southWest, testZoom); + final nePixel = project(maxBounds.northEast, testZoom); final centerPix = project(testCenter, testZoom); @@ -729,14 +723,12 @@ class FlutterMapState extends MapGestureMixin double? _safeAreaZoom; //if there is a pan boundary, do not cross - bool isOutOfBounds(LatLng? center) { + bool isOutOfBounds(LatLng center) { if (options.adaptiveBoundaries) { return !_safeArea!.contains(center); } if (options.swPanBoundary != null && options.nePanBoundary != null) { - if (center == null) { - return true; - } else if (center.latitude < options.swPanBoundary!.latitude || + if (center.latitude < options.swPanBoundary!.latitude || center.latitude > options.nePanBoundary!.latitude) { return true; } else if (center.longitude < options.swPanBoundary!.longitude || @@ -817,7 +809,7 @@ class _SafeArea { isLatitudeBlocked = southWest.latitude > northEast.latitude, isLongitudeBlocked = southWest.longitude > northEast.longitude; - bool contains(LatLng? point) => + bool contains(LatLng point) => isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); LatLng containPoint(LatLng point, LatLng fallback) => LatLng( diff --git a/test/geo/latlng_bounds_test.dart b/test/geo/latlng_bounds_test.dart new file mode 100644 index 000000000..a4243d96d --- /dev/null +++ b/test/geo/latlng_bounds_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_map/src/geo/latlng_bounds.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:latlong2/latlong.dart'; + +void main() { + group('LatLngBounds', () { + final london = LatLng(51.5, -0.09); + final paris = LatLng(48.8566, 2.3522); + final dublin = LatLng(53.3498, -6.2603); + + group('LatLngBounds constructor', () { + test('with dublin, paris', () { + final bounds = LatLngBounds(dublin, paris); + + expect(bounds, LatLngBounds.fromPoints([dublin, paris])); + }); + }); + + group('LatLngBounds.fromPoints', () { + test('throw AssertionError if points is empty', () { + expect(() => LatLngBounds.fromPoints([]), throwsAssertionError); + }); + + test('with dublin, paris, london', () { + final bounds = LatLngBounds.fromPoints([ + dublin, + paris, + london, + ]); + + final sw = bounds.southWest; + final ne = bounds.northEast; + + expect(sw.latitude, 48.8566); + expect(sw.longitude, -6.2603); + expect(ne.latitude, 53.3498); + expect(ne.longitude, 2.3522); + }); + }); + + group('hashCode', () { + test('with dublin, paris', () { + final bounds1 = LatLngBounds(dublin, paris); + final bounds2 = LatLngBounds(dublin, paris); + + expect(bounds1 == bounds2, isTrue); + expect(bounds1.hashCode, bounds2.hashCode); + }); + }); + }); +}