From 8bb6b0de596f9a5e92b6f2e8226f620a92ac86cc Mon Sep 17 00:00:00 2001 From: mootw Date: Wed, 25 Oct 2023 12:55:57 -0500 Subject: [PATCH 01/27] implementation of polyline and polygon simplification --- .../layer/polygon_layer/polygon_layer.dart | 41 ++++-- lib/src/layer/polyline_layer.dart | 49 +++++-- lib/src/misc/simplify.dart | 120 ++++++++++++++++++ 3 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 lib/src/misc/simplify.dart diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 16381e423..a5544e96b 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; @@ -5,6 +6,7 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { @@ -95,6 +97,12 @@ class PolygonLayer extends StatelessWidget { /// screen space culling of polygons based on bounding box final bool polygonCulling; + /// how much to simplify the polygons, in decimal degrees scaled to floored zoom + final double? simplificationTolerance; + + /// use high quality simplification + final bool simplificationHighQuality; + // Turn on/off per-polygon label drawing on the layer-level. final bool polygonLabels; @@ -102,6 +110,8 @@ class PolygonLayer extends StatelessWidget { super.key, required this.polygons, this.polygonCulling = false, + this.simplificationTolerance = 1, + this.simplificationHighQuality = false, this.polygonLabels = true, }); @@ -110,7 +120,7 @@ class PolygonLayer extends StatelessWidget { final map = MapCamera.of(context); final size = Size(map.size.x, map.size.y); - final pgons = polygonCulling + final polygonsToRender = polygonCulling ? polygons.where((p) { return p.boundingBox.isOverlapping(map.visibleBounds); }).toList() @@ -118,7 +128,8 @@ class PolygonLayer extends StatelessWidget { return MobileLayerTransformer( child: CustomPaint( - painter: PolygonPainter(pgons, map, polygonLabels), + painter: PolygonPainter(polygonsToRender, map, polygonLabels, + simplificationTolerance, simplificationHighQuality), size: size, isComplex: true, ), @@ -128,12 +139,16 @@ class PolygonLayer extends StatelessWidget { class PolygonPainter extends CustomPainter { final List polygons; - final MapCamera map; + final MapCamera camera; final LatLngBounds bounds; final bool polygonLabels; - PolygonPainter(this.polygons, this.map, this.polygonLabels) - : bounds = map.visibleBounds; + final double? simplificationTolerance; + final bool simplificationHighQuality; + + PolygonPainter(this.polygons, this.camera, this.polygonLabels, + this.simplificationTolerance, this.simplificationHighQuality) + : bounds = camera.visibleBounds; int get hash { _hash ??= Object.hashAll(polygons); @@ -143,10 +158,18 @@ class PolygonPainter extends CustomPainter { int? _hash; List getOffsets(List points) { + final List simplifiedPoints; + if (simplificationTolerance != null) { + simplifiedPoints = simplify(points, + simplificationTolerance! / pow(2, camera.zoom.floorToDouble()), + highestQuality: simplificationHighQuality); + } else { + simplifiedPoints = points; + } return List.generate( - points.length, + simplifiedPoints.length, (index) { - return map.getOffsetFromOrigin(points[index]); + return camera.getOffsetFromOrigin(simplifiedPoints[index]); }, growable: false, ); @@ -248,11 +271,11 @@ class PolygonPainter extends CustomPainter { final painter = buildLabelTextPainter( polygon.points, polygon.labelPosition, - placementPoint: map.getOffsetFromOrigin(polygon.labelPosition), + placementPoint: camera.getOffsetFromOrigin(polygon.labelPosition), points: offsets, labelText: polygon.label!, labelStyle: polygon.labelStyle, - rotationRad: map.rotationRad, + rotationRad: camera.rotationRad, rotate: polygon.rotateLabel, padding: 10, ); diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index cc245a647..55c174c23 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -1,10 +1,12 @@ import 'dart:core'; +import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -58,10 +60,18 @@ class PolylineLayer extends StatelessWidget { final List polylines; final bool polylineCulling; + /// how much to simplify the polygons, in decimal degrees scaled to floored zoom + final double? simplificationTolerance; + + /// use high quality simplification + final bool simplificationHighQuality; + const PolylineLayer({ super.key, required this.polylines, this.polylineCulling = false, + this.simplificationTolerance = 1, + this.simplificationHighQuality = false, }); @override @@ -71,13 +81,15 @@ class PolylineLayer extends StatelessWidget { return MobileLayerTransformer( child: CustomPaint( painter: PolylinePainter( - polylineCulling - ? polylines - .where((p) => p.boundingBox.isOverlapping(map.visibleBounds)) - .toList() - : polylines, - map, - ), + polylineCulling + ? polylines + .where( + (p) => p.boundingBox.isOverlapping(map.visibleBounds)) + .toList() + : polylines, + map, + simplificationTolerance, + simplificationHighQuality), size: Size(map.size.x, map.size.y), isComplex: true, ), @@ -88,22 +100,35 @@ class PolylineLayer extends StatelessWidget { class PolylinePainter extends CustomPainter { final List polylines; - final MapCamera map; + final MapCamera camera; final LatLngBounds bounds; - PolylinePainter(this.polylines, this.map) : bounds = map.visibleBounds; + final double? simplificationTolerance; + final bool simplificationHighQuality; + + PolylinePainter(this.polylines, this.camera, this.simplificationTolerance, + this.simplificationHighQuality) + : bounds = camera.visibleBounds; int get hash => _hash ??= Object.hashAll(polylines); int? _hash; List getOffsets(List points) { - return List.generate(points.length, (index) { - return getOffset(points[index]); + final List simplifiedPoints; + if (simplificationTolerance != null) { + simplifiedPoints = simplify(points, + simplificationTolerance! / pow(2, camera.zoom.floorToDouble()), + highestQuality: simplificationHighQuality); + } else { + simplifiedPoints = points; + } + return List.generate(simplifiedPoints.length, (index) { + return getOffset(simplifiedPoints[index]); }, growable: false); } - Offset getOffset(LatLng point) => map.getOffsetFromOrigin(point); + Offset getOffset(LatLng point) => camera.getOffsetFromOrigin(point); @override void paint(Canvas canvas, Size size) { diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart new file mode 100644 index 000000000..564d47453 --- /dev/null +++ b/lib/src/misc/simplify.dart @@ -0,0 +1,120 @@ +// implementation based on +// https://github.com/mourner/simplify-js/blob/master/simplify.js + + +import 'package:latlong2/latlong.dart'; + +double _getSqDist( + LatLng p1, + LatLng p2, +) { + final double dx = p1.longitude - p2.longitude; + final double dy = p1.latitude - p2.latitude; + return dx * dx + dy * dy; +} + +// square distance from a point to a segment +double _getSqSegDist( + LatLng p, + LatLng p1, + LatLng p2, +) { + double x = p1.longitude; + double y = p1.latitude; + double dx = p2.longitude - x; + double dy = p2.latitude - y; + if (dx != 0 || dy != 0) { + final double t = + ((p.longitude - x) * dx + (p.latitude - y) * dy) / + (dx * dx + dy * dy); + if (t > 1) { + x = p2.longitude; + y = p2.latitude; + } else if (t > 0) { + x += dx * t; + y += dy * t; + } + } + + dx = p.longitude - x; + dy = p.latitude - y; + + return dx * dx + dy * dy; +} + +List _simplifyRadialDist( + List points, + double sqTolerance, +) { + LatLng prevPoint = points[0]; + final List newPoints = [prevPoint]; + late LatLng point; + for (int i = 1, len = points.length; i < len; i++) { + point = points[i]; + if (_getSqDist(point, prevPoint) > sqTolerance) { + newPoints.add(point); + prevPoint = point; + } + } + if (prevPoint != point) { + newPoints.add(point); + } + return newPoints; +} + +void _simplifyDPStep( + List points, + int first, + int last, + double sqTolerance, + List simplified, +) { + double maxSqDist = sqTolerance; + late int index; + for (int i = first + 1; i < last; i++) { + final double sqDist = _getSqSegDist(points[i], points[first], points[last]); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + if (maxSqDist > sqTolerance) { + if (index - first > 1) { + _simplifyDPStep(points, first, index, sqTolerance, simplified); + } + simplified.add(points[index]); + if (last - index > 1) { + _simplifyDPStep(points, index, last, sqTolerance, simplified); + } + } +} + +// simplification using Ramer-Douglas-Peucker algorithm +List _simplifyDouglasPeucker( + List points, + double sqTolerance, +) { + final int last = points.length - 1; + final List simplified = [points[0]]; + _simplifyDPStep(points, 0, last, sqTolerance, simplified); + simplified.add(points[last]); + return simplified; +} + +// both algorithms combined for awesome performance +List simplify( + List points, + double tolerance, { + bool highestQuality = false, +}) { + if (points.length <= 2) { + return points; + } + List nextPoints = points; + final double sqTolerance = tolerance * tolerance; + nextPoints = + highestQuality ? points : _simplifyRadialDist(nextPoints, sqTolerance); + nextPoints = _simplifyDouglasPeucker(nextPoints, sqTolerance); + return nextPoints; +} From 99d1ebf6f57738b3f89cb49935aff3fc009791f8 Mon Sep 17 00:00:00 2001 From: mootw Date: Tue, 7 Nov 2023 01:52:31 -0600 Subject: [PATCH 02/27] implement polyline culling --- example/lib/pages/polyline.dart | 34 +++++++ lib/src/layer/polyline_layer.dart | 154 ++++++++++++++++++++++++++---- 2 files changed, 170 insertions(+), 18 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 5acc40753..05eb7fb66 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/widgets/drawer.dart'; @@ -13,6 +15,21 @@ class PolylinePage extends StatefulWidget { } class _PolylinePageState extends State { + List randomWalk = [const LatLng(45, -93.4)]; + + @override + void initState() { + super.initState(); + + final random = Random(1234); + for (int i = 1; i < 100000; i++) { + final lat = (random.nextDouble() - 0.5) * 0.001; + final lon = (random.nextDouble() - 0.6) * 0.001; + randomWalk.add(LatLng( + randomWalk[i - 1].latitude + lat, randomWalk[i - 1].longitude + lon)); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -39,7 +56,24 @@ class _PolylinePageState extends State { userAgentPackageName: 'dev.fleaflet.flutter_map.example', ), PolylineLayer( + polylineCullingMargin: 25, polylines: [ + Polyline( + points: randomWalk, + strokeWidth: 3, + color: Colors.deepOrange, + ), + Polyline( + points: [ + const LatLng(50, 0), + const LatLng(50, -10), + const LatLng(47, -12), + const LatLng(45, -10), + const LatLng(45, 0), + ], + strokeWidth: 8, + color: Colors.purple, + ), Polyline( points: [ const LatLng(51.5, -0.09), diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 55c174c23..dc2d41037 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -22,11 +22,6 @@ class Polyline { final StrokeJoin strokeJoin; final bool useStrokeWidthInMeter; - LatLngBounds? _boundingBox; - - LatLngBounds get boundingBox => - _boundingBox ??= LatLngBounds.fromPoints(points); - Polyline({ required this.points, this.strokeWidth = 1.0, @@ -58,8 +53,13 @@ class Polyline { @immutable class PolylineLayer extends StatelessWidget { final List polylines; + @Deprecated('has no effect anymore, polyline culling is enabled by default and controlled by margin') final bool polylineCulling; + /// extent outside of the viewport before culling polylines, set to null to + /// disable polyline culling + final double? polylineCullingMargin; + /// how much to simplify the polygons, in decimal degrees scaled to floored zoom final double? simplificationTolerance; @@ -69,28 +69,146 @@ class PolylineLayer extends StatelessWidget { const PolylineLayer({ super.key, required this.polylines, - this.polylineCulling = false, + this.polylineCulling = true, + this.polylineCullingMargin = 0, this.simplificationTolerance = 1, this.simplificationHighQuality = false, }); + bool aabbContainsLine( + num x1, num y1, num x2, num y2, num minX, num minY, num maxX, num maxY) { + // Completely outside. + if ((x1 <= minX && x2 <= minX) || + (y1 <= minY && y2 <= minY) || + (x1 >= maxX && x2 >= maxX) || + (y1 >= maxY && y2 >= maxY)) { + return false; + } + + final m = (y2 - y1) / (x2 - x1); + + num y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + + num x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + + return false; + } + @override Widget build(BuildContext context) { - final map = MapCamera.of(context); + final mapCamera = MapCamera.of(context); + + final renderedLines = []; + + if (polylineCullingMargin == null) { + renderedLines.addAll(polylines); + } else { + final bounds = mapCamera.visibleBounds; + final margin = + polylineCullingMargin! / pow(2, mapCamera.zoom.floorToDouble()); + // The min(-90), max(180), etc.. are used to get around the limits of LatLng + // the value cannot be greater or smaller than that + final boundsAdjusted = LatLngBounds( + LatLng(max(-90, bounds.southWest.latitude - margin), + max(-180, bounds.southWest.longitude - margin)), + LatLng(min(90, bounds.northEast.latitude + margin), + min(180, bounds.northEast.longitude + margin))); + + for (final polyline in polylines) { + // Gradiant poylines do not render identically and cannot be easily segmented + if (polyline.gradientColors != null) { + renderedLines.add(polyline); + continue; + } + // pointer that indicates the start of the visible polyline segment + int start = -1; + bool fullyVisible = true; + for (int i = 0; i < polyline.points.length - 1; i++) { + //current pair + final p1 = polyline.points[i]; + final p2 = polyline.points[i + 1]; + + // segment is visible + if (aabbContainsLine( + p1.longitude, + p1.latitude, + p2.longitude, + p2.latitude, + boundsAdjusted.southWest.longitude, + boundsAdjusted.southWest.latitude, + boundsAdjusted.northEast.longitude, + boundsAdjusted.northEast.latitude)) { + // segment is visible + if (start == -1) { + start = i; + } + if (!fullyVisible && i == polyline.points.length - 2) { + // print("last segment visible"); + //last segment is visible + final segment = polyline.points.sublist(start, i + 2); + renderedLines.add(Polyline( + points: segment, + borderColor: polyline.borderColor, + borderStrokeWidth: polyline.borderStrokeWidth, + color: polyline.color, + colorsStop: polyline.colorsStop, + gradientColors: polyline.gradientColors, + isDotted: polyline.isDotted, + strokeCap: polyline.strokeCap, + strokeJoin: polyline.strokeJoin, + strokeWidth: polyline.strokeWidth, + useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, + )); + } + // print("$i visible"); + } else { + // print("$i not visible"); + fullyVisible = false; + // if we cannot see the segment, then reset start + if (start != -1) { + // print("partial $start $i"); + final segment = polyline.points.sublist(start, i + 1); + renderedLines.add(Polyline( + points: segment, + borderColor: polyline.borderColor, + borderStrokeWidth: polyline.borderStrokeWidth, + color: polyline.color, + colorsStop: polyline.colorsStop, + gradientColors: polyline.gradientColors, + isDotted: polyline.isDotted, + strokeCap: polyline.strokeCap, + strokeJoin: polyline.strokeJoin, + strokeWidth: polyline.strokeWidth, + useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, + )); + start = -1; + } + if (start != -1) { + start = i; + } + } + } + if (fullyVisible) { + //The whole polyline is visible + // print("rendered whole polyhon"); + renderedLines.add(polyline); + } + } + } return MobileLayerTransformer( child: CustomPaint( - painter: PolylinePainter( - polylineCulling - ? polylines - .where( - (p) => p.boundingBox.isOverlapping(map.visibleBounds)) - .toList() - : polylines, - map, - simplificationTolerance, - simplificationHighQuality), - size: Size(map.size.x, map.size.y), + painter: PolylinePainter(renderedLines, mapCamera, + simplificationTolerance, simplificationHighQuality), + size: Size(mapCamera.size.x, mapCamera.size.y), isComplex: true, ), ); From 59134cecce69bb621d3e982f55436e8c9eb764b0 Mon Sep 17 00:00:00 2001 From: mootw Date: Sun, 12 Nov 2023 16:43:30 -0600 Subject: [PATCH 03/27] Update lib/src/layer/polyline_layer.dart Co-authored-by: Luka S --- lib/src/layer/polyline_layer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index dc2d41037..b1a30a6b7 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -198,7 +198,6 @@ class PolylineLayer extends StatelessWidget { } if (fullyVisible) { //The whole polyline is visible - // print("rendered whole polyhon"); renderedLines.add(polyline); } } From 7b52f7098bb8958d7452ff0d39f971ec3ffbd48a Mon Sep 17 00:00:00 2001 From: mootw Date: Sun, 12 Nov 2023 17:18:25 -0600 Subject: [PATCH 04/27] move aabbContainsLine to bounds --- lib/src/layer/polyline_layer.dart | 5 +---- lib/src/misc/bounds.dart | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 28ddab13e..fbe5f508a 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -4,10 +4,6 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/geo/latlng_bounds.dart'; -import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; -import 'package:flutter_map/src/map/camera/camera.dart'; -import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; @@ -146,6 +142,7 @@ class PolylineLayer extends StatelessWidget { if (start != -1) { // partial start final segment = polyline.points.sublist(start, i + 1); + // TODO copyWith method for polyline renderedLines.add(Polyline( points: segment, borderColor: polyline.borderColor, diff --git a/lib/src/misc/bounds.dart b/lib/src/misc/bounds.dart index 731f91031..36e71bfd3 100644 --- a/lib/src/misc/bounds.dart +++ b/lib/src/misc/bounds.dart @@ -87,6 +87,32 @@ class Bounds { (b.max.y >= min.y); } + bool aabbContainsLine(num x1, num y1, num x2, num y2) { + // Completely outside. + if ((x1 <= min.x && x2 <= min.x) || + (y1 <= min.y && y2 <= min.y) || + (x1 >= max.x && x2 >= max.x) || + (y1 >= max.y && y2 >= max.y)) { + return false; + } + + final m = (y2 - y1) / (x2 - x1); + + num y = m * (min.x - x1) + y1; + if (y > min.y && y < max.y) return true; + + y = m * (max.x - x1) + y1; + if (y > min.y && y < max.y) return true; + + num x = (min.y - y1) / m + x1; + if (x > min.x && x < max.x) return true; + + x = (max.y - y1) / m + x1; + if (x > min.x && x < max.x) return true; + + return false; + } + /// Calculates the intersection of two Bounds. The return value will be null /// if there is no intersection. The returned bounds may be zero size /// (bottomLeft == topRight). From ecfe774d7cd74b09a7d3a5ef424e24d7ba30b08b Mon Sep 17 00:00:00 2001 From: mootw Date: Sun, 12 Nov 2023 17:31:37 -0600 Subject: [PATCH 05/27] dart format, move simplify to its own library --- lib/{src/misc => }/simplify.dart | 15 ++++++++------- lib/src/layer/polygon_layer/polygon_layer.dart | 11 ++++++++--- lib/src/layer/polyline_layer.dart | 6 +----- 3 files changed, 17 insertions(+), 15 deletions(-) rename lib/{src/misc => }/simplify.dart (85%) diff --git a/lib/src/misc/simplify.dart b/lib/simplify.dart similarity index 85% rename from lib/src/misc/simplify.dart rename to lib/simplify.dart index 564d47453..99b0c3423 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/simplify.dart @@ -1,6 +1,7 @@ // implementation based on // https://github.com/mourner/simplify-js/blob/master/simplify.js +library simplify; import 'package:latlong2/latlong.dart'; @@ -25,8 +26,7 @@ double _getSqSegDist( double dy = p2.latitude - y; if (dx != 0 || dy != 0) { final double t = - ((p.longitude - x) * dx + (p.latitude - y) * dy) / - (dx * dx + dy * dy); + ((p.longitude - x) * dx + (p.latitude - y) * dy) / (dx * dx + dy * dy); if (t > 1) { x = p2.longitude; y = p2.latitude; @@ -42,7 +42,7 @@ double _getSqSegDist( return dx * dx + dy * dy; } -List _simplifyRadialDist( +List simplifyRadialDist( List points, double sqTolerance, ) { @@ -91,7 +91,7 @@ void _simplifyDPStep( } // simplification using Ramer-Douglas-Peucker algorithm -List _simplifyDouglasPeucker( +List simplifyDouglasPeucker( List points, double sqTolerance, ) { @@ -102,7 +102,8 @@ List _simplifyDouglasPeucker( return simplified; } -// both algorithms combined for awesome performance +/// high quality simplification uses the Ramer-Douglas-Peucker algorithm +/// otherwise it just merges close points List simplify( List points, double tolerance, { @@ -114,7 +115,7 @@ List simplify( List nextPoints = points; final double sqTolerance = tolerance * tolerance; nextPoints = - highestQuality ? points : _simplifyRadialDist(nextPoints, sqTolerance); - nextPoints = _simplifyDouglasPeucker(nextPoints, sqTolerance); + highestQuality ? points : simplifyRadialDist(nextPoints, sqTolerance); + nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); return nextPoints; } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 9dd2284c4..aa7c4c7ac 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -7,7 +7,7 @@ import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; -import 'package:flutter_map/src/misc/simplify.dart'; +import 'package:flutter_map/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { @@ -164,8 +164,13 @@ class PolygonPainter extends CustomPainter { final double? simplificationTolerance; final bool simplificationHighQuality; - PolygonPainter(this.polygons, this.camera, this.polygonLabels, - this.simplificationTolerance, this.simplificationHighQuality, this.drawLabelsLast) + PolygonPainter( + this.polygons, + this.camera, + this.polygonLabels, + this.simplificationTolerance, + this.simplificationHighQuality, + this.drawLabelsLast) : bounds = camera.visibleBounds; int get hash { diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index fbe5f508a..906814346 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -4,7 +4,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/misc/simplify.dart'; +import 'package:flutter_map/simplify.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -51,9 +51,6 @@ class Polyline { @immutable class PolylineLayer extends StatelessWidget { final List polylines; - @Deprecated( - 'has no effect anymore, polyline culling is enabled by default and controlled by margin') - final bool polylineCulling; /// extent outside of the viewport before culling polylines, set to null to /// disable polyline culling @@ -68,7 +65,6 @@ class PolylineLayer extends StatelessWidget { const PolylineLayer({ super.key, required this.polylines, - this.polylineCulling = true, this.polylineCullingMargin = 0, this.simplificationTolerance = 1, this.simplificationHighQuality = false, From 080846f279f89105be4a2e8ad62d3968eafd123e Mon Sep 17 00:00:00 2001 From: mootw Date: Sun, 12 Nov 2023 17:36:28 -0600 Subject: [PATCH 06/27] organize imports --- 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 aa7c4c7ac..da99121c3 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -2,12 +2,12 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; +import 'package:flutter_map/simplify.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; -import 'package:flutter_map/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { From aa2e37e87903a62a1aec231862f093b89e6be274 Mon Sep 17 00:00:00 2001 From: mootw Date: Mon, 20 Nov 2023 19:36:24 -0600 Subject: [PATCH 07/27] add descriptions and move simplify to be internal --- lib/src/layer/polygon_layer/polygon_layer.dart | 6 ++++-- lib/src/layer/polyline_layer.dart | 6 ++++-- lib/{ => src/misc}/simplify.dart | 3 +-- 3 files changed, 9 insertions(+), 6 deletions(-) rename lib/{ => src/misc}/simplify.dart (99%) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index da99121c3..414872794 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/simplify.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; @@ -113,7 +113,9 @@ class PolygonLayer extends StatelessWidget { /// how much to simplify the polygons, in decimal degrees scaled to floored zoom final double? simplificationTolerance; - /// use high quality simplification + /// high quality simplification uses the https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm + /// otherwise, points within the radial distance of the threshold value are merged. (Also called radial distance simplification) + /// radial distance is faster, but does not preserve the shape of the original line as well as Douglas Peucker final bool simplificationHighQuality; // Turn on/off per-polygon label drawing on the layer-level. diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 906814346..ba86fa6f9 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -4,7 +4,7 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/simplify.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; class Polyline { @@ -59,7 +59,9 @@ class PolylineLayer extends StatelessWidget { /// how much to simplify the polygons, in decimal degrees scaled to floored zoom final double? simplificationTolerance; - /// use high quality simplification + /// high quality simplification uses the https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm + /// otherwise, points within the radial distance of the threshold value are merged. (Also called radial distance simplification) + /// radial distance is faster, but does not preserve the shape of the original line as well as Douglas Peucker final bool simplificationHighQuality; const PolylineLayer({ diff --git a/lib/simplify.dart b/lib/src/misc/simplify.dart similarity index 99% rename from lib/simplify.dart rename to lib/src/misc/simplify.dart index 99b0c3423..9f5fcb9b4 100644 --- a/lib/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -1,8 +1,6 @@ // implementation based on // https://github.com/mourner/simplify-js/blob/master/simplify.js -library simplify; - import 'package:latlong2/latlong.dart'; double _getSqDist( @@ -42,6 +40,7 @@ double _getSqSegDist( return dx * dx + dy * dy; } + List simplifyRadialDist( List points, double sqTolerance, From 8edfb7968e739e8f92bcf5822958a144e0016ae7 Mon Sep 17 00:00:00 2001 From: mootw Date: Mon, 20 Nov 2023 19:40:07 -0600 Subject: [PATCH 08/27] dart format --- lib/src/misc/simplify.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 9f5fcb9b4..7349a4e34 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -40,7 +40,6 @@ double _getSqSegDist( return dx * dx + dy * dy; } - List simplifyRadialDist( List points, double sqTolerance, From 9db6a78a842cbfc4cf8e31cecc410ded916a3094 Mon Sep 17 00:00:00 2001 From: mootw Date: Mon, 20 Nov 2023 19:47:55 -0600 Subject: [PATCH 09/27] fix analysis issues --- lib/src/layer/polygon_layer/polygon_layer.dart | 2 +- lib/src/layer/polyline_layer.dart | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 414872794..e4152c3c5 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -2,12 +2,12 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/misc/simplify.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI enum PolygonLabelPlacement { diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index ba86fa6f9..dadbca7df 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -5,7 +5,6 @@ import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/misc/simplify.dart'; -import 'package:latlong2/latlong.dart'; class Polyline { final List points; From bd97f854143304eba3393b73f2bab2d58173c79d Mon Sep 17 00:00:00 2001 From: mootw Date: Mon, 20 Nov 2023 19:53:36 -0600 Subject: [PATCH 10/27] fix linting error (after upgrading my sdk) --- example/lib/pages/tile_loading_error_handle.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/example/lib/pages/tile_loading_error_handle.dart b/example/lib/pages/tile_loading_error_handle.dart index 228a03169..6c601b214 100644 --- a/example/lib/pages/tile_loading_error_handle.dart +++ b/example/lib/pages/tile_loading_error_handle.dart @@ -104,7 +104,6 @@ class _SimulateErrorImageProvider extends ImageProvider<_SimulateErrorImageProvider> { _SimulateErrorImageProvider(); - @override ImageStreamCompleter load( _SimulateErrorImageProvider key, Future Function( From d1f2265ab9cf66f84101f9044934984b2485adcf Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 21 Nov 2023 09:47:39 +0000 Subject: [PATCH 11/27] Improved polyline example page --- example/lib/pages/polyline.dart | 269 +++++++++++++++---------- example/windows/flutter/CMakeLists.txt | 7 +- 2 files changed, 169 insertions(+), 107 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index a3a463741..4ed8621f6 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -14,7 +14,9 @@ class PolylinePage extends StatefulWidget { } class _PolylinePageState extends State { - List randomWalk = [const LatLng(45, -93.4)]; + final randomWalk = [const LatLng(44.861294, 13.845086)]; + double simplificationTolerance = 1; + bool useHighQualitySimplification = false; @override void initState() { @@ -34,118 +36,173 @@ class _PolylinePageState extends State { return Scaffold( appBar: AppBar(title: const Text('Polylines')), drawer: buildDrawer(context, PolylinePage.route), - body: Padding( - padding: const EdgeInsets.all(8), - child: Column( - children: [ - const Padding( - padding: EdgeInsets.only(top: 8, bottom: 8), - child: Text('Polylines'), + body: Stack( + children: [ + FlutterMap( + options: const MapOptions( + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, ), - Flexible( - child: FlutterMap( - options: const MapOptions( - initialCenter: LatLng(51.5, -0.09), - initialZoom: 5, - ), - children: [ - TileLayer( - urlTemplate: - 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - userAgentPackageName: 'dev.fleaflet.flutter_map.example', + children: [ + TileLayer( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + userAgentPackageName: 'dev.fleaflet.flutter_map.example', + ), + PolylineLayer( + simplificationTolerance: simplificationTolerance == 0 + ? null + : simplificationTolerance, + simplificationHighQuality: useHighQualitySimplification, + polylines: [ + Polyline( + points: randomWalk, + strokeWidth: 3, + color: Colors.deepOrange, ), - PolylineLayer( - polylineCullingMargin: 25, - polylines: [ - Polyline( - points: randomWalk, - strokeWidth: 3, - color: Colors.deepOrange, - ), - Polyline( - points: [ - const LatLng(50, 0), - const LatLng(50, -10), - const LatLng(47, -12), - const LatLng(45, -10), - const LatLng(45, 0), - ], - strokeWidth: 8, - color: Colors.purple, - ), - Polyline( - points: [ - const LatLng(51.5, -0.09), - const LatLng(53.3498, -6.2603), - const LatLng(48.8566, 2.3522), - ], - strokeWidth: 4, - color: Colors.purple, - ), - Polyline( - points: [ - const LatLng(55.5, -0.09), - const LatLng(54.3498, -6.2603), - const LatLng(52.8566, 2.3522), - ], - strokeWidth: 4, - gradientColors: [ - const Color(0xffE40203), - const Color(0xffFEED00), - const Color(0xff007E2D), - ], - ), - Polyline( - points: [ - const LatLng(50.5, -0.09), - const LatLng(51.3498, 6.2603), - const LatLng(53.8566, 2.3522), - ], - strokeWidth: 20, - color: Colors.blue.withOpacity(0.6), - borderStrokeWidth: 20, - borderColor: Colors.red.withOpacity(0.4), - ), - Polyline( - points: [ - const LatLng(50.2, -0.08), - const LatLng(51.2498, -10.2603), - const LatLng(54.8566, -9.3522), - ], - strokeWidth: 20, - color: Colors.black.withOpacity(0.2), - borderStrokeWidth: 20, - borderColor: Colors.white30, - ), - Polyline( - points: [ - const LatLng(49.1, -0.06), - const LatLng(52.15, -1.4), - const LatLng(55.5, 0.8), - ], - strokeWidth: 10, - color: Colors.yellow, - borderStrokeWidth: 10, - borderColor: Colors.blue.withOpacity(0.5), - ), - Polyline( - points: [ - const LatLng(48.1, -0.03), - const LatLng(50.5, -7.8), - const LatLng(56.5, 0.4), - ], - strokeWidth: 10, - color: Colors.amber, - borderStrokeWidth: 10, - borderColor: Colors.blue.withOpacity(0.5), - ), + ], + ), + PolylineLayer( + simplificationTolerance: null, + polylines: [ + Polyline( + points: [ + const LatLng(50, 0), + const LatLng(50, -10), + const LatLng(47, -12), + const LatLng(45, -10), + const LatLng(45, 0), ], + strokeWidth: 8, + color: Colors.purple, + ), + Polyline( + points: [ + const LatLng(51.5, -0.09), + const LatLng(53.3498, -6.2603), + const LatLng(48.8566, 2.3522), + ], + strokeWidth: 4, + color: Colors.purple, + ), + Polyline( + points: [ + const LatLng(55.5, -0.09), + const LatLng(54.3498, -6.2603), + const LatLng(52.8566, 2.3522), + ], + strokeWidth: 4, + gradientColors: [ + const Color(0xffE40203), + const Color(0xffFEED00), + const Color(0xff007E2D), + ], + ), + Polyline( + points: [ + const LatLng(50.5, -0.09), + const LatLng(51.3498, 6.2603), + const LatLng(53.8566, 2.3522), + ], + strokeWidth: 20, + color: Colors.blue.withOpacity(0.6), + borderStrokeWidth: 20, + borderColor: Colors.red.withOpacity(0.4), + ), + Polyline( + points: [ + const LatLng(50.2, -0.08), + const LatLng(51.2498, -10.2603), + const LatLng(54.8566, -9.3522), + ], + strokeWidth: 20, + color: Colors.black.withOpacity(0.2), + borderStrokeWidth: 20, + borderColor: Colors.white30, + ), + Polyline( + points: [ + const LatLng(49.1, -0.06), + const LatLng(52.15, -1.4), + const LatLng(55.5, 0.8), + ], + strokeWidth: 10, + color: Colors.yellow, + borderStrokeWidth: 10, + borderColor: Colors.blue.withOpacity(0.5), + ), + Polyline( + points: [ + const LatLng(48.1, -0.03), + const LatLng(50.5, -7.8), + const LatLng(56.5, 0.4), + ], + strokeWidth: 10, + color: Colors.amber, + borderStrokeWidth: 10, + borderColor: Colors.blue.withOpacity(0.5), ), ], ), + ], + ), + Positioned( + left: 32, + top: 16, + right: 32, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(24), + ), + child: Padding( + padding: MediaQuery.sizeOf(context).width >= 500 + ? const EdgeInsets.symmetric(horizontal: 16, vertical: 0) + : const EdgeInsets.only(left: 12, right: 12, top: 8), + child: Column( + children: [ + if (MediaQuery.sizeOf(context).width < 500) + const Text( + 'Simplification Tolerance', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Row( + children: [ + if (MediaQuery.sizeOf(context).width >= 500) + const Text( + 'Simplification Tolerance', + style: TextStyle(fontWeight: FontWeight.bold), + ), + Expanded( + child: Slider( + value: simplificationTolerance, + onChanged: (v) => + setState(() => simplificationTolerance = v), + min: 0, + max: 3, + divisions: 6, + label: simplificationTolerance == 0 + ? 'Disabled' + : simplificationTolerance.toString(), + ), + ), + IconButton( + onPressed: () => setState( + () => useHighQualitySimplification = + !useHighQualitySimplification, + ), + icon: const Icon(Icons.high_quality_outlined), + selectedIcon: const Icon(Icons.high_quality), + isSelected: useHighQualitySimplification, + tooltip: 'Use High Quality Simplification', + ), + ], + ), + ], + ), + ), ), - ], - ), + ), + ], ), ); } diff --git a/example/windows/flutter/CMakeLists.txt b/example/windows/flutter/CMakeLists.txt index 930d2071a..903f4899d 100644 --- a/example/windows/flutter/CMakeLists.txt +++ b/example/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS From 706ce460209e648324321ea8576ac4308bf9de2d Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 21 Nov 2023 09:53:54 +0000 Subject: [PATCH 12/27] Fix Inno Setup for Windows example app --- windowsApplicationInstallerSetup.iss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/windowsApplicationInstallerSetup.iss b/windowsApplicationInstallerSetup.iss index e61c6d378..ce62377a7 100644 --- a/windowsApplicationInstallerSetup.iss +++ b/windowsApplicationInstallerSetup.iss @@ -62,12 +62,12 @@ Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -; Specify all files within 'build/windows/runner/Release' except 'example.exe' +; Specify all files within 'build/windows/x64/runner/Release' except 'example.exe' [Files] -Source: "example\build\windows\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion -Source: "example\build\windows\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "example\build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\url_launcher_windows_plugin.dll"; DestDir: "{app}"; Flags: ignoreversion +Source: "example\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" From 0d2845070d73f4e5ae5bd6f7333a8bea983509a6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 21 Nov 2023 13:01:56 +0000 Subject: [PATCH 13/27] Minor improvements to example application --- example/lib/pages/polyline.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 4ed8621f6..f04d35425 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -178,14 +178,14 @@ class _PolylinePageState extends State { onChanged: (v) => setState(() => simplificationTolerance = v), min: 0, - max: 3, - divisions: 6, + max: 1.25, + divisions: 125, label: simplificationTolerance == 0 ? 'Disabled' - : simplificationTolerance.toString(), + : simplificationTolerance.toStringAsFixed(2), ), ), - IconButton( + IconButton.filledTonal( onPressed: () => setState( () => useHighQualitySimplification = !useHighQualitySimplification, From aff2dfcf4c1d0c8df0ac28d676175905baefe30b Mon Sep 17 00:00:00 2001 From: mootw Date: Sat, 2 Dec 2023 11:06:02 -0600 Subject: [PATCH 14/27] fix lint --- lib/src/layer/polygon_layer/polygon_layer.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 249d6391f..e4152c3c5 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -6,7 +6,6 @@ import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; -import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; // conflict with Path from UI From 5adede52a001e69e6d4dda6a0feee95ec7b202b9 Mon Sep 17 00:00:00 2001 From: mootw Date: Thu, 4 Jan 2024 19:22:15 -0600 Subject: [PATCH 15/27] simplify before culling --- example/lib/pages/polyline.dart | 25 +++++++++- lib/src/layer/polyline_layer.dart | 76 +++++++++++++++++++------------ 2 files changed, 72 insertions(+), 29 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 92dbc9ff5..1e6667cad 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/misc/tile_providers.dart'; @@ -102,6 +104,20 @@ class _PolylinePageState extends State { ), }; + final randomWalk = [const LatLng(44.861294, 13.845086)]; + + @override + void initState() { + super.initState(); + final random = Random(1234); + for (int i = 1; i < 100000; i++) { + final lat = (random.nextDouble() - 0.5) * 0.001; + final lon = (random.nextDouble() - 0.6) * 0.001; + randomWalk.add(LatLng( + randomWalk[i - 1].latitude + lat, randomWalk[i - 1].longitude + lon)); + } + } + List? hoverLines; @override @@ -158,7 +174,14 @@ class _PolylinePageState extends State { ), child: PolylineLayer( hitNotifier: hitNotifier, - polylines: polylines.keys.followedBy(hoverLines ?? []).toList(), + polylines: [ + Polyline( + points: randomWalk, + strokeWidth: 3, + color: Colors.deepOrange, + ), + ...polylines.keys.followedBy(hoverLines ?? []).toList() + ], ), ), ), diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index bb2254557..65ac9a299 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -105,6 +105,7 @@ class PolylineLayer extends StatelessWidget { /// otherwise, points within the radial distance of the threshold value are merged. (Also called radial distance simplification) /// radial distance is faster, but does not preserve the shape of the original line as well as Douglas Peucker final bool simplificationHighQuality; + /// A notifier to notify when a hit is detected over a/multiple [Polyline]s /// /// To listen for hits, wrap the layer in a standard hit detector widget, such @@ -151,8 +152,32 @@ class PolylineLayer extends StatelessWidget { final renderedLines = []; + + final possiblySimplifiedPolylines = []; + if (simplificationTolerance != null) { + possiblySimplifiedPolylines.addAll(polylines.map((polyline) => Polyline( + points: simplify( + polyline.points, + simplificationTolerance! / + math.pow(2, mapCamera.zoom.floorToDouble()), + highestQuality: simplificationHighQuality), + borderColor: polyline.borderColor, + borderStrokeWidth: polyline.borderStrokeWidth, + color: polyline.color, + colorsStop: polyline.colorsStop, + gradientColors: polyline.gradientColors, + isDotted: polyline.isDotted, + strokeCap: polyline.strokeCap, + strokeJoin: polyline.strokeJoin, + strokeWidth: polyline.strokeWidth, + useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, + ))); + } else { + possiblySimplifiedPolylines.addAll(polylines); + } + if (polylineCullingMargin == null) { - renderedLines.addAll(polylines); + renderedLines.addAll(possiblySimplifiedPolylines); } else { final bounds = mapCamera.visibleBounds; final margin = @@ -165,7 +190,7 @@ class PolylineLayer extends StatelessWidget { LatLng(math.min(90, bounds.northEast.latitude + margin), math.min(180, bounds.northEast.longitude + margin))); - for (final polyline in polylines) { + for (final polyline in possiblySimplifiedPolylines) { // Gradiant poylines do not render identically and cannot be easily segmented if (polyline.gradientColors != null) { renderedLines.add(polyline); @@ -242,18 +267,18 @@ class PolylineLayer extends StatelessWidget { } return MobileLayerTransformer( - child: CustomPaint( - painter: _PolylinePainter( - polylines: renderedLines, - simplificationHighQuality: simplificationHighQuality, - simplificationTolerance: simplificationTolerance, - camera: mapCamera, - hitNotifier: hitNotifier, - minimumHitbox: minimumHitbox, - ), - size: Size(mapCamera.size.x, mapCamera.size.y), - isComplex: true, - )); + child: CustomPaint( + painter: _PolylinePainter( + polylines: renderedLines, + simplificationHighQuality: simplificationHighQuality, + simplificationTolerance: simplificationTolerance, + camera: mapCamera, + hitNotifier: hitNotifier, + minimumHitbox: minimumHitbox, + ), + size: Size(mapCamera.size.x, mapCamera.size.y), + isComplex: true, + )); } } @@ -273,23 +298,18 @@ class _PolylinePainter extends CustomPainter { int get hash => _hash ??= Object.hashAll(polylines); int? _hash; - _PolylinePainter({required this.polylines, - required this.camera, this.simplificationTolerance, - required this.simplificationHighQuality, required this.hitNotifier, - required this.minimumHitbox}) + _PolylinePainter( + {required this.polylines, + required this.camera, + this.simplificationTolerance, + required this.simplificationHighQuality, + required this.hitNotifier, + required this.minimumHitbox}) : bounds = camera.visibleBounds; List getOffsets(Offset origin, List points) { - final List simplifiedPoints; - if (simplificationTolerance != null) { - simplifiedPoints = simplify(points, - simplificationTolerance! / math.pow(2, camera.zoom.floorToDouble()), - highestQuality: simplificationHighQuality); - } else { - simplifiedPoints = points; - } - return List.generate(simplifiedPoints.length, (index) { - return getOffset(origin, simplifiedPoints[index]); + return List.generate(points.length, (index) { + return getOffset(origin, points[index]); }, growable: false); } From 056daf8b273fd4a2c63991917f830c807d597257 Mon Sep 17 00:00:00 2001 From: mootw Date: Thu, 4 Jan 2024 19:22:26 -0600 Subject: [PATCH 16/27] dart format --- lib/src/layer/polyline_layer.dart | 39 +++++++++++++++---------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 65ac9a299..bbf175f1a 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -152,32 +152,31 @@ class PolylineLayer extends StatelessWidget { final renderedLines = []; - final possiblySimplifiedPolylines = []; - if (simplificationTolerance != null) { - possiblySimplifiedPolylines.addAll(polylines.map((polyline) => Polyline( - points: simplify( - polyline.points, - simplificationTolerance! / - math.pow(2, mapCamera.zoom.floorToDouble()), - highestQuality: simplificationHighQuality), - borderColor: polyline.borderColor, - borderStrokeWidth: polyline.borderStrokeWidth, - color: polyline.color, - colorsStop: polyline.colorsStop, - gradientColors: polyline.gradientColors, - isDotted: polyline.isDotted, - strokeCap: polyline.strokeCap, - strokeJoin: polyline.strokeJoin, - strokeWidth: polyline.strokeWidth, - useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, - ))); + if (simplificationTolerance != null) { + possiblySimplifiedPolylines.addAll(polylines.map((polyline) => Polyline( + points: simplify( + polyline.points, + simplificationTolerance! / + math.pow(2, mapCamera.zoom.floorToDouble()), + highestQuality: simplificationHighQuality), + borderColor: polyline.borderColor, + borderStrokeWidth: polyline.borderStrokeWidth, + color: polyline.color, + colorsStop: polyline.colorsStop, + gradientColors: polyline.gradientColors, + isDotted: polyline.isDotted, + strokeCap: polyline.strokeCap, + strokeJoin: polyline.strokeJoin, + strokeWidth: polyline.strokeWidth, + useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, + ))); } else { possiblySimplifiedPolylines.addAll(polylines); } if (polylineCullingMargin == null) { - renderedLines.addAll(possiblySimplifiedPolylines); + renderedLines.addAll(possiblySimplifiedPolylines); } else { final bounds = mapCamera.visibleBounds; final margin = From 0cd8088cdbe9be1c179aea5465cc1d0f0064ee67 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 7 Jan 2024 21:19:10 +0000 Subject: [PATCH 17/27] Added caching of simplification to polylines Removed high quality simplification option (and always use it) Added `Polyline.copyWithNewPoints` method Added temporary performance monitors Minor syntactic improvements Formatted Improved documentation Improved Polylines page in example app --- example/lib/pages/polyline.dart | 206 ++++++--- .../layer/polygon_layer/polygon_layer.dart | 93 ++-- lib/src/layer/polyline_layer.dart | 400 ++++++++++-------- lib/src/misc/simplify.dart | 8 + 4 files changed, 440 insertions(+), 267 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 1e6667cad..19138a580 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -16,9 +16,9 @@ class PolylinePage extends StatefulWidget { } class _PolylinePageState extends State { - final PolylineHitNotifier hitNotifier = ValueNotifier(null); - - final polylines = { + final PolylineHitNotifier _hitNotifier = ValueNotifier(null); + List? _hoverLines; + final _polylines = { Polyline( points: [ const LatLng(51.5, -0.09), @@ -104,85 +104,111 @@ class _PolylinePageState extends State { ), }; - final randomWalk = [const LatLng(44.861294, 13.845086)]; + final _randomWalk = [const LatLng(44.861294, 13.845086)]; + + static const double _initialSimplificationTolerance = 1; + double simplificationTolerance = _initialSimplificationTolerance; @override void initState() { super.initState(); final random = Random(1234); - for (int i = 1; i < 100000; i++) { + for (int i = 1; i < 300000; i++) { final lat = (random.nextDouble() - 0.5) * 0.001; final lon = (random.nextDouble() - 0.6) * 0.001; - randomWalk.add(LatLng( - randomWalk[i - 1].latitude + lat, randomWalk[i - 1].longitude + lon)); + _randomWalk.add( + LatLng( + _randomWalk[i - 1].latitude + lat, + _randomWalk[i - 1].longitude + lon, + ), + ); } } - List? hoverLines; - @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Polylines')), drawer: const MenuDrawer(PolylinePage.route), - body: FlutterMap( - options: const MapOptions( - initialCenter: LatLng(51.5, -0.09), - initialZoom: 5, - ), + body: Stack( children: [ - openStreetMapTileLayer, - MouseRegion( - hitTestBehavior: HitTestBehavior.deferToChild, - cursor: SystemMouseCursors.click, - onHover: (_) { - if (hitNotifier.value == null) return; - - final lines = hitNotifier.value!.lines - .where((e) => polylines.containsKey(e)) - .map( - (e) => Polyline( - points: e.points, - strokeWidth: e.strokeWidth + e.borderStrokeWidth, - color: Colors.transparent, - borderStrokeWidth: 15, - borderColor: Colors.green, - useStrokeWidthInMeter: e.useStrokeWidthInMeter, - ), - ) - .toList(); - setState(() => hoverLines = lines); - }, - - /// Clear hovered lines when touched lines modal appears - onExit: (_) => setState(() => hoverLines = null), - child: GestureDetector( - onTap: () => _openTouchedLinesModal( - 'Tapped', - hitNotifier.value!.lines, - hitNotifier.value!.point, - ), - onLongPress: () => _openTouchedLinesModal( - 'Long pressed', - hitNotifier.value!.lines, - hitNotifier.value!.point, - ), - onSecondaryTap: () => _openTouchedLinesModal( - 'Secondary tapped', - hitNotifier.value!.lines, - hitNotifier.value!.point, + FlutterMap( + options: const MapOptions( + initialCenter: LatLng(51.5, -0.09), + initialZoom: 5, + ), + children: [ + openStreetMapTileLayer, + MouseRegion( + hitTestBehavior: HitTestBehavior.deferToChild, + cursor: SystemMouseCursors.click, + onHover: (_) { + if (_hitNotifier.value == null) return; + + final lines = _hitNotifier.value!.lines + .where((e) => _polylines.containsKey(e)) + .map( + (e) => Polyline( + points: e.points, + strokeWidth: e.strokeWidth + e.borderStrokeWidth, + color: Colors.transparent, + borderStrokeWidth: 15, + borderColor: Colors.green, + useStrokeWidthInMeter: e.useStrokeWidthInMeter, + ), + ) + .toList(); + setState(() => _hoverLines = lines); + }, + + /// Clear hovered lines when touched lines modal appears + onExit: (_) => setState(() => _hoverLines = null), + child: GestureDetector( + onTap: () => _openTouchedLinesModal( + 'Tapped', + _hitNotifier.value!.lines, + _hitNotifier.value!.point, + ), + onLongPress: () => _openTouchedLinesModal( + 'Long pressed', + _hitNotifier.value!.lines, + _hitNotifier.value!.point, + ), + onSecondaryTap: () => _openTouchedLinesModal( + 'Secondary tapped', + _hitNotifier.value!.lines, + _hitNotifier.value!.point, + ), + child: PolylineLayer( + hitNotifier: _hitNotifier, + simplificationTolerance: null, + polylines: + _polylines.keys.followedBy(_hoverLines ?? []).toList(), + ), + ), ), - child: PolylineLayer( - hitNotifier: hitNotifier, + PolylineLayer( + simplificationTolerance: simplificationTolerance == 0 + ? null + : simplificationTolerance, polylines: [ Polyline( - points: randomWalk, + points: _randomWalk, strokeWidth: 3, color: Colors.deepOrange, ), - ...polylines.keys.followedBy(hoverLines ?? []).toList() ], ), + ], + ), + Positioned( + left: 16, + top: 16, + right: 16, + child: SimplificationToleranceSlider( + initialTolerance: _initialSimplificationTolerance, + onChangedTolerance: (v) => + setState(() => simplificationTolerance = v), ), ), ], @@ -195,7 +221,7 @@ class _PolylinePageState extends State { List tappedLines, LatLng coords, ) { - tappedLines.removeWhere((e) => !polylines.containsKey(e)); + tappedLines.removeWhere((e) => !_polylines.containsKey(e)); showModalBottomSheet( context: context, @@ -215,7 +241,7 @@ class _PolylinePageState extends State { Expanded( child: ListView.builder( itemBuilder: (context, index) { - final tappedLineData = polylines[tappedLines[index]]!; + final tappedLineData = _polylines[tappedLines[index]]!; return ListTile( leading: index == 0 ? const Icon(Icons.vertical_align_top) @@ -247,3 +273,63 @@ class _PolylinePageState extends State { ); } } + +class SimplificationToleranceSlider extends StatefulWidget { + const SimplificationToleranceSlider({ + super.key, + required this.initialTolerance, + required this.onChangedTolerance, + }); + + final double initialTolerance; + final void Function(double) onChangedTolerance; + + @override + State createState() => + _SimplificationToleranceSliderState(); +} + +class _SimplificationToleranceSliderState + extends State { + late double _simplificationTolerance = widget.initialTolerance; + + @override + Widget build(BuildContext context) { + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.background, + borderRadius: BorderRadius.circular(32), + ), + child: Padding( + padding: const EdgeInsets.only(left: 16, right: 8, top: 4, bottom: 4), + child: Row( + children: [ + const Tooltip( + message: 'Adjust Simplification Tolerance', + child: Row( + children: [ + Icon(Icons.insights), + SizedBox(width: 8), + Icon(Icons.hdr_strong), + ], + ), + ), + Expanded( + child: Slider( + value: _simplificationTolerance, + onChanged: (v) => setState(() => _simplificationTolerance = v), + onChangeEnd: widget.onChangedTolerance, + min: 0, + max: 2.5, + divisions: 125, + label: _simplificationTolerance == 0 + ? 'Disabled' + : _simplificationTolerance.toStringAsFixed(2), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index e4152c3c5..b3ac126cd 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -105,51 +105,67 @@ class Polygon { @immutable class PolygonLayer extends StatelessWidget { + /// [Polygon]s to draw final List polygons; - /// screen space culling of polygons based on bounding box + /// Whether to cull polygons and polygon sections that are outside of the + /// viewport + /// + /// Defaults to `true`. final bool polygonCulling; - /// how much to simplify the polygons, in decimal degrees scaled to floored zoom + /// Distance between two mergeable polygon points, in decimal degrees scaled + /// to floored zoom + /// + /// Increasing results in a more jagged, less accurate simplification, with + /// improved performance; and vice versa. + /// + /// Note that this value is internally scaled using the current map zoom, to + /// optimize visual performance in conjunction with improved performance with + /// culling. + /// + /// Defaults to 1. final double? simplificationTolerance; - /// high quality simplification uses the https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm - /// otherwise, points within the radial distance of the threshold value are merged. (Also called radial distance simplification) - /// radial distance is faster, but does not preserve the shape of the original line as well as Douglas Peucker - final bool simplificationHighQuality; - - // Turn on/off per-polygon label drawing on the layer-level. + /// Whether to draw per-polygon labels + /// + /// Defaults to `true`. final bool polygonLabels; - // Whether to draw labels last and thus over all the polygons. + /// Whether to draw labels last and thus over all the polygons + /// + /// Defaults to `false`. final bool drawLabelsLast; const PolygonLayer({ super.key, required this.polygons, - this.polygonCulling = false, + this.polygonCulling = true, this.simplificationTolerance = 1, - this.simplificationHighQuality = false, this.polygonLabels = true, this.drawLabelsLast = false, }); @override Widget build(BuildContext context) { - final map = MapCamera.of(context); - final size = Size(map.size.x, map.size.y); + final camera = MapCamera.of(context); - final polygonsToRender = polygonCulling - ? polygons.where((p) { - return p.boundingBox.isOverlapping(map.visibleBounds); - }).toList() + final culledPolygons = polygonCulling + ? polygons + .where((p) => p.boundingBox.isOverlapping(camera.visibleBounds)) + .toList() : polygons; return MobileLayerTransformer( child: CustomPaint( - painter: PolygonPainter(polygonsToRender, map, polygonLabels, - simplificationTolerance, simplificationHighQuality, drawLabelsLast), - size: size, + painter: PolygonPainter( + polygons: culledPolygons, + camera: camera, + polygonLabels: polygonLabels, + drawLabelsLast: drawLabelsLast, + simplificationTolerance: simplificationTolerance, + ), + size: Size(camera.size.x, camera.size.y), isComplex: true, ), ); @@ -162,18 +178,15 @@ class PolygonPainter extends CustomPainter { final LatLngBounds bounds; final bool polygonLabels; final bool drawLabelsLast; - final double? simplificationTolerance; - final bool simplificationHighQuality; - PolygonPainter( - this.polygons, - this.camera, - this.polygonLabels, - this.simplificationTolerance, - this.simplificationHighQuality, - this.drawLabelsLast) - : bounds = camera.visibleBounds; + PolygonPainter({ + required this.polygons, + required this.camera, + required this.polygonLabels, + required this.simplificationTolerance, + required this.drawLabelsLast, + }) : bounds = camera.visibleBounds; int get hash { _hash ??= Object.hashAll(polygons); @@ -197,19 +210,17 @@ class PolygonPainter extends CustomPainter { } List getOffsets(Offset origin, List points) { - final List renderedPoints; - if (simplificationTolerance != null) { - renderedPoints = simplify(points, - simplificationTolerance! / pow(2, camera.zoom.floorToDouble()), - highestQuality: simplificationHighQuality); - } else { - renderedPoints = points; - } + final renderedPoints = simplificationTolerance != null + ? simplify( + points, + simplificationTolerance! / pow(2, camera.zoom.floor()), + highestQuality: true, + ) + : points; + return List.generate( renderedPoints.length, - (index) { - return getOffset(origin, renderedPoints[index]); - }, + (index) => getOffset(origin, renderedPoints[index]), growable: false, ); } diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index bbf175f1a..5a6735b49 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -56,6 +56,20 @@ class Polyline { this.useStrokeWidthInMeter = false, }); + Polyline copyWithNewPoints(List points) => Polyline( + points: points, + strokeWidth: strokeWidth, + color: color, + borderStrokeWidth: borderStrokeWidth, + borderColor: borderColor, + gradientColors: gradientColors, + colorsStop: colorsStop, + isDotted: isDotted, + strokeCap: strokeCap, + strokeJoin: strokeJoin, + useStrokeWidthInMeter: useStrokeWidthInMeter, + ); + @override bool operator ==(Object other) => identical(this, other) || @@ -91,40 +105,45 @@ class Polyline { } @immutable -class PolylineLayer extends StatelessWidget { +class PolylineLayer extends StatefulWidget { + /// [Polyline]s to draw final List polylines; - /// extent outside of the viewport before culling polylines, set to null to - /// disable polyline culling - final double? polylineCullingMargin; + /// Acceptable extent outside of viewport before culling polyline segments + /// + /// May need to be increased if the [Polyline.borderStrokeWidth] is large. + /// + /// Defaults to 0: cull aggressively. Set to `null` to disable culling. + final double? cullingMargin; - /// how much to simplify the polygons, in decimal degrees scaled to floored zoom + /// Distance between two mergeable polyline points, in decimal degrees scaled + /// to floored zoom + /// + /// Increasing results in a more jagged, less accurate simplification, with + /// improved performance; and vice versa. + /// + /// Note that this value is internally scaled using the current map zoom, to + /// optimize visual performance in conjunction with improved performance with + /// culling. + /// + /// Defaults to 1. Set to `null` to disable simplification. final double? simplificationTolerance; - /// high quality simplification uses the https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm - /// otherwise, points within the radial distance of the threshold value are merged. (Also called radial distance simplification) - /// radial distance is faster, but does not preserve the shape of the original line as well as Douglas Peucker - final bool simplificationHighQuality; - - /// A notifier to notify when a hit is detected over a/multiple [Polyline]s + /// A notifier to be notified when a hit test occurs on the layer + /// + /// Notified with a [PolylineHit] if any [Polyline]s are hit, otherwise + /// notified with `null`. /// - /// To listen for hits, wrap the layer in a standard hit detector widget, such - /// as [GestureDetector] and/or [MouseRegion] (and set - /// [HitTestBehavior.deferToChild] if necessary). Then use the latest value - /// (via [ValueNotifier.value]) in the detector's callbacks. It is also - /// possible to listen to the notifier directly. + /// Note that a hover event is included as a hit event. If an expensive + /// operation is required on hover, check for equality between the new and old + /// [PolylineHit.lines], and avoid doing heavy work if they are the same. /// - /// Note that a hover event is included as a hit event. Therefore for - /// performance reasons, it may be advantageous to check the new value's - /// equality against the previous value (excluding the [PolylineHit.point], - /// which will always change), and avoid doing any heavy work if they are the - /// same. + /// Note that testing is performed on the visual, simplified polyline. + /// + /// If a notifier is not provided, hit testing is not performed. /// /// See online documentation for more detailed usage instructions. See the /// example project for an example implementation. - /// - /// Will notify with [PolylineHit]s when any [Polyline]s are hit, otherwise - /// will notify with `null`. final PolylineHitNotifier? hitNotifier; /// The minimum radius of the hittable area around each [Polyline] in logical @@ -139,146 +158,200 @@ class PolylineLayer extends StatelessWidget { const PolylineLayer({ super.key, required this.polylines, - this.polylineCullingMargin = 0, + this.cullingMargin = 0, this.simplificationTolerance = 1, - this.simplificationHighQuality = false, this.hitNotifier, this.minimumHitbox = 10, }); @override - Widget build(BuildContext context) { - final mapCamera = MapCamera.of(context); - - final renderedLines = []; - - final possiblySimplifiedPolylines = []; - if (simplificationTolerance != null) { - possiblySimplifiedPolylines.addAll(polylines.map((polyline) => Polyline( - points: simplify( - polyline.points, - simplificationTolerance! / - math.pow(2, mapCamera.zoom.floorToDouble()), - highestQuality: simplificationHighQuality), - borderColor: polyline.borderColor, - borderStrokeWidth: polyline.borderStrokeWidth, - color: polyline.color, - colorsStop: polyline.colorsStop, - gradientColors: polyline.gradientColors, - isDotted: polyline.isDotted, - strokeCap: polyline.strokeCap, - strokeJoin: polyline.strokeJoin, - strokeWidth: polyline.strokeWidth, - useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, - ))); - } else { - possiblySimplifiedPolylines.addAll(polylines); + State createState() => _PolylineLayerState(); +} + +// TODO: This does not work correctly when multiple `PolylineLayer`s are in use, +// as they all share one state. For some reason, I couldn't even get them to +// use seperate states with different `ValueKey`s +class _PolylineLayerState extends State { + final _cachedSimplifiedPolylines = >{}; + + @override + void didUpdateWidget(PolylineLayer oldWidget) { + super.didUpdateWidget(oldWidget); + if (!listEquals(oldWidget.polylines, widget.polylines) || + oldWidget.simplificationTolerance != widget.simplificationTolerance) { + print('clear cache'); + _cachedSimplifiedPolylines.clear(); + _precomputeSimplification(); } + } + + @override + void initState() { + super.initState(); + _precomputeSimplification(); + } + + // Pre-compute simplified polylines for each zoom level 0-21 in an isolate + // on non-web platforms only + void _precomputeSimplification() { + if (widget.simplificationTolerance == null || kIsWeb) return; + + print('started simplification precomputation'); + final stopwatch = Stopwatch()..start(); + + compute( + (msg) => List.generate( + 22, + (zoom) => msg.polylines + .map( + (polyline) => polyline.copyWithNewPoints( + simplify( + polyline.points, + msg.simplificationTolerance! / math.pow(2, zoom), + highestQuality: true, + ), + ), + ) + .toList(), + growable: false, + ).asMap(), + ( + polylines: widget.polylines, + simplificationTolerance: widget.simplificationTolerance, + ), + debugLabel: '[FM] Polyline Simplification Precomputer', + ).then(_cachedSimplifiedPolylines.addAll).then((_) { + stopwatch.stop(); + print('simplification precomp took ${stopwatch.elapsedMicroseconds}us'); + }); + } + + @override + Widget build(BuildContext context) { + final camera = MapCamera.of(context); + + final stopwatch = Stopwatch()..start(); + + final child = MobileLayerTransformer( + child: CustomPaint( + painter: _PolylinePainter( + polylines: _cullPolylines( + polylines: widget.simplificationTolerance == null + ? widget.polylines + : _cachedSimplifiedPolylines[camera.zoom.floor()] ??= + widget.polylines + .map( + (polyline) => polyline.copyWithNewPoints( + simplify( + polyline.points, + widget.simplificationTolerance! / + math.pow(2, camera.zoom.floor()), + highestQuality: true, + ), + ), + ) + .toList(), + camera: camera, + cullingMargin: widget.cullingMargin, + ), + camera: camera, + hitNotifier: widget.hitNotifier, + minimumHitbox: widget.minimumHitbox, + ), + size: Size(camera.size.x, camera.size.y), + isComplex: true, + ), + ); + + stopwatch.stop(); + print('`build` took ${stopwatch.elapsedMicroseconds}us'); + + return child; + } +} - if (polylineCullingMargin == null) { - renderedLines.addAll(possiblySimplifiedPolylines); - } else { - final bounds = mapCamera.visibleBounds; - final margin = - polylineCullingMargin! / math.pow(2, mapCamera.zoom.floorToDouble()); - // The min(-90), max(180), etc.. are used to get around the limits of LatLng - // the value cannot be greater or smaller than that - final boundsAdjusted = LatLngBounds( - LatLng(math.max(-90, bounds.southWest.latitude - margin), - math.max(-180, bounds.southWest.longitude - margin)), - LatLng(math.min(90, bounds.northEast.latitude + margin), - math.min(180, bounds.northEast.longitude + margin))); - - for (final polyline in possiblySimplifiedPolylines) { - // Gradiant poylines do not render identically and cannot be easily segmented - if (polyline.gradientColors != null) { - renderedLines.add(polyline); - continue; +List _cullPolylines({ + required List polylines, + required MapCamera camera, + required double? cullingMargin, +}) { + if (cullingMargin == null) return polylines; + + final stopwatch = Stopwatch()..start(); + + final culledPolylines = []; + + final bounds = camera.visibleBounds; + final margin = cullingMargin / math.pow(2, camera.zoom.floorToDouble()); + // The min(-90), max(180), ... are used to get around the limits of LatLng + // the value cannot be greater or smaller than that + final boundsAdjusted = LatLngBounds( + LatLng( + math.max(-90, bounds.southWest.latitude - margin), + math.max(-180, bounds.southWest.longitude - margin), + ), + LatLng( + math.min(90, bounds.northEast.latitude + margin), + math.min(180, bounds.northEast.longitude + margin), + ), + ); + + for (final polyline in polylines) { + // Gradient poylines cannot be easily segmented + if (polyline.gradientColors != null) { + culledPolylines.add(polyline); + continue; + } + // pointer that indicates the start of the visible polyline segment + int start = -1; + bool fullyVisible = true; + for (int i = 0; i < polyline.points.length - 1; i++) { + //current pair + final p1 = polyline.points[i]; + final p2 = polyline.points[i + 1]; + + // segment is visible + if (Bounds( + math.Point( + boundsAdjusted.southWest.longitude, + boundsAdjusted.southWest.latitude, + ), + math.Point( + boundsAdjusted.northEast.longitude, + boundsAdjusted.northEast.latitude, + ), + ).aabbContainsLine( + p1.longitude, p1.latitude, p2.longitude, p2.latitude)) { + // segment is visible + if (start == -1) { + start = i; + } + if (!fullyVisible && i == polyline.points.length - 2) { + final segment = polyline.points.sublist(start, i + 2); + culledPolylines.add(polyline.copyWithNewPoints(segment)); } - // pointer that indicates the start of the visible polyline segment - int start = -1; - bool fullyVisible = true; - for (int i = 0; i < polyline.points.length - 1; i++) { - //current pair - final p1 = polyline.points[i]; - final p2 = polyline.points[i + 1]; - - // segment is visible - if (Bounds( - math.Point(boundsAdjusted.southWest.longitude, - boundsAdjusted.southWest.latitude), - math.Point(boundsAdjusted.northEast.longitude, - boundsAdjusted.northEast.latitude)) - .aabbContainsLine( - p1.longitude, p1.latitude, p2.longitude, p2.latitude)) { - // segment is visible - if (start == -1) { - start = i; - } - if (!fullyVisible && i == polyline.points.length - 2) { - final segment = polyline.points.sublist(start, i + 2); - renderedLines.add(Polyline( - points: segment, - borderColor: polyline.borderColor, - borderStrokeWidth: polyline.borderStrokeWidth, - color: polyline.color, - colorsStop: polyline.colorsStop, - gradientColors: polyline.gradientColors, - isDotted: polyline.isDotted, - strokeCap: polyline.strokeCap, - strokeJoin: polyline.strokeJoin, - strokeWidth: polyline.strokeWidth, - useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, - )); - } - } else { - fullyVisible = false; - // if we cannot see the segment, then reset start - if (start != -1) { - // partial start - final segment = polyline.points.sublist(start, i + 1); - // TODO copyWith method for polyline - renderedLines.add(Polyline( - points: segment, - borderColor: polyline.borderColor, - borderStrokeWidth: polyline.borderStrokeWidth, - color: polyline.color, - colorsStop: polyline.colorsStop, - gradientColors: polyline.gradientColors, - isDotted: polyline.isDotted, - strokeCap: polyline.strokeCap, - strokeJoin: polyline.strokeJoin, - strokeWidth: polyline.strokeWidth, - useStrokeWidthInMeter: polyline.useStrokeWidthInMeter, - )); - start = -1; - } - if (start != -1) { - start = i; - } - } + } else { + fullyVisible = false; + // if we cannot see the segment, then reset start + if (start != -1) { + // partial start + final segment = polyline.points.sublist(start, i + 1); + culledPolylines.add(polyline.copyWithNewPoints(segment)); + start = -1; } - if (fullyVisible) { - //The whole polyline is visible - renderedLines.add(polyline); + if (start != -1) { + start = i; } } } - return MobileLayerTransformer( - child: CustomPaint( - painter: _PolylinePainter( - polylines: renderedLines, - simplificationHighQuality: simplificationHighQuality, - simplificationTolerance: simplificationTolerance, - camera: mapCamera, - hitNotifier: hitNotifier, - minimumHitbox: minimumHitbox, - ), - size: Size(mapCamera.size.x, mapCamera.size.y), - isComplex: true, - )); + if (fullyVisible) culledPolylines.add(polyline); } + + stopwatch.stop(); + print('`cull` took ${stopwatch.elapsedMicroseconds}us'); + + return culledPolylines; } class _PolylinePainter extends CustomPainter { @@ -288,29 +361,24 @@ class _PolylinePainter extends CustomPainter { final PolylineHitNotifier? hitNotifier; final double minimumHitbox; - final double? simplificationTolerance; - final bool simplificationHighQuality; - // Avoids reallocation on every `hitTest`, is cleared every time final hits = List.empty(growable: true); int get hash => _hash ??= Object.hashAll(polylines); int? _hash; - _PolylinePainter( - {required this.polylines, - required this.camera, - this.simplificationTolerance, - required this.simplificationHighQuality, - required this.hitNotifier, - required this.minimumHitbox}) - : bounds = camera.visibleBounds; - - List getOffsets(Offset origin, List points) { - return List.generate(points.length, (index) { - return getOffset(origin, points[index]); - }, growable: false); - } + _PolylinePainter({ + required this.polylines, + required this.camera, + required this.hitNotifier, + required this.minimumHitbox, + }) : bounds = camera.visibleBounds; + + List getOffsets(Offset origin, List points) => List.generate( + points.length, + (index) => getOffset(origin, points[index]), + growable: false, + ); Offset getOffset(Offset origin, LatLng point) { // Critically create as little garbage as possible. This is called on every frame. diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 7349a4e34..fbc05079a 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -107,6 +107,8 @@ List simplify( double tolerance, { bool highestQuality = false, }) { + final stopwatch = Stopwatch()..start(); + if (points.length <= 2) { return points; } @@ -115,5 +117,11 @@ List simplify( nextPoints = highestQuality ? points : simplifyRadialDist(nextPoints, sqTolerance); nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); + + stopwatch.stop(); + print( + '`simplify` took ${stopwatch.elapsedMicroseconds}us, and outputted ${nextPoints.length} points (~${((1 - (nextPoints.length / points.length)) * 100).toStringAsFixed(2)}% reduction)', + ); + return nextPoints; } From cd4cc0a1a6eb2fa020a7ee9ead56b599bf18532f Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 9 Jan 2024 22:09:14 +0000 Subject: [PATCH 18/27] Fixed issue where `Polyline.hashCode`s were improperly calculated, leading to multiple issues when hit testing was used in conjunction with simplification PARTIALLY fixed issue where simplified and culled `Polyline`s would be notified from hit testing instead of original, leading to multiple issues with the recommended documented method of interactivity handling Improved `_PolylineLayerState.didUpdateWIdget` logic efficiency Improved documentation Removed debugging/performance code Improved Polyline interaction example --- example/lib/pages/polyline.dart | 31 +++- lib/src/layer/polyline_layer.dart | 248 ++++++++++++++++-------------- lib/src/misc/simplify.dart | 10 +- 3 files changed, 155 insertions(+), 134 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 19138a580..091917313 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map_example/misc/tile_providers.dart'; @@ -17,7 +18,9 @@ class PolylinePage extends StatefulWidget { class _PolylinePageState extends State { final PolylineHitNotifier _hitNotifier = ValueNotifier(null); + List? _prevHitLines; List? _hoverLines; + final _polylines = { Polyline( points: [ @@ -143,10 +146,18 @@ class _PolylinePageState extends State { hitTestBehavior: HitTestBehavior.deferToChild, cursor: SystemMouseCursors.click, onHover: (_) { - if (_hitNotifier.value == null) return; - - final lines = _hitNotifier.value!.lines + // Filter out hover outlines, and ignore if no lines were hit + final hitLines = _hitNotifier.value?.lines .where((e) => _polylines.containsKey(e)) + .toList(); + if (hitLines == null) return; + + // Avoid unnecessary rebuilds if no new lines were hit + if (listEquals(hitLines, _prevHitLines)) return; + _prevHitLines = hitLines; + + // Create hover outlines and add them to the map + final hoverLines = hitLines .map( (e) => Polyline( points: e.points, @@ -158,10 +169,9 @@ class _PolylinePageState extends State { ), ) .toList(); - setState(() => _hoverLines = lines); + setState(() => _hoverLines = hoverLines); }, - - /// Clear hovered lines when touched lines modal appears + // Clear hovered lines when touched lines modal appears onExit: (_) => setState(() => _hoverLines = null), child: GestureDetector( onTap: () => _openTouchedLinesModal( @@ -181,7 +191,7 @@ class _PolylinePageState extends State { ), child: PolylineLayer( hitNotifier: _hitNotifier, - simplificationTolerance: null, + //simplificationTolerance: null, polylines: _polylines.keys.followedBy(_hoverLines ?? []).toList(), ), @@ -317,7 +327,12 @@ class _SimplificationToleranceSliderState Expanded( child: Slider( value: _simplificationTolerance, - onChanged: (v) => setState(() => _simplificationTolerance = v), + onChanged: (v) { + if (_simplificationTolerance == 0 && v != 0) { + widget.onChangedTolerance(v); + } + setState(() => _simplificationTolerance = v); + }, onChangeEnd: widget.onChangedTolerance, min: 0, max: 2.5, diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index 5a6735b49..a1b292d73 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -101,7 +101,7 @@ class Polyline { ); @override - int get hashCode => Object.hash(points, renderHashCode); + int get hashCode => Object.hashAll([...points, renderHashCode]); } @immutable @@ -126,11 +126,15 @@ class PolylineLayer extends StatefulWidget { /// optimize visual performance in conjunction with improved performance with /// culling. /// + /// {@macro polyline.hitNotifier.simplificationWarning} + /// /// Defaults to 1. Set to `null` to disable simplification. final double? simplificationTolerance; /// A notifier to be notified when a hit test occurs on the layer /// + /// If a notifier is not provided, hit testing is not performed. + /// /// Notified with a [PolylineHit] if any [Polyline]s are hit, otherwise /// notified with `null`. /// @@ -138,9 +142,13 @@ class PolylineLayer extends StatefulWidget { /// operation is required on hover, check for equality between the new and old /// [PolylineHit.lines], and avoid doing heavy work if they are the same. /// - /// Note that testing is performed on the visual, simplified polyline. - /// - /// If a notifier is not provided, hit testing is not performed. + /// {@template polyline.hitNotifier.simplificationWarning} + /// If hit testing is enabled with simplification, testing is performed on the + /// visual, simplified polyline. If a line is hit, the non-simplified original + /// line is sent within [PolylineHit.lines]. This does incur extra memory + /// overhead, as both the original and simplified lines must be sent to the + /// painter. + /// {@endtemplate} /// /// See online documentation for more detailed usage instructions. See the /// example project for an example implementation. @@ -168,19 +176,30 @@ class PolylineLayer extends StatefulWidget { State createState() => _PolylineLayerState(); } -// TODO: This does not work correctly when multiple `PolylineLayer`s are in use, -// as they all share one state. For some reason, I couldn't even get them to -// use seperate states with different `ValueKey`s class _PolylineLayerState extends State { final _cachedSimplifiedPolylines = >{}; @override void didUpdateWidget(PolylineLayer oldWidget) { super.didUpdateWidget(oldWidget); - if (!listEquals(oldWidget.polylines, widget.polylines) || - oldWidget.simplificationTolerance != widget.simplificationTolerance) { - print('clear cache'); + + // IF old yes & new no, clear + // IF old no & new yes, precompute + // IF old no & new no, nothing + // IF old yes & new yes & (different tolerance | different lines), both + // otherwise, nothing + if (oldWidget.simplificationTolerance != null && + widget.simplificationTolerance != null && + (!listEquals(oldWidget.polylines, widget.polylines) || + oldWidget.simplificationTolerance != + widget.simplificationTolerance)) { + _cachedSimplifiedPolylines.clear(); + _precomputeSimplification(); + } else if (oldWidget.simplificationTolerance != null && + widget.simplificationTolerance == null) { _cachedSimplifiedPolylines.clear(); + } else if (oldWidget.simplificationTolerance == null && + widget.simplificationTolerance != null) { _precomputeSimplification(); } } @@ -196,9 +215,6 @@ class _PolylineLayerState extends State { void _precomputeSimplification() { if (widget.simplificationTolerance == null || kIsWeb) return; - print('started simplification precomputation'); - final stopwatch = Stopwatch()..start(); - compute( (msg) => List.generate( 22, @@ -220,22 +236,17 @@ class _PolylineLayerState extends State { simplificationTolerance: widget.simplificationTolerance, ), debugLabel: '[FM] Polyline Simplification Precomputer', - ).then(_cachedSimplifiedPolylines.addAll).then((_) { - stopwatch.stop(); - print('simplification precomp took ${stopwatch.elapsedMicroseconds}us'); - }); + ).then(_cachedSimplifiedPolylines.addAll); } @override Widget build(BuildContext context) { final camera = MapCamera.of(context); - final stopwatch = Stopwatch()..start(); - - final child = MobileLayerTransformer( + return MobileLayerTransformer( child: CustomPaint( painter: _PolylinePainter( - polylines: _cullPolylines( + polylines: _aggressivelyCullPolylines( polylines: widget.simplificationTolerance == null ? widget.polylines : _cachedSimplifiedPolylines[camera.zoom.floor()] ??= @@ -254,104 +265,101 @@ class _PolylineLayerState extends State { camera: camera, cullingMargin: widget.cullingMargin, ), + // TODO: These must also be culled! Or we need to recommend and + // implement a different method of retrieving the original polyline + // from the hit result by the user. The second is preferable, as the + // first will start incurring too much complexity again. + originalTappableReturnablePolylines: widget.hitNotifier != null && + widget.simplificationTolerance != null + ? widget.polylines + : null, camera: camera, hitNotifier: widget.hitNotifier, minimumHitbox: widget.minimumHitbox, ), size: Size(camera.size.x, camera.size.y), - isComplex: true, ), ); + } - stopwatch.stop(); - print('`build` took ${stopwatch.elapsedMicroseconds}us'); + List _aggressivelyCullPolylines({ + required List polylines, + required MapCamera camera, + required double? cullingMargin, + }) { + if (cullingMargin == null) return polylines; + + final culledPolylines = []; + + final bounds = camera.visibleBounds; + final margin = cullingMargin / math.pow(2, camera.zoom.floorToDouble()); + // The min(-90), max(180), ... are used to get around the limits of LatLng + // the value cannot be greater or smaller than that + final boundsAdjusted = LatLngBounds( + LatLng( + math.max(-90, bounds.southWest.latitude - margin), + math.max(-180, bounds.southWest.longitude - margin), + ), + LatLng( + math.min(90, bounds.northEast.latitude + margin), + math.min(180, bounds.northEast.longitude + margin), + ), + ); - return child; - } -} + for (final polyline in polylines) { + // Gradient poylines cannot be easily segmented + if (polyline.gradientColors != null) { + culledPolylines.add(polyline); + continue; + } + // pointer that indicates the start of the visible polyline segment + int start = -1; + bool fullyVisible = true; + for (int i = 0; i < polyline.points.length - 1; i++) { + //current pair + final p1 = polyline.points[i]; + final p2 = polyline.points[i + 1]; -List _cullPolylines({ - required List polylines, - required MapCamera camera, - required double? cullingMargin, -}) { - if (cullingMargin == null) return polylines; - - final stopwatch = Stopwatch()..start(); - - final culledPolylines = []; - - final bounds = camera.visibleBounds; - final margin = cullingMargin / math.pow(2, camera.zoom.floorToDouble()); - // The min(-90), max(180), ... are used to get around the limits of LatLng - // the value cannot be greater or smaller than that - final boundsAdjusted = LatLngBounds( - LatLng( - math.max(-90, bounds.southWest.latitude - margin), - math.max(-180, bounds.southWest.longitude - margin), - ), - LatLng( - math.min(90, bounds.northEast.latitude + margin), - math.min(180, bounds.northEast.longitude + margin), - ), - ); - - for (final polyline in polylines) { - // Gradient poylines cannot be easily segmented - if (polyline.gradientColors != null) { - culledPolylines.add(polyline); - continue; - } - // pointer that indicates the start of the visible polyline segment - int start = -1; - bool fullyVisible = true; - for (int i = 0; i < polyline.points.length - 1; i++) { - //current pair - final p1 = polyline.points[i]; - final p2 = polyline.points[i + 1]; - - // segment is visible - if (Bounds( - math.Point( - boundsAdjusted.southWest.longitude, - boundsAdjusted.southWest.latitude, - ), - math.Point( - boundsAdjusted.northEast.longitude, - boundsAdjusted.northEast.latitude, - ), - ).aabbContainsLine( - p1.longitude, p1.latitude, p2.longitude, p2.latitude)) { // segment is visible - if (start == -1) { - start = i; - } - if (!fullyVisible && i == polyline.points.length - 2) { - final segment = polyline.points.sublist(start, i + 2); - culledPolylines.add(polyline.copyWithNewPoints(segment)); - } - } else { - fullyVisible = false; - // if we cannot see the segment, then reset start - if (start != -1) { - // partial start - final segment = polyline.points.sublist(start, i + 1); - culledPolylines.add(polyline.copyWithNewPoints(segment)); - start = -1; - } - if (start != -1) { - start = i; + if (Bounds( + math.Point( + boundsAdjusted.southWest.longitude, + boundsAdjusted.southWest.latitude, + ), + math.Point( + boundsAdjusted.northEast.longitude, + boundsAdjusted.northEast.latitude, + ), + ).aabbContainsLine( + p1.longitude, p1.latitude, p2.longitude, p2.latitude)) { + // segment is visible + if (start == -1) { + start = i; + } + if (!fullyVisible && i == polyline.points.length - 2) { + final segment = polyline.points.sublist(start, i + 2); + culledPolylines.add(polyline.copyWithNewPoints(segment)); + } + } else { + fullyVisible = false; + // if we cannot see the segment, then reset start + if (start != -1) { + // partial start + final segment = polyline.points.sublist(start, i + 1); + culledPolylines.add(polyline.copyWithNewPoints(segment)); + start = -1; + } + if (start != -1) { + start = i; + } } } + + if (fullyVisible) culledPolylines.add(polyline); } - if (fullyVisible) culledPolylines.add(polyline); + return culledPolylines; } - - stopwatch.stop(); - print('`cull` took ${stopwatch.elapsedMicroseconds}us'); - - return culledPolylines; } class _PolylinePainter extends CustomPainter { @@ -361,6 +369,9 @@ class _PolylinePainter extends CustomPainter { final PolylineHitNotifier? hitNotifier; final double minimumHitbox; + /// {@macro polyline.hitNotifier.simplificationWarning} + final List? originalTappableReturnablePolylines; + // Avoids reallocation on every `hitTest`, is cleared every time final hits = List.empty(growable: true); @@ -369,6 +380,7 @@ class _PolylinePainter extends CustomPainter { _PolylinePainter({ required this.polylines, + required this.originalTappableReturnablePolylines, required this.camera, required this.hitNotifier, required this.minimumHitbox, @@ -395,7 +407,10 @@ class _PolylinePainter extends CustomPainter { final origin = camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; - for (final p in polylines.reversed) { + int polylineIndex = polylines.length; + for (final polyline in polylines.reversed) { + polylineIndex--; + // TODO: For efficiency we'd ideally filter by bounding box here. However // we'd need to compute an extended bounding box that accounts account for // the stroke width. @@ -403,17 +418,19 @@ class _PolylinePainter extends CustomPainter { // continue; // } - final offsets = getOffsets(origin, p.points); - final strokeWidth = p.useStrokeWidthInMeter + final offsets = getOffsets(origin, polyline.points); + final strokeWidth = polyline.useStrokeWidthInMeter ? _metersToStrokeWidth( origin, - p.points.first, + polyline.points.first, offsets.first, - p.strokeWidth, + polyline.strokeWidth, ) - : p.strokeWidth; - final hittableDistance = - math.max(strokeWidth / 2 + p.borderStrokeWidth / 2, minimumHitbox); + : polyline.strokeWidth; + final hittableDistance = math.max( + strokeWidth / 2 + polyline.borderStrokeWidth / 2, + minimumHitbox, + ); for (int i = 0; i < offsets.length - 1; i++) { final o1 = offsets[i]; @@ -429,7 +446,8 @@ class _PolylinePainter extends CustomPainter { )); if (distance < hittableDistance) { - hits.add(p); + hits.add( + originalTappableReturnablePolylines?[polylineIndex] ?? polyline); break; } } @@ -634,11 +652,7 @@ class _PolylinePainter extends CustomPainter { } @override - bool shouldRepaint(_PolylinePainter oldDelegate) { - return oldDelegate.bounds != bounds || - oldDelegate.polylines.length != polylines.length || - oldDelegate.hash != hash; - } + bool shouldRepaint(_PolylinePainter oldDelegate) => true; } double _distanceSq(double x0, double y0, double x1, double y1) { diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index fbc05079a..95bdd487a 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -107,21 +107,13 @@ List simplify( double tolerance, { bool highestQuality = false, }) { - final stopwatch = Stopwatch()..start(); + if (points.length <= 2) return points; - if (points.length <= 2) { - return points; - } List nextPoints = points; final double sqTolerance = tolerance * tolerance; nextPoints = highestQuality ? points : simplifyRadialDist(nextPoints, sqTolerance); nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); - stopwatch.stop(); - print( - '`simplify` took ${stopwatch.elapsedMicroseconds}us, and outputted ${nextPoints.length} points (~${((1 - (nextPoints.length / points.length)) * 100).toStringAsFixed(2)}% reduction)', - ); - return nextPoints; } From 7aac48663918bdb0bc0e59dc14555f9bab6e8c85 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 10 Jan 2024 19:38:10 +0000 Subject: [PATCH 19/27] Added `hitValue` and generic typing to `Polyline`s, and reflected change elsewhere Removed polyline simplification precomputer Reorganised polyline-related source files Fixed bugs and improved efficiency --- example/lib/pages/polyline.dart | 174 ++--- lib/flutter_map.dart | 2 +- lib/src/layer/polyline_layer.dart | 683 ------------------ lib/src/layer/polyline_layer/hit.dart | 22 + lib/src/layer/polyline_layer/painter.dart | 313 ++++++++ lib/src/layer/polyline_layer/polyline.dart | 89 +++ .../layer/polyline_layer/polyline_layer.dart | 222 ++++++ test/full_coverage_test.dart | 2 +- test/layer/polyline_layer_test.dart | 2 +- test/test_utils/test_app.dart | 2 +- 10 files changed, 740 insertions(+), 771 deletions(-) delete mode 100644 lib/src/layer/polyline_layer.dart create mode 100644 lib/src/layer/polyline_layer/hit.dart create mode 100644 lib/src/layer/polyline_layer/painter.dart create mode 100644 lib/src/layer/polyline_layer/polyline.dart create mode 100644 lib/src/layer/polyline_layer/polyline_layer.dart diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 091917313..cef62bb6d 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -7,6 +7,8 @@ import 'package:flutter_map_example/misc/tile_providers.dart'; import 'package:flutter_map_example/widgets/drawer/menu_drawer.dart'; import 'package:latlong2/latlong.dart'; +typedef PolylineHitValue = ({String title, String subtitle}); + class PolylinePage extends StatefulWidget { static const String route = '/polyline'; @@ -17,95 +19,104 @@ class PolylinePage extends StatefulWidget { } class _PolylinePageState extends State { - final PolylineHitNotifier _hitNotifier = ValueNotifier(null); - List? _prevHitLines; - List? _hoverLines; + final PolylineHitNotifier _hitNotifier = + ValueNotifier(null); + List? _prevHitValues; + List>? _hoverLines; - final _polylines = { - Polyline( + final _polylinesRaw = >[ + const Polyline( points: [ - const LatLng(51.5, -0.09), - const LatLng(53.3498, -6.2603), - const LatLng(48.8566, 2.3522), + LatLng(51.5, -0.09), + LatLng(53.3498, -6.2603), + LatLng(48.8566, 2.3522), ], strokeWidth: 8, - color: const Color(0xFF60399E), - ): ( - title: 'Elizabeth Line', - subtitle: 'Nothing really special here...', + color: Color(0xFF60399E), + hitValue: ( + title: 'Elizabeth Line', + subtitle: 'Nothing really special here...', + ), ), - Polyline( + const Polyline( points: [ - const LatLng(48.5, -3.09), - const LatLng(47.3498, -9.2603), - const LatLng(43.8566, -1.3522), + LatLng(48.5, -3.09), + LatLng(47.3498, -9.2603), + LatLng(43.8566, -1.3522), ], strokeWidth: 16000, color: Colors.pink, useStrokeWidthInMeter: true, - ): ( - title: 'Pink Line', - subtitle: 'Fixed radius in meters instead of pixels', + hitValue: ( + title: 'Pink Line', + subtitle: 'Fixed radius in meters instead of pixels', + ), ), - Polyline( + const Polyline( points: [ - const LatLng(55.5, -0.09), - const LatLng(54.3498, -6.2603), - const LatLng(52.8566, 2.3522), + LatLng(55.5, -0.09), + LatLng(54.3498, -6.2603), + LatLng(52.8566, 2.3522), ], strokeWidth: 4, gradientColors: [ - const Color(0xffE40203), - const Color(0xffFEED00), - const Color(0xff007E2D), + Color(0xffE40203), + Color(0xffFEED00), + Color(0xff007E2D), ], - ): ( - title: 'Traffic Light Line', - subtitle: 'Fancy gradient instead of a solid color', + hitValue: ( + title: 'Traffic Light Line', + subtitle: 'Fancy gradient instead of a solid color', + ), ), Polyline( - points: [ - const LatLng(50.5, -0.09), - const LatLng(51.3498, 6.2603), - const LatLng(53.8566, 2.3522), + points: const [ + LatLng(50.5, -0.09), + LatLng(51.3498, 6.2603), + LatLng(53.8566, 2.3522), ], strokeWidth: 20, color: Colors.blue.withOpacity(0.6), borderStrokeWidth: 20, borderColor: Colors.red.withOpacity(0.4), - ): ( - title: 'BlueRed Line', - subtitle: 'Solid translucent color fill, with different color outline', + hitValue: ( + title: 'BlueRed Line', + subtitle: 'Solid translucent color fill, with different color outline', + ), ), Polyline( - points: [ - const LatLng(50.2, -0.08), - const LatLng(51.2498, -10.2603), - const LatLng(54.8566, -9.3522), + points: const [ + LatLng(50.2, -0.08), + LatLng(51.2498, -10.2603), + LatLng(54.8566, -9.3522), ], strokeWidth: 20, color: Colors.black.withOpacity(0.2), borderStrokeWidth: 20, borderColor: Colors.white30, - ): ( - title: 'BlackWhite Line', - subtitle: 'Solid translucent color fill, with different color outline', + hitValue: ( + title: 'BlackWhite Line', + subtitle: 'Solid translucent color fill, with different color outline', + ), ), Polyline( - points: [ - const LatLng(49.1, -0.06), - const LatLng(52.15, -1.4), - const LatLng(55.5, 0.8), + points: const [ + LatLng(49.1, -0.06), + LatLng(52.15, -1.4), + LatLng(55.5, 0.8), ], strokeWidth: 10, color: Colors.yellow, borderStrokeWidth: 10, borderColor: Colors.blue.withOpacity(0.5), - ): ( - title: 'YellowBlue Line', - subtitle: 'Solid translucent color fill, with different color outline', + hitValue: ( + title: 'YellowBlue Line', + subtitle: 'Solid translucent color fill, with different color outline', + ), ), - }; + ]; + late final _polylines = + Map.fromEntries(_polylinesRaw.map((e) => MapEntry(e.hitValue, e))); final _randomWalk = [const LatLng(44.861294, 13.845086)]; @@ -146,54 +157,51 @@ class _PolylinePageState extends State { hitTestBehavior: HitTestBehavior.deferToChild, cursor: SystemMouseCursors.click, onHover: (_) { - // Filter out hover outlines, and ignore if no lines were hit - final hitLines = _hitNotifier.value?.lines - .where((e) => _polylines.containsKey(e)) - .toList(); - if (hitLines == null) return; + final hitValues = _hitNotifier.value?.hitValues.toList(); + if (hitValues == null) return; + + if (listEquals(hitValues, _prevHitValues)) return; + _prevHitValues = hitValues; - // Avoid unnecessary rebuilds if no new lines were hit - if (listEquals(hitLines, _prevHitLines)) return; - _prevHitLines = hitLines; + final hoverLines = hitValues.map((v) { + final original = _polylines[v]!; - // Create hover outlines and add them to the map - final hoverLines = hitLines - .map( - (e) => Polyline( - points: e.points, - strokeWidth: e.strokeWidth + e.borderStrokeWidth, - color: Colors.transparent, - borderStrokeWidth: 15, - borderColor: Colors.green, - useStrokeWidthInMeter: e.useStrokeWidthInMeter, - ), - ) - .toList(); + return Polyline( + points: original.points, + strokeWidth: + original.strokeWidth + original.borderStrokeWidth, + color: Colors.transparent, + borderStrokeWidth: 15, + borderColor: Colors.green, + useStrokeWidthInMeter: original.useStrokeWidthInMeter, + ); + }).toList(); setState(() => _hoverLines = hoverLines); }, - // Clear hovered lines when touched lines modal appears - onExit: (_) => setState(() => _hoverLines = null), + onExit: (_) { + _prevHitValues = null; + setState(() => _hoverLines = null); + }, child: GestureDetector( onTap: () => _openTouchedLinesModal( 'Tapped', - _hitNotifier.value!.lines, + _hitNotifier.value!.hitValues, _hitNotifier.value!.point, ), onLongPress: () => _openTouchedLinesModal( 'Long pressed', - _hitNotifier.value!.lines, + _hitNotifier.value!.hitValues, _hitNotifier.value!.point, ), onSecondaryTap: () => _openTouchedLinesModal( 'Secondary tapped', - _hitNotifier.value!.lines, + _hitNotifier.value!.hitValues, _hitNotifier.value!.point, ), child: PolylineLayer( hitNotifier: _hitNotifier, - //simplificationTolerance: null, - polylines: - _polylines.keys.followedBy(_hoverLines ?? []).toList(), + simplificationTolerance: null, + polylines: [..._polylinesRaw, ...?_hoverLines], ), ), ), @@ -228,11 +236,9 @@ class _PolylinePageState extends State { void _openTouchedLinesModal( String eventType, - List tappedLines, + List tappedLines, LatLng coords, ) { - tappedLines.removeWhere((e) => !_polylines.containsKey(e)); - showModalBottomSheet( context: context, builder: (context) => Padding( @@ -251,7 +257,7 @@ class _PolylinePageState extends State { Expanded( child: ListView.builder( itemBuilder: (context, index) { - final tappedLineData = _polylines[tappedLines[index]]!; + final tappedLineData = tappedLines[index]; return ListTile( leading: index == 0 ? const Icon(Icons.vertical_align_top) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 1351c3793..0321b8f19 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -33,7 +33,7 @@ export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; -export 'package:flutter_map/src/layer/polyline_layer.dart'; +export 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_display.dart'; diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart deleted file mode 100644 index a1b292d73..000000000 --- a/lib/src/layer/polyline_layer.dart +++ /dev/null @@ -1,683 +0,0 @@ -import 'dart:core'; -import 'dart:math' as math; -import 'dart:ui' as ui; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/misc/simplify.dart'; -import 'package:latlong2/latlong.dart'; - -/// Result from polyline hit detection -/// -/// Emmitted by [PolylineLayer.hitNotifier]'s [ValueNotifier] -/// ([PolylineHitNotifier]). -class PolylineHit { - /// All hit [Polyline]s within the corresponding layer - /// - /// Ordered from first-last, visually top-bottom. - final List lines; - - /// Coordinates of the detected hit - /// - /// Note that this may not lie on a [Polyline]. - final LatLng point; - - const PolylineHit._({required this.lines, required this.point}); -} - -/// Typedef used on [PolylineLayer.hitNotifier] -typedef PolylineHitNotifier = ValueNotifier; - -class Polyline { - final List points; - final double strokeWidth; - final Color color; - final double borderStrokeWidth; - final Color? borderColor; - final List? gradientColors; - final List? colorsStop; - final bool isDotted; - final StrokeCap strokeCap; - final StrokeJoin strokeJoin; - final bool useStrokeWidthInMeter; - - Polyline({ - required this.points, - this.strokeWidth = 1.0, - this.color = const Color(0xFF00FF00), - this.borderStrokeWidth = 0.0, - this.borderColor = const Color(0xFFFFFF00), - this.gradientColors, - this.colorsStop, - this.isDotted = false, - this.strokeCap = StrokeCap.round, - this.strokeJoin = StrokeJoin.round, - this.useStrokeWidthInMeter = false, - }); - - Polyline copyWithNewPoints(List points) => Polyline( - points: points, - strokeWidth: strokeWidth, - color: color, - borderStrokeWidth: borderStrokeWidth, - borderColor: borderColor, - gradientColors: gradientColors, - colorsStop: colorsStop, - isDotted: isDotted, - strokeCap: strokeCap, - strokeJoin: strokeJoin, - useStrokeWidthInMeter: useStrokeWidthInMeter, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is Polyline && - listEquals(points, other.points) && - strokeWidth == other.strokeWidth && - color == other.color && - borderStrokeWidth == other.borderStrokeWidth && - borderColor == other.borderColor && - listEquals(gradientColors, other.gradientColors) && - listEquals(colorsStop, other.colorsStop) && - isDotted == other.isDotted && - strokeCap == other.strokeCap && - strokeJoin == other.strokeJoin && - useStrokeWidthInMeter == other.useStrokeWidthInMeter); - - /// Used to batch draw calls to the canvas - int get renderHashCode => Object.hash( - strokeWidth, - color, - borderStrokeWidth, - borderColor, - gradientColors, - colorsStop, - isDotted, - strokeCap, - strokeJoin, - useStrokeWidthInMeter, - ); - - @override - int get hashCode => Object.hashAll([...points, renderHashCode]); -} - -@immutable -class PolylineLayer extends StatefulWidget { - /// [Polyline]s to draw - final List polylines; - - /// Acceptable extent outside of viewport before culling polyline segments - /// - /// May need to be increased if the [Polyline.borderStrokeWidth] is large. - /// - /// Defaults to 0: cull aggressively. Set to `null` to disable culling. - final double? cullingMargin; - - /// Distance between two mergeable polyline points, in decimal degrees scaled - /// to floored zoom - /// - /// Increasing results in a more jagged, less accurate simplification, with - /// improved performance; and vice versa. - /// - /// Note that this value is internally scaled using the current map zoom, to - /// optimize visual performance in conjunction with improved performance with - /// culling. - /// - /// {@macro polyline.hitNotifier.simplificationWarning} - /// - /// Defaults to 1. Set to `null` to disable simplification. - final double? simplificationTolerance; - - /// A notifier to be notified when a hit test occurs on the layer - /// - /// If a notifier is not provided, hit testing is not performed. - /// - /// Notified with a [PolylineHit] if any [Polyline]s are hit, otherwise - /// notified with `null`. - /// - /// Note that a hover event is included as a hit event. If an expensive - /// operation is required on hover, check for equality between the new and old - /// [PolylineHit.lines], and avoid doing heavy work if they are the same. - /// - /// {@template polyline.hitNotifier.simplificationWarning} - /// If hit testing is enabled with simplification, testing is performed on the - /// visual, simplified polyline. If a line is hit, the non-simplified original - /// line is sent within [PolylineHit.lines]. This does incur extra memory - /// overhead, as both the original and simplified lines must be sent to the - /// painter. - /// {@endtemplate} - /// - /// See online documentation for more detailed usage instructions. See the - /// example project for an example implementation. - final PolylineHitNotifier? hitNotifier; - - /// The minimum radius of the hittable area around each [Polyline] in logical - /// pixels - /// - /// The entire visible area is always hittable, but if the visible area is - /// smaller than this, then this will be the hittable area. - /// - /// Defaults to 10. - final double minimumHitbox; - - const PolylineLayer({ - super.key, - required this.polylines, - this.cullingMargin = 0, - this.simplificationTolerance = 1, - this.hitNotifier, - this.minimumHitbox = 10, - }); - - @override - State createState() => _PolylineLayerState(); -} - -class _PolylineLayerState extends State { - final _cachedSimplifiedPolylines = >{}; - - @override - void didUpdateWidget(PolylineLayer oldWidget) { - super.didUpdateWidget(oldWidget); - - // IF old yes & new no, clear - // IF old no & new yes, precompute - // IF old no & new no, nothing - // IF old yes & new yes & (different tolerance | different lines), both - // otherwise, nothing - if (oldWidget.simplificationTolerance != null && - widget.simplificationTolerance != null && - (!listEquals(oldWidget.polylines, widget.polylines) || - oldWidget.simplificationTolerance != - widget.simplificationTolerance)) { - _cachedSimplifiedPolylines.clear(); - _precomputeSimplification(); - } else if (oldWidget.simplificationTolerance != null && - widget.simplificationTolerance == null) { - _cachedSimplifiedPolylines.clear(); - } else if (oldWidget.simplificationTolerance == null && - widget.simplificationTolerance != null) { - _precomputeSimplification(); - } - } - - @override - void initState() { - super.initState(); - _precomputeSimplification(); - } - - // Pre-compute simplified polylines for each zoom level 0-21 in an isolate - // on non-web platforms only - void _precomputeSimplification() { - if (widget.simplificationTolerance == null || kIsWeb) return; - - compute( - (msg) => List.generate( - 22, - (zoom) => msg.polylines - .map( - (polyline) => polyline.copyWithNewPoints( - simplify( - polyline.points, - msg.simplificationTolerance! / math.pow(2, zoom), - highestQuality: true, - ), - ), - ) - .toList(), - growable: false, - ).asMap(), - ( - polylines: widget.polylines, - simplificationTolerance: widget.simplificationTolerance, - ), - debugLabel: '[FM] Polyline Simplification Precomputer', - ).then(_cachedSimplifiedPolylines.addAll); - } - - @override - Widget build(BuildContext context) { - final camera = MapCamera.of(context); - - return MobileLayerTransformer( - child: CustomPaint( - painter: _PolylinePainter( - polylines: _aggressivelyCullPolylines( - polylines: widget.simplificationTolerance == null - ? widget.polylines - : _cachedSimplifiedPolylines[camera.zoom.floor()] ??= - widget.polylines - .map( - (polyline) => polyline.copyWithNewPoints( - simplify( - polyline.points, - widget.simplificationTolerance! / - math.pow(2, camera.zoom.floor()), - highestQuality: true, - ), - ), - ) - .toList(), - camera: camera, - cullingMargin: widget.cullingMargin, - ), - // TODO: These must also be culled! Or we need to recommend and - // implement a different method of retrieving the original polyline - // from the hit result by the user. The second is preferable, as the - // first will start incurring too much complexity again. - originalTappableReturnablePolylines: widget.hitNotifier != null && - widget.simplificationTolerance != null - ? widget.polylines - : null, - camera: camera, - hitNotifier: widget.hitNotifier, - minimumHitbox: widget.minimumHitbox, - ), - size: Size(camera.size.x, camera.size.y), - ), - ); - } - - List _aggressivelyCullPolylines({ - required List polylines, - required MapCamera camera, - required double? cullingMargin, - }) { - if (cullingMargin == null) return polylines; - - final culledPolylines = []; - - final bounds = camera.visibleBounds; - final margin = cullingMargin / math.pow(2, camera.zoom.floorToDouble()); - // The min(-90), max(180), ... are used to get around the limits of LatLng - // the value cannot be greater or smaller than that - final boundsAdjusted = LatLngBounds( - LatLng( - math.max(-90, bounds.southWest.latitude - margin), - math.max(-180, bounds.southWest.longitude - margin), - ), - LatLng( - math.min(90, bounds.northEast.latitude + margin), - math.min(180, bounds.northEast.longitude + margin), - ), - ); - - for (final polyline in polylines) { - // Gradient poylines cannot be easily segmented - if (polyline.gradientColors != null) { - culledPolylines.add(polyline); - continue; - } - // pointer that indicates the start of the visible polyline segment - int start = -1; - bool fullyVisible = true; - for (int i = 0; i < polyline.points.length - 1; i++) { - //current pair - final p1 = polyline.points[i]; - final p2 = polyline.points[i + 1]; - - // segment is visible - if (Bounds( - math.Point( - boundsAdjusted.southWest.longitude, - boundsAdjusted.southWest.latitude, - ), - math.Point( - boundsAdjusted.northEast.longitude, - boundsAdjusted.northEast.latitude, - ), - ).aabbContainsLine( - p1.longitude, p1.latitude, p2.longitude, p2.latitude)) { - // segment is visible - if (start == -1) { - start = i; - } - if (!fullyVisible && i == polyline.points.length - 2) { - final segment = polyline.points.sublist(start, i + 2); - culledPolylines.add(polyline.copyWithNewPoints(segment)); - } - } else { - fullyVisible = false; - // if we cannot see the segment, then reset start - if (start != -1) { - // partial start - final segment = polyline.points.sublist(start, i + 1); - culledPolylines.add(polyline.copyWithNewPoints(segment)); - start = -1; - } - if (start != -1) { - start = i; - } - } - } - - if (fullyVisible) culledPolylines.add(polyline); - } - - return culledPolylines; - } -} - -class _PolylinePainter extends CustomPainter { - final List polylines; - final MapCamera camera; - final LatLngBounds bounds; - final PolylineHitNotifier? hitNotifier; - final double minimumHitbox; - - /// {@macro polyline.hitNotifier.simplificationWarning} - final List? originalTappableReturnablePolylines; - - // Avoids reallocation on every `hitTest`, is cleared every time - final hits = List.empty(growable: true); - - int get hash => _hash ??= Object.hashAll(polylines); - int? _hash; - - _PolylinePainter({ - required this.polylines, - required this.originalTappableReturnablePolylines, - required this.camera, - required this.hitNotifier, - required this.minimumHitbox, - }) : bounds = camera.visibleBounds; - - List getOffsets(Offset origin, List points) => List.generate( - points.length, - (index) => getOffset(origin, points[index]), - growable: false, - ); - - Offset getOffset(Offset origin, LatLng point) { - // Critically create as little garbage as possible. This is called on every frame. - final projected = camera.project(point); - return Offset(projected.x - origin.dx, projected.y - origin.dy); - } - - @override - bool? hitTest(Offset position) { - if (hitNotifier == null) return null; - - hits.clear(); - - final origin = - camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; - - int polylineIndex = polylines.length; - for (final polyline in polylines.reversed) { - polylineIndex--; - - // TODO: For efficiency we'd ideally filter by bounding box here. However - // we'd need to compute an extended bounding box that accounts account for - // the stroke width. - // if (!p.boundingBox.contains(touch)) { - // continue; - // } - - final offsets = getOffsets(origin, polyline.points); - final strokeWidth = polyline.useStrokeWidthInMeter - ? _metersToStrokeWidth( - origin, - polyline.points.first, - offsets.first, - polyline.strokeWidth, - ) - : polyline.strokeWidth; - final hittableDistance = math.max( - strokeWidth / 2 + polyline.borderStrokeWidth / 2, - minimumHitbox, - ); - - for (int i = 0; i < offsets.length - 1; i++) { - final o1 = offsets[i]; - final o2 = offsets[i + 1]; - - final distance = math.sqrt(_distToSegmentSquared( - position.dx, - position.dy, - o1.dx, - o1.dy, - o2.dx, - o2.dy, - )); - - if (distance < hittableDistance) { - hits.add( - originalTappableReturnablePolylines?[polylineIndex] ?? polyline); - break; - } - } - } - - if (hits.isEmpty) { - hitNotifier!.value = null; - return false; - } - - hitNotifier!.value = PolylineHit._( - lines: hits, - point: camera.pointToLatLng(math.Point(position.dx, position.dy)), - ); - return true; - } - - @override - void paint(Canvas canvas, Size size) { - final rect = Offset.zero & size; - - var path = ui.Path(); - var borderPath = ui.Path(); - var filterPath = ui.Path(); - var paint = Paint(); - var needsLayerSaving = false; - - Paint? borderPaint; - Paint? filterPaint; - int? lastHash; - - void drawPaths() { - final hasBorder = borderPaint != null && filterPaint != null; - if (hasBorder) { - if (needsLayerSaving) { - canvas.saveLayer(rect, Paint()); - } - - canvas.drawPath(borderPath, borderPaint!); - borderPath = ui.Path(); - borderPaint = null; - - if (needsLayerSaving) { - canvas.drawPath(filterPath, filterPaint!); - filterPath = ui.Path(); - filterPaint = null; - - canvas.restore(); - } - } - - canvas.drawPath(path, paint); - path = ui.Path(); - paint = Paint(); - } - - final origin = - camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; - - for (final polyline in polylines) { - final offsets = getOffsets(origin, polyline.points); - if (offsets.isEmpty) { - continue; - } - - final hash = polyline.renderHashCode; - if (needsLayerSaving || (lastHash != null && lastHash != hash)) { - drawPaths(); - } - lastHash = hash; - needsLayerSaving = polyline.color.opacity < 1.0 || - (polyline.gradientColors?.any((c) => c.opacity < 1.0) ?? false); - - late final double strokeWidth; - if (polyline.useStrokeWidthInMeter) { - strokeWidth = _metersToStrokeWidth( - origin, - polyline.points.first, - offsets.first, - polyline.strokeWidth, - ); - } else { - strokeWidth = polyline.strokeWidth; - } - - final isDotted = polyline.isDotted; - paint = Paint() - ..strokeWidth = strokeWidth - ..strokeCap = polyline.strokeCap - ..strokeJoin = polyline.strokeJoin - ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke - ..blendMode = BlendMode.srcOver; - - if (polyline.gradientColors == null) { - paint.color = polyline.color; - } else { - polyline.gradientColors!.isNotEmpty - ? paint.shader = _paintGradient(polyline, offsets) - : paint.color = polyline.color; - } - - if (polyline.borderColor != null && polyline.borderStrokeWidth > 0.0) { - // Outlined lines are drawn by drawing a thicker path underneath, then - // stenciling the middle (in case the line fill is transparent), and - // finally drawing the line fill. - borderPaint = Paint() - ..color = polyline.borderColor ?? const Color(0x00000000) - ..strokeWidth = strokeWidth + polyline.borderStrokeWidth - ..strokeCap = polyline.strokeCap - ..strokeJoin = polyline.strokeJoin - ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke - ..blendMode = BlendMode.srcOver; - - filterPaint = Paint() - ..color = polyline.borderColor!.withAlpha(255) - ..strokeWidth = strokeWidth - ..strokeCap = polyline.strokeCap - ..strokeJoin = polyline.strokeJoin - ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke - ..blendMode = BlendMode.dstOut; - } - - final radius = paint.strokeWidth / 2; - final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2; - - if (isDotted) { - final spacing = strokeWidth * 1.5; - if (borderPaint != null && filterPaint != null) { - _paintDottedLine(borderPath, offsets, borderRadius, spacing); - _paintDottedLine(filterPath, offsets, radius, spacing); - } - _paintDottedLine(path, offsets, radius, spacing); - } else { - if (borderPaint != null && filterPaint != null) { - _paintLine(borderPath, offsets); - _paintLine(filterPath, offsets); - } - _paintLine(path, offsets); - } - } - - drawPaths(); - } - - void _paintDottedLine( - ui.Path path, List offsets, double radius, double stepLength) { - var startDistance = 0.0; - for (var i = 0; i < offsets.length - 1; i++) { - final o0 = offsets[i]; - final o1 = offsets[i + 1]; - final totalDistance = (o0 - o1).distance; - var distance = startDistance; - while (distance < totalDistance) { - final f1 = distance / totalDistance; - final f0 = 1.0 - f1; - final offset = Offset(o0.dx * f0 + o1.dx * f1, o0.dy * f0 + o1.dy * f1); - path.addOval(Rect.fromCircle(center: offset, radius: radius)); - distance += stepLength; - } - startDistance = distance < totalDistance - ? stepLength - (totalDistance - distance) - : distance - totalDistance; - } - path.addOval(Rect.fromCircle(center: offsets.last, radius: radius)); - } - - void _paintLine(ui.Path path, List offsets) { - if (offsets.isEmpty) { - return; - } - path.addPolygon(offsets, false); - } - - ui.Gradient _paintGradient(Polyline polyline, List offsets) => - ui.Gradient.linear(offsets.first, offsets.last, polyline.gradientColors!, - _getColorsStop(polyline)); - - List? _getColorsStop(Polyline polyline) => - (polyline.colorsStop != null && - polyline.colorsStop!.length == polyline.gradientColors!.length) - ? polyline.colorsStop - : _calculateColorsStop(polyline); - - List _calculateColorsStop(Polyline polyline) { - final colorsStopInterval = 1.0 / polyline.gradientColors!.length; - return polyline.gradientColors! - .map((gradientColor) => - polyline.gradientColors!.indexOf(gradientColor) * - colorsStopInterval) - .toList(); - } - - double _metersToStrokeWidth( - Offset origin, - LatLng p0, - Offset o0, - double strokeWidthInMeters, - ) { - final r = _distance.offset(p0, strokeWidthInMeters, 180); - final delta = o0 - getOffset(origin, r); - return delta.distance; - } - - @override - bool shouldRepaint(_PolylinePainter oldDelegate) => true; -} - -double _distanceSq(double x0, double y0, double x1, double y1) { - final dx = x0 - x1; - final dy = y0 - y1; - return dx * dx + dy * dy; -} - -double _distToSegmentSquared( - double px, - double py, - double x0, - double y0, - double x1, - double y1, -) { - final dx = x1 - x0; - final dy = y1 - y0; - final distanceSq = dx * dx + dy * dy; - if (distanceSq == 0) { - return _distanceSq(px, py, x0, y0); - } - - final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp(0, 1); - return _distanceSq(px, py, x0 + t * dx, y0 + t * dy); -} - -const _distance = Distance(); diff --git a/lib/src/layer/polyline_layer/hit.dart b/lib/src/layer/polyline_layer/hit.dart new file mode 100644 index 000000000..612bd5483 --- /dev/null +++ b/lib/src/layer/polyline_layer/hit.dart @@ -0,0 +1,22 @@ +part of 'polyline_layer.dart'; + +/// Result from polyline hit detection +/// +/// Emmitted by [PolylineLayer.hitNotifier]'s [ValueNotifier] +/// ([PolylineHitNotifier]). +class PolylineHit { + /// All hit [Polyline.hitValue]s within the corresponding layer + /// + /// Ordered from first-last, visually top-bottom. + final List hitValues; + + /// Coordinates of the detected hit + /// + /// Note that this may not lie on a [Polyline]. + final LatLng point; + + const PolylineHit._({required this.hitValues, required this.point}); +} + +/// Typedef used on [PolylineLayer.hitNotifier] +typedef PolylineHitNotifier = ValueNotifier?>; diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart new file mode 100644 index 000000000..ce1a3f992 --- /dev/null +++ b/lib/src/layer/polyline_layer/painter.dart @@ -0,0 +1,313 @@ +part of 'polyline_layer.dart'; + +class PolylinePainter extends CustomPainter { + final List> polylines; + final MapCamera camera; + final PolylineHitNotifier? hitNotifier; + final double minimumHitbox; + + final _hits = []; // Avoids repetitive memory reallocation + + int get hash => _hash ??= Object.hashAll(polylines); + int? _hash; + + PolylinePainter({ + required this.polylines, + required this.camera, + required this.hitNotifier, + required this.minimumHitbox, + }); + + List getOffsets(Offset origin, List points) => List.generate( + points.length, + (index) => getOffset(origin, points[index]), + growable: false, + ); + + Offset getOffset(Offset origin, LatLng point) { + // Critically create as little garbage as possible. This is called on every frame. + final projected = camera.project(point); + return Offset(projected.x - origin.dx, projected.y - origin.dy); + } + + @override + bool? hitTest(Offset position) { + if (hitNotifier == null) return null; + + _hits.clear(); + + final origin = + camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; + + for (final polyline in polylines.reversed) { + if (polyline.hitValue == null) continue; + + // TODO: For efficiency we'd ideally filter by bounding box here. However + // we'd need to compute an extended bounding box that accounts account for + // the stroke width. + // if (!p.boundingBox.contains(touch)) { + // continue; + // } + + final offsets = getOffsets(origin, polyline.points); + final strokeWidth = polyline.useStrokeWidthInMeter + ? _metersToStrokeWidth( + origin, + polyline.points.first, + offsets.first, + polyline.strokeWidth, + ) + : polyline.strokeWidth; + final hittableDistance = math.max( + strokeWidth / 2 + polyline.borderStrokeWidth / 2, + minimumHitbox, + ); + + for (int i = 0; i < offsets.length - 1; i++) { + final o1 = offsets[i]; + final o2 = offsets[i + 1]; + + final distance = math.sqrt(_distToSegmentSquared( + position.dx, + position.dy, + o1.dx, + o1.dy, + o2.dx, + o2.dy, + )); + + if (distance < hittableDistance) { + _hits.add(polyline.hitValue!); + break; + } + } + } + + if (_hits.isEmpty) { + hitNotifier!.value = null; + return false; + } + + hitNotifier!.value = PolylineHit._( + hitValues: _hits, + point: camera.pointToLatLng(math.Point(position.dx, position.dy)), + ); + return true; + } + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + + var path = ui.Path(); + var borderPath = ui.Path(); + var filterPath = ui.Path(); + var paint = Paint(); + var needsLayerSaving = false; + + Paint? borderPaint; + Paint? filterPaint; + int? lastHash; + + void drawPaths() { + final hasBorder = borderPaint != null && filterPaint != null; + if (hasBorder) { + if (needsLayerSaving) { + canvas.saveLayer(rect, Paint()); + } + + canvas.drawPath(borderPath, borderPaint!); + borderPath = ui.Path(); + borderPaint = null; + + if (needsLayerSaving) { + canvas.drawPath(filterPath, filterPaint!); + filterPath = ui.Path(); + filterPaint = null; + + canvas.restore(); + } + } + + canvas.drawPath(path, paint); + path = ui.Path(); + paint = Paint(); + } + + final origin = + camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; + + for (final polyline in polylines) { + final offsets = getOffsets(origin, polyline.points); + if (offsets.isEmpty) { + continue; + } + + final hash = polyline.renderHashCode; + if (needsLayerSaving || (lastHash != null && lastHash != hash)) { + drawPaths(); + } + lastHash = hash; + needsLayerSaving = polyline.color.opacity < 1.0 || + (polyline.gradientColors?.any((c) => c.opacity < 1.0) ?? false); + + late final double strokeWidth; + if (polyline.useStrokeWidthInMeter) { + strokeWidth = _metersToStrokeWidth( + origin, + polyline.points.first, + offsets.first, + polyline.strokeWidth, + ); + } else { + strokeWidth = polyline.strokeWidth; + } + + final isDotted = polyline.isDotted; + paint = Paint() + ..strokeWidth = strokeWidth + ..strokeCap = polyline.strokeCap + ..strokeJoin = polyline.strokeJoin + ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke + ..blendMode = BlendMode.srcOver; + + if (polyline.gradientColors == null) { + paint.color = polyline.color; + } else { + polyline.gradientColors!.isNotEmpty + ? paint.shader = _paintGradient(polyline, offsets) + : paint.color = polyline.color; + } + + if (polyline.borderColor != null && polyline.borderStrokeWidth > 0.0) { + // Outlined lines are drawn by drawing a thicker path underneath, then + // stenciling the middle (in case the line fill is transparent), and + // finally drawing the line fill. + borderPaint = Paint() + ..color = polyline.borderColor ?? const Color(0x00000000) + ..strokeWidth = strokeWidth + polyline.borderStrokeWidth + ..strokeCap = polyline.strokeCap + ..strokeJoin = polyline.strokeJoin + ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke + ..blendMode = BlendMode.srcOver; + + filterPaint = Paint() + ..color = polyline.borderColor!.withAlpha(255) + ..strokeWidth = strokeWidth + ..strokeCap = polyline.strokeCap + ..strokeJoin = polyline.strokeJoin + ..style = isDotted ? PaintingStyle.fill : PaintingStyle.stroke + ..blendMode = BlendMode.dstOut; + } + + final radius = paint.strokeWidth / 2; + final borderRadius = (borderPaint?.strokeWidth ?? 0) / 2; + + if (isDotted) { + final spacing = strokeWidth * 1.5; + if (borderPaint != null && filterPaint != null) { + _paintDottedLine(borderPath, offsets, borderRadius, spacing); + _paintDottedLine(filterPath, offsets, radius, spacing); + } + _paintDottedLine(path, offsets, radius, spacing); + } else { + if (borderPaint != null && filterPaint != null) { + _paintLine(borderPath, offsets); + _paintLine(filterPath, offsets); + } + _paintLine(path, offsets); + } + } + + drawPaths(); + } + + void _paintDottedLine( + ui.Path path, List offsets, double radius, double stepLength) { + var startDistance = 0.0; + for (var i = 0; i < offsets.length - 1; i++) { + final o0 = offsets[i]; + final o1 = offsets[i + 1]; + final totalDistance = (o0 - o1).distance; + var distance = startDistance; + while (distance < totalDistance) { + final f1 = distance / totalDistance; + final f0 = 1.0 - f1; + final offset = Offset(o0.dx * f0 + o1.dx * f1, o0.dy * f0 + o1.dy * f1); + path.addOval(Rect.fromCircle(center: offset, radius: radius)); + distance += stepLength; + } + startDistance = distance < totalDistance + ? stepLength - (totalDistance - distance) + : distance - totalDistance; + } + path.addOval(Rect.fromCircle(center: offsets.last, radius: radius)); + } + + void _paintLine(ui.Path path, List offsets) { + if (offsets.isEmpty) { + return; + } + path.addPolygon(offsets, false); + } + + ui.Gradient _paintGradient(Polyline polyline, List offsets) => + ui.Gradient.linear(offsets.first, offsets.last, polyline.gradientColors!, + _getColorsStop(polyline)); + + List? _getColorsStop(Polyline polyline) => + (polyline.colorsStop != null && + polyline.colorsStop!.length == polyline.gradientColors!.length) + ? polyline.colorsStop + : _calculateColorsStop(polyline); + + List _calculateColorsStop(Polyline polyline) { + final colorsStopInterval = 1.0 / polyline.gradientColors!.length; + return polyline.gradientColors! + .map((gradientColor) => + polyline.gradientColors!.indexOf(gradientColor) * + colorsStopInterval) + .toList(); + } + + double _metersToStrokeWidth( + Offset origin, + LatLng p0, + Offset o0, + double strokeWidthInMeters, + ) { + final r = _distance.offset(p0, strokeWidthInMeters, 180); + final delta = o0 - getOffset(origin, r); + return delta.distance; + } + + @override + bool shouldRepaint(PolylinePainter oldDelegate) => false; +} + +double _distanceSq(double x0, double y0, double x1, double y1) { + final dx = x0 - x1; + final dy = y0 - y1; + return dx * dx + dy * dy; +} + +double _distToSegmentSquared( + double px, + double py, + double x0, + double y0, + double x1, + double y1, +) { + final dx = x1 - x0; + final dy = y1 - y0; + final distanceSq = dx * dx + dy * dy; + if (distanceSq == 0) { + return _distanceSq(px, py, x0, y0); + } + + final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp(0, 1); + return _distanceSq(px, py, x0 + t * dx, y0 + t * dy); +} + +const _distance = Distance(); diff --git a/lib/src/layer/polyline_layer/polyline.dart b/lib/src/layer/polyline_layer/polyline.dart new file mode 100644 index 000000000..896285f26 --- /dev/null +++ b/lib/src/layer/polyline_layer/polyline.dart @@ -0,0 +1,89 @@ +part of 'polyline_layer.dart'; + +@immutable +class Polyline { + final List points; + final double strokeWidth; + final Color color; + final double borderStrokeWidth; + final Color? borderColor; + final List? gradientColors; + final List? colorsStop; + final bool isDotted; + final StrokeCap strokeCap; + final StrokeJoin strokeJoin; + final bool useStrokeWidthInMeter; + + /// Value notified in [PolylineLayer.hitNotifier] + /// + /// Polylines without a defined [hitValue] are still hit tested, but are not + /// notified about. + /// + /// Should implement an equality operator to avoid breaking [Polyline.==]. + final R? hitValue; + + const Polyline({ + required this.points, + this.strokeWidth = 1.0, + this.color = const Color(0xFF00FF00), + this.borderStrokeWidth = 0.0, + this.borderColor = const Color(0xFFFFFF00), + this.gradientColors, + this.colorsStop, + this.isDotted = false, + this.strokeCap = StrokeCap.round, + this.strokeJoin = StrokeJoin.round, + this.useStrokeWidthInMeter = false, + this.hitValue, + }); + + Polyline copyWithNewPoints(List points) => Polyline( + points: points, + strokeWidth: strokeWidth, + color: color, + borderStrokeWidth: borderStrokeWidth, + borderColor: borderColor, + gradientColors: gradientColors, + colorsStop: colorsStop, + isDotted: isDotted, + strokeCap: strokeCap, + strokeJoin: strokeJoin, + useStrokeWidthInMeter: useStrokeWidthInMeter, + hitValue: hitValue, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is Polyline && + listEquals(points, other.points) && + strokeWidth == other.strokeWidth && + color == other.color && + borderStrokeWidth == other.borderStrokeWidth && + borderColor == other.borderColor && + listEquals(gradientColors, other.gradientColors) && + listEquals(colorsStop, other.colorsStop) && + isDotted == other.isDotted && + strokeCap == other.strokeCap && + strokeJoin == other.strokeJoin && + useStrokeWidthInMeter == other.useStrokeWidthInMeter && + hitValue == other.hitValue); + + /// Used to batch draw calls to the canvas + int get renderHashCode => Object.hash( + strokeWidth, + color, + borderStrokeWidth, + borderColor, + gradientColors, + colorsStop, + isDotted, + strokeCap, + strokeJoin, + useStrokeWidthInMeter, + hitValue, + ); + + @override + int get hashCode => Object.hashAll([...points, renderHashCode]); +} diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart new file mode 100644 index 000000000..c98011d41 --- /dev/null +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -0,0 +1,222 @@ +import 'dart:core'; +import 'dart:math' as math; +import 'dart:ui' as ui; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; +import 'package:latlong2/latlong.dart'; + +part 'hit.dart'; +part 'painter.dart'; +part 'polyline.dart'; + +@immutable +class PolylineLayer extends StatefulWidget { + /// [Polyline]s to draw + final List> polylines; + + /// Acceptable extent outside of viewport before culling polyline segments + /// + /// May need to be increased if the [Polyline.borderStrokeWidth] is large. + /// + /// Defaults to 0: cull aggressively. Set to `null` to disable culling. + final double? cullingMargin; + + /// Distance between two mergeable polyline points, in decimal degrees scaled + /// to floored zoom + /// + /// Increasing results in a more jagged, less accurate simplification, with + /// improved performance; and vice versa. + /// + /// Note that this value is internally scaled using the current map zoom, to + /// optimize visual performance in conjunction with improved performance with + /// culling. + /// + /// Defaults to 1. Set to `null` to disable simplification. + final double? simplificationTolerance; + + /// A notifier to be notified when a hit test occurs on the layer + /// + /// If a notifier is not provided, hit testing is not performed. + /// + /// Notified with a [PolylineHit] if any [Polyline]s are hit, otherwise + /// notified with `null`. + /// + /// Note that a hover event is included as a hit event. If an expensive + /// operation is required on hover, check for equality between the new and old + /// [PolylineHit.hitValues], and avoid doing heavy work if they are the same. + /// + /// See online documentation for more detailed usage instructions. See the + /// example project for an example implementation. + final PolylineHitNotifier? hitNotifier; + + /// The minimum radius of the hittable area around each [Polyline] in logical + /// pixels + /// + /// The entire visible area is always hittable, but if the visible area is + /// smaller than this, then this will be the hittable area. + /// + /// Defaults to 10. + final double minimumHitbox; + + const PolylineLayer({ + super.key, + required this.polylines, + this.cullingMargin = 0, + this.simplificationTolerance = 1, + this.hitNotifier, + this.minimumHitbox = 10, + }); + + @override + State> createState() => _PolylineLayerState(); +} + +class _PolylineLayerState extends State> { + final _cachedSimplifiedPolylines = >>{}; + + final _culledPolylines = + >[]; // Avoids repetitive memory reallocation + + @override + void didUpdateWidget(PolylineLayer oldWidget) { + super.didUpdateWidget(oldWidget); + + // IF old yes & new no, clear + // IF old no & new yes, compute + // IF old no & new no, nothing + // IF old yes & new yes & (different tolerance | different lines), both + // otherwise, nothing + if (oldWidget.simplificationTolerance != null && + widget.simplificationTolerance != null && + (!listEquals(oldWidget.polylines, widget.polylines) || + oldWidget.simplificationTolerance != + widget.simplificationTolerance)) { + _cachedSimplifiedPolylines.clear(); + _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); + } else if (oldWidget.simplificationTolerance != null && + widget.simplificationTolerance == null) { + _cachedSimplifiedPolylines.clear(); + } else if (oldWidget.simplificationTolerance == null && + widget.simplificationTolerance != null) { + _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); + } + } + + @override + Widget build(BuildContext context) { + final camera = MapCamera.of(context); + + return MobileLayerTransformer( + child: CustomPaint( + painter: PolylinePainter( + polylines: _aggressivelyCullPolylines( + polylines: widget.simplificationTolerance == null + ? widget.polylines + : _computeZoomLevelSimplification(camera.zoom.floor()), + camera: camera, + cullingMargin: widget.cullingMargin, + ), + camera: camera, + hitNotifier: widget.hitNotifier, + minimumHitbox: widget.minimumHitbox, + ), + size: Size(camera.size.x, camera.size.y), + ), + ); + } + + List> _computeZoomLevelSimplification(int zoom) => + _cachedSimplifiedPolylines[zoom] ??= widget.polylines + .map( + (polyline) => polyline.copyWithNewPoints( + simplify( + polyline.points, + widget.simplificationTolerance! / math.pow(2, zoom), + highestQuality: true, + ), + ), + ) + .toList(); + + List> _aggressivelyCullPolylines({ + required List> polylines, + required MapCamera camera, + required double? cullingMargin, + }) { + if (cullingMargin == null) return polylines; + + _culledPolylines.clear(); + + final bounds = camera.visibleBounds; + final margin = cullingMargin / math.pow(2, camera.zoom.floorToDouble()); + // The min(-90), max(180), ... are used to get around the limits of LatLng + // the value cannot be greater or smaller than that + final boundsAdjusted = LatLngBounds( + LatLng( + math.max(-90, bounds.southWest.latitude - margin), + math.max(-180, bounds.southWest.longitude - margin), + ), + LatLng( + math.min(90, bounds.northEast.latitude + margin), + math.min(180, bounds.northEast.longitude + margin), + ), + ); + + for (final polyline in polylines) { + // Gradient poylines cannot be easily segmented + if (polyline.gradientColors != null) { + _culledPolylines.add(polyline); + continue; + } + // pointer that indicates the start of the visible polyline segment + int start = -1; + bool fullyVisible = true; + for (int i = 0; i < polyline.points.length - 1; i++) { + //current pair + final p1 = polyline.points[i]; + final p2 = polyline.points[i + 1]; + + // segment is visible + if (Bounds( + math.Point( + boundsAdjusted.southWest.longitude, + boundsAdjusted.southWest.latitude, + ), + math.Point( + boundsAdjusted.northEast.longitude, + boundsAdjusted.northEast.latitude, + ), + ).aabbContainsLine( + p1.longitude, p1.latitude, p2.longitude, p2.latitude)) { + // segment is visible + if (start == -1) { + start = i; + } + if (!fullyVisible && i == polyline.points.length - 2) { + final segment = polyline.points.sublist(start, i + 2); + _culledPolylines.add(polyline.copyWithNewPoints(segment)); + } + } else { + fullyVisible = false; + // if we cannot see the segment, then reset start + if (start != -1) { + // partial start + final segment = polyline.points.sublist(start, i + 1); + _culledPolylines.add(polyline.copyWithNewPoints(segment)); + start = -1; + } + if (start != -1) { + start = i; + } + } + } + + if (fullyVisible) _culledPolylines.add(polyline); + } + + return _culledPolylines; + } +} diff --git a/test/full_coverage_test.dart b/test/full_coverage_test.dart index 0f567f3ff..934c834e1 100644 --- a/test/full_coverage_test.dart +++ b/test/full_coverage_test.dart @@ -19,7 +19,7 @@ import 'package:flutter_map/src/layer/marker_layer.dart'; import 'package:flutter_map/src/layer/overlay_image_layer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.dart'; import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; -import 'package:flutter_map/src/layer/polyline_layer.dart'; +import 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; diff --git a/test/layer/polyline_layer_test.dart b/test/layer/polyline_layer_test.dart index e2290ae70..566bb9fab 100644 --- a/test/layer/polyline_layer_test.dart +++ b/test/layer/polyline_layer_test.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/polyline_layer.dart'; +import 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; import 'package:flutter_map/src/map/widget.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:latlong2/latlong.dart'; diff --git a/test/test_utils/test_app.dart b/test/test_utils/test_app.dart index e0ea2cc21..f4cf83993 100644 --- a/test/test_utils/test_app.dart +++ b/test/test_utils/test_app.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/src/layer/circle_layer.dart'; import 'package:flutter_map/src/layer/marker_layer.dart'; import 'package:flutter_map/src/layer/polygon_layer/polygon_layer.dart'; -import 'package:flutter_map/src/layer/polyline_layer.dart'; +import 'package:flutter_map/src/layer/polyline_layer/polyline_layer.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; import 'package:flutter_map/src/map/controller/map_controller.dart'; import 'package:flutter_map/src/map/options/options.dart'; From 7af63877b885e9c10c0a89054d7e386bb8fed948 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Jan 2024 22:02:19 +0000 Subject: [PATCH 20/27] De-associated `PolylineHit(Notifier)` with polylines by renaming to `LayerHit(Notifier)` --- example/lib/pages/polyline.dart | 3 +- lib/flutter_map.dart | 1 + lib/src/layer/general/hit_detection.dart | 29 +++++++++++++++++++ lib/src/layer/polyline_layer/hit.dart | 22 -------------- lib/src/layer/polyline_layer/painter.dart | 4 +-- .../layer/polyline_layer/polyline_layer.dart | 11 ++----- 6 files changed, 36 insertions(+), 34 deletions(-) create mode 100644 lib/src/layer/general/hit_detection.dart delete mode 100644 lib/src/layer/polyline_layer/hit.dart diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index cef62bb6d..83f55a96a 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -19,8 +19,7 @@ class PolylinePage extends StatefulWidget { } class _PolylinePageState extends State { - final PolylineHitNotifier _hitNotifier = - ValueNotifier(null); + final LayerHitNotifier _hitNotifier = ValueNotifier(null); List? _prevHitValues; List>? _hoverLines; diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 0321b8f19..d1d3ec2db 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -28,6 +28,7 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/source.dart'; export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer.dart'; +export 'package:flutter_map/src/layer/general/hit_detection.dart'; export 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; export 'package:flutter_map/src/layer/general/translucent_pointer.dart'; export 'package:flutter_map/src/layer/marker_layer.dart'; diff --git a/lib/src/layer/general/hit_detection.dart b/lib/src/layer/general/hit_detection.dart new file mode 100644 index 000000000..686883ce4 --- /dev/null +++ b/lib/src/layer/general/hit_detection.dart @@ -0,0 +1,29 @@ +import 'package:flutter/widgets.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; + +/// Result emmitted by hit notifiers (see [LayerHitNotifier]) when a hit is +/// detected on a feature within the respective layer +/// +/// Not emitted if the hit was not over a feature. +@immutable +class LayerHit { + /// `hitValues` from all features hit + /// + /// Ordered in order of the features first-to-last, visually top-to-bottom. + final List hitValues; + + /// Coordinates of the detected hit + /// + /// Note that this may not lie on a feature. + final LatLng point; + + @internal + const LayerHit({required this.hitValues, required this.point}); +} + +/// A [ValueNotifier] that notifies: +/// +/// * a [LayerHit] when a hit is detected on a feature in a layer +/// * `null` when a hit is detected on the layer but not on a feature +typedef LayerHitNotifier = ValueNotifier?>; diff --git a/lib/src/layer/polyline_layer/hit.dart b/lib/src/layer/polyline_layer/hit.dart deleted file mode 100644 index 612bd5483..000000000 --- a/lib/src/layer/polyline_layer/hit.dart +++ /dev/null @@ -1,22 +0,0 @@ -part of 'polyline_layer.dart'; - -/// Result from polyline hit detection -/// -/// Emmitted by [PolylineLayer.hitNotifier]'s [ValueNotifier] -/// ([PolylineHitNotifier]). -class PolylineHit { - /// All hit [Polyline.hitValue]s within the corresponding layer - /// - /// Ordered from first-last, visually top-bottom. - final List hitValues; - - /// Coordinates of the detected hit - /// - /// Note that this may not lie on a [Polyline]. - final LatLng point; - - const PolylineHit._({required this.hitValues, required this.point}); -} - -/// Typedef used on [PolylineLayer.hitNotifier] -typedef PolylineHitNotifier = ValueNotifier?>; diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index ce1a3f992..4f80092e0 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -3,7 +3,7 @@ part of 'polyline_layer.dart'; class PolylinePainter extends CustomPainter { final List> polylines; final MapCamera camera; - final PolylineHitNotifier? hitNotifier; + final LayerHitNotifier? hitNotifier; final double minimumHitbox; final _hits = []; // Avoids repetitive memory reallocation @@ -88,7 +88,7 @@ class PolylinePainter extends CustomPainter { return false; } - hitNotifier!.value = PolylineHit._( + hitNotifier!.value = LayerHit( hitValues: _hits, point: camera.pointToLatLng(math.Point(position.dx, position.dy)), ); diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index c98011d41..b0774c534 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -8,7 +8,6 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; -part 'hit.dart'; part 'painter.dart'; part 'polyline.dart'; @@ -41,16 +40,12 @@ class PolylineLayer extends StatefulWidget { /// /// If a notifier is not provided, hit testing is not performed. /// - /// Notified with a [PolylineHit] if any [Polyline]s are hit, otherwise - /// notified with `null`. - /// - /// Note that a hover event is included as a hit event. If an expensive - /// operation is required on hover, check for equality between the new and old - /// [PolylineHit.hitValues], and avoid doing heavy work if they are the same. + /// Notified with a [LayerHit] if any polylines are hit, otherwise notified + /// with `null`. /// /// See online documentation for more detailed usage instructions. See the /// example project for an example implementation. - final PolylineHitNotifier? hitNotifier; + final LayerHitNotifier? hitNotifier; /// The minimum radius of the hittable area around each [Polyline] in logical /// pixels From b45cff6201374f57c1c19f55922e86f7392a6b21 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Jan 2024 22:09:55 +0000 Subject: [PATCH 21/27] Minor documentation improvements to `LayerHit` --- lib/src/layer/general/hit_detection.dart | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/general/hit_detection.dart b/lib/src/layer/general/hit_detection.dart index 686883ce4..5c7536085 100644 --- a/lib/src/layer/general/hit_detection.dart +++ b/lib/src/layer/general/hit_detection.dart @@ -8,9 +8,12 @@ import 'package:meta/meta.dart'; /// Not emitted if the hit was not over a feature. @immutable class LayerHit { - /// `hitValues` from all features hit + /// `hitValue`s from all features hit (which have `hitValue`s defined) /// - /// Ordered in order of the features first-to-last, visually top-to-bottom. + /// If a feature is hit but has no `hitValue` defined, it will not be included. + /// + /// Ordered by their corresponding feature, first-to-last, visually + /// top-to-bottom. final List hitValues; /// Coordinates of the detected hit From 8e276d11edfc10171ebf5e0e08ce20c3c520c979 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 12 Jan 2024 09:42:40 +0000 Subject: [PATCH 22/27] Renamed `LayerHit` to `LayerHitResult` for improved clarity --- lib/src/layer/general/hit_detection.dart | 8 ++++---- lib/src/layer/polyline_layer/painter.dart | 2 +- lib/src/layer/polyline_layer/polyline_layer.dart | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/layer/general/hit_detection.dart b/lib/src/layer/general/hit_detection.dart index 5c7536085..2e28c211e 100644 --- a/lib/src/layer/general/hit_detection.dart +++ b/lib/src/layer/general/hit_detection.dart @@ -7,7 +7,7 @@ import 'package:meta/meta.dart'; /// /// Not emitted if the hit was not over a feature. @immutable -class LayerHit { +class LayerHitResult { /// `hitValue`s from all features hit (which have `hitValue`s defined) /// /// If a feature is hit but has no `hitValue` defined, it will not be included. @@ -22,11 +22,11 @@ class LayerHit { final LatLng point; @internal - const LayerHit({required this.hitValues, required this.point}); + const LayerHitResult({required this.hitValues, required this.point}); } /// A [ValueNotifier] that notifies: /// -/// * a [LayerHit] when a hit is detected on a feature in a layer +/// * a [LayerHitResult] when a hit is detected on a feature in a layer /// * `null` when a hit is detected on the layer but not on a feature -typedef LayerHitNotifier = ValueNotifier?>; +typedef LayerHitNotifier = ValueNotifier?>; diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 4f80092e0..6637c4a84 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -88,7 +88,7 @@ class PolylinePainter extends CustomPainter { return false; } - hitNotifier!.value = LayerHit( + hitNotifier!.value = LayerHitResult( hitValues: _hits, point: camera.pointToLatLng(math.Point(position.dx, position.dy)), ); diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index b0774c534..25d14010e 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -40,8 +40,8 @@ class PolylineLayer extends StatefulWidget { /// /// If a notifier is not provided, hit testing is not performed. /// - /// Notified with a [LayerHit] if any polylines are hit, otherwise notified - /// with `null`. + /// Notified with a [LayerHitResult] if any polylines are hit, otherwise + /// notified with `null`. /// /// See online documentation for more detailed usage instructions. See the /// example project for an example implementation. From f56828f3752469df24179ec785b21d8828837ff9 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 12 Jan 2024 21:58:17 +0000 Subject: [PATCH 23/27] Minor documentation improvements --- lib/src/layer/polyline_layer/polyline_layer.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 25d14010e..ade06e6e0 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -18,7 +18,9 @@ class PolylineLayer extends StatefulWidget { /// Acceptable extent outside of viewport before culling polyline segments /// - /// May need to be increased if the [Polyline.borderStrokeWidth] is large. + /// May need to be increased if the [Polyline.strokeWidth] + + /// [Polyline.borderStrokeWidth] is large. See online documentation for more + /// information. /// /// Defaults to 0: cull aggressively. Set to `null` to disable culling. final double? cullingMargin; From 1915959db8b0bccf63a394d138b02189227588ab Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 12 Jan 2024 23:02:07 +0000 Subject: [PATCH 24/27] Adjusted default `PolylineLayer.cullingMargin` value Minor improvements to Polyline example --- example/lib/pages/polyline.dart | 2 +- lib/src/layer/polyline_layer/polyline_layer.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 83f55a96a..05fbc5bb7 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -53,7 +53,7 @@ class _PolylinePageState extends State { ), const Polyline( points: [ - LatLng(55.5, -0.09), + LatLng(51.74904, -10.32324), LatLng(54.3498, -6.2603), LatLng(52.8566, 2.3522), ], diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index ade06e6e0..55c726d8d 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -22,7 +22,7 @@ class PolylineLayer extends StatefulWidget { /// [Polyline.borderStrokeWidth] is large. See online documentation for more /// information. /// - /// Defaults to 0: cull aggressively. Set to `null` to disable culling. + /// Defaults to 10. Set to `null` to disable culling. final double? cullingMargin; /// Distance between two mergeable polyline points, in decimal degrees scaled @@ -61,7 +61,7 @@ class PolylineLayer extends StatefulWidget { const PolylineLayer({ super.key, required this.polylines, - this.cullingMargin = 0, + this.cullingMargin = 10, this.simplificationTolerance = 1, this.hitNotifier, this.minimumHitbox = 10, From 7eba1435b91af84c803e2ac7bdf231657c680fb6 Mon Sep 17 00:00:00 2001 From: Luka S Date: Fri, 12 Jan 2024 23:22:40 +0000 Subject: [PATCH 25/27] Minor documentation improvement --- lib/src/misc/simplify.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 95bdd487a..ef4574e4e 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -12,7 +12,7 @@ double _getSqDist( return dx * dx + dy * dy; } -// square distance from a point to a segment +/// square distance from a point to a segment double _getSqSegDist( LatLng p, LatLng p1, From 111943554601e850484da4a604b52919a46dad95 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 12 Jan 2024 23:40:58 +0000 Subject: [PATCH 26/27] Removed nullability from `simplificationTolerance` Changed `simplificationTolerance` default to 0.5 --- example/lib/pages/polyline.dart | 12 +++++----- .../layer/polygon_layer/polygon_layer.dart | 12 +++++----- .../layer/polyline_layer/polyline_layer.dart | 22 +++++++++---------- 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index 05fbc5bb7..bb1052cb6 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -119,14 +119,14 @@ class _PolylinePageState extends State { final _randomWalk = [const LatLng(44.861294, 13.845086)]; - static const double _initialSimplificationTolerance = 1; + static const double _initialSimplificationTolerance = 0.5; double simplificationTolerance = _initialSimplificationTolerance; @override void initState() { super.initState(); final random = Random(1234); - for (int i = 1; i < 300000; i++) { + for (int i = 1; i < 200000; i++) { final lat = (random.nextDouble() - 0.5) * 0.001; final lon = (random.nextDouble() - 0.6) * 0.001; _randomWalk.add( @@ -199,15 +199,13 @@ class _PolylinePageState extends State { ), child: PolylineLayer( hitNotifier: _hitNotifier, - simplificationTolerance: null, + simplificationTolerance: 0, polylines: [..._polylinesRaw, ...?_hoverLines], ), ), ), PolylineLayer( - simplificationTolerance: simplificationTolerance == 0 - ? null - : simplificationTolerance, + simplificationTolerance: simplificationTolerance, polylines: [ Polyline( points: _randomWalk, @@ -340,7 +338,7 @@ class _SimplificationToleranceSliderState }, onChangeEnd: widget.onChangedTolerance, min: 0, - max: 2.5, + max: 2, divisions: 125, label: _simplificationTolerance == 0 ? 'Disabled' diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index b3ac126cd..fcce81dee 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -124,8 +124,8 @@ class PolygonLayer extends StatelessWidget { /// optimize visual performance in conjunction with improved performance with /// culling. /// - /// Defaults to 1. - final double? simplificationTolerance; + /// Defaults to 0.5. Set to 0 to disable simplification. + final double simplificationTolerance; /// Whether to draw per-polygon labels /// @@ -141,7 +141,7 @@ class PolygonLayer extends StatelessWidget { super.key, required this.polygons, this.polygonCulling = true, - this.simplificationTolerance = 1, + this.simplificationTolerance = 0.5, this.polygonLabels = true, this.drawLabelsLast = false, }); @@ -178,7 +178,7 @@ class PolygonPainter extends CustomPainter { final LatLngBounds bounds; final bool polygonLabels; final bool drawLabelsLast; - final double? simplificationTolerance; + final double simplificationTolerance; PolygonPainter({ required this.polygons, @@ -210,10 +210,10 @@ class PolygonPainter extends CustomPainter { } List getOffsets(Offset origin, List points) { - final renderedPoints = simplificationTolerance != null + final renderedPoints = simplificationTolerance != 0 ? simplify( points, - simplificationTolerance! / pow(2, camera.zoom.floor()), + simplificationTolerance / pow(2, camera.zoom.floor()), highestQuality: true, ) : points; diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 55c726d8d..fddb16a2f 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -35,8 +35,8 @@ class PolylineLayer extends StatefulWidget { /// optimize visual performance in conjunction with improved performance with /// culling. /// - /// Defaults to 1. Set to `null` to disable simplification. - final double? simplificationTolerance; + /// Defaults to 0.5. Set to 0 to disable simplification. + final double simplificationTolerance; /// A notifier to be notified when a hit test occurs on the layer /// @@ -62,7 +62,7 @@ class PolylineLayer extends StatefulWidget { super.key, required this.polylines, this.cullingMargin = 10, - this.simplificationTolerance = 1, + this.simplificationTolerance = 0.5, this.hitNotifier, this.minimumHitbox = 10, }); @@ -86,18 +86,18 @@ class _PolylineLayerState extends State> { // IF old no & new no, nothing // IF old yes & new yes & (different tolerance | different lines), both // otherwise, nothing - if (oldWidget.simplificationTolerance != null && - widget.simplificationTolerance != null && + if (oldWidget.simplificationTolerance != 0 && + widget.simplificationTolerance != 0 && (!listEquals(oldWidget.polylines, widget.polylines) || oldWidget.simplificationTolerance != widget.simplificationTolerance)) { _cachedSimplifiedPolylines.clear(); _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); - } else if (oldWidget.simplificationTolerance != null && - widget.simplificationTolerance == null) { + } else if (oldWidget.simplificationTolerance != 0 && + widget.simplificationTolerance == 0) { _cachedSimplifiedPolylines.clear(); - } else if (oldWidget.simplificationTolerance == null && - widget.simplificationTolerance != null) { + } else if (oldWidget.simplificationTolerance == 0 && + widget.simplificationTolerance != 0) { _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); } } @@ -110,7 +110,7 @@ class _PolylineLayerState extends State> { child: CustomPaint( painter: PolylinePainter( polylines: _aggressivelyCullPolylines( - polylines: widget.simplificationTolerance == null + polylines: widget.simplificationTolerance == 0 ? widget.polylines : _computeZoomLevelSimplification(camera.zoom.floor()), camera: camera, @@ -131,7 +131,7 @@ class _PolylineLayerState extends State> { (polyline) => polyline.copyWithNewPoints( simplify( polyline.points, - widget.simplificationTolerance! / math.pow(2, zoom), + widget.simplificationTolerance / math.pow(2, zoom), highestQuality: true, ), ), From 12612d53b574e5526e02a15180f39f929976f9b2 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sat, 13 Jan 2024 00:01:54 +0000 Subject: [PATCH 27/27] Minor improvements to Polyline example --- example/lib/pages/polyline.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/pages/polyline.dart b/example/lib/pages/polyline.dart index bb1052cb6..9eb11e99a 100644 --- a/example/lib/pages/polyline.dart +++ b/example/lib/pages/polyline.dart @@ -339,7 +339,7 @@ class _SimplificationToleranceSliderState onChangeEnd: widget.onChangedTolerance, min: 0, max: 2, - divisions: 125, + divisions: 100, label: _simplificationTolerance == 0 ? 'Disabled' : _simplificationTolerance.toStringAsFixed(2),