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 7b4a977e5..632e472fb 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); @@ -74,6 +74,51 @@ abstract class Crs { bool get replicatesWorldLongitude => false; } +/// Mixin which uses [_ScaleZoomCache] to implement [scale] & [zoom] methods +/// backed by a cache, to improve performance. +mixin _ScaleCacheMixin on Crs { + static final _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; + } +} + +/// Object used by a caching Crs (which mixes in [_ScaleCacheMixin]) to store +/// cache details +class _ScaleZoomCache { + _ScaleZoomCache(); + + double lastScaleZoom = double.nan; + double lastScaleValue = double.nan; + double lastZoomScale = double.nan; + double lastZoomValue = double.nan; +} + /// Internal base class for CRS with a single zoom-level independent transformation. @immutable @internal @@ -133,7 +178,9 @@ abstract class CrsWithStaticTransformation extends Crs { } } -/// Custom CRS for non geographical maps +/// Custom CRS for non geographical maps. +/// +/// See also [CrsSimpleCached]. @immutable class CrsSimple extends CrsWithStaticTransformation { /// Create a new [CrsSimple]. @@ -148,7 +195,15 @@ class CrsSimple extends CrsWithStaticTransformation { ); } -/// EPSG:3857, The most common CRS used for rendering maps. +/// [CrsSimple] with cached scale/zoom. +class CrsSimpleCached extends CrsSimple with _ScaleCacheMixin { + /// [CrsSimple] with cached scale/zoom. + CrsSimpleCached() : super(); +} + +/// EPSG:3857, the most common CRS used for rendering maps. +/// +/// See also [Epsg3857Cached]. @immutable class Epsg3857 extends CrsWithStaticTransformation { static const double _scale = 0.5 / (math.pi * SphericalMercator.r); @@ -185,8 +240,16 @@ class Epsg3857 extends CrsWithStaticTransformation { bool get replicatesWorldLongitude => true; } -/// EPSG:4326, A common CRS among GIS enthusiasts. -/// Uses simple Equirectangular projection. +/// [Epsg3857] with cached scale/zoom. +class Epsg3857Cached extends Epsg3857 with _ScaleCacheMixin { + /// [Epsg3857] with cached scale/zoom. + Epsg3857Cached() : super(); +} + +/// EPSG:4326, a common CRS among GIS enthusiasts using simple equirectangular +/// projection. +/// +/// See also [Epsg4326Cached]. @immutable class Epsg4326 extends CrsWithStaticTransformation { /// Create a new [Epsg4326] CRS instance. @@ -200,6 +263,12 @@ class Epsg4326 extends CrsWithStaticTransformation { ); } +/// [Epsg4326] with cached scale/zoom. +class Epsg4326Cached extends Epsg4326 with _ScaleCacheMixin { + /// [Epsg4326] with cached scale/zoom. + Epsg4326Cached() : super(); +} + /// Custom CRS @immutable class Proj4Crs extends Crs {