Skip to content
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -62,6 +63,7 @@ class MyApp extends StatelessWidget {
TileLoadingErrorHandle.route: (context) => TileLoadingErrorHandle(),
TileBuilderPage.route: (context) => TileBuilderPage(),
InteractiveTestPage.route: (context) => InteractiveTestPage(),
ManyMarkersPage.route: (context) => ManyMarkersPage(),
},
);
}
Expand Down
95 changes: 95 additions & 0 deletions example/lib/pages/many_markers.dart
Original file line number Diff line number Diff line change
@@ -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<ManyMarkersPage> {
double doubleInRange(Random source, num start, num end) =>
source.nextDouble() * (end - start) + start;
List<Marker> 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))),
],
),
),
],
),
);
}
}
8 changes: 8 additions & 0 deletions example/lib/widgets/drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
},
)
],
),
);
Expand Down
100 changes: 64 additions & 36 deletions lib/src/layer/marker_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -157,69 +157,97 @@ class MarkerLayerWidget extends StatelessWidget {
}
}

class MarkerLayer extends StatelessWidget {
class MarkerLayer extends StatefulWidget {
final MarkerLayerOptions markerLayerOptions;
final MapState map;
final Stream<Null> stream;

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<MarkerLayer> {
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 = <CustomPoint>[];

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<CustomPoint> 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<int>(
stream: stream, // a Stream<int> or null
stream: widget.stream, // a Stream<int> or null
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
var markers = <Widget>[];
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,
Expand Down