From 5986b39c07d9828ae84593303a7f77d2d8708006 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 15 Jan 2024 23:23:11 +0100 Subject: [PATCH] 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 | 27 +++++--- lib/src/misc/offsets.dart | 34 +++++++++- .../tile_layer/tile_bounds/crs_fakes.dart | 14 +++-- 6 files changed, 111 insertions(+), 53 deletions(-) diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index bedca69d6..792ad0e8a 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) { final (x, y) = latLngToXY(latlng, scale(zoom)); @@ -53,32 +58,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), @@ -92,8 +104,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), @@ -103,9 +115,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(), @@ -117,11 +129,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(), @@ -131,12 +143,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), @@ -147,9 +162,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', @@ -221,6 +236,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 7fcae41e6..ae2efd0ee 100644 --- a/lib/src/layer/polygon_layer/polygon_layer.dart +++ b/lib/src/layer/polygon_layer/polygon_layer.dart @@ -4,9 +4,11 @@ import 'dart:ui' as ui; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:flutter_map/src/misc/offsets.dart'; import 'package:flutter_map/src/misc/point_extensions.dart'; import 'package:flutter_map/src/misc/simplify.dart'; import 'package:latlong2/latlong.dart' hide Path; @@ -119,15 +121,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 c399a07ae..e6e0c6d88 100644 --- a/lib/src/misc/offsets.dart +++ b/lib/src/misc/offsets.dart @@ -20,7 +20,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); @@ -38,3 +38,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();