diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index dee8474af..0bc3bff8b 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -84,7 +84,7 @@ class _PolygonLayerState extends State { final projected = _cachedProjectedPolygons ??= List.generate( widget.polygons.length, - (i) => _ProjectedPolygon.fromPolygon( + (i) => _ProjectedPolygon._fromPolygon( camera.crs.projection, widget.polygons[i], ), diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index f597244c7..ca1e74a94 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -12,7 +12,7 @@ class _ProjectedPolygon { this.holePoints, }); - _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) + _ProjectedPolygon._fromPolygon(Projection projection, Polygon polygon) : this._( polygon: polygon, points: List.generate( diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 28ef16ec2..dddf9c2cf 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'; /// [CustomPainter] for [Polygon]s. class _PolylinePainter extends CustomPainter { /// Reference to the list of [Polyline]s. - final List> polylines; + final List<_ProjectedPolyline> polylines; /// Reference to the [MapCamera]. final MapCamera camera; @@ -20,18 +20,6 @@ class _PolylinePainter extends CustomPainter { 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; @@ -41,8 +29,11 @@ class _PolylinePainter extends CustomPainter { final origin = camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; - for (final polyline in polylines.reversed) { - if (polyline.hitValue == null) continue; + for (final projectedPolyline in polylines.reversed) { + final polyline = projectedPolyline.polyline as Polyline; + 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 @@ -51,11 +42,11 @@ class _PolylinePainter extends CustomPainter { // continue; // } - final offsets = getOffsets(origin, polyline.points); + final offsets = getOffsetsXY(camera, origin, projectedPolyline.points); final strokeWidth = polyline.useStrokeWidthInMeter ? _metersToStrokeWidth( origin, - polyline.points.first, + _unproject(projectedPolyline.points.first), offsets.first, polyline.strokeWidth, ) @@ -141,8 +132,9 @@ class _PolylinePainter extends CustomPainter { final origin = camera.project(camera.center).toOffset() - camera.size.toOffset() / 2; - for (final polyline in polylines) { - final offsets = getOffsets(origin, polyline.points); + for (final projectedPolyline in polylines) { + final polyline = projectedPolyline.polyline; + final offsets = getOffsetsXY(camera, origin, projectedPolyline.points); if (offsets.isEmpty) { continue; } @@ -159,7 +151,7 @@ class _PolylinePainter extends CustomPainter { if (polyline.useStrokeWidthInMeter) { strokeWidth = _metersToStrokeWidth( origin, - polyline.points.first, + _unproject(projectedPolyline.points.first), offsets.first, polyline.strokeWidth, ); @@ -281,10 +273,13 @@ class _PolylinePainter extends CustomPainter { double strokeWidthInMeters, ) { final r = _distance.offset(p0, strokeWidthInMeters, 180); - final delta = o0 - getOffset(origin, r); + final delta = o0 - getOffset(camera, origin, r); return delta.distance; } + LatLng _unproject(DoublePoint p0) => + camera.crs.projection.unprojectXY(p0.x, p0.y); + @override bool shouldRepaint(_PolylinePainter oldDelegate) => false; } diff --git a/lib/src/layer/polyline_layer/polyline.dart b/lib/src/layer/polyline_layer/polyline.dart index 30bca02da..d1e5f9788 100644 --- a/lib/src/layer/polyline_layer/polyline.dart +++ b/lib/src/layer/polyline_layer/polyline.dart @@ -52,21 +52,6 @@ class Polyline { 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) || diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index e908effe8..cc89a2baf 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -5,11 +5,13 @@ 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/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; part 'painter.dart'; part 'polyline.dart'; +part 'projected_polyline.dart'; /// A [Polyline] (aka. LineString) layer for [FlutterMap]. @immutable @@ -26,17 +28,14 @@ class PolylineLayer extends StatefulWidget { /// Defaults to 10. Set to `null` to disable culling. final double? cullingMargin; - /// Distance between two mergeable polyline points, in decimal degrees scaled + /// Distance between two neighboring polyline points, in logical pixels scaled /// to floored zoom /// - /// Increasing results in a more jagged, less accurate simplification, with - /// improved performance; and vice versa. + /// Increasing this value results in points further apart being collapsed and + /// thus more simplified polylines. Higher values improve performance at the + /// cost of visual fidelity 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 0.5. Set to 0 to disable simplification. + /// Defaults to 0.4. Set to 0 to disable simplification. final double simplificationTolerance; /// A notifier to be notified when a hit test occurs on the layer @@ -64,7 +63,7 @@ class PolylineLayer extends StatefulWidget { super.key, required this.polylines, this.cullingMargin = 10, - this.simplificationTolerance = 0.5, + this.simplificationTolerance = 0.4, this.hitNotifier, this.minimumHitbox = 10, }); @@ -74,47 +73,51 @@ class PolylineLayer extends StatefulWidget { } class _PolylineLayerState extends State> { - final _cachedSimplifiedPolylines = >>{}; + List<_ProjectedPolyline>? _cachedProjectedPolylines; + final _cachedSimplifiedPolylines = >{}; final _culledPolylines = - >[]; // Avoids repetitive memory reallocation + <_ProjectedPolyline>[]; // 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 != 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 != 0 && - widget.simplificationTolerance == 0) { - _cachedSimplifiedPolylines.clear(); - } else if (oldWidget.simplificationTolerance == 0 && - widget.simplificationTolerance != 0) { - _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); - } + // Reuse cache + if (widget.simplificationTolerance != 0 && + oldWidget.simplificationTolerance == widget.simplificationTolerance && + listEquals(oldWidget.polylines, widget.polylines)) return; + + _cachedSimplifiedPolylines.clear(); + _cachedProjectedPolylines = null; } @override Widget build(BuildContext context) { final camera = MapCamera.of(context); + final projected = _cachedProjectedPolylines ??= List.generate( + widget.polylines.length, + (i) => _ProjectedPolyline._fromPolyline( + camera.crs.projection, + widget.polylines[i], + ), + growable: false, + ); + final simplified = widget.simplificationTolerance == 0 - ? widget.polylines - : _computeZoomLevelSimplification(camera.zoom.floor()); + ? projected + : _cachedSimplifiedPolylines[camera.zoom.floor()] ??= + _computeZoomLevelSimplification( + polylines: projected, + pixelTolerance: widget.simplificationTolerance, + camera: camera, + ); final culled = widget.cullingMargin == null ? simplified : _aggressivelyCullPolylines( + projection: camera.crs.projection, polylines: simplified, camera: camera, cullingMargin: widget.cullingMargin!, @@ -133,29 +136,17 @@ class _PolylineLayerState extends State> { ); } - // TODO BEFORE v7: Use same algorithm as polygons - List> _computeZoomLevelSimplification(int zoom) => - _cachedSimplifiedPolylines[zoom] ??= widget.polylines - .map( - (polyline) => polyline.copyWithNewPoints( - simplify( - points: polyline.points, - tolerance: widget.simplificationTolerance / math.pow(2, zoom), - highQuality: true, - ), - ), - ) - .toList(); - - List> _aggressivelyCullPolylines({ - required List> polylines, + List<_ProjectedPolyline> _aggressivelyCullPolylines({ + required Projection projection, + required List<_ProjectedPolyline> polylines, required MapCamera camera, required double cullingMargin, }) { _culledPolylines.clear(); final bounds = camera.visibleBounds; - final margin = cullingMargin / math.pow(2, camera.zoom.floorToDouble()); + final margin = cullingMargin / math.pow(2, camera.zoom); + // 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( @@ -169,58 +160,92 @@ class _PolylineLayerState extends State> { ), ); - for (final polyline in polylines) { + // segment is visible + final projBounds = Bounds( + projection.project(boundsAdjusted.southWest), + projection.project(boundsAdjusted.northEast), + ); + + for (final projectedPolyline in polylines) { + final polyline = projectedPolyline.polyline; + // Gradient poylines cannot be easily segmented if (polyline.gradientColors != null) { - _culledPolylines.add(polyline); + _culledPolylines.add(projectedPolyline); 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 + bool containsSegment = false; + for (int i = 0; i < projectedPolyline.points.length - 1; i++) { + // Current segment (p1, p2). + final p1 = projectedPolyline.points[i]; + final p2 = projectedPolyline.points[i + 1]; + + containsSegment = projBounds.aabbContainsLine(p1.x, p1.y, p2.x, p2.y); + if (containsSegment) { 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 we cannot see this segment but have seen previous ones, flush the last polyline fragment. if (start != -1) { - // partial start - final segment = polyline.points.sublist(start, i + 1); - _culledPolylines.add(polyline.copyWithNewPoints(segment)); + _culledPolylines.add(_ProjectedPolyline._( + polyline: polyline, + points: projectedPolyline.points.sublist(start, i + 1), + )); + + // Reset start. start = -1; } - if (start != -1) { - start = i; - } } } - if (fullyVisible) _culledPolylines.add(polyline); + // If the last segment was visible push that last visible polyline + // fragment, which may also be the entire polyline if `start == 0`. + if (containsSegment) { + _culledPolylines.add( + start == 0 + ? projectedPolyline + : _ProjectedPolyline._( + polyline: polyline, + // Special case: the entire polyline is visible + points: projectedPolyline.points.sublist(start), + ), + ); + } } return _culledPolylines; } + + static List<_ProjectedPolyline> _computeZoomLevelSimplification({ + required List<_ProjectedPolyline> polylines, + required double pixelTolerance, + required MapCamera camera, + }) { + final tolerance = getEffectiveSimplificationTolerance( + crs: camera.crs, + zoom: camera.zoom.floor(), + pixelTolerance: pixelTolerance, + ); + + return List<_ProjectedPolyline>.generate( + polylines.length, + (i) { + final polyline = polylines[i]; + + return _ProjectedPolyline._( + polyline: polyline.polyline, + points: simplifyPoints( + points: polyline.points, + tolerance: tolerance, + highQuality: true, + ), + ); + }, + growable: false, + ); + } } diff --git a/lib/src/layer/polyline_layer/projected_polyline.dart b/lib/src/layer/polyline_layer/projected_polyline.dart new file mode 100644 index 000000000..a9f2facca --- /dev/null +++ b/lib/src/layer/polyline_layer/projected_polyline.dart @@ -0,0 +1,25 @@ +part of 'polyline_layer.dart'; + +@immutable +class _ProjectedPolyline { + final Polyline polyline; + final List points; + + const _ProjectedPolyline._({ + required this.polyline, + required this.points, + }); + + _ProjectedPolyline._fromPolyline(Projection projection, Polyline polyline) + : this._( + polyline: polyline, + points: List.generate( + polyline.points.length, + (j) { + final (x, y) = projection.projectXY(polyline.points[j]); + return DoublePoint(x, y); + }, + growable: false, + ), + ); +} diff --git a/lib/src/misc/bounds.dart b/lib/src/misc/bounds.dart index 4c4de27a2..e7270aefb 100644 --- a/lib/src/misc/bounds.dart +++ b/lib/src/misc/bounds.dart @@ -111,7 +111,7 @@ class Bounds { (b.max.y >= min.y); } - bool aabbContainsLine(num x1, num y1, num x2, num y2) { + bool aabbContainsLine(double x1, double y1, double x2, double y2) { // Completely outside. if ((x1 <= min.x && x2 <= min.x) || (y1 <= min.y && y2 <= min.y) || @@ -122,13 +122,13 @@ class Bounds { final m = (y2 - y1) / (x2 - x1); - num y = m * (min.x - x1) + y1; + double 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; + double x = (min.y - y1) / m + x1; if (x > min.x && x < max.x) return true; x = (max.y - y1) / m + x1; diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 84dae5177..ea0828ec6 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:flutter_map/src/geo/crs.dart'; -import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; /// Internal double-precision point/vector implementation not to be used in publicly. @@ -125,29 +124,6 @@ List simplifyDouglasPeucker( return simplified; } -List simplify({ - required List points, - required double tolerance, - required bool highQuality, -}) { - // Don't simplify anything less than a square - if (points.length <= 4) return points; - - List nextPoints = List.generate( - points.length, - (i) => DoublePoint(points[i].longitude, points[i].latitude), - ); - final double sqTolerance = tolerance * tolerance; - nextPoints = highQuality - ? simplifyDouglasPeucker(nextPoints, sqTolerance) - : simplifyRadialDist(nextPoints, sqTolerance); - - return List.generate( - nextPoints.length, - (i) => LatLng(nextPoints[i].y, nextPoints[i].x), - ); -} - List simplifyPoints({ required final List points, required double tolerance,