From ba17b39013bc94e325bc6ba1ee1758eba59cb9c0 Mon Sep 17 00:00:00 2001 From: Luka S Date: Sat, 8 Jun 2024 10:42:40 +0100 Subject: [PATCH] feat: implement efficient(-ish) change detection for `PolygonLayer` & `PolylineLayer` (#1904) --- example/lib/pages/polygon_perf_stress.dart | 18 +- example/lib/pages/polyline_perf_stress.dart | 2 +- lib/src/layer/circle_layer/circle_marker.dart | 7 +- .../layer/polygon_layer/polygon_layer.dart | 152 ++++--------- .../polygon_layer/projected_polygon.dart | 2 +- .../layer/polyline_layer/polyline_layer.dart | 172 +++++---------- .../polyline_layer/projected_polyline.dart | 3 +- .../internal_hit_detectable.dart | 6 +- .../state.dart | 205 ++++++++++++++++++ .../widget.dart | 49 +++++ 10 files changed, 367 insertions(+), 249 deletions(-) create mode 100644 lib/src/layer/shared/layer_projection_simplification/state.dart create mode 100644 lib/src/layer/shared/layer_projection_simplification/widget.dart diff --git a/example/lib/pages/polygon_perf_stress.dart b/example/lib/pages/polygon_perf_stress.dart index 47d48dfed..d6997da9d 100644 --- a/example/lib/pages/polygon_perf_stress.dart +++ b/example/lib/pages/polygon_perf_stress.dart @@ -19,7 +19,7 @@ class PolygonPerfStressPage extends StatefulWidget { } class _PolygonPerfStressPageState extends State { - double simplificationTolerance = 0.5; + double simplificationTolerance = 0.3; bool useAltRendering = true; double borderThickness = 1; @@ -63,15 +63,13 @@ class _PolygonPerfStressPageState extends State { openStreetMapTileLayer, FutureBuilder( future: geoJsonParser, - builder: (context, geoJsonParser) => - geoJsonParser.connectionState != ConnectionState.done || - geoJsonParser.data == null - ? const SizedBox.shrink() - : PolygonLayer( - polygons: geoJsonParser.data!.polygons, - useAltRendering: useAltRendering, - simplificationTolerance: simplificationTolerance, - ), + builder: (context, geoJsonParser) => geoJsonParser.data == null + ? const SizedBox.shrink() + : PolygonLayer( + polygons: geoJsonParser.data!.polygons, + useAltRendering: useAltRendering, + simplificationTolerance: simplificationTolerance, + ), ), ], ), diff --git a/example/lib/pages/polyline_perf_stress.dart b/example/lib/pages/polyline_perf_stress.dart index fcfcf3f65..34ab85f44 100644 --- a/example/lib/pages/polyline_perf_stress.dart +++ b/example/lib/pages/polyline_perf_stress.dart @@ -19,7 +19,7 @@ class PolylinePerfStressPage extends StatefulWidget { } class _PolylinePerfStressPageState extends State { - double simplificationTolerance = 0.5; + double simplificationTolerance = 0.3; final _randomWalk = [const LatLng(44.861294, 13.845086)]; diff --git a/lib/src/layer/circle_layer/circle_marker.dart b/lib/src/layer/circle_layer/circle_marker.dart index 23f37ba5f..f4776f713 100644 --- a/lib/src/layer/circle_layer/circle_marker.dart +++ b/lib/src/layer/circle_layer/circle_marker.dart @@ -3,7 +3,7 @@ part of 'circle_layer.dart'; /// Immutable marker options for [CircleMarker]. Circle markers are a more /// simple and performant way to draw markers as the regular [Marker] @immutable -base class CircleMarker extends HitDetectableElement { +class CircleMarker with HitDetectableElement { /// An optional [Key] for the [CircleMarker]. /// This key is not used internally. final Key? key; @@ -27,6 +27,9 @@ base class CircleMarker extends HitDetectableElement { /// Set to true if the radius should use the unit meters. final bool useRadiusInMeter; + @override + final R? hitValue; + /// Constructor to create a new [CircleMarker] object const CircleMarker({ required this.point, @@ -36,6 +39,6 @@ base class CircleMarker extends HitDetectableElement { this.color = const Color(0xFF00FF00), this.borderStrokeWidth = 0.0, this.borderColor = const Color(0xFFFFFF00), - super.hitValue, + this.hitValue, }); } diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index db5fa4299..dd71a4d13 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -7,6 +7,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_in_polygon.dart'; @@ -21,7 +23,8 @@ part 'projected_polygon.dart'; /// A polygon layer for [FlutterMap]. @immutable -class PolygonLayer extends StatefulWidget { +base class PolygonLayer + extends ProjectionSimplificationManagementSupportedWidget { /// [Polygon]s to draw final List> polygons; @@ -52,16 +55,6 @@ class PolygonLayer extends StatefulWidget { /// Defaults to `true`. Disabling is not recommended. final bool polygonCulling; - /// Distance between two neighboring polygon points, in logical pixels scaled - /// to floored zoom - /// - /// Increasing this value results in points further apart being collapsed and - /// thus more simplified polygons. Higher values improve performance at the - /// cost of visual fidelity and vice versa. - /// - /// Defaults to 0.5. Set to 0 to disable simplification. - final double simplificationTolerance; - /// Whether to draw per-polygon labels /// /// Defaults to `true`. @@ -82,79 +75,63 @@ class PolygonLayer extends StatefulWidget { this.useAltRendering = false, this.debugAltRenderer = false, this.polygonCulling = true, - this.simplificationTolerance = 0.5, this.polygonLabels = true, this.drawLabelsLast = false, this.hitNotifier, - }) : assert( - simplificationTolerance >= 0, - 'simplificationTolerance cannot be negative: $simplificationTolerance', - ); + super.simplificationTolerance, + super.useDynamicUpdate, + }) : super(); @override State> createState() => _PolygonLayerState(); } -class _PolygonLayerState extends State> { - List<_ProjectedPolygon>? _cachedProjectedPolygons; - final _cachedSimplifiedPolygons = >>{}; - - double? _devicePixelRatio; +class _PolygonLayerState extends State> + with + ProjectionSimplificationManagement<_ProjectedPolygon, Polygon, + PolygonLayer> { + @override + _ProjectedPolygon projectElement({ + required Projection projection, + required Polygon element, + }) => + _ProjectedPolygon._fromPolygon(projection, element); @override - void didUpdateWidget(PolygonLayer oldWidget) { - super.didUpdateWidget(oldWidget); + _ProjectedPolygon simplifyProjectedElement({ + required _ProjectedPolygon projectedElement, + required double tolerance, + }) => + _ProjectedPolygon._( + polygon: projectedElement.polygon, + points: simplifyPoints( + points: projectedElement.points, + tolerance: tolerance, + highQuality: true, + ), + holePoints: List.generate( + projectedElement.holePoints.length, + (j) => simplifyPoints( + points: projectedElement.holePoints[j], + tolerance: tolerance, + highQuality: true, + ), + growable: false, + ), + ); - if (!listEquals(oldWidget.polygons, widget.polygons)) { - // If the polylines have changed, then both the projections and the - // projection-dependendent simplifications must be invalidated - _cachedProjectedPolygons = null; - _cachedSimplifiedPolygons.clear(); - } else if (oldWidget.simplificationTolerance != - widget.simplificationTolerance) { - // If only the simplification tolerance has changed, this does not affect - // the projections (as that is done before simplification), so only - // invalidate the simplifications - _cachedSimplifiedPolygons.clear(); - } - } + @override + Iterable> getElements(PolygonLayer widget) => widget.polygons; @override Widget build(BuildContext context) { - final camera = MapCamera.of(context); + super.build(context); - final projected = _cachedProjectedPolygons ??= List.generate( - widget.polygons.length, - (i) => _ProjectedPolygon._fromPolygon( - camera.crs.projection, - widget.polygons[i], - ), - growable: false, - ); - - late final List<_ProjectedPolygon> simplified; - if (widget.simplificationTolerance == 0) { - simplified = projected; - } else { - // If the DPR has changed, invalidate the simplification cache - final newDPR = MediaQuery.devicePixelRatioOf(context); - if (newDPR != _devicePixelRatio) { - _devicePixelRatio = newDPR; - _cachedSimplifiedPolygons.clear(); - } - - simplified = _cachedSimplifiedPolygons[camera.zoom.floor()] ??= - _computeZoomLevelSimplification( - camera: camera, - polygons: projected, - pixelTolerance: widget.simplificationTolerance, - devicePixelRatio: newDPR, - ); - } + final camera = MapCamera.of(context); final culled = !widget.polygonCulling - ? simplified - : simplified + ? simplifiedElements.toList() + : simplifiedElements .where( (p) => p.polygon.boundingBox.isOverlapping(camera.visibleBounds), ) @@ -213,45 +190,4 @@ class _PolygonLayerState extends State> { yield prevValue += polygon.holePoints[i].length; } } - - List<_ProjectedPolygon> _computeZoomLevelSimplification({ - required MapCamera camera, - required List<_ProjectedPolygon> polygons, - required double pixelTolerance, - required double devicePixelRatio, - }) { - final tolerance = getEffectiveSimplificationTolerance( - crs: camera.crs, - zoom: camera.zoom.floor(), - pixelTolerance: pixelTolerance, - devicePixelRatio: devicePixelRatio, - ); - - return List<_ProjectedPolygon>.generate( - polygons.length, - (i) { - final polygon = polygons[i]; - final holes = polygon.holePoints; - - return _ProjectedPolygon._( - polygon: polygon.polygon, - points: simplifyPoints( - points: polygon.points, - tolerance: tolerance, - highQuality: true, - ), - holePoints: List.generate( - holes.length, - (j) => simplifyPoints( - points: holes[j], - tolerance: tolerance, - highQuality: true, - ), - growable: false, - ), - ); - }, - growable: false, - ); - } } diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart index c5f0c000a..1c25ef0f5 100644 --- a/lib/src/layer/polygon_layer/projected_polygon.dart +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -1,7 +1,7 @@ part of 'polygon_layer.dart'; @immutable -base class _ProjectedPolygon extends HitDetectableElement { +class _ProjectedPolygon with HitDetectableElement { final Polygon polygon; final List points; final List> holePoints; diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index 68acb0bb4..029b21794 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -6,6 +6,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/shared/layer_interactivity/internal_hit_detectable.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; import 'package:flutter_map/src/layer/shared/line_patterns/pixel_hiker.dart'; import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/simplify.dart'; @@ -17,7 +19,8 @@ part 'projected_polyline.dart'; /// A [Polyline] (aka. LineString) layer for [FlutterMap]. @immutable -class PolylineLayer extends StatefulWidget { +base class PolylineLayer + extends ProjectionSimplificationManagementSupportedWidget { /// [Polyline]s to draw final List> polylines; @@ -30,16 +33,6 @@ class PolylineLayer extends StatefulWidget { /// Defaults to 10. Set to `null` to disable culling. final double? cullingMargin; - /// Distance between two neighboring polyline points, in logical pixels scaled - /// to floored zoom - /// - /// 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. - /// - /// Defaults to 0.4. Set to 0 to disable simplification. - final double simplificationTolerance; - /// {@macro fm.lhn.layerHitNotifier.usage} final LayerHitNotifier? hitNotifier; @@ -57,83 +50,59 @@ class PolylineLayer extends StatefulWidget { super.key, required this.polylines, this.cullingMargin = 10, - this.simplificationTolerance = 0.4, this.hitNotifier, this.minimumHitbox = 10, - }) : assert( - simplificationTolerance >= 0, - 'simplificationTolerance cannot be negative: $simplificationTolerance', - ); + super.useDynamicUpdate, + super.simplificationTolerance, + }) : super(); @override State> createState() => _PolylineLayerState(); } -class _PolylineLayerState extends State> { - List<_ProjectedPolyline>? _cachedProjectedPolylines; - final _cachedSimplifiedPolylines = >>{}; - - double? _devicePixelRatio; +class _PolylineLayerState extends State> + with + ProjectionSimplificationManagement<_ProjectedPolyline, Polyline, + PolylineLayer> { + @override + _ProjectedPolyline projectElement({ + required Projection projection, + required Polyline element, + }) => + _ProjectedPolyline._fromPolyline(projection, element); @override - void didUpdateWidget(PolylineLayer oldWidget) { - super.didUpdateWidget(oldWidget); + _ProjectedPolyline simplifyProjectedElement({ + required _ProjectedPolyline projectedElement, + required double tolerance, + }) => + _ProjectedPolyline._( + polyline: projectedElement.polyline, + points: simplifyPoints( + points: projectedElement.points, + tolerance: tolerance, + highQuality: true, + ), + ); - if (!listEquals(oldWidget.polylines, widget.polylines)) { - // If the polylines have changed, then both the projections and the - // projection-dependendent simplifications must be invalidated - _cachedProjectedPolylines = null; - _cachedSimplifiedPolylines.clear(); - } else if (oldWidget.simplificationTolerance != - widget.simplificationTolerance) { - // If only the simplification tolerance has changed, this does not affect - // the projections (as that is done before simplification), so only - // invalidate the simplifications - _cachedSimplifiedPolylines.clear(); - } - } + @override + Iterable> getElements(PolylineLayer widget) => + widget.polylines; @override Widget build(BuildContext context) { - final camera = MapCamera.of(context); + super.build(context); - final projected = _cachedProjectedPolylines ??= List.generate( - widget.polylines.length, - (i) => _ProjectedPolyline._fromPolyline( - camera.crs.projection, - widget.polylines[i], - ), - growable: false, - ); - - late final List<_ProjectedPolyline> simplified; - if (widget.simplificationTolerance == 0) { - simplified = projected; - } else { - // If the DPR has changed, invalidate the simplification cache - final newDPR = MediaQuery.devicePixelRatioOf(context); - if (newDPR != _devicePixelRatio) { - _devicePixelRatio = newDPR; - _cachedSimplifiedPolylines.clear(); - } - - simplified = _cachedSimplifiedPolylines[camera.zoom.floor()] ??= - _computeZoomLevelSimplification( - camera: camera, - polylines: projected, - pixelTolerance: widget.simplificationTolerance, - devicePixelRatio: newDPR, - ); - } + final camera = MapCamera.of(context); final culled = widget.cullingMargin == null - ? simplified + ? simplifiedElements.toList() : _aggressivelyCullPolylines( projection: camera.crs.projection, - polylines: simplified, + polylines: simplifiedElements, camera: camera, cullingMargin: widget.cullingMargin!, - ); + ).toList(); return MobileLayerTransformer( child: CustomPaint( @@ -148,14 +117,12 @@ class _PolylineLayerState extends State> { ); } - List<_ProjectedPolyline> _aggressivelyCullPolylines({ + Iterable<_ProjectedPolyline> _aggressivelyCullPolylines({ required Projection projection, - required List<_ProjectedPolyline> polylines, + required Iterable<_ProjectedPolyline> polylines, required MapCamera camera, required double cullingMargin, - }) { - final culledPolylines = <_ProjectedPolyline>[]; - + }) sync* { final bounds = camera.visibleBounds; final margin = cullingMargin / math.pow(2, camera.zoom); @@ -179,7 +146,7 @@ class _PolylineLayerState extends State> { // Gradient poylines cannot be easily segmented if (polyline.gradientColors != null) { - culledPolylines.add(projectedPolyline); + yield projectedPolyline; continue; } @@ -199,11 +166,9 @@ class _PolylineLayerState extends State> { } else { // If we cannot see this segment but have seen previous ones, flush the last polyline fragment. if (start != -1) { - culledPolylines.add( - _ProjectedPolyline._( - polyline: polyline, - points: projectedPolyline.points.sublist(start, i + 1), - ), + yield _ProjectedPolyline._( + polyline: polyline, + points: projectedPolyline.points.sublist(start, i + 1), ); // Reset start. @@ -215,49 +180,14 @@ class _PolylineLayerState extends State> { // 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), - ), - ); + yield start == 0 + ? projectedPolyline + : _ProjectedPolyline._( + polyline: polyline, + // Special case: the entire polyline is visible + points: projectedPolyline.points.sublist(start), + ); } } - - return culledPolylines; - } - - List<_ProjectedPolyline> _computeZoomLevelSimplification({ - required MapCamera camera, - required List<_ProjectedPolyline> polylines, - required double pixelTolerance, - required double devicePixelRatio, - }) { - final tolerance = getEffectiveSimplificationTolerance( - crs: camera.crs, - zoom: camera.zoom.floor(), - pixelTolerance: pixelTolerance, - devicePixelRatio: devicePixelRatio, - ); - - 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 index 8c0b6c424..eca98465a 100644 --- a/lib/src/layer/polyline_layer/projected_polyline.dart +++ b/lib/src/layer/polyline_layer/projected_polyline.dart @@ -1,8 +1,7 @@ part of 'polyline_layer.dart'; @immutable -base class _ProjectedPolyline - extends HitDetectableElement { +class _ProjectedPolyline with HitDetectableElement { final Polyline polyline; final List points; diff --git a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart index 1a6ef6c2c..3a9c3c8ee 100644 --- a/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart +++ b/lib/src/layer/shared/layer_interactivity/internal_hit_detectable.dart @@ -6,9 +6,7 @@ import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @internal -abstract base class HitDetectableElement { - const HitDetectableElement({this.hitValue}); - +mixin HitDetectableElement { /// {@template fm.hde.hitValue} /// Value to notify layer's `hitNotifier` with (such as /// [PolygonLayer.hitNotifier]) @@ -19,7 +17,7 @@ abstract base class HitDetectableElement { /// The object should have a valid & useful equality, as it may be used /// by FM internals. /// {@endtemplate} - final R? hitValue; + R? get hitValue; } @internal diff --git a/lib/src/layer/shared/layer_projection_simplification/state.dart b/lib/src/layer/shared/layer_projection_simplification/state.dart new file mode 100644 index 000000000..db30f201d --- /dev/null +++ b/lib/src/layer/shared/layer_projection_simplification/state.dart @@ -0,0 +1,205 @@ +import 'dart:collection'; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/widget.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; +import 'package:meta/meta.dart'; + +/// A mixin to be applied on the [State] of a +/// [ProjectionSimplificationManagementSupportedWidget], which provides +/// pre-projection and pre-simplification support for layers that paint elements +/// (particularly [PolylineLayer] and [PolygonLayer]), and updates them as +/// necessary +/// +/// Subclasses must implement [build], and invoke `super.build()` (but ignore +/// the result) at the start. The `build` method should/can then use +/// [simplifiedElements]. +mixin ProjectionSimplificationManagement< + ProjectedElement extends Object, + Element extends Object, + W extends ProjectionSimplificationManagementSupportedWidget> on State { + /// Project [Element] to [ProjectedElement] using the specified [projection] + ProjectedElement projectElement({ + required Projection projection, + required Element element, + }); + + /// Simplify the points of [ProjectedElement] with the given [tolerance] + /// + /// Should not call [getEffectiveSimplificationTolerance]; [tolerance] has + /// already been processed. + ProjectedElement simplifyProjectedElement({ + required ProjectedElement projectedElement, + required double tolerance, + }); + + /// Return the individual elements given the + /// [ProjectionSimplificationManagementSupportedWidget] + Iterable getElements(W widget); + + /// An iterable of simplified [ProjectedElement]s, which is always ready + /// after the [build] method has been invoked, and should then be used in the + /// next [build] stage (usually culling) + /// + /// Do not use before invoking [build]. Only necessarily up to date directly + /// after [build] has been invoked. + late Iterable simplifiedElements; + + final _cachedProjectedElements = SplayTreeMap(); + final _cachedSimplifiedElements = + >{}; + + double? _devicePixelRatio; + + @mustCallSuper + @override + void didUpdateWidget(W oldWidget) { + super.didUpdateWidget(oldWidget); + + if (!widget.useDynamicUpdate) return; + + final camera = MapCamera.of(context); + + // If the simplification tolerance has changed, then clear all + // simplifications to allow `build` to re-simplify. + final hasSimplficationToleranceChanged = + oldWidget.simplificationTolerance != widget.simplificationTolerance; + if (hasSimplficationToleranceChanged) _cachedSimplifiedElements.clear(); + + final elements = getElements(widget); + + // We specifically only use basic equality here, and not deep, since deep + // will always be equal. + if (getElements(oldWidget) == elements) return; + + // Loop through all polygons in the new widget + // If not in the projection cache, then re-project. Also, do the same for + // the simplification cache, across all zoom levels for each polygon. + // Then, remove all polygons no longer in the new widget from each cache. + // + // This is an O(n^3) operation, assuming n is the number of polygons + // (assuming they are all similar, otherwise exact runtime will depend on + // existing cache lengths, etc.). However, compared to previous versions, it + // takes approximately the same duration, as it relieves the work from the + // `build` method. + for (final element in getElements(widget)) { + final existingProjection = _cachedProjectedElements[element.hashCode]; + + if (existingProjection == null) { + _cachedProjectedElements[element.hashCode] = + projectElement(projection: camera.crs.projection, element: element); + + if (hasSimplficationToleranceChanged) continue; + + for (final MapEntry(key: zoomLvl, value: simplifiedElements) + in _cachedSimplifiedElements.entries) { + final simplificationTolerance = getEffectiveSimplificationTolerance( + crs: camera.crs, + zoom: zoomLvl, + // When the tolerance changes, this method handles resetting and filling + pixelTolerance: widget.simplificationTolerance, + // When the DPR changes, the `build` method handles resetting and filling + devicePixelRatio: MediaQuery.devicePixelRatioOf(context), + ); + + final existingSimplification = simplifiedElements[element.hashCode]; + + if (existingSimplification == null) { + _cachedSimplifiedElements[zoomLvl]![element.hashCode] = + simplifyProjectedElement( + projectedElement: _cachedProjectedElements[element.hashCode]!, + tolerance: simplificationTolerance, + ); + } + } + } + } + + _cachedProjectedElements + .removeWhere((k, v) => !elements.map((p) => p.hashCode).contains(k)); + + for (final simplifiedElement in _cachedSimplifiedElements.values) { + simplifiedElement + .removeWhere((k, v) => !elements.map((p) => p.hashCode).contains(k)); + } + } + + @mustCallSuper + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + // Performed once only, at load - projects all initial polygons + if (_cachedProjectedElements.isEmpty) { + final camera = MapCamera.of(context); + + for (final element in getElements(widget)) { + _cachedProjectedElements[element.hashCode] = + projectElement(projection: camera.crs.projection, element: element); + } + } + } + + @mustBeOverridden + @mustCallSuper + @override + Widget build(BuildContext context) { + final camera = MapCamera.of(context); + + // The `build` method handles initial simplification, re-simplification only + // when the DPR has changed, and re-simplification implicitly when the + // tolerance is changed (and the cache is emptied by `didUpdateWidget`). + if (widget.simplificationTolerance == 0) { + simplifiedElements = _cachedProjectedElements.values; + } else { + // If the DPR has changed, invalidate the simplification cache + final newDPR = MediaQuery.devicePixelRatioOf(context); + if (newDPR != _devicePixelRatio) { + _devicePixelRatio = newDPR; + _cachedSimplifiedElements.clear(); + } + + simplifiedElements = (_cachedSimplifiedElements[camera.zoom.floor()] ??= + SplayTreeMap.fromIterables( + _cachedProjectedElements.keys, + _simplifyElements( + camera: camera, + projectedElements: _cachedProjectedElements.values, + pixelTolerance: widget.simplificationTolerance, + devicePixelRatio: newDPR, + ), + )) + .values; + } + + return Builder( + builder: (context) => throw UnimplementedError( + 'Widgets that mix ProjectionSimplificationManagement into their State ' + 'must call super.build() but must ignore the return value of the ' + 'superclass.', + ), + ); + } + + Iterable _simplifyElements({ + required Iterable projectedElements, + required MapCamera camera, + required double pixelTolerance, + required double devicePixelRatio, + }) sync* { + final tolerance = getEffectiveSimplificationTolerance( + crs: camera.crs, + zoom: camera.zoom.floor(), + pixelTolerance: pixelTolerance, + devicePixelRatio: devicePixelRatio, + ); + + for (final projectedElement in projectedElements) { + yield simplifyProjectedElement( + projectedElement: projectedElement, + tolerance: tolerance, + ); + } + } +} diff --git a/lib/src/layer/shared/layer_projection_simplification/widget.dart b/lib/src/layer/shared/layer_projection_simplification/widget.dart new file mode 100644 index 000000000..84944a75f --- /dev/null +++ b/lib/src/layer/shared/layer_projection_simplification/widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/shared/layer_projection_simplification/state.dart'; + +/// A [StatefulWidget] that includes the properties used by the [State] component +/// which mixes [ProjectionSimplificationManagement] in +@immutable +abstract base class ProjectionSimplificationManagementSupportedWidget + extends StatefulWidget { + /// Whether to apply the auto-update algorithm to re-paint the necessary + /// [Polygon]s when they change + /// + /// It is recommended to leave this `true`, as default, otherwise changes to + /// child polygons may not update. It will detect which polygons have changed, + /// and only 'update' (re-project and re-simplify) those that are necessary. + /// + /// However, where there are a large number of polygons, the majority (or more) + /// of which change at the same time, then it is recommended to set this + /// `false`. This will avoid a large unnecessary loop to detect changes, and + /// is likely to improve performance on state changes. If `false`, then the + /// layer will need to be manually rebuilt from scratch using new [Key]s + /// whenever necessary. Do not use a [UniqueKey] : this will cause the entire + /// widget to reset and rebuild every time the map camera changes. + final bool useDynamicUpdate; + + /// Distance between two neighboring polyline points, in logical pixels scaled + /// to floored zoom + /// + /// 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. + /// + /// Defaults to 0.3. Set to 0 to disable simplification. + final double simplificationTolerance; + + /// A [StatefulWidget] that includes the properties used by the [State] + /// component which mixes [ProjectionSimplificationManagement] in + /// + /// Constructors should call `super()` (the super constructor) to ensure the + /// necessary assertions are made. + const ProjectionSimplificationManagementSupportedWidget({ + super.key, + this.useDynamicUpdate = true, + this.simplificationTolerance = 0.3, + }) : assert( + simplificationTolerance >= 0, + 'simplificationTolerance cannot be negative: $simplificationTolerance', + ); +}