diff --git a/lib/src/geo/crs.dart b/lib/src/geo/crs.dart index 5ec3d7277..b78b04f2b 100644 --- a/lib/src/geo/crs.dart +++ b/lib/src/geo/crs.dart @@ -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? 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(min, max); } bool get infinite; @@ -239,7 +239,7 @@ class Proj4Crs extends Crs { /// Rescales the bounds to a given zoom value. @override - Bounds? getProjectedBounds(double zoom) { + Bounds? getProjectedBounds(double zoom) { if (infinite) return null; final b = projection.bounds!; @@ -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(min, max); } /// Zoom to Scale function. diff --git a/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart b/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart index 99c596f67..122468ec7 100644 --- a/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart +++ b/lib/src/layer/tile_layer/tile_bounds/tile_bounds.dart @@ -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'; @@ -76,13 +75,13 @@ class DiscreteTileBounds extends TileBounds { TileBoundsAtZoom _tileBoundsAtZoomImpl(int zoom) { final zoomDouble = zoom.toDouble(); - final Bounds pixelBounds; + final Bounds 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( + crs.latLngToPoint(_latLngBounds!.southWest, zoomDouble), + crs.latLngToPoint(_latLngBounds!.northEast, zoomDouble), ); } @@ -115,13 +114,13 @@ class WrappedTileBounds extends TileBounds { WrappedTileBoundsAtZoom _tileBoundsAtZoomImpl(int zoom) { final zoomDouble = zoom.toDouble(); - final Bounds pixelBounds; + final Bounds 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( + crs.latLngToPoint(_latLngBounds!.southWest, zoomDouble), + crs.latLngToPoint(_latLngBounds!.northEast, zoomDouble), ); } diff --git a/lib/src/layer/tile_layer/tile_image_manager.dart b/lib/src/layer/tile_layer/tile_image_manager.dart index 7f5345123..1eeca51bb 100644 --- a/lib/src/layer/tile_layer/tile_image_manager.dart +++ b/lib/src/layer/tile_layer/tile_image_manager.dart @@ -24,10 +24,23 @@ class TileImageManager { bool get allLoaded => _tiles.values.none((tile) => tile.loadFinishedAt == null); - Iterable get tiles => _tiles.values; + /// Filter tiles to only tiles that would 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 renderTiles({ + 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(); - // Creates missing tiles in the given range. Does not initiate loading of the - // tiles. + /// Creates missing tiles in the given range. Does not initiate loading of the + /// tiles. void createMissingTiles( DiscreteTileRange tileRange, TileBoundsAtZoom tileBoundsAtZoom, { diff --git a/lib/src/layer/tile_layer/tile_image_view.dart b/lib/src/layer/tile_layer/tile_image_view.dart index f39efec11..2423b78ca 100644 --- a/lib/src/layer/tile_layer/tile_image_view.dart +++ b/lib/src/layer/tile_layer/tile_image_view.dart @@ -49,6 +49,26 @@ class TileImageView { return stale.where((tile) => !retain.contains(tile)); } + Iterable renderTiles() { + final retain = HashSet(); + + for (final tile in _tileImages.values) { + final c = tile.coordinates; + if (_visibleRange.contains(c)) { + retain.add(tile); + + if (!tile.readyToDisplay) { + 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 retain; + } + // Recurses through the ancestors of the Tile at the given coordinates adding // them to [retain] if they are ready to display or loaded. Returns true if // any of the ancestor tiles were ready to display. diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 0e59ae6a1..7f641d656 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:collection'; import 'dart:math'; import 'package:collection/collection.dart' show MapEquality; @@ -534,36 +533,13 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileScaleCalculator.clearCacheUnlessZoomMatches(map.zoom); - // Note: We're filtering out tiles that are off screen and all tiles at a - // different zoom level but only if all tiles at the target level are ready - // to display. This saves us render time later. - bool allTilesAtTargetZoomLevelReady = true; - for (final tile in _tileImageManager.tiles) { - if (tile.coordinates.z == tileZoom) { - allTilesAtTargetZoomLevelReady &= tile.readyToDisplay; - } - } - - final otherVisibleTileRanges = HashMap(); - final tiles = _tileImageManager.tiles - .where((tile) { - final c = tile.coordinates; - final zoom = c.z; - - if (zoom != tileZoom && allTilesAtTargetZoomLevelReady) { - return false; - } - - final visibleRange = (zoom == tileZoom) - ? visibleTileRange - : (otherVisibleTileRanges[zoom] ??= - _tileRangeCalculator.calculate( - camera: map, - tileZoom: zoom, - )); - - return visibleRange.contains(c); - }) + // Note: `renderTiles` filters out all tiles that are either off-screen or + // tiles at non-target zoom levels that are would be completely covered by + // tiles that are *ready* and at the target zoom level. + // We're happy to do a bit of diligent work here, since tiles not rendered are + // cycles saved later on in the render pipeline. + final tiles = _tileImageManager + .renderTiles(visibleRange: visibleTileRange) .map((tileImage) => Tile( // Must be an ObjectKey, not a ValueKey using the coordinates, in // case we remove and replace the TileImage with a different one. diff --git a/lib/src/layer/tile_layer/tile_range.dart b/lib/src/layer/tile_layer/tile_range.dart index ed3ebaeb4..af91f6504 100644 --- a/lib/src/layer/tile_layer/tile_range.dart +++ b/lib/src/layer/tile_layer/tile_range.dart @@ -34,7 +34,7 @@ class DiscreteTileRange extends TileRange { factory DiscreteTileRange.fromPixelBounds({ required int zoom, required double tileSize, - required Bounds pixelBounds, + required Bounds pixelBounds, }) { final Bounds bounds; if (pixelBounds.min == pixelBounds.max) { @@ -77,9 +77,9 @@ class DiscreteTileRange extends TileRange { return DiscreteTileRange( zoom, - Bounds( - Point(math.max(min.x, minX), min.y), - Point(math.min(max.x, maxX), max.y), + Bounds( + Point(math.max(min.x, minX), min.y), + Point(math.min(max.x, maxX), max.y), ), ); } @@ -92,9 +92,9 @@ class DiscreteTileRange extends TileRange { return DiscreteTileRange( zoom, - Bounds( - Point(min.x, math.max(min.y, minY)), - Point(max.x, math.min(max.y, maxY)), + Bounds( + Point(min.x, math.max(min.y, minY)), + Point(max.x, math.min(max.y, maxY)), ), ); } diff --git a/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart b/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart index e7e390af3..c15bf57e1 100644 --- a/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart +++ b/test/layer/tile_layer/tile_bounds/tile_bounds_at_zoom_test.dart @@ -9,10 +9,12 @@ import 'package:test/test.dart'; void main() { group('TileBoundsAtZoom', () { const hugeCoordinate = TileCoordinates(999999999, 999999999, 0); + final hugePoint = + Point(hugeCoordinate.x.toDouble(), hugeCoordinate.y.toDouble()); final tileRangeWithHugeCoordinate = DiscreteTileRange.fromPixelBounds( zoom: 0, tileSize: 1, - pixelBounds: Bounds(hugeCoordinate, hugeCoordinate), + pixelBounds: Bounds(hugePoint, hugePoint), ); DiscreteTileRange discreteTileRange(