diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 63b5e1936..446ab371e 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -67,6 +67,9 @@ abstract class Crs { /// Rescales the bounds to a given zoom value. Bounds? getProjectedBounds(double zoom); + + /// Returns true if we want the world to be replicated, longitude-wise. + bool get replicatesWorldLongitude => false; } /// Internal base class for CRS with a single zoom-level independent transformation. @@ -175,6 +178,9 @@ class Epsg3857 extends CrsWithStaticTransformation { ); return Point(x, y); } + + @override + bool get replicatesWorldLongitude => true; } /// EPSG:4326, A common CRS among GIS enthusiasts. diff --git a/lib/src/gestures/map_interactive_viewer.dart b/lib/src/gestures/map_interactive_viewer.dart index a41e4b732..ebe756de5 100644 --- a/lib/src/gestures/map_interactive_viewer.dart +++ b/lib/src/gestures/map_interactive_viewer.dart @@ -883,18 +883,23 @@ class MapInteractiveViewerState extends State final newCenterPoint = _camera.project(_mapCenterStart) + _flingAnimation.value.toPoint().rotate(_camera.rotationRad); - 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); + final LatLng newCenter; + if (!_camera.crs.replicatesWorldLongitude) { + newCenter = _camera.unproject(newCenterPoint); } else { - bestCenterPoint = 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; + } + newCenter = _camera.unproject(bestCenterPoint); } - final newCenter = _camera.unproject(bestCenterPoint); widget.controller.moveRaw( newCenter, diff --git a/lib/src/layer/tile_layer/tile_coordinates.dart b/lib/src/layer/tile_layer/tile_coordinates.dart index 2e5a1baa0..ec1e4806d 100644 --- a/lib/src/layer/tile_layer/tile_coordinates.dart +++ b/lib/src/layer/tile_layer/tile_coordinates.dart @@ -20,29 +20,6 @@ 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)'; @@ -70,3 +47,48 @@ class TileCoordinates extends Point { return x ^ y << 24 ^ z << 48; } } + +/// Resolves coordinates in the context of world replications. +/// +/// On maps with world replications, different tile coordinates may actually +/// refer to the same "resolved" tile coordinate - the coordinate that starts +/// from 0. +/// For instance, on zoom level 0, all tile coordinates can be simplified to +/// (0,0), which is the only tile. +/// On zoom level 1, (0, 1) and (2, 1) can be simplified to (0, 1), as they both +/// mean the bottom left tile. +/// And when we're not in the context of world replications, we don't have to +/// simplify the tile coordinates: we just return the same value. +class TileCoordinatesResolver { + /// Resolves coordinates in the context of world replications. + const TileCoordinatesResolver(this.replicatesWorldLongitude); + + /// True if we simplify the coordinates according to the world replications. + final bool replicatesWorldLongitude; + + /// Returns the simplification of the coordinates. + TileCoordinates get(TileCoordinates positionCoordinates) { + if (!replicatesWorldLongitude) { + return positionCoordinates; + } + if (positionCoordinates.z < 0) { + return positionCoordinates; + } + final modulo = 1 << positionCoordinates.z; + int x = positionCoordinates.x; + while (x < 0) { + x += modulo; + } + while (x >= modulo) { + x -= modulo; + } + int y = positionCoordinates.y; + while (y < 0) { + y += modulo; + } + while (y >= modulo) { + y -= modulo; + } + return TileCoordinates(x, y, positionCoordinates.z); + } +} diff --git a/lib/src/layer/tile_layer/tile_image_manager.dart b/lib/src/layer/tile_layer/tile_image_manager.dart index 9fabcd544..2b69d200c 100644 --- a/lib/src/layer/tile_layer/tile_image_manager.dart +++ b/lib/src/layer/tile_layer/tile_image_manager.dart @@ -7,13 +7,11 @@ import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom 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]. +/// Callback definition to create a [TileImage] for [TileCoordinates]. typedef TileCreator = TileImage Function(TileCoordinates coordinates); /// The [TileImageManager] orchestrates the loading and pruning of tiles. -@immutable class TileImageManager { final Set _positionCoordinates = HashSet(); @@ -28,6 +26,17 @@ class TileImageManager { bool get allLoaded => _tiles.values.none((tile) => tile.loadFinishedAt == null); + /// Coordinates resolver. + TileCoordinatesResolver _resolver = const TileCoordinatesResolver(false); + + /// Sets if we replicate the world longitude in several worlds. + void setReplicatesWorldLongitude(bool replicatesWorldLongitude) { + if (_resolver.replicatesWorldLongitude == replicatesWorldLongitude) { + return; + } + _resolver = TileCoordinatesResolver(replicatesWorldLongitude); + } + /// Filter tiles to only tiles that would be visible on screen. Specifically: /// 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 @@ -35,9 +44,7 @@ class TileImageManager { Iterable getTilesToRender({ required DiscreteTileRange visibleRange, }) { - final Iterable positionCoordinates = TileImageView( - tileImages: _tiles, - positionCoordinates: _positionCoordinates, + final Iterable positionCoordinates = _getTileImageView( 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. @@ -45,7 +52,7 @@ class TileImageManager { ).renderTiles; final List tileRenderers = []; for (final position in positionCoordinates) { - final TileImage? tileImage = _tiles[TileCoordinates.key(position)]; + final TileImage? tileImage = _tiles[_resolver.get(position)]; if (tileImage != null) { tileRenderers.add(TileRenderer(tileImage, position)); } @@ -53,6 +60,18 @@ class TileImageManager { return tileRenderers; } + TileImageView _getTileImageView({ + required DiscreteTileRange visibleRange, + required DiscreteTileRange keepRange, + }) => + TileImageView( + tileImages: _tiles, + positionCoordinates: _positionCoordinates, + visibleRange: visibleRange, + keepRange: keepRange, + resolver: _resolver, + ); + /// Check if all loaded tiles are within the [minZoom] and [maxZoom] level. bool allWithinZoom(double minZoom, double maxZoom) => _tiles.values .map((e) => e.coordinates) @@ -68,7 +87,7 @@ class TileImageManager { final notLoaded = []; for (final coordinates in tileBoundsAtZoom.validCoordinatesIn(tileRange)) { - final cleanCoordinates = TileCoordinates.key(coordinates); + final cleanCoordinates = _resolver.get(coordinates); TileImage? tile = _tiles[cleanCoordinates]; if (tile == null) { tile = createTile(cleanCoordinates); @@ -97,11 +116,11 @@ class TileImageManager { required bool Function(TileImage tileImage) evictImageFromCache, }) { _positionCoordinates.remove(key); - final cleanKey = TileCoordinates.key(key); + final cleanKey = _resolver.get(key); // guard if positionCoordinates with the same tileImage. for (final positionCoordinates in _positionCoordinates) { - if (TileCoordinates.key(positionCoordinates) == cleanKey) { + if (_resolver.get(positionCoordinates) == cleanKey) { return; } } @@ -167,9 +186,7 @@ class TileImageManager { required int pruneBuffer, required EvictErrorTileStrategy evictStrategy, }) { - final pruningState = TileImageView( - tileImages: _tiles, - positionCoordinates: _positionCoordinates, + final pruningState = _getTileImageView( visibleRange: visibleRange, keepRange: visibleRange.expand(pruneBuffer), ); @@ -205,9 +222,7 @@ class TileImageManager { required EvictErrorTileStrategy evictStrategy, }) { _prune( - TileImageView( - tileImages: _tiles, - positionCoordinates: _positionCoordinates, + _getTileImageView( visibleRange: visibleRange, keepRange: visibleRange.expand(pruneBuffer), ), diff --git a/lib/src/layer/tile_layer/tile_image_view.dart b/lib/src/layer/tile_layer/tile_image_view.dart index fb39c591b..1537e6a29 100644 --- a/lib/src/layer/tile_layer/tile_image_view.dart +++ b/lib/src/layer/tile_layer/tile_image_view.dart @@ -10,6 +10,7 @@ final class TileImageView { final Set _positionCoordinates; final DiscreteTileRange _visibleRange; final DiscreteTileRange _keepRange; + final TileCoordinatesResolver _resolver; /// Create a new [TileImageView] instance. const TileImageView({ @@ -17,10 +18,13 @@ final class TileImageView { required Set positionCoordinates, required DiscreteTileRange visibleRange, required DiscreteTileRange keepRange, + final TileCoordinatesResolver resolver = + const TileCoordinatesResolver(false), }) : _tileImages = tileImages, _positionCoordinates = positionCoordinates, _visibleRange = visibleRange, - _keepRange = keepRange; + _keepRange = keepRange, + _resolver = resolver; /// Get a list with all tiles that have an error and are outside of the /// margin that should get kept. @@ -37,11 +41,14 @@ final class TileImageView { List _errorTilesWithinRange(DiscreteTileRange range) { final List result = []; for (final positionCoordinates in _positionCoordinates) { - if (range.contains(positionCoordinates)) { + if (range.contains( + positionCoordinates, + replicatesWorldLongitude: _resolver.replicatesWorldLongitude, + )) { continue; } final TileImage? tileImage = - _tileImages[TileCoordinates.key(positionCoordinates)]; + _tileImages[_resolver.get(positionCoordinates)]; if (tileImage?.loadError ?? false) { result.add(positionCoordinates); } @@ -55,7 +62,10 @@ final class TileImageView { final retain = HashSet(); for (final positionCoordinates in _positionCoordinates) { - if (!_keepRange.contains(positionCoordinates)) { + if (!_keepRange.contains( + positionCoordinates, + replicatesWorldLongitude: _resolver.replicatesWorldLongitude, + )) { stale.add(positionCoordinates); continue; } @@ -86,14 +96,16 @@ final class TileImageView { final retain = HashSet(); for (final positionCoordinates in _positionCoordinates) { - if (!_visibleRange.contains(positionCoordinates)) { + if (!_visibleRange.contains( + positionCoordinates, + replicatesWorldLongitude: _resolver.replicatesWorldLongitude, + )) { continue; } retain.add(positionCoordinates); - final TileImage? tile = - _tileImages[TileCoordinates.key(positionCoordinates)]; + final TileImage? tile = _tileImages[_resolver.get(positionCoordinates)]; if (tile == null || !tile.readyToDisplay) { final retainedAncestor = _retainAncestor( retain, @@ -131,7 +143,7 @@ final class TileImageView { final z2 = z - 1; final coords2 = TileCoordinates(x2, y2, z2); - final tile = _tileImages[TileCoordinates.key(coords2)]; + final tile = _tileImages[_resolver.get(coords2)]; if (tile != null) { if (tile.readyToDisplay) { retain.add(coords2); @@ -160,7 +172,7 @@ 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[TileCoordinates.key(coords)]; + final tile = _tileImages[_resolver.get(coords)]; if (tile != null) { if (tile.readyToDisplay || tile.loadFinishedAt != null) { retain.add(coords); diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index ab383a85e..b6c9f538c 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -362,6 +362,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { final camera = MapCamera.of(context); final mapController = MapController.of(context); + _tileImageManager.setReplicatesWorldLongitude( + camera.crs.replicatesWorldLongitude, + ); + if (_mapControllerHashCode != mapController.hashCode) { _tileUpdateSubscription?.cancel(); diff --git a/lib/src/layer/tile_layer/tile_range.dart b/lib/src/layer/tile_layer/tile_range.dart index 8970e54e1..0f4c889bf 100644 --- a/lib/src/layer/tile_layer/tile_range.dart +++ b/lib/src/layer/tile_layer/tile_range.dart @@ -113,7 +113,14 @@ 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) { + bool contains( + Point point, { + bool replicatesWorldLongitude = false, + }) { + if (!replicatesWorldLongitude) { + return _bounds.contains(point); + } + final int modulo = 1 << zoom; bool containsCoordinate(int value, int min, int max) { diff --git a/lib/src/map/camera/camera.dart b/lib/src/map/camera/camera.dart index 18dc81eab..81853ea09 100644 --- a/lib/src/map/camera/camera.dart +++ b/lib/src/map/camera/camera.dart @@ -189,6 +189,9 @@ class MapCamera { /// Jumps camera to opposite side of the world to enable seamless scrolling /// between 180 and -180 longitude. LatLng _adjustPositionForSeamlessScrolling(LatLng? position) { + if (!crs.replicatesWorldLongitude) { + return position ?? center; + } if (position == null) { return center; }