From f9d37df578cd96e2ee205e1e7d49ab2fd8846b08 Mon Sep 17 00:00:00 2001 From: Luka S Date: Tue, 13 Feb 2024 11:40:52 +0000 Subject: [PATCH] perf: add `Canvas.drawVertices` render pathway for `PolygonLayer` & fix bundled drawing (#1800) Co-authored-by: Sebastian --- example/lib/pages/many_circles.dart | 9 +- example/lib/pages/many_markers.dart | 9 +- example/lib/pages/polygon.dart | 116 +++++++++--- example/lib/pages/polygon_perf_stress.dart | 174 ++++++++++++++++-- example/lib/pages/polyline_perf_stress.dart | 8 +- .../lib/widgets/number_of_items_slider.dart | 37 ++-- .../simplification_tolerance_slider.dart | 36 +--- example/pubspec.yaml | 8 +- lib/src/layer/polygon_layer/label.dart | 3 +- lib/src/layer/polygon_layer/painter.dart | 91 ++++++--- lib/src/layer/polygon_layer/polygon.dart | 8 +- .../layer/polygon_layer/polygon_layer.dart | 75 ++++++-- .../polygon_layer/projected_polygon.dart | 10 +- lib/src/layer/polyline_layer/painter.dart | 15 +- lib/src/misc/offsets.dart | 30 +-- lib/src/misc/simplify.dart | 3 + pubspec.yaml | 1 + 17 files changed, 461 insertions(+), 172 deletions(-) diff --git a/example/lib/pages/many_circles.dart b/example/lib/pages/many_circles.dart index 62e7adee7..4c8989607 100644 --- a/example/lib/pages/many_circles.dart +++ b/example/lib/pages/many_circles.dart @@ -29,8 +29,7 @@ class ManyCirclesPageState extends State { source.nextDouble() * (end - start) + start; List allCircles = []; - static const int _initialNumOfCircles = _maxCirclesCount ~/ 10; - int numOfCircles = _initialNumOfCircles; + int numOfCircles = _maxCirclesCount ~/ 10; @override void initState() { @@ -85,10 +84,10 @@ class ManyCirclesPageState extends State { top: 16, right: 16, child: NumberOfItemsSlider( - itemDescription: 'Circle', + number: numOfCircles, + onChanged: (v) => setState(() => numOfCircles = v), maxNumber: _maxCirclesCount, - initialNumber: _initialNumOfCircles, - onChangedNumber: (v) => setState(() => numOfCircles = v), + itemDescription: 'Circle', ), ), if (!kIsWeb) diff --git a/example/lib/pages/many_markers.dart b/example/lib/pages/many_markers.dart index 20e02b446..8b3f54545 100644 --- a/example/lib/pages/many_markers.dart +++ b/example/lib/pages/many_markers.dart @@ -29,8 +29,7 @@ class ManyMarkersPageState extends State { source.nextDouble() * (end - start) + start; List allMarkers = []; - static const int _initialNumOfMarkers = _maxMarkersCount ~/ 10; - int numOfMarkers = _initialNumOfMarkers; + int numOfMarkers = _maxMarkersCount ~/ 10; @override void initState() { @@ -86,10 +85,10 @@ class ManyMarkersPageState extends State { top: 16, right: 16, child: NumberOfItemsSlider( - itemDescription: 'Marker', + number: numOfMarkers, + onChanged: (v) => setState(() => numOfMarkers = v), maxNumber: _maxMarkersCount, - initialNumber: _initialNumOfMarkers, - onChangedNumber: (v) => setState(() => numOfMarkers = v), + itemDescription: 'Marker', ), ), if (!kIsWeb) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 4920f2bd9..04de145bb 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -19,12 +19,6 @@ class PolygonPage extends StatelessWidget { LatLng(54.3498, -6.2603), LatLng(52.8566, 2.3522), ]; - final _notFilledDotedPoints = const [ - LatLng(49.29, -2.57), - LatLng(51.46, -6.43), - LatLng(49.86, -8.17), - LatLng(48.39, -3.49), - ]; final _filledDotedPoints = const [ LatLng(46.35, 4.94), LatLng(46.22, -0.11), @@ -43,17 +37,36 @@ class PolygonPage extends StatelessWidget { LatLng(59.77, -7.01), LatLng(60.77, -6.01), ]; - final _holeOuterPoints = const [ + final _normalHoleOuterPoints = const [ LatLng(50, -18), LatLng(50, -14), + LatLng(51.5, -12.5), + LatLng(54, -14), + LatLng(54, -18), + ]; + final _brokenHoleOuterPoints = const [ + LatLng(50, -18), + LatLng(53, -16), + LatLng(51.5, -12.5), LatLng(54, -14), LatLng(54, -18), ]; final _holeInnerPoints = const [ - LatLng(51, -17), - LatLng(51, -16), - LatLng(52, -16), - LatLng(52, -17), + [ + LatLng(52, -17), + LatLng(52, -16), + LatLng(51.5, -15.5), + LatLng(51, -16), + LatLng(51, -17), + ], + [ + LatLng(53.5, -17), + LatLng(53.5, -16), + LatLng(53, -15), + LatLng(52.25, -15), + LatLng(52.25, -16), + LatLng(52.75, -17), + ], ]; @override @@ -84,12 +97,6 @@ class PolygonPage extends StatelessWidget { borderColor: Colors.yellow, borderStrokeWidth: 4, ), - Polygon( - points: _notFilledDotedPoints, - isDotted: true, - borderColor: Colors.green, - borderStrokeWidth: 4, - ), Polygon( points: _filledDotedPoints, isDotted: true, @@ -112,25 +119,78 @@ class PolygonPage extends StatelessWidget { labelPlacement: PolygonLabelPlacement.polylabel, ), Polygon( - points: _holeOuterPoints, - holePointsList: [_holeInnerPoints], + points: _normalHoleOuterPoints + .map((latlng) => + LatLng(latlng.latitude, latlng.longitude + 8)) + .toList(), + isDotted: true, + holePointsList: _holeInnerPoints + .map( + (latlngs) => latlngs + .map((latlng) => + LatLng(latlng.latitude, latlng.longitude + 8)) + .toList(), + ) + .toList(), borderStrokeWidth: 4, - borderColor: Colors.green, + borderColor: Colors.orange, + color: Colors.orange.withOpacity(0.5), + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacement: PolygonLabelPlacement.centroid, + labelStyle: const TextStyle(color: Colors.black), ), Polygon( - points: _holeOuterPoints + points: _brokenHoleOuterPoints .map((latlng) => - LatLng(latlng.latitude, latlng.longitude + 8)) + LatLng(latlng.latitude - 6, latlng.longitude + 8)) .toList(), isDotted: true, - holePointsList: [ - _holeInnerPoints - .map((latlng) => - LatLng(latlng.latitude, latlng.longitude + 8)) - .toList() - ], + holePointsList: _holeInnerPoints + .map( + (latlngs) => latlngs + .map((latlng) => LatLng( + latlng.latitude - 6, latlng.longitude + 8)) + .toList(), + ) + .toList(), borderStrokeWidth: 4, borderColor: Colors.orange, + color: Colors.orange.withOpacity(0.5), + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacement: PolygonLabelPlacement.centroid, + labelStyle: const TextStyle(color: Colors.black), + ), + ], + ), + PolygonLayer( + simplificationTolerance: 0, + useAltRendering: true, + polygons: [ + Polygon( + points: _normalHoleOuterPoints, + holePointsList: _holeInnerPoints, + borderStrokeWidth: 4, + borderColor: Colors.black, + color: Colors.green, + ), + Polygon( + points: _brokenHoleOuterPoints + .map((latlng) => + LatLng(latlng.latitude - 6, latlng.longitude)) + .toList(), + holePointsList: _holeInnerPoints + .map( + (latlngs) => latlngs + .map((latlng) => + LatLng(latlng.latitude - 6, latlng.longitude)) + .toList(), + ) + .toList(), + borderStrokeWidth: 4, + borderColor: Colors.black, + color: Colors.green, ), ], ), diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index f812ce6d1..086ab727c 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -19,16 +19,11 @@ class PolygonPerfStressPage extends StatefulWidget { } class _PolygonPerfStressPageState extends State { - static const double _initialSimplificationTolerance = 0.5; - double simplificationTolerance = _initialSimplificationTolerance; + double simplificationTolerance = 0.5; + bool useAltRendering = true; + double borderThickness = 1; - late final geoJsonLoader = - rootBundle.loadString('assets/138k-polygon-points.geojson.noformat').then( - (geoJson) => compute( - (geoJson) => GeoJsonParser()..parseGeoJsonAsString(geoJson), - geoJson, - ), - ); + late Future geoJsonParser = loadPolygonsFromGeoJson(); @override void initState() { @@ -38,7 +33,7 @@ class _PolygonPerfStressPageState extends State { @override void dispose() { - geoJsonLoader.ignore(); + geoJsonParser.ignore(); super.dispose(); } @@ -59,22 +54,23 @@ class _PolygonPerfStressPageState extends State { padding: const EdgeInsets.only( left: 16, right: 16, - top: 88, - bottom: 192, + top: 145, + bottom: 175, ), ), ), children: [ openStreetMapTileLayer, FutureBuilder( - future: geoJsonLoader, + future: geoJsonParser, builder: (context, geoJsonParser) => geoJsonParser.connectionState != ConnectionState.done || geoJsonParser.data == null ? const SizedBox.shrink() : PolygonLayer( - simplificationTolerance: simplificationTolerance, polygons: geoJsonParser.data!.polygons, + useAltRendering: useAltRendering, + simplificationTolerance: simplificationTolerance, ), ), ], @@ -83,10 +79,105 @@ class _PolygonPerfStressPageState extends State { left: 16, top: 16, right: 16, - child: SimplificationToleranceSlider( - initialTolerance: _initialSimplificationTolerance, - onChangedTolerance: (v) => - setState(() => simplificationTolerance = v), + child: RepaintBoundary( + child: Column( + children: [ + SimplificationToleranceSlider( + tolerance: simplificationTolerance, + onChanged: (v) => + setState(() => simplificationTolerance = v), + ), + const SizedBox(height: 12), + Wrap( + alignment: WrapAlignment.center, + spacing: 12, + runSpacing: 12, + children: [ + UnconstrainedBox( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(32), + ), + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 16, + ), + child: Row( + children: [ + const Tooltip( + message: 'Use Alternative Rendering Pathway', + child: Icon(Icons.speed_rounded), + ), + const SizedBox(width: 8), + Switch.adaptive( + value: useAltRendering, + onChanged: (v) => + setState(() => useAltRendering = v), + ), + ], + ), + ), + ), + // Not ideal that we have to re-parse the GeoJson every + // time this is changed, but the library gives no easy + // way to change it after + UnconstrainedBox( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(32), + ), + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 6, + children: [ + const Tooltip( + message: 'Border Thickness', + child: Icon(Icons.line_weight_rounded), + ), + if (MediaQuery.devicePixelRatioOf(context) > 1 && + borderThickness == 1) + const Tooltip( + message: 'Screen has a high DPR: 1lp > 1dp', + child: Icon( + Icons.warning, + color: Colors.amber, + ), + ), + const SizedBox.shrink(), + ...List.generate( + 4, + (i) { + final thickness = i * i; + return ChoiceChip( + label: Text( + thickness == 0 + ? 'None' + : '${thickness}px', + ), + selected: borderThickness == thickness, + shape: const StadiumBorder(), + onSelected: (selected) => reloadGeoJson( + context: context, + selected: selected, + thickness: thickness, + ), + ); + }, + ), + ], + ), + ), + ), + ], + ), + ], + ), ), ), if (!kIsWeb) @@ -100,4 +191,51 @@ class _PolygonPerfStressPageState extends State { ), ); } + + Future reloadGeoJson({ + required BuildContext context, + required bool selected, + required num thickness, + }) async { + if (!selected) return; + setState(() => borderThickness = thickness.toDouble()); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + SizedBox.square( + dimension: 16, + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: AlwaysStoppedAnimation( + Colors.white, + ), + ), + ), + SizedBox(width: 12), + Text('Loading GeoJson polygons...'), + ], + ), + ), + ); + await (geoJsonParser = loadPolygonsFromGeoJson()); + if (!context.mounted) return; + ScaffoldMessenger.of(context).clearSnackBars(); + setState(() {}); + } + + Future loadPolygonsFromGeoJson() async { + const filePath = 'assets/138k-polygon-points.geojson.noformat'; + + return rootBundle.loadString(filePath).then( + (geoJson) => compute( + (msg) => GeoJsonParser( + defaultPolygonBorderStroke: msg.borderThickness, + defaultPolygonBorderColor: Colors.black.withOpacity(0.5), + defaultPolygonFillColor: Colors.orange[700]!.withOpacity(0.75), + )..parseGeoJsonAsString(msg.geoJson), + (geoJson: geoJson, borderThickness: borderThickness), + ), + ); + } } diff --git a/example/lib/pages/polyline_perf_stress.dart b/example/lib/pages/polyline_perf_stress.dart index f68f6a755..fcfcf3f65 100644 --- a/example/lib/pages/polyline_perf_stress.dart +++ b/example/lib/pages/polyline_perf_stress.dart @@ -19,8 +19,7 @@ class PolylinePerfStressPage extends StatefulWidget { } class _PolylinePerfStressPageState extends State { - static const double _initialSimplificationTolerance = 0.5; - double simplificationTolerance = _initialSimplificationTolerance; + double simplificationTolerance = 0.5; final _randomWalk = [const LatLng(44.861294, 13.845086)]; @@ -81,9 +80,8 @@ class _PolylinePerfStressPageState extends State { top: 16, right: 16, child: SimplificationToleranceSlider( - initialTolerance: _initialSimplificationTolerance, - onChangedTolerance: (v) => - setState(() => simplificationTolerance = v), + tolerance: simplificationTolerance, + onChanged: (v) => setState(() => simplificationTolerance = v), ), ), if (!kIsWeb) diff --git a/example/lib/widgets/number_of_items_slider.dart b/example/lib/widgets/number_of_items_slider.dart index adb2572a2..ee290c0a4 100644 --- a/example/lib/widgets/number_of_items_slider.dart +++ b/example/lib/widgets/number_of_items_slider.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; -class NumberOfItemsSlider extends StatefulWidget { +class NumberOfItemsSlider extends StatelessWidget { const NumberOfItemsSlider({ super.key, - required this.initialNumber, - required this.onChangedNumber, + required this.number, + required this.onChanged, required this.maxNumber, this.itemDescription = 'Item', int itemsPerDivision = 1000, @@ -14,19 +14,12 @@ class NumberOfItemsSlider extends StatefulWidget { ), divisions = maxNumber ~/ itemsPerDivision; - final int initialNumber; - final void Function(int) onChangedNumber; + final int number; + final void Function(int) onChanged; final String itemDescription; final int maxNumber; final int divisions; - @override - State createState() => _NumberOfItemsSliderState(); -} - -class _NumberOfItemsSliderState extends State { - late int _number = widget.initialNumber; - @override Widget build(BuildContext context) { return DecoratedBox( @@ -39,23 +32,17 @@ class _NumberOfItemsSliderState extends State { child: Row( children: [ Tooltip( - message: 'Adjust Number of ${widget.itemDescription}s', + message: 'Adjust Number of ${itemDescription}s', child: const Icon(Icons.numbers), ), Expanded( - child: Slider( - value: _number.toDouble(), - onChanged: (v) { - if (_number == 0 && v != 0) { - widget.onChangedNumber(v.toInt()); - } - setState(() => _number = v.toInt()); - }, - onChangeEnd: (v) => widget.onChangedNumber(v.toInt()), + child: Slider.adaptive( + value: number.toDouble(), + onChanged: (v) => onChanged(v.toInt()), min: 0, - max: widget.maxNumber.toDouble(), - divisions: widget.divisions, - label: _number.toString(), + max: maxNumber.toDouble(), + divisions: divisions, + label: number.toString(), ), ), ], diff --git a/example/lib/widgets/simplification_tolerance_slider.dart b/example/lib/widgets/simplification_tolerance_slider.dart index e1ecafbe9..d82da1834 100644 --- a/example/lib/widgets/simplification_tolerance_slider.dart +++ b/example/lib/widgets/simplification_tolerance_slider.dart @@ -1,23 +1,14 @@ import 'package:flutter/material.dart'; -class SimplificationToleranceSlider extends StatefulWidget { +class SimplificationToleranceSlider extends StatelessWidget { const SimplificationToleranceSlider({ super.key, - required this.initialTolerance, - required this.onChangedTolerance, + required this.tolerance, + required this.onChanged, }); - final double initialTolerance; - final void Function(double) onChangedTolerance; - - @override - State createState() => - _SimplificationToleranceSliderState(); -} - -class _SimplificationToleranceSliderState - extends State { - late double _simplificationTolerance = widget.initialTolerance; + final double tolerance; + final void Function(double) onChanged; @override Widget build(BuildContext context) { @@ -41,21 +32,14 @@ class _SimplificationToleranceSliderState ), ), Expanded( - child: Slider( - value: _simplificationTolerance, - onChanged: (v) { - if (_simplificationTolerance == 0 && v != 0) { - widget.onChangedTolerance(v); - } - setState(() => _simplificationTolerance = v); - }, - onChangeEnd: widget.onChangedTolerance, + child: Slider.adaptive( + value: tolerance, + onChanged: onChanged, min: 0, max: 2, divisions: 100, - label: _simplificationTolerance == 0 - ? 'Disabled' - : _simplificationTolerance.toStringAsFixed(2), + label: + tolerance == 0 ? 'Disabled' : tolerance.toStringAsFixed(2), ), ), ], diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6edd76632..3ebb255a0 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,11 +14,11 @@ dependencies: flutter_map_cancellable_tile_provider: latlong2: ^0.9.0 proj4dart: ^2.1.0 - url_launcher: ^6.1.14 - shared_preferences: ^2.2.1 + url_launcher: ^6.2.4 + shared_preferences: ^2.2.2 url_strategy: ^0.2.0 - http: ^1.1.0 - vector_math: ^2.1.2 + http: ^1.2.0 + vector_math: ^2.1.4 flutter_map_geojson: ^1.0.6 dependency_overrides: diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index 153fc9e09..372cdead0 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -85,7 +85,8 @@ LatLng _computePolylabel(List points) { // point with more distance to the polygon's outline. It's given in // point-units, i.e. degrees here. A bigger number means less precision, // i.e. cheaper at the expense off less optimal label placement. - precision: 0.000001, + // TODO: Make this an external option + precision: 0.0001, ); return LatLng( labelPosition.point.y.toDouble(), diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 5f12de4fa..af5457eaa 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -6,18 +6,27 @@ class _PolygonPainter extends CustomPainter { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; + /// Triangulated [polygons] if available + /// + /// Expected to be in same/corresponding order as [polygons]. + final List?>? triangles; + /// Reference to the [MapCamera]. final MapCamera camera; /// Reference to the bounding box of the [Polygon]. final LatLngBounds bounds; + /// Whether to draw per-polygon labels final bool polygonLabels; + + /// Whether to draw labels last and thus over all the polygons final bool drawLabelsLast; /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, + required this.triangles, required this.camera, required this.polygonLabels, required this.drawLabelsLast, @@ -33,8 +42,10 @@ class _PolygonPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { - var filledPath = ui.Path(); - var borderPath = ui.Path(); + final trianglePoints = []; + + final filledPath = Path(); + final borderPath = Path(); Polygon? lastPolygon; int? lastHash; @@ -51,18 +62,30 @@ class _PolygonPainter extends CustomPainter { ..style = PaintingStyle.fill ..color = color; - canvas.drawPath(filledPath, paint); + if (trianglePoints.isNotEmpty) { + final points = Float32List(trianglePoints.length * 2); + for (int i = 0; i < trianglePoints.length; ++i) { + points[i * 2] = trianglePoints[i].dx; + points[i * 2 + 1] = trianglePoints[i].dy; + } + final vertices = Vertices.raw(VertexMode.triangles, points); + canvas.drawVertices(vertices, BlendMode.src, paint); + } else { + canvas.drawPath(filledPath, paint); + } } } // Draw polygon outline if (polygon.borderStrokeWidth > 0) { - final borderPaint = _getBorderPaint(polygon); - canvas.drawPath(borderPath, borderPaint); + canvas.drawPath(borderPath, _getBorderPaint(polygon)); } - filledPath = ui.Path(); - borderPath = ui.Path(); + trianglePoints.clear(); + filledPath.reset(); + + borderPath.reset(); + lastPolygon = null; lastHash = null; } @@ -70,12 +93,20 @@ class _PolygonPainter extends CustomPainter { final origin = (camera.project(camera.center) - camera.size / 2).toOffset(); // Main loop constructing batched fill and border paths from given polygons. - for (final projectedPolygon in polygons) { - if (projectedPolygon.points.isEmpty) { - continue; - } + for (int i = 0; i <= polygons.length - 1; i++) { + final projectedPolygon = polygons[i]; + if (projectedPolygon.points.isEmpty) continue; final polygon = projectedPolygon.polygon; - final offsets = getOffsetsXY(camera, origin, projectedPolygon.points); + + final polygonTriangles = triangles?[i]; + + final fillOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + holePoints: + polygonTriangles != null ? projectedPolygon.holePoints : null, + ); // The hash is based on the polygons visual properties. If the hash from // the current and the previous polygon no longer match, we need to flush @@ -91,11 +122,27 @@ class _PolygonPainter extends CustomPainter { // ignore: deprecated_member_use_from_same_package if (polygon.isFilled ?? true) { if (polygon.color != null) { - filledPath.addPolygon(offsets, true); + if (polygonTriangles != null) { + final len = polygonTriangles.length; + for (int i = 0; i < len; ++i) { + trianglePoints.add(fillOffsets[polygonTriangles[i]]); + } + } else { + filledPath.addPolygon(fillOffsets, true); + } } } + if (polygon.borderStrokeWidth > 0.0) { - _addBorderToPath(borderPath, polygon, offsets); + _addBorderToPath( + borderPath, + polygon, + getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + ), + ); } // Afterwards deal with more complicated holes. @@ -133,7 +180,7 @@ class _PolygonPainter extends CustomPainter { // there isn't enough space. final painter = _buildLabelTextPainter( mapSize: camera.size, - placementPoint: camera.getOffsetFromOrigin(polygon.labelPosition), + placementPoint: getOffset(camera, origin, polygon.labelPosition), bounds: getBounds(origin, polygon), textPainter: polygon.textPainter!, rotationRad: camera.rotationRad, @@ -162,8 +209,7 @@ class _PolygonPainter extends CustomPainter { if (textPainter != null) { final painter = _buildLabelTextPainter( mapSize: camera.size, - placementPoint: - camera.project(polygon.labelPosition).toOffset() - origin, + placementPoint: getOffset(camera, origin, polygon.labelPosition), bounds: getBounds(origin, polygon), textPainter: textPainter, rotationRad: camera.rotationRad, @@ -188,7 +234,7 @@ class _PolygonPainter extends CustomPainter { } void _addBorderToPath( - ui.Path path, + Path path, Polygon polygon, List offsets, ) { @@ -202,7 +248,7 @@ class _PolygonPainter extends CustomPainter { } void _addHoleBordersToPath( - ui.Path path, + Path path, Polygon polygon, List> holeOffsetsList, ) { @@ -220,7 +266,7 @@ class _PolygonPainter extends CustomPainter { } void _addDottedLineToPath( - ui.Path path, + Path path, List offsets, double radius, double stepLength, @@ -256,10 +302,13 @@ class _PolygonPainter extends CustomPainter { path.addOval(Rect.fromCircle(center: offsets.last, radius: radius)); } - void _addLineToPath(ui.Path path, List offsets) { + void _addLineToPath(Path path, List offsets) { path.addPolygon(offsets, true); } + // TODO: Fix bug where wrapping layer in some widgets (eg. opacity) causes the + // features to not move unless this is `true`, but `true` significantly impacts + // performance @override bool shouldRepaint(_PolygonPainter oldDelegate) => false; } diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 5dea13cf5..8e3bd0d95 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -49,9 +49,14 @@ class Polygon { /// The [TextStyle] of the [Polygon.label]. final TextStyle labelStyle; - /// The placement logic of the [Polygon.label]. + /// The placement logic of the [Polygon.label] + /// + /// [PolygonLabelPlacement.polylabel] can be expensive for some polygons. If + /// there is a large lag spike, try using [PolygonLabelPlacement.centroid]. final PolygonLabelPlacement labelPlacement; + /// Whether to rotate the label counter to the camera's rotation, to ensure + /// it remains upright final bool rotateLabel; /// Designates whether the given polygon points follow a clock or @@ -152,7 +157,6 @@ class Polygon { /// An optimized hash code dedicated to be used inside the [PolygonPainter]. int get renderHashCode => _renderHashCode ??= Object.hash( - holePointsList, color, borderStrokeWidth, borderColor, diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 3e99ca3a3..091381789 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -1,7 +1,8 @@ import 'dart:math' as math; -import 'dart:ui' as ui; +import 'dart:ui'; import 'package:collection/collection.dart'; +import 'package:dart_earcut/dart_earcut.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -21,14 +22,31 @@ class PolygonLayer extends StatefulWidget { /// [Polygon]s to draw final List polygons; + /// Whether to use an alternative rendering pathway to draw polygons onto the + /// underlying `Canvas`, which can be more performant in *some* circumstances + /// + /// This will not always improve performance, and there are other important + /// considerations before enabling it. It is intended for use when prior + /// profiling indicates more performance is required after other methods are + /// already in use. For example, it may worsen performance when there are a + /// huge number of polygons to triangulate - and so this is best used in + /// conjunction with simplification, not as a replacement. + /// + /// For more information about usage and pitfalls, see the + /// [online documentation](https://docs.fleaflet.dev/layers/polygon-layer#performant-rendering-with-drawvertices-internal-disabled). + /// + /// Defaults to `false`. Ensure you have read and understood the documentation + /// above before enabling. + final bool useAltRendering; + /// Whether to cull polygons and polygon sections that are outside of the /// viewport /// - /// Defaults to `true`. + /// Defaults to `true`. Disabling is not recommended. final bool polygonCulling; /// Distance between two neighboring polygon points, in logical pixels scaled - /// to floored zoom. + /// to floored zoom /// /// Increasing this value results in points further apart being collapsed and /// thus more simplified polygons. Higher values improve performance at the @@ -51,6 +69,7 @@ class PolygonLayer extends StatefulWidget { const PolygonLayer({ super.key, required this.polygons, + this.useAltRendering = false, this.polygonCulling = true, this.simplificationTolerance = 0.5, this.polygonLabels = true, @@ -126,10 +145,40 @@ class _PolygonLayerState extends State { ) .toList(); + final triangles = !widget.useAltRendering + ? null + : List.generate( + culled.length, + (i) { + final culledPolygon = culled[i]; + + final points = culledPolygon.holePoints.isEmpty + ? culledPolygon.points + : culledPolygon.points + .followedBy(culledPolygon.holePoints.expand((e) => e)); + + return Earcut.triangulateRaw( + List.generate( + points.length * 2, + (ii) => ii % 2 == 0 + ? points.elementAt(ii ~/ 2).x + : points.elementAt(ii ~/ 2).y, + growable: false, + ), + // Not sure how just this works but it seems to :D + holeIndices: culledPolygon.holePoints.isEmpty + ? null + : [culledPolygon.points.length], + ); + }, + growable: false, + ); + return MobileLayerTransformer( child: CustomPaint( painter: _PolygonPainter( polygons: culled, + triangles: triangles, camera: camera, polygonLabels: widget.polygonLabels, drawLabelsLast: widget.drawLabelsLast, @@ -165,17 +214,15 @@ class _PolygonLayerState extends State { tolerance: tolerance, highQuality: true, ), - holePoints: holes == null - ? null - : List>.generate( - holes.length, - (j) => simplifyPoints( - points: holes[j], - tolerance: tolerance, - highQuality: true, - ), - growable: false, - ), + holePoints: List.generate( + holes.length, + (j) => simplifyPoints( + points: holes[j], + tolerance: tolerance, + highQuality: true, + ), + growable: false, + ), ); }, growable: false, diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index ca1e74a94..ec8f22508 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -4,12 +4,12 @@ part of 'polygon_layer.dart'; class _ProjectedPolygon { final Polygon polygon; final List points; - final List>? holePoints; + final List> holePoints; const _ProjectedPolygon._({ required this.polygon, required this.points, - this.holePoints, + required this.holePoints, }); _ProjectedPolygon._fromPolygon(Projection projection, Polygon polygon) @@ -25,7 +25,11 @@ class _ProjectedPolygon { ), holePoints: () { final holes = polygon.holePointsList; - if (holes == null) return null; + if (holes == null || + holes.isEmpty || + holes.every((e) => e.isEmpty)) { + return >[]; + } return List>.generate( holes.length, diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index dddf9c2cf..9b9a4c4b1 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -42,7 +42,11 @@ class _PolylinePainter extends CustomPainter { // continue; // } - final offsets = getOffsetsXY(camera, origin, projectedPolyline.points); + final offsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolyline.points, + ); final strokeWidth = polyline.useStrokeWidthInMeter ? _metersToStrokeWidth( origin, @@ -134,7 +138,11 @@ class _PolylinePainter extends CustomPainter { for (final projectedPolyline in polylines) { final polyline = projectedPolyline.polyline; - final offsets = getOffsetsXY(camera, origin, projectedPolyline.points); + final offsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolyline.points, + ); if (offsets.isEmpty) { continue; } @@ -280,6 +288,9 @@ class _PolylinePainter extends CustomPainter { LatLng _unproject(DoublePoint p0) => camera.crs.projection.unprojectXY(p0.x, p0.y); + // TODO: Fix bug where wrapping layer in some widgets (eg. opacity) causes the + // features to not move unless this is `true`, but `true` significantly impacts + // performance @override bool shouldRepaint(_PolylinePainter oldDelegate) => false; } diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 050994651..5966c3e1c 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -22,8 +22,7 @@ List getOffsets(MapCamera camera, Offset origin, List points) { final len = points.length; // Optimization: monomorphize the Epsg3857-case to avoid the virtual function overhead. - if (crs is Epsg3857) { - final Epsg3857 epsg3857 = crs; + if (crs case final Epsg3857 epsg3857) { final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { final (x, y) = epsg3857.latLngToXY(points[i], zoomScale); @@ -40,27 +39,32 @@ List getOffsets(MapCamera camera, Offset origin, List points) { return v; } -List getOffsetsXY( - MapCamera camera, - Offset origin, - List points, -) { +/// Suitable for both lines, filled polygons, and holed polygons +List getOffsetsXY({ + required MapCamera camera, + required Offset origin, + required List points, + List>? holePoints, +}) { // Critically create as little garbage as possible. This is called on every frame. final crs = camera.crs; final zoomScale = crs.scale(camera.zoom); + final realPoints = holePoints == null || holePoints.isEmpty + ? points + : points.followedBy(holePoints.expand((e) => e)); + final ox = -origin.dx; final oy = -origin.dy; - final len = points.length; + final len = realPoints.length; // Optimization: monomorphize the CrsWithStaticTransformation-case to avoid // the virtual function overhead. - if (crs is CrsWithStaticTransformation) { - final CrsWithStaticTransformation mcrs = crs; + if (crs case final CrsWithStaticTransformation crs) { final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final p = points[i]; - final (x, y) = mcrs.transform(p.x, p.y, zoomScale); + final p = realPoints.elementAt(i); + final (x, y) = crs.transform(p.x, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } return v; @@ -68,7 +72,7 @@ List getOffsetsXY( final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final p = points[i]; + final p = realPoints.elementAt(i); final (x, y) = crs.transform(p.x, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 35c63cf3e..536d4ab6f 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -27,6 +27,9 @@ final class DoublePoint { final double dy = y - rhs.y; return dx * dx + dy * dy; } + + @override + String toString() => 'DoublePoint($x, $y)'; } /// square distance from a point to a segment diff --git a/pubspec.yaml b/pubspec.yaml index 987e6898e..1522269ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ environment: dependencies: async: ^2.9.0 collection: ^1.17.1 + dart_earcut: ^1.1.0 flutter: sdk: flutter http: ^1.0.0