From e4b7f19553deff8b6a7d71e59d76c715d5371c1e Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 15 Jan 2024 23:23:11 +0100 Subject: [PATCH 01/11] Cache projection of polygon points. --- lib/src/geo/crs.dart | 62 +++++++++++++------ lib/src/layer/polygon_layer/painter.dart | 21 ++----- lib/src/layer/polygon_layer/polygon.dart | 6 ++ .../layer/polygon_layer/polygon_layer.dart | 26 +++++--- lib/src/misc/offsets.dart | 34 +++++++++- .../tile_layer/tile_bounds/crs_fakes.dart | 14 +++-- 6 files changed, 110 insertions(+), 53 deletions(-) diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index f762cb2fc..a324e85ba 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -29,10 +29,15 @@ abstract class Crs { this.wrapLat, }); + // Project a spherical LatLng coordinate into planar space (unscaled). Projection get projection; - /// Converts a point on the sphere surface (with a certain zoom) in a - /// map point. + // Scale planar coordinate to scaled map point. + (double, double) transform(double x, double y, double scale); + (double, double) untransform(double x, double y, double scale); + + /// Converts a point on the sphere surface (with a certain zoom) to a + /// scaled map point. (double, double) latLngToXY(LatLng latlng, double scale); Point latLngToPoint(LatLng latlng, double zoom) { @@ -54,32 +59,39 @@ abstract class Crs { } @immutable -abstract class _CrsWithStaticTransformation extends Crs { +abstract class CrsWithStaticTransformation extends Crs { @nonVirtual @protected - final _Transformation transformation; + final _Transformation _transformation; @override final Projection projection; - const _CrsWithStaticTransformation({ - required this.transformation, + @override + (double, double) transform(double x, double y, double scale) => + _transformation.transform(x, y, scale); + @override + (double, double) untransform(double x, double y, double scale) => + _transformation.untransform(x, y, scale); + + const CrsWithStaticTransformation._({ + required _Transformation transformation, required this.projection, required super.code, required super.infinite, super.wrapLng, super.wrapLat, - }); + }) : _transformation = transformation; @override (double, double) latLngToXY(LatLng latlng, double scale) { final (x, y) = projection.projectXY(latlng); - return transformation.transform(x, y, scale); + return _transformation.transform(x, y, scale); } @override LatLng pointToLatLng(Point point, double zoom) { - final (x, y) = transformation.untransform( + final (x, y) = _transformation.untransform( point.x.toDouble(), point.y.toDouble(), scale(zoom), @@ -93,8 +105,8 @@ abstract class _CrsWithStaticTransformation extends Crs { final b = projection.bounds!; final s = scale(zoom); - final (minx, miny) = transformation.transform(b.min.x, b.min.y, s); - final (maxx, maxy) = transformation.transform(b.max.x, b.max.y, s); + final (minx, miny) = _transformation.transform(b.min.x, b.min.y, s); + final (maxx, maxy) = _transformation.transform(b.max.x, b.max.y, s); return Bounds( Point(minx, miny), Point(maxx, maxy), @@ -104,9 +116,9 @@ abstract class _CrsWithStaticTransformation extends Crs { // Custom CRS for non geographical maps @immutable -class CrsSimple extends _CrsWithStaticTransformation { +class CrsSimple extends CrsWithStaticTransformation { const CrsSimple() - : super( + : super._( code: 'CRS.SIMPLE', transformation: const _Transformation(1, 0, -1, 0), projection: const _LonLat(), @@ -118,11 +130,11 @@ class CrsSimple extends _CrsWithStaticTransformation { /// The most common CRS used for rendering maps. @immutable -class Epsg3857 extends _CrsWithStaticTransformation { +class Epsg3857 extends CrsWithStaticTransformation { static const double _scale = 0.5 / (math.pi * SphericalMercator.r); const Epsg3857() - : super( + : super._( code: 'EPSG:3857', transformation: const _Transformation(_scale, 0.5, -_scale, 0.5), projection: const SphericalMercator(), @@ -132,12 +144,15 @@ class Epsg3857 extends _CrsWithStaticTransformation { @override (double, double) latLngToXY(LatLng latlng, double scale) => - transformation.transform(SphericalMercator.projectLng(latlng.longitude), - SphericalMercator.projectLat(latlng.latitude), scale); + _transformation.transform( + SphericalMercator.projectLng(latlng.longitude), + SphericalMercator.projectLat(latlng.latitude), + scale, + ); @override Point latLngToPoint(LatLng latlng, double zoom) { - final (x, y) = transformation.transform( + final (x, y) = _transformation.transform( SphericalMercator.projectLng(latlng.longitude), SphericalMercator.projectLat(latlng.latitude), scale(zoom), @@ -148,9 +163,9 @@ class Epsg3857 extends _CrsWithStaticTransformation { /// A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. @immutable -class Epsg4326 extends _CrsWithStaticTransformation { +class Epsg4326 extends CrsWithStaticTransformation { const Epsg4326() - : super( + : super._( projection: const _LonLat(), transformation: const _Transformation(1 / 180, 1, -1 / 180, 0.5), code: 'EPSG:4326', @@ -222,6 +237,13 @@ class Proj4Crs extends Crs { ); } + @override + (double, double) transform(double x, double y, double scale) => + _getTransformationByZoom(zoom(scale)).transform(x, y, scale); + @override + (double, double) untransform(double x, double y, double scale) => + _getTransformationByZoom(zoom(scale)).untransform(x, y, scale); + /// Converts a point on the sphere surface (with a certain zoom) in a /// map point. @override diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index b7cf54130..052052336 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -17,23 +17,11 @@ class PolygonPainter extends CustomPainter { ({Offset min, Offset max}) getBounds(Offset origin, Polygon polygon) { final bbox = polygon.boundingBox; return ( - min: getOffset(origin, bbox.southWest), - max: getOffset(origin, bbox.northEast), + min: getOffset(camera, origin, bbox.southWest), + max: getOffset(camera, origin, bbox.northEast), ); } - 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); - } - - List getOffsets(Offset origin, List points) => List.generate( - points.length, - (index) => getOffset(origin, points[index]), - growable: false, - ); - @override void paint(Canvas canvas, Size size) { var filledPath = ui.Path(); @@ -77,7 +65,8 @@ class PolygonPainter extends CustomPainter { if (polygon.points.isEmpty) { continue; } - final offsets = getOffsets(origin, polygon.points); + final offsets = getOffsetsXY( + camera, origin, polygon.getProjectedPoints(camera.crs.projection)); // The hash is based on the polygons visual properties. If the hash from // the current and the previous polygon no longer match, we need to flush @@ -110,7 +99,7 @@ class PolygonPainter extends CustomPainter { final holeOffsetsList = List>.generate( holePointsList.length, - (i) => getOffsets(origin, holePointsList[i]), + (i) => getOffsets(camera, origin, holePointsList[i]), growable: false, ); diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index bf215451c..37e3077fb 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -41,6 +41,12 @@ class Polygon { LatLngBounds get boundingBox => _boundingBox ??= LatLngBounds.fromPoints(points); + List<(double, double)>? _projectedPoints; + List<(double, double)> getProjectedPoints(Projection projection) => + _projectedPoints ??= List<(double, double)>.generate( + points.length, (i) => projection.projectXY(points[i]), + growable: false); + TextPainter? _textPainter; TextPainter? get textPainter { if (label != null) { diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index fffb8e869..398ef7a82 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; 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' hide Path; import 'package:polylabel/polylabel.dart'; // conflict with Path from UI @@ -116,15 +117,20 @@ class _PolygonLayerState extends State { } List _computeZoomLevelSimplification(int zoom) => - _cachedSimplifiedPolygons[zoom] ??= widget.polygons - .map( - (polygon) => polygon.copyWithNewPoints( - simplify( - polygon.points, - widget.simplificationTolerance / math.pow(2, zoom), - highestQuality: true, - ), + _cachedSimplifiedPolygons[zoom] ??= List.generate( + widget.polygons.length, + (i) { + final polygon = widget.polygons[i]; + return polygon.copyWithNewPoints( + // TODO: Ideally we'd simplify in projected space to minimize issues with map distortion. + // TODO: Simplify polygon holes as well. + simplify( + polygon.points, + widget.simplificationTolerance / math.pow(2, zoom), + highestQuality: true, ), - ) - .toList(); + ); + }, + growable: false, + ); } diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 888518ff9..0a5c730f6 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -19,7 +19,7 @@ List getOffsets(MapCamera camera, Offset origin, List points) { final oy = -origin.dy; final len = points.length; - // Optimization: monomorphize the Epsg3857-case to save the virtual function overhead. + // Optimization: monomorphize the Epsg3857-case to avoid the virtual function overhead. if (crs is Epsg3857) { final Epsg3857 epsg3857 = crs; final v = List.filled(len, Offset.zero); @@ -37,3 +37,35 @@ List getOffsets(MapCamera camera, Offset origin, List points) { } return v; } + +List getOffsetsXY( + MapCamera camera, Offset origin, List<(double, double)> points) { + // Critically create as little garbage as possible. This is called on every frame. + final crs = camera.crs; + final zoomScale = crs.scale(camera.zoom); + + final ox = -origin.dx; + final oy = -origin.dy; + final len = points.length; + + // Optimization: monomorphize the CrsWithStaticTransformation-case to avoid + // the virtual function overhead. + if (crs is CrsWithStaticTransformation) { + final CrsWithStaticTransformation mcrs = crs; + final v = List.filled(len, Offset.zero); + for (int i = 0; i < len; ++i) { + final (px, py) = points[i]; + final (x, y) = mcrs.transform(px, py, zoomScale); + v[i] = Offset(x + ox, y + oy); + } + return v; + } + + final v = List.filled(len, Offset.zero); + for (int i = 0; i < len; ++i) { + final (px, py) = points[i]; + final (x, y) = crs.transform(px, py, zoomScale); + v[i] = Offset(x + ox, y + oy); + } + return v; +} diff --git a/test/layer/tile_layer/tile_bounds/crs_fakes.dart b/test/layer/tile_layer/tile_bounds/crs_fakes.dart index cfa16956b..0800bb24d 100644 --- a/test/layer/tile_layer/tile_bounds/crs_fakes.dart +++ b/test/layer/tile_layer/tile_bounds/crs_fakes.dart @@ -14,14 +14,16 @@ class FakeInfiniteCrs extends Crs { /// Any projection just to get non-zero coordiantes. @override - Point latLngToPoint(LatLng latlng, double zoom) { - return const Epsg3857().latLngToPoint(latlng, zoom); - } + (double, double) latLngToXY(LatLng latlng, double scale) => + const Epsg3857().latLngToXY(latlng, scale); @override - (double, double) latLngToXY(LatLng latlng, double scale) { - return const Epsg3857().latLngToXY(latlng, scale); - } + (double, double) transform(double x, double y, double scale) => + const Epsg3857().transform(x, y, scale); + + @override + (double, double) untransform(double x, double y, double scale) => + const Epsg3857().untransform(x, y, scale); @override LatLng pointToLatLng(Point point, double zoom) => throw UnimplementedError(); From 992315da8553f56520e7fabdd8fb8e09e19a0abc Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 16 Jan 2024 11:43:22 +0100 Subject: [PATCH 02/11] Introduce _ProjectedPolygon and simplify points in planar space. --- lib/src/layer/polygon_layer/painter.dart | 53 ++++++++-- lib/src/layer/polygon_layer/polygon.dart | 24 ----- .../layer/polygon_layer/polygon_layer.dart | 75 +++++++------- lib/src/misc/offsets.dart | 11 ++- lib/src/misc/simplify.dart | 98 ++++++++++++------- 5 files changed, 152 insertions(+), 109 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 052052336..085d33c12 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,13 +1,45 @@ part of 'polygon_layer.dart'; -class PolygonPainter extends CustomPainter { - final List polygons; +class _ProjectedPolygon { + final Polygon polygon; + final List points; + final List>? holePoints; + + const _ProjectedPolygon._({ + required this.polygon, + required this.points, + this.holePoints, + }); + + factory _ProjectedPolygon.fromPolygon( + Projection projection, Polygon polygon) => + _ProjectedPolygon._( + polygon: polygon, + points: List.generate(polygon.points.length, (j) { + final (x, y) = projection.projectXY(polygon.points[j]); + return DoublePoint(x, y); + }, growable: false), + holePoints: polygon.holePointsList == null + ? null + : List>.generate(polygon.holePointsList!.length, + (j) { + final list = polygon.holePointsList![j]; + return List.generate(list.length, (k) { + final (x, y) = projection.projectXY(list[k]); + return DoublePoint(x, y); + }, growable: false); + }, growable: false), + ); +} + +class _PolygonPainter extends CustomPainter { + final List<_ProjectedPolygon> polygons; final MapCamera camera; final LatLngBounds bounds; final bool polygonLabels; final bool drawLabelsLast; - PolygonPainter({ + _PolygonPainter({ required this.polygons, required this.camera, required this.polygonLabels, @@ -61,12 +93,12 @@ class PolygonPainter extends CustomPainter { final origin = (camera.project(camera.center) - camera.size / 2).toOffset(); // Main loop constructing batched fill and border paths from given polygons. - for (final polygon in polygons) { - if (polygon.points.isEmpty) { + for (final projectedPolygon in polygons) { + if (projectedPolygon.points.isEmpty) { continue; } - final offsets = getOffsetsXY( - camera, origin, polygon.getProjectedPoints(camera.crs.projection)); + final polygon = projectedPolygon.polygon; + final offsets = getOffsetsXY(camera, origin, projectedPolygon.points); // The hash is based on the polygons visual properties. If the hash from // the current and the previous polygon no longer match, we need to flush @@ -144,10 +176,11 @@ class PolygonPainter extends CustomPainter { drawPaths(); if (polygonLabels && drawLabelsLast) { - for (final polygon in polygons) { - if (polygon.points.isEmpty) { + for (final projectedPolygon in polygons) { + if (projectedPolygon.points.isEmpty) { continue; } + final polygon = projectedPolygon.polygon; final textPainter = polygon.textPainter; if (textPainter != null) { final painter = _buildLabelTextPainter( @@ -245,5 +278,5 @@ class PolygonPainter extends CustomPainter { } @override - bool shouldRepaint(PolygonPainter oldDelegate) => false; + bool shouldRepaint(_PolygonPainter oldDelegate) => false; } diff --git a/lib/src/layer/polygon_layer/polygon.dart b/lib/src/layer/polygon_layer/polygon.dart index 37e3077fb..ad65e19d3 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -41,12 +41,6 @@ class Polygon { LatLngBounds get boundingBox => _boundingBox ??= LatLngBounds.fromPoints(points); - List<(double, double)>? _projectedPoints; - List<(double, double)> getProjectedPoints(Projection projection) => - _projectedPoints ??= List<(double, double)>.generate( - points.length, (i) => projection.projectXY(points[i]), - growable: false); - TextPainter? _textPainter; TextPainter? get textPainter { if (label != null) { @@ -83,24 +77,6 @@ class Polygon { }) : _filledAndClockwise = (isFilled ?? (color != null)) && isClockwise(points); - Polygon copyWithNewPoints(List points) => Polygon( - points: points, - holePointsList: holePointsList, - color: color, - borderStrokeWidth: borderStrokeWidth, - borderColor: borderColor, - disableHolesBorder: disableHolesBorder, - isDotted: isDotted, - // ignore: deprecated_member_use_from_same_package - isFilled: isFilled, - strokeCap: strokeCap, - strokeJoin: strokeJoin, - label: label, - labelStyle: labelStyle, - labelPlacement: labelPlacement, - rotateLabel: rotateLabel, - ); - static bool isClockwise(List points) { double sum = 0; for (int i = 0; i < points.length; ++i) { diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 398ef7a82..762ae1448 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -62,50 +62,50 @@ class PolygonLayer extends StatefulWidget { } class _PolygonLayerState extends State { - final _cachedSimplifiedPolygons = >{}; + List<_ProjectedPolygon>? _polygons; + final _cachedSimplifiedPolygons = >{}; @override void didUpdateWidget(PolygonLayer 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.polygons, widget.polygons) || - oldWidget.simplificationTolerance != - widget.simplificationTolerance)) { - _cachedSimplifiedPolygons.clear(); - _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); - } else if (oldWidget.simplificationTolerance != 0 && - widget.simplificationTolerance == 0) { - _cachedSimplifiedPolygons.clear(); - } else if (oldWidget.simplificationTolerance == 0 && - widget.simplificationTolerance != 0) { - _computeZoomLevelSimplification(MapCamera.of(context).zoom.floor()); + if (widget.simplificationTolerance != 0 && + oldWidget.simplificationTolerance == widget.simplificationTolerance && + listEquals(oldWidget.polygons, widget.polygons)) { + // Reuse cache. + return; } + + _cachedSimplifiedPolygons.clear(); + _polygons = null; } @override Widget build(BuildContext context) { final camera = MapCamera.of(context); + final zoom = camera.zoom.floor(); + + final projectedPolygons = _polygons ??= + List<_ProjectedPolygon>.generate(widget.polygons.length, (i) { + final polygon = widget.polygons[i]; + return _ProjectedPolygon.fromPolygon(camera.crs.projection, polygon); + }, growable: false); final simplified = widget.simplificationTolerance == 0 - ? widget.polygons - : _computeZoomLevelSimplification(camera.zoom.floor()); + ? projectedPolygons + : _cachedSimplifiedPolygons[zoom] ??= _computeZoomLevelSimplification( + projectedPolygons, widget.simplificationTolerance, zoom); final culled = !widget.polygonCulling ? simplified : simplified - .where((p) => p.boundingBox.isOverlapping(camera.visibleBounds)) + .where((p) => + p.polygon.boundingBox.isOverlapping(camera.visibleBounds)) .toList(); return MobileLayerTransformer( child: CustomPaint( - painter: PolygonPainter( + painter: _PolygonPainter( polygons: culled, camera: camera, polygonLabels: widget.polygonLabels, @@ -116,20 +116,23 @@ class _PolygonLayerState extends State { ); } - List _computeZoomLevelSimplification(int zoom) => - _cachedSimplifiedPolygons[zoom] ??= List.generate( - widget.polygons.length, + static List<_ProjectedPolygon> _computeZoomLevelSimplification( + List<_ProjectedPolygon> polygons, + double tolerance, + int zoom, + ) => + List<_ProjectedPolygon>.generate( + polygons.length, (i) { - final polygon = widget.polygons[i]; - return polygon.copyWithNewPoints( - // TODO: Ideally we'd simplify in projected space to minimize issues with map distortion. - // TODO: Simplify polygon holes as well. - simplify( - polygon.points, - widget.simplificationTolerance / math.pow(2, zoom), - highestQuality: true, - ), - ); + final polygon = polygons[i]; + return _ProjectedPolygon._( + polygon: polygon.polygon, + points: simplifyPoints( + polygon.points, + tolerance / math.pow(2, zoom), + highestQuality: true, + ), + holePoints: polygon.holePoints); }, growable: false, ); diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 0a5c730f6..8a7f40fbd 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; Offset getOffset(MapCamera camera, Offset origin, LatLng point) { @@ -39,7 +40,7 @@ List getOffsets(MapCamera camera, Offset origin, List points) { } List getOffsetsXY( - MapCamera camera, Offset origin, List<(double, double)> points) { + MapCamera camera, Offset origin, List points) { // Critically create as little garbage as possible. This is called on every frame. final crs = camera.crs; final zoomScale = crs.scale(camera.zoom); @@ -54,8 +55,8 @@ List getOffsetsXY( final CrsWithStaticTransformation mcrs = crs; final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final (px, py) = points[i]; - final (x, y) = mcrs.transform(px, py, zoomScale); + final p = points[i]; + final (x, y) = mcrs.transform(p.x, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } return v; @@ -63,8 +64,8 @@ List getOffsetsXY( final v = List.filled(len, Offset.zero); for (int i = 0; i < len; ++i) { - final (px, py) = points[i]; - final (x, y) = crs.transform(px, py, zoomScale); + final p = points[i]; + final (x, y) = crs.transform(p.x, p.y, zoomScale); v[i] = Offset(x + ox, y + oy); } return v; diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index ef4574e4e..db4cd1fd6 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -3,53 +3,63 @@ 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; +// Custom point due to math.Point being slow. Math operations tend to +// have 20+x penalty for virtual function overhead given the reified nature of +// Dart generics. +class DoublePoint { + // Note: Allow mutability for reuse/pooling to reduce GC pressure and increase performance. + // Geometry operations should be safe-by-default to avoid accidental bugs. + double x; + double y; + + DoublePoint(this.x, this.y); + + DoublePoint operator -(DoublePoint rhs) => DoublePoint(x - rhs.x, y - rhs.y); + + double distanceSq(DoublePoint rhs) { + final double dx = x - rhs.x; + final double dy = y - rhs.y; + return dx * dx + dy * dy; + } } /// square distance from a point to a segment double _getSqSegDist( - LatLng p, - LatLng p1, - LatLng p2, + DoublePoint p, + DoublePoint p1, + DoublePoint p2, ) { - double x = p1.longitude; - double y = p1.latitude; - double dx = p2.longitude - x; - double dy = p2.latitude - y; + double x = p1.x; + double y = p1.y; + double dx = p2.x - x; + double dy = p2.y - y; if (dx != 0 || dy != 0) { - final double t = - ((p.longitude - x) * dx + (p.latitude - y) * dy) / (dx * dx + dy * dy); + final double t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy); if (t > 1) { - x = p2.longitude; - y = p2.latitude; + x = p2.x; + y = p2.y; } else if (t > 0) { x += dx * t; y += dy * t; } } - dx = p.longitude - x; - dy = p.latitude - y; + dx = p.x - x; + dy = p.y - y; return dx * dx + dy * dy; } -List simplifyRadialDist( - List points, +List simplifyRadialDist( + List points, double sqTolerance, ) { - LatLng prevPoint = points[0]; - final List newPoints = [prevPoint]; - late LatLng point; + DoublePoint prevPoint = points[0]; + final List newPoints = [prevPoint]; + late DoublePoint point; for (int i = 1, len = points.length; i < len; i++) { point = points[i]; - if (_getSqDist(point, prevPoint) > sqTolerance) { + if (point.distanceSq(prevPoint) > sqTolerance) { newPoints.add(point); prevPoint = point; } @@ -61,11 +71,11 @@ List simplifyRadialDist( } void _simplifyDPStep( - List points, + List points, int first, int last, double sqTolerance, - List simplified, + List simplified, ) { double maxSqDist = sqTolerance; late int index; @@ -89,12 +99,12 @@ void _simplifyDPStep( } // simplification using Ramer-Douglas-Peucker algorithm -List simplifyDouglasPeucker( - List points, +List simplifyDouglasPeucker( + List points, double sqTolerance, ) { final int last = points.length - 1; - final List simplified = [points[0]]; + final List simplified = [points[0]]; _simplifyDPStep(points, 0, last, sqTolerance, simplified); simplified.add(points[last]); return simplified; @@ -107,12 +117,32 @@ List simplify( double tolerance, { bool highestQuality = false, }) { - if (points.length <= 2) return points; + // 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 = + highestQuality ? nextPoints : simplifyRadialDist(nextPoints, sqTolerance); + nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); + + return List.generate( + nextPoints.length, (i) => LatLng(nextPoints[i].y, nextPoints[i].x)); +} + +List simplifyPoints( + List points, + double tolerance, { + bool highestQuality = false, +}) { + // Don't simplify anything less than a square + if (points.length <= 4) return points; - List nextPoints = points; + List nextPoints = points; final double sqTolerance = tolerance * tolerance; nextPoints = - highestQuality ? points : simplifyRadialDist(nextPoints, sqTolerance); + highestQuality ? nextPoints : simplifyRadialDist(nextPoints, sqTolerance); nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); return nextPoints; From bac77a437cb88ead412b4987017a5c60f18ffc06 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 16 Jan 2024 11:50:01 +0100 Subject: [PATCH 03/11] Simplify polygon holes as well. --- .../layer/polygon_layer/polygon_layer.dart | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 762ae1448..50a91be42 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -125,14 +125,26 @@ class _PolygonLayerState extends State { polygons.length, (i) { final polygon = polygons[i]; + final holes = polygon.holePoints; + return _ProjectedPolygon._( - polygon: polygon.polygon, - points: simplifyPoints( - polygon.points, - tolerance / math.pow(2, zoom), - highestQuality: true, - ), - holePoints: polygon.holePoints); + polygon: polygon.polygon, + points: simplifyPoints( + polygon.points, + tolerance / math.pow(2, zoom), + highestQuality: true, + ), + holePoints: holes == null + ? null + : List>.generate(holes.length, (j) { + final hole = holes[j]; + return simplifyPoints( + hole, + tolerance / math.pow(2, zoom), + highestQuality: true, + ); + }), + ); }, growable: false, ); From ff7bd016091a8faebb321cddd22b12bb4e182817 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 16 Jan 2024 11:59:46 +0100 Subject: [PATCH 04/11] Minor brush-up. --- lib/src/layer/polygon_layer/painter.dart | 34 ++++++++++++------------ 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 085d33c12..4e355ed0e 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -11,25 +11,25 @@ class _ProjectedPolygon { this.holePoints, }); - factory _ProjectedPolygon.fromPolygon( - Projection projection, Polygon polygon) => - _ProjectedPolygon._( - polygon: polygon, - points: List.generate(polygon.points.length, (j) { - final (x, y) = projection.projectXY(polygon.points[j]); - return DoublePoint(x, y); - }, growable: false), - holePoints: polygon.holePointsList == null - ? null - : List>.generate(polygon.holePointsList!.length, - (j) { - final list = polygon.holePointsList![j]; - return List.generate(list.length, (k) { - final (x, y) = projection.projectXY(list[k]); + _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) + : this._( + polygon: polygon, + points: List.generate(polygon.points.length, (j) { + final (x, y) = projection.projectXY(polygon.points[j]); + return DoublePoint(x, y); + }, growable: false), + holePoints: () { + final holes = polygon.holePointsList; + if (holes == null) return null; + + return List>.generate(holes.length, (j) { + final points = holes[j]; + return List.generate(points.length, (k) { + final (x, y) = projection.projectXY(points[k]); return DoublePoint(x, y); }, growable: false); - }, growable: false), - ); + }, growable: false); + }()); } class _PolygonPainter extends CustomPainter { From 28fb73cb7ec357f52ca4ee651987e843c5a2e254 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 16 Jan 2024 12:30:18 +0100 Subject: [PATCH 05/11] Re-use squared-distance-from-point-to-segment code in simplification and polyline hit detection code. --- lib/src/layer/polygon_layer/painter.dart | 12 +++++-- lib/src/layer/polyline_layer/painter.dart | 27 +------------- lib/src/misc/simplify.dart | 43 +++++++++++++---------- 3 files changed, 35 insertions(+), 47 deletions(-) diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 4e355ed0e..d317b07a0 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -243,19 +243,23 @@ class _PolygonPainter extends CustomPainter { } void _addDottedLineToPath( - ui.Path path, List offsets, double radius, double stepLength) { + ui.Path path, + List offsets, + double radius, + double stepLength, + ) { if (offsets.isEmpty) { return; } double startDistance = 0; - for (var i = 0; i < offsets.length; i++) { + for (int i = 0; i < offsets.length; i++) { final o0 = offsets[i % offsets.length]; final o1 = offsets[(i + 1) % offsets.length]; final totalDistance = (o0 - o1).distance; double distance = startDistance; - for (; distance < totalDistance; distance += stepLength) { + while (distance < totalDistance) { final done = distance / totalDistance; final remain = 1.0 - done; final offset = Offset( @@ -263,6 +267,8 @@ class _PolygonPainter extends CustomPainter { o0.dy * remain + o1.dy * done, ); path.addOval(Rect.fromCircle(center: offset, radius: radius)); + + distance += stepLength; } startDistance = distance < totalDistance diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 59e724395..76a36e83a 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -64,7 +64,7 @@ class PolylinePainter extends CustomPainter { final o1 = offsets[i]; final o2 = offsets[i + 1]; - final distance = math.sqrt(_distToSegmentSquared( + final distance = math.sqrt(getSqSegDist( position.dx, position.dy, o1.dx, @@ -282,29 +282,4 @@ class PolylinePainter extends CustomPainter { 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/misc/simplify.dart b/lib/src/misc/simplify.dart index db4cd1fd6..955bd1bdf 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -24,28 +24,31 @@ class DoublePoint { } /// square distance from a point to a segment -double _getSqSegDist( - DoublePoint p, - DoublePoint p1, - DoublePoint p2, +double getSqSegDist( + final double px, + final double py, + final double x0, + final double y0, + final double x1, + final double y1, ) { - double x = p1.x; - double y = p1.y; - double dx = p2.x - x; - double dy = p2.y - y; + double dx = x1 - x0; + double dy = y1 - y0; if (dx != 0 || dy != 0) { - final double t = ((p.x - x) * dx + (p.y - y) * dy) / (dx * dx + dy * dy); + final double t = ((px - x0) * dx + (py - y0) * dy) / (dx * dx + dy * dy); if (t > 1) { - x = p2.x; - y = p2.y; + dx = px - x1; + dy = py - y1; + return dx * dx + dy * dy; } else if (t > 0) { - x += dx * t; - y += dy * t; + dx = px - (x0 + dx * t); + dy = py - (y0 + dy * t); + return dx * dx + dy * dy; } } - dx = p.x - x; - dy = p.y - y; + dx = px - x0; + dy = py - y0; return dx * dx + dy * dy; } @@ -72,15 +75,19 @@ List simplifyRadialDist( void _simplifyDPStep( List points, - int first, - int last, + final int first, + final int last, double sqTolerance, List simplified, ) { double maxSqDist = sqTolerance; + final p0 = points[first]; + final p1 = points[last]; + late int index; for (int i = first + 1; i < last; i++) { - final double sqDist = _getSqSegDist(points[i], points[first], points[last]); + final p = points[i]; + final double sqDist = getSqSegDist(p.x, p.y, p0.x, p0.y, p1.x, p1.y); if (sqDist > maxSqDist) { index = i; From b169f7c622f34ed74c0334b0fe5ec8a77e28716f Mon Sep 17 00:00:00 2001 From: Luka S Date: Tue, 16 Jan 2024 17:30:05 +0000 Subject: [PATCH 06/11] Converted comments to docstrings --- lib/src/geo/crs.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index a324e85ba..2e22a7249 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -29,10 +29,10 @@ abstract class Crs { this.wrapLat, }); - // Project a spherical LatLng coordinate into planar space (unscaled). + /// Project a spherical LatLng coordinate into planar space (unscaled). Projection get projection; - // Scale planar coordinate to scaled map point. + /// Scale planar coordinate to scaled map point. (double, double) transform(double x, double y, double scale); (double, double) untransform(double x, double y, double scale); From e170a1a31b9299312157c0eea94bce41aad810dd Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 16 Jan 2024 17:53:53 +0000 Subject: [PATCH 07/11] Minor formatting improvements --- lib/src/geo/crs.dart | 14 +++--- lib/src/layer/polygon_layer/painter.dart | 41 +++++++++++------ .../layer/polygon_layer/polygon_layer.dart | 44 +++++++++++-------- lib/src/layer/polyline_layer/painter.dart | 24 +++++----- .../layer/polyline_layer/polyline_layer.dart | 2 +- lib/src/misc/offsets.dart | 5 ++- lib/src/misc/simplify.dart | 14 +++--- 7 files changed, 86 insertions(+), 58 deletions(-) diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 2e22a7249..80963a862 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -67,13 +67,6 @@ abstract class CrsWithStaticTransformation extends Crs { @override final Projection projection; - @override - (double, double) transform(double x, double y, double scale) => - _transformation.transform(x, y, scale); - @override - (double, double) untransform(double x, double y, double scale) => - _transformation.untransform(x, y, scale); - const CrsWithStaticTransformation._({ required _Transformation transformation, required this.projection, @@ -83,6 +76,13 @@ abstract class CrsWithStaticTransformation extends Crs { super.wrapLat, }) : _transformation = transformation; + @override + (double, double) transform(double x, double y, double scale) => + _transformation.transform(x, y, scale); + @override + (double, double) untransform(double x, double y, double scale) => + _transformation.untransform(x, y, scale); + @override (double, double) latLngToXY(LatLng latlng, double scale) { final (x, y) = projection.projectXY(latlng); diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index d317b07a0..580122b2e 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -13,23 +13,36 @@ class _ProjectedPolygon { _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) : this._( - polygon: polygon, - points: List.generate(polygon.points.length, (j) { + polygon: polygon, + points: List.generate( + polygon.points.length, + (j) { final (x, y) = projection.projectXY(polygon.points[j]); return DoublePoint(x, y); - }, growable: false), - holePoints: () { - final holes = polygon.holePointsList; - if (holes == null) return null; - - return List>.generate(holes.length, (j) { + }, + growable: false, + ), + holePoints: () { + final holes = polygon.holePointsList; + if (holes == null) return null; + + return List>.generate( + holes.length, + (j) { final points = holes[j]; - return List.generate(points.length, (k) { - final (x, y) = projection.projectXY(points[k]); - return DoublePoint(x, y); - }, growable: false); - }, growable: false); - }()); + return List.generate( + points.length, + (k) { + final (x, y) = projection.projectXY(points[k]); + return DoublePoint(x, y); + }, + growable: false, + ); + }, + growable: false, + ); + }(), + ); } class _PolygonPainter extends CustomPainter { diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 50a91be42..bfceab245 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -62,7 +62,7 @@ class PolygonLayer extends StatefulWidget { } class _PolygonLayerState extends State { - List<_ProjectedPolygon>? _polygons; + List<_ProjectedPolygon>? _cachedProjectedPolygons; final _cachedSimplifiedPolygons = >{}; @override @@ -77,30 +77,37 @@ class _PolygonLayerState extends State { } _cachedSimplifiedPolygons.clear(); - _polygons = null; + _cachedProjectedPolygons = null; } @override Widget build(BuildContext context) { final camera = MapCamera.of(context); - final zoom = camera.zoom.floor(); - final projectedPolygons = _polygons ??= - List<_ProjectedPolygon>.generate(widget.polygons.length, (i) { - final polygon = widget.polygons[i]; - return _ProjectedPolygon.fromPolygon(camera.crs.projection, polygon); - }, growable: false); + final projected = _cachedProjectedPolygons ??= List.generate( + widget.polygons.length, + (i) => _ProjectedPolygon.fromPolygon( + camera.crs.projection, + widget.polygons[i], + ), + growable: false, + ); + final zoom = camera.zoom.floor(); final simplified = widget.simplificationTolerance == 0 - ? projectedPolygons + ? projected : _cachedSimplifiedPolygons[zoom] ??= _computeZoomLevelSimplification( - projectedPolygons, widget.simplificationTolerance, zoom); + projected, + widget.simplificationTolerance, + zoom, + ); final culled = !widget.polygonCulling ? simplified : simplified - .where((p) => - p.polygon.boundingBox.isOverlapping(camera.visibleBounds)) + .where( + (p) => p.polygon.boundingBox.isOverlapping(camera.visibleBounds), + ) .toList(); return MobileLayerTransformer( @@ -136,14 +143,15 @@ class _PolygonLayerState extends State { ), holePoints: holes == null ? null - : List>.generate(holes.length, (j) { - final hole = holes[j]; - return simplifyPoints( - hole, + : List>.generate( + holes.length, + (j) => simplifyPoints( + holes[j], tolerance / math.pow(2, zoom), highestQuality: true, - ); - }), + ), + growable: false, + ), ); }, growable: false, diff --git a/lib/src/layer/polyline_layer/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 76a36e83a..de6759df8 100644 --- a/lib/src/layer/polyline_layer/painter.dart +++ b/lib/src/layer/polyline_layer/painter.dart @@ -1,6 +1,6 @@ part of 'polyline_layer.dart'; -class PolylinePainter extends CustomPainter { +class _PolylinePainter extends CustomPainter { final List> polylines; final MapCamera camera; final LayerHitNotifier? hitNotifier; @@ -8,7 +8,7 @@ class PolylinePainter extends CustomPainter { final _hits = []; // Avoids repetitive memory reallocation - PolylinePainter({ + _PolylinePainter({ required this.polylines, required this.camera, required this.hitNotifier, @@ -64,14 +64,16 @@ class PolylinePainter extends CustomPainter { final o1 = offsets[i]; final o2 = offsets[i + 1]; - final distance = math.sqrt(getSqSegDist( - position.dx, - position.dy, - o1.dx, - o1.dy, - o2.dx, - o2.dy, - )); + final distance = math.sqrt( + getSqSegDist( + position.dx, + position.dy, + o1.dx, + o1.dy, + o2.dx, + o2.dy, + ), + ); if (distance < hittableDistance) { _hits.add(polyline.hitValue!); @@ -279,7 +281,7 @@ class PolylinePainter extends CustomPainter { } @override - bool shouldRepaint(PolylinePainter oldDelegate) => false; + bool shouldRepaint(_PolylinePainter oldDelegate) => false; } const _distance = Distance(); diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index cf93e8887..c3103d7e1 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -120,7 +120,7 @@ class _PolylineLayerState extends State> { return MobileLayerTransformer( child: CustomPaint( - painter: PolylinePainter( + painter: _PolylinePainter( polylines: culled, camera: camera, hitNotifier: widget.hitNotifier, diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 8a7f40fbd..10a6447bd 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -40,7 +40,10 @@ List getOffsets(MapCamera camera, Offset origin, List points) { } List getOffsetsXY( - MapCamera camera, Offset origin, List points) { + MapCamera camera, + Offset origin, + List points, +) { // Critically create as little garbage as possible. This is called on every frame. final crs = camera.crs; final zoomScale = crs.scale(camera.zoom); diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 955bd1bdf..ce98ca49a 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -117,8 +117,6 @@ List simplifyDouglasPeucker( return simplified; } -/// high quality simplification uses the Ramer-Douglas-Peucker algorithm -/// otherwise it just merges close points List simplify( List points, double tolerance, { @@ -127,19 +125,23 @@ List simplify( // 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)); + List nextPoints = List.generate( + points.length, + (i) => DoublePoint(points[i].longitude, points[i].latitude), + ); final double sqTolerance = tolerance * tolerance; nextPoints = highestQuality ? nextPoints : simplifyRadialDist(nextPoints, sqTolerance); nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); return List.generate( - nextPoints.length, (i) => LatLng(nextPoints[i].y, nextPoints[i].x)); + nextPoints.length, + (i) => LatLng(nextPoints[i].y, nextPoints[i].x), + ); } List simplifyPoints( - List points, + final List points, double tolerance, { bool highestQuality = false, }) { From d7fb1ab0bc1d1ce0f083e99da7b6dcc81967ce85 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 17 Jan 2024 09:17:16 +0100 Subject: [PATCH 08/11] Compute simplification tolerance based of given projection. Now the user-provided tolerance will have similar meaning independent of the user-selected projection. --- lib/src/layer/polygon_layer/polygon_layer.dart | 8 +++++++- lib/src/misc/simplify.dart | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index bfceab245..1d012a6da 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -63,6 +63,7 @@ class PolygonLayer extends StatefulWidget { class _PolygonLayerState extends State { List<_ProjectedPolygon>? _cachedProjectedPolygons; + double? _effectiveTolerance; final _cachedSimplifiedPolygons = >{}; @override @@ -78,6 +79,7 @@ class _PolygonLayerState extends State { _cachedSimplifiedPolygons.clear(); _cachedProjectedPolygons = null; + _effectiveTolerance = null; } @override @@ -92,13 +94,17 @@ class _PolygonLayerState extends State { ), growable: false, ); + final simplificationTolerance = _effectiveTolerance ??= + getEffectiveSimplificationTolerance( + camera.crs.projection, widget.simplificationTolerance); final zoom = camera.zoom.floor(); + final simplified = widget.simplificationTolerance == 0 ? projected : _cachedSimplifiedPolygons[zoom] ??= _computeZoomLevelSimplification( projected, - widget.simplificationTolerance, + simplificationTolerance, zoom, ); diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index ce98ca49a..0aaf4f0f4 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -1,6 +1,6 @@ // implementation based on // https://github.com/mourner/simplify-js/blob/master/simplify.js - +import 'package:flutter_map/src/geo/crs.dart'; import 'package:latlong2/latlong.dart'; // Custom point due to math.Point being slow. Math operations tend to @@ -156,3 +156,17 @@ List simplifyPoints( return nextPoints; } + +double getEffectiveSimplificationTolerance( + Projection projection, + double tolerance, { + LatLng point = const LatLng(45, 90), +}) { + if (tolerance <= 0) return 0; + + final p0 = projection.project(point); + final p1 = projection + .project(LatLng(point.latitude + tolerance, point.longitude + tolerance)); + + return p0.distanceTo(p1); +} From 2fdccb42723a3de69baa25ff41d2b8c002231731 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 17 Jan 2024 14:11:41 +0100 Subject: [PATCH 09/11] Interpret tolerance as pixel tolerance. --- .../layer/polygon_layer/polygon_layer.dart | 89 ++++++++++--------- lib/src/misc/simplify.dart | 25 ++++-- 2 files changed, 61 insertions(+), 53 deletions(-) diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index 1d012a6da..eef7c45c8 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -25,15 +25,12 @@ class PolygonLayer extends StatefulWidget { /// Defaults to `true`. final bool polygonCulling; - /// Distance between two mergeable polygon points, in decimal degrees scaled - /// to floored zoom + /// Distance between two neighboring polygon points, in logical pixels 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. + /// 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; @@ -63,7 +60,6 @@ class PolygonLayer extends StatefulWidget { class _PolygonLayerState extends State { List<_ProjectedPolygon>? _cachedProjectedPolygons; - double? _effectiveTolerance; final _cachedSimplifiedPolygons = >{}; @override @@ -79,7 +75,6 @@ class _PolygonLayerState extends State { _cachedSimplifiedPolygons.clear(); _cachedProjectedPolygons = null; - _effectiveTolerance = null; } @override @@ -94,17 +89,15 @@ class _PolygonLayerState extends State { ), growable: false, ); - final simplificationTolerance = _effectiveTolerance ??= - getEffectiveSimplificationTolerance( - camera.crs.projection, widget.simplificationTolerance); final zoom = camera.zoom.floor(); - final simplified = widget.simplificationTolerance == 0 + final simplified = widget.simplificationTolerance <= 0 ? projected : _cachedSimplifiedPolygons[zoom] ??= _computeZoomLevelSimplification( + camera.crs, projected, - simplificationTolerance, + widget.simplificationTolerance, zoom, ); @@ -130,36 +123,44 @@ class _PolygonLayerState extends State { } static List<_ProjectedPolygon> _computeZoomLevelSimplification( + Crs crs, List<_ProjectedPolygon> polygons, - double tolerance, + double pixelTolerance, int zoom, - ) => - List<_ProjectedPolygon>.generate( - polygons.length, - (i) { - final polygon = polygons[i]; - final holes = polygon.holePoints; - - return _ProjectedPolygon._( - polygon: polygon.polygon, - points: simplifyPoints( - polygon.points, - tolerance / math.pow(2, zoom), - highestQuality: true, - ), - holePoints: holes == null - ? null - : List>.generate( - holes.length, - (j) => simplifyPoints( - holes[j], - tolerance / math.pow(2, zoom), - highestQuality: true, - ), - growable: false, + ) { + final tolerance = getEffectiveSimplificationTolerance( + crs, + zoom, + pixelTolerance, + ); + + return List<_ProjectedPolygon>.generate( + polygons.length, + (i) { + final polygon = polygons[i]; + final holes = polygon.holePoints; + + return _ProjectedPolygon._( + polygon: polygon.polygon, + points: simplifyPoints( + polygon.points, + tolerance, + highestQuality: true, + ), + holePoints: holes == null + ? null + : List>.generate( + holes.length, + (j) => simplifyPoints( + holes[j], + tolerance, + highestQuality: true, ), - ); - }, - growable: false, - ); + growable: false, + ), + ); + }, + growable: false, + ); + } } diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 0aaf4f0f4..3b6c5b84d 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -1,5 +1,7 @@ // implementation based on // https://github.com/mourner/simplify-js/blob/master/simplify.js +import 'dart:math' as math; + import 'package:flutter_map/src/geo/crs.dart'; import 'package:latlong2/latlong.dart'; @@ -158,15 +160,20 @@ List simplifyPoints( } double getEffectiveSimplificationTolerance( - Projection projection, - double tolerance, { - LatLng point = const LatLng(45, 90), -}) { - if (tolerance <= 0) return 0; + Crs crs, + int zoom, + double pixelTolerance, +) { + if (pixelTolerance <= 0) return 0; - final p0 = projection.project(point); - final p1 = projection - .project(LatLng(point.latitude + tolerance, point.longitude + tolerance)); + final (x0, y0) = crs.untransform(0, 0, crs.scale(zoom.toDouble())); + final (x1, y1) = crs.untransform( + pixelTolerance, + pixelTolerance, + crs.scale(zoom.toDouble()), + ); - return p0.distanceTo(p1); + final dx = x1 - x0; + final dy = y1 - y0; + return math.sqrt(dx * dx + dy * dy); } From 9e07d1fedaf1bfbea9bbb684539fa739de642ebb Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Wed, 17 Jan 2024 19:03:06 +0000 Subject: [PATCH 10/11] Minor layout/style improvements --- lib/src/layer/polygon_layer/painter.dart | 45 ----------------- .../layer/polygon_layer/polygon_layer.dart | 48 +++++++++---------- .../polygon_layer/projected_polygon.dart | 47 ++++++++++++++++++ .../layer/polyline_layer/polyline_layer.dart | 7 +-- lib/src/misc/simplify.dart | 46 +++++++++--------- 5 files changed, 96 insertions(+), 97 deletions(-) create mode 100644 lib/src/layer/polygon_layer/projected_polygon.dart diff --git a/lib/src/layer/polygon_layer/painter.dart b/lib/src/layer/polygon_layer/painter.dart index 580122b2e..9b2656879 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,50 +1,5 @@ part of 'polygon_layer.dart'; -class _ProjectedPolygon { - final Polygon polygon; - final List points; - final List>? holePoints; - - const _ProjectedPolygon._({ - required this.polygon, - required this.points, - this.holePoints, - }); - - _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) - : this._( - polygon: polygon, - points: List.generate( - polygon.points.length, - (j) { - final (x, y) = projection.projectXY(polygon.points[j]); - return DoublePoint(x, y); - }, - growable: false, - ), - holePoints: () { - final holes = polygon.holePointsList; - if (holes == null) return null; - - return List>.generate( - holes.length, - (j) { - final points = holes[j]; - return List.generate( - points.length, - (k) { - final (x, y) = projection.projectXY(points[k]); - return DoublePoint(x, y); - }, - growable: false, - ); - }, - growable: false, - ); - }(), - ); -} - class _PolygonPainter extends CustomPainter { final List<_ProjectedPolygon> polygons; final MapCamera camera; diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index eef7c45c8..602a9816a 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -13,6 +13,7 @@ import 'package:polylabel/polylabel.dart'; // conflict with Path from UI part 'label.dart'; part 'painter.dart'; part 'polygon.dart'; +part 'projected_polygon.dart'; @immutable class PolygonLayer extends StatefulWidget { @@ -66,12 +67,10 @@ class _PolygonLayerState extends State { void didUpdateWidget(PolygonLayer oldWidget) { super.didUpdateWidget(oldWidget); + // Reuse cache if (widget.simplificationTolerance != 0 && oldWidget.simplificationTolerance == widget.simplificationTolerance && - listEquals(oldWidget.polygons, widget.polygons)) { - // Reuse cache. - return; - } + listEquals(oldWidget.polygons, widget.polygons)) return; _cachedSimplifiedPolygons.clear(); _cachedProjectedPolygons = null; @@ -90,15 +89,13 @@ class _PolygonLayerState extends State { growable: false, ); - final zoom = camera.zoom.floor(); - final simplified = widget.simplificationTolerance <= 0 ? projected - : _cachedSimplifiedPolygons[zoom] ??= _computeZoomLevelSimplification( - camera.crs, - projected, - widget.simplificationTolerance, - zoom, + : _cachedSimplifiedPolygons[camera.zoom.floor()] ??= + _computeZoomLevelSimplification( + polygons: projected, + pixelTolerance: widget.simplificationTolerance, + camera: camera, ); final culled = !widget.polygonCulling @@ -122,16 +119,15 @@ class _PolygonLayerState extends State { ); } - static List<_ProjectedPolygon> _computeZoomLevelSimplification( - Crs crs, - List<_ProjectedPolygon> polygons, - double pixelTolerance, - int zoom, - ) { + static List<_ProjectedPolygon> _computeZoomLevelSimplification({ + required List<_ProjectedPolygon> polygons, + required double pixelTolerance, + required MapCamera camera, + }) { final tolerance = getEffectiveSimplificationTolerance( - crs, - zoom, - pixelTolerance, + crs: camera.crs, + zoom: camera.zoom.floor(), + pixelTolerance: pixelTolerance, ); return List<_ProjectedPolygon>.generate( @@ -143,18 +139,18 @@ class _PolygonLayerState extends State { return _ProjectedPolygon._( polygon: polygon.polygon, points: simplifyPoints( - polygon.points, - tolerance, - highestQuality: true, + points: polygon.points, + tolerance: tolerance, + highQuality: true, ), holePoints: holes == null ? null : List>.generate( holes.length, (j) => simplifyPoints( - holes[j], - tolerance, - highestQuality: true, + points: holes[j], + tolerance: tolerance, + highQuality: true, ), growable: false, ), diff --git a/lib/src/layer/polygon_layer/projected_polygon.dart b/lib/src/layer/polygon_layer/projected_polygon.dart new file mode 100644 index 000000000..f597244c7 --- /dev/null +++ b/lib/src/layer/polygon_layer/projected_polygon.dart @@ -0,0 +1,47 @@ +part of 'polygon_layer.dart'; + +@immutable +class _ProjectedPolygon { + final Polygon polygon; + final List points; + final List>? holePoints; + + const _ProjectedPolygon._({ + required this.polygon, + required this.points, + this.holePoints, + }); + + _ProjectedPolygon.fromPolygon(Projection projection, Polygon polygon) + : this._( + polygon: polygon, + points: List.generate( + polygon.points.length, + (j) { + final (x, y) = projection.projectXY(polygon.points[j]); + return DoublePoint(x, y); + }, + growable: false, + ), + holePoints: () { + final holes = polygon.holePointsList; + if (holes == null) return null; + + return List>.generate( + holes.length, + (j) { + final points = holes[j]; + return List.generate( + points.length, + (k) { + final (x, y) = projection.projectXY(points[k]); + return DoublePoint(x, y); + }, + growable: false, + ); + }, + growable: false, + ); + }(), + ); +} diff --git a/lib/src/layer/polyline_layer/polyline_layer.dart b/lib/src/layer/polyline_layer/polyline_layer.dart index c3103d7e1..87b97c8b3 100644 --- a/lib/src/layer/polyline_layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer/polyline_layer.dart @@ -131,14 +131,15 @@ 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( - polyline.points, - widget.simplificationTolerance / math.pow(2, zoom), - highestQuality: true, + points: polyline.points, + tolerance: widget.simplificationTolerance / math.pow(2, zoom), + highQuality: true, ), ), ) diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index 3b6c5b84d..f6520e306 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -1,13 +1,16 @@ // implementation based on // https://github.com/mourner/simplify-js/blob/master/simplify.js + import 'dart:math' as math; import 'package:flutter_map/src/geo/crs.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; // Custom point due to math.Point being slow. Math operations tend to // have 20+x penalty for virtual function overhead given the reified nature of // Dart generics. +@internal class DoublePoint { // Note: Allow mutability for reuse/pooling to reduce GC pressure and increase performance. // Geometry operations should be safe-by-default to avoid accidental bugs. @@ -55,6 +58,7 @@ double getSqSegDist( return dx * dx + dy * dy; } +//! Might actually be more expensive than DP, which is also better List simplifyRadialDist( List points, double sqTolerance, @@ -107,7 +111,6 @@ void _simplifyDPStep( } } -// simplification using Ramer-Douglas-Peucker algorithm List simplifyDouglasPeucker( List points, double sqTolerance, @@ -119,10 +122,10 @@ List simplifyDouglasPeucker( return simplified; } -List simplify( - List points, - double tolerance, { - bool highestQuality = false, +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; @@ -132,9 +135,9 @@ List simplify( (i) => DoublePoint(points[i].longitude, points[i].latitude), ); final double sqTolerance = tolerance * tolerance; - nextPoints = - highestQuality ? nextPoints : simplifyRadialDist(nextPoints, sqTolerance); - nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); + nextPoints = highQuality + ? simplifyDouglasPeucker(nextPoints, sqTolerance) + : simplifyRadialDist(nextPoints, sqTolerance); return List.generate( nextPoints.length, @@ -142,28 +145,25 @@ List simplify( ); } -List simplifyPoints( - final List points, - double tolerance, { - bool highestQuality = false, +List simplifyPoints({ + required final List points, + required double tolerance, + required bool highQuality, }) { // Don't simplify anything less than a square if (points.length <= 4) return points; - List nextPoints = points; final double sqTolerance = tolerance * tolerance; - nextPoints = - highestQuality ? nextPoints : simplifyRadialDist(nextPoints, sqTolerance); - nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); - - return nextPoints; + return highQuality + ? simplifyDouglasPeucker(points, sqTolerance) + : simplifyRadialDist(points, sqTolerance); } -double getEffectiveSimplificationTolerance( - Crs crs, - int zoom, - double pixelTolerance, -) { +double getEffectiveSimplificationTolerance({ + required Crs crs, + required int zoom, + required double pixelTolerance, +}) { if (pixelTolerance <= 0) return 0; final (x0, y0) = crs.untransform(0, 0, crs.scale(zoom.toDouble())); From d6ff03fc8b3d0bfedd568dbae0b67f0901e35834 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 21 Jan 2024 13:06:17 +0100 Subject: [PATCH 11/11] Add more comments on the internal nature of types and explicitly annotate CrsWithStaticTransformation as internal. --- lib/flutter_map.dart | 2 +- lib/src/geo/crs.dart | 2 ++ lib/src/misc/offsets.dart | 1 + lib/src/misc/simplify.dart | 14 ++++++++------ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 15adc1280..7cb3fa6e9 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -16,7 +16,7 @@ /// * discord.gg: library flutter_map; -export 'package:flutter_map/src/geo/crs.dart'; +export 'package:flutter_map/src/geo/crs.dart' hide CrsWithStaticTransformation; export 'package:flutter_map/src/geo/latlng_bounds.dart'; export 'package:flutter_map/src/gestures/interactive_flag.dart'; export 'package:flutter_map/src/gestures/latlng_tween.dart'; diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 80963a862..89fe1b9d1 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -58,7 +58,9 @@ abstract class Crs { Bounds? getProjectedBounds(double zoom); } +/// Internal base class for CRS with a single zoom-level independent transformation. @immutable +@internal abstract class CrsWithStaticTransformation extends Crs { @nonVirtual @protected diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart index 10a6447bd..050994651 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart'; diff --git a/lib/src/misc/simplify.dart b/lib/src/misc/simplify.dart index f6520e306..b3573adba 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -7,13 +7,15 @@ import 'package:flutter_map/src/geo/crs.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; -// Custom point due to math.Point being slow. Math operations tend to -// have 20+x penalty for virtual function overhead given the reified nature of -// Dart generics. +/// Internal double-precision point/vector implementation not to be used in publicly. +/// +/// This is an optimization. Vector operations on math.Point tend to incur a 20+x +/// penalty due to virtual function overhead caused by reified generics. +/// +/// Further note that unlike math.Point, members are mutable to allow object reuse/pooling +/// and therefore reduce GC pressure. @internal -class DoublePoint { - // Note: Allow mutability for reuse/pooling to reduce GC pressure and increase performance. - // Geometry operations should be safe-by-default to avoid accidental bugs. +final class DoublePoint { double x; double y;