From e30d6ab646ffeb95772157ee5b9beb11e447cc07 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 18 Aug 2024 13:11:32 +0200 Subject: [PATCH 1/8] feat: 1582 - smooth scroll beyond the end of the world New file: * `tile_renderer.dart`: Display of a [TileImage] at given [TileCoordinates]. Impacted files: * `camera.dart`: reused @ReinisSprogis' code - first I implemented in `MapControllerImpl.dragUpdated` but that was less generic * `tile.dart`: additional `positionCoordinates` field * `tile_coordinates.dart`: new factory `key` which returns the same value for the same tile in all world replications * `tile_image.dart`: typo fix * `tile_image_manager.dart`: additional `_positionCoordinates` field * `tile_image_view.dart`: additional `positionCoordinates` field * `tile_layer.dart`: now using a `TileRenderer` instead of a `TileImage` * `tile_range.dart`: now using a zoom modulo when checking if a coordinate fits into a range --- lib/src/layer/tile_layer/tile.dart | 12 ++- .../layer/tile_layer/tile_coordinates.dart | 23 ++++++ lib/src/layer/tile_layer/tile_image.dart | 2 +- .../layer/tile_layer/tile_image_manager.dart | 78 ++++++++++++++----- lib/src/layer/tile_layer/tile_image_view.dart | 71 ++++++++++------- lib/src/layer/tile_layer/tile_layer.dart | 9 ++- lib/src/layer/tile_layer/tile_range.dart | 18 ++++- lib/src/layer/tile_layer/tile_renderer.dart | 27 +++++++ lib/src/map/camera/camera.dart | 20 ++++- 9 files changed, 203 insertions(+), 57 deletions(-) create mode 100644 lib/src/layer/tile_layer/tile_renderer.dart diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index b438380a5..d1f5c5889 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -21,6 +21,13 @@ class Tile extends StatefulWidget { /// visible pixel when the map is rotated. final Point currentPixelOrigin; + /// Position Coordinates. + /// + /// Most of the time, they are the same as in [tileImage]. + /// Except for multi-world maps. + /// TODO replace tileImage + positionCoordinates with tileRenderer? + final TileCoordinates positionCoordinates; + /// Creates a new instance of [Tile]. const Tile({ super.key, @@ -28,6 +35,7 @@ class Tile extends StatefulWidget { required this.currentPixelOrigin, required this.tileImage, required this.tileBuilder, + required this.positionCoordinates, }); @override @@ -54,9 +62,9 @@ class _TileState extends State { @override Widget build(BuildContext context) { return Positioned( - left: widget.tileImage.coordinates.x * widget.scaledTileSize - + left: widget.positionCoordinates.x * widget.scaledTileSize - widget.currentPixelOrigin.x, - top: widget.tileImage.coordinates.y * widget.scaledTileSize - + top: widget.positionCoordinates.y * widget.scaledTileSize - widget.currentPixelOrigin.y, width: widget.scaledTileSize, height: widget.scaledTileSize, diff --git a/lib/src/layer/tile_layer/tile_coordinates.dart b/lib/src/layer/tile_layer/tile_coordinates.dart index fc45701c5..2e5a1baa0 100644 --- a/lib/src/layer/tile_layer/tile_coordinates.dart +++ b/lib/src/layer/tile_layer/tile_coordinates.dart @@ -20,6 +20,29 @@ class TileCoordinates extends Point { /// Create a new [TileCoordinates] instance. const TileCoordinates(super.x, super.y, this.z); + /// Returns a unique value for the same tile on all world replications. + factory TileCoordinates.key(TileCoordinates coordinates) { + if (coordinates.z < 0) { + return coordinates; + } + final modulo = 1 << coordinates.z; + int x = coordinates.x; + while (x < 0) { + x += modulo; + } + while (x >= modulo) { + x -= modulo; + } + int y = coordinates.y; + while (y < 0) { + y += modulo; + } + while (y >= modulo) { + y -= modulo; + } + return TileCoordinates(x, y, coordinates.z); + } + @override String toString() => 'TileCoordinate($x, $y, $z)'; diff --git a/lib/src/layer/tile_layer/tile_image.dart b/lib/src/layer/tile_layer/tile_image.dart index 0fa452b77..882384931 100644 --- a/lib/src/layer/tile_layer/tile_image.dart +++ b/lib/src/layer/tile_layer/tile_image.dart @@ -21,7 +21,7 @@ class TileImage extends ChangeNotifier { /// indicate the position of the tile at that zoom level. final TileCoordinates coordinates; - /// Callback fired when loading finishes with or withut an error. This + /// Callback fired when loading finishes with or without an error. This /// callback is not triggered after this TileImage is disposed. final void Function(TileCoordinates coordinates) onLoadComplete; diff --git a/lib/src/layer/tile_layer/tile_image_manager.dart b/lib/src/layer/tile_layer/tile_image_manager.dart index 986e51e7f..c0b727717 100644 --- a/lib/src/layer/tile_layer/tile_image_manager.dart +++ b/lib/src/layer/tile_layer/tile_image_manager.dart @@ -6,6 +6,7 @@ import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_image_view.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_renderer.dart'; import 'package:meta/meta.dart'; /// Callback definition to crete a [TileImage] for [TileCoordinates]. @@ -14,12 +15,14 @@ typedef TileCreator = TileImage Function(TileCoordinates coordinates); /// The [TileImageManager] orchestrates the loading and pruning of tiles. @immutable class TileImageManager { + final Set _positionCoordinates = HashSet(); + final Map _tiles = HashMap(); - /// Check if the [TileImageManager] has the tile for a given tile cooridantes. + /// Check if the [TileImageManager] has the tile for a given tile coordinates. bool containsTileAt(TileCoordinates coordinates) => - _tiles.containsKey(coordinates); + _positionCoordinates.contains(coordinates); /// Check if all tile images are loaded bool get allLoaded => @@ -29,16 +32,26 @@ class TileImageManager { /// 1. Tiles in the visible range at the target zoom level. /// 2. Tiles at non-target zoom level that would cover up holes that would /// be left by tiles in #1, which are not ready yet. - Iterable getTilesToRender({ + Iterable getTilesToRender({ required DiscreteTileRange visibleRange, - }) => - TileImageView( - tileImages: _tiles, - visibleRange: visibleRange, - // `keepRange` is irrelevant here since we're not using the output for - // pruning storage but rather to decide on what to put on screen. - keepRange: visibleRange, - ).renderTiles; + }) { + final Iterable positionCoordinates = TileImageView( + tileImages: _tiles, + positionCoordinates: _positionCoordinates, + visibleRange: visibleRange, + // `keepRange` is irrelevant here since we're not using the output for + // pruning storage but rather to decide on what to put on screen. + keepRange: visibleRange, + ).renderTiles; + final List tileRenderers = []; + for (final position in positionCoordinates) { + final TileImage? tileImage = _tiles[TileCoordinates.key(position)]; + if (tileImage != null) { + tileRenderers.add(TileRenderer(tileImage, position)); + } + } + return tileRenderers; + } /// Check if all loaded tiles are within the [minZoom] and [maxZoom] level. bool allWithinZoom(double minZoom, double maxZoom) => _tiles.values @@ -55,7 +68,13 @@ class TileImageManager { final notLoaded = []; for (final coordinates in tileBoundsAtZoom.validCoordinatesIn(tileRange)) { - final tile = _tiles[coordinates] ??= createTile(coordinates); + final cleanCoordinates = TileCoordinates.key(coordinates); + TileImage? tile = _tiles[cleanCoordinates]; + if (tile == null) { + tile = createTile(cleanCoordinates); + _tiles[cleanCoordinates] = tile; + } + _positionCoordinates.add(coordinates); if (tile.loadStarted == null) { notLoaded.add(tile); } @@ -77,7 +96,24 @@ class TileImageManager { TileCoordinates key, { required bool Function(TileImage tileImage) evictImageFromCache, }) { - final removed = _tiles.remove(key); + _positionCoordinates.remove(key); + final cleanKey = TileCoordinates.key(key); + + /// True if there are other positionCoordinates with the same tileImage. + bool findCleanKey() { + for (final positionCoordinates in _positionCoordinates) { + if (TileCoordinates.key(positionCoordinates) == cleanKey) { + return true; + } + } + return false; + } + + if (findCleanKey()) { + return; + } + + final removed = _tiles.remove(cleanKey); if (removed != null) { removed.dispose(evictImageFromCache: evictImageFromCache(removed)); @@ -97,7 +133,7 @@ class TileImageManager { /// Remove all tiles with a given [EvictErrorTileStrategy]. void removeAll(EvictErrorTileStrategy evictStrategy) { - final keysToRemove = List.from(_tiles.keys); + final keysToRemove = List.from(_positionCoordinates); for (final key in keysToRemove) { _removeWithEvictionStrategy(key, evictStrategy); @@ -140,6 +176,7 @@ class TileImageManager { }) { final pruningState = TileImageView( tileImages: _tiles, + positionCoordinates: _positionCoordinates, visibleRange: visibleRange, keepRange: visibleRange.expand(pruneBuffer), ); @@ -154,13 +191,13 @@ class TileImageManager { ) { switch (evictStrategy) { case EvictErrorTileStrategy.notVisibleRespectMargin: - for (final tileImage + for (final coordinates in tileRemovalState.errorTilesOutsideOfKeepMargin()) { - _remove(tileImage.coordinates, evictImageFromCache: (_) => true); + _remove(coordinates, evictImageFromCache: (_) => true); } case EvictErrorTileStrategy.notVisible: - for (final tileImage in tileRemovalState.errorTilesNotVisible()) { - _remove(tileImage.coordinates, evictImageFromCache: (_) => true); + for (final coordinates in tileRemovalState.errorTilesNotVisible()) { + _remove(coordinates, evictImageFromCache: (_) => true); } case EvictErrorTileStrategy.dispose: case EvictErrorTileStrategy.none: @@ -177,6 +214,7 @@ class TileImageManager { _prune( TileImageView( tileImages: _tiles, + positionCoordinates: _positionCoordinates, visibleRange: visibleRange, keepRange: visibleRange.expand(pruneBuffer), ), @@ -189,8 +227,8 @@ class TileImageManager { TileImageView tileRemovalState, EvictErrorTileStrategy evictStrategy, ) { - for (final tileImage in tileRemovalState.staleTiles) { - _removeWithEvictionStrategy(tileImage.coordinates, evictStrategy); + for (final coordinates in tileRemovalState.staleTiles) { + _removeWithEvictionStrategy(coordinates, evictStrategy); } } } diff --git a/lib/src/layer/tile_layer/tile_image_view.dart b/lib/src/layer/tile_layer/tile_image_view.dart index eb80f7540..943bee0cd 100644 --- a/lib/src/layer/tile_layer/tile_image_view.dart +++ b/lib/src/layer/tile_layer/tile_image_view.dart @@ -7,41 +7,56 @@ import 'package:flutter_map/src/layer/tile_layer/tile_range.dart'; /// [TileCoordinates]. final class TileImageView { final Map _tileImages; + final Set _positionCoordinates; final DiscreteTileRange _visibleRange; final DiscreteTileRange _keepRange; /// Create a new [TileImageView] instance. const TileImageView({ required Map tileImages, + required Set positionCoordinates, required DiscreteTileRange visibleRange, required DiscreteTileRange keepRange, }) : _tileImages = tileImages, + _positionCoordinates = positionCoordinates, _visibleRange = visibleRange, _keepRange = keepRange; /// Get a list with all tiles that have an error and are outside of the /// margin that should get kept. - List errorTilesOutsideOfKeepMargin() => _tileImages.values - .where((tileImage) => - tileImage.loadError && !_keepRange.contains(tileImage.coordinates)) - .toList(); + List errorTilesOutsideOfKeepMargin() => + _errorTilesWithinRange(_keepRange); /// Get a list with all tiles that are not visible on the current map /// viewport. - List errorTilesNotVisible() => _tileImages.values - .where((tileImage) => - tileImage.loadError && !_visibleRange.contains(tileImage.coordinates)) - .toList(); + List errorTilesNotVisible() => + _errorTilesWithinRange(_visibleRange); + + /// Get a list with all tiles that are not visible on the current map + /// viewport. + List _errorTilesWithinRange(DiscreteTileRange range) { + final List result = []; + for (final positionCoordinates in _positionCoordinates) { + if (range.contains(positionCoordinates)) { + continue; + } + final TileImage? tileImage = + _tileImages[TileCoordinates.key(positionCoordinates)]; + if (tileImage?.loadError ?? false) { + result.add(positionCoordinates); + } + } + return result; + } /// Get a list of [TileImage] that are stale and can get for pruned. - Iterable get staleTiles { - final stale = HashSet(); - final retain = HashSet(); + Iterable get staleTiles { + final stale = HashSet(); + final retain = HashSet(); - for (final tile in _tileImages.values) { - final c = tile.coordinates; + for (final c in _positionCoordinates) { if (!_keepRange.contains(c)) { - stale.add(tile); + stale.add(c); continue; } @@ -54,19 +69,19 @@ final class TileImageView { return stale.where((tile) => !retain.contains(tile)); } - /// Get a list of [TileImage] that need to get rendered on screen. - Iterable get renderTiles { - final retain = HashSet(); + /// Get a list of [TileCoordinates] that need to get rendered on screen. + Iterable get renderTiles { + final retain = HashSet(); - for (final tile in _tileImages.values) { - final c = tile.coordinates; + for (final c in _positionCoordinates) { if (!_visibleRange.contains(c)) { continue; } - retain.add(tile); + retain.add(c); - if (!tile.readyToDisplay) { + final TileImage? tile = _tileImages[TileCoordinates.key(c)]; + if (tile == null || !tile.readyToDisplay) { final retainedAncestor = _retainAncestor(retain, c.x, c.y, c.z, c.z - 5); if (!retainedAncestor) { @@ -81,7 +96,7 @@ final class TileImageView { /// them to [retain] if they are ready to display or loaded. Returns true if /// any of the ancestor tiles were ready to display. bool _retainAncestor( - Set retain, + Set retain, int x, int y, int z, @@ -92,13 +107,13 @@ final class TileImageView { final z2 = z - 1; final coords2 = TileCoordinates(x2, y2, z2); - final tile = _tileImages[coords2]; + final tile = _tileImages[TileCoordinates.key(coords2)]; if (tile != null) { if (tile.readyToDisplay) { - retain.add(tile); + retain.add(coords2); return true; } else if (tile.loadFinishedAt != null) { - retain.add(tile); + retain.add(coords2); } } @@ -112,7 +127,7 @@ final class TileImageView { /// Recurse through the descendants of the Tile at the given coordinates /// adding them to [retain] if they are ready to display or loaded. void _retainChildren( - Set retain, + Set retain, int x, int y, int z, @@ -121,10 +136,10 @@ final class TileImageView { for (final (i, j) in const [(0, 0), (0, 1), (1, 0), (1, 1)]) { final coords = TileCoordinates(2 * x + i, 2 * y + j, z + 1); - final tile = _tileImages[coords]; + final tile = _tileImages[TileCoordinates.key(coords)]; if (tile != null) { if (tile.readyToDisplay || tile.loadFinishedAt != null) { - retain.add(tile); + retain.add(coords); // If have the child, we do not recurse. We don't need the child's children. continue; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 09c0e75f5..b7067613b 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -504,16 +504,17 @@ class _TileLayerState extends State with TickerProviderStateMixin { // cycles saved later on in the render pipeline. final tiles = _tileImageManager .getTilesToRender(visibleRange: visibleTileRange) - .map((tileImage) => Tile( + .map((tileRenderer) => Tile( // Must be an ObjectKey, not a ValueKey using the coordinates, in // case we remove and replace the TileImage with a different one. - key: ObjectKey(tileImage), + key: ObjectKey(tileRenderer), scaledTileSize: _tileScaleCalculator.scaledTileSize( map.zoom, - tileImage.coordinates.z, + tileRenderer.positionCoordinates.z, ), currentPixelOrigin: map.pixelOrigin, - tileImage: tileImage, + tileImage: tileRenderer.tileImage, + positionCoordinates: tileRenderer.positionCoordinates, tileBuilder: widget.tileBuilder, )) .toList(); diff --git a/lib/src/layer/tile_layer/tile_range.dart b/lib/src/layer/tile_layer/tile_range.dart index e39200be1..8970e54e1 100644 --- a/lib/src/layer/tile_layer/tile_range.dart +++ b/lib/src/layer/tile_layer/tile_range.dart @@ -111,8 +111,24 @@ class DiscreteTileRange extends TileRange { } /// Check if a [Point] is inside of the bounds of the [DiscreteTileRange]. + /// + /// We use a modulo in order to prevent side-effects at the end of the world. bool contains(Point point) { - return _bounds.contains(point); + final int modulo = 1 << zoom; + + bool containsCoordinate(int value, int min, int max) { + int tmp = value; + while (tmp < min) { + tmp += modulo; + } + while (tmp > max) { + tmp -= modulo; + } + return tmp >= min && tmp <= max; + } + + return containsCoordinate(point.x, min.x, max.x) && + containsCoordinate(point.y, min.y, max.y); } /// The minimum [Point] of the [DiscreteTileRange] diff --git a/lib/src/layer/tile_layer/tile_renderer.dart b/lib/src/layer/tile_layer/tile_renderer.dart new file mode 100644 index 000000000..ad916720e --- /dev/null +++ b/lib/src/layer/tile_layer/tile_renderer.dart @@ -0,0 +1,27 @@ +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_image.dart'; + +/// Display of a [TileImage] at given [TileCoordinates]. +/// +/// In most cases, the [positionCoordinates] are equal to tileImage coordinates. +/// Except when we display several worlds in the same map, or when we cross the +/// 180/-180 border. +class TileRenderer { + /// TileImage to display. + final TileImage tileImage; + + /// Position where to display [tileImage]. + final TileCoordinates positionCoordinates; + + const TileRenderer(this.tileImage, this.positionCoordinates); + + @override + bool operator ==(Object other) { + // TODO may not be good enough with the ObjectKey + return other is TileRenderer && + other.positionCoordinates == positionCoordinates; + } + + @override + int get hashCode => positionCoordinates.hashCode; +} diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index 2bd37d522..ba6aab948 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -179,13 +179,31 @@ class MapCamera { crs: crs, minZoom: minZoom, maxZoom: maxZoom, - center: center ?? this.center, + center: _adjustPositionForSeamlessScrolling(center), zoom: zoom ?? this.zoom, rotation: rotation, nonRotatedSize: nonRotatedSize, size: _cameraSize, ); + /// Jumps camera to opposite side of the world to enable seamless scrolling + /// between 180 and -180 longitude. + LatLng _adjustPositionForSeamlessScrolling(LatLng? position) { + if (position == null) { + return center; + } + double adjustedLongitude = position.longitude; + while (adjustedLongitude > 180) { + adjustedLongitude -= 360; + } + while (adjustedLongitude < -180) { + adjustedLongitude += 360; + } + return adjustedLongitude == position.longitude + ? position + : LatLng(position.latitude, adjustedLongitude); + } + /// Calculates the size of a bounding box which surrounds a box of size /// [nonRotatedSize] which is rotated by [rotation]. static Point calculateRotatedSize( From 77cd7d40e9634b8405d9fc7337c221d10e0e1782 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 18 Aug 2024 13:26:10 +0200 Subject: [PATCH 2/8] fixed side-effect (again!) on camera.dart --- lib/src/map/camera/camera.dart | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index ba6aab948..18dc81eab 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -193,11 +193,10 @@ class MapCamera { return center; } double adjustedLongitude = position.longitude; - while (adjustedLongitude > 180) { - adjustedLongitude -= 360; - } - while (adjustedLongitude < -180) { - adjustedLongitude += 360; + if (adjustedLongitude >= 180.0) { + adjustedLongitude -= 360.0; + } else if (adjustedLongitude <= -180.0) { + adjustedLongitude += 360.0; } return adjustedLongitude == position.longitude ? position From 9fd0d2018139101f6cbf563456768a8164ee9607 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 18 Aug 2024 16:08:25 +0200 Subject: [PATCH 3/8] fixed tests --- lib/src/layer/tile_layer/tile_renderer.dart | 1 + .../tile_layer/tile_image_view_test.dart | 20 ++++++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_renderer.dart b/lib/src/layer/tile_layer/tile_renderer.dart index ad916720e..9534af902 100644 --- a/lib/src/layer/tile_layer/tile_renderer.dart +++ b/lib/src/layer/tile_layer/tile_renderer.dart @@ -13,6 +13,7 @@ class TileRenderer { /// Position where to display [tileImage]. final TileCoordinates positionCoordinates; + /// Create an instance of [TileRenderer]. const TileRenderer(this.tileImage, this.positionCoordinates); @override diff --git a/test/layer/tile_layer/tile_image_view_test.dart b/test/layer/tile_layer/tile_image_view_test.dart index 9bf21922b..e06919ff1 100644 --- a/test/layer/tile_layer/tile_image_view_test.dart +++ b/test/layer/tile_layer/tile_image_view_test.dart @@ -47,6 +47,7 @@ void main() { final removalState = TileImageView( tileImages: tileImages, + positionCoordinates: Set.from(tileImages.keys), visibleRange: discreteTileRange(2, 1, 3, 3, zoom: 1), keepRange: discreteTileRange(2, 1, 3, 3, zoom: 1), ); @@ -63,6 +64,7 @@ void main() { ]); final removalState = TileImageView( tileImages: tileImages, + positionCoordinates: Set.from(tileImages.keys), visibleRange: discreteTileRange(0, 0, 0, 0, zoom: 1), keepRange: discreteTileRange(0, 0, 0, 0, zoom: 1), ); @@ -81,6 +83,7 @@ void main() { ]); final removalState = TileImageView( tileImages: tileImages, + positionCoordinates: Set.from(tileImages.keys), visibleRange: discreteTileRange(0, 0, 0, 0, zoom: 1), keepRange: discreteTileRange(0, 0, 0, 0, zoom: 1), ); @@ -99,6 +102,7 @@ void main() { final removalState = TileImageView( tileImages: tileImages, + positionCoordinates: Set.from(tileImages.keys), visibleRange: discreteTileRange(2, 1, 3, 3, zoom: 1), keepRange: discreteTileRange(2, 1, 3, 3, zoom: 1), ); @@ -111,7 +115,7 @@ void main() { // a concurrent modification exception is thrown. This ensures that the // returned collection is not an iterable over the original collection. for (final staleTile in removalState.staleTiles) { - tileImages.remove(staleTile.coordinates)!; + tileImages.remove(staleTile)!; } }); }); @@ -125,11 +129,12 @@ void main() { ]); final tileImageView = TileImageView( tileImages: tileImages, + positionCoordinates: Set.from(tileImages.keys), visibleRange: discreteTileRange(1, 2, 1, 2, zoom: 1), keepRange: discreteTileRange(1, 2, 2, 2, zoom: 1), ); expect( - tileImageView.errorTilesOutsideOfKeepMargin().map((e) => e.coordinates), + tileImageView.errorTilesOutsideOfKeepMargin(), [const TileCoordinates(1, 1, 1)], ); @@ -137,8 +142,8 @@ void main() { // looping over that iterator and removing from the original collection // a concurrent modification exception is thrown. This ensures that the // returned collection is not an iterable over the original collection. - for (final tileImage in tileImageView.errorTilesOutsideOfKeepMargin()) { - tileImages.remove(tileImage.coordinates)!; + for (final coordinates in tileImageView.errorTilesOutsideOfKeepMargin()) { + tileImages.remove(coordinates)!; } }); @@ -151,11 +156,12 @@ void main() { ]); final tileImageView = TileImageView( tileImages: tileImages, + positionCoordinates: Set.from(tileImages.keys), visibleRange: discreteTileRange(1, 2, 1, 2, zoom: 1), keepRange: discreteTileRange(1, 2, 2, 2, zoom: 1), ); expect( - tileImageView.errorTilesNotVisible().map((e) => e.coordinates), + tileImageView.errorTilesNotVisible(), [const TileCoordinates(1, 1, 1), const TileCoordinates(2, 2, 1)], ); @@ -163,8 +169,8 @@ void main() { // looping over that iterator and removing from the original collection // a concurrent modification exception is thrown. This ensures that the // returned collection is not an iterable over the original collection. - for (final tileImage in tileImageView.errorTilesOutsideOfKeepMargin()) { - tileImages.remove(tileImage.coordinates)!; + for (final coordinates in tileImageView.errorTilesOutsideOfKeepMargin()) { + tileImages.remove(coordinates)!; } }); } From 9da2351b518edea1c43c8fa7ffe14bff6eb158d1 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Mon, 19 Aug 2024 15:08:54 +0200 Subject: [PATCH 4/8] fixed tests, again --- .../tile_layer/tile_image_view_test.dart | 51 ++++++++----------- test/layer/tile_layer/tile_range_test.dart | 2 +- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/test/layer/tile_layer/tile_image_view_test.dart b/test/layer/tile_layer/tile_image_view_test.dart index e06919ff1..00e7820a7 100644 --- a/test/layer/tile_layer/tile_image_view_test.dart +++ b/test/layer/tile_layer/tile_image_view_test.dart @@ -14,18 +14,6 @@ void main() { List tileImages) => {for (final tileImage in tileImages) tileImage.coordinates: tileImage}; - Matcher containsTileImage( - Map tileImages, - TileCoordinates coordinates, - ) => - contains(tileImages[coordinates]); - - Matcher doesNotContainTileImage( - Map tileImages, - TileCoordinates coordinates, - ) => - isNot(containsTileImage(tileImages, coordinates)); - DiscreteTileRange discreteTileRange( int x1, int y1, @@ -40,20 +28,21 @@ void main() { group('staleTiles', () { test('tiles outside of the keep range are stale', () { + const zoom = 10; final tileImages = tileImagesMappingFrom([ - MockTileImage(1, 1, 1), - MockTileImage(2, 1, 1), + MockTileImage(1, 1, zoom), + MockTileImage(2, 1, zoom), ]); final removalState = TileImageView( tileImages: tileImages, positionCoordinates: Set.from(tileImages.keys), - visibleRange: discreteTileRange(2, 1, 3, 3, zoom: 1), - keepRange: discreteTileRange(2, 1, 3, 3, zoom: 1), + visibleRange: discreteTileRange(2, 1, 3, 3, zoom: zoom), + keepRange: discreteTileRange(2, 1, 3, 3, zoom: zoom), ); expect( removalState.staleTiles, - containsTileImage(tileImages, const TileCoordinates(1, 1, 1)), + contains(const TileCoordinates(1, 1, zoom)), ); }); @@ -70,7 +59,7 @@ void main() { ); expect( removalState.staleTiles, - doesNotContainTileImage(tileImages, const TileCoordinates(0, 0, 0)), + isNot(contains(const TileCoordinates(0, 0, 0))), ); }); @@ -89,26 +78,27 @@ void main() { ); expect( removalState.staleTiles, - doesNotContainTileImage(tileImages, const TileCoordinates(0, 0, 2)), + isNot(contains(const TileCoordinates(0, 0, 2))), ); }); test( 'returned elements can be removed from the source collection in a for loop', () { + const zoom = 10; final tileImages = tileImagesMappingFrom([ - MockTileImage(1, 1, 1), + MockTileImage(1, 1, zoom), ]); final removalState = TileImageView( tileImages: tileImages, positionCoordinates: Set.from(tileImages.keys), - visibleRange: discreteTileRange(2, 1, 3, 3, zoom: 1), - keepRange: discreteTileRange(2, 1, 3, 3, zoom: 1), + visibleRange: discreteTileRange(2, 1, 3, 3, zoom: zoom), + keepRange: discreteTileRange(2, 1, 3, 3, zoom: zoom), ); expect( removalState.staleTiles, - containsTileImage(tileImages, const TileCoordinates(1, 1, 1)), + contains(const TileCoordinates(1, 1, zoom)), ); // If an iterator over the original collection is returned then when // looping over that iterator and removing from the original collection @@ -148,21 +138,22 @@ void main() { }); test('errorTilesNotVisible', () { + const zoom = 10; final tileImages = tileImagesMappingFrom([ - MockTileImage(1, 1, 1, loadError: true), - MockTileImage(2, 1, 1), - MockTileImage(1, 2, 1), - MockTileImage(2, 2, 1, loadError: true), + MockTileImage(1, 1, zoom, loadError: true), + MockTileImage(2, 1, zoom), + MockTileImage(1, 2, zoom), + MockTileImage(2, 2, zoom, loadError: true), ]); final tileImageView = TileImageView( tileImages: tileImages, positionCoordinates: Set.from(tileImages.keys), - visibleRange: discreteTileRange(1, 2, 1, 2, zoom: 1), - keepRange: discreteTileRange(1, 2, 2, 2, zoom: 1), + visibleRange: discreteTileRange(1, 2, 1, 2, zoom: zoom), + keepRange: discreteTileRange(1, 2, 2, 2, zoom: zoom), ); expect( tileImageView.errorTilesNotVisible(), - [const TileCoordinates(1, 1, 1), const TileCoordinates(2, 2, 1)], + [const TileCoordinates(1, 1, zoom), const TileCoordinates(2, 2, zoom)], ); // If an iterator over the original collection is returned then when diff --git a/test/layer/tile_layer/tile_range_test.dart b/test/layer/tile_layer/tile_range_test.dart index 346ffb532..62d001557 100644 --- a/test/layer/tile_layer/tile_range_test.dart +++ b/test/layer/tile_layer/tile_range_test.dart @@ -263,7 +263,7 @@ void main() { test('contains', () { final tileRange = DiscreteTileRange.fromPixelBounds( - zoom: 0, + zoom: 10, tileSize: 10, pixelBounds: Bounds( const Point(35, 35), From f998a8d78e58e0a9658db74eb90d477d644b4388 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Tue, 20 Aug 2024 16:24:39 +0200 Subject: [PATCH 5/8] Fixed fling gesture. --- lib/src/gestures/map_interactive_viewer.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index 8f4ba975a..a41e4b732 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -883,7 +883,18 @@ class MapInteractiveViewerState extends State final newCenterPoint = _camera.project(_mapCenterStart) + _flingAnimation.value.toPoint().rotate(_camera.rotationRad); - final newCenter = _camera.unproject(newCenterPoint); + final math.Point bestCenterPoint; + final double worldSize = _camera.crs.scale(_camera.zoom); + if (newCenterPoint.x > worldSize) { + bestCenterPoint = + math.Point(newCenterPoint.x - worldSize, newCenterPoint.y); + } else if (newCenterPoint.x < 0) { + bestCenterPoint = + math.Point(newCenterPoint.x + worldSize, newCenterPoint.y); + } else { + bestCenterPoint = newCenterPoint; + } + final newCenter = _camera.unproject(bestCenterPoint); widget.controller.moveRaw( newCenter, From 6f93793e79ead28d12217d49d5a24e20d4aaeef0 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Sun, 25 Aug 2024 17:38:02 +0200 Subject: [PATCH 6/8] More verbose comment, minor fix and additional checks. --- lib/src/layer/tile_layer/tile.dart | 12 ++++++++++-- lib/src/layer/tile_layer/tile_renderer.dart | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index d1f5c5889..509419fc1 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -24,8 +24,16 @@ class Tile extends StatefulWidget { /// Position Coordinates. /// /// Most of the time, they are the same as in [tileImage]. - /// Except for multi-world maps. - /// TODO replace tileImage + positionCoordinates with tileRenderer? + /// Except for multi-world or scrolled maps, for instance, scrolling from + /// Europe to Alaska on zoom level 3 (i.e. tile coordinates between 0 and 7): + /// * Alaska is first considered as from the next world (tile X: 8) + /// * Scrolling again, Alaska is considered as part of the current world, as + /// the center of the map is now in America (tile X: 0) + /// In both cases, we reuse the same [tileImage] (tile X: 0) for different + /// [positionCoordinates] (tile X: 0 and 8). This prevents a "flash" effect + /// when scrolling beyond the end of the world: we skip the part where we + /// create a new tileImage (for tile X: 0) as we've already downloaded it + /// (for tile X: 8). final TileCoordinates positionCoordinates; /// Creates a new instance of [Tile]. diff --git a/lib/src/layer/tile_layer/tile_renderer.dart b/lib/src/layer/tile_layer/tile_renderer.dart index 9534af902..2425b32aa 100644 --- a/lib/src/layer/tile_layer/tile_renderer.dart +++ b/lib/src/layer/tile_layer/tile_renderer.dart @@ -18,7 +18,9 @@ class TileRenderer { @override bool operator ==(Object other) { - // TODO may not be good enough with the ObjectKey + if (identical(this, other)) { + return true; + } return other is TileRenderer && other.positionCoordinates == positionCoordinates; } From bbc36c4cc8d5fbc820cea1771a743aeced2943d1 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Thu, 29 Aug 2024 08:34:34 +0200 Subject: [PATCH 7/8] Update lib/src/layer/tile_layer/tile_image_manager.dart Co-authored-by: mootw --- lib/src/layer/tile_layer/tile_image_manager.dart | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_image_manager.dart b/lib/src/layer/tile_layer/tile_image_manager.dart index c0b727717..9fabcd544 100644 --- a/lib/src/layer/tile_layer/tile_image_manager.dart +++ b/lib/src/layer/tile_layer/tile_image_manager.dart @@ -99,18 +99,11 @@ class TileImageManager { _positionCoordinates.remove(key); final cleanKey = TileCoordinates.key(key); - /// True if there are other positionCoordinates with the same tileImage. - bool findCleanKey() { - for (final positionCoordinates in _positionCoordinates) { - if (TileCoordinates.key(positionCoordinates) == cleanKey) { - return true; - } + // guard if positionCoordinates with the same tileImage. + for (final positionCoordinates in _positionCoordinates) { + if (TileCoordinates.key(positionCoordinates) == cleanKey) { + return; } - return false; - } - - if (findCleanKey()) { - return; } final removed = _tiles.remove(cleanKey); From 968024374db705685c8203d3c713c0cfa29f36f4 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Tue, 3 Sep 2024 08:49:13 +0200 Subject: [PATCH 8/8] Renamed variable for better maintenance. --- lib/src/layer/tile_layer/tile_image_view.dart | 48 ++++++++++++++----- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_image_view.dart b/lib/src/layer/tile_layer/tile_image_view.dart index 943bee0cd..fb39c591b 100644 --- a/lib/src/layer/tile_layer/tile_image_view.dart +++ b/lib/src/layer/tile_layer/tile_image_view.dart @@ -54,15 +54,27 @@ final class TileImageView { final stale = HashSet(); final retain = HashSet(); - for (final c in _positionCoordinates) { - if (!_keepRange.contains(c)) { - stale.add(c); + for (final positionCoordinates in _positionCoordinates) { + if (!_keepRange.contains(positionCoordinates)) { + stale.add(positionCoordinates); continue; } - final retainedAncestor = _retainAncestor(retain, c.x, c.y, c.z, c.z - 5); + final retainedAncestor = _retainAncestor( + retain, + positionCoordinates.x, + positionCoordinates.y, + positionCoordinates.z, + positionCoordinates.z - 5, + ); if (!retainedAncestor) { - _retainChildren(retain, c.x, c.y, c.z, c.z + 2); + _retainChildren( + retain, + positionCoordinates.x, + positionCoordinates.y, + positionCoordinates.z, + positionCoordinates.z + 2, + ); } } @@ -73,19 +85,31 @@ final class TileImageView { Iterable get renderTiles { final retain = HashSet(); - for (final c in _positionCoordinates) { - if (!_visibleRange.contains(c)) { + for (final positionCoordinates in _positionCoordinates) { + if (!_visibleRange.contains(positionCoordinates)) { continue; } - retain.add(c); + retain.add(positionCoordinates); - final TileImage? tile = _tileImages[TileCoordinates.key(c)]; + final TileImage? tile = + _tileImages[TileCoordinates.key(positionCoordinates)]; if (tile == null || !tile.readyToDisplay) { - final retainedAncestor = - _retainAncestor(retain, c.x, c.y, c.z, c.z - 5); + final retainedAncestor = _retainAncestor( + retain, + positionCoordinates.x, + positionCoordinates.y, + positionCoordinates.z, + positionCoordinates.z - 5, + ); if (!retainedAncestor) { - _retainChildren(retain, c.x, c.y, c.z, c.z + 2); + _retainChildren( + retain, + positionCoordinates.x, + positionCoordinates.y, + positionCoordinates.z, + positionCoordinates.z + 2, + ); } } }