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

feat: unbounded horizontal scroll #1948

Merged
merged 8 commits into from
Sep 10, 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
13 changes: 12 additions & 1 deletion lib/src/gestures/map_interactive_viewer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,18 @@ class MapInteractiveViewerState extends State<MapInteractiveViewer>

final newCenterPoint = _camera.project(_mapCenterStart) +
_flingAnimation.value.toPoint().rotate(_camera.rotationRad);
final newCenter = _camera.unproject(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;
}
final newCenter = _camera.unproject(bestCenterPoint);

widget.controller.moveRaw(
newCenter,
Expand Down
20 changes: 18 additions & 2 deletions lib/src/layer/tile_layer/tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,29 @@ class Tile extends StatefulWidget {
/// visible pixel when the map is rotated.
final Point<double> currentPixelOrigin;

/// Position Coordinates.
///
/// Most of the time, they are the same as in [tileImage].
/// 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].
const Tile({
super.key,
required this.scaledTileSize,
required this.currentPixelOrigin,
required this.tileImage,
required this.tileBuilder,
required this.positionCoordinates,
});

@override
Expand All @@ -54,9 +70,9 @@ class _TileState extends State<Tile> {
@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,
Expand Down
23 changes: 23 additions & 0 deletions lib/src/layer/tile_layer/tile_coordinates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,29 @@ 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
2 changes: 1 addition & 1 deletion lib/src/layer/tile_layer/tile_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
71 changes: 51 additions & 20 deletions lib/src/layer/tile_layer/tile_image_manager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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].
Expand All @@ -14,12 +15,14 @@ typedef TileCreator = TileImage Function(TileCoordinates coordinates);
/// The [TileImageManager] orchestrates the loading and pruning of tiles.
@immutable
class TileImageManager {
final Set<TileCoordinates> _positionCoordinates = HashSet<TileCoordinates>();

final Map<TileCoordinates, TileImage> _tiles =
HashMap<TileCoordinates, TileImage>();

/// 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 =>
Expand All @@ -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<TileImage> getTilesToRender({
Iterable<TileRenderer> 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<TileCoordinates> 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<TileRenderer> tileRenderers = <TileRenderer>[];
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
Expand All @@ -55,7 +68,13 @@ class TileImageManager {
final notLoaded = <TileImage>[];

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);
}
Expand All @@ -77,7 +96,17 @@ class TileImageManager {
TileCoordinates key, {
required bool Function(TileImage tileImage) evictImageFromCache,
}) {
final removed = _tiles.remove(key);
_positionCoordinates.remove(key);
final cleanKey = TileCoordinates.key(key);

// guard if positionCoordinates with the same tileImage.
for (final positionCoordinates in _positionCoordinates) {
if (TileCoordinates.key(positionCoordinates) == cleanKey) {
return;
}
}

final removed = _tiles.remove(cleanKey);

if (removed != null) {
removed.dispose(evictImageFromCache: evictImageFromCache(removed));
Expand All @@ -97,7 +126,7 @@ class TileImageManager {

/// Remove all tiles with a given [EvictErrorTileStrategy].
void removeAll(EvictErrorTileStrategy evictStrategy) {
final keysToRemove = List<TileCoordinates>.from(_tiles.keys);
final keysToRemove = List<TileCoordinates>.from(_positionCoordinates);

for (final key in keysToRemove) {
_removeWithEvictionStrategy(key, evictStrategy);
Expand Down Expand Up @@ -140,6 +169,7 @@ class TileImageManager {
}) {
final pruningState = TileImageView(
tileImages: _tiles,
positionCoordinates: _positionCoordinates,
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
);
Expand All @@ -154,13 +184,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:
Expand All @@ -177,6 +207,7 @@ class TileImageManager {
_prune(
TileImageView(
tileImages: _tiles,
positionCoordinates: _positionCoordinates,
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
),
Expand All @@ -189,8 +220,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);
}
}
}
Loading
Loading