Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CircleMarker Performance Optimizations #1679

Merged
merged 7 commits into from
Oct 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:flutter_map_example/pages/fallback_url_network_page.dart';
import 'package:flutter_map_example/pages/home.dart';
import 'package:flutter_map_example/pages/interactive_test_page.dart';
import 'package:flutter_map_example/pages/latlng_to_screen_point.dart';
import 'package:flutter_map_example/pages/many_circles.dart';
import 'package:flutter_map_example/pages/many_markers.dart';
import 'package:flutter_map_example/pages/map_controller.dart';
import 'package:flutter_map_example/pages/map_inside_listview.dart';
Expand Down Expand Up @@ -60,6 +61,7 @@ class MyApp extends StatelessWidget {
PluginZoomButtons.route: (context) => const PluginZoomButtons(),
OfflineMapPage.route: (context) => const OfflineMapPage(),
MovingMarkersPage.route: (context) => const MovingMarkersPage(),
ManyCirclesPage.route: (context) => const ManyCirclesPage(),
CirclePage.route: (context) => const CirclePage(),
OverlayImagePage.route: (context) => const OverlayImagePage(),
PolygonPage.route: (context) => const PolygonPage(),
Expand Down
94 changes: 94 additions & 0 deletions example/lib/pages/many_circles.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_example/widgets/drawer.dart';
import 'package:latlong2/latlong.dart';

const maxCirclesCount = 20000;

/// On this page, [maxCirclesCount] circles 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 circles
class ManyCirclesPage extends StatefulWidget {
static const String route = '/many_circles';

const ManyCirclesPage({Key? key}) : super(key: key);

@override
_ManyCirclesPageState createState() => _ManyCirclesPageState();
}

class _ManyCirclesPageState extends State<ManyCirclesPage> {
double doubleInRange(Random source, num start, num end) =>
source.nextDouble() * (end - start) + start;
List<CircleMarker> allCircles = [];

int _sliderVal = maxCirclesCount ~/ 10;

@override
void initState() {
super.initState();
Future.microtask(() {
final r = Random();
for (var x = 0; x < maxCirclesCount; x++) {
allCircles.add(
CircleMarker(
point: LatLng(
doubleInRange(r, 37, 55),
doubleInRange(r, -9, 30),
),
color: Colors.red,
radius: 5,
),
);
}
setState(() {});
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('A lot of circles')),
drawer: buildDrawer(context, ManyCirclesPage.route),
body: Column(
children: [
Slider(
min: 0,
max: maxCirclesCount.toDouble(),
divisions: maxCirclesCount ~/ 500,
label: 'Circles',
value: _sliderVal.toDouble(),
onChanged: (newVal) {
_sliderVal = newVal.toInt();
setState(() {});
},
),
Text('$_sliderVal circles'),
Flexible(
child: FlutterMap(
options: const MapOptions(
initialCenter: LatLng(50, 20),
initialZoom: 5,
interactionOptions: InteractionOptions(
flags: InteractiveFlag.all - InteractiveFlag.rotate,
),
),
children: [
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'dev.fleaflet.flutter_map.example',
),
CircleLayer(
circles: allCircles.sublist(
0, min(allCircles.length, _sliderVal))),
],
),
),
],
),
);
}
}
7 changes: 7 additions & 0 deletions example/lib/widgets/drawer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter_map_example/pages/fallback_url_network_page.dart';
import 'package:flutter_map_example/pages/home.dart';
import 'package:flutter_map_example/pages/interactive_test_page.dart';
import 'package:flutter_map_example/pages/latlng_to_screen_point.dart';
import 'package:flutter_map_example/pages/many_circles.dart';
import 'package:flutter_map_example/pages/many_markers.dart';
import 'package:flutter_map_example/pages/map_controller.dart';
import 'package:flutter_map_example/pages/map_inside_listview.dart';
Expand Down Expand Up @@ -180,6 +181,12 @@ Drawer buildDrawer(BuildContext context, String currentRoute) {
MovingMarkersPage.route,
currentRoute,
),
_buildMenuItem(
context,
const Text('Many Circles'),
ManyCirclesPage.route,
currentRoute,
),
const Divider(),
_buildMenuItem(
context,
Expand Down
107 changes: 65 additions & 42 deletions lib/src/layer/circle_layer.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:ui';

import 'package:flutter/widgets.dart';
import 'package:flutter_map/src/map/camera/camera.dart';
import 'package:latlong2/latlong.dart' hide Path;
Expand Down Expand Up @@ -32,71 +34,92 @@ class CircleLayer extends StatelessWidget {

@override
Widget build(BuildContext context) {
const distance = Distance();
final map = MapCamera.of(context);
return LayoutBuilder(
builder: (context, bc) {
final size = Size(bc.maxWidth, bc.maxHeight);
final map = MapCamera.of(context);
final circleWidgets = circles.map<Widget>((circle) {
final offset = map.getOffsetFromOrigin(circle.point);
double? realRadius;
if (circle.useRadiusInMeter) {
final r = distance.offset(circle.point, circle.radius, 180);
final delta = offset - map.getOffsetFromOrigin(r);
realRadius = delta.distance;
}
return CustomPaint(
key: circle.key,
painter: CirclePainter(
circle,
offset: offset,
radius: realRadius ?? 0,
),
size: size,
);
}).toList(growable: false);
return Stack(children: circleWidgets);
return CustomPaint(
painter: CirclePainter(circles, map),
size: size,
);
},
);
}
}

@immutable
class CirclePainter extends CustomPainter {
final CircleMarker circle;
final Offset offset;
final double radius;
final List<CircleMarker> circles;
final MapCamera map;

const CirclePainter(
this.circle, {
this.offset = Offset.zero,
this.radius = 0,
});
const CirclePainter(this.circles, this.map);

@override
void paint(Canvas canvas, Size size) {
const distance = Distance();
final rect = Offset.zero & size;
canvas.clipRect(rect);
final paint = Paint()
..style = PaintingStyle.fill
..color = circle.color;

_paintCircle(canvas, offset,
circle.useRadiusInMeter ? radius : circle.radius, paint);
// Let's calculate all the points grouped by color and radius
final points = <Color, Map<double, List<Offset>>>{};
final pointsBorder = <Color, Map<double, List<Offset>>>{};
for (final circle in circles) {
final offset = map.getOffsetFromOrigin(circle.point);
double radius = circle.radius;
if (circle.useRadiusInMeter) {
final r = distance.offset(circle.point, circle.radius, 180);
final delta = offset - map.getOffsetFromOrigin(r);
radius = delta.distance;
}
points[circle.color] ??= {};
points[circle.color]![radius] ??= [];
points[circle.color]![radius]!.add(offset);

if (circle.borderStrokeWidth > 0) {
double radiusBorder = circle.radius + circle.borderStrokeWidth;
if (circle.useRadiusInMeter) {
final rBorder = distance.offset(circle.point, radiusBorder, 180);
final deltaBorder = offset - map.getOffsetFromOrigin(rBorder);
radiusBorder = deltaBorder.distance;
}
pointsBorder[circle.borderColor] ??= {};
pointsBorder[circle.borderColor]![radiusBorder] ??= [];
pointsBorder[circle.borderColor]![radiusBorder]!.add(offset);
}
}

if (circle.borderStrokeWidth > 0) {
// Now that all the points are grouped, let's draw them
// First by border in order to be under the circle
for (final color in pointsBorder.keys) {
final paint = Paint()
..style = PaintingStyle.stroke
..color = circle.borderColor
..strokeWidth = circle.borderStrokeWidth;
..strokeCap = StrokeCap.round
..isAntiAlias = false
..color = color;
final pointsByRadius = pointsBorder[color]!;
for (final radius in pointsByRadius.keys) {
final pointsByRadiusColor = pointsByRadius[radius]!;
final radiusPaint = paint..strokeWidth = radius;
_paintCircle(canvas, pointsByRadiusColor, radiusPaint);
}
}

_paintCircle(canvas, offset,
circle.useRadiusInMeter ? radius : circle.radius, paint);
// And then the circle
for (final color in points.keys) {
final paint = Paint()
..isAntiAlias = false
..strokeCap = StrokeCap.round
..color = color;
final pointsByRadius = points[color]!;
for (final radius in pointsByRadius.keys) {
final pointsByRadiusColor = pointsByRadius[radius]!;
final radiusPaint = paint..strokeWidth = radius;
_paintCircle(canvas, pointsByRadiusColor, radiusPaint);
}
}
}

void _paintCircle(Canvas canvas, Offset offset, double radius, Paint paint) {
canvas.drawCircle(offset, radius, paint);
void _paintCircle(Canvas canvas, List<Offset> offsets, Paint paint) {
canvas.drawPoints(PointMode.points, offsets, paint);
}

@override
Expand Down
10 changes: 8 additions & 2 deletions test/layer/circle_layer_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ void main() {

await tester.pumpWidget(TestApp(circles: circles));
expect(find.byType(FlutterMap), findsOneWidget);
expect(find.byType(CircleLayer), findsWidgets);
expect(find.byKey(key), findsOneWidget);
expect(find.byType(CircleLayer), findsOneWidget);

// Assert that batching works and all circles are drawn into the same
// CustomPaint/Canvas.
expect(
find.descendant(
of: find.byType(CircleLayer), matching: find.byType(CustomPaint)),
findsOneWidget);
});
}