Skip to content

Commit

Permalink
Increase scope of polygon draw batching for polygons with labels (fle…
Browse files Browse the repository at this point in the history
…aflet#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.
  • Loading branch information
ignatz authored Aug 4, 2023
1 parent 1477eab commit 771d4c2
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 73 deletions.
98 changes: 47 additions & 51 deletions lib/src/layer/label.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,47 @@ import 'package:flutter/material.dart';
import 'package:flutter_map/plugin_api.dart';
import 'package:polylabel/polylabel.dart';

@immutable
class Label {
final List<Offset> points;
final String? labelText;
final TextStyle? labelStyle;
final double rotationRad;
final bool rotate;
final PolygonLabelPlacement labelPlacement;
void Function(Canvas canvas)? buildLabelTextPainter({
required String labelText,
required List<Offset> points,
required double rotationRad,
bool rotate = false,
TextStyle? labelStyle,
PolygonLabelPlacement labelPlacement = PolygonLabelPlacement.polylabel,
double padding = 0,
}) {
final placementPoint = switch (labelPlacement) {
PolygonLabelPlacement.centroid => _computeCentroid(points),
PolygonLabelPlacement.polylabel => _computePolylabel(points),
};

const Label({
required this.points,
this.labelText,
this.labelStyle,
required this.rotationRad,
this.rotate = false,
this.labelPlacement = PolygonLabelPlacement.polylabel,
});

void paintText(Canvas canvas) {
final placementPoint = switch (labelPlacement) {
PolygonLabelPlacement.centroid => _computeCentroid(points),
PolygonLabelPlacement.polylabel => _computePolylabel(points),
};

var dx = placementPoint.dx;
var dy = placementPoint.dy;
var dx = placementPoint.dx;
var dy = placementPoint.dy;

if (dx > 0) {
final textSpan = TextSpan(text: labelText, style: labelStyle);
final textPainter = TextPainter(
text: textSpan,
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
maxLines: 1,
);
if (dx > 0) {
textPainter.layout();
dx -= textPainter.width / 2;
dy -= textPainter.height / 2;

var maxDx = 0.0;
var minDx = double.infinity;
for (final point in points) {
maxDx = math.max(maxDx, point.dx);
minDx = math.min(minDx, point.dx);
}
textPainter.layout();
dx -= textPainter.width / 2;
dy -= textPainter.height / 2;

var maxDx = 0.0;
var minDx = double.infinity;
for (final point in points) {
maxDx = math.max(maxDx, point.dx);
minDx = math.min(minDx, point.dx);
}

if (maxDx - minDx > textPainter.width) {
canvas.save();
if (maxDx - minDx - padding > textPainter.width) {
return (canvas) {
if (rotate) {
canvas.save();
canvas.translate(placementPoint.dx, placementPoint.dy);
canvas.rotate(-rotationRad);
canvas.translate(-placementPoint.dx, -placementPoint.dy);
Expand All @@ -62,22 +54,26 @@ class Label {
canvas,
Offset(dx, dy),
);
canvas.restore();
}
if (rotate) {
canvas.restore();
}
};
}
}
return null;
}

Offset _computeCentroid(List<Offset> points) {
return Offset(
points.map((e) => e.dx).toList().average,
points.map((e) => e.dy).toList().average,
);
}
Offset _computeCentroid(List<Offset> points) {
return Offset(
points.map((e) => e.dx).average,
points.map((e) => e.dy).average,
);
}

Offset _computePolylabel(List<Offset> points) {
final labelPosition = polylabel([
points.map((p) => math.Point(p.dx, p.dy)).toList(),
]);
return labelPosition.point.toOffset();
}
Offset _computePolylabel(List<Offset> points) {
final labelPosition = polylabel([
List<math.Point>.generate(
points.length, (i) => math.Point(points[i].dx, points[i].dy)),
]);
return labelPosition.point.toOffset();
}
67 changes: 45 additions & 22 deletions lib/src/layer/polygon_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,22 @@ class Polygon {
}) : _filledAndClockwise = isFilled && isClockwise(points);

/// Used to batch draw calls to the canvas.
int get renderHashCode => Object.hash(
holePointsList,
color,
borderStrokeWidth,
borderColor,
isDotted,
isFilled,
strokeCap,
strokeJoin,
labelStyle,
_filledAndClockwise,
);
int get renderHashCode {
_hash ??= Object.hash(
holePointsList,
color,
borderStrokeWidth,
borderColor,
isDotted,
isFilled,
strokeCap,
strokeJoin,
_filledAndClockwise,
);
return _hash!;
}

int? _hash;
}

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

// Turn on/off per-polygon label drawing on the layer-level.
final bool polygonLabels;

const PolygonLayer({
super.key,
this.polygons = const [],
this.polygonCulling = false,
this.polygonLabels = true,
});

@override
Expand All @@ -105,7 +113,7 @@ class PolygonLayer extends StatelessWidget {
: polygons;

return CustomPaint(
painter: PolygonPainter(pgons, map),
painter: PolygonPainter(pgons, map, polygonLabels),
size: size,
isComplex: true,
);
Expand All @@ -116,8 +124,10 @@ class PolygonPainter extends CustomPainter {
final List<Polygon> polygons;
final MapCamera map;
final LatLngBounds bounds;
final bool polygonLabels;

PolygonPainter(this.polygons, this.map) : bounds = map.visibleBounds;
PolygonPainter(this.polygons, this.map, this.polygonLabels)
: bounds = map.visibleBounds;

int get hash {
_hash ??= Object.hashAll(polygons);
Expand Down Expand Up @@ -219,19 +229,32 @@ class PolygonPainter extends CustomPainter {
}
}

if (polygon.label != null) {
// Labels are expensive. The `paintText` below is a canvas draw
// operation and thus requires us to reset the draw batching here.
drawPaths();

Label(
if (polygonLabels && polygon.label != null) {
// Labels are expensive because:
// * they themselves cannot easily be pulled into our batched path
// painting with the given text APIs
// * therefore, they require us to flush the batch of polygon draws to
// ensure polygons and labels are stacked correctly, i.e.:
// p1, p1_label, p2, p2_label, ... .

// The painter will be null if the layouting algorithm determined that
// there isn't enough space.
final painter = buildLabelTextPainter(
points: offsets,
labelText: polygon.label,
labelText: polygon.label!,
labelStyle: polygon.labelStyle,
rotationRad: map.rotationRad,
rotate: polygon.rotateLabel,
labelPlacement: polygon.labelPlacement,
).paintText(canvas);
padding: 10,
);

if (painter != null) {
// Flush the batch before painting to preserve stacking.
drawPaths();

painter(canvas);
}
}
}

Expand Down

0 comments on commit 771d4c2

Please sign in to comment.