Skip to content

Commit d816b4d

Browse files
fix: make horizontal repetition CRS dependent (#1978)
1 parent 59d7f69 commit d816b4d

File tree

8 files changed

+133
-59
lines changed

8 files changed

+133
-59
lines changed

lib/src/geo/crs.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ abstract class Crs {
6767

6868
/// Rescales the bounds to a given zoom value.
6969
Bounds<double>? getProjectedBounds(double zoom);
70+
71+
/// Returns true if we want the world to be replicated, longitude-wise.
72+
bool get replicatesWorldLongitude => false;
7073
}
7174

7275
/// Internal base class for CRS with a single zoom-level independent transformation.
@@ -175,6 +178,9 @@ class Epsg3857 extends CrsWithStaticTransformation {
175178
);
176179
return Point<double>(x, y);
177180
}
181+
182+
@override
183+
bool get replicatesWorldLongitude => true;
178184
}
179185

180186
/// EPSG:4326, A common CRS among GIS enthusiasts.

lib/src/gestures/map_interactive_viewer.dart

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -883,18 +883,23 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>
883883

884884
final newCenterPoint = _camera.project(_mapCenterStart) +
885885
_flingAnimation.value.toPoint().rotate(_camera.rotationRad);
886-
final math.Point<double> bestCenterPoint;
887-
final double worldSize = _camera.crs.scale(_camera.zoom);
888-
if (newCenterPoint.x > worldSize) {
889-
bestCenterPoint =
890-
math.Point(newCenterPoint.x - worldSize, newCenterPoint.y);
891-
} else if (newCenterPoint.x < 0) {
892-
bestCenterPoint =
893-
math.Point(newCenterPoint.x + worldSize, newCenterPoint.y);
886+
final LatLng newCenter;
887+
if (!_camera.crs.replicatesWorldLongitude) {
888+
newCenter = _camera.unproject(newCenterPoint);
894889
} else {
895-
bestCenterPoint = newCenterPoint;
890+
final math.Point<double> bestCenterPoint;
891+
final double worldSize = _camera.crs.scale(_camera.zoom);
892+
if (newCenterPoint.x > worldSize) {
893+
bestCenterPoint =
894+
math.Point(newCenterPoint.x - worldSize, newCenterPoint.y);
895+
} else if (newCenterPoint.x < 0) {
896+
bestCenterPoint =
897+
math.Point(newCenterPoint.x + worldSize, newCenterPoint.y);
898+
} else {
899+
bestCenterPoint = newCenterPoint;
900+
}
901+
newCenter = _camera.unproject(bestCenterPoint);
896902
}
897-
final newCenter = _camera.unproject(bestCenterPoint);
898903

899904
widget.controller.moveRaw(
900905
newCenter,

lib/src/layer/tile_layer/tile_coordinates.dart

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,6 @@ class TileCoordinates extends Point<int> {
2020
/// Create a new [TileCoordinates] instance.
2121
const TileCoordinates(super.x, super.y, this.z);
2222

23-
/// Returns a unique value for the same tile on all world replications.
24-
factory TileCoordinates.key(TileCoordinates coordinates) {
25-
if (coordinates.z < 0) {
26-
return coordinates;
27-
}
28-
final modulo = 1 << coordinates.z;
29-
int x = coordinates.x;
30-
while (x < 0) {
31-
x += modulo;
32-
}
33-
while (x >= modulo) {
34-
x -= modulo;
35-
}
36-
int y = coordinates.y;
37-
while (y < 0) {
38-
y += modulo;
39-
}
40-
while (y >= modulo) {
41-
y -= modulo;
42-
}
43-
return TileCoordinates(x, y, coordinates.z);
44-
}
45-
4623
@override
4724
String toString() => 'TileCoordinate($x, $y, $z)';
4825

@@ -70,3 +47,48 @@ class TileCoordinates extends Point<int> {
7047
return x ^ y << 24 ^ z << 48;
7148
}
7249
}
50+
51+
/// Resolves coordinates in the context of world replications.
52+
///
53+
/// On maps with world replications, different tile coordinates may actually
54+
/// refer to the same "resolved" tile coordinate - the coordinate that starts
55+
/// from 0.
56+
/// For instance, on zoom level 0, all tile coordinates can be simplified to
57+
/// (0,0), which is the only tile.
58+
/// On zoom level 1, (0, 1) and (2, 1) can be simplified to (0, 1), as they both
59+
/// mean the bottom left tile.
60+
/// And when we're not in the context of world replications, we don't have to
61+
/// simplify the tile coordinates: we just return the same value.
62+
class TileCoordinatesResolver {
63+
/// Resolves coordinates in the context of world replications.
64+
const TileCoordinatesResolver(this.replicatesWorldLongitude);
65+
66+
/// True if we simplify the coordinates according to the world replications.
67+
final bool replicatesWorldLongitude;
68+
69+
/// Returns the simplification of the coordinates.
70+
TileCoordinates get(TileCoordinates positionCoordinates) {
71+
if (!replicatesWorldLongitude) {
72+
return positionCoordinates;
73+
}
74+
if (positionCoordinates.z < 0) {
75+
return positionCoordinates;
76+
}
77+
final modulo = 1 << positionCoordinates.z;
78+
int x = positionCoordinates.x;
79+
while (x < 0) {
80+
x += modulo;
81+
}
82+
while (x >= modulo) {
83+
x -= modulo;
84+
}
85+
int y = positionCoordinates.y;
86+
while (y < 0) {
87+
y += modulo;
88+
}
89+
while (y >= modulo) {
90+
y -= modulo;
91+
}
92+
return TileCoordinates(x, y, positionCoordinates.z);
93+
}
94+
}

lib/src/layer/tile_layer/tile_image_manager.dart

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ import 'package:flutter_map/src/layer/tile_layer/tile_bounds/tile_bounds_at_zoom
77
import 'package:flutter_map/src/layer/tile_layer/tile_image_view.dart';
88
import 'package:flutter_map/src/layer/tile_layer/tile_range.dart';
99
import 'package:flutter_map/src/layer/tile_layer/tile_renderer.dart';
10-
import 'package:meta/meta.dart';
1110

12-
/// Callback definition to crete a [TileImage] for [TileCoordinates].
11+
/// Callback definition to create a [TileImage] for [TileCoordinates].
1312
typedef TileCreator = TileImage Function(TileCoordinates coordinates);
1413

1514
/// The [TileImageManager] orchestrates the loading and pruning of tiles.
16-
@immutable
1715
class TileImageManager {
1816
final Set<TileCoordinates> _positionCoordinates = HashSet<TileCoordinates>();
1917

@@ -28,31 +26,52 @@ class TileImageManager {
2826
bool get allLoaded =>
2927
_tiles.values.none((tile) => tile.loadFinishedAt == null);
3028

29+
/// Coordinates resolver.
30+
TileCoordinatesResolver _resolver = const TileCoordinatesResolver(false);
31+
32+
/// Sets if we replicate the world longitude in several worlds.
33+
void setReplicatesWorldLongitude(bool replicatesWorldLongitude) {
34+
if (_resolver.replicatesWorldLongitude == replicatesWorldLongitude) {
35+
return;
36+
}
37+
_resolver = TileCoordinatesResolver(replicatesWorldLongitude);
38+
}
39+
3140
/// Filter tiles to only tiles that would be visible on screen. Specifically:
3241
/// 1. Tiles in the visible range at the target zoom level.
3342
/// 2. Tiles at non-target zoom level that would cover up holes that would
3443
/// be left by tiles in #1, which are not ready yet.
3544
Iterable<TileRenderer> getTilesToRender({
3645
required DiscreteTileRange visibleRange,
3746
}) {
38-
final Iterable<TileCoordinates> positionCoordinates = TileImageView(
39-
tileImages: _tiles,
40-
positionCoordinates: _positionCoordinates,
47+
final Iterable<TileCoordinates> positionCoordinates = _getTileImageView(
4148
visibleRange: visibleRange,
4249
// `keepRange` is irrelevant here since we're not using the output for
4350
// pruning storage but rather to decide on what to put on screen.
4451
keepRange: visibleRange,
4552
).renderTiles;
4653
final List<TileRenderer> tileRenderers = <TileRenderer>[];
4754
for (final position in positionCoordinates) {
48-
final TileImage? tileImage = _tiles[TileCoordinates.key(position)];
55+
final TileImage? tileImage = _tiles[_resolver.get(position)];
4956
if (tileImage != null) {
5057
tileRenderers.add(TileRenderer(tileImage, position));
5158
}
5259
}
5360
return tileRenderers;
5461
}
5562

63+
TileImageView _getTileImageView({
64+
required DiscreteTileRange visibleRange,
65+
required DiscreteTileRange keepRange,
66+
}) =>
67+
TileImageView(
68+
tileImages: _tiles,
69+
positionCoordinates: _positionCoordinates,
70+
visibleRange: visibleRange,
71+
keepRange: keepRange,
72+
resolver: _resolver,
73+
);
74+
5675
/// Check if all loaded tiles are within the [minZoom] and [maxZoom] level.
5776
bool allWithinZoom(double minZoom, double maxZoom) => _tiles.values
5877
.map((e) => e.coordinates)
@@ -68,7 +87,7 @@ class TileImageManager {
6887
final notLoaded = <TileImage>[];
6988

7089
for (final coordinates in tileBoundsAtZoom.validCoordinatesIn(tileRange)) {
71-
final cleanCoordinates = TileCoordinates.key(coordinates);
90+
final cleanCoordinates = _resolver.get(coordinates);
7291
TileImage? tile = _tiles[cleanCoordinates];
7392
if (tile == null) {
7493
tile = createTile(cleanCoordinates);
@@ -97,11 +116,11 @@ class TileImageManager {
97116
required bool Function(TileImage tileImage) evictImageFromCache,
98117
}) {
99118
_positionCoordinates.remove(key);
100-
final cleanKey = TileCoordinates.key(key);
119+
final cleanKey = _resolver.get(key);
101120

102121
// guard if positionCoordinates with the same tileImage.
103122
for (final positionCoordinates in _positionCoordinates) {
104-
if (TileCoordinates.key(positionCoordinates) == cleanKey) {
123+
if (_resolver.get(positionCoordinates) == cleanKey) {
105124
return;
106125
}
107126
}
@@ -167,9 +186,7 @@ class TileImageManager {
167186
required int pruneBuffer,
168187
required EvictErrorTileStrategy evictStrategy,
169188
}) {
170-
final pruningState = TileImageView(
171-
tileImages: _tiles,
172-
positionCoordinates: _positionCoordinates,
189+
final pruningState = _getTileImageView(
173190
visibleRange: visibleRange,
174191
keepRange: visibleRange.expand(pruneBuffer),
175192
);
@@ -205,9 +222,7 @@ class TileImageManager {
205222
required EvictErrorTileStrategy evictStrategy,
206223
}) {
207224
_prune(
208-
TileImageView(
209-
tileImages: _tiles,
210-
positionCoordinates: _positionCoordinates,
225+
_getTileImageView(
211226
visibleRange: visibleRange,
212227
keepRange: visibleRange.expand(pruneBuffer),
213228
),

lib/src/layer/tile_layer/tile_image_view.dart

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,21 @@ final class TileImageView {
1010
final Set<TileCoordinates> _positionCoordinates;
1111
final DiscreteTileRange _visibleRange;
1212
final DiscreteTileRange _keepRange;
13+
final TileCoordinatesResolver _resolver;
1314

1415
/// Create a new [TileImageView] instance.
1516
const TileImageView({
1617
required Map<TileCoordinates, TileImage> tileImages,
1718
required Set<TileCoordinates> positionCoordinates,
1819
required DiscreteTileRange visibleRange,
1920
required DiscreteTileRange keepRange,
21+
final TileCoordinatesResolver resolver =
22+
const TileCoordinatesResolver(false),
2023
}) : _tileImages = tileImages,
2124
_positionCoordinates = positionCoordinates,
2225
_visibleRange = visibleRange,
23-
_keepRange = keepRange;
26+
_keepRange = keepRange,
27+
_resolver = resolver;
2428

2529
/// Get a list with all tiles that have an error and are outside of the
2630
/// margin that should get kept.
@@ -37,11 +41,14 @@ final class TileImageView {
3741
List<TileCoordinates> _errorTilesWithinRange(DiscreteTileRange range) {
3842
final List<TileCoordinates> result = <TileCoordinates>[];
3943
for (final positionCoordinates in _positionCoordinates) {
40-
if (range.contains(positionCoordinates)) {
44+
if (range.contains(
45+
positionCoordinates,
46+
replicatesWorldLongitude: _resolver.replicatesWorldLongitude,
47+
)) {
4148
continue;
4249
}
4350
final TileImage? tileImage =
44-
_tileImages[TileCoordinates.key(positionCoordinates)];
51+
_tileImages[_resolver.get(positionCoordinates)];
4552
if (tileImage?.loadError ?? false) {
4653
result.add(positionCoordinates);
4754
}
@@ -55,7 +62,10 @@ final class TileImageView {
5562
final retain = HashSet<TileCoordinates>();
5663

5764
for (final positionCoordinates in _positionCoordinates) {
58-
if (!_keepRange.contains(positionCoordinates)) {
65+
if (!_keepRange.contains(
66+
positionCoordinates,
67+
replicatesWorldLongitude: _resolver.replicatesWorldLongitude,
68+
)) {
5969
stale.add(positionCoordinates);
6070
continue;
6171
}
@@ -86,14 +96,16 @@ final class TileImageView {
8696
final retain = HashSet<TileCoordinates>();
8797

8898
for (final positionCoordinates in _positionCoordinates) {
89-
if (!_visibleRange.contains(positionCoordinates)) {
99+
if (!_visibleRange.contains(
100+
positionCoordinates,
101+
replicatesWorldLongitude: _resolver.replicatesWorldLongitude,
102+
)) {
90103
continue;
91104
}
92105

93106
retain.add(positionCoordinates);
94107

95-
final TileImage? tile =
96-
_tileImages[TileCoordinates.key(positionCoordinates)];
108+
final TileImage? tile = _tileImages[_resolver.get(positionCoordinates)];
97109
if (tile == null || !tile.readyToDisplay) {
98110
final retainedAncestor = _retainAncestor(
99111
retain,
@@ -131,7 +143,7 @@ final class TileImageView {
131143
final z2 = z - 1;
132144
final coords2 = TileCoordinates(x2, y2, z2);
133145

134-
final tile = _tileImages[TileCoordinates.key(coords2)];
146+
final tile = _tileImages[_resolver.get(coords2)];
135147
if (tile != null) {
136148
if (tile.readyToDisplay) {
137149
retain.add(coords2);
@@ -160,7 +172,7 @@ final class TileImageView {
160172
for (final (i, j) in const [(0, 0), (0, 1), (1, 0), (1, 1)]) {
161173
final coords = TileCoordinates(2 * x + i, 2 * y + j, z + 1);
162174

163-
final tile = _tileImages[TileCoordinates.key(coords)];
175+
final tile = _tileImages[_resolver.get(coords)];
164176
if (tile != null) {
165177
if (tile.readyToDisplay || tile.loadFinishedAt != null) {
166178
retain.add(coords);

lib/src/layer/tile_layer/tile_layer.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,10 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
362362
final camera = MapCamera.of(context);
363363
final mapController = MapController.of(context);
364364

365+
_tileImageManager.setReplicatesWorldLongitude(
366+
camera.crs.replicatesWorldLongitude,
367+
);
368+
365369
if (_mapControllerHashCode != mapController.hashCode) {
366370
_tileUpdateSubscription?.cancel();
367371

lib/src/layer/tile_layer/tile_range.dart

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ class DiscreteTileRange extends TileRange {
113113
/// Check if a [Point] is inside of the bounds of the [DiscreteTileRange].
114114
///
115115
/// We use a modulo in order to prevent side-effects at the end of the world.
116-
bool contains(Point<int> point) {
116+
bool contains(
117+
Point<int> point, {
118+
bool replicatesWorldLongitude = false,
119+
}) {
120+
if (!replicatesWorldLongitude) {
121+
return _bounds.contains(point);
122+
}
123+
117124
final int modulo = 1 << zoom;
118125

119126
bool containsCoordinate(int value, int min, int max) {

lib/src/map/camera/camera.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ class MapCamera {
189189
/// Jumps camera to opposite side of the world to enable seamless scrolling
190190
/// between 180 and -180 longitude.
191191
LatLng _adjustPositionForSeamlessScrolling(LatLng? position) {
192+
if (!crs.replicatesWorldLongitude) {
193+
return position ?? center;
194+
}
192195
if (position == null) {
193196
return center;
194197
}

0 commit comments

Comments
 (0)