From c7b0de5b73d1637e605b38a0528e907b7f6bd750 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 2 Dec 2023 00:25:17 +0100 Subject: [PATCH] perf!: optimize CRS performance and add micro-benchmark of limited expressiveness (#1727) --- benchmark/crs.dart | 107 +++++ example/lib/pages/custom_crs/custom_crs.dart | 2 - example/lib/pages/epsg3413_crs.dart | 1 - lib/src/geo/crs.dart | 416 +++++++++--------- .../layer/polygon_layer/polygon_layer.dart | 23 +- lib/src/layer/polyline_layer.dart | 19 +- lib/src/misc/bounds.dart | 1 + lib/src/misc/offsets.dart | 40 ++ .../tile_layer/tile_bounds/crs_fakes.dart | 25 +- .../tile_bounds/tile_bounds_test.dart | 4 +- 10 files changed, 377 insertions(+), 261 deletions(-) create mode 100644 benchmark/crs.dart create mode 100644 lib/src/misc/offsets.dart diff --git a/benchmark/crs.dart b/benchmark/crs.dart new file mode 100644 index 000000000..c2025cfd9 --- /dev/null +++ b/benchmark/crs.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:logger/logger.dart'; + +class NoFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) => true; +} + +typedef Result = ({ + String name, + Duration duration, +}); + +Future timedRun(String name, dynamic Function() body) async { + Logger().i('running $name...'); + final watch = Stopwatch()..start(); + await body(); + watch.stop(); + + return (name: name, duration: watch.elapsed); +} + +// NOTE: to have a more prod like comparison, run with: +// $ dart compile exe benchmark/crs.dart && ./benchmark/crs.exe +// +// If you run in JIT mode, the resulting execution times will be a lot more similar. +Future main() async { + Logger.level = Level.all; + Logger.defaultFilter = () => NoFilter(); + Logger.defaultPrinter = () => SimplePrinter(); + + final results = []; + const N = 100000000; + + const crs = Epsg3857(); + results.add(await timedRun('Concrete type: ${crs.code}.latLngToXY()', () { + double x = 0; + double y = 0; + for (int i = 0; i < N; ++i) { + final latlng = LatLng((i % 90).toDouble(), (i % 180).toDouble()); + final (cx, cy) = crs.latLngToXY(latlng, 1); + x += cx; + y += cy; + } + return x + y; + })); + + results.add(await timedRun('Concrete type: ${crs.code}.latLngToPoint()', () { + double x = 0; + double y = 0; + for (int i = 0; i < N; ++i) { + final latlng = LatLng((i % 90).toDouble(), (i % 180).toDouble()); + final p = crs.latLngToPoint(latlng, 1); + x += p.x; + y += p.y; + } + return x + y; + })); + + const crss = [ + Epsg3857(), + Epsg4326(), + ]; + + for (final crs in crss) { + results.add(await timedRun('${crs.code}.latLngToXY()', () { + double x = 0; + double y = 0; + for (int i = 0; i < N; ++i) { + final latlng = LatLng((i % 90).toDouble(), (i % 180).toDouble()); + final (cx, cy) = crs.latLngToXY(latlng, 1); + x += cx; + y += cy; + } + return x + y; + })); + + results.add(await timedRun('${crs.code}.latlngToPoint()', () { + double x = 0; + double y = 0; + for (int i = 0; i < N; ++i) { + final latlng = LatLng((i % 90).toDouble(), (i % 180).toDouble()); + final point = crs.latLngToPoint(latlng, 1); + x += point.x; + y += point.y; + } + return x + y; + })); + + results.add(await timedRun('${crs.code}.pointToLatLng()', () { + double x = 0; + double y = 0; + for (int i = 0; i < N; ++i) { + final latlng = crs.pointToLatLng(math.Point(x, y), 1); + x += latlng.longitude; + y += latlng.latitude; + } + return x + y; + })); + } + + Logger().i('Results:\n${results.map((r) => r.toString()).join('\n')}'); +} diff --git a/example/lib/pages/custom_crs/custom_crs.dart b/example/lib/pages/custom_crs/custom_crs.dart index aba6f6bb1..96d589cf8 100644 --- a/example/lib/pages/custom_crs/custom_crs.dart +++ b/example/lib/pages/custom_crs/custom_crs.dart @@ -83,8 +83,6 @@ class CustomCrsPageState extends State { // Scale factors (pixels per projection unit, for example pixels/meter) for zoom levels; // specify either scales or resolutions, not both scales: null, - // The transformation to use when transforming projected coordinates into pixel coordinates - transformation: null, ); } diff --git a/example/lib/pages/epsg3413_crs.dart b/example/lib/pages/epsg3413_crs.dart index dd266f3c6..a3c31986d 100644 --- a/example/lib/pages/epsg3413_crs.dart +++ b/example/lib/pages/epsg3413_crs.dart @@ -64,7 +64,6 @@ class EPSG3413PageState extends State { bounds: epsg3413Bounds, origins: const [Point(0, 0)], scales: null, - transformation: null, ); } diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index b78b04f2b..bedca69d6 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -13,24 +13,34 @@ import 'package:proj4dart/proj4dart.dart' as proj4; /// points of objects of different dimensions. In our case 3D and 2D objects. @immutable abstract class Crs { - const Crs(); + @nonVirtual + final String code; + @nonVirtual + final bool infinite; + @nonVirtual + final (double, double)? wrapLng; + @nonVirtual + final (double, double)? wrapLat; - String get code; + const Crs({ + required this.code, + required this.infinite, + this.wrapLng, + this.wrapLat, + }); Projection get projection; - Transformation get transformation; - /// Converts a point on the sphere surface (with a certain zoom) in a /// map point. + (double, double) latLngToXY(LatLng latlng, double scale); Point latLngToPoint(LatLng latlng, double zoom) { - final projectedPoint = projection.project(latlng); - return transformation.transform(projectedPoint, scale(zoom)); + final (x, y) = latLngToXY(latlng, scale(zoom)); + return Point(x, y); } /// Converts a map point to the sphere coordinate (at a certain zoom). - LatLng pointToLatLng(Point point, double zoom) => - projection.unproject(transformation.untransform(point, scale(zoom))); + LatLng pointToLatLng(Point point, double zoom); /// Zoom to Scale function. double scale(double zoom) => 256.0 * math.pow(2, zoom); @@ -39,156 +49,147 @@ abstract class Crs { double zoom(double scale) => math.log(scale / 256) / math.ln2; /// Rescales the bounds to a given zoom value. - Bounds? getProjectedBounds(double zoom) { - if (infinite) return null; - - final b = projection.bounds!; - final s = scale(zoom); - final min = transformation.transform(b.min, s); - final max = transformation.transform(b.max, s); - return Bounds(min, max); - } - - bool get infinite; - - (double, double)? get wrapLng; - - (double, double)? get wrapLat; + Bounds? getProjectedBounds(double zoom); } -// Custom CRS for non geographical maps @immutable -class CrsSimple extends Crs { - @override - final String code = 'CRS.SIMPLE'; +abstract class _CrsWithStaticTransformation extends Crs { + @nonVirtual + @protected + final _Transformation transformation; @override final Projection projection; - @override - final Transformation transformation; - - const CrsSimple() - : projection = const _LonLat(), - transformation = const Transformation(1, 0, -1, 0), - super(); + const _CrsWithStaticTransformation({ + required this.transformation, + required this.projection, + required super.code, + required super.infinite, + super.wrapLng, + super.wrapLat, + }); @override - bool get infinite => false; + (double, double) latLngToXY(LatLng latlng, double scale) { + final (x, y) = projection.projectXY(latlng); + return transformation.transform(x, y, scale); + } @override - (double, double)? get wrapLat => null; + LatLng pointToLatLng(Point point, double zoom) { + final (x, y) = transformation.untransform( + point.x.toDouble(), + point.y.toDouble(), + scale(zoom), + ); + return projection.unprojectXY(x, y); + } @override - (double, double)? get wrapLng => null; + Bounds? getProjectedBounds(double zoom) { + if (infinite) return null; + + 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); + return Bounds( + Point(minx, miny), + Point(maxx, maxy), + ); + } } +// Custom CRS for non geographical maps @immutable -abstract class Earth extends Crs { - @override - bool get infinite => false; - - @override - final (double, double) wrapLng = const (-180, 180); - - @override - final (double, double)? wrapLat = null; - - const Earth() : super(); +class CrsSimple extends _CrsWithStaticTransformation { + const CrsSimple() + : super( + code: 'CRS.SIMPLE', + transformation: const _Transformation(1, 0, -1, 0), + projection: const _LonLat(), + infinite: false, + wrapLat: null, + wrapLng: null, + ); } /// The most common CRS used for rendering maps. @immutable -class Epsg3857 extends Earth { - @override - final String code = 'EPSG:3857'; - - @override - final Projection projection; - - @override - final Transformation transformation; - +class Epsg3857 extends _CrsWithStaticTransformation { static const double _scale = 0.5 / (math.pi * SphericalMercator.r); const Epsg3857() - : projection = const SphericalMercator(), - transformation = const Transformation(_scale, 0.5, -_scale, 0.5), - super(); - -// Epsg3857 seems to have latitude limits. https://epsg.io/3857 -//@override -//(double, double) get wrapLat => const (-85.06, 85.06); -} - -/// A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. -@immutable -class Epsg4326 extends Earth { - @override - final String code = 'EPSG:4326'; + : super( + code: 'EPSG:3857', + transformation: const _Transformation(_scale, 0.5, -_scale, 0.5), + projection: const SphericalMercator(), + infinite: false, + wrapLng: const (-180, 180), + ); @override - final Projection projection; + (double, double) latLngToXY(LatLng latlng, double scale) => + transformation.transform(SphericalMercator.projectLng(latlng.longitude), + SphericalMercator.projectLat(latlng.latitude), scale); @override - final Transformation transformation; + Point latLngToPoint(LatLng latlng, double zoom) { + final (x, y) = transformation.transform( + SphericalMercator.projectLng(latlng.longitude), + SphericalMercator.projectLat(latlng.latitude), + scale(zoom), + ); + return Point(x, y); + } +} +/// A common CRS among GIS enthusiasts. Uses simple Equirectangular projection. +@immutable +class Epsg4326 extends _CrsWithStaticTransformation { const Epsg4326() - : projection = const _LonLat(), - transformation = const Transformation(1 / 180, 1, -1 / 180, 0.5), - super(); + : super( + projection: const _LonLat(), + transformation: const _Transformation(1 / 180, 1, -1 / 180, 0.5), + code: 'EPSG:4326', + infinite: false, + wrapLng: const (-180, 180), + ); } /// Custom CRS @immutable class Proj4Crs extends Crs { - @override - final String code; - @override final Projection projection; - - @override - final Transformation transformation; - - @override - final bool infinite; - - @override - final (double, double)? wrapLat = null; - - @override - final (double, double)? wrapLng = null; - - final List? _transformations; - + final List<_Transformation> _transformations; final List _scales; const Proj4Crs._({ - required this.code, + required super.code, required this.projection, - required this.transformation, - required this.infinite, - List? transformations, + required super.infinite, + required List<_Transformation> transformations, required List scales, }) : _transformations = transformations, - _scales = scales; + _scales = scales, + super(wrapLat: null, wrapLng: null); factory Proj4Crs.fromFactory({ required String code, required proj4.Projection proj4Projection, - Transformation? transformation, List>? origins, Bounds? bounds, List? scales, List? resolutions, }) { - final projection = - _Proj4Projection(proj4Projection: proj4Projection, bounds: bounds); - List? transformations; - final infinite = null == bounds; - List finalScales; + final projection = _Proj4Projection( + proj4Projection: proj4Projection, + bounds: bounds, + ); + List finalScales; if (null != scales && scales.isNotEmpty) { finalScales = scales; } else if (null != resolutions && resolutions.isNotEmpty) { @@ -198,24 +199,23 @@ class Proj4Crs extends Crs { 'Please provide scales or resolutions to determine scales'); } + List<_Transformation> transformations; if (null == origins || origins.isEmpty) { - transformation ??= const Transformation(1, 0, -1, 0); + transformations = [const _Transformation(1, 0, -1, 0)]; } else { if (origins.length == 1) { final origin = origins[0]; - transformation = Transformation(1, -origin.x, -1, origin.y); + transformations = [_Transformation(1, -origin.x, -1, origin.y)]; } else { transformations = - origins.map((p) => Transformation(1, -p.x, -1, p.y)).toList(); - transformation = null; + origins.map((p) => _Transformation(1, -p.x, -1, p.y)).toList(); } } return Proj4Crs._( code: code, projection: projection, - transformation: transformation!, - infinite: infinite, + infinite: null == bounds, transformations: transformations, scales: finalScales, ); @@ -224,18 +224,22 @@ class Proj4Crs extends Crs { /// Converts a point on the sphere surface (with a certain zoom) in a /// map point. @override - Point latLngToPoint(LatLng latlng, double zoom) { - final projectedPoint = projection.project(latlng); - final scale = this.scale(zoom); - final transformation = _getTransformationByZoom(zoom); - - return transformation.transform(projectedPoint, scale); + (double, double) latLngToXY(LatLng latlng, double scale) { + final (x, y) = projection.projectXY(latlng); + final transformation = _getTransformationByZoom(zoom(scale)); + return transformation.transform(x, y, scale); } /// Converts a map point to the sphere coordinate (at a certain zoom). @override - LatLng pointToLatLng(Point point, double zoom) => projection.unproject( - _getTransformationByZoom(zoom).untransform(point, scale(zoom))); + LatLng pointToLatLng(Point point, double zoom) { + final (x, y) = _getTransformationByZoom(zoom).untransform( + point.x.toDouble(), + point.y.toDouble(), + scale(zoom), + ); + return projection.unprojectXY(x, y); + } /// Rescales the bounds to a given zoom value. @override @@ -243,13 +247,15 @@ class Proj4Crs extends Crs { if (infinite) return null; final b = projection.bounds!; - final s = scale(zoom); + final zoomScale = scale(zoom); final transformation = _getTransformationByZoom(zoom); - - final min = transformation.transform(b.min, s); - final max = transformation.transform(b.max, s); - return Bounds(min, max); + final (minx, miny) = transformation.transform(b.min.x, b.min.y, zoomScale); + final (maxx, maxy) = transformation.transform(b.max.x, b.max.y, zoomScale); + return Bounds( + Point(minx, miny), + Point(maxx, maxy), + ); } /// Zoom to Scale function. @@ -303,66 +309,48 @@ class Proj4Crs extends Crs { } /// returns Transformation object based on zoom - Transformation _getTransformationByZoom(double zoom) { - final transformations = _transformations; - if (transformations == null || transformations.isEmpty) { - return transformation; - } - + _Transformation _getTransformationByZoom(double zoom) { final iZoom = zoom.round(); - final lastIdx = transformations.length - 1; - - return transformations[iZoom > lastIdx ? lastIdx : iZoom]; + final lastIdx = _transformations.length - 1; + return _transformations[iZoom > lastIdx ? lastIdx : iZoom]; } } @immutable abstract class Projection { - const Projection(); - - Bounds? get bounds; - - Point project(LatLng latlng); - - LatLng unproject(Point point); + final Bounds? bounds; - double _inclusive(double start, double end, double value) { - if (value < start) return start; - if (value > end) return end; + const Projection(this.bounds); - return value; + @nonVirtual + Point project(LatLng latlng) { + final (x, y) = projectXY(latlng); + return Point(x, y); } - @protected - double inclusiveLat(double value) { - return _inclusive(-90, 90, value); - } + (double, double) projectXY(LatLng latlng); - @protected - double inclusiveLng(double value) { - return _inclusive(-180, 180, value); - } + @nonVirtual + LatLng unproject(Point point) => + unprojectXY(point.x.toDouble(), point.y.toDouble()); + LatLng unprojectXY(double x, double y); } class _LonLat extends Projection { - static final Bounds _bounds = Bounds( - const Point(-180, -90), const Point(180, 90)); - - const _LonLat() : super(); + static const _bounds = Bounds.unsafe( + Point(-180, -90), + Point(180, 90), + ); - @override - Bounds get bounds => _bounds; + const _LonLat() : super(_bounds); @override - Point project(LatLng latlng) { - return Point(latlng.longitude, latlng.latitude); - } + (double, double) projectXY(LatLng latlng) => + (latlng.longitude, latlng.latitude); @override - LatLng unproject(Point point) { - return LatLng( - inclusiveLat(point.y.toDouble()), inclusiveLng(point.x.toDouble())); - } + LatLng unprojectXY(double x, double y) => + LatLng(_inclusiveLat(y), _inclusiveLng(x)); } @immutable @@ -370,89 +358,97 @@ class SphericalMercator extends Projection { static const int r = 6378137; static const double maxLatitude = 85.0511287798; static const double _boundsD = r * math.pi; - static final Bounds _bounds = Bounds( - const Point(-_boundsD, -_boundsD), - const Point(_boundsD, _boundsD), + + static const Bounds _bounds = Bounds.unsafe( + Point(-_boundsD, -_boundsD), + Point(_boundsD, _boundsD), ); - const SphericalMercator() : super(); + const SphericalMercator() : super(_bounds); - @override - Bounds get bounds => _bounds; + static double projectLat(double latitude) { + final lat = _clampSym(latitude, maxLatitude); + final sin = math.sin(lat * math.pi / 180); - @override - Point project(LatLng latlng) { - const d = math.pi / 180; - final lat = latlng.latitude.clamp(-maxLatitude, maxLatitude); - final sin = math.sin(lat * d); + return r / 2 * math.log((1 + sin) / (1 - sin)); + } + + static double projectLng(double longitude) { + return r * math.pi / 180 * longitude; + } - return Point( - r * d * latlng.longitude, - r / 2 * math.log((1 + sin) / (1 - sin)), + @override + (double, double) projectXY(LatLng latlng) { + return ( + projectLng(latlng.longitude), + projectLat(latlng.latitude), ); } @override - LatLng unproject(Point point) { + LatLng unprojectXY(double x, double y) { const d = 180 / math.pi; return LatLng( - inclusiveLat( - (2 * math.atan(math.exp(point.y / r)) - (math.pi / 2)) * d), - inclusiveLng(point.x * d / r)); + _inclusiveLat((2 * math.atan(math.exp(y / r)) - (math.pi / 2)) * d), + _inclusiveLng(x * d / r), + ); } } @immutable class _Proj4Projection extends Projection { final proj4.Projection epsg4326; - final proj4.Projection proj4Projection; - @override - final Bounds? bounds; - _Proj4Projection({ required this.proj4Projection, - this.bounds, - }) : epsg4326 = proj4.Projection.WGS84; + required Bounds? bounds, + }) : epsg4326 = proj4.Projection.WGS84, + super(bounds); @override - Point project(LatLng latlng) { + (double, double) projectXY(LatLng latlng) { final point = epsg4326.transform( proj4Projection, proj4.Point(x: latlng.longitude, y: latlng.latitude)); - return Point(point.x, point.y); + return (point.x, point.y); } @override - LatLng unproject(Point point) { - final point2 = proj4Projection.transform( - epsg4326, proj4.Point(x: point.x.toDouble(), y: point.y.toDouble())); + LatLng unprojectXY(double x, double y) { + final point = proj4Projection.transform(epsg4326, proj4.Point(x: x, y: y)); - return LatLng(inclusiveLat(point2.y), inclusiveLng(point2.x)); + return LatLng( + _inclusiveLat(point.y), + _inclusiveLng(point.x), + ); } } @immutable -class Transformation { +class _Transformation { final double a; final double b; final double c; final double d; - const Transformation(this.a, this.b, this.c, this.d); + const _Transformation(this.a, this.b, this.c, this.d); - Point transform(Point point, double? scale) { - scale ??= 1.0; - final x = scale * (a * point.x + b); - final y = scale * (c * point.y + d); - return Point(x, y); - } + @nonVirtual + (double, double) transform(double x, double y, double scale) => ( + scale * (a * x + b), + scale * (c * y + d), + ); - Point untransform(Point point, double? scale) { - scale ??= 1.0; - final x = (point.x / scale - b) / a; - final y = (point.y / scale - d) / c; - return Point(x, y); - } + @nonVirtual + (double, double) untransform(double x, double y, double scale) => ( + (x / scale - b) / a, + (y / scale - d) / c, + ); } + +// Num.clamp is slow due to virtual function overhead. +double _clampSym(double value, double limit) => + value < -limit ? -limit : (value > limit ? limit : value); +double _inclusiveLat(double value) => _clampSym(value, 90); +double _inclusiveLng(double value) => _clampSym(value, 180); diff --git a/lib/src/layer/polygon_layer/polygon_layer.dart b/lib/src/layer/polygon_layer/polygon_layer.dart index ca5e7f578..414856d7a 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:flutter_map/src/geo/latlng_bounds.dart'; import 'package:flutter_map/src/layer/general/mobile_layer_transformer.dart'; import 'package:flutter_map/src/layer/polygon_layer/label.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:latlong2/latlong.dart' hide Path; // conflict with Path from UI @@ -164,22 +165,8 @@ 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), - ); - } - - Offset getOffset(Offset origin, LatLng point) { - // Critically create as little garbage as possible. This is called on every frame. - final projected = map.project(point); - return Offset(projected.x - origin.dx, projected.y - origin.dy); - } - - List getOffsets(Offset origin, List points) { - return List.generate( - points.length, - (index) => getOffset(origin, points[index]), - growable: false, + min: getOffset(map, origin, bbox.southWest), + max: getOffset(map, origin, bbox.northEast), ); } @@ -225,7 +212,7 @@ class PolygonPainter extends CustomPainter { if (polygon.points.isEmpty) { continue; } - final offsets = getOffsets(origin, polygon.points); + final offsets = getOffsets(map, origin, polygon.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 @@ -255,7 +242,7 @@ class PolygonPainter extends CustomPainter { final holeOffsetsList = List>.generate( holePointsList.length, - (i) => getOffsets(origin, holePointsList[i]), + (i) => getOffsets(map, origin, holePointsList[i]), growable: false, ); diff --git a/lib/src/layer/polyline_layer.dart b/lib/src/layer/polyline_layer.dart index b20de99f1..9e71f60df 100644 --- a/lib/src/layer/polyline_layer.dart +++ b/lib/src/layer/polyline_layer.dart @@ -5,6 +5,7 @@ import 'package:flutter/widgets.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:latlong2/latlong.dart'; @@ -98,20 +99,6 @@ class PolylinePainter extends CustomPainter { int? _hash; - Offset getOffset(Offset origin, LatLng point) { - // Critically create as little garbage as possible. This is called on every frame. - final projected = map.project(point); - return Offset(projected.x - origin.dx, projected.y - origin.dy); - } - - List getOffsets(Offset origin, List points) { - return List.generate( - points.length, - (index) => getOffset(origin, points[index]), - growable: false, - ); - } - @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; @@ -154,7 +141,7 @@ class PolylinePainter extends CustomPainter { final origin = map.project(map.center).toOffset() - map.size.toOffset() / 2; for (final polyline in polylines) { - final offsets = getOffsets(origin, polyline.points); + final offsets = getOffsets(map, origin, polyline.points); if (offsets.isEmpty) { continue; } @@ -176,7 +163,7 @@ class PolylinePainter extends CustomPainter { polyline.strokeWidth, 180, ); - final delta = firstOffset - getOffset(origin, r); + final delta = firstOffset - getOffset(map, origin, r); strokeWidth = delta.distance; } else { diff --git a/lib/src/misc/bounds.dart b/lib/src/misc/bounds.dart index 731f91031..5ef5df29d 100644 --- a/lib/src/misc/bounds.dart +++ b/lib/src/misc/bounds.dart @@ -16,6 +16,7 @@ class Bounds { return Bounds._(Point(minx, miny), Point(maxx, maxy)); } + const Bounds.unsafe(this.min, this.max); const Bounds._(this.min, this.max); static Bounds containing(Iterable> points) { diff --git a/lib/src/misc/offsets.dart b/lib/src/misc/offsets.dart new file mode 100644 index 000000000..c399a07ae --- /dev/null +++ b/lib/src/misc/offsets.dart @@ -0,0 +1,40 @@ +import 'dart:ui'; + +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/map/camera/camera.dart'; +import 'package:latlong2/latlong.dart'; + +Offset getOffset(MapCamera camera, Offset origin, LatLng point) { + final crs = camera.crs; + final zoomScale = crs.scale(camera.zoom); + final (x, y) = crs.latLngToXY(point, zoomScale); + return Offset(x - origin.dx, y - origin.dy); +} + +List getOffsets(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 Epsg3857-case to save the virtual function overhead. + if (crs is Epsg3857) { + final Epsg3857 epsg3857 = crs; + final v = List.filled(len, Offset.zero); + for (int i = 0; i < len; ++i) { + final (x, y) = epsg3857.latLngToXY(points[i], 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 (x, y) = crs.latLngToXY(points[i], 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 4465604c0..cfa16956b 100644 --- a/test/layer/tile_layer/tile_bounds/crs_fakes.dart +++ b/test/layer/tile_layer/tile_bounds/crs_fakes.dart @@ -1,30 +1,31 @@ import 'dart:math'; import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/misc/bounds.dart'; import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; +@immutable class FakeInfiniteCrs extends Crs { - @override - String get code => throw UnimplementedError(); - - @override - bool get infinite => true; + const FakeInfiniteCrs() : super(code: 'fake', infinite: true); @override Projection get projection => throw UnimplementedError(); + /// Any projection just to get non-zero coordiantes. @override - Transformation get transformation => throw UnimplementedError(); + Point latLngToPoint(LatLng latlng, double zoom) { + return const Epsg3857().latLngToPoint(latlng, zoom); + } @override - (double, double)? get wrapLat => null; + (double, double) latLngToXY(LatLng latlng, double scale) { + return const Epsg3857().latLngToXY(latlng, scale); + } @override - (double, double)? get wrapLng => null; + LatLng pointToLatLng(Point point, double zoom) => throw UnimplementedError(); - /// Any projection just to get non-zero coordiantes. @override - Point latLngToPoint(LatLng latlng, double zoom) { - return const Epsg3857().latLngToPoint(latlng, zoom); - } + Bounds? getProjectedBounds(double zoom) => throw UnimplementedError(); } diff --git a/test/layer/tile_layer/tile_bounds/tile_bounds_test.dart b/test/layer/tile_layer/tile_bounds/tile_bounds_test.dart index c782f1794..c7da1a45a 100644 --- a/test/layer/tile_layer/tile_bounds/tile_bounds_test.dart +++ b/test/layer/tile_layer/tile_bounds/tile_bounds_test.dart @@ -14,7 +14,7 @@ void main() { group('TileBounds', () { test('crs is infinite, latLngBounds null', () { final tileBounds = TileBounds( - crs: FakeInfiniteCrs(), + crs: const FakeInfiniteCrs(), tileSize: 256, ); @@ -24,7 +24,7 @@ void main() { test('crs is infinite, latLngBounds provided', () { final tileBounds = TileBounds( - crs: FakeInfiniteCrs(), + crs: const FakeInfiniteCrs(), tileSize: 256, latLngBounds: LatLngBounds.fromPoints( [const LatLng(-44, -55), const LatLng(44, 55)],