Skip to content

Commit

Permalink
perf!: reduce work to build and fix bugs in TileLayer, and improve …
Browse files Browse the repository at this point in the history
…culling of off-screen tiles (#1718)
  • Loading branch information
ignatz authored Nov 9, 2023
1 parent 0dc3fa9 commit 270b331
Show file tree
Hide file tree
Showing 11 changed files with 191 additions and 167 deletions.
8 changes: 4 additions & 4 deletions lib/src/geo/crs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ abstract class Crs {
double zoom(double scale) => math.log(scale / 256) / math.ln2;

/// Rescales the bounds to a given zoom value.
Bounds? getProjectedBounds(double zoom) {
Bounds<double>? getProjectedBounds(double zoom) {
if (infinite) return null;

final b = projection.bounds!;
final s = scale(zoom);
final min = transformation.transform(b.min, s);
final max = transformation.transform(b.max, s);
return Bounds(min, max);
return Bounds<double>(min, max);
}

bool get infinite;
Expand Down Expand Up @@ -239,7 +239,7 @@ class Proj4Crs extends Crs {

/// Rescales the bounds to a given zoom value.
@override
Bounds? getProjectedBounds(double zoom) {
Bounds<double>? getProjectedBounds(double zoom) {
if (infinite) return null;

final b = projection.bounds!;
Expand All @@ -249,7 +249,7 @@ class Proj4Crs extends Crs {

final min = transformation.transform(b.min, s);
final max = transformation.transform(b.max, s);
return Bounds(min, max);
return Bounds<double>(min, max);
}

/// Zoom to Scale function.
Expand Down
17 changes: 8 additions & 9 deletions lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import 'package:flutter_map/src/geo/latlng_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_range.dart';
import 'package:flutter_map/src/misc/bounds.dart';
import 'package:flutter_map/src/misc/point_extensions.dart';
import 'package:latlong2/latlong.dart';
import 'package:meta/meta.dart';

Expand Down Expand Up @@ -76,13 +75,13 @@ class DiscreteTileBounds extends TileBounds {
TileBoundsAtZoom _tileBoundsAtZoomImpl(int zoom) {
final zoomDouble = zoom.toDouble();

final Bounds<num> pixelBounds;
final Bounds<double> pixelBounds;
if (_latLngBounds == null) {
pixelBounds = crs.getProjectedBounds(zoomDouble)!;
} else {
pixelBounds = Bounds(
crs.latLngToPoint(_latLngBounds!.southWest, zoomDouble).floor(),
crs.latLngToPoint(_latLngBounds!.northEast, zoomDouble).ceil(),
pixelBounds = Bounds<double>(
crs.latLngToPoint(_latLngBounds!.southWest, zoomDouble),
crs.latLngToPoint(_latLngBounds!.northEast, zoomDouble),
);
}

Expand Down Expand Up @@ -115,13 +114,13 @@ class WrappedTileBounds extends TileBounds {
WrappedTileBoundsAtZoom _tileBoundsAtZoomImpl(int zoom) {
final zoomDouble = zoom.toDouble();

final Bounds<num> pixelBounds;
final Bounds<double> pixelBounds;
if (_latLngBounds == null) {
pixelBounds = crs.getProjectedBounds(zoomDouble)!;
} else {
pixelBounds = Bounds(
crs.latLngToPoint(_latLngBounds!.southWest, zoomDouble).floor(),
crs.latLngToPoint(_latLngBounds!.northEast, zoomDouble).ceil(),
pixelBounds = Bounds<double>(
crs.latLngToPoint(_latLngBounds!.southWest, zoomDouble),
crs.latLngToPoint(_latLngBounds!.northEast, zoomDouble),
);
}

Expand Down
2 changes: 1 addition & 1 deletion lib/src/layer/tile_layer/tile_coordinates.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ class TileCoordinates extends Point<int> {
@override
int get hashCode {
// NOTE: the odd numbers are due to JavaScript's integer precision of 53 bits.
return x | y << 24 | z << 48;
return x ^ y << 24 ^ z << 48;
}
}
6 changes: 0 additions & 6 deletions lib/src/layer/tile_layer/tile_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ class TileImage extends ChangeNotifier {

AnimationController? get animation => _animationController;

TileCoordinates get coordinatesKey => coordinates;

/// Whether the tile is displayable. This means that either:
/// * Loading errored but an error image is configured.
/// * Loading succeeded and the fade animation has finished.
Expand All @@ -94,10 +92,6 @@ class TileImage extends ChangeNotifier {
/// tile display is used with a maximum opacity less than 1.
bool get readyToDisplay => _readyToDisplay;

// Used to sort TileImages by their distance from the current zoom.
double zIndex(double maxZoom, int currentZoom) =>
maxZoom - (currentZoom - coordinates.z).abs();

/// Change the tile display options.
set tileDisplay(TileDisplay newTileDisplay) {
final oldTileDisplay = _tileDisplay;
Expand Down
76 changes: 32 additions & 44 deletions lib/src/layer/tile_layer/tile_image_manager.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:collection';

import 'package:collection/collection.dart';
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';
Expand All @@ -13,62 +15,48 @@ typedef TileCreator = TileImage Function(TileCoordinates coordinates);

@immutable
class TileImageManager {
final Map<TileCoordinates, TileImage> _tiles = {};
final Map<TileCoordinates, TileImage> _tiles =
HashMap<TileCoordinates, TileImage>();

bool containsTileAt(TileCoordinates coordinates) =>
_tiles.containsKey(coordinates);

bool get allLoaded =>
_tiles.values.none((tile) => tile.loadFinishedAt == null);

/// Returns in the order in which they should be rendered:
/// 1. Tiles at the current zoom.
/// 2. Tiles at the current zoom +/- 1.
/// 3. Tiles at the current zoom +/- 2.
/// 4. ...etc
List<TileImage> inRenderOrder(double maxZoom, int currentZoom) {
final result = _tiles.values.toList()
..sort((a, b) => a
.zIndex(maxZoom, currentZoom)
.compareTo(b.zIndex(maxZoom, currentZoom)));

return result;
}

// Creates missing tiles in the given range. Does not initiate loading of the
// tiles.
void createMissingTiles(
DiscreteTileRange tileRange,
TileBoundsAtZoom tileBoundsAtZoom, {
required TileCreator createTileImage,
}) {
for (final coordinates in tileBoundsAtZoom.validCoordinatesIn(tileRange)) {
_tiles.putIfAbsent(
coordinates,
() => createTileImage(coordinates),
);
}
}
/// 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<TileImage> 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;

bool allWithinZoom(double minZoom, double maxZoom) => _tiles.values
.map((e) => e.coordinates)
.every((coord) => coord.z > maxZoom || coord.z < minZoom);

/// Creates and returns [TileImage]s which do not already exist with the given
/// [tileCoordinates].
List<TileImage> createMissingTilesIn(
Iterable<TileCoordinates> tileCoordinates, {
/// Creates missing [TileImage]s within the provided tile range. Returns a
/// list of [TileImage]s which haven't started loading yet.
List<TileImage> createMissingTiles(
DiscreteTileRange tileRange,
TileBoundsAtZoom tileBoundsAtZoom, {
required TileCreator createTile,
}) {
final notLoaded = <TileImage>[];

for (final coordinates in tileCoordinates) {
final tile = _tiles.putIfAbsent(
coordinates,
() => createTile(coordinates),
);

if (tile.loadStarted == null) notLoaded.add(tile);
for (final coordinates in tileBoundsAtZoom.validCoordinatesIn(tileRange)) {
final tile = _tiles[coordinates] ??= createTile(coordinates);
if (tile.loadStarted == null) {
notLoaded.add(tile);
}
}

return notLoaded;
Expand Down Expand Up @@ -162,11 +150,11 @@ class TileImageManager {
case EvictErrorTileStrategy.notVisibleRespectMargin:
for (final tileImage
in tileRemovalState.errorTilesOutsideOfKeepMargin()) {
_remove(tileImage.coordinatesKey, evictImageFromCache: (_) => true);
_remove(tileImage.coordinates, evictImageFromCache: (_) => true);
}
case EvictErrorTileStrategy.notVisible:
for (final tileImage in tileRemovalState.errorTilesNotVisible()) {
_remove(tileImage.coordinatesKey, evictImageFromCache: (_) => true);
_remove(tileImage.coordinates, evictImageFromCache: (_) => true);
}
case EvictErrorTileStrategy.dispose:
case EvictErrorTileStrategy.none:
Expand All @@ -193,8 +181,8 @@ class TileImageManager {
TileImageView tileRemovalState,
EvictErrorTileStrategy evictStrategy,
) {
for (final tileImage in tileRemovalState.staleTiles()) {
_removeWithEvictionStrategy(tileImage.coordinatesKey, evictStrategy);
for (final tileImage in tileRemovalState.staleTiles) {
_removeWithEvictionStrategy(tileImage.coordinates, evictStrategy);
}
}
}
79 changes: 51 additions & 28 deletions lib/src/layer/tile_layer/tile_image_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_image.dart';
import 'package:flutter_map/src/layer/tile_layer/tile_range.dart';

class TileImageView {
final class TileImageView {
final Map<TileCoordinates, TileImage> _tileImages;
final DiscreteTileRange _visibleRange;
final DiscreteTileRange _keepRange;

TileImageView({
const TileImageView({
required Map<TileCoordinates, TileImage> tileImages,
required DiscreteTileRange visibleRange,
required DiscreteTileRange keepRange,
}) : _tileImages = UnmodifiableMapView(tileImages),
}) : _tileImages = tileImages,
_visibleRange = visibleRange,
_keepRange = keepRange;

Expand All @@ -27,24 +27,46 @@ class TileImageView {
tileImage.loadError && !_visibleRange.contains(tileImage.coordinates))
.toList();

List<TileImage> staleTiles() {
final tilesInKeepRange = _tileImages.values
.where((tileImage) => _keepRange.contains(tileImage.coordinates));
final retain = Set<TileImage>.from(tilesInKeepRange);
Iterable<TileImage> get staleTiles {
final stale = HashSet<TileImage>();
final retain = HashSet<TileImage>();

for (final tile in _tileImages.values) {
final c = tile.coordinates;
if (!_keepRange.contains(c)) {
stale.add(tile);
continue;
}

final retainedAncestor = _retainAncestor(retain, c.x, c.y, c.z, c.z - 5);
if (!retainedAncestor) {
_retainChildren(retain, c.x, c.y, c.z, c.z + 2);
}
}

return stale.where((tile) => !retain.contains(tile));
}

Iterable<TileImage> get renderTiles {
final retain = HashSet<TileImage>();

for (final tile in _tileImages.values) {
final c = tile.coordinates;
if (!_visibleRange.contains(c)) {
continue;
}

retain.add(tile);

for (final tile in tilesInKeepRange) {
if (!tile.readyToDisplay) {
final coords = tile.coordinates;
if (!_retainAncestor(
retain, coords.x, coords.y, coords.z, coords.z - 5)) {
_retainChildren(retain, coords.x, coords.y, coords.z, coords.z + 2);
final retainedAncestor =
_retainAncestor(retain, c.x, c.y, c.z, c.z - 5);
if (!retainedAncestor) {
_retainChildren(retain, c.x, c.y, c.z, c.z + 2);
}
}
}

return _tileImages.values
.where((tileImage) => !retain.contains(tileImage))
.toList();
return retain;
}

// Recurses through the ancestors of the Tile at the given coordinates adding
Expand Down Expand Up @@ -88,21 +110,22 @@ class TileImageView {
int z,
int maxZoom,
) {
for (var i = 2 * x; i < 2 * x + 2; i++) {
for (var j = 2 * y; j < 2 * y + 2; j++) {
final coords = TileCoordinates(i, j, z + 1);

final tile = _tileImages[coords];
if (tile != null) {
if (tile.readyToDisplay || tile.loadFinishedAt != null) {
retain.add(tile);
}
}
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];
if (tile != null) {
if (tile.readyToDisplay || tile.loadFinishedAt != null) {
retain.add(tile);

if (z + 1 < maxZoom) {
_retainChildren(retain, i, j, z + 1, maxZoom);
// If have the child, we do not recurse. We don't need the child's children.
continue;
}
}

if (z + 1 < maxZoom) {
_retainChildren(retain, i, j, z + 1, maxZoom);
}
}
}
}
Loading

0 comments on commit 270b331

Please sign in to comment.