diff --git a/lib/src/layer/tile_layer/tile_cover.dart b/lib/src/layer/tile_layer/tile_cover.dart index 3e9a6a5e7..da4d11475 100644 --- a/lib/src/layer/tile_layer/tile_cover.dart +++ b/lib/src/layer/tile_layer/tile_cover.dart @@ -39,6 +39,7 @@ class _TileSet implements TileCover { Set? _wrappedTiles; final bool Function(TileCoordinates tile)? _isCovered; + final _expansions = {}; _TileSet( Iterable tiles, { @@ -60,8 +61,10 @@ class _TileSet implements TileCover { TileCoordinates(tile.x % (1 << tile.z), tile.y, tile.z); @override - TileCover expand(int count) { - if (count == 0) return this; + TileCover expand(int count) => + count == 0 ? this : _expansions.putIfAbsent(count, () => _expand(count)); + + TileCover _expand(int count) { final expanded = LinkedHashSet.of(_tiles); for (final tile in _tiles) { for (var y = tile.y - count; y <= tile.y + count; y++) { diff --git a/lib/src/layer/tile_layer/tile_cover_calculator.dart b/lib/src/layer/tile_layer/tile_cover_calculator.dart index 2f7e46a4a..cc457e999 100644 --- a/lib/src/layer/tile_layer/tile_cover_calculator.dart +++ b/lib/src/layer/tile_layer/tile_cover_calculator.dart @@ -60,7 +60,7 @@ class _Cover { final List _corners; final Offset _pixelCenter; final int _tileZoom; - final _polygons = >{}; + final _grounds = {}; _Cover({ required this.camera, @@ -95,12 +95,15 @@ class _Cover { camera.getZoomScale(zoom.toDouble(), _tileZoom.toDouble()); /// The visible ground in pixels at [zoom]. - List _polygon(int zoom) => _polygons.putIfAbsent(zoom, () { + _Ground _ground(int zoom) => _grounds.putIfAbsent(zoom, () { final center = _centerAt(zoom); final scale = camera.getZoomScale(viewingZoom, zoom.toDouble()); - return [for (final corner in _corners) center + corner / scale]; + return _Ground( + [for (final corner in _corners) center + corner / scale]); }); + List _polygon(int zoom) => _ground(zoom).points; + TileCover tiles() { final startZoom = _zoomAtScale(_corners.map(camera.groundScale).reduce(math.min)); @@ -158,35 +161,12 @@ class _Cover { ); /// Whether [tile] and the visible ground overlap in more than an edge, as - /// in [DiscreteTileRange.fromPixelBounds]: a separating axis test. - bool _intersects(TileCoordinates tile) { - final polygon = _polygon(tile.z); - final square = _square(tile); - final squareCorners = [ - square.topLeft, - square.topRight, - square.bottomRight, - square.bottomLeft, - ]; - bool separated(Offset axis) { - double dot(Offset p) => p.dx * axis.dx + p.dy * axis.dy; - final a = polygon.map(dot); - final b = squareCorners.map(dot); - return a.reduce(math.max) <= b.reduce(math.min) || - b.reduce(math.max) <= a.reduce(math.min); - } - - if (separated(const Offset(1, 0)) || separated(const Offset(0, 1))) { - return false; - } - for (var i = 0; i < polygon.length; i++) { - final edge = polygon[(i + 1) % polygon.length] - polygon[i]; - // Edges along the x and y axes were tested above. - if (edge.dx == 0 || edge.dy == 0) continue; - if (separated(Offset(-edge.dy, edge.dx))) return false; - } - return true; - } + /// in [DiscreteTileRange.fromPixelBounds]. + bool _intersects(TileCoordinates tile) => _ground(tile.z).overlaps( + (tile.x * tileDimension).toDouble(), + (tile.y * tileDimension).toDouble(), + tileDimension.toDouble(), + ); /// The part of the convex [polygon] inside [rect]. static List _clip(List polygon, Rect rect) { @@ -221,3 +201,57 @@ class _Cover { static Offset _atY(Offset a, Offset b, double y) => Offset(a.dx + (b.dx - a.dx) * (y - a.dy) / (b.dy - a.dy), y); } + +/// A convex polygon, prepared for a separating axis test against squares. +class _Ground { + final List points; + + /// The x and y axes, and the normals of the edges that aren't along them. + final List _axes; + + /// The smallest and largest projection of [points] onto each of [_axes]. + final List _mins; + final List _maxs; + + factory _Ground(List points) { + final axes = [const Offset(1, 0), const Offset(0, 1)]; + for (var i = 0; i < points.length; i++) { + final edge = points[(i + 1) % points.length] - points[i]; + if (edge.dx != 0 && edge.dy != 0) axes.add(Offset(-edge.dy, edge.dx)); + } + final mins = []; + final maxs = []; + for (final axis in axes) { + var min = double.infinity; + var max = double.negativeInfinity; + for (final p in points) { + final d = p.dx * axis.dx + p.dy * axis.dy; + if (d < min) min = d; + if (d > max) max = d; + } + mins.add(min); + maxs.add(max); + } + return _Ground._(points, axes, mins, maxs); + } + + const _Ground._(this.points, this._axes, this._mins, this._maxs); + + /// Whether the square at [left], [top] with sides of [size] and this polygon + /// overlap in more than an edge. + bool overlaps(double left, double top, double size) { + final right = left + size; + final bottom = top + size; + for (var i = 0; i < _axes.length; i++) { + final axis = _axes[i]; + final x0 = axis.dx * left; + final x1 = axis.dx * right; + final y0 = axis.dy * top; + final y1 = axis.dy * bottom; + final min = math.min(x0, x1) + math.min(y0, y1); + final max = math.max(x0, x1) + math.max(y0, y1); + if (_maxs[i] <= min || max <= _mins[i]) return false; + } + return true; + } +} diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index b6ba5a611..48b3ed25e 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -367,6 +367,15 @@ class _TileLayerState extends State with TickerProviderStateMixin { final _tileImageManager = TileImageManager(); late TileBounds _tileBounds; late TileCoverCalculator _tileCoverCalculator; + + /// The last result of [_visibleTileCover], which a build and the tile + /// update event of the same camera both use. + ({ + MapCamera camera, + LatLng center, + double zoom, + TileCover cover + })? _lastTileCover; late TileScaleCalculator _tileScaleCalculator; // We have to hold on to the mapController hashCode to determine whether we @@ -450,6 +459,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { // There is no caching in TileCoverCalculator so we can just replace it. _tileCoverCalculator = TileCoverCalculator(tileDimension: _tileDimension); + _lastTileCover = null; if (_tileBounds.shouldReplace( _tileBounds.crs, _tileDimension, widget.tileBounds)) { @@ -661,14 +671,26 @@ class _TileLayerState extends State with TickerProviderStateMixin { MapCamera camera, { LatLng? center, double? zoom, - }) => - _tileCoverCalculator.calculate( - camera: camera, - tileZoom: _clampToNativeZoom(zoom ?? camera.zoom), - minNativeZoom: widget.minNativeZoom, - center: center, - viewingZoom: zoom, - ); + }) { + center ??= camera.center; + zoom ??= camera.zoom; + final last = _lastTileCover; + if (last != null && + last.camera == camera && + last.center == center && + last.zoom == zoom) { + return last.cover; + } + final cover = _tileCoverCalculator.calculate( + camera: camera, + tileZoom: _clampToNativeZoom(zoom), + minNativeZoom: widget.minNativeZoom, + center: center, + viewingZoom: zoom, + ); + _lastTileCover = (camera: camera, center: center, zoom: zoom, cover: cover); + return cover; + } // For all valid TileCoordinates in the [tileLoadCover], expanded by the // [TileLayer.panBuffer], this method will do the following depending on