From 8766aa1e1b32266fb21f708fa150b51c13980a5b Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 15 Jan 2024 20:58:57 +0000 Subject: [PATCH 01/22] Added 'dart_earcut' dependency --- pubspec.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/pubspec.yaml b/pubspec.yaml index 987e6898e..d2d830f4b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ environment: dependencies: async: ^2.9.0 collection: ^1.17.1 + dart_earcut: ^1.0.1 flutter: sdk: flutter http: ^1.0.0 From 747593085fd609fd2e27b8261521e243727fddb8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 23 Jan 2024 22:12:39 +0000 Subject: [PATCH 02/22] Initial implementation of triangulation and `drawVertices`/`drawRawPoints` --- lib/src/layer/polygon_layer/painter.dart | 109 ++++++++++++++++-- .../layer/polygon_layer/polygon_layer.dart | 24 +++- .../polygon_layer/projected_polygon.dart | 10 +- 3 files changed, 125 insertions(+), 18 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 5f12de4fa..d5f3845ad 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,11 +1,19 @@ part of 'polygon_layer.dart'; +const bool _renderVertexes = true; // TODO: Remove, true is best performance +const bool _renderPoints = false; // TODO: Remove, false is best performance + /// The [_PolygonPainter] class is used to render [Polygon]s for /// the [PolygonLayer]. class _PolygonPainter extends CustomPainter { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; + /// Triangulated [polygons] + /// + /// Expected to be in same/corresponding order as [polygons] + final List> triangles; + /// Reference to the [MapCamera]. final MapCamera camera; @@ -18,6 +26,7 @@ class _PolygonPainter extends CustomPainter { /// Create a new [_PolygonPainter] instance. _PolygonPainter({ required this.polygons, + required this.triangles, required this.camera, required this.polygonLabels, required this.drawLabelsLast, @@ -33,6 +42,9 @@ class _PolygonPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { + final trianglePoints = []; + final outlinePoints = >[]; + var filledPath = ui.Path(); var borderPath = ui.Path(); Polygon? lastPolygon; @@ -51,18 +63,75 @@ class _PolygonPainter extends CustomPainter { ..style = PaintingStyle.fill ..color = color; - canvas.drawPath(filledPath, paint); + if (_renderVertexes) { + 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 = ui.Vertices.raw(ui.VertexMode.triangles, points); + canvas.drawVertices(vertices, ui.BlendMode.src, paint); + } else { + canvas.drawPath(filledPath, paint); + } } } // Draw polygon outline if (polygon.borderStrokeWidth > 0) { final borderPaint = _getBorderPaint(polygon); - canvas.drawPath(borderPath, borderPaint); + + if (_renderPoints) { + int len = 0; + for (final outline in outlinePoints) { + len += outline.length; + } + + final segments = Float32List(len * 4); + + int index = 0; + for (final outline in outlinePoints) { + for (int i = 0; i < outline.length; ++i) { + final p1 = outline[i]; + segments[index] = p1.dx; + segments[index + 1] = p1.dy; + + final p2 = outline[(i + 1) % outline.length]; + segments[index + 2] = p2.dx; + segments[index + 3] = p2.dy; + + index += 4; + } + } + canvas.drawRawPoints(ui.PointMode.lines, segments, borderPaint); + + // for (final outline in outlinePoints) { + // final segments = Float32List(outline.length * 2); + // + // for (int i = 0; i < outline.length * 2; i += 2) { + // final p1 = outline[i~/2]; + // segments[i] = p1.dx; + // segments[i + 1] = p1.dy; + // } + // canvas.drawRawPoints(ui.PointMode.polygon, segments, borderPaint); + // } + } else { + canvas.drawPath(borderPath, borderPaint); + } + } + + if (_renderVertexes) { + trianglePoints.clear(); + } else { + filledPath = ui.Path(); + } + + if (_renderPoints) { + outlinePoints.clear(); + } else { + borderPath = ui.Path(); } - filledPath = ui.Path(); - borderPath = ui.Path(); lastPolygon = null; lastHash = null; } @@ -70,10 +139,14 @@ 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) { + for (int i = 0; i <= polygons.length - 1; i++) { + final projectedPolygon = polygons[i]; + final polygonTriangles = triangles[i]; + if (projectedPolygon.points.isEmpty) { continue; } + final polygon = projectedPolygon.polygon; final offsets = getOffsetsXY(camera, origin, projectedPolygon.points); @@ -91,15 +164,28 @@ class _PolygonPainter extends CustomPainter { // ignore: deprecated_member_use_from_same_package if (polygon.isFilled ?? true) { if (polygon.color != null) { - filledPath.addPolygon(offsets, true); + if (_renderVertexes) { + final len = polygonTriangles.length; + for (int i = 0; i < len; ++i) { + trianglePoints.add(offsets[polygonTriangles[i]]); + } + } else { + filledPath.addPolygon(offsets, true); + } } } + if (polygon.borderStrokeWidth > 0.0) { - _addBorderToPath(borderPath, polygon, offsets); + if (_renderPoints) { + outlinePoints.add(offsets); + } else { + _addBorderToPath(borderPath, polygon, offsets); + } } // Afterwards deal with more complicated holes. - final holePointsList = polygon.holePointsList; + // TODO: Handle holes + /*final holePointsList = polygon.holePointsList; if (holePointsList != null && holePointsList.isNotEmpty) { // Ideally we'd use `Path.combine(PathOperation.difference, ...)` // instead of evenOdd fill-type, however it creates visual artifacts @@ -119,7 +205,7 @@ class _PolygonPainter extends CustomPainter { if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { _addHoleBordersToPath(borderPath, polygon, holeOffsetsList); } - } + }*/ if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { // Labels are expensive because: @@ -133,7 +219,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 +248,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, diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index dee8474af..5c86db189 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'dart:ui' as 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'; @@ -108,10 +109,27 @@ class _PolygonLayerState extends State { ) .toList(); + // TODO: Handle holes + // TODO: Make optional + // TODO: What to do with more complex polys (such as self intersecting) + // TODO: Check deviation if possible (https://github.com/mapbox/earcut/blob/afb5797dbf9272661ca4d49ee2e08bd0cd96e1ed/src/earcut.js#L629C4-L629C18) + final triangles = List.generate( + culled.length, + (i) => Earcut.triangulateRaw( + culled[i] + .points + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(growable: false), + ), + growable: false, + ); + return MobileLayerTransformer( child: CustomPaint( painter: _PolygonPainter( polygons: culled, + triangles: triangles, camera: camera, polygonLabels: widget.polygonLabels, drawLabelsLast: widget.drawLabelsLast, @@ -145,9 +163,9 @@ class _PolygonLayerState extends State { tolerance: tolerance, highQuality: true, ), - holePoints: holes == null - ? null - : List>.generate( + holePoints: holes.isEmpty + ? [] + : List.generate( holes.length, (j) => simplifyPoints( points: holes[j], diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index f597244c7..25ffc227e 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, From e51877c300ec5f2b9d456ee1288ebc2447800ffd Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 24 Jan 2024 21:39:23 +0000 Subject: [PATCH 03/22] Improvements to Polygon Stress Testing example page --- example/lib/pages/polygon_perf_stress.dart | 114 ++++++++++++++++-- .../lib/widgets/number_of_items_slider.dart | 2 +- .../simplification_tolerance_slider.dart | 2 +- lib/src/layer/polygon_layer/painter.dart | 22 ++-- .../layer/polygon_layer/polygon_layer.dart | 2 +- 5 files changed, 115 insertions(+), 27 deletions(-) diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index f812ce6d1..f6c3fdac1 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -21,14 +21,10 @@ class PolygonPerfStressPage extends StatefulWidget { class _PolygonPerfStressPageState extends State { static const double _initialSimplificationTolerance = 0.5; double simplificationTolerance = _initialSimplificationTolerance; + static const bool _initialUseThickOutlines = false; + bool useThickOutlines = _initialUseThickOutlines; - 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 +34,7 @@ class _PolygonPerfStressPageState extends State { @override void dispose() { - geoJsonLoader.ignore(); + geoJsonParser.ignore(); super.dispose(); } @@ -67,7 +63,7 @@ class _PolygonPerfStressPageState extends State { children: [ openStreetMapTileLayer, FutureBuilder( - future: geoJsonLoader, + future: geoJsonParser, builder: (context, geoJsonParser) => geoJsonParser.connectionState != ConnectionState.done || geoJsonParser.data == null @@ -83,10 +79,48 @@ class _PolygonPerfStressPageState extends State { left: 16, top: 16, right: 16, - child: SimplificationToleranceSlider( - initialTolerance: _initialSimplificationTolerance, - onChangedTolerance: (v) => - setState(() => simplificationTolerance = v), + child: Column( + children: [ + SimplificationToleranceSlider( + initialTolerance: _initialSimplificationTolerance, + onChangedTolerance: (v) => + setState(() => simplificationTolerance = v), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerLeft, + child: _ThickOutlinesSwitch( + useThickOutlines: useThickOutlines, + onChangedUseThickOutlines: (v) async { + setState(() => useThickOutlines = v); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + SizedBox.square( + dimension: 16, + child: Center( + 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(() {}); + }, + ), + ), + ], ), ), if (!kIsWeb) @@ -100,4 +134,58 @@ class _PolygonPerfStressPageState extends State { ), ); } + + Future loadPolygonsFromGeoJson() async { + const filePath = 'assets/138k-polygon-points.geojson.noformat'; + + return rootBundle.loadString(filePath).then( + (geoJson) => compute( + (msg) => GeoJsonParser( + defaultPolygonBorderStroke: msg.useThickOutlines ? 15 : 1, + defaultPolygonBorderColor: Colors.black.withOpacity(0.5), + defaultPolygonFillColor: Colors.amber.withOpacity(0.5), + )..parseGeoJsonAsString(msg.geoJson), + (geoJson: geoJson, useThickOutlines: useThickOutlines), + ), + ); + } +} + +class _ThickOutlinesSwitch extends StatelessWidget { + const _ThickOutlinesSwitch({ + required this.useThickOutlines, + required this.onChangedUseThickOutlines, + }); + + final bool useThickOutlines; + final void Function(bool) onChangedUseThickOutlines; + + @override + Widget build(BuildContext context) { + return UnconstrainedBox( + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(32), + ), + child: Padding( + padding: + const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 4), + child: Row( + children: [ + const Tooltip( + message: 'Thick Outlines', + child: Icon(Icons.line_weight_rounded), + ), + const SizedBox(width: 8), + Switch.adaptive( + value: useThickOutlines, + onChanged: onChangedUseThickOutlines, + ), + ], + ), + ), + ), + ); + } } diff --git a/example/lib/widgets/number_of_items_slider.dart b/example/lib/widgets/number_of_items_slider.dart index adb2572a2..781032ab3 100644 --- a/example/lib/widgets/number_of_items_slider.dart +++ b/example/lib/widgets/number_of_items_slider.dart @@ -43,7 +43,7 @@ class _NumberOfItemsSliderState extends State { child: const Icon(Icons.numbers), ), Expanded( - child: Slider( + child: Slider.adaptive( value: _number.toDouble(), onChanged: (v) { if (_number == 0 && v != 0) { diff --git a/example/lib/widgets/simplification_tolerance_slider.dart b/example/lib/widgets/simplification_tolerance_slider.dart index e1ecafbe9..c606b7562 100644 --- a/example/lib/widgets/simplification_tolerance_slider.dart +++ b/example/lib/widgets/simplification_tolerance_slider.dart @@ -41,7 +41,7 @@ class _SimplificationToleranceSliderState ), ), Expanded( - child: Slider( + child: Slider.adaptive( value: _simplificationTolerance, onChanged: (v) { if (_simplificationTolerance == 0 && v != 0) { diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index d5f3845ad..d55b5d7e1 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -45,8 +45,8 @@ class _PolygonPainter extends CustomPainter { final trianglePoints = []; final outlinePoints = >[]; - var filledPath = ui.Path(); - var borderPath = ui.Path(); + var filledPath = Path(); + var borderPath = Path(); Polygon? lastPolygon; int? lastHash; @@ -69,8 +69,8 @@ class _PolygonPainter extends CustomPainter { points[i * 2] = trianglePoints[i].dx; points[i * 2 + 1] = trianglePoints[i].dy; } - final vertices = ui.Vertices.raw(ui.VertexMode.triangles, points); - canvas.drawVertices(vertices, ui.BlendMode.src, paint); + final vertices = Vertices.raw(VertexMode.triangles, points); + canvas.drawVertices(vertices, BlendMode.src, paint); } else { canvas.drawPath(filledPath, paint); } @@ -103,7 +103,7 @@ class _PolygonPainter extends CustomPainter { index += 4; } } - canvas.drawRawPoints(ui.PointMode.lines, segments, borderPaint); + canvas.drawRawPoints(PointMode.lines, segments, borderPaint); // for (final outline in outlinePoints) { // final segments = Float32List(outline.length * 2); @@ -123,13 +123,13 @@ class _PolygonPainter extends CustomPainter { if (_renderVertexes) { trianglePoints.clear(); } else { - filledPath = ui.Path(); + filledPath = Path(); } if (_renderPoints) { outlinePoints.clear(); } else { - borderPath = ui.Path(); + borderPath = Path(); } lastPolygon = null; @@ -273,7 +273,7 @@ class _PolygonPainter extends CustomPainter { } void _addBorderToPath( - ui.Path path, + Path path, Polygon polygon, List offsets, ) { @@ -287,7 +287,7 @@ class _PolygonPainter extends CustomPainter { } void _addHoleBordersToPath( - ui.Path path, + Path path, Polygon polygon, List> holeOffsetsList, ) { @@ -305,7 +305,7 @@ class _PolygonPainter extends CustomPainter { } void _addDottedLineToPath( - ui.Path path, + Path path, List offsets, double radius, double stepLength, @@ -341,7 +341,7 @@ 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); } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 2019796af..56d4b4ab7 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -1,5 +1,5 @@ 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'; From 62650f9a7d8aaa8155b0536399baa7e92747ea6e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 24 Jan 2024 21:42:28 +0000 Subject: [PATCH 04/22] Removed internal `_renderPoints` flag in favour of `false` (as yields better performance in most/all cases) --- lib/src/layer/polygon_layer/painter.dart | 54 ++---------------------- 1 file changed, 3 insertions(+), 51 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index d55b5d7e1..b31bd40da 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,7 +1,6 @@ part of 'polygon_layer.dart'; const bool _renderVertexes = true; // TODO: Remove, true is best performance -const bool _renderPoints = false; // TODO: Remove, false is best performance /// The [_PolygonPainter] class is used to render [Polygon]s for /// the [PolygonLayer]. @@ -43,7 +42,6 @@ class _PolygonPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { final trianglePoints = []; - final outlinePoints = >[]; var filledPath = Path(); var borderPath = Path(); @@ -79,45 +77,7 @@ class _PolygonPainter extends CustomPainter { // Draw polygon outline if (polygon.borderStrokeWidth > 0) { - final borderPaint = _getBorderPaint(polygon); - - if (_renderPoints) { - int len = 0; - for (final outline in outlinePoints) { - len += outline.length; - } - - final segments = Float32List(len * 4); - - int index = 0; - for (final outline in outlinePoints) { - for (int i = 0; i < outline.length; ++i) { - final p1 = outline[i]; - segments[index] = p1.dx; - segments[index + 1] = p1.dy; - - final p2 = outline[(i + 1) % outline.length]; - segments[index + 2] = p2.dx; - segments[index + 3] = p2.dy; - - index += 4; - } - } - canvas.drawRawPoints(PointMode.lines, segments, borderPaint); - - // for (final outline in outlinePoints) { - // final segments = Float32List(outline.length * 2); - // - // for (int i = 0; i < outline.length * 2; i += 2) { - // final p1 = outline[i~/2]; - // segments[i] = p1.dx; - // segments[i + 1] = p1.dy; - // } - // canvas.drawRawPoints(ui.PointMode.polygon, segments, borderPaint); - // } - } else { - canvas.drawPath(borderPath, borderPaint); - } + canvas.drawPath(borderPath, _getBorderPaint(polygon)); } if (_renderVertexes) { @@ -126,11 +86,7 @@ class _PolygonPainter extends CustomPainter { filledPath = Path(); } - if (_renderPoints) { - outlinePoints.clear(); - } else { - borderPath = Path(); - } + borderPath = Path(); lastPolygon = null; lastHash = null; @@ -176,11 +132,7 @@ class _PolygonPainter extends CustomPainter { } if (polygon.borderStrokeWidth > 0.0) { - if (_renderPoints) { - outlinePoints.add(offsets); - } else { - _addBorderToPath(borderPath, polygon, offsets); - } + _addBorderToPath(borderPath, polygon, offsets); } // Afterwards deal with more complicated holes. From 5b650170fa457b9e191eb4591cbdb425e5e50167 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 24 Jan 2024 22:08:30 +0000 Subject: [PATCH 05/22] Added `PolygonLayer.performantRendering` argument to replace internal `_renderVertexes` flag Improved Polygon Stress Testing example page --- example/lib/pages/polygon_perf_stress.dart | 100 ++++++++++-------- lib/src/layer/polygon_layer/painter.dart | 26 ++--- .../layer/polygon_layer/polygon_layer.dart | 51 ++++++--- 3 files changed, 106 insertions(+), 71 deletions(-) diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index f6c3fdac1..09faefc5e 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -21,6 +21,8 @@ class PolygonPerfStressPage extends StatefulWidget { class _PolygonPerfStressPageState extends State { static const double _initialSimplificationTolerance = 0.5; double simplificationTolerance = _initialSimplificationTolerance; + static const bool _initialUsePerformantDrawing = true; + bool usePerformantDrawing = _initialUsePerformantDrawing; static const bool _initialUseThickOutlines = false; bool useThickOutlines = _initialUseThickOutlines; @@ -69,8 +71,9 @@ class _PolygonPerfStressPageState extends State { geoJsonParser.data == null ? const SizedBox.shrink() : PolygonLayer( - simplificationTolerance: simplificationTolerance, polygons: geoJsonParser.data!.polygons, + performantRendering: usePerformantDrawing, + simplificationTolerance: simplificationTolerance, ), ), ], @@ -87,38 +90,49 @@ class _PolygonPerfStressPageState extends State { setState(() => simplificationTolerance = v), ), const SizedBox(height: 12), - Align( - alignment: Alignment.centerLeft, - child: _ThickOutlinesSwitch( - useThickOutlines: useThickOutlines, - onChangedUseThickOutlines: (v) async { - setState(() => useThickOutlines = v); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Row( - children: [ - SizedBox.square( - dimension: 16, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: - AlwaysStoppedAnimation(Colors.white), + Row( + children: [ + _LabelledSwitch( + tooltipMessage: 'Performant Drawing', + icon: const Icon(Icons.speed_rounded), + value: usePerformantDrawing, + onChanged: (v) => + setState(() => usePerformantDrawing = v), + ), + const SizedBox(width: 12), + _LabelledSwitch( + tooltipMessage: 'Thick Outlines', + icon: const Icon(Icons.line_weight_rounded), + value: useThickOutlines, + onChanged: (v) async { + setState(() => useThickOutlines = v); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + SizedBox.square( + dimension: 16, + child: Center( + child: CircularProgressIndicator( + strokeWidth: 3, + valueColor: + AlwaysStoppedAnimation(Colors.white), + ), ), ), - ), - SizedBox(width: 12), - Text('Loading GeoJson polygons...'), - ], + SizedBox(width: 12), + Text('Loading GeoJson polygons...'), + ], + ), ), - ), - ); - await (geoJsonParser = loadPolygonsFromGeoJson()); - if (!context.mounted) return; - ScaffoldMessenger.of(context).clearSnackBars(); - setState(() {}); - }, - ), + ); + await (geoJsonParser = loadPolygonsFromGeoJson()); + if (!context.mounted) return; + ScaffoldMessenger.of(context).clearSnackBars(); + setState(() {}); + }, + ), + ], ), ], ), @@ -151,14 +165,18 @@ class _PolygonPerfStressPageState extends State { } } -class _ThickOutlinesSwitch extends StatelessWidget { - const _ThickOutlinesSwitch({ - required this.useThickOutlines, - required this.onChangedUseThickOutlines, +class _LabelledSwitch extends StatelessWidget { + const _LabelledSwitch({ + required this.tooltipMessage, + required this.icon, + required this.value, + required this.onChanged, }); - final bool useThickOutlines; - final void Function(bool) onChangedUseThickOutlines; + final String tooltipMessage; + final Icon icon; + final bool value; + final void Function(bool) onChanged; @override Widget build(BuildContext context) { @@ -173,15 +191,9 @@ class _ThickOutlinesSwitch extends StatelessWidget { const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 4), child: Row( children: [ - const Tooltip( - message: 'Thick Outlines', - child: Icon(Icons.line_weight_rounded), - ), + Tooltip(message: tooltipMessage, child: icon), const SizedBox(width: 8), - Switch.adaptive( - value: useThickOutlines, - onChanged: onChangedUseThickOutlines, - ), + Switch.adaptive(value: value, onChanged: onChanged), ], ), ), diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index b31bd40da..4c5d4f32d 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,7 +1,5 @@ part of 'polygon_layer.dart'; -const bool _renderVertexes = true; // TODO: Remove, true is best performance - /// The [_PolygonPainter] class is used to render [Polygon]s for /// the [PolygonLayer]. class _PolygonPainter extends CustomPainter { @@ -11,7 +9,8 @@ class _PolygonPainter extends CustomPainter { /// Triangulated [polygons] /// /// Expected to be in same/corresponding order as [polygons] - final List> triangles; + final List>? triangles; + final bool useDrawVertices; /// Reference to the [MapCamera]. final MapCamera camera; @@ -19,7 +18,10 @@ class _PolygonPainter extends CustomPainter { /// 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. @@ -29,7 +31,8 @@ class _PolygonPainter extends CustomPainter { required this.camera, required this.polygonLabels, required this.drawLabelsLast, - }) : bounds = camera.visibleBounds; + }) : bounds = camera.visibleBounds, + useDrawVertices = triangles != null; ({Offset min, Offset max}) getBounds(Offset origin, Polygon polygon) { final bbox = polygon.boundingBox; @@ -61,7 +64,7 @@ class _PolygonPainter extends CustomPainter { ..style = PaintingStyle.fill ..color = color; - if (_renderVertexes) { + if (useDrawVertices) { final points = Float32List(trianglePoints.length * 2); for (int i = 0; i < trianglePoints.length; ++i) { points[i * 2] = trianglePoints[i].dx; @@ -80,7 +83,7 @@ class _PolygonPainter extends CustomPainter { canvas.drawPath(borderPath, _getBorderPaint(polygon)); } - if (_renderVertexes) { + if (useDrawVertices) { trianglePoints.clear(); } else { filledPath = Path(); @@ -97,13 +100,12 @@ class _PolygonPainter extends CustomPainter { // Main loop constructing batched fill and border paths from given polygons. for (int i = 0; i <= polygons.length - 1; i++) { final projectedPolygon = polygons[i]; - final polygonTriangles = triangles[i]; + if (projectedPolygon.points.isEmpty) continue; + final polygon = projectedPolygon.polygon; - if (projectedPolygon.points.isEmpty) { - continue; - } + final polygonTriangles = triangles?[i]; + final useEfficientMethods = polygonTriangles != null; - final polygon = projectedPolygon.polygon; final offsets = getOffsetsXY(camera, origin, projectedPolygon.points); // The hash is based on the polygons visual properties. If the hash from @@ -120,7 +122,7 @@ class _PolygonPainter extends CustomPainter { // ignore: deprecated_member_use_from_same_package if (polygon.isFilled ?? true) { if (polygon.color != null) { - if (_renderVertexes) { + if (useEfficientMethods) { final len = polygonTriangles.length; for (int i = 0; i < len; ++i) { trianglePoints.add(offsets[polygonTriangles[i]]); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 56d4b4ab7..f648f0eb0 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -22,14 +22,34 @@ class PolygonLayer extends StatefulWidget { /// [Polygon]s to draw final List polygons; + /// Whether to use more performant methods to draw polygons + /// + /// When enabled, this internally: + /// * triangulates each polygon using the + /// ['dart_earcut' package](https://github.com/JaffaKetchup/dart_earcut) + /// * uses [`drawVertices`](https://www.youtube.com/watch?v=pD38Yyz7N2E) to + /// draw the triangles to the underlying canvas + /// + /// In some cases, such as when input polygons are self intersecting, + /// the triangulation step can yield poor results, which will appear as + /// malformed polygons on the canvas. Disable this argument to use standard + /// canvas drawing methods which don't suffer this issue. + /// + /// Defaults to `true`. + // TODO: Toggle triangulation per polygon + // TODO: Detect self intersections? + // TODO: Detect holes (if support not added) + // TODO: Add argument per polygon + final bool performantRendering; + /// 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 @@ -52,6 +72,7 @@ class PolygonLayer extends StatefulWidget { const PolygonLayer({ super.key, required this.polygons, + this.performantRendering = true, this.polygonCulling = true, this.simplificationTolerance = 0.5, this.polygonLabels = true, @@ -110,20 +131,20 @@ class _PolygonLayerState extends State { .toList(); // TODO: Handle holes - // TODO: Make optional - // TODO: What to do with more complex polys (such as self intersecting) // TODO: Check deviation if possible (https://github.com/mapbox/earcut/blob/afb5797dbf9272661ca4d49ee2e08bd0cd96e1ed/src/earcut.js#L629C4-L629C18) - final triangles = List.generate( - culled.length, - (i) => Earcut.triangulateRaw( - culled[i] - .points - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false), - ), - growable: false, - ); + final triangles = !widget.performantRendering + ? null + : List.generate( + culled.length, + (i) => Earcut.triangulateRaw( + culled[i] + .points + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(growable: false), + ), + growable: false, + ); return MobileLayerTransformer( child: CustomPaint( From 9a0e7bdbf0ef03737996022e40b707e8294b347f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 28 Jan 2024 12:36:31 +0000 Subject: [PATCH 06/22] Add support for holes --- example/lib/pages/polygon.dart | 26 +++++++++++++---- lib/src/layer/polygon_layer/painter.dart | 29 ++++++++++++++----- .../layer/polygon_layer/polygon_layer.dart | 26 ++++++++++------- lib/src/layer/polyline_layer/painter.dart | 15 ++++++++-- lib/src/misc/offsets.dart | 22 +++++++++----- lib/src/misc/simplify.dart | 3 ++ 6 files changed, 89 insertions(+), 32 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 4920f2bd9..3f77c8d20 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -46,14 +46,16 @@ class PolygonPage extends StatelessWidget { final _holeOuterPoints = const [ LatLng(50, -18), LatLng(50, -14), + 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, -16), + LatLng(51.5, -15.5), + LatLng(51, -16), + LatLng(51, -17), ]; @override @@ -113,9 +115,23 @@ class PolygonPage extends StatelessWidget { ), Polygon( points: _holeOuterPoints, - holePointsList: [_holeInnerPoints], + holePointsList: [ + _holeInnerPoints, + const [ + LatLng(53.5, -17), + LatLng(53.5, -16), + LatLng(53, -15), + LatLng(52.25, -15), + LatLng(52.25, -16), + LatLng(52.75, -17), + ] + ], borderStrokeWidth: 4, - borderColor: Colors.green, + borderColor: Colors.black, + color: Colors.green, + label: 'Rotated!', + rotateLabel: true, + labelPlacement: PolygonLabelPlacement.polylabel, ), Polygon( points: _holeOuterPoints diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 4c5d4f32d..c96f9f8d6 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -106,7 +106,12 @@ class _PolygonPainter extends CustomPainter { final polygonTriangles = triangles?[i]; final useEfficientMethods = polygonTriangles != null; - final offsets = getOffsetsXY(camera, origin, projectedPolygon.points); + final fillOffsets = getOffsetsXY( + camera: camera, + origin: origin, + points: projectedPolygon.points, + holePoints: useEfficientMethods ? 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 @@ -125,21 +130,28 @@ class _PolygonPainter extends CustomPainter { if (useEfficientMethods) { final len = polygonTriangles.length; for (int i = 0; i < len; ++i) { - trianglePoints.add(offsets[polygonTriangles[i]]); + trianglePoints.add(fillOffsets[polygonTriangles[i]]); } } else { - filledPath.addPolygon(offsets, true); + 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. - // TODO: Handle holes - /*final holePointsList = polygon.holePointsList; + final holePointsList = polygon.holePointsList; if (holePointsList != null && holePointsList.isNotEmpty) { // Ideally we'd use `Path.combine(PathOperation.difference, ...)` // instead of evenOdd fill-type, however it creates visual artifacts @@ -159,7 +171,7 @@ class _PolygonPainter extends CustomPainter { if (!polygon.disableHolesBorder && polygon.borderStrokeWidth > 0.0) { _addHoleBordersToPath(borderPath, polygon, holeOffsetsList); } - }*/ + } if (!drawLabelsLast && polygonLabels && polygon.textPainter != null) { // Labels are expensive because: @@ -299,6 +311,9 @@ class _PolygonPainter extends CustomPainter { 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_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index f648f0eb0..cb40332f4 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -38,8 +38,6 @@ class PolygonLayer extends StatefulWidget { /// Defaults to `true`. // TODO: Toggle triangulation per polygon // TODO: Detect self intersections? - // TODO: Detect holes (if support not added) - // TODO: Add argument per polygon final bool performantRendering; /// Whether to cull polygons and polygon sections that are outside of the @@ -130,19 +128,27 @@ class _PolygonLayerState extends State { ) .toList(); - // TODO: Handle holes // TODO: Check deviation if possible (https://github.com/mapbox/earcut/blob/afb5797dbf9272661ca4d49ee2e08bd0cd96e1ed/src/earcut.js#L629C4-L629C18) final triangles = !widget.performantRendering ? null : List.generate( culled.length, - (i) => Earcut.triangulateRaw( - culled[i] - .points - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false), - ), + (i) { + final culledPolygon = culled[i]; + return Earcut.triangulateRaw( + (culledPolygon.holePoints.isNotEmpty + ? culledPolygon.points.followedBy( + culledPolygon.holePoints.expand((e) => e)) + : culledPolygon.points) + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(growable: false), + // Not sure how just this works but it seems to :D + holeIndices: culledPolygon.holePoints.isNotEmpty + ? [culledPolygon.points.length] + : null, + ); + }, growable: false, ); 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..82091cbf9 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -40,18 +40,24 @@ 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, ...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. @@ -59,7 +65,7 @@ List getOffsetsXY( final CrsWithStaticTransformation mcrs = crs; final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final p = points[i]; + final p = realPoints[i]; final (x, y) = mcrs.transform(p.x, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } @@ -68,7 +74,7 @@ List getOffsetsXY( final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final p = points[i]; + final p = realPoints[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 ea0828ec6..e4e05886f 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 From 946db37ef9a16baf949d6ffcd9c199fc3329b773 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 28 Jan 2024 13:06:58 +0000 Subject: [PATCH 07/22] Check deviation --- .../layer/polygon_layer/polygon_layer.dart | 80 ++++++++++++++++--- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index cb40332f4..86325d275 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -128,26 +128,33 @@ class _PolygonLayerState extends State { ) .toList(); - // TODO: Check deviation if possible (https://github.com/mapbox/earcut/blob/afb5797dbf9272661ca4d49ee2e08bd0cd96e1ed/src/earcut.js#L629C4-L629C18) final triangles = !widget.performantRendering ? null : List.generate( culled.length, (i) { final culledPolygon = culled[i]; - return Earcut.triangulateRaw( - (culledPolygon.holePoints.isNotEmpty - ? culledPolygon.points.followedBy( - culledPolygon.holePoints.expand((e) => e)) - : culledPolygon.points) - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false), - // Not sure how just this works but it seems to :D - holeIndices: culledPolygon.holePoints.isNotEmpty - ? [culledPolygon.points.length] - : null, + + final vertices = (culledPolygon.holePoints.isNotEmpty + ? culledPolygon.points + .followedBy(culledPolygon.holePoints.expand((e) => e)) + : culledPolygon.points) + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(growable: false); + // Not sure how just this works but it seems to :D + final holeIndices = culledPolygon.holePoints.isNotEmpty + ? [culledPolygon.points.length] + : null; + + final triangulated = Earcut.triangulateRaw( + vertices, + holeIndices: holeIndices, ); + + print(deviation(vertices, holeIndices, 2, triangulated)); + + return triangulated; }, growable: false, ); @@ -206,4 +213,51 @@ class _PolygonLayerState extends State { growable: false, ); } + + double deviation( + List data, + List? holeIndices, + int dim, + List triangles, + ) { + final hasHoles = holeIndices?.isNotEmpty ?? false; + final outerLen = hasHoles ? holeIndices![0] * dim : data.length; + + var polygonArea = signedArea(data, 0, outerLen, dim).abs(); + if (hasHoles) { + for (var i = 0, len = holeIndices!.length; i < len; i++) { + final start = holeIndices[i] * dim; + final end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; + polygonArea -= signedArea(data, start, end, dim).abs(); + } + } + + double trianglesArea = 0; + for (int i = 0; i < triangles.length; i += 3) { + final a = triangles[i] * dim; + final b = triangles[i + 1] * dim; + final c = triangles[i + 2] * dim; + trianglesArea += ((data[a] - data[c]) * (data[b + 1] - data[a + 1]) - + (data[a] - data[b]) * (data[c + 1] - data[a + 1])) + .abs(); + } + + return polygonArea == 0 && trianglesArea == 0 + ? 0 + : ((trianglesArea - polygonArea) / polygonArea).abs(); + } + + double signedArea( + List data, + int start, + int end, + int dim, + ) { + double sum = 0; + for (var i = start, j = end - dim; i < end; i += dim) { + sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); + j = i; + } + return sum; + } } From 45dac8e235abb22caf731e20ed70302a892849ee Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 28 Jan 2024 13:07:12 +0000 Subject: [PATCH 08/22] Revert "Check deviation" This reverts commit 946db37ef9a16baf949d6ffcd9c199fc3329b773. --- .../layer/polygon_layer/polygon_layer.dart | 80 +++---------------- 1 file changed, 13 insertions(+), 67 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 86325d275..cb40332f4 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -128,33 +128,26 @@ class _PolygonLayerState extends State { ) .toList(); + // TODO: Check deviation if possible (https://github.com/mapbox/earcut/blob/afb5797dbf9272661ca4d49ee2e08bd0cd96e1ed/src/earcut.js#L629C4-L629C18) final triangles = !widget.performantRendering ? null : List.generate( culled.length, (i) { final culledPolygon = culled[i]; - - final vertices = (culledPolygon.holePoints.isNotEmpty - ? culledPolygon.points - .followedBy(culledPolygon.holePoints.expand((e) => e)) - : culledPolygon.points) - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false); - // Not sure how just this works but it seems to :D - final holeIndices = culledPolygon.holePoints.isNotEmpty - ? [culledPolygon.points.length] - : null; - - final triangulated = Earcut.triangulateRaw( - vertices, - holeIndices: holeIndices, + return Earcut.triangulateRaw( + (culledPolygon.holePoints.isNotEmpty + ? culledPolygon.points.followedBy( + culledPolygon.holePoints.expand((e) => e)) + : culledPolygon.points) + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(growable: false), + // Not sure how just this works but it seems to :D + holeIndices: culledPolygon.holePoints.isNotEmpty + ? [culledPolygon.points.length] + : null, ); - - print(deviation(vertices, holeIndices, 2, triangulated)); - - return triangulated; }, growable: false, ); @@ -213,51 +206,4 @@ class _PolygonLayerState extends State { growable: false, ); } - - double deviation( - List data, - List? holeIndices, - int dim, - List triangles, - ) { - final hasHoles = holeIndices?.isNotEmpty ?? false; - final outerLen = hasHoles ? holeIndices![0] * dim : data.length; - - var polygonArea = signedArea(data, 0, outerLen, dim).abs(); - if (hasHoles) { - for (var i = 0, len = holeIndices!.length; i < len; i++) { - final start = holeIndices[i] * dim; - final end = i < len - 1 ? holeIndices[i + 1] * dim : data.length; - polygonArea -= signedArea(data, start, end, dim).abs(); - } - } - - double trianglesArea = 0; - for (int i = 0; i < triangles.length; i += 3) { - final a = triangles[i] * dim; - final b = triangles[i + 1] * dim; - final c = triangles[i + 2] * dim; - trianglesArea += ((data[a] - data[c]) * (data[b + 1] - data[a + 1]) - - (data[a] - data[b]) * (data[c + 1] - data[a + 1])) - .abs(); - } - - return polygonArea == 0 && trianglesArea == 0 - ? 0 - : ((trianglesArea - polygonArea) / polygonArea).abs(); - } - - double signedArea( - List data, - int start, - int end, - int dim, - ) { - double sum = 0; - for (var i = start, j = end - dim; i < end; i += dim) { - sum += (data[j] - data[i]) * (data[i + 1] + data[j + 1]); - j = i; - } - return sum; - } } From 539f3de2d6b6d051cde750140f525aab9514ff79 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 28 Jan 2024 14:08:47 +0000 Subject: [PATCH 09/22] Added `Polygon` feature level `performantRendering` parameter Reduced quality/`precision` of `Polylabel` label placement to improve performance in edge-cases Improved example application `Polygon` demo page --- example/lib/pages/polygon.dart | 120 ++++++++++++------ lib/src/layer/polygon_layer/label.dart | 2 +- lib/src/layer/polygon_layer/painter.dart | 29 ++--- lib/src/layer/polygon_layer/polygon.dart | 26 +++- .../layer/polygon_layer/polygon_layer.dart | 14 +- 5 files changed, 127 insertions(+), 64 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index 3f77c8d20..c3effbdf3 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,19 +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(52, -17), - LatLng(52, -16), - LatLng(51.5, -15.5), - LatLng(51, -16), - LatLng(51, -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 @@ -86,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, @@ -114,39 +119,74 @@ class PolygonPage extends StatelessWidget { labelPlacement: PolygonLabelPlacement.polylabel, ), Polygon( - points: _holeOuterPoints, - holePointsList: [ - _holeInnerPoints, - const [ - LatLng(53.5, -17), - LatLng(53.5, -16), - LatLng(53, -15), - LatLng(52.25, -15), - LatLng(52.25, -16), - LatLng(52.75, -17), - ] - ], + points: _normalHoleOuterPoints, + holePointsList: _holeInnerPoints, borderStrokeWidth: 4, borderColor: Colors.black, color: Colors.green, - label: 'Rotated!', - rotateLabel: true, - labelPlacement: PolygonLabelPlacement.polylabel, ), Polygon( - points: _holeOuterPoints + points: _normalHoleOuterPoints .map((latlng) => LatLng(latlng.latitude, 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, latlng.longitude + 8)) + .toList(), + ) + .toList(), + borderStrokeWidth: 4, + borderColor: Colors.orange, + color: Colors.orange.withOpacity(0.5), + performantRendering: false, + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacement: PolygonLabelPlacement.centroid, + labelStyle: const TextStyle(color: Colors.black), + ), + 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, + ), + Polygon( + points: _brokenHoleOuterPoints + .map((latlng) => + LatLng(latlng.latitude - 6, latlng.longitude + 8)) + .toList(), + isDotted: true, + 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), + performantRendering: false, + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacement: PolygonLabelPlacement.centroid, + labelStyle: const TextStyle(color: Colors.black), ), ], ), diff --git a/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index 153fc9e09..458ddc63b 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -85,7 +85,7 @@ 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, + 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 c96f9f8d6..2d23d2483 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -6,11 +6,14 @@ class _PolygonPainter extends CustomPainter { /// Reference to the list of [_ProjectedPolygon]s final List<_ProjectedPolygon> polygons; - /// Triangulated [polygons] + /// Triangulated [polygons] if available /// - /// Expected to be in same/corresponding order as [polygons] - final List>? triangles; - final bool useDrawVertices; + /// Expected to be in same/corresponding order as [polygons]. + /// + /// Outer will be `null` when [PolygonLayer.performantRendering] is `false`. + /// Inner will be `null` when [Polygon.performantRendering] is `false`. + /// Lists *should* never be empty. + final List?>? triangles; /// Reference to the [MapCamera]. final MapCamera camera; @@ -31,8 +34,7 @@ class _PolygonPainter extends CustomPainter { required this.camera, required this.polygonLabels, required this.drawLabelsLast, - }) : bounds = camera.visibleBounds, - useDrawVertices = triangles != null; + }) : bounds = camera.visibleBounds; ({Offset min, Offset max}) getBounds(Offset origin, Polygon polygon) { final bbox = polygon.boundingBox; @@ -64,7 +66,7 @@ class _PolygonPainter extends CustomPainter { ..style = PaintingStyle.fill ..color = color; - if (useDrawVertices) { + if (trianglePoints.isNotEmpty) { final points = Float32List(trianglePoints.length * 2); for (int i = 0; i < trianglePoints.length; ++i) { points[i * 2] = trianglePoints[i].dx; @@ -83,11 +85,8 @@ class _PolygonPainter extends CustomPainter { canvas.drawPath(borderPath, _getBorderPaint(polygon)); } - if (useDrawVertices) { - trianglePoints.clear(); - } else { - filledPath = Path(); - } + trianglePoints.clear(); + filledPath = Path(); borderPath = Path(); @@ -104,13 +103,13 @@ class _PolygonPainter extends CustomPainter { final polygon = projectedPolygon.polygon; final polygonTriangles = triangles?[i]; - final useEfficientMethods = polygonTriangles != null; final fillOffsets = getOffsetsXY( camera: camera, origin: origin, points: projectedPolygon.points, - holePoints: useEfficientMethods ? projectedPolygon.holePoints : null, + holePoints: + polygonTriangles != null ? projectedPolygon.holePoints : null, ); // The hash is based on the polygons visual properties. If the hash from @@ -127,7 +126,7 @@ class _PolygonPainter extends CustomPainter { // ignore: deprecated_member_use_from_same_package if (polygon.isFilled ?? true) { if (polygon.color != null) { - if (useEfficientMethods) { + if (polygonTriangles != null) { final len = polygonTriangles.length; for (int i = 0; i < len; ++i) { trianglePoints.add(fillOffsets[polygonTriangles[i]]); diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 5dea13cf5..bac553d13 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -49,11 +49,34 @@ 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; + /// Whether to use more performant methods to draw this polygon + /// + /// When enabled, this internally: + /// * triangulates each polygon using the + /// ['dart_earcut' package](https://github.com/JaffaKetchup/dart_earcut) + /// * then uses [`drawVertices`](https://www.youtube.com/watch?v=pD38Yyz7N2E) + /// to draw the triangles to the underlying canvas + /// + /// In some cases, such as when this polygon is complex/self-intersecting, + /// the triangulation step can yield poor results, which will appear as + /// malformed polygons on the canvas. Disable this argument to use standard + /// canvas drawing methods which don't suffer this issue. + /// + /// Defaults to `true`. Will be ignored if the layer level + /// [PolygonLayer.performantRendering] argument is `false`. + // TODO: Detect self intersections? + final bool performantRendering; + /// Designates whether the given polygon points follow a clock or /// anti-clockwise direction. /// This is respected during draw call batching for filled polygons. @@ -112,6 +135,7 @@ class Polygon { this.labelStyle = const TextStyle(), this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, + this.performantRendering = true, }) : _filledAndClockwise = (isFilled ?? (color != null)) && isClockwise(points); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index cb40332f4..bb7c7f3bf 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -27,17 +27,16 @@ class PolygonLayer extends StatefulWidget { /// When enabled, this internally: /// * triangulates each polygon using the /// ['dart_earcut' package](https://github.com/JaffaKetchup/dart_earcut) - /// * uses [`drawVertices`](https://www.youtube.com/watch?v=pD38Yyz7N2E) to - /// draw the triangles to the underlying canvas + /// * then uses [`drawVertices`](https://www.youtube.com/watch?v=pD38Yyz7N2E) + /// to draw the triangles to the underlying canvas /// - /// In some cases, such as when input polygons are self intersecting, + /// In some cases, such as when input polygons are complex/self-intersecting, /// the triangulation step can yield poor results, which will appear as /// malformed polygons on the canvas. Disable this argument to use standard /// canvas drawing methods which don't suffer this issue. /// - /// Defaults to `true`. - // TODO: Toggle triangulation per polygon - // TODO: Detect self intersections? + /// Defaults to `true`. Will respect feature level + /// [Polygon.performantRendering] when this is `true`. final bool performantRendering; /// Whether to cull polygons and polygon sections that are outside of the @@ -128,13 +127,14 @@ class _PolygonLayerState extends State { ) .toList(); - // TODO: Check deviation if possible (https://github.com/mapbox/earcut/blob/afb5797dbf9272661ca4d49ee2e08bd0cd96e1ed/src/earcut.js#L629C4-L629C18) final triangles = !widget.performantRendering ? null : List.generate( culled.length, (i) { final culledPolygon = culled[i]; + if (!culledPolygon.polygon.performantRendering) return null; + return Earcut.triangulateRaw( (culledPolygon.holePoints.isNotEmpty ? culledPolygon.points.followedBy( From beb598f90f9f42bbb629b2822fbcdaf999f5ad95 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 28 Jan 2024 14:27:03 +0000 Subject: [PATCH 10/22] Changed `Polygon.performantRendering` type to `bool?` (to allow to inherit from layer correctly) --- lib/src/layer/polygon_layer/painter.dart | 8 +-- lib/src/layer/polygon_layer/polygon.dart | 10 ++-- .../layer/polygon_layer/polygon_layer.dart | 51 +++++++++---------- 3 files changed, 32 insertions(+), 37 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 2d23d2483..81dbc5fd7 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -9,11 +9,7 @@ class _PolygonPainter extends CustomPainter { /// Triangulated [polygons] if available /// /// Expected to be in same/corresponding order as [polygons]. - /// - /// Outer will be `null` when [PolygonLayer.performantRendering] is `false`. - /// Inner will be `null` when [Polygon.performantRendering] is `false`. - /// Lists *should* never be empty. - final List?>? triangles; + final List?> triangles; /// Reference to the [MapCamera]. final MapCamera camera; @@ -102,7 +98,7 @@ class _PolygonPainter extends CustomPainter { if (projectedPolygon.points.isEmpty) continue; final polygon = projectedPolygon.polygon; - final polygonTriangles = triangles?[i]; + final polygonTriangles = triangles[i]; final fillOffsets = getOffsetsXY( camera: camera, diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index bac553d13..10f3b96a0 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -72,10 +72,10 @@ class Polygon { /// malformed polygons on the canvas. Disable this argument to use standard /// canvas drawing methods which don't suffer this issue. /// - /// Defaults to `true`. Will be ignored if the layer level - /// [PolygonLayer.performantRendering] argument is `false`. - // TODO: Detect self intersections? - final bool performantRendering; + /// Defaults to `null` - respect layer level + /// [PolygonLayer.performantRendering]. + // TODO: Detect self intersections (Shamos-Hoey algorithm) and self-set? + final bool? performantRendering; /// Designates whether the given polygon points follow a clock or /// anti-clockwise direction. @@ -135,7 +135,7 @@ class Polygon { this.labelStyle = const TextStyle(), this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, - this.performantRendering = true, + this.performantRendering, }) : _filledAndClockwise = (isFilled ?? (color != null)) && isClockwise(points); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index bb7c7f3bf..08b4c4b14 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -35,8 +35,8 @@ class PolygonLayer extends StatefulWidget { /// malformed polygons on the canvas. Disable this argument to use standard /// canvas drawing methods which don't suffer this issue. /// - /// Defaults to `true`. Will respect feature level - /// [Polygon.performantRendering] when this is `true`. + /// Defaults to `true`. Individual polygons may be overriden using + /// [Polygon.performantRendering]. final bool performantRendering; /// Whether to cull polygons and polygon sections that are outside of the @@ -127,30 +127,29 @@ class _PolygonLayerState extends State { ) .toList(); - final triangles = !widget.performantRendering - ? null - : List.generate( - culled.length, - (i) { - final culledPolygon = culled[i]; - if (!culledPolygon.polygon.performantRendering) return null; - - return Earcut.triangulateRaw( - (culledPolygon.holePoints.isNotEmpty - ? culledPolygon.points.followedBy( - culledPolygon.holePoints.expand((e) => e)) - : culledPolygon.points) - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false), - // Not sure how just this works but it seems to :D - holeIndices: culledPolygon.holePoints.isNotEmpty - ? [culledPolygon.points.length] - : null, - ); - }, - growable: false, - ); + final triangles = List.generate( + culled.length, + (i) { + final culledPolygon = culled[i]; + if (!(culledPolygon.polygon.performantRendering ?? + widget.performantRendering)) return null; + + return Earcut.triangulateRaw( + (culledPolygon.holePoints.isEmpty + ? culledPolygon.points + : (culledPolygon.points + .followedBy(culledPolygon.holePoints.expand((e) => e)))) + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(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( From 7f2af33ba300250438cfa9d3ee99387d5b7a133a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 31 Jan 2024 12:31:35 +0000 Subject: [PATCH 11/22] Updated dart_earcut dependency to v1.1.0 --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index d2d830f4b..1522269ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ environment: dependencies: async: ^2.9.0 collection: ^1.17.1 - dart_earcut: ^1.0.1 + dart_earcut: ^1.1.0 flutter: sdk: flutter http: ^1.0.0 From 9f4b4dee948ae2cee1fbedf73f1a8c277d19f157 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 31 Jan 2024 20:56:40 +0000 Subject: [PATCH 12/22] Disabled `performantRendering` by default (at layer-level) Modified types and meanings of `performantRendering` flags Improved documentation on `performantRendering` flags --- lib/src/layer/polygon_layer/painter.dart | 4 +- lib/src/layer/polygon_layer/polygon.dart | 22 ++--- .../layer/polygon_layer/polygon_layer.dart | 80 ++++++++++--------- 3 files changed, 53 insertions(+), 53 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 81dbc5fd7..98ae24caa 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -9,7 +9,7 @@ class _PolygonPainter extends CustomPainter { /// Triangulated [polygons] if available /// /// Expected to be in same/corresponding order as [polygons]. - final List?> triangles; + final List?>? triangles; /// Reference to the [MapCamera]. final MapCamera camera; @@ -98,7 +98,7 @@ class _PolygonPainter extends CustomPainter { if (projectedPolygon.points.isEmpty) continue; final polygon = projectedPolygon.polygon; - final polygonTriangles = triangles[i]; + final polygonTriangles = triangles?[i]; final fillOffsets = getOffsetsXY( camera: camera, diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 10f3b96a0..df5c64493 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -59,23 +59,15 @@ class Polygon { /// it remains upright final bool rotateLabel; - /// Whether to use more performant methods to draw this polygon + /// {@macro fm.PolygonLayer.performantRendering} /// - /// When enabled, this internally: - /// * triangulates each polygon using the - /// ['dart_earcut' package](https://github.com/JaffaKetchup/dart_earcut) - /// * then uses [`drawVertices`](https://www.youtube.com/watch?v=pD38Yyz7N2E) - /// to draw the triangles to the underlying canvas + /// Value meanings (defaults to `true`): /// - /// In some cases, such as when this polygon is complex/self-intersecting, - /// the triangulation step can yield poor results, which will appear as - /// malformed polygons on the canvas. Disable this argument to use standard - /// canvas drawing methods which don't suffer this issue. + /// - `true` : respect layer-level value (disabled by default) + /// - `false`: disabled, ignore layer-level value /// - /// Defaults to `null` - respect layer level - /// [PolygonLayer.performantRendering]. - // TODO: Detect self intersections (Shamos-Hoey algorithm) and self-set? - final bool? performantRendering; + /// Also see [PolygonLayer.performantRendering]. + final bool performantRendering; /// Designates whether the given polygon points follow a clock or /// anti-clockwise direction. @@ -135,7 +127,7 @@ class Polygon { this.labelStyle = const TextStyle(), this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, - this.performantRendering, + this.performantRendering = true, }) : _filledAndClockwise = (isFilled ?? (color != null)) && isClockwise(points); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 08b4c4b14..5e9584991 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -22,21 +22,28 @@ class PolygonLayer extends StatefulWidget { /// [Polygon]s to draw final List polygons; - /// Whether to use more performant methods to draw polygons + /// {@template fm.PolygonLayer.performantRendering} + /// Whether to use an alternative, specialised, rendering pathway to draw + /// polygons, which can be more performant in some circumstances /// - /// When enabled, this internally: - /// * triangulates each polygon using the - /// ['dart_earcut' package](https://github.com/JaffaKetchup/dart_earcut) - /// * then uses [`drawVertices`](https://www.youtube.com/watch?v=pD38Yyz7N2E) - /// to draw the triangles to the underlying canvas + /// 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. /// - /// In some cases, such as when input polygons are complex/self-intersecting, - /// the triangulation step can yield poor results, which will appear as - /// malformed polygons on the canvas. Disable this argument to use standard - /// canvas drawing methods which don't suffer this issue. + /// For more information about usage (and the rendering pathway), see the + /// [online documentation](https://docs.fleaflet.dev/layers/polygon-layer#performant-rendering-drawvertices). + /// {@endtemplate} /// - /// Defaults to `true`. Individual polygons may be overriden using - /// [Polygon.performantRendering]. + /// Value meanings (defaults to `false`): + /// + /// - `true` : enabled, but respect individual feature-level overrides + /// - `false`: disabled, ignore feature-level overrides + /// - (no option is provided to disable by default but respect feature-level + /// overrides, as this will likely not be useful for this option's intended + /// purpose) + /// + /// Also see [Polygon.performantRendering]. final bool performantRendering; /// Whether to cull polygons and polygon sections that are outside of the @@ -69,7 +76,7 @@ class PolygonLayer extends StatefulWidget { const PolygonLayer({ super.key, required this.polygons, - this.performantRendering = true, + this.performantRendering = false, this.polygonCulling = true, this.simplificationTolerance = 0.5, this.polygonLabels = true, @@ -127,29 +134,30 @@ class _PolygonLayerState extends State { ) .toList(); - final triangles = List.generate( - culled.length, - (i) { - final culledPolygon = culled[i]; - if (!(culledPolygon.polygon.performantRendering ?? - widget.performantRendering)) return null; - - return Earcut.triangulateRaw( - (culledPolygon.holePoints.isEmpty - ? culledPolygon.points - : (culledPolygon.points - .followedBy(culledPolygon.holePoints.expand((e) => e)))) - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false), - // Not sure how just this works but it seems to :D - holeIndices: culledPolygon.holePoints.isEmpty - ? null - : [culledPolygon.points.length], - ); - }, - growable: false, - ); + final triangles = !widget.performantRendering + ? null + : List.generate( + culled.length, + (i) { + final culledPolygon = culled[i]; + return culledPolygon.polygon.performantRendering + ? Earcut.triangulateRaw( + (culledPolygon.holePoints.isEmpty + ? culledPolygon.points + : (culledPolygon.points.followedBy( + culledPolygon.holePoints.expand((e) => e)))) + .map((e) => [e.x, e.y]) + .expand((e) => e) + .toList(growable: false), + // Not sure how just this works but it seems to :D + holeIndices: culledPolygon.holePoints.isEmpty + ? null + : [culledPolygon.points.length], + ) + : null; + }, + growable: false, + ); return MobileLayerTransformer( child: CustomPaint( From 8dcdf8a07edc085bd323ecf045b8c658e62eaaa3 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 31 Jan 2024 22:02:34 +0000 Subject: [PATCH 13/22] Fix Polygons page in example app Updated example app dependencies --- example/lib/pages/polygon.dart | 48 +++++++++++++++++++--------------- example/pubspec.yaml | 8 +++--- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/example/lib/pages/polygon.dart b/example/lib/pages/polygon.dart index c3effbdf3..d3f1292f2 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -118,13 +118,6 @@ class PolygonPage extends StatelessWidget { rotateLabel: true, labelPlacement: PolygonLabelPlacement.polylabel, ), - Polygon( - points: _normalHoleOuterPoints, - holePointsList: _holeInnerPoints, - borderStrokeWidth: 4, - borderColor: Colors.black, - color: Colors.green, - ), Polygon( points: _normalHoleOuterPoints .map((latlng) => @@ -151,42 +144,55 @@ class PolygonPage extends StatelessWidget { Polygon( points: _brokenHoleOuterPoints .map((latlng) => - LatLng(latlng.latitude - 6, latlng.longitude)) + LatLng(latlng.latitude - 6, latlng.longitude + 8)) .toList(), + isDotted: true, holePointsList: _holeInnerPoints .map( (latlngs) => latlngs - .map((latlng) => - LatLng(latlng.latitude - 6, latlng.longitude)) + .map((latlng) => LatLng( + latlng.latitude - 6, latlng.longitude + 8)) .toList(), ) .toList(), borderStrokeWidth: 4, + borderColor: Colors.orange, + color: Colors.orange.withOpacity(0.5), + performantRendering: false, + label: 'This one is not\nperformantly rendered', + rotateLabel: true, + labelPlacement: PolygonLabelPlacement.centroid, + labelStyle: const TextStyle(color: Colors.black), + ), + ], + ), + PolygonLayer( + simplificationTolerance: 0, + performantRendering: 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 + 8)) + LatLng(latlng.latitude - 6, latlng.longitude)) .toList(), - isDotted: true, holePointsList: _holeInnerPoints .map( (latlngs) => latlngs - .map((latlng) => LatLng( - latlng.latitude - 6, latlng.longitude + 8)) + .map((latlng) => + LatLng(latlng.latitude - 6, latlng.longitude)) .toList(), ) .toList(), borderStrokeWidth: 4, - borderColor: Colors.orange, - color: Colors.orange.withOpacity(0.5), - performantRendering: false, - label: 'This one is not\nperformantly rendered', - rotateLabel: true, - labelPlacement: PolygonLabelPlacement.centroid, - labelStyle: const TextStyle(color: Colors.black), + borderColor: Colors.black, + color: Colors.green, ), ], ), 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: From 5b69740d3e9d047620eac4c93e0948a1c3d39e77 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 5 Feb 2024 22:01:37 +0000 Subject: [PATCH 14/22] Improved Polygon Stress Test example with more customizable border thicknesses --- example/lib/pages/polygon_perf_stress.dart | 232 +++++++++++++-------- 1 file changed, 142 insertions(+), 90 deletions(-) diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index 09faefc5e..828bba7f7 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -22,9 +24,9 @@ class _PolygonPerfStressPageState extends State { static const double _initialSimplificationTolerance = 0.5; double simplificationTolerance = _initialSimplificationTolerance; static const bool _initialUsePerformantDrawing = true; - bool usePerformantDrawing = _initialUsePerformantDrawing; - static const bool _initialUseThickOutlines = false; - bool useThickOutlines = _initialUseThickOutlines; + bool usePerformantRendering = _initialUsePerformantDrawing; + static const double _initialBorderThickness = 1; + double borderThickness = _initialBorderThickness; late Future geoJsonParser = loadPolygonsFromGeoJson(); @@ -72,7 +74,7 @@ class _PolygonPerfStressPageState extends State { ? const SizedBox.shrink() : PolygonLayer( polygons: geoJsonParser.data!.polygons, - performantRendering: usePerformantDrawing, + performantRendering: usePerformantRendering, simplificationTolerance: simplificationTolerance, ), ), @@ -82,59 +84,146 @@ class _PolygonPerfStressPageState extends State { left: 16, top: 16, right: 16, - child: Column( - children: [ - SimplificationToleranceSlider( - initialTolerance: _initialSimplificationTolerance, - onChangedTolerance: (v) => - setState(() => simplificationTolerance = v), - ), - const SizedBox(height: 12), - Row( - children: [ - _LabelledSwitch( - tooltipMessage: 'Performant Drawing', - icon: const Icon(Icons.speed_rounded), - value: usePerformantDrawing, - onChanged: (v) => - setState(() => usePerformantDrawing = v), - ), - const SizedBox(width: 12), - _LabelledSwitch( - tooltipMessage: 'Thick Outlines', - icon: const Icon(Icons.line_weight_rounded), - value: useThickOutlines, - onChanged: (v) async { - setState(() => useThickOutlines = v); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Row( + child: RepaintBoundary( + child: Column( + children: [ + SimplificationToleranceSlider( + initialTolerance: _initialSimplificationTolerance, + onChangedTolerance: (v) => + setState(() => simplificationTolerance = v), + ), + const SizedBox(height: 12), + Wrap( + alignment: WrapAlignment.center, + spacing: 12, + runSpacing: 12, + children: [ + UnconstrainedBox( + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(32), + ), + child: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 4, + bottom: 4, + ), + child: Row( + children: [ + const Tooltip( + message: 'Use Performant Rendering', + child: Icon(Icons.speed_rounded), + ), + const SizedBox(width: 8), + Switch.adaptive( + value: usePerformantRendering, + onChanged: (v) => setState( + () => usePerformantRendering = 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: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(32), + ), + child: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 8, + bottom: 8, + ), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 6, children: [ - SizedBox.square( - dimension: 16, - child: Center( - child: CircularProgressIndicator( - strokeWidth: 3, - valueColor: - AlwaysStoppedAnimation(Colors.white), + 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: 1px border may be more than 1px.', + child: Icon( + Icons.warning, + color: Colors.amber, ), ), + const SizedBox.shrink(), + ...List.generate( + 4, + (i) { + final thickness = pow(i, 2); + return ChoiceChip( + label: Text( + thickness == 0 + ? 'None' + : '${thickness}px', + ), + selected: borderThickness == thickness, + shape: const StadiumBorder(), + onSelected: (selected) 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(() {}); + }, + ); + }, ), - SizedBox(width: 12), - Text('Loading GeoJson polygons...'), ], ), ), - ); - await (geoJsonParser = loadPolygonsFromGeoJson()); - if (!context.mounted) return; - ScaffoldMessenger.of(context).clearSnackBars(); - setState(() {}); - }, - ), - ], - ), - ], + ), + ), + ], + ), + ], + ), ), ), if (!kIsWeb) @@ -155,49 +244,12 @@ class _PolygonPerfStressPageState extends State { return rootBundle.loadString(filePath).then( (geoJson) => compute( (msg) => GeoJsonParser( - defaultPolygonBorderStroke: msg.useThickOutlines ? 15 : 1, + defaultPolygonBorderStroke: msg.borderThickness, defaultPolygonBorderColor: Colors.black.withOpacity(0.5), - defaultPolygonFillColor: Colors.amber.withOpacity(0.5), + defaultPolygonFillColor: Colors.orange[700]!.withOpacity(0.75), )..parseGeoJsonAsString(msg.geoJson), - (geoJson: geoJson, useThickOutlines: useThickOutlines), + (geoJson: geoJson, borderThickness: borderThickness), ), ); } } - -class _LabelledSwitch extends StatelessWidget { - const _LabelledSwitch({ - required this.tooltipMessage, - required this.icon, - required this.value, - required this.onChanged, - }); - - final String tooltipMessage; - final Icon icon; - final bool value; - final void Function(bool) onChanged; - - @override - Widget build(BuildContext context) { - return UnconstrainedBox( - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.background, - borderRadius: BorderRadius.circular(32), - ), - child: Padding( - padding: - const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 4), - child: Row( - children: [ - Tooltip(message: tooltipMessage, child: icon), - const SizedBox(width: 8), - Switch.adaptive(value: value, onChanged: onChanged), - ], - ), - ), - ), - ); - } -} From e879cc6d8fbd6ef3527c39899443898b40563a92 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 6 Feb 2024 09:54:46 +0000 Subject: [PATCH 15/22] Fix `Polygon` equality to include `performantRendering` --- lib/src/layer/polygon_layer/polygon.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index df5c64493..a113e49dc 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -159,6 +159,7 @@ class Polygon { labelStyle == other.labelStyle && labelPlacement == other.labelPlacement && rotateLabel == other.rotateLabel && + performantRendering == other.performantRendering && // Expensive computations last to take advantage of lazy logic gates listEquals(holePointsList, other.holePointsList) && listEquals(points, other.points)); @@ -190,6 +191,7 @@ class Polygon { labelStyle, labelPlacement, rotateLabel, + performantRendering, renderHashCode, ]); } From 94feab1e22360d7832b68c84e9447fb808ff0bc6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 6 Feb 2024 10:02:33 +0000 Subject: [PATCH 16/22] Removed unneccesary `holePointsList` from `Polygon.renderHashCode` --- lib/src/layer/polygon_layer/polygon.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index a113e49dc..083ee3fc4 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -169,7 +169,6 @@ class Polygon { /// An optimized hash code dedicated to be used inside the [PolygonPainter]. int get renderHashCode => _renderHashCode ??= Object.hash( - holePointsList, color, borderStrokeWidth, borderColor, From d6ad7522a08bad203cb05712416defc943e91dc1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 6 Feb 2024 20:07:45 +0000 Subject: [PATCH 17/22] Minor performance improvements --- .../layer/polygon_layer/polygon_layer.dart | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 1eab7a27e..38dac6e74 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -158,21 +158,26 @@ class _PolygonLayerState extends State { culled.length, (i) { final culledPolygon = culled[i]; - return culledPolygon.polygon.performantRendering - ? Earcut.triangulateRaw( - (culledPolygon.holePoints.isEmpty - ? culledPolygon.points - : (culledPolygon.points.followedBy( - culledPolygon.holePoints.expand((e) => e)))) - .map((e) => [e.x, e.y]) - .expand((e) => e) - .toList(growable: false), - // Not sure how just this works but it seems to :D - holeIndices: culledPolygon.holePoints.isEmpty - ? null - : [culledPolygon.points.length], - ) - : null; + if (!culledPolygon.polygon.performantRendering) return null; + + 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, ); From dc0d83a20ab5f24e0ebc22b068cd1d4a4e1a2c82 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 6 Feb 2024 20:18:21 +0000 Subject: [PATCH 18/22] Minor performance improvements --- lib/src/misc/offsets.dart | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 82091cbf9..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); @@ -53,7 +52,7 @@ List getOffsetsXY({ final realPoints = holePoints == null || holePoints.isEmpty ? points - : [...points, ...holePoints.expand((e) => e)]; + : points.followedBy(holePoints.expand((e) => e)); final ox = -origin.dx; final oy = -origin.dy; @@ -61,12 +60,11 @@ List getOffsetsXY({ // 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 = realPoints[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; @@ -74,7 +72,7 @@ List getOffsetsXY({ final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final p = realPoints[i]; + final p = realPoints.elementAt(i); final (x, y) = crs.transform(p.x, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } From 87e0bd56330dbf8ec03a11acf6a920c1a92711d6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 6 Feb 2024 20:32:28 +0000 Subject: [PATCH 19/22] Documentation fix --- lib/src/layer/polygon_layer/polygon_layer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 38dac6e74..0db8e0160 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -32,7 +32,7 @@ class PolygonLayer extends StatefulWidget { /// already in use. /// /// For more information about usage (and the rendering pathway), see the - /// [online documentation](https://docs.fleaflet.dev/layers/polygon-layer#performant-rendering-drawvertices). + /// [online documentation](https://docs.fleaflet.dev/layers/polygon-layer#performant-rendering-with-drawvertices-internal-disabled). /// {@endtemplate} /// /// Value meanings (defaults to `false`): From 9475c7aa706f6abafaafa3c5420f3909b7ee671e Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 12 Feb 2024 23:08:17 +0000 Subject: [PATCH 20/22] Renamed `PolygonLayer.performantRendering` to `useAltRendering` Removed feature level `Polygon.performantRendering` Minor improvements to example app Improved documentation Made review required changes --- example/lib/pages/many_circles.dart | 9 +- example/lib/pages/many_markers.dart | 9 +- example/lib/pages/polygon.dart | 4 +- example/lib/pages/polygon_perf_stress.dart | 210 +++++++++--------- example/lib/pages/polyline_perf_stress.dart | 8 +- .../lib/widgets/number_of_items_slider.dart | 35 +-- .../simplification_tolerance_slider.dart | 34 +-- lib/src/layer/polygon_layer/label.dart | 1 + lib/src/layer/polygon_layer/polygon.dart | 13 -- .../layer/polygon_layer/polygon_layer.dart | 50 ++--- 10 files changed, 152 insertions(+), 221 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 d3f1292f2..04de145bb 100644 --- a/example/lib/pages/polygon.dart +++ b/example/lib/pages/polygon.dart @@ -135,7 +135,6 @@ class PolygonPage extends StatelessWidget { borderStrokeWidth: 4, borderColor: Colors.orange, color: Colors.orange.withOpacity(0.5), - performantRendering: false, label: 'This one is not\nperformantly rendered', rotateLabel: true, labelPlacement: PolygonLabelPlacement.centroid, @@ -158,7 +157,6 @@ class PolygonPage extends StatelessWidget { borderStrokeWidth: 4, borderColor: Colors.orange, color: Colors.orange.withOpacity(0.5), - performantRendering: false, label: 'This one is not\nperformantly rendered', rotateLabel: true, labelPlacement: PolygonLabelPlacement.centroid, @@ -168,7 +166,7 @@ class PolygonPage extends StatelessWidget { ), PolygonLayer( simplificationTolerance: 0, - performantRendering: true, + useAltRendering: true, polygons: [ Polygon( points: _normalHoleOuterPoints, diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index 828bba7f7..aff27f40b 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -21,12 +21,9 @@ class PolygonPerfStressPage extends StatefulWidget { } class _PolygonPerfStressPageState extends State { - static const double _initialSimplificationTolerance = 0.5; - double simplificationTolerance = _initialSimplificationTolerance; - static const bool _initialUsePerformantDrawing = true; - bool usePerformantRendering = _initialUsePerformantDrawing; - static const double _initialBorderThickness = 1; - double borderThickness = _initialBorderThickness; + double simplificationTolerance = 0.5; + bool useAltRendering = true; + double borderThickness = 1; late Future geoJsonParser = loadPolygonsFromGeoJson(); @@ -59,8 +56,8 @@ class _PolygonPerfStressPageState extends State { padding: const EdgeInsets.only( left: 16, right: 16, - top: 88, - bottom: 192, + top: 145, + bottom: 175, ), ), ), @@ -74,7 +71,7 @@ class _PolygonPerfStressPageState extends State { ? const SizedBox.shrink() : PolygonLayer( polygons: geoJsonParser.data!.polygons, - performantRendering: usePerformantRendering, + useAltRendering: useAltRendering, simplificationTolerance: simplificationTolerance, ), ), @@ -88,8 +85,8 @@ class _PolygonPerfStressPageState extends State { child: Column( children: [ SimplificationToleranceSlider( - initialTolerance: _initialSimplificationTolerance, - onChangedTolerance: (v) => + tolerance: simplificationTolerance, + onChanged: (v) => setState(() => simplificationTolerance = v), ), const SizedBox(height: 12), @@ -99,33 +96,28 @@ class _PolygonPerfStressPageState extends State { runSpacing: 12, children: [ UnconstrainedBox( - child: DecoratedBox( + child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(32), ), - child: Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 4, - bottom: 4, - ), - child: Row( - children: [ - const Tooltip( - message: 'Use Performant Rendering', - child: Icon(Icons.speed_rounded), - ), - const SizedBox(width: 8), - Switch.adaptive( - value: usePerformantRendering, - onChanged: (v) => setState( - () => usePerformantRendering = v, - ), - ), - ], - ), + 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), + ), + ], ), ), ), @@ -133,90 +125,54 @@ class _PolygonPerfStressPageState extends State { // time this is changed, but the library gives no easy // way to change it after UnconstrainedBox( - child: DecoratedBox( + child: Container( decoration: BoxDecoration( color: Theme.of(context).colorScheme.background, borderRadius: BorderRadius.circular(32), ), - child: Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - top: 8, - bottom: 8, - ), - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 6, - children: [ + 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: 'Border Thickness', - child: Icon(Icons.line_weight_rounded), - ), - if (MediaQuery.devicePixelRatioOf(context) > - 1 && - borderThickness == 1) - const Tooltip( - message: - 'Screen has a high DPR: 1px border may be more than 1px.', - child: Icon( - Icons.warning, - color: Colors.amber, - ), + message: 'Screen has a high DPR: 1lp > 1dp', + child: Icon( + Icons.warning, + color: Colors.amber, ), - const SizedBox.shrink(), - ...List.generate( - 4, - (i) { - final thickness = pow(i, 2); - return ChoiceChip( - label: Text( - thickness == 0 - ? 'None' - : '${thickness}px', - ), - selected: borderThickness == thickness, - shape: const StadiumBorder(), - onSelected: (selected) 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(() {}); - }, - ); - }, ), - ], - ), + const SizedBox.shrink(), + ...List.generate( + 4, + (i) { + final thickness = pow(i, 2); + return ChoiceChip( + label: Text( + thickness == 0 + ? 'None' + : '${thickness}px', + ), + selected: borderThickness == thickness, + shape: const StadiumBorder(), + onSelected: (selected) => reloadGeoJson( + context: context, + selected: selected, + thickness: thickness, + ), + ); + }, + ), + ], ), ), ), @@ -238,6 +194,38 @@ 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'; 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 781032ab3..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.adaptive( - value: _number.toDouble(), - onChanged: (v) { - if (_number == 0 && v != 0) { - widget.onChangedNumber(v.toInt()); - } - setState(() => _number = v.toInt()); - }, - onChangeEnd: (v) => widget.onChangedNumber(v.toInt()), + 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 c606b7562..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) { @@ -42,20 +33,13 @@ class _SimplificationToleranceSliderState ), Expanded( child: Slider.adaptive( - value: _simplificationTolerance, - onChanged: (v) { - if (_simplificationTolerance == 0 && v != 0) { - widget.onChangedTolerance(v); - } - setState(() => _simplificationTolerance = v); - }, - onChangeEnd: widget.onChangedTolerance, + 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/lib/src/layer/polygon_layer/label.dart b/lib/src/layer/polygon_layer/label.dart index 458ddc63b..372cdead0 100644 --- a/lib/src/layer/polygon_layer/label.dart +++ b/lib/src/layer/polygon_layer/label.dart @@ -85,6 +85,7 @@ 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. + // TODO: Make this an external option precision: 0.0001, ); return LatLng( diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 083ee3fc4..8e3bd0d95 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -59,16 +59,6 @@ class Polygon { /// it remains upright final bool rotateLabel; - /// {@macro fm.PolygonLayer.performantRendering} - /// - /// Value meanings (defaults to `true`): - /// - /// - `true` : respect layer-level value (disabled by default) - /// - `false`: disabled, ignore layer-level value - /// - /// Also see [PolygonLayer.performantRendering]. - final bool performantRendering; - /// Designates whether the given polygon points follow a clock or /// anti-clockwise direction. /// This is respected during draw call batching for filled polygons. @@ -127,7 +117,6 @@ class Polygon { this.labelStyle = const TextStyle(), this.labelPlacement = PolygonLabelPlacement.centroid, this.rotateLabel = false, - this.performantRendering = true, }) : _filledAndClockwise = (isFilled ?? (color != null)) && isClockwise(points); @@ -159,7 +148,6 @@ class Polygon { labelStyle == other.labelStyle && labelPlacement == other.labelPlacement && rotateLabel == other.rotateLabel && - performantRendering == other.performantRendering && // Expensive computations last to take advantage of lazy logic gates listEquals(holePointsList, other.holePointsList) && listEquals(points, other.points)); @@ -190,7 +178,6 @@ class Polygon { labelStyle, labelPlacement, rotateLabel, - performantRendering, renderHashCode, ]); } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 0db8e0160..091381789 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -22,29 +22,22 @@ class PolygonLayer extends StatefulWidget { /// [Polygon]s to draw final List polygons; - /// {@template fm.PolygonLayer.performantRendering} - /// Whether to use an alternative, specialised, rendering pathway to draw - /// polygons, which can be more performant in some circumstances + /// 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. + /// 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 the rendering pathway), see the + /// For more information about usage and pitfalls, see the /// [online documentation](https://docs.fleaflet.dev/layers/polygon-layer#performant-rendering-with-drawvertices-internal-disabled). - /// {@endtemplate} /// - /// Value meanings (defaults to `false`): - /// - /// - `true` : enabled, but respect individual feature-level overrides - /// - `false`: disabled, ignore feature-level overrides - /// - (no option is provided to disable by default but respect feature-level - /// overrides, as this will likely not be useful for this option's intended - /// purpose) - /// - /// Also see [Polygon.performantRendering]. - final bool performantRendering; + /// 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 @@ -76,7 +69,7 @@ class PolygonLayer extends StatefulWidget { const PolygonLayer({ super.key, required this.polygons, - this.performantRendering = false, + this.useAltRendering = false, this.polygonCulling = true, this.simplificationTolerance = 0.5, this.polygonLabels = true, @@ -152,13 +145,12 @@ class _PolygonLayerState extends State { ) .toList(); - final triangles = !widget.performantRendering + final triangles = !widget.useAltRendering ? null : List.generate( culled.length, (i) { final culledPolygon = culled[i]; - if (!culledPolygon.polygon.performantRendering) return null; final points = culledPolygon.holePoints.isEmpty ? culledPolygon.points @@ -222,17 +214,15 @@ class _PolygonLayerState extends State { tolerance: tolerance, highQuality: true, ), - holePoints: holes.isEmpty - ? [] - : 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, From dfcac55064b8e1b683cb405d1d436a182b2f113c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 12 Feb 2024 23:15:51 +0000 Subject: [PATCH 21/22] Minor change to remove dependency on 'dart:math' in one file --- example/lib/pages/polygon_perf_stress.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index aff27f40b..086ab727c 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -155,7 +153,7 @@ class _PolygonPerfStressPageState extends State { ...List.generate( 4, (i) { - final thickness = pow(i, 2); + final thickness = i * i; return ChoiceChip( label: Text( thickness == 0 From 69f7f734c960c64f069cc3c6b04f127dea17c9bb Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 13 Feb 2024 11:30:37 +0000 Subject: [PATCH 22/22] Minor efficiency improvement --- lib/src/layer/polygon_layer/painter.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 98ae24caa..af5457eaa 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -44,8 +44,8 @@ class _PolygonPainter extends CustomPainter { void paint(Canvas canvas, Size size) { final trianglePoints = []; - var filledPath = Path(); - var borderPath = Path(); + final filledPath = Path(); + final borderPath = Path(); Polygon? lastPolygon; int? lastHash; @@ -82,9 +82,9 @@ class _PolygonPainter extends CustomPainter { } trianglePoints.clear(); - filledPath = Path(); + filledPath.reset(); - borderPath = Path(); + borderPath.reset(); lastPolygon = null; lastHash = null;