diff --git a/example/lib/main.dart b/example/lib/main.dart index 7cf4012d0..19523df7d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -7,11 +7,13 @@ import './pages/circle.dart'; import './pages/custom_crs/custom_crs.dart'; import './pages/esri.dart'; import './pages/home.dart'; +import './pages/interactive_test_page.dart'; import './pages/live_location.dart'; import './pages/many_markers.dart'; import './pages/map_controller.dart'; import './pages/marker_anchor.dart'; import './pages/marker_rotate.dart'; +import './pages/max_bounds.dart'; import './pages/moving_markers.dart'; import './pages/offline_map.dart'; import './pages/on_tap.dart'; @@ -29,7 +31,6 @@ import './pages/tile_builder_example.dart'; import './pages/tile_loading_error_handle.dart'; import './pages/widgets.dart'; import './pages/wms_tile_layer.dart'; -import 'pages/interactive_test_page.dart'; void main() => runApp(MyApp()); @@ -74,6 +75,7 @@ class MyApp extends StatelessWidget { StatefulMarkersPage.route: (context) => StatefulMarkersPage(), MapInsideListViewPage.route: (context) => MapInsideListViewPage(), ResetTileLayerPage.route: (context) => ResetTileLayerPage(), + MaxBoundsPage.route: (context) => MaxBoundsPage() }, ); } diff --git a/example/lib/pages/max_bounds.dart b/example/lib/pages/max_bounds.dart new file mode 100644 index 000000000..cacf9fe81 --- /dev/null +++ b/example/lib/pages/max_bounds.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +import '../widgets/drawer.dart'; + +class MaxBoundsPage extends StatelessWidget { + static const String route = '/max_bounds'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('Max Bounds edges check')), + drawer: buildDrawer(context, route), + body: Padding( + padding: EdgeInsets.all(8.0), + child: Column( + children: [ + Padding( + padding: EdgeInsets.only(top: 8.0, bottom: 8.0), + child: Text( + 'This is a map that has edges constrained to a latlng bounds.'), + ), + Flexible( + child: FlutterMap( + options: MapOptions( + center: LatLng(56.704173, 11.543808), + zoom: 3.0, + maxBounds: + LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)), + screenSize: MediaQuery.of(context).size, + ), + layers: [ + TileLayerOptions( + maxZoom: 15.0, + urlTemplate: + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/example/lib/widgets/drawer.dart b/example/lib/widgets/drawer.dart index 064a07325..f1b98689e 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -13,6 +13,7 @@ import '../pages/live_location.dart'; import '../pages/many_markers.dart'; import '../pages/map_controller.dart'; import '../pages/marker_anchor.dart'; +import '../pages/max_bounds.dart'; import '../pages/moving_markers.dart'; import '../pages/offline_map.dart'; import '../pages/on_tap.dart'; @@ -213,6 +214,12 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { InteractiveTestPage.route, currentRoute, ), + _buildMenuItem( + context, + const Text('Max Bounds test page'), + MaxBoundsPage.route, + currentRoute, + ), ListTile( title: const Text('A lot of markers'), selected: currentRoute == ManyMarkersPage.route, diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index bf9ffcf0f..40d49f95d 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -251,6 +251,11 @@ class MapOptions { final LatLng? swPanBoundary; final LatLng? nePanBoundary; + /// Restrict outer edges of map to LatLng Bounds, to prevent gray areas when + /// panning or zooming. LatLngBounds(LatLng(-90, -180.0), LatLng(90.0, 180.0)) + /// would represent the full extent of the map, so no gray area outside of it. + final LatLngBounds? maxBounds; + _SafeArea? _safeAreaCache; double? _safeAreaZoom; @@ -288,6 +293,7 @@ class MapOptions { this.controller, this.swPanBoundary, this.nePanBoundary, + this.maxBounds, }) : center = center ?? LatLng(50.5, 30.51), assert(rotationThreshold >= 0.0), assert(pinchZoomThreshold >= 0.0), diff --git a/lib/src/map/map.dart b/lib/src/map/map.dart index 3c56ac171..6823f0470 100644 --- a/lib/src/map/map.dart +++ b/lib/src/map/map.dart @@ -316,6 +316,18 @@ class MapState { center = options.containPoint(center, _lastCenter ?? center); } + // Try and fit the corners of the map inside the visible area. + // If it's still outside (so response is null), don't perform a move. + if (options.maxBounds != null) { + var adjustedCenter = + adjustCenterIfOutsideMaxBounds(center, zoom, options.maxBounds); + if (adjustedCenter == null) { + return false; + } else { + center = adjustedCenter; + } + } + _handleMoveEmit(center, zoom, hasGesture, source, id); _zoom = zoom; @@ -489,6 +501,69 @@ class MapState { return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); } + LatLng? adjustCenterIfOutsideMaxBounds( + LatLng testCenter, testZoom, maxBounds) { + LatLng? newCenter; + + var swPixel = project(maxBounds.southWest!, testZoom); + var nePixel = project(maxBounds.northEast!, testZoom); + + var centerPix = project(testCenter, testZoom); + + var halfSizeX = size.x / 2; + var halfSizeY = size.y / 2; + + // Try and find the edge value that the center could use to stay within + // the maxBounds. This should be ok for panning. If we zoom, it is possible + // there is no solution to keep all corners within the bounds. If the edges + // are still outside the bounds, don't return anything. + var leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; + var rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; + var topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; + var botOkCenter = math.max(swPixel.y, nePixel.y) - halfSizeY; + + double? newCenterX; + double? newCenterY; + + var wasAdjusted = false; + + if (centerPix.x < leftOkCenter) { + wasAdjusted = true; + newCenterX = leftOkCenter; + } else if (centerPix.x > rightOkCenter) { + wasAdjusted = true; + newCenterX = rightOkCenter; + } + + if (centerPix.y < topOkCenter) { + wasAdjusted = true; + newCenterY = topOkCenter; + } else if (centerPix.y > botOkCenter) { + wasAdjusted = true; + newCenterY = botOkCenter; + } + + if (!wasAdjusted) { + return testCenter; + } + + var newCx = newCenterX ?? centerPix.x; + var newCy = newCenterY ?? centerPix.y; + + // Have a final check, see if the adjusted center is within maxBounds. + // If not, give up. + if (newCx < leftOkCenter || + newCx > rightOkCenter || + newCy < topOkCenter || + newCy > botOkCenter) { + return null; + } else { + newCenter = unproject(CustomPoint(newCx, newCy), testZoom); + } + + return newCenter; + } + static MapState? maybeOf(BuildContext context, {bool nullOk = false}) { final widget = context.dependOnInheritedWidgetOfExactType();