Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: make horizontal repetition CRS dependent #1978

Merged
merged 7 commits into from
Nov 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ abstract class Crs {

/// Rescales the bounds to a given zoom value.
Bounds<double>? 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.
Expand Down Expand Up @@ -175,6 +178,9 @@ class Epsg3857 extends CrsWithStaticTransformation {
);
return Point<double>(x, y);
}

@override
bool get replicatesWorldLongitude => true;
}

/// EPSG:4326, A common CRS among GIS enthusiasts.
Expand Down
25 changes: 15 additions & 10 deletions lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -883,18 +883,23 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

final newCenterPoint = _camera.project(_mapCenterStart) +
_flingAnimation.value.toPoint().rotate(_camera.rotationRad);
final math.Point<double> 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<double> 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,
Expand Down
68 changes: 45 additions & 23 deletions lib/src/layer/tile_layer/tile_coordinates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,6 @@ class TileCoordinates extends Point<int> {
/// 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)';

Expand Down Expand Up @@ -70,3 +47,48 @@ class TileCoordinates extends Point<int> {
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);
}
}
47 changes: 31 additions & 16 deletions lib/src/layer/tile_layer/tile_image_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<TileCoordinates> _positionCoordinates = HashSet<TileCoordinates>();

Expand All @@ -28,31 +26,52 @@ 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
/// be left by tiles in #1, which are not ready yet.
Iterable<TileRenderer> getTilesToRender({
required DiscreteTileRange visibleRange,
}) {
final Iterable<TileCoordinates> positionCoordinates = TileImageView(
tileImages: _tiles,
positionCoordinates: _positionCoordinates,
final Iterable<TileCoordinates> 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.
keepRange: visibleRange,
).renderTiles;
final List<TileRenderer> tileRenderers = <TileRenderer>[];
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));
}
}
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)
Expand All @@ -68,7 +87,7 @@ class TileImageManager {
final notLoaded = <TileImage>[];

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);
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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),
);
Expand Down Expand Up @@ -205,9 +222,7 @@ class TileImageManager {
required EvictErrorTileStrategy evictStrategy,
}) {
_prune(
TileImageView(
tileImages: _tiles,
positionCoordinates: _positionCoordinates,
_getTileImageView(
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
),
Expand Down
30 changes: 21 additions & 9 deletions lib/src/layer/tile_layer/tile_image_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,21 @@ final class TileImageView {
final Set<TileCoordinates> _positionCoordinates;
final DiscreteTileRange _visibleRange;
final DiscreteTileRange _keepRange;
final TileCoordinatesResolver _resolver;

/// Create a new [TileImageView] instance.
const TileImageView({
required Map<TileCoordinates, TileImage> tileImages,
required Set<TileCoordinates> 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.
Expand All @@ -37,11 +41,14 @@ final class TileImageView {
List<TileCoordinates> _errorTilesWithinRange(DiscreteTileRange range) {
final List<TileCoordinates> result = <TileCoordinates>[];
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);
}
Expand All @@ -55,7 +62,10 @@ final class TileImageView {
final retain = HashSet<TileCoordinates>();

for (final positionCoordinates in _positionCoordinates) {
if (!_keepRange.contains(positionCoordinates)) {
if (!_keepRange.contains(
positionCoordinates,
replicatesWorldLongitude: _resolver.replicatesWorldLongitude,
)) {
stale.add(positionCoordinates);
continue;
}
Expand Down Expand Up @@ -86,14 +96,16 @@ final class TileImageView {
final retain = HashSet<TileCoordinates>();

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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
4 changes: 4 additions & 0 deletions lib/src/layer/tile_layer/tile_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,10 @@ class _TileLayerState extends State<TileLayer> with TickerProviderStateMixin {
final camera = MapCamera.of(context);
final mapController = MapController.of(context);

_tileImageManager.setReplicatesWorldLongitude(
camera.crs.replicatesWorldLongitude,
);

if (_mapControllerHashCode != mapController.hashCode) {
_tileUpdateSubscription?.cancel();

Expand Down
9 changes: 8 additions & 1 deletion lib/src/layer/tile_layer/tile_range.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> point) {
bool contains(
Point<int> point, {
bool replicatesWorldLongitude = false,
}) {
if (!replicatesWorldLongitude) {
return _bounds.contains(point);
}

final int modulo = 1 << zoom;

bool containsCoordinate(int value, int min, int max) {
Expand Down
3 changes: 3 additions & 0 deletions lib/src/map/camera/camera.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
Loading