From b42008c963acc5e27ac2388db3b7f4d7147a7aa4 Mon Sep 17 00:00:00 2001 From: Wojciech Warwas Date: Sat, 4 Apr 2020 14:38:50 +0200 Subject: [PATCH] show old tiles until new ones are downloaded --- lib/src/layer/image_tile.dart | 65 ++++++++++++ lib/src/layer/tile_layer.dart | 186 +++++++++++++++++++++++++--------- 2 files changed, 201 insertions(+), 50 deletions(-) create mode 100644 lib/src/layer/image_tile.dart diff --git a/lib/src/layer/image_tile.dart b/lib/src/layer/image_tile.dart new file mode 100644 index 000000000..19b771d6d --- /dev/null +++ b/lib/src/layer/image_tile.dart @@ -0,0 +1,65 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:transparent_image/transparent_image.dart'; + +typedef DownloadListener = void Function(Coords downloadedCoords); + +class ImageTile extends StatefulWidget { + + final TileLayerOptions options; + final Coords coords; + final Size size; + final DownloadListener listener; + + const ImageTile({Key key, this.options, this.coords, this.size, this.listener}) : super(key: key); + + @override + _ImageTileState createState() => _ImageTileState(); +} + +class _ImageTileState extends State { + + bool _isDownloaded = false; + ImageInfo _imageInfo; + ImageProvider _provider; + ImageStream _stream; + + @override + void initState() { + super.initState(); + _provider = widget.options.tileProvider.getImage(widget.coords, widget.options); + _stream = _provider.resolve(ImageConfiguration(size: widget.size)); + _stream.addListener(ImageStreamListener(_updateImage)); + } + + @override + void dispose() { + _stream.removeListener(ImageStreamListener(_updateImage)); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 100), + child: _isDownloaded + ? ConstrainedBox( + constraints: BoxConstraints.expand(), + child: RawImage( + image: _imageInfo?.image, + fit: BoxFit.fill, + ), + ) + : Image(image: MemoryImage(kTransparentImage)), + ); + } + + void _updateImage(ImageInfo imageInfo, bool synchronousCall) { + widget.listener(widget.coords); + setState(() { + _isDownloaded = true; + _imageInfo = imageInfo; + }); + } +} diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index d0d518c8c..74e974cd0 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -6,10 +6,10 @@ import 'package:flutter_map/src/core/bounds.dart'; import 'package:flutter_map/src/core/point.dart'; import 'package:flutter_map/src/core/util.dart' as util; import 'package:flutter_map/src/geo/crs/crs.dart'; +import 'package:flutter_map/src/layer/image_tile.dart'; import 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; import 'package:flutter_map/src/map/map.dart'; import 'package:latlong/latlong.dart'; -import 'package:transparent_image/transparent_image.dart'; import 'package:tuple/tuple.dart'; import 'layer.dart'; @@ -116,10 +116,13 @@ class TileLayerOptions extends LayerOptions { /// 'id': 'mapbox.streets', /// }, /// ), - /// ``` + /// /// Map additionalOptions; + // Enables tile coordinates printed on every tile and a tile borders + final bool tileDebugInfo; + TileLayerOptions( {this.urlTemplate, this.tileSize = 256.0, @@ -136,6 +139,7 @@ class TileLayerOptions extends LayerOptions { // ignore: avoid_init_to_null this.wmsOptions = null, this.opacity = 1.0, + this.tileDebugInfo = false, rebuild}) : super(rebuild: rebuild); } @@ -190,16 +194,12 @@ class WMSTileLayerOptions { String _buildEncodedBaseUrl() { final projectionKey = _versionNumber >= 1.3 ? 'crs' : 'srs'; final buffer = StringBuffer(baseUrl) - ..write('&service=$service') - ..write('&request=$request') - ..write('&layers=${layers.map(Uri.encodeComponent).join(',')}') - ..write('&styles=${styles.map(Uri.encodeComponent).join(',')}') - ..write('&format=${Uri.encodeComponent(format)}') - ..write('&$projectionKey=${Uri.encodeComponent(crs.code)}') - ..write('&version=${Uri.encodeComponent(version)}') - ..write('&transparent=$transparent'); - otherParameters - .forEach((k, v) => buffer.write('&$k=${Uri.encodeComponent(v)}')); + ..write('&service=$service')..write('&request=$request')..write( + '&layers=${layers.map(Uri.encodeComponent).join(',')}')..write( + '&styles=${styles.map(Uri.encodeComponent).join(',')}')..write( + '&format=${Uri.encodeComponent(format)}')..write('&$projectionKey=${Uri.encodeComponent(crs.code)}')..write( + '&version=${Uri.encodeComponent(version)}')..write('&transparent=$transparent'); + otherParameters.forEach((k, v) => buffer.write('&$k=${Uri.encodeComponent(v)}')); return buffer.toString(); } @@ -254,6 +254,7 @@ class _TileLayerState extends State { final Map _tiles = {}; final Map _levels = {}; + final Set _downloadedCoords = {}; @override void initState() { @@ -331,7 +332,7 @@ class _TileLayerState extends State { void _pruneTiles() { var center = map.center; - var pixelBounds = _getTiledPixelBounds(center); + var pixelBounds = _getTiledPixelBounds(center, _tileZoom); var tileRange = _pxBoundsToTileRange(pixelBounds); var margin = options.keepBuffer ?? 2; var noPruneRange = Bounds( @@ -400,26 +401,34 @@ class _TileLayerState extends State { _wrapX = crs.wrapLng; if (_wrapX != null) { var first = - (map.project(LatLng(0.0, crs.wrapLng.item1), tileZoom).x / tileSize.x) - .floor() - .toDouble(); + (map + .project(LatLng(0.0, crs.wrapLng.item1), tileZoom) + .x / tileSize.x) + .floor() + .toDouble(); var second = - (map.project(LatLng(0.0, crs.wrapLng.item2), tileZoom).x / tileSize.y) - .ceil() - .toDouble(); + (map + .project(LatLng(0.0, crs.wrapLng.item2), tileZoom) + .x / tileSize.y) + .ceil() + .toDouble(); _wrapX = Tuple2(first, second); } _wrapY = crs.wrapLat; if (_wrapY != null) { var first = - (map.project(LatLng(crs.wrapLat.item1, 0.0), tileZoom).y / tileSize.x) - .floor() - .toDouble(); + (map + .project(LatLng(crs.wrapLat.item1, 0.0), tileZoom) + .y / tileSize.x) + .floor() + .toDouble(); var second = - (map.project(LatLng(crs.wrapLat.item2, 0.0), tileZoom).y / tileSize.y) - .ceil() - .toDouble(); + (map + .project(LatLng(crs.wrapLat.item2, 0.0), tileZoom) + .y / tileSize.y) + .ceil() + .toDouble(); _wrapY = Tuple2(first, second); } } @@ -435,10 +444,12 @@ class _TileLayerState extends State { @override Widget build(BuildContext context) { - var pixelBounds = _getTiledPixelBounds(map.center); + var pixelBounds = _getTiledPixelBounds(map.center, _tileZoom); var tileRange = _pxBoundsToTileRange(pixelBounds); + var tileCenter = tileRange.getCenter(); var queue = []; + var bgQueue = []; // mark tiles as out of view... for (var key in _tiles.keys) { @@ -450,6 +461,8 @@ class _TileLayerState extends State { _setView(map.center, map.zoom); + var isCurrentLevelDownloaded = true; + for (var j = tileRange.min.y; j <= tileRange.max.y; j++) { for (var i = tileRange.min.x; i <= tileRange.max.x; i++) { var coords = Coords(i.toDouble(), j.toDouble()); @@ -461,6 +474,47 @@ class _TileLayerState extends State { // Add all valid tiles to the queue on Flutter queue.add(coords); + + if (!_downloadedCoords.contains(coords)) { + isCurrentLevelDownloaded = false; + } + } + } + + if (!isCurrentLevelDownloaded) { + for (var zoomToCheck = _tileZoom.toInt() - 1; zoomToCheck >= 0; zoomToCheck--) { + var pixelBoundsForLevel = _getTiledPixelBounds(map.center, zoomToCheck.toDouble()); + var tileRangeForLevel = _pxBoundsToTileRange(pixelBoundsForLevel); + + var tempTiles = []; + + if (!_levels.containsKey(zoomToCheck)) { + break; + } + + for (var j = tileRangeForLevel.min.y; j <= tileRangeForLevel.max.y; j++) { + for (var i = tileRangeForLevel.min.x; i <= tileRangeForLevel.max.x; i++) { + var coords = Coords(i.toDouble(), j.toDouble()); + coords.z = zoomToCheck.toDouble(); + + if (!_isValidTile(coords)) { + continue; + } + + // Add all valid tiles to the queue on Flutter + tempTiles.add(coords); + } + } + + for (var coords in tempTiles) { + if (_downloadedCoords.contains(coords)) { + bgQueue.add(coords); + } + } + var areAllTilesFromZoomDownloaded = !tempTiles.any((element) => !_downloadedCoords.contains(element)); + if (areAllTilesFromZoomDownloaded) { + break; + } } } @@ -470,24 +524,27 @@ class _TileLayerState extends State { } } - var tilesToRender = [ - for (var tile in _tiles.values) - if ((tile.coords.z - _level.zoom).abs() <= 1) tile - ]; + var tilesToRender = [for (var tile in _tiles.values) if (tile.coords.z == _tileZoom) tile]; + + if (bgQueue.isNotEmpty) { + for (var i = 0; i < bgQueue.length; i++) { + var tile = Tile(_wrapCoords(bgQueue[i]), true); + _tiles[_tileCoordsToKey(bgQueue[i])] = tile; + tilesToRender.add(tile); + } + } tilesToRender.sort((aTile, bTile) { final a = aTile.coords; // TODO there was an implicit casting here. final b = bTile.coords; // a = 13, b = 12, b is less than a, the result should be positive. if (a.z != b.z) { - return (b.z - a.z).toInt(); + return (a.z - b.z).toInt(); } return (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); }); - var tileWidgets = [ - for (var tile in tilesToRender) _createTileWidget(tile.coords) - ]; + var tileWidgets = [for (var tile in tilesToRender) _createTileWidget(tile)]; return Opacity( opacity: options.opacity, @@ -500,8 +557,8 @@ class _TileLayerState extends State { ); } - Bounds _getTiledPixelBounds(LatLng center) { - return map.getPixelBounds(_tileZoom); + Bounds _getTiledPixelBounds(LatLng center, double zoom) { + return map.getPixelBounds(zoom); } Bounds _pxBoundsToTileRange(Bounds bounds) { @@ -516,10 +573,8 @@ class _TileLayerState extends State { var crs = map.options.crs; if (!crs.infinite) { var bounds = _globalTileRange; - if ((crs.wrapLng == null && - (coords.x < bounds.min.x || coords.x > bounds.max.x)) || - (crs.wrapLat == null && - (coords.y < bounds.min.y || coords.y > bounds.max.y))) { + if ((crs.wrapLng == null && (coords.x < bounds.min.x || coords.x > bounds.max.x)) || + (crs.wrapLat == null && (coords.y < bounds.min.y || coords.y > bounds.max.y))) { return false; } } @@ -530,7 +585,8 @@ class _TileLayerState extends State { return '${coords.x}:${coords.y}:${coords.z}'; } - Widget _createTileWidget(Coords coords) { + Widget _createTileWidget(Tile tile) { + var coords = tile.coords; var tilePos = _getTilePos(coords); var level = _levels[coords.z]; var tileSize = getTileSize(); @@ -538,16 +594,29 @@ class _TileLayerState extends State { var width = tileSize.x * level.scale; var height = tileSize.y * level.scale; + final tileWidget = ImageTile( + key: Key(_tileCoordsToKey(coords)), + options: options, + coords: coords, + size: Size(width, height), + listener: _tileDownloaded, + ); + final Widget content = Container( - child: FadeInImage( - fadeInDuration: const Duration(milliseconds: 100), - key: Key(_tileCoordsToKey(coords)), - placeholder: options.placeholderImage != null - ? options.placeholderImage - : MemoryImage(kTransparentImage), - image: options.tileProvider.getImage(coords, options), - fit: BoxFit.fill, - ), + decoration: options.tileDebugInfo ? BoxDecoration(border: Border.all()) : null, + child: options.tileDebugInfo + ? Stack( + fit: StackFit.expand, + children: [ + tileWidget, + Align( + alignment: Alignment.center, + child: Text('x: ${coords.x.toStringAsFixed(0)} y: ${coords.y.toStringAsFixed(0)} ' + 'z: ${coords.z.toStringAsFixed(0)}'), + ), + ], + ) + : tileWidget, ); return Positioned( @@ -558,6 +627,17 @@ class _TileLayerState extends State { child: content); } + void _tileDownloaded(Coords coords) { + _downloadedCoords.add(coords); + if(_tiles.values.every((element) => _downloadedCoords.contains(element.coords))) { + WidgetsBinding.instance.addPostFrameCallback((_){ + setState(() { + }); + }); + + } + } + Coords _wrapCoords(Coords coords) { var newCoords = Coords( _wrapX != null @@ -582,6 +662,9 @@ class Tile { bool current; Tile(this.coords, this.current); + + @override + String toString() => 'Tile(coords: $coords, current: $current)'; } class Level { @@ -591,6 +674,9 @@ class Level { double zoom; CustomPoint translatePoint; double scale; + + @override + String toString() => 'Level(zoom: $zoom, scale: $scale, zIndex: $zIndex, childrenCount: ${children.length}'; } class Coords extends CustomPoint {