Skip to content

Commit 771d4c2

Browse files
authored
Increase scope of polygon draw batching for polygons with labels (#1607)
* Polygon labels are expensive. Add a layer-level switch to disable labels to allow reusing the same polygons while toggling the labels, e.g. depending on zoom level. Also a bunch of small optimizations to minimize canvas operations (save/restore) and reduce strain on garbage collection (fewer ephemeral allocations) * Improve scope of polygon draw batching by flushing polygons with labels only if the label would actually be drawn, as determined by the layouting algorithm.
1 parent 1477eab commit 771d4c2

File tree

2 files changed

+92
-73
lines changed

2 files changed

+92
-73
lines changed

lib/src/layer/label.dart

Lines changed: 47 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,55 +5,47 @@ import 'package:flutter/material.dart';
55
import 'package:flutter_map/plugin_api.dart';
66
import 'package:polylabel/polylabel.dart';
77

8-
@immutable
9-
class Label {
10-
final List<Offset> points;
11-
final String? labelText;
12-
final TextStyle? labelStyle;
13-
final double rotationRad;
14-
final bool rotate;
15-
final PolygonLabelPlacement labelPlacement;
8+
void Function(Canvas canvas)? buildLabelTextPainter({
9+
required String labelText,
10+
required List<Offset> points,
11+
required double rotationRad,
12+
bool rotate = false,
13+
TextStyle? labelStyle,
14+
PolygonLabelPlacement labelPlacement = PolygonLabelPlacement.polylabel,
15+
double padding = 0,
16+
}) {
17+
final placementPoint = switch (labelPlacement) {
18+
PolygonLabelPlacement.centroid => _computeCentroid(points),
19+
PolygonLabelPlacement.polylabel => _computePolylabel(points),
20+
};
1621

17-
const Label({
18-
required this.points,
19-
this.labelText,
20-
this.labelStyle,
21-
required this.rotationRad,
22-
this.rotate = false,
23-
this.labelPlacement = PolygonLabelPlacement.polylabel,
24-
});
25-
26-
void paintText(Canvas canvas) {
27-
final placementPoint = switch (labelPlacement) {
28-
PolygonLabelPlacement.centroid => _computeCentroid(points),
29-
PolygonLabelPlacement.polylabel => _computePolylabel(points),
30-
};
31-
32-
var dx = placementPoint.dx;
33-
var dy = placementPoint.dy;
22+
var dx = placementPoint.dx;
23+
var dy = placementPoint.dy;
3424

25+
if (dx > 0) {
3526
final textSpan = TextSpan(text: labelText, style: labelStyle);
3627
final textPainter = TextPainter(
3728
text: textSpan,
3829
textAlign: TextAlign.center,
3930
textDirection: TextDirection.ltr,
4031
maxLines: 1,
4132
);
42-
if (dx > 0) {
43-
textPainter.layout();
44-
dx -= textPainter.width / 2;
45-
dy -= textPainter.height / 2;
4633

47-
var maxDx = 0.0;
48-
var minDx = double.infinity;
49-
for (final point in points) {
50-
maxDx = math.max(maxDx, point.dx);
51-
minDx = math.min(minDx, point.dx);
52-
}
34+
textPainter.layout();
35+
dx -= textPainter.width / 2;
36+
dy -= textPainter.height / 2;
37+
38+
var maxDx = 0.0;
39+
var minDx = double.infinity;
40+
for (final point in points) {
41+
maxDx = math.max(maxDx, point.dx);
42+
minDx = math.min(minDx, point.dx);
43+
}
5344

54-
if (maxDx - minDx > textPainter.width) {
55-
canvas.save();
45+
if (maxDx - minDx - padding > textPainter.width) {
46+
return (canvas) {
5647
if (rotate) {
48+
canvas.save();
5749
canvas.translate(placementPoint.dx, placementPoint.dy);
5850
canvas.rotate(-rotationRad);
5951
canvas.translate(-placementPoint.dx, -placementPoint.dy);
@@ -62,22 +54,26 @@ class Label {
6254
canvas,
6355
Offset(dx, dy),
6456
);
65-
canvas.restore();
66-
}
57+
if (rotate) {
58+
canvas.restore();
59+
}
60+
};
6761
}
6862
}
63+
return null;
64+
}
6965

70-
Offset _computeCentroid(List<Offset> points) {
71-
return Offset(
72-
points.map((e) => e.dx).toList().average,
73-
points.map((e) => e.dy).toList().average,
74-
);
75-
}
66+
Offset _computeCentroid(List<Offset> points) {
67+
return Offset(
68+
points.map((e) => e.dx).average,
69+
points.map((e) => e.dy).average,
70+
);
71+
}
7672

77-
Offset _computePolylabel(List<Offset> points) {
78-
final labelPosition = polylabel([
79-
points.map((p) => math.Point(p.dx, p.dy)).toList(),
80-
]);
81-
return labelPosition.point.toOffset();
82-
}
73+
Offset _computePolylabel(List<Offset> points) {
74+
final labelPosition = polylabel([
75+
List<math.Point>.generate(
76+
points.length, (i) => math.Point(points[i].dx, points[i].dy)),
77+
]);
78+
return labelPosition.point.toOffset();
8379
}

lib/src/layer/polygon_layer.dart

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,18 +66,22 @@ class Polygon {
6666
}) : _filledAndClockwise = isFilled && isClockwise(points);
6767

6868
/// Used to batch draw calls to the canvas.
69-
int get renderHashCode => Object.hash(
70-
holePointsList,
71-
color,
72-
borderStrokeWidth,
73-
borderColor,
74-
isDotted,
75-
isFilled,
76-
strokeCap,
77-
strokeJoin,
78-
labelStyle,
79-
_filledAndClockwise,
80-
);
69+
int get renderHashCode {
70+
_hash ??= Object.hash(
71+
holePointsList,
72+
color,
73+
borderStrokeWidth,
74+
borderColor,
75+
isDotted,
76+
isFilled,
77+
strokeCap,
78+
strokeJoin,
79+
_filledAndClockwise,
80+
);
81+
return _hash!;
82+
}
83+
84+
int? _hash;
8185
}
8286

8387
@immutable
@@ -87,10 +91,14 @@ class PolygonLayer extends StatelessWidget {
8791
/// screen space culling of polygons based on bounding box
8892
final bool polygonCulling;
8993

94+
// Turn on/off per-polygon label drawing on the layer-level.
95+
final bool polygonLabels;
96+
9097
const PolygonLayer({
9198
super.key,
9299
this.polygons = const [],
93100
this.polygonCulling = false,
101+
this.polygonLabels = true,
94102
});
95103

96104
@override
@@ -105,7 +113,7 @@ class PolygonLayer extends StatelessWidget {
105113
: polygons;
106114

107115
return CustomPaint(
108-
painter: PolygonPainter(pgons, map),
116+
painter: PolygonPainter(pgons, map, polygonLabels),
109117
size: size,
110118
isComplex: true,
111119
);
@@ -116,8 +124,10 @@ class PolygonPainter extends CustomPainter {
116124
final List<Polygon> polygons;
117125
final MapCamera map;
118126
final LatLngBounds bounds;
127+
final bool polygonLabels;
119128

120-
PolygonPainter(this.polygons, this.map) : bounds = map.visibleBounds;
129+
PolygonPainter(this.polygons, this.map, this.polygonLabels)
130+
: bounds = map.visibleBounds;
121131

122132
int get hash {
123133
_hash ??= Object.hashAll(polygons);
@@ -219,19 +229,32 @@ class PolygonPainter extends CustomPainter {
219229
}
220230
}
221231

222-
if (polygon.label != null) {
223-
// Labels are expensive. The `paintText` below is a canvas draw
224-
// operation and thus requires us to reset the draw batching here.
225-
drawPaths();
226-
227-
Label(
232+
if (polygonLabels && polygon.label != null) {
233+
// Labels are expensive because:
234+
// * they themselves cannot easily be pulled into our batched path
235+
// painting with the given text APIs
236+
// * therefore, they require us to flush the batch of polygon draws to
237+
// ensure polygons and labels are stacked correctly, i.e.:
238+
// p1, p1_label, p2, p2_label, ... .
239+
240+
// The painter will be null if the layouting algorithm determined that
241+
// there isn't enough space.
242+
final painter = buildLabelTextPainter(
228243
points: offsets,
229-
labelText: polygon.label,
244+
labelText: polygon.label!,
230245
labelStyle: polygon.labelStyle,
231246
rotationRad: map.rotationRad,
232247
rotate: polygon.rotateLabel,
233248
labelPlacement: polygon.labelPlacement,
234-
).paintText(canvas);
249+
padding: 10,
250+
);
251+
252+
if (painter != null) {
253+
// Flush the batch before painting to preserve stacking.
254+
drawPaths();
255+
256+
painter(canvas);
257+
}
235258
}
236259
}
237260

0 commit comments

Comments
 (0)