diff --git a/example/lib/main.dart b/example/lib/main.dart index d540dbc0c..f72ec1760 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -21,7 +21,6 @@ import 'package:flutter_map_example/pages/network_tile_provider.dart'; import 'package:flutter_map_example/pages/offline_map.dart'; import 'package:flutter_map_example/pages/on_tap.dart'; import 'package:flutter_map_example/pages/overlay_image.dart'; -import 'package:flutter_map_example/pages/plugin_api.dart'; import 'package:flutter_map_example/pages/plugin_scalebar.dart'; import 'package:flutter_map_example/pages/plugin_zoombuttons.dart'; import 'package:flutter_map_example/pages/point_to_latlng.dart'; @@ -61,7 +60,6 @@ class MyApp extends StatelessWidget { AnimatedMapControllerPage.route: (context) => const AnimatedMapControllerPage(), MarkerAnchorPage.route: (context) => const MarkerAnchorPage(), - PluginPage.route: (context) => const PluginPage(), PluginScaleBar.route: (context) => const PluginScaleBar(), PluginZoomButtons.route: (context) => const PluginZoomButtons(), OfflineMapPage.route: (context) => const OfflineMapPage(), diff --git a/example/lib/pages/animated_map_controller.dart b/example/lib/pages/animated_map_controller.dart index 1c661f193..74679c821 100644 --- a/example/lib/pages/animated_map_controller.dart +++ b/example/lib/pages/animated_map_controller.dart @@ -179,14 +179,14 @@ class AnimatedMapControllerPageState extends State zoom: 5, maxZoom: 10, minZoom: 3), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers), ], ), ), diff --git a/example/lib/pages/circle.dart b/example/lib/pages/circle.dart index 9b40c8fcf..b6c09c080 100644 --- a/example/lib/pages/circle.dart +++ b/example/lib/pages/circle.dart @@ -37,14 +37,14 @@ class CirclePage extends StatelessWidget { center: LatLng(51.5, -0.09), zoom: 11, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - CircleLayerOptions(circles: circleMarkers) + CircleLayer(circles: circleMarkers), ], ), ), diff --git a/example/lib/pages/custom_crs/custom_crs.dart b/example/lib/pages/custom_crs/custom_crs.dart index ade2ec8fa..f190f9e00 100644 --- a/example/lib/pages/custom_crs/custom_crs.dart +++ b/example/lib/pages/custom_crs/custom_crs.dart @@ -137,8 +137,8 @@ class _CustomCrsPageState extends State { point = proj4.Point(x: p.latitude, y: p.longitude); }), ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( opacity: 1, backgroundColor: Colors.transparent, wmsOptions: WMSTileLayerOptions( diff --git a/example/lib/pages/epsg3413_crs.dart b/example/lib/pages/epsg3413_crs.dart index 14718d9ce..43ea26d6a 100644 --- a/example/lib/pages/epsg3413_crs.dart +++ b/example/lib/pages/epsg3413_crs.dart @@ -133,8 +133,8 @@ class _EPSG3413PageState extends State { zoom: 3, maxZoom: maxZoom, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( opacity: 1, backgroundColor: Colors.transparent, wmsOptions: WMSTileLayerOptions( @@ -146,7 +146,7 @@ class _EPSG3413PageState extends State { layers: ['gebco_north_polar_view'], ), ), - OverlayImageLayerOptions( + OverlayImageLayer( overlayImages: [ OverlayImage( bounds: LatLngBounds( @@ -159,9 +159,7 @@ class _EPSG3413PageState extends State { ) ], ), - CircleLayerOptions( - circles: circles, - ), + CircleLayer(circles: circles), ], ), ), diff --git a/example/lib/pages/epsg4326_crs.dart b/example/lib/pages/epsg4326_crs.dart index 0f4d3103c..7bd3f8664 100644 --- a/example/lib/pages/epsg4326_crs.dart +++ b/example/lib/pages/epsg4326_crs.dart @@ -29,8 +29,8 @@ class EPSG4326Page extends StatelessWidget { center: LatLng(0, 0), zoom: 0, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( wmsOptions: WMSTileLayerOptions( crs: const Epsg4326(), baseUrl: 'https://ows.mundialis.de/services/service?', diff --git a/example/lib/pages/esri.dart b/example/lib/pages/esri.dart index 417dff46f..bc5245bc1 100644 --- a/example/lib/pages/esri.dart +++ b/example/lib/pages/esri.dart @@ -27,8 +27,8 @@ class EsriPage extends StatelessWidget { center: LatLng(45.5231, -122.6765), zoom: 13, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', userAgentPackageName: 'dev.fleaflet.flutter_map.example', diff --git a/example/lib/pages/home.dart b/example/lib/pages/home.dart index f12ef4e49..54b526b97 100644 --- a/example/lib/pages/home.dart +++ b/example/lib/pages/home.dart @@ -57,21 +57,21 @@ class HomePage extends StatelessWidget { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( - urlTemplate: - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: ['a', 'b', 'c'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - MarkerLayerOptions(markers: markers) - ], nonRotatedChildren: [ AttributionWidget.defaultWidget( source: 'OpenStreetMap contributors', onSourceTapped: () {}, ), ], + children: [ + TileLayer( + urlTemplate: + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + MarkerLayer(markers: markers), + ], ), ), ], diff --git a/example/lib/pages/interactive_test_page.dart b/example/lib/pages/interactive_test_page.dart index f04ad46db..db566a894 100644 --- a/example/lib/pages/interactive_test_page.dart +++ b/example/lib/pages/interactive_test_page.dart @@ -17,26 +17,15 @@ class InteractiveTestPage extends StatefulWidget { } class _InteractiveTestPageState extends State { - late final MapController mapController; // Enable pinchZoom and doubleTapZoomBy by default int flags = InteractiveFlag.pinchZoom | InteractiveFlag.doubleTapZoom; - late final StreamSubscription subscription; + MapEvent? _latestEvent; @override void initState() { super.initState(); - mapController = MapController(); - - subscription = mapController.mapEventStream.listen(onMapEvent); - } - - @override - void dispose() { - subscription.cancel(); - - super.dispose(); } void onMapEvent(MapEvent mapEvent) { @@ -44,6 +33,10 @@ class _InteractiveTestPageState extends State { // do not flood console with move and rotate events debugPrint(mapEvent.toString()); } + + setState(() { + _latestEvent = mapEvent; + }); } void updateFlags(int flag) { @@ -148,35 +141,22 @@ class _InteractiveTestPageState extends State { Padding( padding: const EdgeInsets.only(top: 8, bottom: 8), child: Center( - child: StreamBuilder( - stream: mapController.mapEventStream, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - if (!snapshot.hasData) { - return const Text( - 'Current event: none\nSource: none', - textAlign: TextAlign.center, - ); - } - - return Text( - 'Current event: ${snapshot.data.runtimeType}\nSource: ${snapshot.data!.source}', + child: Text( + 'Current event: ${_latestEvent?.runtimeType ?? "none"}\nSource: ${_latestEvent?.source ?? "none"}', textAlign: TextAlign.center, - ); - }, - ), + ), ), ), Flexible( child: FlutterMap( - mapController: mapController, options: MapOptions( + onMapEvent: onMapEvent, center: LatLng(51.5, -0.09), zoom: 11, interactiveFlags: flags, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], diff --git a/example/lib/pages/latlng_to_screen_point.dart b/example/lib/pages/latlng_to_screen_point.dart index e68fe4de0..6988c2b75 100644 --- a/example/lib/pages/latlng_to_screen_point.dart +++ b/example/lib/pages/latlng_to_screen_point.dart @@ -18,23 +18,14 @@ class LatLngScreenPointTestPage extends StatefulWidget { } class _LatLngScreenPointTestPageState extends State { - late final MapController mapController; - late final StreamSubscription subscription; + late final MapController _mapController; - CustomPoint textPos = const CustomPoint(10.0, 10.0); + CustomPoint _textPos = const CustomPoint(10.0, 10.0); @override void initState() { super.initState(); - mapController = MapController(); - subscription = mapController.mapEventStream.listen(onMapEvent); - } - - @override - void dispose() { - subscription.cancel(); - - super.dispose(); + _mapController = MapController(); } void onMapEvent(MapEvent mapEvent) { @@ -53,19 +44,20 @@ class _LatLngScreenPointTestPageState extends State { Padding( padding: const EdgeInsets.all(8), child: FlutterMap( - mapController: mapController, + mapController: _mapController, options: MapOptions( + onMapEvent: onMapEvent, onTap: (tapPos, latLng) { - final pt1 = mapController.latLngToScreenPoint(latLng); - textPos = CustomPoint(pt1!.x, pt1.y); + final pt1 = _mapController.latLngToScreenPoint(latLng); + _textPos = CustomPoint(pt1!.x, pt1.y); setState(() {}); }, center: LatLng(51.5, -0.09), zoom: 11, rotation: 0, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], @@ -75,8 +67,8 @@ class _LatLngScreenPointTestPageState extends State { ), ), Positioned( - left: textPos.x.toDouble(), - top: textPos.y.toDouble(), + left: _textPos.x.toDouble(), + top: _textPos.y.toDouble(), width: 20, height: 20, child: const FlutterLogo()) diff --git a/example/lib/pages/live_location.dart b/example/lib/pages/live_location.dart index ecc77089f..36fddd722 100644 --- a/example/lib/pages/live_location.dart +++ b/example/lib/pages/live_location.dart @@ -139,14 +139,14 @@ class _LiveLocationPageState extends State { zoom: 5, interactiveFlags: interActiveFlags, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers), ], ), ), diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index 8a39e45c2..0a18d4c89 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -77,14 +77,14 @@ class _ManyMarkersPageState extends State { zoom: 5, interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions( + MarkerLayer( markers: allMarkers.sublist( 0, min(allMarkers.length, _sliderVal))), ], diff --git a/example/lib/pages/map_controller.dart b/example/lib/pages/map_controller.dart index 98ac8c024..b32c434be 100644 --- a/example/lib/pages/map_controller.dart +++ b/example/lib/pages/map_controller.dart @@ -17,18 +17,18 @@ class MapControllerPage extends StatefulWidget { } } -class MapControllerPageState extends State { - static LatLng london = LatLng(51.5, -0.09); - static LatLng paris = LatLng(48.8566, 2.3522); - static LatLng dublin = LatLng(53.3498, -6.2603); +final LatLng london = LatLng(51.5, -0.09); +final LatLng paris = LatLng(48.8566, 2.3522); +final LatLng dublin = LatLng(53.3498, -6.2603); - late final MapController mapController; - double rotation = 0; +class MapControllerPageState extends State { + late final MapController _mapController; + double _rotation = 0; @override void initState() { super.initState(); - mapController = MapController(); + _mapController = MapController(); } @override @@ -76,23 +76,23 @@ class MapControllerPageState extends State { children: [ MaterialButton( onPressed: () { - mapController.move(london, 18); + _mapController.move(london, 18); }, child: const Text('London'), ), MaterialButton( onPressed: () { - mapController.move(paris, 5); + _mapController.move(paris, 5); }, child: const Text('Paris'), ), MaterialButton( onPressed: () { - mapController.move(dublin, 5); + _mapController.move(dublin, 5); }, child: const Text('Dublin'), ), - CurrentLocation(mapController: mapController), + CurrentLocation(mapController: _mapController), ], ), ), @@ -106,7 +106,7 @@ class MapControllerPageState extends State { bounds.extend(dublin); bounds.extend(paris); bounds.extend(london); - mapController.fitBounds( + _mapController.fitBounds( bounds, options: const FitBoundsOptions( padding: EdgeInsets.only(left: 15, right: 15), @@ -118,7 +118,7 @@ class MapControllerPageState extends State { Builder(builder: (BuildContext context) { return MaterialButton( onPressed: () { - final bounds = mapController.bounds!; + final bounds = _mapController.bounds!; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( @@ -136,14 +136,14 @@ class MapControllerPageState extends State { const Text('Rotation:'), Expanded( child: Slider( - value: rotation, + value: _rotation, min: 0, max: 360, onChanged: (degree) { setState(() { - rotation = degree; + _rotation = degree; }); - mapController.rotate(degree); + _mapController.rotate(degree); }, ), ) @@ -152,21 +152,21 @@ class MapControllerPageState extends State { ), Flexible( child: FlutterMap( - mapController: mapController, + mapController: _mapController, options: MapOptions( center: LatLng(51.5, -0.09), zoom: 5, maxZoom: 5, minZoom: 3, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers), ], ), ), @@ -198,7 +198,6 @@ class _CurrentLocationState extends State { @override void initState() { super.initState(); - mapEventSubscription = widget.mapController.mapEventStream.listen(onMapEvent); } @@ -218,7 +217,8 @@ class _CurrentLocationState extends State { } void onMapEvent(MapEvent mapEvent) { - if (mapEvent is MapEventMove && mapEvent.id == _eventKey.toString()) { + if (mapEvent is MapEventMove && mapEvent.id != _eventKey.toString()) { + print("map event ${mapEvent.id}"); setIcon(Icons.gps_not_fixed); } } @@ -236,6 +236,7 @@ class _CurrentLocationState extends State { ); if (moved) { + print("moveed"); setIcon(Icons.gps_fixed); } else { setIcon(Icons.gps_not_fixed); diff --git a/example/lib/pages/map_inside_listview.dart b/example/lib/pages/map_inside_listview.dart index e4b57527d..d4eba274a 100644 --- a/example/lib/pages/map_inside_listview.dart +++ b/example/lib/pages/map_inside_listview.dart @@ -23,29 +23,23 @@ class MapInsideListViewPage extends StatelessWidget { height: 300, child: FlutterMap( options: MapOptions( + absorbPanEventsOnScrollables: true, center: LatLng(51.5, -0.09), zoom: 5, - plugins: [ - ZoomButtonsPlugin(), - ], ), - layers: [ - ZoomButtonsPluginOption( - minZoom: 4, - maxZoom: 19, - mini: true, - padding: 10, - alignment: Alignment.bottomLeft, - ) - ], - children: [ - TileLayerWidget( - options: TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), + const FlutterMapZoomButtons( + minZoom: 4, + maxZoom: 19, + mini: true, + padding: 10, + alignment: Alignment.bottomLeft, ), ], ), diff --git a/example/lib/pages/marker_anchor.dart b/example/lib/pages/marker_anchor.dart index 3017f1106..c4f60cec4 100644 --- a/example/lib/pages/marker_anchor.dart +++ b/example/lib/pages/marker_anchor.dart @@ -112,14 +112,14 @@ class MarkerAnchorPageState extends State { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers), ], ), ), diff --git a/example/lib/pages/marker_rotate.dart b/example/lib/pages/marker_rotate.dart index 1e5431b56..b895ab012 100644 --- a/example/lib/pages/marker_rotate.dart +++ b/example/lib/pages/marker_rotate.dart @@ -137,14 +137,14 @@ class MarkerRotatePageState extends State { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions( + MarkerLayer( rotate: rotateMarkerLayerOptions, markers: markers, ) diff --git a/example/lib/pages/max_bounds.dart b/example/lib/pages/max_bounds.dart index 0701cfaf0..b0a423774 100644 --- a/example/lib/pages/max_bounds.dart +++ b/example/lib/pages/max_bounds.dart @@ -30,8 +30,8 @@ class MaxBoundsPage extends StatelessWidget { maxBounds: LatLngBounds(LatLng(-90, -180), LatLng(90, 180)), screenSize: MediaQuery.of(context).size, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( maxZoom: 15, urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', diff --git a/example/lib/pages/moving_markers.dart b/example/lib/pages/moving_markers.dart index ecb7d6673..6406d1709 100644 --- a/example/lib/pages/moving_markers.dart +++ b/example/lib/pages/moving_markers.dart @@ -58,14 +58,14 @@ class _MovingMarkersPageState extends State { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: [_marker!]) + MarkerLayer(markers: [_marker!]), ], ), ), diff --git a/example/lib/pages/network_tile_provider.dart b/example/lib/pages/network_tile_provider.dart index 3f957dc7b..f512637bd 100644 --- a/example/lib/pages/network_tile_provider.dart +++ b/example/lib/pages/network_tile_provider.dart @@ -64,15 +64,15 @@ class NetworkTileProviderPage extends StatelessWidget { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], tileProvider: NetworkTileProvider(), userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers) ], ), ), diff --git a/example/lib/pages/offline_map.dart b/example/lib/pages/offline_map.dart index cef4ca252..95a969b2a 100644 --- a/example/lib/pages/offline_map.dart +++ b/example/lib/pages/offline_map.dart @@ -32,8 +32,8 @@ class OfflineMapPage extends StatelessWidget { swPanBoundary: LatLng(56.6877, 11.5089), nePanBoundary: LatLng(56.7378, 11.6644), ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( tileProvider: AssetTileProvider(), maxZoom: 14, urlTemplate: 'assets/map/anholt_osmbright/{z}/{x}/{y}.png', diff --git a/example/lib/pages/on_tap.dart b/example/lib/pages/on_tap.dart index 8c9798605..8c0bb8849 100644 --- a/example/lib/pages/on_tap.dart +++ b/example/lib/pages/on_tap.dart @@ -84,14 +84,14 @@ class OnTapPageState extends State { maxZoom: 5, minZoom: 3, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers), ], ), ), diff --git a/example/lib/pages/overlay_image.dart b/example/lib/pages/overlay_image.dart index 41b8bac6c..a90135dec 100644 --- a/example/lib/pages/overlay_image.dart +++ b/example/lib/pages/overlay_image.dart @@ -46,15 +46,16 @@ class OverlayImagePage extends StatelessWidget { center: LatLng(51.5, -0.09), zoom: 6, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - OverlayImageLayerOptions(overlayImages: overlayImages), - MarkerLayerOptions(markers: [ + OverlayImageLayer( + overlayImages: overlayImages), + MarkerLayer(markers: [ Marker( point: topLeftCorner, builder: (context) => const _Circle( diff --git a/example/lib/pages/plugin_api.dart b/example/lib/pages/plugin_api.dart deleted file mode 100644 index 6b25f3424..000000000 --- a/example/lib/pages/plugin_api.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_map/plugin_api.dart'; -import 'package:flutter_map_example/widgets/drawer.dart'; -import 'package:latlong2/latlong.dart'; - -class PluginPage extends StatelessWidget { - static const String route = 'plugins'; - - const PluginPage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: const Text('Plugins')), - drawer: buildDrawer(context, PluginPage.route), - body: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - Flexible( - child: FlutterMap( - options: MapOptions( - center: LatLng(51.5, -0.09), - zoom: 5, - plugins: [ - MyCustomPlugin(), - ], - ), - layers: [ - TileLayerOptions( - urlTemplate: - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: ['a', 'b', 'c'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - ], - nonRotatedLayers: [ - MyCustomPluginOptions(text: "I'm a plugin!"), - ], - ), - ), - ], - ), - ), - ); - } -} - -class MyCustomPluginOptions extends LayerOptions { - final String text; - MyCustomPluginOptions({ - Key? key, - this.text = '', - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - -class MyCustomPlugin implements MapPlugin { - @override - Widget createLayer( - LayerOptions options, MapState mapState, Stream stream) { - if (options is MyCustomPluginOptions) { - const style = TextStyle( - fontWeight: FontWeight.bold, - fontSize: 24, - color: Colors.red, - ); - return Text( - options.text, - key: options.key, - style: style, - ); - } - throw Exception('Unknown options type for MyCustom' - 'plugin: $options'); - } - - @override - bool supportsLayer(LayerOptions options) { - return options is MyCustomPluginOptions; - } -} diff --git a/example/lib/pages/plugin_scalebar.dart b/example/lib/pages/plugin_scalebar.dart index d2139506b..51de08e60 100644 --- a/example/lib/pages/plugin_scalebar.dart +++ b/example/lib/pages/plugin_scalebar.dart @@ -23,25 +23,23 @@ class PluginScaleBar extends StatelessWidget { options: MapOptions( center: LatLng(51.5, -0.09), zoom: 5, - plugins: [ - ScaleLayerPlugin(), - ], ), - layers: [ - TileLayerOptions( - urlTemplate: - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: ['a', 'b', 'c'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - ], - nonRotatedLayers: [ - ScaleLayerPluginOption( + nonRotatedChildren: [ + ScaleLayerWidget( + options: ScaleLayerPluginOption( lineColor: Colors.blue, lineWidth: 2, textStyle: const TextStyle(color: Colors.blue, fontSize: 12), padding: const EdgeInsets.all(10), + )), + ], + children: [ + TileLayer( + urlTemplate: + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), ], ), diff --git a/example/lib/pages/plugin_zoombuttons.dart b/example/lib/pages/plugin_zoombuttons.dart index e8509daf9..9939e4c2d 100644 --- a/example/lib/pages/plugin_zoombuttons.dart +++ b/example/lib/pages/plugin_zoombuttons.dart @@ -23,20 +23,9 @@ class PluginZoomButtons extends StatelessWidget { options: MapOptions( center: LatLng(51.5, -0.09), zoom: 5, - plugins: [ - ZoomButtonsPlugin(), - ], ), - layers: [ - TileLayerOptions( - urlTemplate: - 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - subdomains: ['a', 'b', 'c'], - userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), - ], - nonRotatedLayers: [ - ZoomButtonsPluginOption( + nonRotatedChildren: const [ + FlutterMapZoomButtons( minZoom: 4, maxZoom: 19, mini: true, @@ -44,7 +33,14 @@ class PluginZoomButtons extends StatelessWidget { alignment: Alignment.bottomRight, ), ], - ), + children: [ + TileLayer( + urlTemplate: + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + ]), ), ], ), diff --git a/example/lib/pages/point_to_latlng.dart b/example/lib/pages/point_to_latlng.dart index 608f6b1a7..10142578a 100644 --- a/example/lib/pages/point_to_latlng.dart +++ b/example/lib/pages/point_to_latlng.dart @@ -17,8 +17,7 @@ class PointToLatLngPage extends StatefulWidget { } class PointToLatlngPage extends State { - late final MapController mapController; - late final StreamSubscription mapEventSubscription; + late final MapController mapController = MapController(); final pointSize = 40.0; final pointY = 200.0; @@ -27,13 +26,9 @@ class PointToLatlngPage extends State { @override void initState() { super.initState(); - mapController = MapController(); - mapEventSubscription = mapController.mapEventStream - .listen((mapEvent) => onMapEvent(mapEvent, context)); - - Future.delayed(Duration.zero, () { - mapController.onReady.then((_) => _updatePointLatLng(context)); + WidgetsBinding.instance.addPostFrameCallback((_) { + updatePoint(null, context); }); } @@ -45,11 +40,13 @@ class PointToLatlngPage extends State { mainAxisAlignment: MainAxisAlignment.end, children: [ FloatingActionButton( + heroTag: 'rotate', child: const Icon(Icons.rotate_right), onPressed: () => mapController.rotate(60), ), const SizedBox(height: 15), FloatingActionButton( + heroTag: 'cancel', child: const Icon(Icons.cancel), onPressed: () => mapController.rotate(0), ), @@ -61,21 +58,22 @@ class PointToLatlngPage extends State { FlutterMap( mapController: mapController, options: MapOptions( + onMapEvent: (event) { + updatePoint(null, context); + }, center: LatLng(51.5, -0.09), zoom: 5, minZoom: 3, ), children: [ - TileLayerWidget( - options: TileLayerOptions( + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', - )), + ), if (latLng != null) - MarkerLayerWidget( - options: MarkerLayerOptions( + MarkerLayer( markers: [ Marker( width: pointSize, @@ -84,7 +82,7 @@ class PointToLatlngPage extends State { builder: (ctx) => const FlutterLogo(), ) ], - )) + ) ], ), Container( @@ -109,27 +107,14 @@ class PointToLatlngPage extends State { ); } - void onMapEvent(MapEvent mapEvent, BuildContext context) { - _updatePointLatLng(context); - } - - void _updatePointLatLng(BuildContext context) { + void updatePoint(MapEvent? event, BuildContext context) { final pointX = _getPointX(context); - - final latLng = mapController.pointToLatLng(CustomPoint(pointX, pointY)); - setState(() { - this.latLng = latLng; + latLng = mapController.pointToLatLng(CustomPoint(pointX, pointY)); }); } double _getPointX(BuildContext context) { return MediaQuery.of(context).size.width / 2; } - - @override - void dispose() { - super.dispose(); - mapEventSubscription.cancel(); - } } diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 35e28f351..b50806c20 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -52,14 +52,14 @@ class PolygonPage extends StatelessWidget { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - PolygonLayerOptions(polygons: [ + PolygonLayer(polygons: [ Polygon( points: notFilledPoints, isFilled: false, // By default it's false diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 6de576a6f..6eda841ec 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -80,24 +80,22 @@ class _PolylinePageState extends State { }); }, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - PolylineLayerOptions( - polylines: [ + PolylineLayer(polylines: [ Polyline( points: points, strokeWidth: 4, color: Colors.purple), ], ), - PolylineLayerOptions( - polylines: [ + PolylineLayer(polylines: [ Polyline( points: pointsGradient, strokeWidth: 4, @@ -109,7 +107,7 @@ class _PolylinePageState extends State { ), ], ), - PolylineLayerOptions( + PolylineLayer( polylines: snapshot.data!, polylineCulling: true, ), diff --git a/example/lib/pages/reset_tile_layer.dart b/example/lib/pages/reset_tile_layer.dart index 0156211e1..a8d76b048 100644 --- a/example/lib/pages/reset_tile_layer.dart +++ b/example/lib/pages/reset_tile_layer.dart @@ -75,14 +75,14 @@ class ResetTileLayerPageState extends State { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( reset: resetController.stream, urlTemplate: layerToggle ? layer1 : layer2, subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers) ], ), ), diff --git a/example/lib/pages/scale_layer_plugin_option.dart b/example/lib/pages/scale_layer_plugin_option.dart index cfd45efb8..8ddc430a2 100644 --- a/example/lib/pages/scale_layer_plugin_option.dart +++ b/example/lib/pages/scale_layer_plugin_option.dart @@ -6,57 +6,22 @@ import 'package:flutter_map/plugin_api.dart'; import 'package:flutter_map_example/pages/scalebar_utils.dart' as util; -class ScaleLayerPluginOption extends LayerOptions { +class ScaleLayerPluginOption { TextStyle? textStyle; Color lineColor; double lineWidth; final EdgeInsets? padding; ScaleLayerPluginOption({ - Key? key, this.textStyle, this.lineColor = Colors.white, this.lineWidth = 2, this.padding, - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - -class ScaleLayerPlugin implements MapPlugin { - @override - Widget createLayer( - LayerOptions options, MapState mapState, Stream stream) { - if (options is ScaleLayerPluginOption) { - return ScaleLayerWidget(options, mapState); - } - throw Exception('Unknown options type for ScaleLayerPlugin: $options'); - } - - @override - bool supportsLayer(LayerOptions options) { - return options is ScaleLayerPluginOption; - } + }) ; } class ScaleLayerWidget extends StatelessWidget { - final ScaleLayerPluginOption scaleLayerOpts; - final MapState map; - ScaleLayerWidget(this.scaleLayerOpts, this.map) - : super(key: scaleLayerOpts.key); - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context); - return StreamBuilder( - stream: mapState?.onMoved, - builder: (BuildContext context, _) { - return ScaleLayer(scaleLayerOpts, map); - }); - } -} - -class ScaleLayer extends StatelessWidget { - final ScaleLayerPluginOption scaleLayerOpts; - final MapState map; + final ScaleLayerPluginOption options; final scale = [ 25000000, 15000000, @@ -83,10 +48,11 @@ class ScaleLayer extends StatelessWidget { 5 ]; - ScaleLayer(this.scaleLayerOpts, this.map) : super(key: scaleLayerOpts.key); + ScaleLayerWidget({super.key, required this.options}); @override Widget build(BuildContext context) { + final map = FlutterMapState.maybeOf(context)!; final zoom = map.zoom; final distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); final center = map.center; @@ -105,10 +71,10 @@ class ScaleLayer extends StatelessWidget { painter: ScalePainter( width, displayDistance, - lineColor: scaleLayerOpts.lineColor, - lineWidth: scaleLayerOpts.lineWidth, - padding: scaleLayerOpts.padding, - textStyle: scaleLayerOpts.textStyle, + lineColor: options.lineColor, + lineWidth: options.lineWidth, + padding: options.padding, + textStyle: options.textStyle, ), ); }, diff --git a/example/lib/pages/sliding_map.dart b/example/lib/pages/sliding_map.dart index bc74030ea..ed0262d22 100644 --- a/example/lib/pages/sliding_map.dart +++ b/example/lib/pages/sliding_map.dart @@ -34,8 +34,8 @@ class SlidingMapPage extends StatelessWidget { slideOnBoundaries: true, screenSize: MediaQuery.of(context).size, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( tileProvider: AssetTileProvider(), maxZoom: 14, urlTemplate: 'assets/map/anholt_osmbright/{z}/{x}/{y}.png', diff --git a/example/lib/pages/stateful_markers.dart b/example/lib/pages/stateful_markers.dart index 16ffe056f..5544f834c 100644 --- a/example/lib/pages/stateful_markers.dart +++ b/example/lib/pages/stateful_markers.dart @@ -63,14 +63,14 @@ class _StatefulMarkersPageState extends State { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: _markers) + MarkerLayer(markers: _markers), ], ), ), diff --git a/example/lib/pages/tap_to_add.dart b/example/lib/pages/tap_to_add.dart index f79afa4a1..28ff287ca 100644 --- a/example/lib/pages/tap_to_add.dart +++ b/example/lib/pages/tap_to_add.dart @@ -46,13 +46,13 @@ class TapToAddPageState extends State { center: LatLng(45.5231, -122.6765), zoom: 13, onTap: _handleTap), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), - MarkerLayerOptions(markers: markers) + MarkerLayer(markers: markers), ], ), ), diff --git a/example/lib/pages/tile_builder_example.dart b/example/lib/pages/tile_builder_example.dart index 7dc4584d3..07c47bce9 100644 --- a/example/lib/pages/tile_builder_example.dart +++ b/example/lib/pages/tile_builder_example.dart @@ -111,8 +111,8 @@ class _TileBuilderPageState extends State { center: LatLng(51.5, -0.09), zoom: 5, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', @@ -120,7 +120,7 @@ class _TileBuilderPageState extends State { tilesContainerBuilder: darkMode ? darkModeTilesContainerBuilder : null, ), - MarkerLayerOptions( + MarkerLayer( markers: [ Marker( width: 80, @@ -131,7 +131,7 @@ class _TileBuilderPageState extends State { ), ), ], - ) + ), ], ), ), diff --git a/example/lib/pages/tile_loading_error_handle.dart b/example/lib/pages/tile_loading_error_handle.dart index 09a55eda3..340eb77a7 100644 --- a/example/lib/pages/tile_loading_error_handle.dart +++ b/example/lib/pages/tile_loading_error_handle.dart @@ -38,8 +38,8 @@ class _TileLoadingErrorHandleState extends State { needLoadingError = true; }, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], diff --git a/example/lib/pages/widgets.dart b/example/lib/pages/widgets.dart index 47689bf6a..ad5bea8ad 100644 --- a/example/lib/pages/widgets.dart +++ b/example/lib/pages/widgets.dart @@ -25,20 +25,15 @@ class WidgetsPage extends StatelessWidget { options: MapOptions( center: LatLng(51.5, -0.09), zoom: 5, - plugins: [ - ZoomButtonsPlugin(), - ], ), - nonRotatedLayers: [ - ZoomButtonsPluginOption( + nonRotatedChildren: const [ + FlutterMapZoomButtons( minZoom: 4, maxZoom: 19, mini: true, padding: 10, alignment: Alignment.bottomLeft, - ) - ], - nonRotatedChildren: const [ + ), Text( 'Plugin is just Text widget', style: TextStyle( @@ -48,14 +43,12 @@ class WidgetsPage extends StatelessWidget { backgroundColor: Colors.yellow), ) ], - children: [ - TileLayerWidget( - options: TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], userAgentPackageName: 'dev.fleaflet.flutter_map.example', - ), ), const MovingWithoutRefreshAllMapMarkers(), ], @@ -102,8 +95,7 @@ class _MovingWithoutRefreshAllMapMarkersState @override Widget build(BuildContext context) { - return MarkerLayerWidget( - options: MarkerLayerOptions(markers: [_marker!]), + return MarkerLayer(markers: [_marker!], ); } } diff --git a/example/lib/pages/wms_tile_layer.dart b/example/lib/pages/wms_tile_layer.dart index b800cb007..c3b1414ee 100644 --- a/example/lib/pages/wms_tile_layer.dart +++ b/example/lib/pages/wms_tile_layer.dart @@ -27,8 +27,8 @@ class WMSLayerPage extends StatelessWidget { center: LatLng(42.58, 12.43), zoom: 6, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( wmsOptions: WMSTileLayerOptions( baseUrl: 'https://{s}.s2maps-tiles.eu/wms/?', layers: ['s2cloudless-2018_3857'], diff --git a/example/lib/pages/zoombuttons_plugin_option.dart b/example/lib/pages/zoombuttons_plugin_option.dart index 132ee0c76..0783bfd47 100644 --- a/example/lib/pages/zoombuttons_plugin_option.dart +++ b/example/lib/pages/zoombuttons_plugin_option.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; -class ZoomButtonsPluginOption extends LayerOptions { +class FlutterMapZoomButtons extends StatelessWidget { + final double minZoom; final double maxZoom; final bool mini; @@ -14,8 +15,11 @@ class ZoomButtonsPluginOption extends LayerOptions { final IconData zoomInIcon; final IconData zoomOutIcon; - ZoomButtonsPluginOption({ - Key? key, + final FitBoundsOptions options = + const FitBoundsOptions(padding: EdgeInsets.all(12)); + + const FlutterMapZoomButtons({ + super.key, this.minZoom = 1, this.maxZoom = 18, this.mini = true, @@ -27,87 +31,60 @@ class ZoomButtonsPluginOption extends LayerOptions { this.zoomOutColor, this.zoomOutColorIcon, this.zoomOutIcon = Icons.zoom_out, - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - -class ZoomButtonsPlugin implements MapPlugin { - @override - Widget createLayer( - LayerOptions options, MapState mapState, Stream stream) { - if (options is ZoomButtonsPluginOption) { - return ZoomButtons(options, mapState, stream); - } - throw Exception('Unknown options type for ZoomButtonsPlugin: $options'); - } - - @override - bool supportsLayer(LayerOptions options) { - return options is ZoomButtonsPluginOption; - } -} - -class ZoomButtons extends StatelessWidget { - final ZoomButtonsPluginOption zoomButtonsOpts; - final MapState map; - final Stream stream; - final FitBoundsOptions options = - const FitBoundsOptions(padding: EdgeInsets.all(12)); - - ZoomButtons(this.zoomButtonsOpts, this.map, this.stream) - : super(key: zoomButtonsOpts.key); + }); @override Widget build(BuildContext context) { + final map = FlutterMapState.maybeOf(context)!; return Align( - alignment: zoomButtonsOpts.alignment, + alignment: alignment, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.only( - left: zoomButtonsOpts.padding, - top: zoomButtonsOpts.padding, - right: zoomButtonsOpts.padding), + left: padding, + top: padding, + right: padding), child: FloatingActionButton( heroTag: 'zoomInButton', - mini: zoomButtonsOpts.mini, + mini: mini, backgroundColor: - zoomButtonsOpts.zoomInColor ?? Theme.of(context).primaryColor, + zoomInColor ?? Theme.of(context).primaryColor, onPressed: () { - final bounds = map.getBounds(); + final bounds = map.bounds; final centerZoom = map.getBoundsCenterZoom(bounds, options); var zoom = centerZoom.zoom + 1; - if (zoom > zoomButtonsOpts.maxZoom) { - zoom = zoomButtonsOpts.maxZoom; + if (zoom > maxZoom) { + zoom = maxZoom; } map.move(centerZoom.center, zoom, source: MapEventSource.custom); }, - child: Icon(zoomButtonsOpts.zoomInIcon, - color: zoomButtonsOpts.zoomInColorIcon ?? + child: Icon(zoomInIcon, + color: zoomInColorIcon ?? IconTheme.of(context).color), ), ), Padding( - padding: EdgeInsets.all(zoomButtonsOpts.padding), + padding: EdgeInsets.all(padding), child: FloatingActionButton( heroTag: 'zoomOutButton', - mini: zoomButtonsOpts.mini, - backgroundColor: zoomButtonsOpts.zoomOutColor ?? + mini: mini, + backgroundColor: zoomOutColor ?? Theme.of(context).primaryColor, onPressed: () { - final bounds = map.getBounds(); + final bounds = map.bounds; final centerZoom = map.getBoundsCenterZoom(bounds, options); var zoom = centerZoom.zoom - 1; - if (zoom < zoomButtonsOpts.minZoom) { - zoom = zoomButtonsOpts.minZoom; + if (zoom < minZoom) { + zoom = minZoom; } map.move(centerZoom.center, zoom, source: MapEventSource.custom); }, - child: Icon(zoomButtonsOpts.zoomOutIcon, - color: zoomButtonsOpts.zoomOutColorIcon ?? + child: Icon(zoomOutIcon, + color: zoomOutColorIcon ?? IconTheme.of(context).color), ), ), diff --git a/example/lib/test_app.dart b/example/lib/test_app.dart index 8a0fa89dd..908645a94 100644 --- a/example/lib/test_app.dart +++ b/example/lib/test_app.dart @@ -32,8 +32,8 @@ class _TestAppState extends State { center: LatLng(45.5231, -122.6765), zoom: 13, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( 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 f896d3ebb..a7418499e 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -21,7 +21,6 @@ import 'package:flutter_map_example/pages/network_tile_provider.dart'; import 'package:flutter_map_example/pages/offline_map.dart'; import 'package:flutter_map_example/pages/on_tap.dart'; import 'package:flutter_map_example/pages/overlay_image.dart'; -import 'package:flutter_map_example/pages/plugin_api.dart'; import 'package:flutter_map_example/pages/plugin_scalebar.dart'; import 'package:flutter_map_example/pages/plugin_zoombuttons.dart'; import 'package:flutter_map_example/pages/point_to_latlng.dart'; @@ -134,12 +133,6 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { MarkerRotatePage.route, currentRoute, ), - _buildMenuItem( - context, - const Text('Plugins'), - PluginPage.route, - currentRoute, - ), _buildMenuItem( context, const Text('ScaleBar Plugins'), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 07296df8a..8043cddc4 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ publish_to: "none" version: 1.0.0 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: flutter: diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index afbeb2329..0a798a4e3 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -1,7 +1,6 @@ library flutter_map; import 'dart:async'; -import 'dart:math'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; @@ -15,10 +14,8 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/gestures/interactive_flag.dart'; import 'package:flutter_map/src/gestures/map_events.dart'; import 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; -import 'package:flutter_map/src/layer/layer.dart'; import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/map/map.dart'; -import 'package:flutter_map/src/plugins/plugin.dart'; export 'package:flutter_map/src/core/center_zoom.dart'; export 'package:flutter_map/src/core/point.dart'; @@ -29,8 +26,6 @@ export 'package:flutter_map/src/gestures/map_events.dart'; export 'package:flutter_map/src/gestures/multi_finger_gesture.dart'; export 'package:flutter_map/src/layer/attribution_layer.dart'; export 'package:flutter_map/src/layer/circle_layer.dart'; -export 'package:flutter_map/src/layer/group_layer.dart'; -export 'package:flutter_map/src/layer/layer.dart'; export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer.dart'; @@ -44,7 +39,6 @@ export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_tile_provide if (dart.library.html) 'package:flutter_map/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_provider/tile_provider_io.dart' if (dart.library.html) 'package:flutter_map/src/layer/tile_layer/tile_provider/tile_provider_web.dart'; -export 'package:flutter_map/src/plugins/plugin.dart'; /// Renders a map composed of a list of layers powered by [LayerOptions]. /// @@ -52,20 +46,6 @@ export 'package:flutter_map/src/plugins/plugin.dart'; /// /// Through [MapOptions] map's callbacks and properties can be defined. class FlutterMap extends StatefulWidget { - /// A set of layers' options to used to create the layers on the map. - /// - /// Usually a list of [TileLayerOptions], [MarkerLayerOptions] and - /// [PolylineLayerOptions]. - /// - /// These layers will render above [children] - final List layers; - - /// These layers won't be rotated. - /// Usually these are plugins which are floating above [layers] - /// - /// These layers will render above [nonRotatedChildren] - final List nonRotatedLayers; - /// A set of layers' widgets to used to create the layers on the map. final List children; @@ -83,14 +63,12 @@ class FlutterMap extends StatefulWidget { final MapController? mapController; const FlutterMap({ - Key? key, + super.key, required this.options, - this.layers = const [], - this.nonRotatedLayers = const [], this.children = const [], this.nonRotatedChildren = const [], this.mapController, - }) : super(key: key); + }); @override FlutterMapState createState() => FlutterMapState(); @@ -105,7 +83,7 @@ class FlutterMap extends StatefulWidget { abstract class MapController { /// Moves the map to a specific location and zoom level /// - /// Optionally provide [id] attribute and if you listen to [mapEventStream] + /// Optionally provide [id] attribute and if you listen to [mapEventCallback] /// later a [MapEventMove] event will be emitted (if move was success) with /// same [id] attribute. Event's source attribute will be /// [MapEventSource.mapController]. @@ -117,7 +95,7 @@ abstract class MapController { /// Sets the map rotation to a certain degrees angle (in decimal). /// - /// Optionally provide [id] attribute and if you listen to [mapEventStream] + /// Optionally provide [id] attribute and if you listen to [mapEventCallback] /// later a [MapEventRotate] event will be emitted (if rotate was success) /// with same [id] attribute. Event's source attribute will be /// [MapEventSource.mapController]. @@ -140,8 +118,6 @@ abstract class MapController { CenterZoom centerZoomFitBounds(LatLngBounds bounds, {FitBoundsOptions? options}); - Future get onReady; - LatLng get center; LatLngBounds? get bounds; @@ -150,14 +126,14 @@ abstract class MapController { double get rotation; - Stream get mapEventStream; - - StreamSink get mapEventSink; + set state(FlutterMapState state); - set state(MapState state); + Stream get mapEventStream; void dispose(); + StreamSink get mapEventSink; + LatLng? pointToLatLng(CustomPoint point); CustomPoint? latLngToScreenPoint(LatLng latLng); @@ -176,7 +152,7 @@ typedef PointerCancelCallback = void Function( typedef PointerHoverCallback = void Function( PointerHoverEvent event, LatLng point); typedef PositionCallback = void Function(MapPosition position, bool hasGesture); -typedef MapCreatedCallback = void Function(MapController mapController); +typedef MapEventCallback = void Function(MapEvent); /// Allows you to provide your map's starting properties for [zoom], [rotation] /// and [center]. Alternatively you can provide [bounds] instead of [center]. @@ -265,8 +241,7 @@ class MapOptions { /// see [InteractiveFlag] for custom settings final int interactiveFlags; - final bool allowPanning; - final bool allowPanningOnScrollingParent; + final bool absorbPanEventsOnScrollables; final TapCallback? onTap; final LongPressCallback? onLongPress; @@ -275,18 +250,24 @@ class MapOptions { final PointerCancelCallback? onPointerCancel; final PointerHoverCallback? onPointerHover; final PositionCallback? onPositionChanged; - final MapCreatedCallback? onMapCreated; - final List plugins; + final MapEventCallback? onMapEvent; final bool slideOnBoundaries; final Size? screenSize; final bool adaptiveBoundaries; - final MapController? controller; final LatLng center; final LatLngBounds? bounds; final FitBoundsOptions boundsOptions; final LatLng? swPanBoundary; final LatLng? nePanBoundary; + /// OnMapReady is called after the map runs it's initState. + /// At that point the map has assigned its state to the controller + /// Only use this if your map isn't built immediately (like inside FutureBuilder) + /// and you need to access the controller as soon as the map is built. + /// Otherwise you can use WidgetsBinding.instance.addPostFrameCallback + /// In initState to controll the map before the next frame + final void Function()? onMapReady; + /// 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. @@ -300,11 +281,8 @@ class MapOptions { /// widget from rebuilding. final bool keepAlive; - _SafeArea? _safeAreaCache; - double? _safeAreaZoom; - MapOptions({ - this.allowPanningOnScrollingParent = true, + this.absorbPanEventsOnScrollables = true, this.crs = const Epsg3857(), LatLng? center, this.bounds, @@ -326,7 +304,6 @@ class MapOptions { this.minZoom, this.maxZoom, this.interactiveFlags = InteractiveFlag.all, - this.allowPanning = true, this.onTap, this.onLongPress, this.onPointerDown, @@ -334,12 +311,11 @@ class MapOptions { this.onPointerCancel, this.onPointerHover, this.onPositionChanged, - this.onMapCreated, - this.plugins = const [], + this.onMapEvent, + this.onMapReady, this.slideOnBoundaries = false, this.adaptiveBoundaries = false, this.screenSize, - this.controller, this.swPanBoundary, this.nePanBoundary, this.maxBounds, @@ -348,80 +324,9 @@ class MapOptions { assert(rotationThreshold >= 0.0), assert(pinchZoomThreshold >= 0.0), assert(pinchMoveThreshold >= 0.0) { - _safeAreaZoom = zoom; - assert(slideOnBoundaries || - !isOutOfBounds(center)); //You cannot start outside pan boundary - assert(!adaptiveBoundaries || screenSize != null, - 'screenSize must be set in order to enable adaptive boundaries.'); - assert(!adaptiveBoundaries || controller != null, - 'controller must be set in order to enable adaptive boundaries.'); - } - - //if there is a pan boundary, do not cross - bool isOutOfBounds(LatLng? center) { - if (adaptiveBoundaries) { - return !_safeArea!.contains(center); - } - if (swPanBoundary != null && nePanBoundary != null) { - if (center == null) { - return true; - } else if (center.latitude < swPanBoundary!.latitude || - center.latitude > nePanBoundary!.latitude) { - return true; - } else if (center.longitude < swPanBoundary!.longitude || - center.longitude > nePanBoundary!.longitude) { - return true; - } - } - return false; - } - - LatLng containPoint(LatLng point, LatLng fallback) { - if (adaptiveBoundaries) { - return _safeArea!.containPoint(point, fallback); - } else { - return LatLng( - point.latitude.clamp(swPanBoundary!.latitude, nePanBoundary!.latitude), - point.longitude - .clamp(swPanBoundary!.longitude, nePanBoundary!.longitude), - ); - } - } - - _SafeArea? get _safeArea { - final controllerZoom = _getControllerZoom(); - if (controllerZoom != _safeAreaZoom || _safeAreaCache == null) { - _safeAreaZoom = controllerZoom; - final halfScreenHeight = _calculateScreenHeightInDegrees() / 2; - final halfScreenWidth = _calculateScreenWidthInDegrees() / 2; - final southWestLatitude = swPanBoundary!.latitude + halfScreenHeight; - final southWestLongitude = swPanBoundary!.longitude + halfScreenWidth; - final northEastLatitude = nePanBoundary!.latitude - halfScreenHeight; - final northEastLongitude = nePanBoundary!.longitude - halfScreenWidth; - _safeAreaCache = _SafeArea( - LatLng( - southWestLatitude, - southWestLongitude, - ), - LatLng( - northEastLatitude, - northEastLongitude, - ), - ); - } - return _safeAreaCache; - } - - double _calculateScreenWidthInDegrees() { - final zoom = _getControllerZoom(); - final degreesPerPixel = 360 / pow(2, zoom + 8); - return screenSize!.width * degreesPerPixel; + assert(!adaptiveBoundaries || screenSize != null, + 'screenSize must be set in order to enable adaptive boundaries.'); } - - double _calculateScreenHeightInDegrees() => - screenSize!.height * 170.102258 / pow(2, _getControllerZoom() + 8); - - double _getControllerZoom() => controller!.zoom; } class FitBoundsOptions { @@ -458,29 +363,6 @@ class MapPosition { other.zoom == zoom; } -class _SafeArea { - final LatLngBounds bounds; - final bool isLatitudeBlocked; - final bool isLongitudeBlocked; - - _SafeArea(LatLng southWest, LatLng northEast) - : bounds = LatLngBounds(southWest, northEast), - isLatitudeBlocked = southWest.latitude > northEast.latitude, - isLongitudeBlocked = southWest.longitude > northEast.longitude; - - bool contains(LatLng? point) => - isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); - - LatLng containPoint(LatLng point, LatLng fallback) => LatLng( - isLatitudeBlocked - ? fallback.latitude - : point.latitude.clamp(bounds.south, bounds.north), - isLongitudeBlocked - ? fallback.longitude - : point.longitude.clamp(bounds.west, bounds.east), - ); -} - class MoveAndRotateResult { final bool moveSuccess; final bool rotateSuccess; diff --git a/lib/plugin_api.dart b/lib/plugin_api.dart index 5a34ac49d..c006a5d58 100644 --- a/lib/plugin_api.dart +++ b/lib/plugin_api.dart @@ -1,6 +1,7 @@ library flutter_map.plugin_api; export 'package:flutter_map/flutter_map.dart'; +export 'package:flutter_map/src/map/flutter_map_state.dart'; export 'package:flutter_map/src/core/bounds.dart'; export 'package:flutter_map/src/core/center_zoom.dart'; export 'package:flutter_map/src/map/map.dart'; diff --git a/lib/src/gestures/gestures.dart b/lib/src/gestures/gestures.dart index 48002d46e..0a5df7348 100644 --- a/lib/src/gestures/gestures.dart +++ b/lib/src/gestures/gestures.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/physics.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/latlng_tween.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; import 'package:positioned_tap_detector_2/positioned_tap_detector_2.dart'; @@ -19,6 +19,8 @@ abstract class MapGestureMixin extends State var _pointerCounter = 0; + bool _isListeningForInterruptions = false; + void onPointerDown(PointerDownEvent event) { ++_pointerCounter; if (mapState.options.onPointerDown != null) { @@ -106,7 +108,7 @@ abstract class MapGestureMixin extends State @override FlutterMap get widget; - MapState get mapState; + FlutterMapState get mapState; MapController get mapController; @@ -454,7 +456,6 @@ abstract class MapGestureMixin extends State newCenter, newZoom, hasGesture: true, - callOnMoveSink: false, source: eventSource, ); } @@ -477,14 +478,16 @@ abstract class MapGestureMixin extends State mapRotated = mapState.rotate( mapState.rotation + rotationDiff, hasGesture: true, - callOnMoveSink: false, source: eventSource, ); } } + //TODO maybe not needed? if (mapMoved || mapRotated) { - mapState.rebuildLayers(); + mapState.setState(() { + + }); } } } @@ -498,10 +501,6 @@ abstract class MapGestureMixin extends State void handleScaleEnd(ScaleEndDetails details) { _resetDoubleTapHold(); - if (!options.allowPanning) { - return; - } - final eventSource = _dragMode ? MapEventSource.dragEnd : MapEventSource.multiFingerEnd; @@ -547,8 +546,8 @@ abstract class MapGestureMixin extends State final direction = details.velocity.pixelsPerSecond / magnitude; final distance = (Offset.zero & - Size(mapState.originalSize!.x as double, - mapState.originalSize!.y as double)) + Size(mapState.nonrotatedSize!.x as double, + mapState.nonrotatedSize!.y as double)) .shortestSide; final flingOffset = _focalStartLocal - _lastFocalLocal; @@ -613,7 +612,7 @@ abstract class MapGestureMixin extends State LatLng _offsetToCrs(Offset offset, [double? zoom]) { final focalStartPt = mapState.project(mapState.center, zoom ?? mapState.zoom); - final point = (_offsetToPoint(offset) - (mapState.originalSize! / 2.0)) + final point = (_offsetToPoint(offset) - (mapState.nonrotatedSize! / 2.0)) .rotate(mapState.rotationRad); final newCenterPt = focalStartPt + point; @@ -622,9 +621,6 @@ abstract class MapGestureMixin extends State void handleDoubleTap(TapPosition tapPosition) { _resetDoubleTapHold(); - if (!options.allowPanning) { - return; - } closeFlingAnimationController(MapEventSource.doubleTap); closeDoubleTapController(MapEventSource.doubleTap); @@ -646,7 +642,7 @@ abstract class MapGestureMixin extends State List _getNewEventCenterZoomPosition( CustomPoint cursorPos, double newZoom) { // Calculate offset of mouse cursor from viewport center - final viewCenter = mapState.originalSize! / 2; + final viewCenter = mapState.nonrotatedSize! / 2; final offset = (cursorPos - viewCenter).rotate(mapState.rotationRad); // Match new center coordinate to mouse cursor position final scale = mapState.getZoomScale(newZoom, mapState.zoom); @@ -769,22 +765,20 @@ abstract class MapGestureMixin extends State ); } + //TODO refactor void _startListeningForAnimationInterruptions() { - if (_mapControllerAnimationInterruption != null) return; - // cancel map animation controllers on map controller move events - _mapControllerAnimationInterruption = mapController.mapEventStream - .where((event) => - event.source == MapEventSource.mapController && - event is MapEventMove) - .listen(_handleAnimationInterruptions); + _isListeningForInterruptions = true; } void _stopListeningForAnimationInterruptions() { - _mapControllerAnimationInterruption?.cancel(); - _mapControllerAnimationInterruption = null; + _isListeningForInterruptions = false; } - void _handleAnimationInterruptions(MapEvent event) { + void handleAnimationInterruptions(MapEvent event) { + if(_isListeningForInterruptions == false) { + //Do not handle animation interruptions if not listening + return; + } closeDoubleTapController(event.source); closeFlingAnimationController(event.source); } diff --git a/lib/src/layer/circle_layer.dart b/lib/src/layer/circle_layer.dart index bf790980f..ccac574c3 100644 --- a/lib/src/layer/circle_layer.dart +++ b/lib/src/layer/circle_layer.dart @@ -1,17 +1,7 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart' hide Path; -class CircleLayerOptions extends LayerOptions { - final List circles; - CircleLayerOptions({ - Key? key, - this.circles = const [], - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - class CircleMarker { final LatLng point; final double radius; @@ -31,41 +21,21 @@ class CircleMarker { }); } -class CircleLayerWidget extends StatelessWidget { - final CircleLayerOptions options; - - const CircleLayerWidget({Key? key, required this.options}) : super(key: key); - - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - return CircleLayer(options, mapState, mapState.onMoved); - } -} - class CircleLayer extends StatelessWidget { - final CircleLayerOptions circleOpts; - final MapState map; - final Stream? stream; - CircleLayer(this.circleOpts, this.map, this.stream) - : super(key: circleOpts.key); + final List circles; + const CircleLayer({ + super.key, + this.circles = const [], + }); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints bc) { final size = Size(bc.maxWidth, bc.maxHeight); - return _build(context, size); - }, - ); - } - - Widget _build(BuildContext context, Size size) { - return StreamBuilder( - stream: stream, // a Stream or null - builder: (BuildContext context, _) { + final map = FlutterMapState.maybeOf(context)!; final circleWidgets = []; - for (final circle in circleOpts.circles) { + for (final circle in circles) { circle.offset = map.getOffsetFromOrigin(circle.point); if (circle.useRadiusInMeter) { diff --git a/lib/src/layer/group_layer.dart b/lib/src/layer/group_layer.dart deleted file mode 100644 index 319bfef40..000000000 --- a/lib/src/layer/group_layer.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/map.dart'; - -/// [LayerOptions] that describe a layer composed by multiple built-in layers. -class GroupLayerOptions extends LayerOptions { - List group = []; - - GroupLayerOptions({ - Key? key, - this.group = const [], - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - -class GroupLayerWidget extends StatelessWidget { - final GroupLayerOptions options; - - const GroupLayerWidget({Key? key, required this.options}) : super(key: key); - - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - return GroupLayer(options, mapState, mapState.onMoved); - } -} - -class GroupLayer extends StatelessWidget { - final GroupLayerOptions groupOpts; - final MapState map; - final Stream stream; - - GroupLayer(this.groupOpts, this.map, this.stream) : super(key: groupOpts.key); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: stream, - builder: (BuildContext context, _) { - final layers = [ - for (var options in groupOpts.group) _createLayer(options) - ]; - - return Stack( - children: layers, - ); - }, - ); - } - - Widget _createLayer(LayerOptions options) { - if (options is MarkerLayerOptions) { - return MarkerLayer(options, map, options.rebuild); - } - if (options is CircleLayerOptions) { - return CircleLayer(options, map, options.rebuild); - } - if (options is PolylineLayerOptions) { - return PolylineLayer(options, map, options.rebuild); - } - if (options is PolygonLayerOptions) { - return PolygonLayer(options, map, options.rebuild); - } - if (options is OverlayImageLayerOptions) { - return OverlayImageLayer(options, map, options.rebuild); - } - throw Exception('Unknown options type for GeometryLayer: $options'); - } -} diff --git a/lib/src/layer/layer.dart b/lib/src/layer/layer.dart deleted file mode 100644 index 8863b8041..000000000 --- a/lib/src/layer/layer.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/foundation.dart'; - -/// Common type between all LayerOptions. -/// -/// All LayerOptions have access to a stream that notifies when the map needs -/// rebuilding. -class LayerOptions { - final Key? key; - final Stream? rebuild; - LayerOptions({this.key, this.rebuild}); -} diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 26b2992b3..9112e6208 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -1,52 +1,9 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/core/bounds.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; -/// Configuration for marker layer -class MarkerLayerOptions extends LayerOptions { - final List markers; - - /// Toggle marker position caching. Enabling will improve performance, but may introducen - /// errors when adding/removing markers. Default is enabled (`true`). - final bool usePxCache; - - /// If true markers will be counter rotated to the map rotation - final bool? rotate; - - /// The origin of the coordinate system (relative to the upper left corner of - /// this render object) in which to apply the matrix. - /// - /// Setting an origin is equivalent to conjugating the transform matrix by a - /// translation. This property is provided just for convenience. - final Offset? rotateOrigin; - - /// The alignment of the origin, relative to the size of the box. - /// - /// This is equivalent to setting an origin based on the size of the box. - /// If it is specified at the same time as the [rotateOrigin], both are applied. - /// - /// An [AlignmentDirectional.centerStart] value is the same as an [Alignment] - /// whose [Alignment.x] value is `-1.0` if [Directionality.of] returns - /// [TextDirection.ltr], and `1.0` if [Directionality.of] returns - /// [TextDirection.rtl]. Similarly [AlignmentDirectional.centerEnd] is the - /// same as an [Alignment] whose [Alignment.x] value is `1.0` if - /// [Directionality.of] returns [TextDirection.ltr], and `-1.0` if - /// [Directionality.of] returns [TextDirection.rtl]. - final AlignmentGeometry? rotateAlignment; - - MarkerLayerOptions({ - Key? key, - this.markers = const [], - this.rotate = false, - this.rotateOrigin, - this.rotateAlignment = Alignment.center, - this.usePxCache = true, - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - class Anchor { final double left; final double top; @@ -166,149 +123,101 @@ class Marker { }) : anchor = Anchor.forPos(anchorPos, width, height); } -class MarkerLayerWidget extends StatelessWidget { - final MarkerLayerOptions options; +class MarkerLayer extends StatefulWidget { + final List markers; - const MarkerLayerWidget({Key? key, required this.options}) : super(key: key); + /// If true markers will be counter rotated to the map rotation + final bool? rotate; - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - return MarkerLayer(options, mapState, mapState.onMoved); - } -} + /// The origin of the coordinate system (relative to the upper left corner of + /// this render object) in which to apply the matrix. + /// + /// Setting an origin is equivalent to conjugating the transform matrix by a + /// translation. This property is provided just for convenience. + final Offset? rotateOrigin; -class MarkerLayer extends StatefulWidget { - final MarkerLayerOptions markerLayerOptions; - final MapState map; - final Stream? stream; + /// The alignment of the origin, relative to the size of the box. + /// + /// This is equivalent to setting an origin based on the size of the box. + /// If it is specified at the same time as the [rotateOrigin], both are applied. + /// + /// An [AlignmentDirectional.centerStart] value is the same as an [Alignment] + /// whose [Alignment.x] value is `-1.0` if [Directionality.of] returns + /// [TextDirection.ltr], and `1.0` if [Directionality.of] returns + /// [TextDirection.rtl]. Similarly [AlignmentDirectional.centerEnd] is the + /// same as an [Alignment] whose [Alignment.x] value is `1.0` if + /// [Directionality.of] returns [TextDirection.ltr], and `-1.0` if + /// [Directionality.of] returns [TextDirection.rtl]. + final AlignmentGeometry? rotateAlignment; - MarkerLayer(this.markerLayerOptions, this.map, this.stream) - : super(key: markerLayerOptions.key); + const MarkerLayer( + {super.key, + this.markers = const [], + this.rotate = false, + this.rotateOrigin, + this.rotateAlignment = Alignment.center}); @override State createState() => _MarkerLayerState(); } class _MarkerLayerState extends State { - double lastZoom = -1; - - /// List containing cached pixel positions of markers - /// Should be discarded when zoom changes - // Has a fixed length of markerOpts.markers.length - better performance: - // https://stackoverflow.com/questions/15943890/is-there-a-performance-benefit-in-using-fixed-length-lists-in-dart - var _pxCache = []; - - /// Calling this every time markerOpts change should guarantee proper length - List generatePxCache() { - if (widget.markerLayerOptions.usePxCache) { - return List.generate( - widget.markerLayerOptions.markers.length, - (i) => widget.map.project(widget.markerLayerOptions.markers[i].point), - ); - } - return []; - } - - bool updatePxCacheIfNeeded() { - var didUpdate = false; - - /// markers may be modified, so update cache. Note, someone may - /// have not added to a cache, but modified, so this won't catch - /// this case. Parent widget setState should be called to call - /// didUpdateWidget to force a cache reload - - if (widget.markerLayerOptions.markers.length != _pxCache.length) { - _pxCache = generatePxCache(); - didUpdate = true; - } - return didUpdate; - } - - @override - void initState() { - super.initState(); - _pxCache = generatePxCache(); - } - - @override - void didUpdateWidget(covariant MarkerLayer oldWidget) { - super.didUpdateWidget(oldWidget); - lastZoom = -1.0; - _pxCache = generatePxCache(); - } @override Widget build(BuildContext context) { - return StreamBuilder( - stream: widget.stream, - builder: (BuildContext context, AsyncSnapshot snapshot) { - final layerOptions = widget.markerLayerOptions; - final map = widget.map; - final usePxCache = layerOptions.usePxCache; - final markers = []; - final sameZoom = map.zoom == lastZoom; - - final cacheUpdated = updatePxCacheIfNeeded(); - - for (var i = 0; i < layerOptions.markers.length; i++) { - final marker = layerOptions.markers[i]; - - // Decide whether to use cached point or calculate it - final pxPoint = usePxCache && (sameZoom || cacheUpdated) - ? _pxCache[i] - : map.project(marker.point); - if (!sameZoom && usePxCache) { - _pxCache[i] = pxPoint; - } - - // See if any portion of the Marker rect resides in the map bounds - // If not, don't spend any resources on build function. - // This calculation works for any Anchor position whithin the Marker - // Note that Anchor coordinates of (0,0) are at bottom-right of the Marker - // unlike the map coordinates. - final rightPortion = marker.width - marker.anchor.left; - final leftPortion = marker.anchor.left; - final bottomPortion = marker.height - marker.anchor.top; - final topPortion = marker.anchor.top; - - final sw = - CustomPoint(pxPoint.x + leftPortion, pxPoint.y - bottomPortion); - final ne = - CustomPoint(pxPoint.x - rightPortion, pxPoint.y + topPortion); - - if (!map.pixelBounds.containsPartialBounds(Bounds(sw, ne))) { - continue; - } - - final pos = pxPoint - map.getPixelOrigin(); - final markerWidget = (marker.rotate ?? layerOptions.rotate ?? false) - // Counter rotated marker to the map rotation - ? Transform.rotate( - angle: -map.rotationRad, - origin: marker.rotateOrigin ?? layerOptions.rotateOrigin, - alignment: - marker.rotateAlignment ?? layerOptions.rotateAlignment, - child: marker.builder(context), - ) - : marker.builder(context); - - markers.add( - Positioned( - key: marker.key, - width: marker.width, - height: marker.height, - left: pos.x - rightPortion, - top: pos.y - bottomPortion, - child: markerWidget, - ), - ); - } - lastZoom = map.zoom; - return Stack( - children: markers, - ); - }, + final map = FlutterMapState.maybeOf(context)!; + final markers = []; + + for (var i = 0; i < widget.markers.length; i++) { + final marker = widget.markers[i]; + + // print(usePxCache && (sameZoom || cacheUpdated)); + + // Find the position of the point on the screen + final pxPoint = map.project(marker.point); + + // See if any portion of the Marker rect resides in the map bounds + // If not, don't spend any resources on build function. + // This calculation works for any Anchor position whithin the Marker + // Note that Anchor coordinates of (0,0) are at bottom-right of the Marker + // unlike the map coordinates. + final rightPortion = marker.width - marker.anchor.left; + final leftPortion = marker.anchor.left; + final bottomPortion = marker.height - marker.anchor.top; + final topPortion = marker.anchor.top; + + final sw = + CustomPoint(pxPoint.x + leftPortion, pxPoint.y - bottomPortion); + final ne = CustomPoint(pxPoint.x - rightPortion, pxPoint.y + topPortion); + + if (!map.pixelBounds.containsPartialBounds(Bounds(sw, ne))) { + continue; + } + + final pos = pxPoint - map.pixelOrigin; + final markerWidget = (marker.rotate ?? widget.rotate ?? false) + // Counter rotated marker to the map rotation + ? Transform.rotate( + angle: -map.rotationRad, + origin: marker.rotateOrigin ?? widget.rotateOrigin, + alignment: marker.rotateAlignment ?? widget.rotateAlignment, + child: marker.builder(context), + ) + : marker.builder(context); + + markers.add( + Positioned( + key: marker.key, + width: marker.width, + height: marker.height, + left: pos.x - rightPortion, + top: pos.y - bottomPortion, + child: markerWidget, + ), + ); + } + return Stack( + children: markers, ); } } diff --git a/lib/src/layer/overlay_image_layer.dart b/lib/src/layer/overlay_image_layer.dart index 49dfdac64..7d6390d8c 100644 --- a/lib/src/layer/overlay_image_layer.dart +++ b/lib/src/layer/overlay_image_layer.dart @@ -1,21 +1,9 @@ -import 'dart:async'; - import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:flutter_map/src/core/bounds.dart'; import 'package:latlong2/latlong.dart'; -class OverlayImageLayerOptions extends LayerOptions { - final List overlayImages; - - OverlayImageLayerOptions({ - Key? key, - this.overlayImages = const [], - Stream? rebuild, - }) : super(key: key, rebuild: rebuild); -} - /// Base class for all overlay images. abstract class BaseOverlayImage { ImageProvider get imageProvider; @@ -24,7 +12,7 @@ abstract class BaseOverlayImage { bool get gaplessPlayback; - Positioned buildPositionedForOverlay(MapState map); + Positioned buildPositionedForOverlay(FlutterMapState map); Image buildImageForOverlay() { return Image( @@ -57,12 +45,11 @@ class OverlayImage extends BaseOverlayImage { this.gaplessPlayback = false}); @override - Positioned buildPositionedForOverlay(MapState map) { - final pixelOrigin = map.getPixelOrigin(); + Positioned buildPositionedForOverlay(FlutterMapState map) { // northWest is not necessarily upperLeft depending on projection final bounds = Bounds( - map.project(this.bounds.northWest) - pixelOrigin, - map.project(this.bounds.southEast) - pixelOrigin, + map.project(this.bounds.northWest) - map.pixelOrigin, + map.project(this.bounds.southEast) - map.pixelOrigin, ); return Positioned( left: bounds.topLeft.x.toDouble(), @@ -105,12 +92,10 @@ class RotatedOverlayImage extends BaseOverlayImage { this.filterQuality = FilterQuality.medium}); @override - Positioned buildPositionedForOverlay(MapState map) { - final pixelOrigin = map.getPixelOrigin(); - - final pxTopLeft = map.project(topLeftCorner) - pixelOrigin; - final pxBottomRight = map.project(bottomRightCorner) - pixelOrigin; - final pxBottomLeft = (map.project(bottomLeftCorner) - pixelOrigin); + Positioned buildPositionedForOverlay(FlutterMapState map) { + final pxTopLeft = map.project(topLeftCorner) - map.pixelOrigin; + final pxBottomRight = map.project(bottomRightCorner) - map.pixelOrigin; + final pxBottomLeft = map.project(bottomLeftCorner) - map.pixelOrigin; // calculate pixel coordinate of top-right corner by calculating the // vector from bottom-left to top-left and adding it to bottom-right final pxTopRight = (pxTopLeft - pxBottomLeft + pxBottomRight); @@ -144,41 +129,21 @@ class RotatedOverlayImage extends BaseOverlayImage { } } -class OverlayImageLayerWidget extends StatelessWidget { - final OverlayImageLayerOptions options; - - const OverlayImageLayerWidget({Key? key, required this.options}) - : super(key: key); - - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - return OverlayImageLayer(options, mapState, mapState.onMoved); - } -} - class OverlayImageLayer extends StatelessWidget { - final OverlayImageLayerOptions overlayImageOpts; - final MapState map; - final Stream? stream; + final List overlayImages; - OverlayImageLayer(this.overlayImageOpts, this.map, this.stream) - : super(key: overlayImageOpts.key); + const OverlayImageLayer({super.key, this.overlayImages = const []}); @override Widget build(BuildContext context) { - return StreamBuilder( - stream: stream, - builder: (BuildContext context, _) { - return ClipRect( - child: Stack( - children: [ - for (var overlayImage in overlayImageOpts.overlayImages) - overlayImage.buildPositionedForOverlay(map), - ], - ), - ); - }, + final map = FlutterMapState.maybeOf(context)!; + return ClipRect( + child: Stack( + children: [ + for (var overlayImage in overlayImages) + overlayImage.buildPositionedForOverlay(map), + ], + ), ); } } diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index daaccb812..bebffe7dc 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -3,28 +3,9 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/label.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI -class PolygonLayerOptions extends LayerOptions { - final List polygons; - final bool polygonCulling; - - /// screen space culling of polygons based on bounding box - PolygonLayerOptions({ - Key? key, - this.polygons = const [], - this.polygonCulling = false, - Stream? rebuild, - }) : super(key: key, rebuild: rebuild) { - if (polygonCulling) { - for (final polygon in polygons) { - polygon.boundingBox = LatLngBounds.fromPoints(polygon.points); - } - } - } -} - enum PolygonLabelPlacement { centroid, polylabel, @@ -67,42 +48,33 @@ class Polygon { : List.generate(holePointsList.length, (_) => []); } -class PolygonLayerWidget extends StatelessWidget { - final PolygonLayerOptions options; - const PolygonLayerWidget({Key? key, required this.options}) : super(key: key); - - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - return PolygonLayer(options, mapState, mapState.onMoved); - } -} - class PolygonLayer extends StatelessWidget { - final PolygonLayerOptions polygonOpts; - final MapState map; - final Stream? stream; + final List polygons; - PolygonLayer(this.polygonOpts, this.map, this.stream) - : super(key: polygonOpts.key); + /// screen space culling of polygons based on bounding box + final bool polygonCulling; + + PolygonLayer({ + super.key, + this.polygons = const [], + this.polygonCulling = false, + }) { + if (polygonCulling) { + for (final polygon in polygons) { + polygon.boundingBox = LatLngBounds.fromPoints(polygon.points); + } + } + } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints bc) { + final map = FlutterMapState.maybeOf(context)!; final size = Size(bc.maxWidth, bc.maxHeight); - return _build(context, size); - }, - ); - } - - Widget _build(BuildContext context, Size size) { - return StreamBuilder( - stream: stream, // a Stream or null - builder: (BuildContext context, _) { - final polygons = []; + final polygonsWidget = []; - for (final polygon in polygonOpts.polygons) { + for (final polygon in polygons) { polygon.offsets.clear(); if (null != polygon.holeOffsetsList) { @@ -111,23 +83,23 @@ class PolygonLayer extends StatelessWidget { } } - if (polygonOpts.polygonCulling && + if (polygonCulling && !polygon.boundingBox.isOverlapping(map.bounds)) { // skip this polygon as it's offscreen continue; } - _fillOffsets(polygon.offsets, polygon.points); + _fillOffsets(polygon.offsets, polygon.points, map); if (null != polygon.holePointsList) { final len = polygon.holePointsList!.length; for (var i = 0; i < len; ++i) { _fillOffsets( - polygon.holeOffsetsList![i], polygon.holePointsList![i]); + polygon.holeOffsetsList![i], polygon.holePointsList![i], map); } } - polygons.add( + polygonsWidget.add( CustomPaint( painter: PolygonPainter(polygon), size: size, @@ -136,13 +108,14 @@ class PolygonLayer extends StatelessWidget { } return Stack( - children: polygons, + children: polygonsWidget, ); }, ); } - void _fillOffsets(final List offsets, final List points) { + void _fillOffsets( + final List offsets, final List points, FlutterMapState map) { final len = points.length; for (var i = 0; i < len; ++i) { final point = points[i]; diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index e925d8762..190b003ed 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -3,40 +3,9 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; -class PolylineLayerOptions extends LayerOptions { - /// List of polylines to draw. - final List polylines; - - final bool polylineCulling; - - /// {@macro newPolylinePainter.saveLayers} - /// - /// By default, this value is set to `false` to improve performance on - /// layers containing a lot of polylines. - /// - /// You might want to set this to `true` if you get unwanted darker lines - /// where they overlap but, keep in mind that this might reduce the - /// performance of the layer. - final bool saveLayers; - - PolylineLayerOptions({ - Key? key, - this.polylines = const [], - this.polylineCulling = false, - Stream? rebuild, - this.saveLayers = false, - }) : super(key: key, rebuild: rebuild) { - if (polylineCulling) { - for (final polyline in polylines) { - polyline.boundingBox = LatLngBounds.fromPoints(polyline.points); - } - } - } -} - class Polyline { final List points; final List offsets = []; @@ -65,68 +34,69 @@ class Polyline { }); } -class PolylineLayerWidget extends StatelessWidget { - final PolylineLayerOptions options; - - const PolylineLayerWidget({Key? key, required this.options}) - : super(key: key); +class PolylineLayer extends StatelessWidget { + /// List of polylines to draw. + final List polylines; - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - return PolylineLayer(options, mapState, mapState.onMoved); - } -} + final bool polylineCulling; -class PolylineLayer extends StatelessWidget { - final PolylineLayerOptions polylineOpts; - final MapState map; - final Stream? stream; + /// {@macro newPolylinePainter.saveLayers} + /// + /// By default, this value is set to `false` to improve performance on + /// layers containing a lot of polylines. + /// + /// You might want to set this to `true` if you get unwanted darker lines + /// where they overlap but, keep in mind that this might reduce the + /// performance of the layer. + final bool saveLayers; - PolylineLayer(this.polylineOpts, this.map, this.stream) - : super(key: polylineOpts.key); + PolylineLayer({ + super.key, + this.polylines = const [], + this.polylineCulling = false, + this.saveLayers = false, + }) { + if (polylineCulling) { + for (final polyline in polylines) { + polyline.boundingBox = LatLngBounds.fromPoints(polyline.points); + } + } + } @override Widget build(BuildContext context) { + final map = FlutterMapState.maybeOf(context)!; return LayoutBuilder( builder: (BuildContext context, BoxConstraints bc) { final size = Size(bc.maxWidth, bc.maxHeight); - return _build(context, size); - }, - ); - } - - Widget _build(BuildContext context, Size size) { - return StreamBuilder( - stream: stream, // a Stream or null - builder: (BuildContext context, _) { - final polylines = []; + final polylineWidgets = []; - for (final polylineOpt in polylineOpts.polylines) { + for (final polylineOpt in polylines) { polylineOpt.offsets.clear(); - if (polylineOpts.polylineCulling && + if (polylineCulling && !polylineOpt.boundingBox.isOverlapping(map.bounds)) { // skip this polyline as it's offscreen continue; } - _fillOffsets(polylineOpt.offsets, polylineOpt.points); + _fillOffsets(polylineOpt.offsets, polylineOpt.points, map); - polylines.add(CustomPaint( - painter: PolylinePainter(polylineOpt, polylineOpts.saveLayers), + polylineWidgets.add(CustomPaint( + painter: PolylinePainter(polylineOpt, saveLayers), size: size, )); } return Stack( - children: polylines, + children: polylineWidgets, ); }, ); } - void _fillOffsets(final List offsets, final List points) { + void _fillOffsets( + final List offsets, final List points, FlutterMapState map) { final len = points.length; for (var i = 0; i < len; ++i) { final point = points[i]; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index ff246fd93..835e1351a 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -12,54 +12,317 @@ import 'package:flutter_map/src/layer/tile_layer/tile_manager.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_transformation.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_widget.dart'; import 'package:flutter_map/src/layer/tile_layer/transformation_calculator.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; import 'package:tuple/tuple.dart'; part 'tile_layer_options.dart'; -class TileLayerWidget extends StatelessWidget { - final TileLayerOptions options; - - const TileLayerWidget({Key? key, required this.options}) : super(key: key); - - @override - Widget build(BuildContext context) { - final mapState = MapState.maybeOf(context)!; - - return TileLayer( - mapState: mapState, - stream: mapState.onMoved, - options: options, - ); - } -} - +/// Describes the needed properties to create a tile-based layer. A tile is an +/// image bound to a specific geographical position. +/// +/// You should read up about the options by exploring each one, or visiting +/// https://docs.fleaflet.dev/usage/layers/tile-layer. Some are important to +/// avoid issues. class TileLayer extends StatefulWidget { - final TileLayerOptions options; - final MapState mapState; - final Stream stream; + + /// Defines the structure to create the URLs for the tiles. `{s}` means one of + /// the available subdomains (can be omitted) `{z}` zoom level `{x}` and `{y}` + /// — tile coordinates `{r}` can be used to add "@2x" to the URL to + /// load retina tiles (can be omitted) + /// + /// Example: + /// + /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png + /// + /// Is translated to this: + /// + /// https://a.tile.openstreetmap.org/12/2177/1259.png + final String? urlTemplate; + + /// If `true`, inverses Y axis numbering for tiles (turn this on for + /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services). + final bool tms; + + /// If not `null`, then tiles will pull's WMS protocol requests + final WMSTileLayerOptions? wmsOptions; + + /// Size for the tile. + /// Default is 256 + final double tileSize; + + // The minimum zoom level down to which this layer will be + // displayed (inclusive). + final double minZoom; + + /// The maximum zoom level up to which this layer will be displayed + /// (inclusive). In most tile providers goes from 0 to 19. + final double maxZoom; + + /// Minimum zoom number the tile source has available. If it is specified, the + /// tiles on all zoom levels lower than minNativeZoom will be loaded from + /// minNativeZoom level and auto-scaled. + final double? minNativeZoom; + + /// Maximum zoom number the tile source has available. If it is specified, the + /// tiles on all zoom levels higher than maxNativeZoom will be loaded from + /// maxNativeZoom level and auto-scaled. + final double? maxNativeZoom; + + /// If set to true, the zoom number used in tile URLs will be reversed + /// (`maxZoom - zoom` instead of `zoom`) + final bool zoomReverse; + + /// The zoom number used in tile URLs will be offset with this value. + final double zoomOffset; + + /// List of subdomains for the URL. + /// + /// Example: + /// + /// Subdomains = {a,b,c} + /// + /// and the URL is as follows: + /// + /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png + /// + /// then: + /// + /// https://a.tile.openstreetmap.org/{z}/{x}/{y}.png + /// https://b.tile.openstreetmap.org/{z}/{x}/{y}.png + /// https://c.tile.openstreetmap.org/{z}/{x}/{y}.png + final List subdomains; + + /// Color shown behind the tiles + final Color backgroundColor; + + /// Opacity of the rendered tile + final double opacity; + + /// Provider with which to load map tiles + /// + /// The default is [NetworkNoRetryTileProvider]. Alternatively, use + /// [NetworkTileProvider] for a network provider which will retry requests. + /// + /// Both network providers will use some form of caching, although not reliable. For + /// better options, see https://docs.fleaflet.dev/usage/layers/tile-layer#caching. + /// + /// `userAgentPackageName` is a construction parameter, which should be passed + /// the application's correct package name, such as 'com.example.app'. If no + /// value is passed, it defaults to 'unknown'. This parameter is used to form + /// part of the 'User-Agent' header, which is important to avoid blocking by + /// tile servers. Namely, the header is the following 'flutter_map ()'. + /// + /// Header rules are as follows, after 'User-Agent' is generated as above: + /// + /// * If no provider is specified here, the default will be used with + /// 'User-Agent' header injected (recommended) + /// * If a provider is specified here with no 'User-Agent' header, that + /// provider will be used and the 'User-Agent' header will be injected + /// * If a provider is specified here with a 'User-Agent' header, that + /// provider will be used and the 'User-Agent' header will not be changed to any created here + /// + /// [AssetTileProvider] and [FileTileProvider] are alternatives to network + /// providers, which use the [urlTemplate] as a path instead. + /// For example, 'assets/map/{z}/{x}/{y}.png' or + /// '/storage/emulated/0/map_app/tiles/{z}/{x}/{y}.png'. + /// + /// Custom [TileProvider]s can also be used, but these will not follow the header + /// rules above. + final TileProvider tileProvider; + + /// When panning the map, keep this many rows and columns of tiles before + /// unloading them. + final int keepBuffer; + + /// Tile image to show in place of the tile that failed to load. + final ImageProvider? errorImage; + + /// Static information that should replace placeholders in the [urlTemplate]. + /// Applying API keys is a good example on how to use this parameter. + /// + /// Example: + /// + /// ```dart + /// + /// TileLayerOptions( + /// urlTemplate: "https://api.tiles.mapbox.com/v4/" + /// "{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}", + /// additionalOptions: { + /// 'accessToken': '', + /// 'id': 'mapbox.streets', + /// }, + /// ), + /// ``` + final Map additionalOptions; + + /// Tiles will not update more than once every `updateInterval` (default 200 + /// milliseconds) when panning. It can be null (but it will calculating for + /// loading tiles every frame when panning / zooming, flutter is fast) This + /// can save some fps and even bandwidth (ie. when fast panning / animating + /// between long distances in short time) + final Duration? updateInterval; + + /// Tiles fade in duration in milliseconds (default 100). This can be null to + /// avoid fade in. + final Duration? tileFadeInDuration; + + /// Opacity start value when Tile starts fade in (0.0 - 1.0) Takes effect if + /// `tileFadeInDuration` is not null + final double tileFadeInStart; + + /// Opacity start value when an exists Tile starts fade in with different Url + /// (0.0 - 1.0) Takes effect when `tileFadeInDuration` is not null and if + /// `overrideTilesWhenUrlChanges` if true + final double tileFadeInStartWhenOverride; + + /// `false`: current Tiles will be first dropped and then reload via new url + /// (default) `true`: current Tiles will be visible until new ones aren't + /// loaded (new Tiles are loaded independently) @see + /// https://github.com/johnpryan/flutter_map/issues/583 + final bool overrideTilesWhenUrlChanges; + + /// If `true`, it will request four tiles of half the specified size and a + /// bigger zoom level in place of one to utilize the high resolution. + /// + /// If `true` then MapOptions's `maxZoom` should be `maxZoom - 1` since + /// retinaMode just simulates retina display by playing with `zoomOffset`. If + /// geoserver supports retina `@2` tiles then it it advised to use them + /// instead of simulating it (use {r} in the [urlTemplate]) + /// + /// It is advised to use retinaMode if display supports it, write code like + /// this: + /// + /// ```dart + /// TileLayerOptions( + /// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0, + /// ), + /// ``` + final bool retinaMode; + + /// This callback will be execute if some errors occur when fetching tiles. + final ErrorTileCallBack? errorTileCallback; + + final TemplateFunction templateFunction; + + /// Function which may Wrap Tile with custom Widget + /// There are predefined examples in 'tile_builder.dart' + final TileBuilder? tileBuilder; + + /// Function which may wrap Tiles Container with custom Widget + /// There are predefined examples in 'tile_builder.dart' + final TilesContainerBuilder? tilesContainerBuilder; + + // If a Tile was loaded with error and if strategy isn't `none` then TileProvider + // will be asked to evict Image based on current strategy + // (see #576 - even Error Images are cached in flutter) + final EvictErrorTileStrategy evictErrorTileStrategy; + + /// This option is useful when you have a transparent layer: rather than + /// keeping the old layer visible when zooming (resulting in both layers + /// being temporarily visible), the old layer is removed as quickly as + /// possible when this is set to `true` (default `false`). + /// + /// This option is likely to cause some flickering of the transparent layer, + /// most noticeable when using pinch-to-zoom. It's best used with maps that + /// have `interactive` set to `false`, and zoom using buttons that call + /// `MapController.move()`. + /// + /// When set to `true`, the `tileFadeIn*` options will be ignored. + final bool fastReplace; + + /// Stream to notify the [TileLayer] that it needs resetting + final Stream? reset; + + /// Only load tiles that are within these bounds + final LatLngBounds? tileBounds; TileLayer({ - required this.options, - required this.mapState, - required this.stream, - }) : super(key: options.key); + super.key, + this.urlTemplate, + double tileSize = 256.0, + double minZoom = 0.0, + double maxZoom = 18.0, + this.minNativeZoom, + this.maxNativeZoom, + this.zoomReverse = false, + double zoomOffset = 0.0, + Map? additionalOptions, + this.subdomains = const [], + this.keepBuffer = 2, + this.backgroundColor = const Color(0xFFE0E0E0), + this.errorImage, + TileProvider? tileProvider, + this.tms = false, + this.wmsOptions, + this.opacity = 1.0, + + /// Tiles will not update more than once every `updateInterval` milliseconds + /// (default 200) when panning. It can be 0 (but it will calculating for + /// loading tiles every frame when panning / zooming, flutter is fast) This + /// can save some fps and even bandwidth (ie. when fast panning / animating + /// between long distances in short time) + Duration updateInterval = const Duration(milliseconds: 200), + Duration tileFadeInDuration = const Duration(milliseconds: 100), + this.tileFadeInStart = 0.0, + this.tileFadeInStartWhenOverride = 0.0, + this.overrideTilesWhenUrlChanges = false, + this.retinaMode = false, + this.errorTileCallback, + this.templateFunction = util.template, + this.tileBuilder, + this.tilesContainerBuilder, + this.evictErrorTileStrategy = EvictErrorTileStrategy.none, + this.fastReplace = false, + this.reset, + this.tileBounds, + String userAgentPackageName = 'unknown', + }) : updateInterval = + updateInterval <= Duration.zero ? null : updateInterval, + tileFadeInDuration = + tileFadeInDuration <= Duration.zero ? null : tileFadeInDuration, + assert(tileFadeInStart >= 0.0 && tileFadeInStart <= 1.0), + assert(tileFadeInStartWhenOverride >= 0.0 && + tileFadeInStartWhenOverride <= 1.0), + maxZoom = + wmsOptions == null && retinaMode && maxZoom > 0.0 && !zoomReverse + ? maxZoom - 1.0 + : maxZoom, + minZoom = + wmsOptions == null && retinaMode && maxZoom > 0.0 && zoomReverse + ? math.max(minZoom + 1.0, 0) + : minZoom, + zoomOffset = wmsOptions == null && retinaMode && maxZoom > 0.0 + ? (zoomReverse ? zoomOffset - 1.0 : zoomOffset + 1.0) + : zoomOffset, + tileSize = wmsOptions == null && retinaMode && maxZoom > 0.0 + ? (tileSize / 2.0).floorToDouble() + : tileSize, + additionalOptions = additionalOptions == null + ? const {} + : Map.from(additionalOptions), + tileProvider = tileProvider == null + ? NetworkNoRetryTileProvider( + headers: {'User-Agent': 'flutter_map ($userAgentPackageName)'}, + ) + : (tileProvider + ..headers = { + ...tileProvider.headers, + if (!tileProvider.headers.containsKey('User-Agent')) + 'User-Agent': 'flutter_map ($userAgentPackageName)', + }); @override State createState() => _TileLayerState(); } class _TileLayerState extends State with TickerProviderStateMixin { - MapState get map => widget.mapState; - TileLayerOptions get options => widget.options; late Bounds _globalTileRange; Tuple2? _wrapX; Tuple2? _wrapY; double? _tileZoom; - StreamSubscription? _moveSub; StreamSubscription? _resetSub; StreamController? _throttleUpdate; late CustomPoint _tileSize; @@ -74,16 +337,14 @@ class _TileLayerState extends State with TickerProviderStateMixin { super.initState(); _tileManager = TileManager(); _transformationCalculator = TransformationCalculator(); - _tileSize = CustomPoint(options.tileSize, options.tileSize); - _resetView(); - _update(null); - _moveSub = widget.stream.listen((_) => _handleMove()); + _tileSize = CustomPoint(widget.tileSize, widget.tileSize); - if (options.reset != null) { - _resetSub = options.reset?.listen((_) => _resetTiles()); + if (widget.reset != null) { + _resetSub = widget.reset?.listen((_) => _resetTiles()); } - _initThrottleUpdate(); + //TODO fix + // _initThrottleUpdate(); } @override @@ -91,36 +352,37 @@ class _TileLayerState extends State with TickerProviderStateMixin { super.didUpdateWidget(oldWidget); var reloadTiles = false; - if (oldWidget.options.tileSize != options.tileSize) { - _tileSize = CustomPoint(options.tileSize, options.tileSize); + if (oldWidget.tileSize != widget.tileSize) { + _tileSize = CustomPoint(widget.tileSize, widget.tileSize); reloadTiles = true; } - if (oldWidget.options.retinaMode != options.retinaMode) { + if (oldWidget.retinaMode != widget.retinaMode) { reloadTiles = true; } reloadTiles |= - !_tileManager.allWithinZoom(options.minZoom, options.maxZoom); + !_tileManager.allWithinZoom(widget.minZoom, widget.maxZoom); - if (oldWidget.options.updateInterval != options.updateInterval) { + if (oldWidget.updateInterval != widget.updateInterval) { _throttleUpdate?.close(); - _initThrottleUpdate(); + //TODO fix + // _initThrottleUpdate(); } if (!reloadTiles) { - final oldUrl = oldWidget.options.wmsOptions?._encodedBaseUrl ?? - oldWidget.options.urlTemplate; - final newUrl = options.wmsOptions?._encodedBaseUrl ?? options.urlTemplate; + final oldUrl = oldWidget.wmsOptions?._encodedBaseUrl ?? + oldWidget.urlTemplate; + final newUrl = widget.wmsOptions?._encodedBaseUrl ?? widget.urlTemplate; - final oldOptions = oldWidget.options.additionalOptions; - final newOptions = options.additionalOptions; + final oldOptions = oldWidget.additionalOptions; + final newOptions = widget.additionalOptions; if (oldUrl != newUrl || !(const MapEquality()) .equals(oldOptions, newOptions)) { - if (options.overrideTilesWhenUrlChanges) { - _tileManager.reloadImages(options, _wrapX, _wrapY); + if (widget.overrideTilesWhenUrlChanges) { + _tileManager.reloadImages(widget, _wrapX, _wrapY); } else { reloadTiles = true; } @@ -128,34 +390,32 @@ class _TileLayerState extends State with TickerProviderStateMixin { } if (reloadTiles) { - _tileManager.removeAll(options.evictErrorTileStrategy); - _resetView(); - _update(null); + _tileManager.removeAll(widget.evictErrorTileStrategy); } } - void _initThrottleUpdate() { - if (options.updateInterval == null) { - _throttleUpdate = null; - } else { - _throttleUpdate = StreamController(sync: true); - _throttleUpdate!.stream - .transform( - util.throttleStreamTransformerWithTrailingCall( - options.updateInterval!, - ), - ) - .listen(_update); - } - } +//TODO fix + // void _initThrottleUpdate() { + // if (widget.updateInterval == null) { + // _throttleUpdate = null; + // } else { + // _throttleUpdate = StreamController(sync: true); + // _throttleUpdate!.stream + // .transform( + // util.throttleStreamTransformerWithTrailingCall( + // widget.updateInterval!, + // ), + // ) + // .listen(_update); + // } + // } @override void dispose() { - _tileManager.removeAll(options.evictErrorTileStrategy); + _tileManager.removeAll(widget.evictErrorTileStrategy); _resetSub?.cancel(); - _moveSub?.cancel(); _pruneLater?.cancel(); - options.tileProvider.dispose(); + widget.tileProvider.dispose(); _throttleUpdate?.close(); super.dispose(); @@ -163,58 +423,81 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return StreamBuilder( - stream: widget.stream, - builder: (context, snapshot) { - final tilesToRender = _tileZoom == null - ? _tileManager.all() - : _tileManager.sortedByDistanceToZoomAscending( - options.maxZoom, _tileZoom!); - final Map zoomToTransformation = {}; - - final tileWidgets = [ - for (var tile in tilesToRender) - TileWidget( - tile: tile, - size: _tileSize, - tileTransformation: zoomToTransformation[tile.coords.z] ?? - (zoomToTransformation[tile.coords.z] = - _transformationCalculator.transformationFor( - tile.coords.z, - map, - )), - errorImage: options.errorImage, - tileBuilder: options.tileBuilder, - key: ValueKey(tile.coordsKey), - ) - ]; - - final tilesContainer = Stack( - children: tileWidgets, - ); - - final tilesLayer = options.tilesContainerBuilder == null - ? tilesContainer - : options.tilesContainerBuilder!( - context, - tilesContainer, - tilesToRender, - ); - - return Opacity( - opacity: options.opacity, - child: Container( - color: options.backgroundColor, - child: tilesLayer, - ), - ); - }, + final map = FlutterMapState.maybeOf(context)!; + + //Handle movement + final tileZoom = _clampZoom(map.zoom.roundToDouble()); + + if (_tileZoom == null) { + // if there is no _tileZoom available it means we are out within zoom level + // we will restore fully via _setView call if we are back on trail + if ((tileZoom <= widget.maxZoom) && (tileZoom >= widget.minZoom)) { + _tileZoom = tileZoom; + _setView(map, map.center, tileZoom); + } + } else { + if ((tileZoom - _tileZoom!).abs() >= 1) { + // It was a zoom lvl change + _setView(map, map.center, tileZoom); + } else { + if (_throttleUpdate == null) { + //TODO what is this for? + // _update(null); + } else { + _throttleUpdate!.add(null); + } + } + } + + _setView(map, map.center, map.zoom); + + final tilesToRender = _tileZoom == null + ? _tileManager.all() + : _tileManager.sortedByDistanceToZoomAscending( + widget.maxZoom, _tileZoom!); + final Map zoomToTransformation = {}; + + final tileWidgets = [ + for (var tile in tilesToRender) + TileWidget( + tile: tile, + size: _tileSize, + tileTransformation: zoomToTransformation[tile.coords.z] ?? + (zoomToTransformation[tile.coords.z] = + _transformationCalculator.transformationFor( + tile.coords.z, + map, + )), + errorImage: widget.errorImage, + tileBuilder: widget.tileBuilder, + key: ValueKey(tile.coordsKey), + ) + ]; + + final tilesContainer = Stack( + children: tileWidgets, + ); + + final tilesLayer = widget.tilesContainerBuilder == null + ? tilesContainer + : widget.tilesContainerBuilder!( + context, + tilesContainer, + tilesToRender, + ); + + return Opacity( + opacity: widget.opacity, + child: Container( + color: widget.backgroundColor, + child: tilesLayer, + ), ); } CustomPoint getTileSize() => _tileSize; - Level? _updateLevels() { + Level? _updateLevels(FlutterMapState map) { final zoom = _tileZoom; if (zoom == null) return null; @@ -223,7 +506,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { levelZoom != zoom && !_tileManager.anyWithZoomLevel(levelZoom)); for (final z in toRemove) { - _tileManager.removeAtZoom(z, options.evictErrorTileStrategy); + _tileManager.removeAtZoom(z, widget.evictErrorTileStrategy); _transformationCalculator.removeLevel(z); } @@ -232,48 +515,42 @@ class _TileLayerState extends State with TickerProviderStateMixin { ///removes all loaded tiles and resets the view void _resetTiles() { - _tileManager.removeAll(options.evictErrorTileStrategy); - _resetView(); - } - - void _resetView() { - _setView(map.center, map.zoom); + _tileManager.removeAll(widget.evictErrorTileStrategy); } double _clampZoom(double zoom) { - if (null != options.minNativeZoom && zoom < options.minNativeZoom!) { - return options.minNativeZoom!; + if (null != widget.minNativeZoom && zoom < widget.minNativeZoom!) { + return widget.minNativeZoom!; } - if (null != options.maxNativeZoom && options.maxNativeZoom! < zoom) { - return options.maxNativeZoom!; + if (null != widget.maxNativeZoom && widget.maxNativeZoom! < zoom) { + return widget.maxNativeZoom!; } return zoom; } - void _setView(LatLng center, double zoom) { + void _setView(FlutterMapState map, LatLng center, double zoom) { double? tileZoom = _clampZoom(zoom.roundToDouble()); - if ((tileZoom > options.maxZoom) || (tileZoom < options.minZoom)) { + if ((tileZoom > widget.maxZoom) || (tileZoom < widget.minZoom)) { tileZoom = null; } _tileZoom = tileZoom; - _tileManager.abortLoading(_tileZoom, options.evictErrorTileStrategy); + _tileManager.abortLoading(_tileZoom, widget.evictErrorTileStrategy); - _updateLevels(); - _resetGrid(); + _updateLevels(map); + _resetGrid(map); if (_tileZoom != null) { - _update(center); + _update(map, center); } - _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); + _tileManager.prune(_tileZoom, widget.evictErrorTileStrategy); } - void _resetGrid() { - final map = this.map; + void _resetGrid(FlutterMapState map) { final crs = map.options.crs; final tileSize = getTileSize(); final tileZoom = _tileZoom; @@ -307,35 +584,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { } } - void _handleMove() { - final tileZoom = _clampZoom(map.zoom.roundToDouble()); - if (_tileZoom == null) { - // if there is no _tileZoom available it means we are out within zoom level - // we will restore fully via _setView call if we are back on trail - if ((tileZoom <= options.maxZoom) && (tileZoom >= options.minZoom)) { - _tileZoom = tileZoom; - setState(() { - _setView(map.center, tileZoom); - }); - } - } else { - setState(() { - if ((tileZoom - _tileZoom!).abs() >= 1) { - // It was a zoom lvl change - _setView(map.center, tileZoom); - } else { - if (_throttleUpdate == null) { - _update(null); - } else { - _throttleUpdate!.add(null); - } - } - }); - } - } - - Bounds _getTiledPixelBounds(LatLng center) { + Bounds _getTiledPixelBounds(FlutterMapState map, LatLng center) { final scale = map.getZoomScale(map.zoom, _tileZoom); final pixelCenter = map.project(center, _tileZoom).floor(); final halfSize = map.size / (scale * 2); @@ -345,7 +595,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { // Private method to load tiles in the grid's active zoom level according to // map bounds - void _update(LatLng? center) { + void _update(FlutterMapState map, LatLng? center) { if (_tileZoom == null) { return; } @@ -353,11 +603,11 @@ class _TileLayerState extends State with TickerProviderStateMixin { final zoom = _clampZoom(map.zoom); center ??= map.center; - final pixelBounds = _getTiledPixelBounds(center); + final pixelBounds = _getTiledPixelBounds(map, center); final tileRange = _pxBoundsToTileRange(pixelBounds); final tileCenter = tileRange.center; final queue = >[]; - final margin = options.keepBuffer; + final margin = widget.keepBuffer; final noPruneRange = Bounds( tileRange.bottomLeft - CustomPoint(margin, -margin), tileRange.topRight + CustomPoint(margin, -margin), @@ -368,7 +618,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { // _update just loads more tiles. If the tile zoom level differs too much // from the map's, let _setView reset levels and prune old tiles. if ((zoom - _tileZoom!).abs() > 1) { - _setView(center, zoom); + _setView(map, center, zoom); return; } @@ -378,15 +628,15 @@ class _TileLayerState extends State with TickerProviderStateMixin { final coords = Coords(i.toDouble(), j.toDouble()); coords.z = _tileZoom!; - if (options.tileBounds != null) { + if (widget.tileBounds != null) { final tilePxBounds = _pxBoundsToTileRange( - _latLngBoundsToPixelBounds(options.tileBounds!, _tileZoom!)); + _latLngBoundsToPixelBounds(map, widget.tileBounds!, _tileZoom!)); if (!_areCoordsInsideTileBounds(coords, tilePxBounds)) { continue; } } - if (!_isValidTile(coords)) { + if (!_isValidTile(map.options.crs, coords)) { continue; } @@ -397,7 +647,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } _tileManager.evictErrorTilesBasedOnStrategy( - tileRange, options.evictErrorTileStrategy); + tileRange, widget.evictErrorTileStrategy); // sort tile queue to load tiles in order of their distance to center queue.sort((a, b) => @@ -406,10 +656,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { for (final coords in queue) { final newTile = Tile( coords: coords, - tilePos: _getTilePos(coords), + tilePos: _getTilePos(map, coords), current: true, imageProvider: - options.tileProvider.getImage(coords.wrap(_wrapX, _wrapY), options), + widget.tileProvider.getImage(coords.wrap(_wrapX, _wrapY), widget), tileReady: _tileReady, ); @@ -421,9 +671,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } } - bool _isValidTile(Coords coords) { - final crs = map.options.crs; - + bool _isValidTile(Crs crs, Coords coords) { if (!crs.infinite) { // don't load tile if it's out of bounds and not wrapped final bounds = _globalTileRange; @@ -447,7 +695,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { return true; } - Bounds _latLngBoundsToPixelBounds(LatLngBounds bounds, double thisZoom) { + 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 pxBounds = Bounds(swPixel, nePixel); @@ -460,8 +708,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { tile!.loadError = true; - if (options.errorTileCallback != null) { - options.errorTileCallback!(tile, error); + if (widget.errorTileCallback != null) { + widget.errorTileCallback!(tile, error); } } else { tile!.loadError = false; @@ -470,29 +718,29 @@ class _TileLayerState extends State with TickerProviderStateMixin { tile = _tileManager.tileAt(tile.coords); if (tile == null) return; - if (options.fastReplace && mounted) { + if (widget.fastReplace && mounted) { setState(() { tile!.active = true; if (_tileManager.allLoaded) { // We're not waiting for anything, prune the tiles immediately. - _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); + _tileManager.prune(_tileZoom, widget.evictErrorTileStrategy); } }); return; } final fadeInStart = tile.loaded == null - ? options.tileFadeInStart - : options.tileFadeInStartWhenOverride; + ? widget.tileFadeInStart + : widget.tileFadeInStartWhenOverride; tile.loaded = DateTime.now(); - if (options.tileFadeInDuration == null || + if (widget.tileFadeInDuration == null || fadeInStart == 1.0 || - (tile.loadError && null == options.errorImage)) { + (tile.loadError && null == widget.errorImage)) { tile.active = true; } else { tile.startFadeInAnimation( - options.tileFadeInDuration!, + widget.tileFadeInDuration!, this, from: fadeInStart, ); @@ -507,13 +755,13 @@ class _TileLayerState extends State with TickerProviderStateMixin { // fade-in) to trigger a pruning. _pruneLater?.cancel(); _pruneLater = Timer( - options.tileFadeInDuration != null - ? options.tileFadeInDuration! + const Duration(milliseconds: 50) + widget.tileFadeInDuration != null + ? widget.tileFadeInDuration! + const Duration(milliseconds: 50) : const Duration(milliseconds: 50), () { if (mounted) { setState(() { - _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); + _tileManager.prune(_tileZoom, widget.evictErrorTileStrategy); }); } }, @@ -521,7 +769,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } } - CustomPoint _getTilePos(Coords coords) { + CustomPoint _getTilePos(FlutterMapState map, Coords coords) { final level = _transformationCalculator.getOrCreateLevel(coords.z as double, map); return coords.scaleBy(getTileSize()) - level.origin; diff --git a/lib/src/layer/tile_layer/tile_layer_options.dart b/lib/src/layer/tile_layer/tile_layer_options.dart index af76b482d..443e584e5 100644 --- a/lib/src/layer/tile_layer/tile_layer_options.dart +++ b/lib/src/layer/tile_layer/tile_layer_options.dart @@ -18,302 +18,6 @@ enum EvictErrorTileStrategy { typedef ErrorTileCallBack = void Function(Tile tile, dynamic error); -/// Describes the needed properties to create a tile-based layer. A tile is an -/// image bound to a specific geographical position. -/// -/// You should read up about the options by exploring each one, or visiting -/// https://docs.fleaflet.dev/usage/layers/tile-layer. Some are important to -/// avoid issues. -class TileLayerOptions extends LayerOptions { - /// Defines the structure to create the URLs for the tiles. `{s}` means one of - /// the available subdomains (can be omitted) `{z}` zoom level `{x}` and `{y}` - /// — tile coordinates `{r}` can be used to add "@2x" to the URL to - /// load retina tiles (can be omitted) - /// - /// Example: - /// - /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png - /// - /// Is translated to this: - /// - /// https://a.tile.openstreetmap.org/12/2177/1259.png - final String? urlTemplate; - - /// If `true`, inverses Y axis numbering for tiles (turn this on for - /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services). - final bool tms; - - /// If not `null`, then tiles will pull's WMS protocol requests - final WMSTileLayerOptions? wmsOptions; - - /// Size for the tile. - /// Default is 256 - final double tileSize; - - // The minimum zoom level down to which this layer will be - // displayed (inclusive). - final double minZoom; - - /// The maximum zoom level up to which this layer will be displayed - /// (inclusive). In most tile providers goes from 0 to 19. - final double maxZoom; - - /// Minimum zoom number the tile source has available. If it is specified, the - /// tiles on all zoom levels lower than minNativeZoom will be loaded from - /// minNativeZoom level and auto-scaled. - final double? minNativeZoom; - - /// Maximum zoom number the tile source has available. If it is specified, the - /// tiles on all zoom levels higher than maxNativeZoom will be loaded from - /// maxNativeZoom level and auto-scaled. - final double? maxNativeZoom; - - /// If set to true, the zoom number used in tile URLs will be reversed - /// (`maxZoom - zoom` instead of `zoom`) - final bool zoomReverse; - - /// The zoom number used in tile URLs will be offset with this value. - final double zoomOffset; - - /// List of subdomains for the URL. - /// - /// Example: - /// - /// Subdomains = {a,b,c} - /// - /// and the URL is as follows: - /// - /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png - /// - /// then: - /// - /// https://a.tile.openstreetmap.org/{z}/{x}/{y}.png - /// https://b.tile.openstreetmap.org/{z}/{x}/{y}.png - /// https://c.tile.openstreetmap.org/{z}/{x}/{y}.png - final List subdomains; - - /// Color shown behind the tiles - final Color backgroundColor; - - /// Opacity of the rendered tile - final double opacity; - - /// Provider with which to load map tiles - /// - /// The default is [NetworkNoRetryTileProvider]. Alternatively, use - /// [NetworkTileProvider] for a network provider which will retry requests. - /// - /// Both network providers will use some form of caching, although not reliable. For - /// better options, see https://docs.fleaflet.dev/usage/layers/tile-layer#caching. - /// - /// `userAgentPackageName` is a construction parameter, which should be passed - /// the application's correct package name, such as 'com.example.app'. If no - /// value is passed, it defaults to 'unknown'. This parameter is used to form - /// part of the 'User-Agent' header, which is important to avoid blocking by - /// tile servers. Namely, the header is the following 'flutter_map ()'. - /// - /// Header rules are as follows, after 'User-Agent' is generated as above: - /// - /// * If no provider is specified here, the default will be used with - /// 'User-Agent' header injected (recommended) - /// * If a provider is specified here with no 'User-Agent' header, that - /// provider will be used and the 'User-Agent' header will be injected - /// * If a provider is specified here with a 'User-Agent' header, that - /// provider will be used and the 'User-Agent' header will not be changed to any created here - /// - /// [AssetTileProvider] and [FileTileProvider] are alternatives to network - /// providers, which use the [urlTemplate] as a path instead. - /// For example, 'assets/map/{z}/{x}/{y}.png' or - /// '/storage/emulated/0/map_app/tiles/{z}/{x}/{y}.png'. - /// - /// Custom [TileProvider]s can also be used, but these will not follow the header - /// rules above. - late final TileProvider tileProvider; - - /// When panning the map, keep this many rows and columns of tiles before - /// unloading them. - final int keepBuffer; - - /// Tile image to show in place of the tile that failed to load. - final ImageProvider? errorImage; - - /// Static information that should replace placeholders in the [urlTemplate]. - /// Applying API keys is a good example on how to use this parameter. - /// - /// Example: - /// - /// ```dart - /// - /// TileLayerOptions( - /// urlTemplate: "https://api.tiles.mapbox.com/v4/" - /// "{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}", - /// additionalOptions: { - /// 'accessToken': '', - /// 'id': 'mapbox.streets', - /// }, - /// ), - /// ``` - final Map additionalOptions; - - /// Tiles will not update more than once every `updateInterval` (default 200 - /// milliseconds) when panning. It can be null (but it will calculating for - /// loading tiles every frame when panning / zooming, flutter is fast) This - /// can save some fps and even bandwidth (ie. when fast panning / animating - /// between long distances in short time) - final Duration? updateInterval; - - /// Tiles fade in duration in milliseconds (default 100). This can be null to - /// avoid fade in. - final Duration? tileFadeInDuration; - - /// Opacity start value when Tile starts fade in (0.0 - 1.0) Takes effect if - /// `tileFadeInDuration` is not null - final double tileFadeInStart; - - /// Opacity start value when an exists Tile starts fade in with different Url - /// (0.0 - 1.0) Takes effect when `tileFadeInDuration` is not null and if - /// `overrideTilesWhenUrlChanges` if true - final double tileFadeInStartWhenOverride; - - /// `false`: current Tiles will be first dropped and then reload via new url - /// (default) `true`: current Tiles will be visible until new ones aren't - /// loaded (new Tiles are loaded independently) @see - /// https://github.com/johnpryan/flutter_map/issues/583 - final bool overrideTilesWhenUrlChanges; - - /// If `true`, it will request four tiles of half the specified size and a - /// bigger zoom level in place of one to utilize the high resolution. - /// - /// If `true` then MapOptions's `maxZoom` should be `maxZoom - 1` since - /// retinaMode just simulates retina display by playing with `zoomOffset`. If - /// geoserver supports retina `@2` tiles then it it advised to use them - /// instead of simulating it (use {r} in the [urlTemplate]) - /// - /// It is advised to use retinaMode if display supports it, write code like - /// this: - /// - /// ```dart - /// TileLayerOptions( - /// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0, - /// ), - /// ``` - final bool retinaMode; - - /// This callback will be execute if some errors occur when fetching tiles. - final ErrorTileCallBack? errorTileCallback; - - final TemplateFunction templateFunction; - - /// Function which may Wrap Tile with custom Widget - /// There are predefined examples in 'tile_builder.dart' - final TileBuilder? tileBuilder; - - /// Function which may wrap Tiles Container with custom Widget - /// There are predefined examples in 'tile_builder.dart' - final TilesContainerBuilder? tilesContainerBuilder; - - // If a Tile was loaded with error and if strategy isn't `none` then TileProvider - // will be asked to evict Image based on current strategy - // (see #576 - even Error Images are cached in flutter) - final EvictErrorTileStrategy evictErrorTileStrategy; - - /// This option is useful when you have a transparent layer: rather than - /// keeping the old layer visible when zooming (resulting in both layers - /// being temporarily visible), the old layer is removed as quickly as - /// possible when this is set to `true` (default `false`). - /// - /// This option is likely to cause some flickering of the transparent layer, - /// most noticeable when using pinch-to-zoom. It's best used with maps that - /// have `interactive` set to `false`, and zoom using buttons that call - /// `MapController.move()`. - /// - /// When set to `true`, the `tileFadeIn*` options will be ignored. - final bool fastReplace; - - /// Stream to notify the [TileLayer] that it needs resetting - Stream? reset; - - /// Only load tiles that are within these bounds - LatLngBounds? tileBounds; - - TileLayerOptions({ - Key? key, - this.urlTemplate, - double tileSize = 256.0, - double minZoom = 0.0, - double maxZoom = 18.0, - this.minNativeZoom, - this.maxNativeZoom, - this.zoomReverse = false, - double zoomOffset = 0.0, - Map? additionalOptions, - this.subdomains = const [], - this.keepBuffer = 2, - this.backgroundColor = const Color(0xFFE0E0E0), - this.errorImage, - TileProvider? tileProvider, - this.tms = false, - this.wmsOptions, - this.opacity = 1.0, - - /// Tiles will not update more than once every `updateInterval` milliseconds - /// (default 200) when panning. It can be 0 (but it will calculating for - /// loading tiles every frame when panning / zooming, flutter is fast) This - /// can save some fps and even bandwidth (ie. when fast panning / animating - /// between long distances in short time) - Duration updateInterval = const Duration(milliseconds: 200), - Duration tileFadeInDuration = const Duration(milliseconds: 100), - this.tileFadeInStart = 0.0, - this.tileFadeInStartWhenOverride = 0.0, - this.overrideTilesWhenUrlChanges = false, - this.retinaMode = false, - this.errorTileCallback, - Stream? rebuild, - this.templateFunction = util.template, - this.tileBuilder, - this.tilesContainerBuilder, - this.evictErrorTileStrategy = EvictErrorTileStrategy.none, - this.fastReplace = false, - this.reset, - this.tileBounds, - String userAgentPackageName = 'unknown', - }) : updateInterval = - updateInterval <= Duration.zero ? null : updateInterval, - tileFadeInDuration = - tileFadeInDuration <= Duration.zero ? null : tileFadeInDuration, - assert(tileFadeInStart >= 0.0 && tileFadeInStart <= 1.0), - assert(tileFadeInStartWhenOverride >= 0.0 && - tileFadeInStartWhenOverride <= 1.0), - maxZoom = - wmsOptions == null && retinaMode && maxZoom > 0.0 && !zoomReverse - ? maxZoom - 1.0 - : maxZoom, - minZoom = - wmsOptions == null && retinaMode && maxZoom > 0.0 && zoomReverse - ? math.max(minZoom + 1.0, 0) - : minZoom, - zoomOffset = wmsOptions == null && retinaMode && maxZoom > 0.0 - ? (zoomReverse ? zoomOffset - 1.0 : zoomOffset + 1.0) - : zoomOffset, - tileSize = wmsOptions == null && retinaMode && maxZoom > 0.0 - ? (tileSize / 2.0).floorToDouble() - : tileSize, - additionalOptions = additionalOptions == null - ? const {} - : Map.from(additionalOptions), - tileProvider = tileProvider == null - ? NetworkNoRetryTileProvider( - headers: {'User-Agent': 'flutter_map ($userAgentPackageName)'}, - ) - : (tileProvider - ..headers = { - ...tileProvider.headers, - if (!tileProvider.headers.containsKey('User-Agent')) - 'User-Agent': 'flutter_map ($userAgentPackageName)', - }), - super(key: key, rebuild: rebuild); -} - class WMSTileLayerOptions { final service = 'WMS'; final request = 'GetMap'; diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index 408435a3d..9f71ebb1a 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -93,13 +93,13 @@ class TileManager { } void reloadImages( - TileLayerOptions options, + TileLayer layer, Tuple2? wrapX, Tuple2? wrapY, ) { for (final tile in _tiles.values) { - tile.imageProvider = options.tileProvider - .getImage(tile.coords.wrap(wrapX, wrapY), options); + tile.imageProvider = layer.tileProvider + .getImage(tile.coords.wrap(wrapX, wrapY), layer); tile.loadTileImage(); } } diff --git a/lib/src/layer/tile_layer/tile_provider/base_tile_provider.dart b/lib/src/layer/tile_layer/tile_provider/base_tile_provider.dart index 9ede51ece..345e74fa9 100644 --- a/lib/src/layer/tile_layer/tile_provider/base_tile_provider.dart +++ b/lib/src/layer/tile_layer/tile_provider/base_tile_provider.dart @@ -17,13 +17,13 @@ abstract class TileProvider { }); /// Retrieve a tile as an image, based on it's coordinates and the current [TileLayerOptions] - ImageProvider getImage(Coords coords, TileLayerOptions options); + ImageProvider getImage(Coords coords, TileLayer options); /// Called when the [TileLayerWidget] is disposed void dispose() {} /// Generate a valid URL for a tile, based on it's coordinates and the current [TileLayerOptions] - String getTileUrl(Coords coords, TileLayerOptions options) { + String getTileUrl(Coords coords, TileLayer options) { final urlTemplate = (options.wmsOptions != null) ? options.wmsOptions! .getUrl(coords, options.tileSize.toInt(), options.retinaMode) @@ -46,7 +46,7 @@ abstract class TileProvider { return options.templateFunction(urlTemplate!, allOpts); } - double _getZoomForUrl(Coords coords, TileLayerOptions options) { + double _getZoomForUrl(Coords coords, TileLayer options) { var zoom = coords.z; if (options.zoomReverse) { @@ -61,7 +61,7 @@ abstract class TileProvider { } /// Get a subdomain value for a tile, based on it's coordinates and the current [TileLayerOptions] - String getSubdomain(Coords coords, TileLayerOptions options) { + String getSubdomain(Coords coords, TileLayer options) { if (options.subdomains.isEmpty) { return ''; } diff --git a/lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart index 0085d4236..46dc14ea9 100644 --- a/lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart +++ b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart @@ -8,7 +8,7 @@ class FileTileProvider extends TileProvider { FileTileProvider(); @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { + ImageProvider getImage(Coords coords, TileLayer options) { return FileImage(File(getTileUrl(coords, options))); } } diff --git a/lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart index 0c842f4c1..2a6264e1f 100644 --- a/lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart +++ b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart @@ -9,7 +9,7 @@ class FileTileProvider extends TileProvider { FileTileProvider(); @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { + ImageProvider getImage(Coords coords, TileLayer options) { return NetworkImage(getTileUrl(coords, options)); } } diff --git a/lib/src/layer/tile_layer/tile_provider/tile_provider_io.dart b/lib/src/layer/tile_layer/tile_provider/tile_provider_io.dart index f1e126af6..324099ca4 100644 --- a/lib/src/layer/tile_layer/tile_provider/tile_provider_io.dart +++ b/lib/src/layer/tile_layer/tile_provider/tile_provider_io.dart @@ -27,7 +27,7 @@ class NetworkTileProvider extends TileProvider { late final RetryClient retryClient; @override - ImageProvider getImage(Coords coords, TileLayerOptions options) => + ImageProvider getImage(Coords coords, TileLayer options) => HttpOverrides.runZoned( () => FMNetworkImageProvider( getTileUrl(coords, options), @@ -56,7 +56,7 @@ class NetworkNoRetryTileProvider extends TileProvider { late final HttpClient httpClient; @override - ImageProvider getImage(Coords coords, TileLayerOptions options) => + ImageProvider getImage(Coords coords, TileLayer options) => FMNetworkNoRetryImageProvider( getTileUrl(coords, options), headers: headers, @@ -64,53 +64,21 @@ class NetworkNoRetryTileProvider extends TileProvider { ); } -/// Deprecated due to internal refactoring. The name is misleading, as the internal [ImageProvider] always caches, and this is recommended by most tile servers anyway. For the same functionality, migrate to [NetworkNoRetryTileProvider] before the next minor update. -@Deprecated( - '`NonCachingNetworkTileProvider` has been deprecated due to internal refactoring. The name is misleading, as the internal `ImageProvider` always caches, and this is recommended by most tile servers anyway. For the same functionality, migrate to `NetworkNoRetryTileProvider` before the next minor update.') -class NonCachingNetworkTileProvider extends TileProvider { - NonCachingNetworkTileProvider({ - Map? headers, - HttpClient? httpClient, - }) { - this.headers = headers ?? {}; - this.httpClient = httpClient ?? HttpClient() - ..userAgent = null; - } - - late final HttpClient httpClient; - - @override - ImageProvider getImage(Coords coords, TileLayerOptions options) => - NetworkNoRetryTileProvider( - headers: headers, - httpClient: httpClient, - ).getImage(coords, options); -} - -class AssetTileProvider extends TileProvider { - AssetTileProvider(); - - @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { - return AssetImage(getTileUrl(coords, options)); - } -} - /// A very basic [TileProvider] implementation, that can be extended to create your own provider /// /// Using this method is not recommended any more, except for very simple custom [TileProvider]s. Instead, visit the online documentation at https://docs.fleaflet.dev/plugins/making-a-plugin/creating-new-tile-providers. class CustomTileProvider extends TileProvider { - final String Function(Coords coors, TileLayerOptions options) customTileUrl; + final String Function(Coords coors, TileLayer options) customTileUrl; CustomTileProvider({required this.customTileUrl}); @override - String getTileUrl(Coords coords, TileLayerOptions options) { + String getTileUrl(Coords coords, TileLayer options) { return customTileUrl(coords, options); } @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { + ImageProvider getImage(Coords coords, TileLayer options) { return AssetImage(getTileUrl(coords, options)); } } diff --git a/lib/src/layer/tile_layer/tile_provider/tile_provider_web.dart b/lib/src/layer/tile_layer/tile_provider/tile_provider_web.dart index b4ed8cd47..cba3bb93d 100644 --- a/lib/src/layer/tile_layer/tile_provider/tile_provider_web.dart +++ b/lib/src/layer/tile_layer/tile_provider/tile_provider_web.dart @@ -21,7 +21,7 @@ class NetworkTileProvider extends TileProvider { late final RetryClient retryClient; @override - ImageProvider getImage(Coords coords, TileLayerOptions options) => + ImageProvider getImage(Coords coords, TileLayer options) => FMNetworkImageProvider( getTileUrl(coords, options), headers: headers..remove('User-Agent'), @@ -41,54 +41,28 @@ class NetworkNoRetryTileProvider extends TileProvider { } @override - ImageProvider getImage(Coords coords, TileLayerOptions options) => + ImageProvider getImage(Coords coords, TileLayer options) => NetworkImage( getTileUrl(coords, options), headers: headers..remove('User-Agent'), ); } -/// Deprecated due to internal refactoring. The name is misleading, as the internal [ImageProvider] always caches, and this is recommended by most tile servers anyway. For the same functionality, migrate to [NetworkNoRetryTileProvider] before the next minor update. -@Deprecated( - '`NonCachingNetworkTileProvider` has been deprecated due to internal refactoring. The name is misleading, as the internal `ImageProvider` always caches, and this is recommended by most tile servers anyway. For the same functionality, migrate to `NetworkNoRetryTileProvider` before the next minor update.') -class NonCachingNetworkTileProvider extends TileProvider { - NonCachingNetworkTileProvider({ - Map? headers, - }) { - this.headers = headers ?? {}; - } - - @override - ImageProvider getImage(Coords coords, TileLayerOptions options) => - NetworkNoRetryTileProvider( - headers: headers, - ).getImage(coords, options); -} - -class AssetTileProvider extends TileProvider { - AssetTileProvider(); - - @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { - return AssetImage(getTileUrl(coords, options)); - } -} - /// A very basic [TileProvider] implementation, that can be extended to create your own provider /// /// Using this method is not recommended any more, except for very simple custom [TileProvider]s. Instead, visit the online documentation at https://docs.fleaflet.dev/plugins/making-a-plugin/creating-new-tile-providers. class CustomTileProvider extends TileProvider { - final String Function(Coords coors, TileLayerOptions options) customTileUrl; + final String Function(Coords coors, TileLayer options) customTileUrl; CustomTileProvider({required this.customTileUrl}); @override - String getTileUrl(Coords coords, TileLayerOptions options) { + String getTileUrl(Coords coords, TileLayer options) { return customTileUrl(coords, options); } @override - ImageProvider getImage(Coords coords, TileLayerOptions options) { + ImageProvider getImage(Coords coords, TileLayer options) { return AssetImage(getTileUrl(coords, options)); } } diff --git a/lib/src/layer/tile_layer/transformation_calculator.dart b/lib/src/layer/tile_layer/transformation_calculator.dart index 6f1419198..afab98b5b 100644 --- a/lib/src/layer/tile_layer/transformation_calculator.dart +++ b/lib/src/layer/tile_layer/transformation_calculator.dart @@ -1,18 +1,18 @@ import 'package:flutter_map/src/layer/tile_layer/level.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_transformation.dart'; -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; class TransformationCalculator { final Map _levels = {}; Level? levelAt(double zoom) => _levels[zoom]; - Level getOrCreateLevel(double zoom, MapState map) { + Level getOrCreateLevel(double zoom, FlutterMapState map) { final level = _levels[zoom]; if (level != null) return level; return _levels[zoom] = Level( - origin: map.project(map.unproject(map.getPixelOrigin()), zoom), + origin: map.project(map.unproject(map.pixelOrigin), zoom), zoom: zoom, ); } @@ -30,7 +30,7 @@ class TransformationCalculator { _levels.remove(levelZoom); } - TileTransformation transformationFor(double levelZoom, MapState map) { + TileTransformation transformationFor(double levelZoom, FlutterMapState map) { final level = _levels[levelZoom]!; final scale = map.getZoomScale(map.zoom, level.zoom); final pixelOrigin = map.getNewPixelOrigin(map.center, map.zoom).round(); diff --git a/lib/src/map/flutter_map_state.dart b/lib/src/map/flutter_map_state.dart index e953f8308..361c377a8 100644 --- a/lib/src/map/flutter_map_state.dart +++ b/lib/src/map/flutter_map_state.dart @@ -1,135 +1,185 @@ import 'dart:async'; -import 'package:async/async.dart'; import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/gestures/gestures.dart'; import 'package:flutter_map/src/map/map.dart'; import 'package:flutter_map/src/map/map_state_widget.dart'; +import 'package:latlong2/latlong.dart'; import 'package:positioned_tap_detector_2/positioned_tap_detector_2.dart'; +import 'dart:math' as math; +import 'package:flutter_map/src/core/bounds.dart'; class FlutterMapState extends MapGestureMixin with AutomaticKeepAliveClientMixin { - final List> groups = >[]; final _positionedTapController = PositionedTapController(); - MapController? _localController; + final GestureArenaTeam _team = GestureArenaTeam(); - @override - MapOptions get options => widget.options; + final MapController _localController = MapControllerImpl(); @override - late final MapState mapState; + MapOptions get options => widget.options; @override - MapController get mapController => widget.mapController ?? _localController!; + FlutterMapState get mapState => this; @override - void didUpdateWidget(FlutterMap oldWidget) { - super.didUpdateWidget(oldWidget); - - mapState.options = options; - } + MapController get mapController => widget.mapController ?? _localController; @override void initState() { super.initState(); - if (widget.mapController == null) _localController = MapControllerImpl(); - mapState = MapState(options, (degree) { - if (mounted) setState(() {}); - }, mapController.mapEventSink); - mapController.state = mapState; - // Callback onMapCreated if not null - if (options.onMapCreated != null) { - options.onMapCreated!(mapController); - } - } + mapController.state = this; - void _disposeStreamGroups() { - for (final group in groups) { - group.close(); - } + // Initialize all variables here, if they need to be updated after the map changes + // like center, or bounds they also need to be updated in build. + _rotation = options.rotation; + _center = options.center; + _zoom = options.zoom; + _pixelBounds = getPixelBounds(zoom); + _bounds = _calculateBounds(); - groups.clear(); - } + move(options.center, zoom, source: MapEventSource.initialization); - @override - void dispose() { - _disposeStreamGroups(); - mapState.dispose(); - _localController?.dispose(); - - super.dispose(); + // Funally, fit the map to restrictions + if (options.bounds != null) { + fitBounds(options.bounds!, options.boundsOptions); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + options.onMapReady?.call(); + }); } - Stream _merge(LayerOptions options) { - if (options.rebuild == null) return mapState.onMoved; - - final group = StreamGroup(); - group.add(mapState.onMoved); - group.add(options.rebuild!); - groups.add(group); - return group.stream; + //This may not be required. + @override + void didUpdateWidget(FlutterMap oldWidget) { + super.didUpdateWidget(oldWidget); + mapController.state = this; } @override Widget build(BuildContext context) { - _disposeStreamGroups(); super.build(context); - return LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - final hasLateSize = mapState.hasLateSize(constraints); - mapState.setOriginalSize(constraints.maxWidth, constraints.maxHeight); + final DeviceGestureSettings? gestureSettings = + MediaQuery.maybeOf(context)?.gestureSettings; + final Map gestures = + {}; - // It's possible on first call to LayoutBuilder, it may not know a size - // which will cause methods like fitBounds to break. These methods - // could be called in initIfLateSize() - if (hasLateSize) { - mapState.initIfLateSize(); - } - final size = mapState.size; - - final scaleGestureTeam = GestureArenaTeam(); - - RawGestureDetector scaleGestureDetector({required Widget child}) => - RawGestureDetector( - gestures: { - ScaleGestureRecognizer: - GestureRecognizerFactoryWithHandlers( - () => ScaleGestureRecognizer(), - (ScaleGestureRecognizer instance) { - scaleGestureTeam.captain = instance; - instance.team ??= scaleGestureTeam; - instance - ..onStart = handleScaleStart - ..onUpdate = handleScaleUpdate - ..onEnd = handleScaleEnd; - }), - VerticalDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - VerticalDragGestureRecognizer>( - () => VerticalDragGestureRecognizer(), - (VerticalDragGestureRecognizer instance) { - instance.team ??= scaleGestureTeam; - // these empty lambdas are necessary to activate this gesture recognizer - instance.onUpdate = (_) {}; - }), - HorizontalDragGestureRecognizer: - GestureRecognizerFactoryWithHandlers< - HorizontalDragGestureRecognizer>( - () => HorizontalDragGestureRecognizer(), - (HorizontalDragGestureRecognizer instance) { - instance.team ??= scaleGestureTeam; - instance.onUpdate = (_) {}; - }) - }, - child: child, - ); + gestures[TapGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => TapGestureRecognizer(debugOwner: this), + (TapGestureRecognizer instance) { + instance + ..onTapDown = _positionedTapController.onTapDown + ..onTapUp = handleOnTapUp + ..onTap = _positionedTapController.onTap; + // ..onTapCancel = onTapCancel + // ..onSecondaryTap = onSecondaryTap + // ..onSecondaryTapDown = onSecondaryTapDown + // ..onSecondaryTapUp = onSecondaryTapUp + // ..onSecondaryTapCancel = onSecondaryTapCancel + // ..onTertiaryTapDown = onTertiaryTapDown + // ..onTertiaryTapUp = onTertiaryTapUp + // ..onTertiaryTapCancel = onTertiaryTapCancel + // ..gestureSettings = gestureSettings; + // instance.team = _team; + }, + ); + + gestures[LongPressGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => LongPressGestureRecognizer(debugOwner: this), + (LongPressGestureRecognizer instance) { + instance.onLongPress = _positionedTapController.onLongPress; + // ..onLongPressDown = onLongPressDown + // ..onLongPressCancel = onLongPressCancel + // ..onLongPressStart = onLongPressStart + // ..onLongPressMoveUpdate = onLongPressMoveUpdate + // ..onLongPressUp = onLongPressUp + // ..onLongPressEnd = onLongPressEnd + // ..onSecondaryLongPressDown = onSecondaryLongPressDown + // ..onSecondaryLongPressCancel = onSecondaryLongPressCancel + // ..onSecondaryLongPress = onSecondaryLongPress + // ..onSecondaryLongPressStart = onSecondaryLongPressStart + // ..onSecondaryLongPressMoveUpdate = onSecondaryLongPressMoveUpdate + // ..onSecondaryLongPressUp = onSecondaryLongPressUp + // ..onSecondaryLongPressEnd = onSecondaryLongPressEnd + // ..onTertiaryLongPressDown = onTertiaryLongPressDown + // ..onTertiaryLongPressCancel = onTertiaryLongPressCancel + // ..onTertiaryLongPress = onTertiaryLongPress + // ..onTertiaryLongPressStart = onTertiaryLongPressStart + // ..onTertiaryLongPressMoveUpdate = onTertiaryLongPressMoveUpdate + // ..onTertiaryLongPressUp = onTertiaryLongPressUp + // ..onTertiaryLongPressEnd = onTertiaryLongPressEnd + // ..gestureSettings = gestureSettings; + // instance.team = _team; + }, + ); + + if (options.absorbPanEventsOnScrollables && + InteractiveFlag.hasFlag( + options.interactiveFlags, InteractiveFlag.drag)) { + gestures[VerticalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => VerticalDragGestureRecognizer(debugOwner: this), + (VerticalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + //Absorbing vertical drags + }; + // ..dragStartBehavior = dragStartBehavior + instance.gestureSettings = gestureSettings; + instance.team ??= _team; + }, + ); + gestures[HorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => HorizontalDragGestureRecognizer(debugOwner: this), + (HorizontalDragGestureRecognizer instance) { + instance.onUpdate = (details) { + //Absorbing horizontal drags + }; + // ..dragStartBehavior = dragStartBehavior + instance.gestureSettings = gestureSettings; + instance.team ??= _team; + }, + ); + } + + gestures[ScaleGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( + () => ScaleGestureRecognizer(debugOwner: this), + (ScaleGestureRecognizer instance) { + instance + ..onStart = handleScaleStart + ..onUpdate = handleScaleUpdate + ..onEnd = handleScaleEnd; + instance.team ??= _team; + _team.captain = instance; + }, + ); + + //Update on state change + _pixelBounds = getPixelBounds(zoom); + _bounds = _calculateBounds(); + _pixelOrigin = getNewPixelOrigin(_center); + + if (options.bounds != null) { + fitBounds(options.bounds!, options.boundsOptions); + } + + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + //Update on layout change + setSize(constraints.maxWidth, constraints.maxHeight); + _pixelBounds = getPixelBounds(zoom); + _bounds = _calculateBounds(); return MapStateInheritedWidget( - mapState: mapState, + mapState: this, child: Listener( onPointerDown: onPointerDown, onPointerUp: onPointerUp, @@ -141,23 +191,8 @@ class FlutterMapState extends MapGestureMixin onTap: handleTap, onLongPress: handleLongPress, onDoubleTap: handleDoubleTap, - child: options.allowPanningOnScrollingParent - ? GestureDetector( - onTap: _positionedTapController.onTap, - onLongPress: _positionedTapController.onLongPress, - onTapDown: _positionedTapController.onTapDown, - onTapUp: handleOnTapUp, - child: scaleGestureDetector(child: _buildMap(size)), - ) - : GestureDetector( - onScaleStart: handleScaleStart, - onScaleUpdate: handleScaleUpdate, - onScaleEnd: handleScaleEnd, - onTap: _positionedTapController.onTap, - onLongPress: _positionedTapController.onLongPress, - onTapDown: _positionedTapController.onTapDown, - onTapUp: handleOnTapUp, - child: _buildMap(size)), + child: + RawGestureDetector(gestures: gestures, child: _buildMap(size)), ), ), ); @@ -174,67 +209,615 @@ class FlutterMapState extends MapGestureMixin minHeight: size.y, maxHeight: size.y, child: Transform.rotate( - angle: mapState.rotationRad, + angle: rotationRad, child: Stack( - children: [ - if (widget.children.isNotEmpty) ...widget.children, - if (widget.layers.isNotEmpty) - ...widget.layers.map( - (layer) => _createLayer(layer, options.plugins), - ) - ], + children: widget.children, ), ), ), Stack( - children: [ - if (widget.nonRotatedChildren.isNotEmpty) - ...widget.nonRotatedChildren, - if (widget.nonRotatedLayers.isNotEmpty) - ...widget.nonRotatedLayers.map( - (layer) => _createLayer(layer, options.plugins), - ) - ], + children: widget.nonRotatedChildren, ), ], ), ); } - Widget _createLayer(LayerOptions options, List plugins) { - for (final plugin in plugins) { - if (plugin.supportsLayer(options)) { - return plugin.createLayer(options, mapState, _merge(options)); + @override + bool get wantKeepAlive => options.keepAlive; + + ///MAP STATE + ///MAP STATE + ///MAP STATE + ///MAP STATE + ///MAP STATE + ///MAP STATE + ///MAP STATE + ///MAP STATE + + late double _zoom; + late double _rotation; + + double get zoom => _zoom; + + double get rotation => _rotation; + + double get rotationRad => degToRadian(_rotation); + + late CustomPoint _pixelOrigin; + CustomPoint get pixelOrigin => _pixelOrigin; + + late LatLng _center; + LatLng get center => _center; + + late LatLngBounds _bounds; + LatLngBounds get bounds => _bounds; + + late Bounds _pixelBounds; + Bounds get pixelBounds => _pixelBounds; + + // Original size of the map where rotation isn't calculated + CustomPoint? _nonrotatedSize; + CustomPoint? get nonrotatedSize => _nonrotatedSize; + + void setSize(double width, double height) { + final isCurrSizeNull = _nonrotatedSize == null; + if (isCurrSizeNull || + _nonrotatedSize!.x != width || + _nonrotatedSize!.y != height) { + _nonrotatedSize = CustomPoint(width, height); + + _updateSizeByOriginalSizeAndRotation(); + } + } + + // Extended size of the map where rotation is calculated + CustomPoint? _size; + + CustomPoint get size => _size ?? const CustomPoint(0.0, 0.0); + + void _updateSizeByOriginalSizeAndRotation() { + final originalWidth = _nonrotatedSize!.x; + final originalHeight = _nonrotatedSize!.y; + + if (_rotation != 0.0) { + final cosAngle = math.cos(rotationRad).abs(); + final sinAngle = math.sin(rotationRad).abs(); + final num width = + (originalWidth * cosAngle) + (originalHeight * sinAngle); + final num height = + (originalHeight * cosAngle) + (originalWidth * sinAngle); + + _size = CustomPoint(width, height); + } else { + _size = CustomPoint(originalWidth, originalHeight); + } + + _pixelOrigin = getNewPixelOrigin(_center); + } + + void _handleMoveEmit(LatLng targetCenter, double targetZoom, LatLng oldCenter, + double oldZoom, bool hasGesture, MapEventSource source, String? id) { + if (source == MapEventSource.flingAnimationController) { + emitMapEvent( + MapEventFlingAnimation( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ), + ); + } else if (source == MapEventSource.doubleTapZoomAnimationController) { + emitMapEvent( + MapEventDoubleTapZoom( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ), + ); + } else if (source == MapEventSource.scrollWheel) { + emitMapEvent( + MapEventScrollWheelZoom( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ), + ); + } else if (source == MapEventSource.onDrag || + source == MapEventSource.onMultiFinger) { + emitMapEvent( + MapEventMove( + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ), + ); + } else if (source == MapEventSource.mapController) { + emitMapEvent( + MapEventMove( + id: id, + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ), + ); + } else if (source == MapEventSource.custom) { + // for custom source, emit move event if zoom or center has changed + if (targetZoom != oldZoom || + targetCenter.latitude != oldCenter.latitude || + targetCenter.longitude != oldCenter.longitude) { + emitMapEvent( + MapEventMove( + id: id, + center: oldCenter, + zoom: oldZoom, + targetCenter: targetCenter, + targetZoom: targetZoom, + source: source, + ), + ); } } - if (options is TileLayerOptions) { - return TileLayer( - options: options, mapState: mapState, stream: _merge(options)); + } + + void emitMapEvent(MapEvent event) { + if (event.source == MapEventSource.mapController && event is MapEventMove) { + handleAnimationInterruptions(event); } - if (options is MarkerLayerOptions) { - return MarkerLayer(options, mapState, _merge(options)); + + setState(() { + widget.options.onMapEvent?.call(event); + }); + mapController.mapEventSink.add(event); + } + + bool rotate( + double newRotation, { + bool hasGesture = false, + required MapEventSource source, + String? id, + }) { + if (newRotation != _rotation) { + final double oldRotation = _rotation; + //Apply state then emit events and callbacks + setState(() { + _rotation = newRotation; + }); + _updateSizeByOriginalSizeAndRotation(); + + emitMapEvent( + MapEventRotate( + id: id, + currentRotation: oldRotation, + targetRotation: _rotation, + center: _center, + zoom: _zoom, + source: source, + ), + ); + return true; } - if (options is PolylineLayerOptions) { - return PolylineLayer(options, mapState, _merge(options)); + + return false; + } + + MoveAndRotateResult moveAndRotate( + LatLng newCenter, double newZoom, double newRotation, + {required MapEventSource source, String? id}) { + final moveSucc = move(newCenter, newZoom, id: id, source: source); + final rotateSucc = rotate(newRotation, id: id, source: source); + + return MoveAndRotateResult(moveSucc, rotateSucc); + } + + bool move(LatLng newCenter, double newZoom, + {bool hasGesture = false, required MapEventSource source, String? id}) { + newZoom = fitZoomToBounds(newZoom); + final mapMoved = newCenter != _center || newZoom != _zoom; + + if (!mapMoved || !_bounds.isValid) { + return false; } - if (options is PolygonLayerOptions) { - return PolygonLayer(options, mapState, _merge(options)); + + if (isOutOfBounds(newCenter)) { + if (!options.slideOnBoundaries) { + return false; + } + newCenter = containPoint(newCenter, _center); } - if (options is CircleLayerOptions) { - return CircleLayer(options, mapState, _merge(options)); + + // 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) { + final adjustedCenter = adjustCenterIfOutsideMaxBounds( + newCenter, newZoom, options.maxBounds!); + if (adjustedCenter == null) { + return false; + } else { + newCenter = adjustedCenter; + } } - if (options is GroupLayerOptions) { - return GroupLayer(options, mapState, _merge(options)); + + final LatLng oldCenter = _center; + final double oldZoom = _zoom; + + //Apply state then emit events and callbacks + setState(() { + _zoom = newZoom; + _center = newCenter; + }); + + _pixelBounds = getPixelBounds(_zoom); + _pixelOrigin = getNewPixelOrigin(newCenter); + + _handleMoveEmit( + newCenter, newZoom, oldCenter, oldZoom, hasGesture, source, id); + + options.onPositionChanged?.call( + MapPosition( + center: newCenter, + bounds: _bounds, + zoom: newZoom, + hasGesture: hasGesture), + hasGesture); + + return true; + } + + double fitZoomToBounds(double zoom) { + // Abide to min/max zoom + if (options.maxZoom != null) { + zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; } - if (options is OverlayImageLayerOptions) { - return OverlayImageLayer(options, mapState, _merge(options)); + if (options.minZoom != null) { + zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; } - throw (StateError(""" -Can't find correct layer for $options. Perhaps when you create your FlutterMap you need something like this: + return zoom; + } - options: new MapOptions(plugins: [MyFlutterMapPlugin()])""")); + 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); } - @override - bool get wantKeepAlive => options.keepAlive; + CenterZoom centerZoomFitBounds( + LatLngBounds bounds, FitBoundsOptions options) { + if (!bounds.isValid) { + throw Exception('Bounds are not valid.'); + } + return getBoundsCenterZoom(bounds, options); + } + + LatLngBounds _calculateBounds() { + return LatLngBounds( + unproject(_pixelBounds.bottomLeft), + unproject(_pixelBounds.topRight), + ); + } + + CenterZoom getBoundsCenterZoom( + LatLngBounds bounds, FitBoundsOptions options) { + final paddingTL = + CustomPoint(options.padding.left, options.padding.top); + final paddingBR = + CustomPoint(options.padding.right, options.padding.bottom); + + final paddingTotalXY = paddingTL + paddingBR; + + var zoom = getBoundsZoom(bounds, paddingTotalXY, inside: options.inside); + zoom = math.min(options.maxZoom, zoom); + + final paddingOffset = (paddingBR - paddingTL) / 2; + final swPoint = project(bounds.southWest!, zoom); + final nePoint = project(bounds.northEast!, zoom); + final center = unproject((swPoint + nePoint) / 2 + paddingOffset, zoom); + return CenterZoom( + center: center, + zoom: zoom, + ); + } + + double getBoundsZoom(LatLngBounds bounds, CustomPoint padding, + {bool inside = false}) { + var zoom = this.zoom; + final min = options.minZoom ?? 0.0; + final max = options.maxZoom ?? double.infinity; + final nw = bounds.northWest; + final se = bounds.southEast; + var size = this.size - padding; + // Prevent negative size which results in NaN zoom value later on in the calculation + size = CustomPoint(math.max(0.0, size.x), math.max(0.0, size.y)); + final boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; + final scaleX = size.x / boundsSize.x; + final scaleY = size.y / boundsSize.y; + final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); + + zoom = getScaleZoom(scale, zoom); + + return math.max(min, math.min(max, zoom)); + } + + CustomPoint project(LatLng latlng, [double? zoom]) { + zoom ??= _zoom; + return options.crs.latLngToPoint(latlng, zoom); + } + + LatLng unproject(CustomPoint point, [double? zoom]) { + zoom ??= _zoom; + return options.crs.pointToLatLng(point, zoom)!; + } + + LatLng layerPointToLatLng(CustomPoint point) { + return unproject(point); + } + + double getZoomScale(double toZoom, double? fromZoom) { + final crs = options.crs; + fromZoom = fromZoom ?? _zoom; + return crs.scale(toZoom) / crs.scale(fromZoom); + } + + double getScaleZoom(double scale, double? fromZoom) { + final crs = options.crs; + fromZoom = fromZoom ?? _zoom; + return crs.zoom(scale * crs.scale(fromZoom)) as double; + } + + Bounds? getPixelWorldBounds(double? zoom) { + return options.crs.getProjectedBounds(zoom ?? _zoom); + } + + Offset getOffsetFromOrigin(LatLng pos) { + final delta = project(pos) - _pixelOrigin; + return Offset(delta.x.toDouble(), delta.y.toDouble()); + } + + CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) { + final viewHalf = size / 2.0; + return (project(center, zoom) - viewHalf).round(); + } + + Bounds getPixelBounds(double zoom) { + final mapZoom = zoom; + final scale = getZoomScale(mapZoom, zoom); + final pixelCenter = project(center, zoom).floor(); + final halfSize = size / (scale * 2); + return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); + } + + LatLng? adjustCenterIfOutsideMaxBounds( + LatLng testCenter, double testZoom, LatLngBounds maxBounds) { + LatLng? newCenter; + + final swPixel = project(maxBounds.southWest!, testZoom); + final nePixel = project(maxBounds.northEast!, testZoom); + + final centerPix = project(testCenter, testZoom); + + final halfSizeX = size.x / 2; + final 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. + final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; + final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; + final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; + final 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; + } + + final newCx = newCenterX ?? centerPix.x; + final 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; + } + + // This will convert a latLng to a position that we could use with a widget + // outside of FlutterMap layer space. Eg using a Positioned Widget. + CustomPoint latLngToScreenPoint(LatLng latLng) { + final nonRotatedPixelOrigin = + (project(_center, zoom) - nonrotatedSize! / 2.0).round(); + + var point = options.crs.latLngToPoint(latLng, zoom); + + final mapCenter = options.crs.latLngToPoint(center, zoom); + + if (rotation != 0.0) { + point = rotatePoint(mapCenter, point, counterRotation: false); + } + + return point - nonRotatedPixelOrigin; + } + + LatLng? pointToLatLng(CustomPoint localPoint) { + if (nonrotatedSize == null) { + return null; + } + + final width = nonrotatedSize!.x; + final height = nonrotatedSize!.y; + + final localPointCenterDistance = + CustomPoint((width / 2) - localPoint.x, (height / 2) - localPoint.y); + final mapCenter = options.crs.latLngToPoint(center, zoom); + + var point = mapCenter - localPointCenterDistance; + + if (rotation != 0.0) { + point = rotatePoint(mapCenter, point); + } + + return options.crs.pointToLatLng(point, zoom); + } + + // Sometimes we need to make allowances that a rotation already exists, so + // it needs to be reversed (pointToLatLng), and sometimes we want to use + // the same rotation to create a new position (latLngToScreenpoint). + // counterRotation just makes allowances this for this. + CustomPoint rotatePoint( + CustomPoint mapCenter, CustomPoint point, + {bool counterRotation = true}) { + final counterRotationFactor = counterRotation ? -1 : 1; + + final m = Matrix4.identity() + ..translate(mapCenter.x.toDouble(), mapCenter.y.toDouble()) + ..rotateZ(rotationRad * counterRotationFactor) + ..translate(-mapCenter.x.toDouble(), -mapCenter.y.toDouble()); + + final tp = MatrixUtils.transformPoint( + m, Offset(point.x.toDouble(), point.y.toDouble())); + + return CustomPoint(tp.dx, tp.dy); + } + + + _SafeArea? _safeAreaCache; + double? _safeAreaZoom; + + //if there is a pan boundary, do not cross + 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 || + center.latitude > options.nePanBoundary!.latitude) { + return true; + } else if (center.longitude < options.swPanBoundary!.longitude || + center.longitude > options.nePanBoundary!.longitude) { + return true; + } + } + return false; + } + + LatLng containPoint(LatLng point, LatLng fallback) { + if (options.adaptiveBoundaries) { + return _safeArea!.containPoint(point, fallback); + } else { + return LatLng( + point.latitude.clamp(options.swPanBoundary!.latitude, options.nePanBoundary!.latitude), + point.longitude + .clamp(options.swPanBoundary!.longitude, options.nePanBoundary!.longitude), + ); + } + } + + _SafeArea? get _safeArea { + final controllerZoom = _zoom; + if (controllerZoom != _safeAreaZoom || _safeAreaCache == null) { + _safeAreaZoom = controllerZoom; + final halfScreenHeight = _calculateScreenHeightInDegrees() / 2; + final halfScreenWidth = _calculateScreenWidthInDegrees() / 2; + final southWestLatitude = options.swPanBoundary!.latitude + halfScreenHeight; + final southWestLongitude = options.swPanBoundary!.longitude + halfScreenWidth; + final northEastLatitude = options.nePanBoundary!.latitude - halfScreenHeight; + final northEastLongitude = options.nePanBoundary!.longitude - halfScreenWidth; + _safeAreaCache = _SafeArea( + LatLng( + southWestLatitude, + southWestLongitude, + ), + LatLng( + northEastLatitude, + northEastLongitude, + ), + ); + } + return _safeAreaCache; + } + + double _calculateScreenWidthInDegrees() { + final degreesPerPixel = 360 / math.pow(2, zoom + 8); + return options.screenSize!.width * degreesPerPixel; + } + + double _calculateScreenHeightInDegrees() => + options.screenSize!.height * 170.102258 / math.pow(2, zoom + 8); + + + static FlutterMapState? maybeOf(BuildContext context, {bool nullOk = false}) { + final widget = + context.dependOnInheritedWidgetOfExactType(); + if (nullOk || widget != null) { + return widget?.mapState; + } + throw FlutterError( + 'MapState.of() called with a context that does not contain a FlutterMap.'); + } } + + + +class _SafeArea { + final LatLngBounds bounds; + final bool isLatitudeBlocked; + final bool isLongitudeBlocked; + + _SafeArea(LatLng southWest, LatLng northEast) + : bounds = LatLngBounds(southWest, northEast), + isLatitudeBlocked = southWest.latitude > northEast.latitude, + isLongitudeBlocked = southWest.longitude > northEast.longitude; + + bool contains(LatLng? point) => + isLatitudeBlocked || isLongitudeBlocked ? false : bounds.contains(point); + + LatLng containPoint(LatLng point, LatLng fallback) => LatLng( + isLatitudeBlocked + ? fallback.latitude + : point.latitude.clamp(bounds.south, bounds.north), + isLongitudeBlocked + ? fallback.longitude + : point.longitude.clamp(bounds.west, bounds.east), + ); +} \ No newline at end of file diff --git a/lib/src/map/map.dart b/lib/src/map/map.dart index aa9e6e78d..8d1e4f0da 100644 --- a/lib/src/map/map.dart +++ b/lib/src/map/map.dart @@ -1,35 +1,30 @@ import 'dart:async'; -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/core/bounds.dart'; -import 'package:flutter_map/src/map/map_state_widget.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; import 'package:latlong2/latlong.dart'; class MapControllerImpl implements MapController { - final Completer _readyCompleter = Completer(); + final StreamController _mapEventSink = StreamController.broadcast(); @override StreamSink get mapEventSink => _mapEventSink.sink; @override - Future get onReady => _readyCompleter.future; + Stream get mapEventStream => _mapEventSink.stream; + + late FlutterMapState _state; @override - void dispose() { - _mapEventSink.close(); + set state(FlutterMapState state) { + _state = state; } - late MapState _state; - @override - set state(MapState state) { - _state = state; - if (!_readyCompleter.isCompleted) { - _readyCompleter.complete(); - } + void dispose() { + _mapEventSink.close(); } @override @@ -96,604 +91,5 @@ class MapControllerImpl implements MapController { return _state.rotatePoint(mapCenter, point, counterRotation: counterRotation); } - - @override - Stream get mapEventStream => _mapEventSink.stream; -} - -class MapState { - MapOptions options; - final ValueChanged onRotationChanged; - final StreamController _onMoveSink; - final StreamSink _mapEventSink; - - double _zoom; - double _rotation; - double _rotationRad; - - double get zoom => _zoom; - - double get rotation => _rotation; - - set rotation(double rotation) { - _rotation = rotation; - _rotationRad = degToRadian(rotation); - } - - double get rotationRad => _rotationRad; - - LatLng? _lastCenter; - LatLngBounds? _lastBounds; - Bounds? _lastPixelBounds; - late CustomPoint _pixelOrigin; - bool _initialized = false; - - MapState(this.options, this.onRotationChanged, this._mapEventSink) - : _rotation = options.rotation, - _rotationRad = degToRadian(options.rotation), - _zoom = options.zoom, - _onMoveSink = StreamController.broadcast(); - - Stream get onMoved => _onMoveSink.stream; - - // Original size of the map where rotation isn't calculated - CustomPoint? _originalSize; - - CustomPoint? get originalSize => _originalSize; - - void setOriginalSize(double width, double height) { - final isCurrSizeNull = _originalSize == null; - if (isCurrSizeNull || - _originalSize!.x != width || - _originalSize!.y != height) { - _originalSize = CustomPoint(width, height); - - _updateSizeByOriginalSizeAndRotation(); - - // rebuild layers if screen size has been changed - if (!isCurrSizeNull) { - _onMoveSink.add(null); - } - } - } - - // Extended size of the map where rotation is calculated - CustomPoint? _size; - - CustomPoint get size => _size ?? const CustomPoint(0.0, 0.0); - - void _updateSizeByOriginalSizeAndRotation() { - final originalWidth = _originalSize!.x; - final originalHeight = _originalSize!.y; - - if (_rotation != 0.0) { - final cosAngle = math.cos(_rotationRad).abs(); - final sinAngle = math.sin(_rotationRad).abs(); - final num width = - (originalWidth * cosAngle) + (originalHeight * sinAngle); - final num height = - (originalHeight * cosAngle) + (originalWidth * sinAngle); - - _size = CustomPoint(width, height); - } else { - _size = CustomPoint(originalWidth, originalHeight); - } - - if (!_initialized) { - _init(); - _initialized = true; - } - - _pixelOrigin = getNewPixelOrigin(_lastCenter!); - } - - LatLng get center => getCenter(); - - LatLngBounds get bounds => getBounds(); - - Bounds get pixelBounds => getLastPixelBounds(); - - void _init() { - if (options.bounds != null) { - fitBounds(options.bounds!, options.boundsOptions); - } else { - move(options.center, zoom, source: MapEventSource.initialization); - } - } - - // Check if we've just got a new size constraints. Initially a layoutBuilder - // May not be able to calculate a size, and end up with 0,0 - bool hasLateSize(BoxConstraints constraints) { - if (options.bounds != null && - originalSize != null && - originalSize!.x == 0.0 && - constraints.maxWidth != 0.0) { - return true; - } - return false; - } - - // If we've just calculated a size, we may want to call some methods that - // rely on it, like fitBounds. Add any others here. - void initIfLateSize() { - if (options.bounds != null) { - fitBounds(options.bounds!, options.boundsOptions); - } - } - - void _handleMoveEmit(LatLng targetCenter, double targetZoom, bool hasGesture, - MapEventSource source, String? id) { - if (source == MapEventSource.flingAnimationController) { - emitMapEvent( - MapEventFlingAnimation( - center: _lastCenter!, - zoom: _zoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.doubleTapZoomAnimationController) { - emitMapEvent( - MapEventDoubleTapZoom( - center: _lastCenter!, - zoom: _zoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.scrollWheel) { - emitMapEvent( - MapEventScrollWheelZoom( - center: _lastCenter!, - zoom: _zoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.onDrag || - source == MapEventSource.onMultiFinger) { - emitMapEvent( - MapEventMove( - center: _lastCenter!, - zoom: _zoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.mapController) { - emitMapEvent( - MapEventMove( - id: id, - center: _lastCenter!, - zoom: _zoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } else if (source == MapEventSource.custom) { - // for custom source, emit move event if zoom or center has changed - if (targetZoom != _zoom || - _lastCenter == null || - targetCenter.latitude != _lastCenter!.latitude || - targetCenter.longitude != _lastCenter!.longitude) { - emitMapEvent( - MapEventMove( - id: id, - center: _lastCenter!, - zoom: _zoom, - targetCenter: targetCenter, - targetZoom: targetZoom, - source: source, - ), - ); - } - } - } - - void emitMapEvent(MapEvent event) { - _mapEventSink.add(event); - } - - void dispose() { - _onMoveSink.close(); - _mapEventSink.close(); - } - - void rebuildLayers() { - _onMoveSink.add(null); - } - - bool rotate( - double degree, { - bool hasGesture = false, - bool callOnMoveSink = true, - required MapEventSource source, - String? id, - }) { - if (degree != _rotation) { - final oldRotation = _rotation; - rotation = degree; - _updateSizeByOriginalSizeAndRotation(); - - onRotationChanged(_rotation); - - emitMapEvent( - MapEventRotate( - id: id, - currentRotation: oldRotation, - targetRotation: _rotation, - center: _lastCenter!, - zoom: _zoom, - source: source, - ), - ); - - if (callOnMoveSink) { - _onMoveSink.add(null); - } - - return true; - } - - return false; - } - - MoveAndRotateResult moveAndRotate(LatLng center, double zoom, double degree, - {required MapEventSource source, String? id}) { - final moveSucc = - move(center, zoom, id: id, source: source, callOnMoveSink: false); - final rotateSucc = - rotate(degree, id: id, source: source, callOnMoveSink: false); - - if (moveSucc || rotateSucc) { - _onMoveSink.add(null); - } - - return MoveAndRotateResult(moveSucc, rotateSucc); - } - - bool move(LatLng center, double zoom, - {bool hasGesture = false, - bool callOnMoveSink = true, - required MapEventSource source, - String? id}) { - zoom = fitZoomToBounds(zoom); - final mapMoved = center != _lastCenter || zoom != _zoom; - - if (_lastCenter != null && (!mapMoved || !bounds.isValid)) { - return false; - } - - if (options.isOutOfBounds(center)) { - if (!options.slideOnBoundaries) { - return false; - } - 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) { - final adjustedCenter = - adjustCenterIfOutsideMaxBounds(center, zoom, options.maxBounds!); - if (adjustedCenter == null) { - return false; - } else { - center = adjustedCenter; - } - } - - _handleMoveEmit(center, zoom, hasGesture, source, id); - - _zoom = zoom; - _lastCenter = center; - _lastPixelBounds = getPixelBounds(_zoom); - _lastBounds = _calculateBounds(); - _pixelOrigin = getNewPixelOrigin(center); - if (callOnMoveSink) { - _onMoveSink.add(null); - } - - if (options.onPositionChanged != null) { - final mapPosition = MapPosition( - center: center, bounds: bounds, zoom: zoom, hasGesture: hasGesture); - - options.onPositionChanged!(mapPosition, hasGesture); - } - - return true; - } - - double fitZoomToBounds(double? zoom) { - zoom ??= _zoom; - // Abide to min/max zoom - if (options.maxZoom != null) { - zoom = (zoom > options.maxZoom!) ? options.maxZoom! : zoom; - } - if (options.minZoom != null) { - zoom = (zoom < options.minZoom!) ? options.minZoom! : zoom; - } - return zoom; - } - - 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); - } - - LatLng getCenter() { - if (_lastCenter != null) { - return _lastCenter!; - } - return layerPointToLatLng(_centerLayerPoint); - } - - LatLngBounds getBounds() { - if (_lastBounds != null) { - return _lastBounds!; - } - - return _calculateBounds(); - } - - Bounds getLastPixelBounds() { - if (_lastPixelBounds != null) { - return _lastPixelBounds!; - } - - return getPixelBounds(zoom); - } - - LatLngBounds _calculateBounds() { - final bounds = getLastPixelBounds(); - return LatLngBounds( - unproject(bounds.bottomLeft), - unproject(bounds.topRight), - ); - } - - CenterZoom getBoundsCenterZoom( - LatLngBounds bounds, FitBoundsOptions options) { - final paddingTL = - CustomPoint(options.padding.left, options.padding.top); - final paddingBR = - CustomPoint(options.padding.right, options.padding.bottom); - - final paddingTotalXY = paddingTL + paddingBR; - - var zoom = getBoundsZoom(bounds, paddingTotalXY, inside: options.inside); - zoom = math.min(options.maxZoom, zoom); - - final paddingOffset = (paddingBR - paddingTL) / 2; - final swPoint = project(bounds.southWest!, zoom); - final nePoint = project(bounds.northEast!, zoom); - final center = unproject((swPoint + nePoint) / 2 + paddingOffset, zoom); - return CenterZoom( - center: center, - zoom: zoom, - ); - } - - double getBoundsZoom(LatLngBounds bounds, CustomPoint padding, - {bool inside = false}) { - var zoom = this.zoom; - final min = options.minZoom ?? 0.0; - final max = options.maxZoom ?? double.infinity; - final nw = bounds.northWest; - final se = bounds.southEast; - var size = this.size - padding; - // Prevent negative size which results in NaN zoom value later on in the calculation - size = CustomPoint(math.max(0.0, size.x), math.max(0.0, size.y)); - final boundsSize = Bounds(project(se, zoom), project(nw, zoom)).size; - final scaleX = size.x / boundsSize.x; - final scaleY = size.y / boundsSize.y; - final scale = inside ? math.max(scaleX, scaleY) : math.min(scaleX, scaleY); - - zoom = getScaleZoom(scale, zoom); - - return math.max(min, math.min(max, zoom)); - } - - CustomPoint project(LatLng latlng, [double? zoom]) { - zoom ??= _zoom; - return options.crs.latLngToPoint(latlng, zoom); - } - - LatLng unproject(CustomPoint point, [double? zoom]) { - zoom ??= _zoom; - return options.crs.pointToLatLng(point, zoom)!; - } - - LatLng layerPointToLatLng(CustomPoint point) { - return unproject(point); - } - - CustomPoint get _centerLayerPoint { - return size / 2; - } - - double getZoomScale(double toZoom, double? fromZoom) { - final crs = options.crs; - fromZoom = fromZoom ?? _zoom; - return crs.scale(toZoom) / crs.scale(fromZoom); - } - - double getScaleZoom(double scale, double? fromZoom) { - final crs = options.crs; - fromZoom = fromZoom ?? _zoom; - return crs.zoom(scale * crs.scale(fromZoom)) as double; - } - - Bounds? getPixelWorldBounds(double? zoom) { - return options.crs.getProjectedBounds(zoom ?? _zoom); - } - - CustomPoint getPixelOrigin() { - return _pixelOrigin; - } - - Offset getOffsetFromOrigin(LatLng pos) { - final delta = project(pos) - getPixelOrigin(); - return Offset(delta.x.toDouble(), delta.y.toDouble()); - } - - CustomPoint getNewPixelOrigin(LatLng center, [double? zoom]) { - final viewHalf = size / 2.0; - return (project(center, zoom) - viewHalf).round(); - } - - Bounds getPixelBounds(double zoom) { - final mapZoom = zoom; - final scale = getZoomScale(mapZoom, zoom); - final pixelCenter = project(center, zoom).floor(); - final halfSize = size / (scale * 2); - return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); - } - - LatLng? adjustCenterIfOutsideMaxBounds( - LatLng testCenter, double testZoom, LatLngBounds maxBounds) { - LatLng? newCenter; - - final swPixel = project(maxBounds.southWest!, testZoom); - final nePixel = project(maxBounds.northEast!, testZoom); - - final centerPix = project(testCenter, testZoom); - - final halfSizeX = size.x / 2; - final 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. - final leftOkCenter = math.min(swPixel.x, nePixel.x) + halfSizeX; - final rightOkCenter = math.max(swPixel.x, nePixel.x) - halfSizeX; - final topOkCenter = math.min(swPixel.y, nePixel.y) + halfSizeY; - final 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; - } - - final newCx = newCenterX ?? centerPix.x; - final 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; - } - - // This will convert a latLng to a position that we could use with a widget - // outside of FlutterMap layer space. Eg using a Positioned Widget. - CustomPoint latLngToScreenPoint(LatLng latLng) { - final nonRotatedPixelOrigin = - (project(getCenter(), zoom) - originalSize! / 2.0).round(); - - var point = options.crs.latLngToPoint(latLng, zoom); - - final mapCenter = options.crs.latLngToPoint(center, zoom); - - if (rotation != 0.0) { - point = rotatePoint(mapCenter, point, counterRotation: false); - } - - return point - nonRotatedPixelOrigin; - } - - LatLng? pointToLatLng(CustomPoint localPoint) { - if (originalSize == null) { - return null; - } - - final width = originalSize!.x; - final height = originalSize!.y; - - final localPointCenterDistance = - CustomPoint((width / 2) - localPoint.x, (height / 2) - localPoint.y); - final mapCenter = options.crs.latLngToPoint(center, zoom); - - var point = mapCenter - localPointCenterDistance; - - if (rotation != 0.0) { - point = rotatePoint(mapCenter, point); - } - - return options.crs.pointToLatLng(point, zoom); - } - - // Sometimes we need to make allowances that a rotation already exists, so - // it needs to be reversed (pointToLatLng), and sometimes we want to use - // the same rotation to create a new position (latLngToScreenpoint). - // counterRotation just makes allowances this for this. - CustomPoint rotatePoint( - CustomPoint mapCenter, CustomPoint point, - {bool counterRotation = true}) { - final counterRotationFactor = counterRotation ? -1 : 1; - - final m = Matrix4.identity() - ..translate(mapCenter.x.toDouble(), mapCenter.y.toDouble()) - ..rotateZ(rotationRad * counterRotationFactor) - ..translate(-mapCenter.x.toDouble(), -mapCenter.y.toDouble()); - - final tp = MatrixUtils.transformPoint( - m, Offset(point.x.toDouble(), point.y.toDouble())); - - return CustomPoint(tp.dx, tp.dy); - } - - static MapState? maybeOf(BuildContext context, {bool nullOk = false}) { - final widget = - context.dependOnInheritedWidgetOfExactType(); - if (nullOk || widget != null) { - return widget?.mapState; - } - throw FlutterError( - 'MapState.of() called with a context that does not contain a FlutterMap.'); - } + } diff --git a/lib/src/map/map_state_widget.dart b/lib/src/map/map_state_widget.dart index ac6884b97..11bed19c1 100644 --- a/lib/src/map/map_state_widget.dart +++ b/lib/src/map/map_state_widget.dart @@ -1,15 +1,15 @@ import 'package:flutter/widgets.dart'; - -import 'package:flutter_map/src/map/map.dart'; +import 'package:flutter_map/src/map/flutter_map_state.dart'; class MapStateInheritedWidget extends InheritedWidget { - final MapState mapState; + + final FlutterMapState mapState; const MapStateInheritedWidget({ - Key? key, + super.key, required this.mapState, - required Widget child, - }) : super(key: key, child: child); + required super.child, + }); @override bool updateShouldNotify(MapStateInheritedWidget oldWidget) { diff --git a/lib/src/plugins/plugin.dart b/lib/src/plugins/plugin.dart deleted file mode 100644 index 04062082e..000000000 --- a/lib/src/plugins/plugin.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/layer.dart'; -import 'package:flutter_map/src/map/map.dart'; - -abstract class MapPlugin { - bool supportsLayer(LayerOptions options); - Widget createLayer( - LayerOptions options, MapState mapState, Stream stream); -} diff --git a/pubspec.yaml b/pubspec.yaml index 96f37111a..67af55046 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,11 +1,11 @@ name: flutter_map description: A versatile mapping package for Flutter, based off leaflet.js, that's simple and easy to learn, yet completely customizable and configurable. -version: 2.2.0 +version: 3.0.0 repository: https://github.com/fleaflet/flutter_map documentation: https://docs.fleaflet.dev environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=2.0.0" dependencies: diff --git a/test/flutter_map_test.dart b/test/flutter_map_test.dart index 4ffd8158b..6148ff3b6 100644 --- a/test/flutter_map_test.dart +++ b/test/flutter_map_test.dart @@ -109,12 +109,12 @@ class _TestAppState extends State { center: LatLng(45.5231, -122.6765), zoom: 13, ), - layers: [ - TileLayerOptions( + children: [ + TileLayer( urlTemplate: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c']), - MarkerLayerOptions(markers: _markers), + MarkerLayer(markers: _markers), ], ), ),