Skip to content

Commit

Permalink
feat: unbounded horizontal scroll (#1948)
Browse files Browse the repository at this point in the history
  • Loading branch information
monsieurtanuki authored Sep 10, 2024
1 parent a2aada4 commit 182c4fc
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 105 deletions.
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

0 comments on commit 182c4fc

Please sign in to comment.