From 771d4c23fb72ff264d80ce3ad0f4918be7fee99e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 4 Aug 2023 16:38:57 +0200 Subject: [PATCH] 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. --- lib/src/layer/label.dart | 98 +++++++++++++++----------------- lib/src/layer/polygon_layer.dart | 67 +++++++++++++++------- 2 files changed, 92 insertions(+), 73 deletions(-) diff --git a/lib/src/layer/label.dart b/lib/src/layer/label.dart index 4fec2690e..7e3e43718 100644 --- a/lib/src/layer/label.dart +++ b/lib/src/layer/label.dart @@ -5,33 +5,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/plugin_api.dart'; import 'package:polylabel/polylabel.dart'; -@immutable -class Label { - final List 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 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, @@ -39,21 +30,22 @@ class Label { 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); @@ -62,22 +54,26 @@ class Label { canvas, Offset(dx, dy), ); - canvas.restore(); - } + if (rotate) { + canvas.restore(); + } + }; } } + return null; +} - Offset _computeCentroid(List points) { - return Offset( - points.map((e) => e.dx).toList().average, - points.map((e) => e.dy).toList().average, - ); - } +Offset _computeCentroid(List points) { + return Offset( + points.map((e) => e.dx).average, + points.map((e) => e.dy).average, + ); +} - Offset _computePolylabel(List points) { - final labelPosition = polylabel([ - points.map((p) => math.Point(p.dx, p.dy)).toList(), - ]); - return labelPosition.point.toOffset(); - } +Offset _computePolylabel(List points) { + final labelPosition = polylabel([ + List.generate( + points.length, (i) => math.Point(points[i].dx, points[i].dy)), + ]); + return labelPosition.point.toOffset(); } diff --git a/lib/src/layer/polygon_layer.dart b/lib/src/layer/polygon_layer.dart index 5748f6f2a..0e9a992b0 100644 --- a/lib/src/layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer.dart @@ -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 @@ -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 @@ -105,7 +113,7 @@ class PolygonLayer extends StatelessWidget { : polygons; return CustomPaint( - painter: PolygonPainter(pgons, map), + painter: PolygonPainter(pgons, map, polygonLabels), size: size, isComplex: true, ); @@ -116,8 +124,10 @@ class PolygonPainter extends CustomPainter { final List 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); @@ -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); + } } }