diff --git a/benchmark/crs.dart b/benchmark/crs.dart index c29c4e9ad..368369fa3 100644 --- a/benchmark/crs.dart +++ b/benchmark/crs.dart @@ -29,6 +29,7 @@ Future main() async { const N = 100000000; const crs = Epsg3857(); + final cachedCrs = Epsg3857Cached(); results.add(await timedRun('Concrete type: ${crs.code}.latLngToXY()', () { double x = 0; double y = 0; @@ -40,6 +41,20 @@ Future main() async { } return x + y; })); + results.add(await timedRun( + 'Concrete type (cached): ${cachedCrs.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) = cachedCrs.latLngToXY(latlng, 1); + x += cx; + y += cy; + } + return x + y; + }, + )); results.add(await timedRun('Concrete type: ${crs.code}.latLngToOffset()', () { double x = 0; @@ -53,13 +68,30 @@ Future main() async { return x + y; })); - const crss = [ - Epsg3857(), - Epsg4326(), + results.add(await timedRun( + 'Concrete type (cached): ${cachedCrs.code}.latLngToOffset()', + () { + double x = 0; + double y = 0; + for (int i = 0; i < N; ++i) { + final latlng = LatLng((i % 90).toDouble(), (i % 180).toDouble()); + final p = cachedCrs.latLngToOffset(latlng, 1); + x += p.dx; + y += p.dy; + } + return x + y; + }, + )); + + final crss = <(String, Crs)>[ + ('EPSG:3857', const Epsg3857()), + ('EPSG:3857 (cached)', Epsg3857Cached()), + ('EPSG:4326', const Epsg4326()), + ('EPSG:4326 (cached)', Epsg4326Cached()), ]; - for (final crs in crss) { - results.add(await timedRun('${crs.code}.latLngToXY()', () { + for (final (label, crs) in crss) { + results.add(await timedRun('$label.latLngToXY()', () { double x = 0; double y = 0; for (int i = 0; i < N; ++i) { @@ -71,7 +103,7 @@ Future main() async { return x + y; })); - results.add(await timedRun('${crs.code}.latlngToPoint()', () { + results.add(await timedRun('$label.latlngToPoint()', () { double x = 0; double y = 0; for (int i = 0; i < N; ++i) { @@ -83,7 +115,7 @@ Future main() async { return x + y; })); - results.add(await timedRun('${crs.code}.pointToLatLng()', () { + results.add(await timedRun('$label.pointToLatLng()', () { double x = 0; double y = 0; for (int i = 0; i < N; ++i) { diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index f4dc608e7..a6061c85e 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -61,10 +61,10 @@ abstract class Crs { LatLng offsetToLatLng(Offset point, double zoom); /// Zoom to Scale function. - double scale(double zoom) => 256.0 * math.pow(2, zoom); + double scale(double zoom) => 256.0 * math.pow(2.0, zoom); /// Scale to Zoom function. - double zoom(double scale) => math.log(scale / 256) / math.ln2; + double zoom(double scale) => math.log(scale / 256.0) / math.ln2; /// Rescales the bounds to a given zoom value. Rect? getProjectedBounds(double zoom); @@ -85,6 +85,47 @@ abstract class Crs { } } +final class _ScaleZoomCache { + double lastScaleZoom = double.nan; + double lastScaleValue = double.nan; + double lastZoomScale = double.nan; + double lastZoomValue = double.nan; +} + +mixin _ScaleCacheMixin on Crs { + static final Expando<_ScaleZoomCache> _caches = Expando<_ScaleZoomCache>( + '_scaleZoomCache', + ); + + _ScaleZoomCache get _cache { + final existing = _caches[this]; + if (existing != null) return existing; + final created = _ScaleZoomCache(); + _caches[this] = created; + return created; + } + + @override + double scale(double zoom) { + final cache = _cache; + if (zoom == cache.lastScaleZoom) return cache.lastScaleValue; + final value = super.scale(zoom); + cache.lastScaleZoom = zoom; + cache.lastScaleValue = value; + return value; + } + + @override + double zoom(double scale) { + final cache = _cache; + if (scale == cache.lastZoomScale) return cache.lastZoomValue; + final value = super.zoom(scale); + cache.lastZoomScale = scale; + cache.lastZoomValue = value; + return value; + } +} + /// Internal base class for CRS with a single zoom-level independent transformation. @immutable @internal @@ -159,6 +200,11 @@ class CrsSimple extends CrsWithStaticTransformation { ); } +/// Non-const CRS with cached scale/zoom. +class CrsSimpleCached extends CrsSimple with _ScaleCacheMixin { + CrsSimpleCached() : super(); +} + /// EPSG:3857, The most common CRS used for rendering maps. @immutable class Epsg3857 extends CrsWithStaticTransformation { @@ -199,6 +245,11 @@ class Epsg3857 extends CrsWithStaticTransformation { bool get replicatesWorldLongitude => true; } +/// Non-const CRS with cached scale/zoom. +class Epsg3857Cached extends Epsg3857 with _ScaleCacheMixin { + Epsg3857Cached() : super(); +} + /// EPSG:4326, A common CRS among GIS enthusiasts. /// Uses simple Equirectangular projection. @immutable @@ -214,6 +265,11 @@ class Epsg4326 extends CrsWithStaticTransformation { ); } +/// Non-const CRS with cached scale/zoom. +class Epsg4326Cached extends Epsg4326 with _ScaleCacheMixin { + Epsg4326Cached() : super(); +} + /// Custom CRS @immutable class Proj4Crs extends Crs {