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 5 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
12 changes: 10 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,21 @@ 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 maps.
/// TODO replace tileImage + positionCoordinates with tileRenderer?
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
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 +62,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
78 changes: 58 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,24 @@ class TileImageManager {
TileCoordinates key, {
required bool Function(TileImage tileImage) evictImageFromCache,
}) {
final removed = _tiles.remove(key);
_positionCoordinates.remove(key);
final cleanKey = TileCoordinates.key(key);

/// True if there are other positionCoordinates with the same tileImage.
bool findCleanKey() {
for (final positionCoordinates in _positionCoordinates) {
if (TileCoordinates.key(positionCoordinates) == cleanKey) {
return true;
}
}
return false;
}

if (findCleanKey()) {
return;
}
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved

final removed = _tiles.remove(cleanKey);

if (removed != null) {
removed.dispose(evictImageFromCache: evictImageFromCache(removed));
Expand All @@ -97,7 +133,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 +176,7 @@ class TileImageManager {
}) {
final pruningState = TileImageView(
tileImages: _tiles,
positionCoordinates: _positionCoordinates,
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
);
Expand All @@ -154,13 +191,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 +214,7 @@ class TileImageManager {
_prune(
TileImageView(
tileImages: _tiles,
positionCoordinates: _positionCoordinates,
visibleRange: visibleRange,
keepRange: visibleRange.expand(pruneBuffer),
),
Expand All @@ -189,8 +227,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);
}
}
}
71 changes: 43 additions & 28 deletions lib/src/layer/tile_layer/tile_image_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,56 @@ import 'package:flutter_map/src/layer/tile_layer/tile_range.dart';
/// [TileCoordinates].
final class TileImageView {
final Map<TileCoordinates, TileImage> _tileImages;
final Set<TileCoordinates> _positionCoordinates;
final DiscreteTileRange _visibleRange;
final DiscreteTileRange _keepRange;

/// Create a new [TileImageView] instance.
const TileImageView({
required Map<TileCoordinates, TileImage> tileImages,
required Set<TileCoordinates> positionCoordinates,
required DiscreteTileRange visibleRange,
required DiscreteTileRange keepRange,
}) : _tileImages = tileImages,
_positionCoordinates = positionCoordinates,
_visibleRange = visibleRange,
_keepRange = keepRange;

/// Get a list with all tiles that have an error and are outside of the
/// margin that should get kept.
List<TileImage> errorTilesOutsideOfKeepMargin() => _tileImages.values
.where((tileImage) =>
tileImage.loadError && !_keepRange.contains(tileImage.coordinates))
.toList();
List<TileCoordinates> errorTilesOutsideOfKeepMargin() =>
_errorTilesWithinRange(_keepRange);

/// Get a list with all tiles that are not visible on the current map
/// viewport.
List<TileImage> errorTilesNotVisible() => _tileImages.values
.where((tileImage) =>
tileImage.loadError && !_visibleRange.contains(tileImage.coordinates))
.toList();
List<TileCoordinates> errorTilesNotVisible() =>
_errorTilesWithinRange(_visibleRange);

/// Get a list with all tiles that are not visible on the current map
/// viewport.
List<TileCoordinates> _errorTilesWithinRange(DiscreteTileRange range) {
final List<TileCoordinates> result = <TileCoordinates>[];
for (final positionCoordinates in _positionCoordinates) {
if (range.contains(positionCoordinates)) {
continue;
}
final TileImage? tileImage =
_tileImages[TileCoordinates.key(positionCoordinates)];
if (tileImage?.loadError ?? false) {
result.add(positionCoordinates);
}
}
return result;
}

/// Get a list of [TileImage] that are stale and can get for pruned.
Iterable<TileImage> get staleTiles {
final stale = HashSet<TileImage>();
final retain = HashSet<TileImage>();
Iterable<TileCoordinates> get staleTiles {
final stale = HashSet<TileCoordinates>();
final retain = HashSet<TileCoordinates>();

for (final tile in _tileImages.values) {
final c = tile.coordinates;
for (final c in _positionCoordinates) {
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
if (!_keepRange.contains(c)) {
stale.add(tile);
stale.add(c);
continue;
}

Expand All @@ -54,19 +69,19 @@ final class TileImageView {
return stale.where((tile) => !retain.contains(tile));
}

/// Get a list of [TileImage] that need to get rendered on screen.
Iterable<TileImage> get renderTiles {
final retain = HashSet<TileImage>();
/// Get a list of [TileCoordinates] that need to get rendered on screen.
Iterable<TileCoordinates> get renderTiles {
final retain = HashSet<TileCoordinates>();

for (final tile in _tileImages.values) {
final c = tile.coordinates;
for (final c in _positionCoordinates) {
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
if (!_visibleRange.contains(c)) {
continue;
}

retain.add(tile);
retain.add(c);

if (!tile.readyToDisplay) {
final TileImage? tile = _tileImages[TileCoordinates.key(c)];
if (tile == null || !tile.readyToDisplay) {
final retainedAncestor =
_retainAncestor(retain, c.x, c.y, c.z, c.z - 5);
if (!retainedAncestor) {
Expand All @@ -81,7 +96,7 @@ final class TileImageView {
/// them to [retain] if they are ready to display or loaded. Returns true if
/// any of the ancestor tiles were ready to display.
bool _retainAncestor(
Set<TileImage> retain,
Set<TileCoordinates> retain,
int x,
int y,
int z,
Expand All @@ -92,13 +107,13 @@ final class TileImageView {
final z2 = z - 1;
final coords2 = TileCoordinates(x2, y2, z2);

final tile = _tileImages[coords2];
final tile = _tileImages[TileCoordinates.key(coords2)];
if (tile != null) {
if (tile.readyToDisplay) {
retain.add(tile);
retain.add(coords2);
return true;
} else if (tile.loadFinishedAt != null) {
retain.add(tile);
retain.add(coords2);
}
}

Expand All @@ -112,7 +127,7 @@ final class TileImageView {
/// Recurse through the descendants of the Tile at the given coordinates
/// adding them to [retain] if they are ready to display or loaded.
void _retainChildren(
Set<TileImage> retain,
Set<TileCoordinates> retain,
int x,
int y,
int z,
Expand All @@ -121,10 +136,10 @@ 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[coords];
final tile = _tileImages[TileCoordinates.key(coords)];
if (tile != null) {
if (tile.readyToDisplay || tile.loadFinishedAt != null) {
retain.add(tile);
retain.add(coords);

// If have the child, we do not recurse. We don't need the child's children.
continue;
Expand Down
Loading
Loading