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 f762cb2fc..89fe1b9d1 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) { @@ -53,33 +58,42 @@ abstract class Crs { Bounds? getProjectedBounds(double zoom); } +/// Internal base class for CRS with a single zoom-level independent transformation. @immutable -abstract class _CrsWithStaticTransformation extends Crs { +@internal +abstract class CrsWithStaticTransformation extends Crs { @nonVirtual @protected - final _Transformation transformation; + final _Transformation _transformation; @override final Projection projection; - const _CrsWithStaticTransformation({ - required this.transformation, + const CrsWithStaticTransformation._({ + required _Transformation transformation, required this.projection, required super.code, required super.infinite, super.wrapLng, 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); - 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 +107,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 +118,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 +132,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 +146,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 +165,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 +239,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..9b2656879 100644 --- a/lib/src/layer/polygon_layer/painter.dart +++ b/lib/src/layer/polygon_layer/painter.dart @@ -1,13 +1,13 @@ part of 'polygon_layer.dart'; -class PolygonPainter extends CustomPainter { - final List polygons; +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, @@ -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(); @@ -73,11 +61,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 = getOffsets(origin, polygon.points); + 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 @@ -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, ); @@ -155,10 +144,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( @@ -221,19 +211,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( @@ -241,6 +235,8 @@ class PolygonPainter extends CustomPainter { o0.dy * remain + o1.dy * done, ); path.addOval(Rect.fromCircle(center: offset, radius: radius)); + + distance += stepLength; } startDistance = distance < totalDistance @@ -256,5 +252,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 bf215451c..ad65e19d3 100644 --- a/lib/src/layer/polygon_layer/polygon.dart +++ b/lib/src/layer/polygon_layer/polygon.dart @@ -77,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 fffb8e869..602a9816a 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 @@ -12,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 { @@ -24,15 +26,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; @@ -61,50 +60,55 @@ class PolygonLayer extends StatefulWidget { } class _PolygonLayerState extends State { - final _cachedSimplifiedPolygons = >{}; + List<_ProjectedPolygon>? _cachedProjectedPolygons; + 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()); - } + // Reuse cache + if (widget.simplificationTolerance != 0 && + oldWidget.simplificationTolerance == widget.simplificationTolerance && + listEquals(oldWidget.polygons, widget.polygons)) return; + + _cachedSimplifiedPolygons.clear(); + _cachedProjectedPolygons = null; } @override Widget build(BuildContext context) { final camera = MapCamera.of(context); - final simplified = widget.simplificationTolerance == 0 - ? widget.polygons - : _computeZoomLevelSimplification(camera.zoom.floor()); + final projected = _cachedProjectedPolygons ??= List.generate( + widget.polygons.length, + (i) => _ProjectedPolygon.fromPolygon( + camera.crs.projection, + widget.polygons[i], + ), + growable: false, + ); + + final simplified = widget.simplificationTolerance <= 0 + ? projected + : _cachedSimplifiedPolygons[camera.zoom.floor()] ??= + _computeZoomLevelSimplification( + polygons: projected, + pixelTolerance: widget.simplificationTolerance, + camera: camera, + ); 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, @@ -115,16 +119,44 @@ 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, - ), - ), - ) - .toList(); + static List<_ProjectedPolygon> _computeZoomLevelSimplification({ + required List<_ProjectedPolygon> polygons, + required double pixelTolerance, + required MapCamera camera, + }) { + final tolerance = getEffectiveSimplificationTolerance( + crs: camera.crs, + zoom: camera.zoom.floor(), + pixelTolerance: pixelTolerance, + ); + + 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: holes == null + ? null + : 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 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/painter.dart b/lib/src/layer/polyline_layer/painter.dart index 59e724395..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(_distToSegmentSquared( - 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,32 +281,7 @@ class PolylinePainter extends CustomPainter { } @override - bool shouldRepaint(PolylinePainter oldDelegate) => false; -} - -double _distanceSq(double x0, double y0, double x1, double y1) { - final dx = x0 - x1; - final dy = y0 - y1; - return dx * dx + dy * dy; -} - -double _distToSegmentSquared( - double px, - double py, - double x0, - double y0, - double x1, - double y1, -) { - final dx = x1 - x0; - final dy = y1 - y0; - final distanceSq = dx * dx + dy * dy; - if (distanceSq == 0) { - return _distanceSq(px, py, x0, y0); - } - - final t = (((px - x0) * dx + (py - y0) * dy) / distanceSq).clamp(0, 1); - return _distanceSq(px, py, x0 + t * dx, y0 + t * dy); + 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..87b97c8b3 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, @@ -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/offsets.dart b/lib/src/misc/offsets.dart index 888518ff9..050994651 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -1,6 +1,8 @@ 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'; Offset getOffset(MapCamera camera, Offset origin, LatLng point) { @@ -19,7 +21,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 +39,38 @@ List getOffsets(MapCamera camera, Offset origin, List points) { } return v; } + +List getOffsetsXY( + 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); + + 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 p = points[i]; + final (x, y) = mcrs.transform(p.x, p.y, 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 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..b3573adba 100644 --- a/lib/src/misc/simplify.dart +++ b/lib/src/misc/simplify.dart @@ -1,55 +1,76 @@ // 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'; -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; +/// 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 +final class DoublePoint { + 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, +double getSqSegDist( + final double px, + final double py, + final double x0, + final double y0, + final double x1, + final double y1, ) { - double x = p1.longitude; - double y = p1.latitude; - double dx = p2.longitude - x; - double dy = p2.latitude - y; + double dx = x1 - x0; + double dy = y1 - y0; if (dx != 0 || dy != 0) { - final double t = - ((p.longitude - x) * dx + (p.latitude - y) * dy) / (dx * dx + dy * dy); + final double t = ((px - x0) * dx + (py - y0) * dy) / (dx * dx + dy * dy); if (t > 1) { - x = p2.longitude; - y = p2.latitude; + 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.longitude - x; - dy = p.latitude - y; + dx = px - x0; + dy = py - y0; return dx * dx + dy * dy; } -List simplifyRadialDist( - List points, +//! Might actually be more expensive than DP, which is also better +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,16 +82,20 @@ List simplifyRadialDist( } void _simplifyDPStep( - List points, - int first, - int last, + List points, + final int first, + final int last, double sqTolerance, - List simplified, + 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; @@ -88,32 +113,69 @@ 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; } -/// high quality simplification uses the Ramer-Douglas-Peucker algorithm -/// otherwise it just merges close points -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; + + List nextPoints = List.generate( + points.length, + (i) => DoublePoint(points[i].longitude, points[i].latitude), + ); + final double sqTolerance = tolerance * tolerance; + nextPoints = highQuality + ? simplifyDouglasPeucker(nextPoints, sqTolerance) + : simplifyRadialDist(nextPoints, sqTolerance); + + return List.generate( + nextPoints.length, + (i) => LatLng(nextPoints[i].y, nextPoints[i].x), + ); +} + +List simplifyPoints({ + required final List points, + required double tolerance, + required bool highQuality, }) { - if (points.length <= 2) return points; + // Don't simplify anything less than a square + if (points.length <= 4) return points; - List nextPoints = points; final double sqTolerance = tolerance * tolerance; - nextPoints = - highestQuality ? points : simplifyRadialDist(nextPoints, sqTolerance); - nextPoints = simplifyDouglasPeucker(nextPoints, sqTolerance); + return highQuality + ? simplifyDouglasPeucker(points, sqTolerance) + : simplifyRadialDist(points, sqTolerance); +} + +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())); + final (x1, y1) = crs.untransform( + pixelTolerance, + pixelTolerance, + crs.scale(zoom.toDouble()), + ); - return nextPoints; + final dx = x1 - x0; + final dy = y1 - y0; + return math.sqrt(dx * dx + dy * dy); } 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();