Skip to content

Commit 0326672

Browse files
Optimize markers calculations (fleaflet#826)
* Add ManyMarkers example * Just move the goddamn _boundsContainsMarker to the beginning... * Share the .project() * Share even more stuff and simplify code a little * What is this?? * Experimental: cache list of pixel locations * Apply stuff from code review - change to stateful - make loop nicer * Okay linter, whatever you want... * OMG FIXING MERGE CONFLICTS IS *HARD* Or my IDE is just useless * Fix range error
1 parent f9ad288 commit 0326672

File tree

4 files changed

+169
-36
lines changed

4 files changed

+169
-36
lines changed

example/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import './pages/custom_crs/custom_crs.dart';
66
import './pages/esri.dart';
77
import './pages/home.dart';
88
import './pages/live_location.dart';
9+
import './pages/many_markers.dart';
910
import './pages/map_controller.dart';
1011
import './pages/marker_anchor.dart';
1112
import './pages/marker_rotate.dart';
@@ -62,6 +63,7 @@ class MyApp extends StatelessWidget {
6263
TileLoadingErrorHandle.route: (context) => TileLoadingErrorHandle(),
6364
TileBuilderPage.route: (context) => TileBuilderPage(),
6465
InteractiveTestPage.route: (context) => InteractiveTestPage(),
66+
ManyMarkersPage.route: (context) => ManyMarkersPage(),
6567
},
6668
);
6769
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import 'dart:math';
2+
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_map/flutter_map.dart';
5+
import 'package:latlong2/latlong.dart';
6+
7+
import '../widgets/drawer.dart';
8+
9+
const maxMarkersCount = 5000;
10+
11+
/// On this page, [maxMarkersCount] markers are randomly generated
12+
/// across europe, and then you can limit them with a slider
13+
///
14+
/// This way, you can test how map performs under a lot of markers
15+
class ManyMarkersPage extends StatefulWidget {
16+
static const String route = '/many_markers';
17+
18+
@override
19+
_ManyMarkersPageState createState() => _ManyMarkersPageState();
20+
}
21+
22+
class _ManyMarkersPageState extends State<ManyMarkersPage> {
23+
double doubleInRange(Random source, num start, num end) =>
24+
source.nextDouble() * (end - start) + start;
25+
List<Marker> allMarkers = [];
26+
27+
int _sliderVal = maxMarkersCount ~/ 10;
28+
29+
@override
30+
void initState() {
31+
super.initState();
32+
Future.microtask(() {
33+
var r = Random();
34+
for (var x = 0; x < maxMarkersCount; x++) {
35+
allMarkers.add(
36+
Marker(
37+
point: LatLng(
38+
doubleInRange(r, 37, 55),
39+
doubleInRange(r, -9, 30),
40+
),
41+
builder: (context) => const Icon(
42+
Icons.circle,
43+
color: Colors.red,
44+
size: 12.0,
45+
),
46+
),
47+
);
48+
}
49+
setState(() {});
50+
});
51+
}
52+
53+
@override
54+
Widget build(BuildContext context) {
55+
return Scaffold(
56+
appBar: AppBar(title: Text('A lot of markers')),
57+
drawer: buildDrawer(context, ManyMarkersPage.route),
58+
body: Column(
59+
children: [
60+
Slider(
61+
min: 0,
62+
max: maxMarkersCount.toDouble(),
63+
divisions: maxMarkersCount ~/ 500,
64+
label: 'Markers',
65+
value: _sliderVal.toDouble(),
66+
onChanged: (newVal) {
67+
_sliderVal = newVal.toInt();
68+
setState(() {});
69+
},
70+
),
71+
Text('$_sliderVal markers'),
72+
Flexible(
73+
child: FlutterMap(
74+
options: MapOptions(
75+
center: LatLng(50, 20),
76+
zoom: 5.0,
77+
interactiveFlags: InteractiveFlag.all - InteractiveFlag.rotate,
78+
),
79+
layers: [
80+
TileLayerOptions(
81+
urlTemplate:
82+
'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
83+
subdomains: ['a', 'b', 'c'],
84+
),
85+
MarkerLayerOptions(
86+
markers: allMarkers.sublist(
87+
0, min(allMarkers.length, _sliderVal))),
88+
],
89+
),
90+
),
91+
],
92+
),
93+
);
94+
}
95+
}

example/lib/widgets/drawer.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../pages/esri.dart';
88
import '../pages/home.dart';
99
import '../pages/interactive_test_page.dart';
1010
import '../pages/live_location.dart';
11+
import '../pages/many_markers.dart';
1112
import '../pages/map_controller.dart';
1213
import '../pages/marker_anchor.dart';
1314
import '../pages/moving_markers.dart';
@@ -195,6 +196,13 @@ Drawer buildDrawer(BuildContext context, String currentRoute) {
195196
InteractiveTestPage.route,
196197
currentRoute,
197198
),
199+
ListTile(
200+
title: const Text('A lot of markers'),
201+
selected: currentRoute == ManyMarkersPage.route,
202+
onTap: () {
203+
Navigator.pushReplacementNamed(context, ManyMarkersPage.route);
204+
},
205+
)
198206
],
199207
),
200208
);

lib/src/layer/marker_layer.dart

Lines changed: 64 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -157,69 +157,97 @@ class MarkerLayerWidget extends StatelessWidget {
157157
}
158158
}
159159

160-
class MarkerLayer extends StatelessWidget {
160+
class MarkerLayer extends StatefulWidget {
161161
final MarkerLayerOptions markerLayerOptions;
162162
final MapState map;
163163
final Stream<Null> stream;
164164

165165
MarkerLayer(this.markerLayerOptions, this.map, this.stream)
166166
: super(key: markerLayerOptions.key);
167167

168-
bool _boundsContainsMarker(Marker marker) {
169-
var pixelPoint = map.project(marker.point);
168+
@override
169+
_MarkerLayerState createState() => _MarkerLayerState();
170+
}
171+
172+
class _MarkerLayerState extends State<MarkerLayer> {
173+
var lastZoom = -1.0;
170174

171-
final width = marker.width - marker.anchor.left;
172-
final height = marker.height - marker.anchor.top;
175+
/// List containing cached pixel positions of markers
176+
/// Should be discarded when zoom changes
177+
// Has a fixed length of markerOpts.markers.length - better performance:
178+
// https://stackoverflow.com/questions/15943890/is-there-a-performance-benefit-in-using-fixed-length-lists-in-dart
179+
var _pxCache = <CustomPoint>[];
173180

174-
var sw = CustomPoint(pixelPoint.x + width, pixelPoint.y - height);
175-
var ne = CustomPoint(pixelPoint.x - width, pixelPoint.y + height);
176-
return map.pixelBounds.containsPartialBounds(Bounds(sw, ne));
181+
// Calling this every time markerOpts change should guarantee proper length
182+
List<CustomPoint> generatePxCache() => List.generate(
183+
widget.markerLayerOptions.markers.length,
184+
(i) => widget.map.project(widget.markerLayerOptions.markers[i].point),
185+
);
186+
187+
@override
188+
void initState() {
189+
super.initState();
190+
_pxCache = generatePxCache();
191+
}
192+
193+
@override
194+
void didUpdateWidget(covariant MarkerLayer oldWidget) {
195+
super.didUpdateWidget(oldWidget);
196+
lastZoom = -1.0;
197+
_pxCache = generatePxCache();
177198
}
178199

179200
@override
180201
Widget build(BuildContext context) {
181202
return StreamBuilder<int>(
182-
stream: stream, // a Stream<int> or null
203+
stream: widget.stream, // a Stream<int> or null
183204
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
184205
var markers = <Widget>[];
185-
for (var markerOpt in markerLayerOptions.markers) {
186-
var pos = map.project(markerOpt.point);
187-
pos = pos.multiplyBy(map.getZoomScale(map.zoom, map.zoom)) -
188-
map.getPixelOrigin();
206+
final sameZoom = widget.map.zoom == lastZoom;
207+
for (var i = 0; i < widget.markerLayerOptions.markers.length; i++) {
208+
var marker = widget.markerLayerOptions.markers[i];
189209

190-
var pixelPosX =
191-
(pos.x - (markerOpt.width - markerOpt.anchor.left)).toDouble();
192-
var pixelPosY =
193-
(pos.y - (markerOpt.height - markerOpt.anchor.top)).toDouble();
210+
// Decide whether to use cached point or calculate it
211+
var pxPoint =
212+
sameZoom ? _pxCache[i] : widget.map.project(marker.point);
213+
if (!sameZoom) {
214+
_pxCache[i] = pxPoint;
215+
}
194216

195-
if (!_boundsContainsMarker(markerOpt)) {
217+
final width = marker.width - marker.anchor.left;
218+
final height = marker.height - marker.anchor.top;
219+
var sw = CustomPoint(pxPoint.x + width, pxPoint.y - height);
220+
var ne = CustomPoint(pxPoint.x - width, pxPoint.y + height);
221+
222+
if (!widget.map.pixelBounds.containsPartialBounds(Bounds(sw, ne))) {
196223
continue;
197224
}
198225

199-
Widget marker;
200-
if (markerOpt.rotate ?? markerLayerOptions.rotate) {
201-
// Counter rotated marker to the map rotation
202-
marker = Transform.rotate(
203-
angle: -map.rotationRad,
204-
origin: markerOpt.rotateOrigin ?? markerLayerOptions.rotateOrigin,
205-
alignment: markerOpt.rotateAlignment ??
206-
markerLayerOptions.rotateAlignment,
207-
child: markerOpt.builder(context),
208-
);
209-
} else {
210-
marker = markerOpt.builder(context);
211-
}
226+
final pos = pxPoint - widget.map.getPixelOrigin();
227+
final markerWidget =
228+
(marker.rotate ?? widget.markerLayerOptions.rotate)
229+
// Counter rotated marker to the map rotation
230+
? Transform.rotate(
231+
angle: -widget.map.rotationRad,
232+
origin: marker.rotateOrigin ??
233+
widget.markerLayerOptions.rotateOrigin,
234+
alignment: marker.rotateAlignment ??
235+
widget.markerLayerOptions.rotateAlignment,
236+
child: marker.builder(context),
237+
)
238+
: marker.builder(context);
212239

213240
markers.add(
214241
Positioned(
215-
width: markerOpt.width,
216-
height: markerOpt.height,
217-
left: pixelPosX,
218-
top: pixelPosY,
219-
child: marker,
242+
width: marker.width,
243+
height: marker.height,
244+
left: pos.x - width,
245+
top: pos.y - height,
246+
child: markerWidget,
220247
),
221248
);
222249
}
250+
lastZoom = widget.map.zoom;
223251
return Container(
224252
child: Stack(
225253
children: markers,

0 commit comments

Comments
 (0)