diff --git a/example/lib/main.dart b/example/lib/main.dart index 47baa858d..5b24650d0 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,6 +6,7 @@ import './pages/custom_crs/custom_crs.dart'; import './pages/esri.dart'; import './pages/home.dart'; import './pages/live_location.dart'; +import './pages/many_markers.dart'; import './pages/map_controller.dart'; import './pages/marker_anchor.dart'; import './pages/marker_rotate.dart'; @@ -62,6 +63,7 @@ class MyApp extends StatelessWidget { TileLoadingErrorHandle.route: (context) => TileLoadingErrorHandle(), TileBuilderPage.route: (context) => TileBuilderPage(), InteractiveTestPage.route: (context) => InteractiveTestPage(), + ManyMarkersPage.route: (context) => ManyMarkersPage(), }, ); } diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart new file mode 100644 index 000000000..0b1fc4fd5 --- /dev/null +++ b/example/lib/pages/many_markers.dart @@ -0,0 +1,95 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; + +import '../widgets/drawer.dart'; + +const maxMarkersCount = 5000; + +/// On this page, [maxMarkersCount] markers are randomly generated +/// across europe, and then you can limit them with a slider +/// +/// This way, you can test how map performs under a lot of markers +class ManyMarkersPage extends StatefulWidget { + static const String route = '/many_markers'; + + @override + _ManyMarkersPageState createState() => _ManyMarkersPageState(); +} + +class _ManyMarkersPageState extends State { + double doubleInRange(Random source, num start, num end) => + source.nextDouble() * (end - start) + start; + List allMarkers = []; + + int _sliderVal = maxMarkersCount ~/ 10; + + @override + void initState() { + super.initState(); + Future.microtask(() { + var r = Random(); + for (var x = 0; x < maxMarkersCount; x++) { + allMarkers.add( + Marker( + point: LatLng( + doubleInRange(r, 37, 55), + doubleInRange(r, -9, 30), + ), + builder: (context) => const Icon( + Icons.circle, + color: Colors.red, + size: 12.0, + ), + ), + ); + } + setState(() {}); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text('A lot of markers')), + drawer: buildDrawer(context, ManyMarkersPage.route), + body: Column( + children: [ + Slider( + min: 0, + max: maxMarkersCount.toDouble(), + divisions: maxMarkersCount ~/ 500, + label: 'Markers', + value: _sliderVal.toDouble(), + onChanged: (newVal) { + _sliderVal = newVal.toInt(); + setState(() {}); + }, + ), + Text('$_sliderVal markers'), + Flexible( + child: FlutterMap( + options: MapOptions( + center: LatLng(50, 20), + zoom: 5.0, + interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate, + ), + layers: [ + TileLayerOptions( + urlTemplate: + 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + ), + MarkerLayerOptions( + markers: allMarkers.sublist( + 0, min(allMarkers.length, _sliderVal))), + ], + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/drawer.dart b/example/lib/widgets/drawer.dart index ae5c691f2..d66a7ba9c 100644 --- a/example/lib/widgets/drawer.dart +++ b/example/lib/widgets/drawer.dart @@ -8,6 +8,7 @@ import '../pages/esri.dart'; import '../pages/home.dart'; import '../pages/interactive_test_page.dart'; import '../pages/live_location.dart'; +import '../pages/many_markers.dart'; import '../pages/map_controller.dart'; import '../pages/marker_anchor.dart'; import '../pages/moving_markers.dart'; @@ -195,6 +196,13 @@ Drawer buildDrawer(BuildContext context, String currentRoute) { InteractiveTestPage.route, currentRoute, ), + ListTile( + title: const Text('A lot of markers'), + selected: currentRoute == ManyMarkersPage.route, + onTap: () { + Navigator.pushReplacementNamed(context, ManyMarkersPage.route); + }, + ) ], ), ); diff --git a/lib/src/layer/marker_layer.dart b/lib/src/layer/marker_layer.dart index 1d758e90d..639422178 100644 --- a/lib/src/layer/marker_layer.dart +++ b/lib/src/layer/marker_layer.dart @@ -157,7 +157,7 @@ class MarkerLayerWidget extends StatelessWidget { } } -class MarkerLayer extends StatelessWidget { +class MarkerLayer extends StatefulWidget { final MarkerLayerOptions markerLayerOptions; final MapState map; final Stream stream; @@ -165,61 +165,89 @@ class MarkerLayer extends StatelessWidget { MarkerLayer(this.markerLayerOptions, this.map, this.stream) : super(key: markerLayerOptions.key); - bool _boundsContainsMarker(Marker marker) { - var pixelPoint = map.project(marker.point); + @override + _MarkerLayerState createState() => _MarkerLayerState(); +} + +class _MarkerLayerState extends State { + var lastZoom = -1.0; - final width = marker.width - marker.anchor.left; - final height = marker.height - marker.anchor.top; + /// 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 = []; - var sw = CustomPoint(pixelPoint.x + width, pixelPoint.y - height); - var ne = CustomPoint(pixelPoint.x - width, pixelPoint.y + height); - return map.pixelBounds.containsPartialBounds(Bounds(sw, ne)); + // Calling this every time markerOpts change should guarantee proper length + List generatePxCache() => List.generate( + widget.markerLayerOptions.markers.length, + (i) => widget.map.project(widget.markerLayerOptions.markers[i].point), + ); + + @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: stream, // a Stream or null + stream: widget.stream, // a Stream or null builder: (BuildContext context, AsyncSnapshot snapshot) { var markers = []; - for (var markerOpt in markerLayerOptions.markers) { - var pos = map.project(markerOpt.point); - pos = pos.multiplyBy(map.getZoomScale(map.zoom, map.zoom)) - - map.getPixelOrigin(); + final sameZoom = widget.map.zoom == lastZoom; + for (var i = 0; i < widget.markerLayerOptions.markers.length; i++) { + var marker = widget.markerLayerOptions.markers[i]; - var pixelPosX = - (pos.x - (markerOpt.width - markerOpt.anchor.left)).toDouble(); - var pixelPosY = - (pos.y - (markerOpt.height - markerOpt.anchor.top)).toDouble(); + // Decide whether to use cached point or calculate it + var pxPoint = + sameZoom ? _pxCache[i] : widget.map.project(marker.point); + if (!sameZoom) { + _pxCache[i] = pxPoint; + } - if (!_boundsContainsMarker(markerOpt)) { + final width = marker.width - marker.anchor.left; + final height = marker.height - marker.anchor.top; + var sw = CustomPoint(pxPoint.x + width, pxPoint.y - height); + var ne = CustomPoint(pxPoint.x - width, pxPoint.y + height); + + if (!widget.map.pixelBounds.containsPartialBounds(Bounds(sw, ne))) { continue; } - Widget marker; - if (markerOpt.rotate ?? markerLayerOptions.rotate) { - // Counter rotated marker to the map rotation - marker = Transform.rotate( - angle: -map.rotationRad, - origin: markerOpt.rotateOrigin ?? markerLayerOptions.rotateOrigin, - alignment: markerOpt.rotateAlignment ?? - markerLayerOptions.rotateAlignment, - child: markerOpt.builder(context), - ); - } else { - marker = markerOpt.builder(context); - } + final pos = pxPoint - widget.map.getPixelOrigin(); + final markerWidget = + (marker.rotate ?? widget.markerLayerOptions.rotate) + // Counter rotated marker to the map rotation + ? Transform.rotate( + angle: -widget.map.rotationRad, + origin: marker.rotateOrigin ?? + widget.markerLayerOptions.rotateOrigin, + alignment: marker.rotateAlignment ?? + widget.markerLayerOptions.rotateAlignment, + child: marker.builder(context), + ) + : marker.builder(context); markers.add( Positioned( - width: markerOpt.width, - height: markerOpt.height, - left: pixelPosX, - top: pixelPosY, - child: marker, + width: marker.width, + height: marker.height, + left: pos.x - width, + top: pos.y - height, + child: markerWidget, ), ); } + lastZoom = widget.map.zoom; return Container( child: Stack( children: markers,