From aa0aebcde96dec8e384e2eabd90aa79354e7bbfd Mon Sep 17 00:00:00 2001 From: maRci002 Date: Mon, 30 Mar 2020 17:22:52 +0200 Subject: [PATCH 01/20] imporve tile managment initial commit based on Leaflet --- lib/src/layer/tile_layer.dart | 607 ++++++++++++++++++++++++++-------- pubspec.yaml | 1 + 2 files changed, 462 insertions(+), 146 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index d0d518c8c..7b561d901 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -9,7 +9,7 @@ import 'package:flutter_map/src/geo/crs/crs.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:rxdart/rxdart.dart'; import 'package:tuple/tuple.dart'; import 'layer.dart'; @@ -251,6 +251,7 @@ class _TileLayerState extends State { double _tileZoom; Level _level; StreamSubscription _moveSub; + final _throttleUpdate = PublishSubject(); final Map _tiles = {}; final Map _levels = {}; @@ -259,35 +260,142 @@ class _TileLayerState extends State { void initState() { super.initState(); _resetView(); + _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); + + _throttleUpdate + .throttleTime(Duration(milliseconds: 50)) + .listen((_) => _update(null)); } @override void dispose() { super.dispose(); + + _removeAllTiles(); _moveSub?.cancel(); options.tileProvider.dispose(); + _throttleUpdate?.close(); } - void _handleMove() { - setState(() { - _pruneTiles(); - _resetView(); + @override + Widget build(BuildContext context) { + var pixelBounds = _getTiledPixelBounds(map.center); + var tileRange = _pxBoundsToTileRange(pixelBounds); + + var tileCenter = tileRange.getCenter(); + var tilesToRender = [ + for (var tile in _tiles.values) + // if ((tile.coords.z - _level.zoom).abs() <= 1) tile + if (tile.imageInfo != null) + 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.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); }); + + var tileWidgets = [ + for (var tile in tilesToRender) _createTileWidget(tile) + ]; + + return Opacity( + opacity: options.opacity, + child: Container( + color: options.backgroundColor, + child: Stack( + children: tileWidgets, + ), + ), + ); } - void _resetView() { - _setView(map.center, map.zoom); + Widget _createTileWidget(Tile tile) { + var coords = tile.coords; + + var tilePos = _getTilePos(coords); + var level = _levels[coords.z]; + var tileSize = getTileSize(); + var pos = (tilePos).multiplyBy(level.scale) + level.translatePoint; + var width = tileSize.x * level.scale; + var height = tileSize.y * level.scale; + + /* + FadeInImage( + fadeInDuration: const Duration(milliseconds: 100), + key: Key(_tileCoordsToKey(coords)), + placeholder: options.placeholderImage != null + ? options.placeholderImage + : MemoryImage(kTransparentImage), + image: tile.image, + fit: BoxFit.fill, + ), + + */ + + Widget content = RawImage( + key: Key(_tileCoordsToKey(coords)), + image: tile.imageInfo.image, + fit: BoxFit.fill, + ); +/* + content = Container( + height: 200, + decoration: BoxDecoration( + // color: Colors.green, + image: DecorationImage(image: MemoryImage(kTransparentImage)))); +*/ + return Positioned( + key: Key(_tileCoordsToKey(coords)), + left: pos.x.toDouble(), + top: pos.y.toDouble(), + width: width.toDouble(), + height: height.toDouble(), + child: content); } - void _setView(LatLng center, double zoom) { - var tileZoom = _clampZoom(zoom.round().toDouble()); - if (_tileZoom != tileZoom) { - _tileZoom = tileZoom; - _updateLevels(); - _resetGrid(); + void _abortLoading() { + var toRemove = []; + for (var entry in _tiles.entries) { + var tile = entry.value; + + if (tile.coords.z != _tileZoom) { + if (tile.loaded == null) { + toRemove.add(entry.key); + } + } } - _setZoomTransforms(center, zoom); + + if (toRemove.isNotEmpty) { + print('_prune+abort ---------------------'); + } + + for (var key in toRemove) { + var tile = _tiles[key]; + print('_prune+abort: ${tile.coords}'); + tile.dispose(); + _tiles.remove(key); + } + } + + CustomPoint getTileSize() { + return CustomPoint(options.tileSize, options.tileSize); + } + + bool _hasLevelChildren(double lvl) { + for (var tile in _tiles.values) { + if (tile.coords.z == lvl) { + return true; + } + } + + return false; } Level _updateLevels() { @@ -297,9 +405,12 @@ class _TileLayerState extends State { if (zoom == null) return null; var toRemove = []; - for (var z in _levels.keys) { - if (_levels[z].children.isNotEmpty || z == zoom) { - _levels[z].zIndex = maxZoom = (zoom - z).abs(); + for (var entry in _levels.entries) { + var z = entry.key; + var lvl = entry.value; + + if (z == zoom || _hasLevelChildren(z)) { + lvl.zIndex = maxZoom = (zoom - z).abs(); } else { toRemove.add(z); } @@ -314,75 +425,187 @@ class _TileLayerState extends State { if (level == null) { level = _levels[zoom] = Level(); + level.zIndex = maxZoom; - var newOrigin = map.project(map.unproject(map.getPixelOrigin()), zoom); - if (newOrigin != null) { - level.origin = newOrigin; - } else { - level.origin = CustomPoint(0.0, 0.0); - } + + level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom) ?? + CustomPoint(0.0, 0.0); level.zoom = zoom; _setZoomTransform(level, map.center, map.zoom); } - _level = level; - return level; + + return _level = level; } void _pruneTiles() { - var center = map.center; - var pixelBounds = _getTiledPixelBounds(center); - var tileRange = _pxBoundsToTileRange(pixelBounds); - var margin = options.keepBuffer ?? 2; - var noPruneRange = Bounds( - tileRange.bottomLeft - CustomPoint(margin, -margin), - tileRange.topRight + CustomPoint(margin, -margin)); - for (var tileKey in _tiles.keys) { - var tile = _tiles[tileKey]; - var c = tile.coords; - if (c.z != _tileZoom || !noPruneRange.contains(CustomPoint(c.x, c.y))) { - tile.current = false; - } + if (map == null) { + return; } - _tiles.removeWhere((s, tile) => tile.current == false); - } - void _setZoomTransform(Level level, LatLng center, double zoom) { - var scale = map.getZoomScale(zoom, level.zoom); - var pixelOrigin = map.getNewPixelOrigin(center, zoom).round(); - if (level.origin == null) { + var zoom = map.zoom; + if (zoom > options.maxZoom) { + // TODO: min! + // _removeAllTiles(); return; } - var translate = level.origin.multiplyBy(scale) - pixelOrigin; - level.translatePoint = translate; - level.scale = scale; - } - void _setZoomTransforms(LatLng center, double zoom) { - for (var i in _levels.keys) { - _setZoomTransform(_levels[i], center, zoom); + for (var entry in _tiles.entries) { + var tile = entry.value; + tile.retain = tile.current; + } + + for (var entry in _tiles.entries) { + var tile = entry.value; + + if (tile.current && !tile.active) { + var coords = tile.coords; + if (!_retainParent(coords.x, coords.y, coords.z, coords.z - 5)) { + _retainChildren(coords.x, coords.y, coords.z, coords.z + 2); + } + } + } + + var toRemove = []; + for (var entry in _tiles.entries) { + var tile = entry.value; + + if (!tile.retain) { + toRemove.add(entry.key); + } + } + + // TODO: remove diagnostic + if (toRemove.isNotEmpty) { + print('_prune ---------------------'); + } + + for (var key in toRemove) { + print('_prune: ${_tiles[key].coords}'); + _removeTile(key); } } void _removeTilesAtZoom(double zoom) { var toRemove = []; - for (var key in _tiles.keys) { - if (_tiles[key].coords.z != zoom) { + for (var entry in _tiles.entries) { + if (entry.value.coords.z != zoom) { continue; } - toRemove.add(key); + _removeTile(entry.key); } + for (var key in toRemove) { _removeTile(key); } } - void _removeTile(String key) { + void _removeAllTiles() { + var toRemove = Map.from(_tiles); + + for (var key in toRemove.keys) { + _removeTile(key); + } + } + + bool _retainParent(double x, double y, double z, double minZoom) { + var x2 = (x / 2).floorToDouble(); + var y2 = (y / 2).floorToDouble(); + var z2 = z - 1; + var coords2 = Coords(x2, y2); + coords2.z = z2; + + var key = _tileCoordsToKey(coords2); + var tile = _tiles[key]; - if (tile == null) { + if (tile != null) { + if (tile.active) { + tile.retain = true; + return true; + } else if (tile.loaded != null) { + tile.retain = true; + } + } + + if (z2 > minZoom) { + return _retainParent(x2, y2, z2, minZoom); + } + + return false; + } + + void _retainChildren(double x, double y, double z, double maxZoom) { + for (var i = 2 * x; i < 2 * x + 2; i++) { + for (var j = 2 * y; j < 2 * y + 2; j++) { + var coords = Coords(i, j); + coords.z = z + 1; + + var key = _tileCoordsToKey(coords); + + var tile = _tiles[key]; + if (tile != null) { + if (tile.active) { + tile.retain = true; + continue; + } else if (tile.loaded != null) { + tile.retain = true; + } + } + + if (z + 1 < maxZoom) { + _retainChildren(i, j, z + 1, maxZoom); + } + } + } + } + + void _resetView() { + _setView(map.center, map.zoom); + } + + double _clampZoom(double zoom) { + // TODO + return zoom; + } + + void _setView(LatLng center, double zoom) { + var tileZoom = _clampZoom(zoom.round().toDouble()); + if ((options.maxZoom != null && tileZoom > options.maxZoom)) { + // TODO: minZoom ! + // tileZoom = null; + } + + _tileZoom = tileZoom; + + _abortLoading(); + + _updateLevels(); + _resetGrid(); + + if (_tileZoom != null) { + _update(center); + } + + _pruneTiles(); + + _setZoomTransforms(center, zoom); + } + + void _setZoomTransforms(LatLng center, double zoom) { + for (var i in _levels.keys) { + _setZoomTransform(_levels[i], center, zoom); + } + } + + void _setZoomTransform(Level level, LatLng center, double zoom) { + var scale = map.getZoomScale(zoom, level.zoom); + var pixelOrigin = map.getNewPixelOrigin(center, zoom).round(); + if (level.origin == null) { return; } - _tiles[key].current = false; + var translate = level.origin.multiplyBy(scale) - pixelOrigin; + level.translatePoint = translate; + level.scale = scale; } void _resetGrid() { @@ -424,32 +647,59 @@ class _TileLayerState extends State { } } - double _clampZoom(double zoom) { - // todo - return zoom; + void _handleMove() { + setState(() { + // _update(null); + _throttleUpdate.add(null); + _setZoomTransforms(map.center, map.zoom); + }); } - CustomPoint getTileSize() { - return CustomPoint(options.tileSize, options.tileSize); + Bounds _getTiledPixelBounds(LatLng center) { + var scale = map.getZoomScale(map.zoom, _tileZoom); + var pixelCenter = map.project(center, _tileZoom).floor(); + var halfSize = map.size / (scale * 2); + + return Bounds(pixelCenter - halfSize, pixelCenter + halfSize); } - @override - Widget build(BuildContext context) { - var pixelBounds = _getTiledPixelBounds(map.center); + // Private method to load tiles in the grid's active zoom level according to map bounds + void _update(LatLng center) { + if (map == null || _tileZoom == null) { + return; + } + + var zoom = _clampZoom(map.zoom); + center ??= map.center; + + var pixelBounds = _getTiledPixelBounds(center); var tileRange = _pxBoundsToTileRange(pixelBounds); var tileCenter = tileRange.getCenter(); - var queue = []; + var queue = >[]; + var margin = options.keepBuffer ?? 2; + var noPruneRange = Bounds( + tileRange.bottomLeft - CustomPoint(margin, -margin), + tileRange.topRight + CustomPoint(margin, -margin), + ); - // mark tiles as out of view... - for (var key in _tiles.keys) { - var c = _tiles[key].coords; - if (c.z != _tileZoom) { - _tiles[key].current = false; + for (var entry in _tiles.entries) { + var tile = entry.value; + var c = tile.coords; + + if (tile.current == true && + (c.z != _tileZoom || !noPruneRange.contains(CustomPoint(c.x, c.y)))) { + tile.current = false; } } - _setView(map.center, map.zoom); + // _update just loads more tiles. If the tile zoom level differs too much + // from the map's, let _setView reset levels and prune old tiles. + if ((zoom - _tileZoom).abs() >= 1) { + _setView(center, zoom); + return; + } + // create a queue of coordinates to load tiles from 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()); @@ -459,62 +709,34 @@ class _TileLayerState extends State { continue; } - // Add all valid tiles to the queue on Flutter - queue.add(coords); + var tile = _tiles[_tileCoordsToKey(coords)]; + if (tile != null) { + tile.current = true; + } else { + queue.add(coords); + } } } + // sort tile queue to load tiles in order of their distance to center + queue.sort((a, b) => + (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt()); + + // TODO: remove diagnostic if (queue.isNotEmpty) { - for (var i = 0; i < queue.length; i++) { - _tiles[_tileCoordsToKey(queue[i])] = Tile(_wrapCoords(queue[i]), true); - } + print('_addTile ---------------------'); } - var tilesToRender = [ - for (var tile in _tiles.values) - if ((tile.coords.z - _level.zoom).abs() <= 1) 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.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); - }); - - var tileWidgets = [ - for (var tile in tilesToRender) _createTileWidget(tile.coords) - ]; - - return Opacity( - opacity: options.opacity, - child: Container( - color: options.backgroundColor, - child: Stack( - children: tileWidgets, - ), - ), - ); - } - - Bounds _getTiledPixelBounds(LatLng center) { - return map.getPixelBounds(_tileZoom); - } - - Bounds _pxBoundsToTileRange(Bounds bounds) { - var tileSize = getTileSize(); - return Bounds( - bounds.min.unscaleBy(tileSize).floor(), - bounds.max.unscaleBy(tileSize).ceil() - CustomPoint(1, 1), - ); + for (var i = 0; i < queue.length; i++) { + _addTile(queue[i]); + } } bool _isValidTile(Coords coords) { var crs = map.options.crs; + if (!crs.infinite) { + // don't load tile if it's out of bounds and not wrapped var bounds = _globalTileRange; if ((crs.wrapLng == null && (coords.x < bounds.min.x || coords.x > bounds.max.x)) || @@ -523,6 +745,7 @@ class _TileLayerState extends State { return false; } } + return true; } @@ -530,32 +753,65 @@ class _TileLayerState extends State { return '${coords.x}:${coords.y}:${coords.z}'; } - Widget _createTileWidget(Coords coords) { - var tilePos = _getTilePos(coords); - var level = _levels[coords.z]; - var tileSize = getTileSize(); - var pos = (tilePos).multiplyBy(level.scale) + level.translatePoint; - var width = tileSize.x * level.scale; - var height = tileSize.y * level.scale; + Coords _keyToTileCoords(String key) { + var k = key.split(':'); + var coords = Coords(double.parse(k[0]), double.parse(k[1])); + coords.z = double.parse(k[2]); - 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, - ), + return coords; + } + + void _removeTile(String key) { + var tile = _tiles[key]; + if (tile == null) { + return; + } + + tile.dispose(); + _tiles.remove(key); + } + + void _addTile(Coords coords) { + // TODO: remove diagnostic + print('_fetch tile: $coords'); + + var tileCoordsToKey = _tileCoordsToKey(coords); + _tiles[tileCoordsToKey] = Tile( + coords: coords, + coordsKey: tileCoordsToKey, + tilePos: _getTilePos(coords), + current: true, + imageProvider: + options.tileProvider.getImage(_wrapCoords(coords), options), + tileReady: _tileReady, ); + } - return Positioned( - left: pos.x.toDouble(), - top: pos.y.toDouble(), - width: width.toDouble(), - height: height.toDouble(), - child: content); + void _tileReady(Coords coords, dynamic error, Tile tile) { + if (null != error) { + print(error); + } + + var key = _tileCoordsToKey(coords); + tile = _tiles[key]; + if (null == tile) { + return; + } + + tile.loaded = DateTime.now(); + tile.active = true; // TODO ?? + + setState(() {}); + + if (_noTilesToLoad()) { + // Wait a bit more than 0.2 secs (the duration of the tile fade-in) + // to trigger a pruning. + Future.delayed(Duration(milliseconds: 250), _pruneTiles); + } + } + + CustomPoint _getTilePos(Coords coords) { + return coords.scaleBy(getTileSize()) - _level.origin; } Coords _wrapCoords(Coords coords) { @@ -571,21 +827,80 @@ class _TileLayerState extends State { return newCoords; } - CustomPoint _getTilePos(Coords coords) { - var level = _levels[coords.z]; - return coords.scaleBy(getTileSize()) - level.origin; + Bounds _pxBoundsToTileRange(Bounds bounds) { + var tileSize = getTileSize(); + return Bounds( + bounds.min.unscaleBy(tileSize).floor(), + bounds.max.unscaleBy(tileSize).ceil() - CustomPoint(1, 1), + ); + } + + bool _noTilesToLoad() { + for (var entry in _tiles.entries) { + if (entry.value.loaded == null) { + return false; + } + } + return true; } } +typedef void TileReady(Coords coords, dynamic error, Tile tile); + class Tile { - final Coords coords; + final String coordsKey; + final Coords coords; + final CustomPoint tilePos; + final ImageProvider imageProvider; + bool current; + bool retain; + bool active; + DateTime loaded; + + // callback when tile is ready / error occurred + final TileReady tileReady; + ImageInfo imageInfo; + ImageStream _stream; + ImageStreamListener _listener; + + Tile({ + this.coordsKey, + this.coords, + this.tilePos, + this.imageProvider, + this.tileReady, + this.current = false, + this.active = false, + this.retain = false, + }) { + _stream = imageProvider.resolve(ImageConfiguration()); + _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); + _stream.addListener(_listener); + } + + // call this before GC! + void dispose([bool evict = false]) { + if (evict && imageProvider != null) { + imageProvider + .evict() + .then((bool succ) => print('evict tile: $coords -> $succ')); + } - Tile(this.coords, this.current); + _stream?.removeListener(_listener); + } + + void _tileOnLoad(ImageInfo imageInfo, bool synchronousCall) { + this.imageInfo = imageInfo; + tileReady(coords, null, this); + } + + void _tileOnError(dynamic exception, StackTrace stackTrace) { + tileReady(coords, exception, this); + } } class Level { - List children = []; double zIndex; CustomPoint origin; double zoom; diff --git a/pubspec.yaml b/pubspec.yaml index a111d4621..cddafe22d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,6 +23,7 @@ dependencies: path_provider: ^1.5.1 vector_math: ^2.0.0 proj4dart: ^1.0.4 + rxdart: ^0.23.1 dev_dependencies: pedantic: ^1.8.0 From 07fb9f4d18d30b6920c90a427ffd905fbb81f2d1 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Mon, 30 Mar 2020 18:17:23 +0200 Subject: [PATCH 02/20] abort improve --- lib/src/layer/tile_layer.dart | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 7b561d901..8faad948a 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -372,13 +372,17 @@ class _TileLayerState extends State { } } + // TODO: remove diagnostic if (toRemove.isNotEmpty) { print('_prune+abort ---------------------'); } for (var key in toRemove) { var tile = _tiles[key]; + // TODO: remove diagnostic print('_prune+abort: ${tile.coords}'); + + tile.tileReady = null; tile.dispose(); _tiles.remove(key); } @@ -859,7 +863,8 @@ class Tile { DateTime loaded; // callback when tile is ready / error occurred - final TileReady tileReady; + // it maybe be null forinstance when download aborted + TileReady tileReady; ImageInfo imageInfo; ImageStream _stream; ImageStreamListener _listener; @@ -884,19 +889,24 @@ class Tile { if (evict && imageProvider != null) { imageProvider .evict() - .then((bool succ) => print('evict tile: $coords -> $succ')); + .then((bool succ) => print('evict tile: $coords -> $succ')) + .catchError((error) => print('evict tile: $coords -> $error')); } _stream?.removeListener(_listener); } void _tileOnLoad(ImageInfo imageInfo, bool synchronousCall) { - this.imageInfo = imageInfo; - tileReady(coords, null, this); + if (null != tileReady) { + this.imageInfo = imageInfo; + tileReady(coords, null, this); + } } void _tileOnError(dynamic exception, StackTrace stackTrace) { - tileReady(coords, exception, this); + if (null != tileReady) { + tileReady(coords, exception, this); + } } } From 48915634660c089d7b9ba04100bdf63efa66ad41 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Mon, 30 Mar 2020 19:37:43 +0200 Subject: [PATCH 03/20] let user control throttleUpdate behaviour via updateInterval --- lib/src/layer/tile_layer.dart | 37 ++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 8faad948a..ed818a43a 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -120,6 +120,10 @@ class TileLayerOptions extends LayerOptions { /// Map additionalOptions; + // Tiles will not update more than once every `updateInterval` milliseconds when panning. + // This can save some bandwidth (fast panning / fast animate move) / fps + final int updateInterval; + TileLayerOptions( {this.urlTemplate, this.tileSize = 256.0, @@ -136,8 +140,10 @@ class TileLayerOptions extends LayerOptions { // ignore: avoid_init_to_null this.wmsOptions = null, this.opacity = 1.0, + this.updateInterval = 50, rebuild}) - : super(rebuild: rebuild); + : assert(updateInterval >= 0), + super(rebuild: rebuild); } class WMSTileLayerOptions { @@ -251,7 +257,7 @@ class _TileLayerState extends State { double _tileZoom; Level _level; StreamSubscription _moveSub; - final _throttleUpdate = PublishSubject(); + PublishSubject _throttleUpdate; final Map _tiles = {}; final Map _levels = {}; @@ -263,9 +269,11 @@ class _TileLayerState extends State { _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); - _throttleUpdate - .throttleTime(Duration(milliseconds: 50)) - .listen((_) => _update(null)); + _throttleUpdate = widget.options.updateInterval == 0 + ? null + : (PublishSubject() + ..throttleTime(Duration(milliseconds: 50)) + ..listen((_) => _update(null))); } @override @@ -653,9 +661,20 @@ class _TileLayerState extends State { void _handleMove() { setState(() { - // _update(null); - _throttleUpdate.add(null); - _setZoomTransforms(map.center, map.zoom); + var zoom = _clampZoom(map.zoom); + + if ((zoom - _tileZoom).abs() >= 1) { + // It was a zoom lvl change + _setView(map.center, zoom); + } else { + if (null == _throttleUpdate) { + _update(null); + } else { + _throttleUpdate.add(null); + } + + _setZoomTransforms(map.center, map.zoom); + } }); } @@ -698,7 +717,7 @@ class _TileLayerState extends State { // _update just loads more tiles. If the tile zoom level differs too much // from the map's, let _setView reset levels and prune old tiles. - if ((zoom - _tileZoom).abs() >= 1) { + if ((zoom - _tileZoom).abs() > 1) { _setView(center, zoom); return; } From ec99350a9f4a07248c4a42b1b3f8e8992e74dc75 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Mon, 30 Mar 2020 23:09:17 +0200 Subject: [PATCH 04/20] let user controll throttleUpdate behaviour 2 --- lib/src/layer/tile_layer.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index ed818a43a..adce10bbf 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -272,7 +272,7 @@ class _TileLayerState extends State { _throttleUpdate = widget.options.updateInterval == 0 ? null : (PublishSubject() - ..throttleTime(Duration(milliseconds: 50)) + ..throttleTime(Duration(milliseconds: widget.options.updateInterval)) ..listen((_) => _update(null))); } @@ -422,7 +422,7 @@ class _TileLayerState extends State { var lvl = entry.value; if (z == zoom || _hasLevelChildren(z)) { - lvl.zIndex = maxZoom = (zoom - z).abs(); + lvl.zIndex = maxZoom - (zoom - z).abs(); } else { toRemove.add(z); } From 85c970a4da3775282af272b62d3901db8c9198a8 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Tue, 31 Mar 2020 00:16:55 +0200 Subject: [PATCH 05/20] support fadeIn --- lib/src/layer/tile_layer.dart | 77 ++++++++++++++++++----------------- 1 file changed, 40 insertions(+), 37 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index adce10bbf..0cec8e98a 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -120,9 +120,11 @@ class TileLayerOptions extends LayerOptions { /// Map additionalOptions; - // Tiles will not update more than once every `updateInterval` milliseconds when panning. - // This can save some bandwidth (fast panning / fast animate move) / fps - final int updateInterval; + // Tiles will not update more than once every `updateInterval` when panning. + // This can save some bandwidth (ie. when fast panning / fast panning) / fps + final Duration updateInterval; + + final Duration tileFadeInDuration; TileLayerOptions( {this.urlTemplate, @@ -140,9 +142,11 @@ class TileLayerOptions extends LayerOptions { // ignore: avoid_init_to_null this.wmsOptions = null, this.opacity = 1.0, - this.updateInterval = 50, + this.updateInterval = const Duration(milliseconds: 50), + this.tileFadeInDuration = const Duration(milliseconds: 200), rebuild}) - : assert(updateInterval >= 0), + : assert(tileFadeInDuration != null && !tileFadeInDuration.isNegative), + assert(updateInterval != null && !updateInterval.isNegative), super(rebuild: rebuild); } @@ -269,10 +273,10 @@ class _TileLayerState extends State { _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); - _throttleUpdate = widget.options.updateInterval == 0 + _throttleUpdate = options.updateInterval.inMicroseconds == 0 ? null : (PublishSubject() - ..throttleTime(Duration(milliseconds: widget.options.updateInterval)) + ..throttleTime(options.updateInterval) ..listen((_) => _update(null))); } @@ -334,38 +338,27 @@ class _TileLayerState extends State { var width = tileSize.x * level.scale; var height = tileSize.y * level.scale; - /* - FadeInImage( - fadeInDuration: const Duration(milliseconds: 100), - key: Key(_tileCoordsToKey(coords)), - placeholder: options.placeholderImage != null - ? options.placeholderImage - : MemoryImage(kTransparentImage), - image: tile.image, + final Widget content = AnimatedOpacity( + key: Key(tile.coordsKey), + opacity: tile.active || tile.fadeStarted ? 1.0 : 0.0, + duration: options.tileFadeInDuration, + onEnd: tile.onAnimateEnd, + child: RawImage( + image: tile.imageInfo.image, fit: BoxFit.fill, ), + ); - */ + tile.fadeStarted = true; - Widget content = RawImage( - key: Key(_tileCoordsToKey(coords)), - image: tile.imageInfo.image, - fit: BoxFit.fill, - ); -/* - content = Container( - height: 200, - decoration: BoxDecoration( - // color: Colors.green, - image: DecorationImage(image: MemoryImage(kTransparentImage)))); -*/ return Positioned( - key: Key(_tileCoordsToKey(coords)), - left: pos.x.toDouble(), - top: pos.y.toDouble(), - width: width.toDouble(), - height: height.toDouble(), - child: content); + key: Key(tile.coordsKey), + left: pos.x.toDouble(), + top: pos.y.toDouble(), + width: width.toDouble(), + height: height.toDouble(), + child: content, + ); } void _abortLoading() { @@ -822,14 +815,18 @@ class _TileLayerState extends State { } tile.loaded = DateTime.now(); - tile.active = true; // TODO ?? setState(() {}); if (_noTilesToLoad()) { - // Wait a bit more than 0.2 secs (the duration of the tile fade-in) + // Wait a bit more than tileFadeInDuration (the duration of the tile fade-in) // to trigger a pruning. - Future.delayed(Duration(milliseconds: 250), _pruneTiles); + Future.delayed( + options.tileFadeInDuration + const Duration(milliseconds: 50), + () { + setState(_pruneTiles); + }, + ); } } @@ -881,6 +878,8 @@ class Tile { bool active; DateTime loaded; + bool fadeStarted = false; + // callback when tile is ready / error occurred // it maybe be null forinstance when download aborted TileReady tileReady; @@ -915,6 +914,10 @@ class Tile { _stream?.removeListener(_listener); } + void onAnimateEnd() { + active = true; + } + void _tileOnLoad(ImageInfo imageInfo, bool synchronousCall) { if (null != tileReady) { this.imageInfo = imageInfo; From 76faf54ef3bbdb7f2ac46a9cf7d09ac290e62695 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Wed, 1 Apr 2020 01:07:00 +0200 Subject: [PATCH 06/20] create throttle stream which supports trailing calls --- lib/src/layer/tile_layer.dart | 22 +++++++++++++++------- pubspec.yaml | 1 - 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 0cec8e98a..d36f18078 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -9,7 +9,6 @@ import 'package:flutter_map/src/geo/crs/crs.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:rxdart/rxdart.dart'; import 'package:tuple/tuple.dart'; import 'layer.dart'; @@ -261,7 +260,7 @@ class _TileLayerState extends State { double _tileZoom; Level _level; StreamSubscription _moveSub; - PublishSubject _throttleUpdate; + StreamController _throttleUpdate; final Map _tiles = {}; final Map _levels = {}; @@ -273,11 +272,17 @@ class _TileLayerState extends State { _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); - _throttleUpdate = options.updateInterval.inMicroseconds == 0 - ? null - : (PublishSubject() - ..throttleTime(options.updateInterval) - ..listen((_) => _update(null))); + if (options.updateInterval.inMicroseconds == 0) { + _throttleUpdate = null; + } else { + _throttleUpdate = StreamController(sync: true); + util + .bindAndCreateThrottleStreamWithTrailingCall( + _throttleUpdate, + options.updateInterval, + ) + .listen(_update); + } } @override @@ -815,6 +820,9 @@ class _TileLayerState extends State { } tile.loaded = DateTime.now(); + if (options.tileFadeInDuration.inMicroseconds == 0) { + tile.active = true; + } setState(() {}); diff --git a/pubspec.yaml b/pubspec.yaml index cddafe22d..a111d4621 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -23,7 +23,6 @@ dependencies: path_provider: ^1.5.1 vector_math: ^2.0.0 proj4dart: ^1.0.4 - rxdart: ^0.23.1 dev_dependencies: pedantic: ^1.8.0 From be5d59c60292171f55451e5654916d71ab1ea9ff Mon Sep 17 00:00:00 2001 From: maRci002 Date: Wed, 1 Apr 2020 01:08:42 +0200 Subject: [PATCH 07/20] create trailing call Throttle (add missing util.dart) --- lib/src/core/util.dart | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/lib/src/core/util.dart b/lib/src/core/util.dart index 0d02b490c..27d4a1b08 100644 --- a/lib/src/core/util.dart +++ b/lib/src/core/util.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:tuple/tuple.dart'; var _templateRe = RegExp(r'\{ *([\w_-]+) *\}'); @@ -22,3 +24,39 @@ double wrapNum(double x, Tuple2 range, [bool includeMax]) { var d = max - min; return x == max && includeMax != null ? x : ((x - min) % d + d) % d + min; } + +Stream bindAndCreateThrottleStreamWithTrailingCall( + StreamController sc, Duration duration) { + Timer timer; + T recentData; + var isClosed = false; + var trailingCall = false; + + return StreamTransformer.fromHandlers( + handleData: (T data, EventSink sink) { + recentData = data; + + if (timer == null) { + if (!trailingCall) { + sink.add(recentData); + + timer = Timer(duration, () { + timer = null; + + if (trailingCall) { + trailingCall = false; + + if (!isClosed) { + sc.add(recentData); + } + } + }); + } + } else { + trailingCall = true; + } + }, handleDone: (EventSink sink) { + isClosed = true; + sink.close(); + }).bind(sc.stream); +} From 31e9ed12f7e5bc1b67f9f747626ce86d98312124 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Wed, 1 Apr 2020 23:32:36 +0200 Subject: [PATCH 08/20] update Throttle stream which is independent of StreamController --- lib/src/core/util.dart | 45 ++++++++++++++++------------------- lib/src/layer/tile_layer.dart | 11 ++++----- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/lib/src/core/util.dart b/lib/src/core/util.dart index 27d4a1b08..004a281b7 100644 --- a/lib/src/core/util.dart +++ b/lib/src/core/util.dart @@ -25,38 +25,35 @@ double wrapNum(double x, Tuple2 range, [bool includeMax]) { return x == max && includeMax != null ? x : ((x - min) % d + d) % d + min; } -Stream bindAndCreateThrottleStreamWithTrailingCall( - StreamController sc, Duration duration) { +StreamTransformer throttleStreamTransformerWithTrailingCall( + Duration duration) { Timer timer; T recentData; - var isClosed = false; var trailingCall = false; - return StreamTransformer.fromHandlers( - handleData: (T data, EventSink sink) { + void Function(T data, EventSink sink) throttleHandler; + throttleHandler = (T data, EventSink sink) { recentData = data; if (timer == null) { - if (!trailingCall) { - sink.add(recentData); - - timer = Timer(duration, () { - timer = null; - - if (trailingCall) { - trailingCall = false; - - if (!isClosed) { - sc.add(recentData); - } - } - }); - } + sink.add(recentData); + timer = Timer(duration, () { + timer = null; + + if (trailingCall) { + trailingCall = false; + throttleHandler(recentData, sink); + } + }); } else { trailingCall = true; } - }, handleDone: (EventSink sink) { - isClosed = true; - sink.close(); - }).bind(sc.stream); + }; + + return StreamTransformer.fromHandlers( + handleData: throttleHandler, + handleDone: (EventSink sink) { + timer?.cancel(); + sink.close(); + }); } diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index d36f18078..1051fde09 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -276,12 +276,11 @@ class _TileLayerState extends State { _throttleUpdate = null; } else { _throttleUpdate = StreamController(sync: true); - util - .bindAndCreateThrottleStreamWithTrailingCall( - _throttleUpdate, - options.updateInterval, - ) - .listen(_update); + _throttleUpdate.stream.transform( + util.throttleStreamTransformerWithTrailingCall( + options.updateInterval, + ), + )..listen(_update); } } From f2ba837359422c88e9d72ac4c73136a80c4086b3 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Thu, 2 Apr 2020 17:14:07 +0200 Subject: [PATCH 09/20] order tiles by zIndex --- lib/src/layer/tile_layer.dart | 44 ++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 16 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 1051fde09..de0399164 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -261,6 +261,7 @@ class _TileLayerState extends State { Level _level; StreamSubscription _moveSub; StreamController _throttleUpdate; + CustomPoint _tileSize; final Map _tiles = {}; final Map _levels = {}; @@ -268,6 +269,8 @@ class _TileLayerState extends State { @override void initState() { super.initState(); + + _tileSize = CustomPoint(options.tileSize, options.tileSize); _resetView(); _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); @@ -296,25 +299,30 @@ class _TileLayerState extends State { @override Widget build(BuildContext context) { - var pixelBounds = _getTiledPixelBounds(map.center); - var tileRange = _pxBoundsToTileRange(pixelBounds); + // var pixelBounds = _getTiledPixelBounds(map.center); + // var tileRange = _pxBoundsToTileRange(pixelBounds); - var tileCenter = tileRange.getCenter(); + // var tileCenter = tileRange.getCenter(); var tilesToRender = [ for (var tile in _tiles.values) // if ((tile.coords.z - _level.zoom).abs() <= 1) tile - if (tile.imageInfo != null) - tile + // if (tile.imageInfo != null) + tile ]; tilesToRender.sort((aTile, bTile) { - final a = aTile.coords; // TODO there was an implicit casting here. + final a = aTile.coords; 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(); + + var zIndexA = _levels[a.z].zIndex; + var zIndexB = _levels[b.z].zIndex; + + if (zIndexA == zIndexB) { + // (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); <-- no matters since Positioned don't care about this, however we use this when sorting tiles to download + return 0; + } else { + return zIndexB.compareTo(zIndexA); } - return (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); }); var tileWidgets = [ @@ -335,7 +343,7 @@ class _TileLayerState extends State { Widget _createTileWidget(Tile tile) { var coords = tile.coords; - var tilePos = _getTilePos(coords); + var tilePos = tile.tilePos; // _getTilePos(coords); var level = _levels[coords.z]; var tileSize = getTileSize(); var pos = (tilePos).multiplyBy(level.scale) + level.translatePoint; @@ -348,7 +356,7 @@ class _TileLayerState extends State { duration: options.tileFadeInDuration, onEnd: tile.onAnimateEnd, child: RawImage( - image: tile.imageInfo.image, + image: tile.imageInfo?.image, fit: BoxFit.fill, ), ); @@ -394,7 +402,7 @@ class _TileLayerState extends State { } CustomPoint getTileSize() { - return CustomPoint(options.tileSize, options.tileSize); + return _tileSize; } bool _hasLevelChildren(double lvl) { @@ -578,7 +586,7 @@ class _TileLayerState extends State { } void _setView(LatLng center, double zoom) { - var tileZoom = _clampZoom(zoom.round().toDouble()); + var tileZoom = _clampZoom(zoom.roundToDouble()); if ((options.maxZoom != null && tileZoom > options.maxZoom)) { // TODO: minZoom ! // tileZoom = null; @@ -658,10 +666,13 @@ class _TileLayerState extends State { void _handleMove() { setState(() { - var zoom = _clampZoom(map.zoom); + var zoom = _clampZoom(map.zoom.roundToDouble()); if ((zoom - _tileZoom).abs() >= 1) { // It was a zoom lvl change + // TODO: remove diagnostic + print('Zoom change: $_tileZoom --> $zoom'); + _setView(map.center, zoom); } else { if (null == _throttleUpdate) { @@ -838,7 +849,8 @@ class _TileLayerState extends State { } CustomPoint _getTilePos(Coords coords) { - return coords.scaleBy(getTileSize()) - _level.origin; + var level = _levels[coords.z]; + return coords.scaleBy(getTileSize()) - level.origin; } Coords _wrapCoords(Coords coords) { From beda7ffdd7f17cb7afdd43814a63cd931b59e773 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Thu, 2 Apr 2020 17:42:17 +0200 Subject: [PATCH 10/20] start fade when Tile image loaded --- lib/src/layer/tile_layer.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index de0399164..ae3da00bb 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -142,7 +142,7 @@ class TileLayerOptions extends LayerOptions { this.wmsOptions = null, this.opacity = 1.0, this.updateInterval = const Duration(milliseconds: 50), - this.tileFadeInDuration = const Duration(milliseconds: 200), + this.tileFadeInDuration = const Duration(milliseconds: 100), rebuild}) : assert(tileFadeInDuration != null && !tileFadeInDuration.isNegative), assert(updateInterval != null && !updateInterval.isNegative), @@ -352,7 +352,7 @@ class _TileLayerState extends State { final Widget content = AnimatedOpacity( key: Key(tile.coordsKey), - opacity: tile.active || tile.fadeStarted ? 1.0 : 0.0, + opacity: tile.fadeStarted ? 1.0 : 0.0, duration: options.tileFadeInDuration, onEnd: tile.onAnimateEnd, child: RawImage( @@ -361,7 +361,9 @@ class _TileLayerState extends State { ), ); - tile.fadeStarted = true; + if (null != tile.imageInfo) { + tile.fadeStarted = true; + } return Positioned( key: Key(tile.coordsKey), From c04eefb97e080fa1de0b64649a020ce0e7685316 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Thu, 2 Apr 2020 19:20:26 +0200 Subject: [PATCH 11/20] create Tile sort without any closure --- lib/src/layer/tile_layer.dart | 55 ++++++++++++++--------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index ae3da00bb..e11343e6b 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -299,31 +299,7 @@ class _TileLayerState extends State { @override Widget build(BuildContext context) { - // var pixelBounds = _getTiledPixelBounds(map.center); - // var tileRange = _pxBoundsToTileRange(pixelBounds); - - // var tileCenter = tileRange.getCenter(); - var tilesToRender = [ - for (var tile in _tiles.values) - // if ((tile.coords.z - _level.zoom).abs() <= 1) tile - // if (tile.imageInfo != null) - tile - ]; - - tilesToRender.sort((aTile, bTile) { - final a = aTile.coords; - final b = bTile.coords; - - var zIndexA = _levels[a.z].zIndex; - var zIndexB = _levels[b.z].zIndex; - - if (zIndexA == zIndexB) { - // (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); <-- no matters since Positioned don't care about this, however we use this when sorting tiles to download - return 0; - } else { - return zIndexB.compareTo(zIndexA); - } - }); + var tilesToRender = _tiles.values.toList()..sort(); var tileWidgets = [ for (var tile in tilesToRender) _createTileWidget(tile) @@ -341,10 +317,8 @@ class _TileLayerState extends State { } Widget _createTileWidget(Tile tile) { - var coords = tile.coords; - - var tilePos = tile.tilePos; // _getTilePos(coords); - var level = _levels[coords.z]; + var tilePos = tile.tilePos; + var level = tile.level; var tileSize = getTileSize(); var pos = (tilePos).multiplyBy(level.scale) + level.translatePoint; var width = tileSize.x * level.scale; @@ -437,6 +411,7 @@ class _TileLayerState extends State { for (var z in toRemove) { _removeTilesAtZoom(z); + _levels.remove(z); } var level = _levels[zoom]; @@ -444,9 +419,7 @@ class _TileLayerState extends State { if (level == null) { level = _levels[zoom] = Level(); - level.zIndex = maxZoom; - level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom) ?? CustomPoint(0.0, 0.0); level.zoom = zoom; @@ -511,7 +484,7 @@ class _TileLayerState extends State { if (entry.value.coords.z != zoom) { continue; } - _removeTile(entry.key); + toRemove.add(entry.key); } for (var key in toRemove) { @@ -814,6 +787,7 @@ class _TileLayerState extends State { coordsKey: tileCoordsToKey, tilePos: _getTilePos(coords), current: true, + level: _levels[coords.z], imageProvider: options.tileProvider.getImage(_wrapCoords(coords), options), tileReady: _tileReady, @@ -888,11 +862,12 @@ class _TileLayerState extends State { typedef void TileReady(Coords coords, dynamic error, Tile tile); -class Tile { +class Tile implements Comparable { final String coordsKey; final Coords coords; final CustomPoint tilePos; final ImageProvider imageProvider; + final Level level; bool current; bool retain; @@ -914,6 +889,7 @@ class Tile { this.tilePos, this.imageProvider, this.tileReady, + this.level, this.current = false, this.active = false, this.retain = false, @@ -951,6 +927,19 @@ class Tile { tileReady(coords, exception, this); } } + + @override + int compareTo(Tile other) { + var zIndexA = level.zIndex; + var zIndexB = other.level.zIndex; + + if (zIndexA == zIndexB) { + // (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); <-- no matters since Positioned don't care about this, however we use this when sorting tiles to download + return 0; + } else { + return zIndexB.compareTo(zIndexA); + } + } } class Level { From 583b5e1fc01d32b512d313d44384b81f16ed0dbf Mon Sep 17 00:00:00 2001 From: maRci002 Date: Thu, 2 Apr 2020 23:46:11 +0200 Subject: [PATCH 12/20] beauty code --- lib/src/layer/tile_layer.dart | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index e11343e6b..195d4624a 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -98,7 +98,7 @@ class TileLayerOptions extends LayerOptions { final int keepBuffer; /// Placeholder to show until tile images are fetched by the provider. - ImageProvider placeholderImage; + final ImageProvider placeholderImage; /// Static informations that should replace placeholders in the [urlTemplate]. /// Applying API keys is a good example on how to use this parameter. @@ -117,10 +117,11 @@ class TileLayerOptions extends LayerOptions { /// ), /// ``` /// - Map additionalOptions; + final Map additionalOptions; // Tiles will not update more than once every `updateInterval` when panning. - // This can save some bandwidth (ie. when fast panning / fast panning) / fps + // This can save some fps and even bandwidth + // (ie. when fast panning / animating between long distances in short time) final Duration updateInterval; final Duration tileFadeInDuration; @@ -473,6 +474,7 @@ class _TileLayerState extends State { } for (var key in toRemove) { + // TODO: remove diagnostic print('_prune: ${_tiles[key].coords}'); _removeTile(key); } @@ -616,12 +618,10 @@ class _TileLayerState extends State { if (_wrapX != null) { var first = (map.project(LatLng(0.0, crs.wrapLng.item1), tileZoom).x / tileSize.x) - .floor() - .toDouble(); + .floorToDouble(); var second = (map.project(LatLng(0.0, crs.wrapLng.item2), tileZoom).x / tileSize.y) - .ceil() - .toDouble(); + .ceilToDouble(); _wrapX = Tuple2(first, second); } @@ -629,24 +629,22 @@ class _TileLayerState extends State { if (_wrapY != null) { var first = (map.project(LatLng(crs.wrapLat.item1, 0.0), tileZoom).y / tileSize.x) - .floor() - .toDouble(); + .floorToDouble(); var second = (map.project(LatLng(crs.wrapLat.item2, 0.0), tileZoom).y / tileSize.y) - .ceil() - .toDouble(); + .ceilToDouble(); _wrapY = Tuple2(first, second); } } void _handleMove() { setState(() { - var zoom = _clampZoom(map.zoom.roundToDouble()); + var zoom = _clampZoom(map.zoom); if ((zoom - _tileZoom).abs() >= 1) { // It was a zoom lvl change // TODO: remove diagnostic - print('Zoom change: $_tileZoom --> $zoom'); + print('Zoom change: $_tileZoom --> ${zoom.roundToDouble()}'); _setView(map.center, zoom); } else { @@ -682,7 +680,7 @@ class _TileLayerState extends State { var tileRange = _pxBoundsToTileRange(pixelBounds); var tileCenter = tileRange.getCenter(); var queue = >[]; - var margin = options.keepBuffer ?? 2; + var margin = options.keepBuffer; var noPruneRange = Bounds( tileRange.bottomLeft - CustomPoint(margin, -margin), tileRange.topRight + CustomPoint(margin, -margin), @@ -846,7 +844,7 @@ class _TileLayerState extends State { var tileSize = getTileSize(); return Bounds( bounds.min.unscaleBy(tileSize).floor(), - bounds.max.unscaleBy(tileSize).ceil() - CustomPoint(1, 1), + bounds.max.unscaleBy(tileSize).ceil() - const CustomPoint(1, 1), ); } From c4dae7db513e15c43fadc2cdf880bfda432cc0f8 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Fri, 3 Apr 2020 00:31:15 +0200 Subject: [PATCH 13/20] support minZoom minNativeZoom / maxNativeZoom --- lib/src/layer/tile_layer.dart | 41 +++++++++++++++++++++++++++++------ 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 195d4624a..3828a1140 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -38,9 +38,25 @@ class TileLayerOptions extends LayerOptions { /// Default is 256 final double tileSize; - /// The max zoom applicable. In most tile providers goes from 0 to 19. + // The minimum zoom level down to which this layer will be + // displayed (inclusive). + final double minZoom; + + /// The maximum zoom level up to which this layer will be + /// displayed (inclusive). + /// In most tile providers goes from 0 to 19. final double maxZoom; + // Minimum zoom number the tile source has available. If it is specified, + // the tiles on all zoom levels lower than minNativeZoom will be loaded + // from minNativeZoom level and auto-scaled. + final double minNativeZoom; + + // Maximum zoom number the tile source has available. If it is specified, + // the tiles on all zoom levels higher than maxNativeZoom will be loaded + // from maxNativeZoom level and auto-scaled. + final double maxNativeZoom; + final bool zoomReverse; final double zoomOffset; @@ -129,7 +145,10 @@ class TileLayerOptions extends LayerOptions { TileLayerOptions( {this.urlTemplate, this.tileSize = 256.0, + this.minZoom = 0.0, this.maxZoom = 18.0, + this.minNativeZoom, + this.maxNativeZoom, this.zoomReverse = false, this.zoomOffset = 0.0, this.additionalOptions = const {}, @@ -142,7 +161,7 @@ class TileLayerOptions extends LayerOptions { // ignore: avoid_init_to_null this.wmsOptions = null, this.opacity = 1.0, - this.updateInterval = const Duration(milliseconds: 50), + this.updateInterval = const Duration(milliseconds: 150), this.tileFadeInDuration = const Duration(milliseconds: 100), rebuild}) : assert(tileFadeInDuration != null && !tileFadeInDuration.isNegative), @@ -437,8 +456,8 @@ class _TileLayerState extends State { } var zoom = map.zoom; - if (zoom > options.maxZoom) { - // TODO: min! + if (zoom > options.maxZoom || zoom < options.minZoom) { + // TODO: _removeAllTiles ?? // _removeAllTiles(); return; } @@ -558,14 +577,22 @@ class _TileLayerState extends State { } double _clampZoom(double zoom) { - // TODO + if (null != options.minNativeZoom && zoom < options.minNativeZoom) { + return options.minNativeZoom; + } + + if (null != options.maxNativeZoom && options.maxNativeZoom < zoom) { + return options.maxNativeZoom; + } + return zoom; } void _setView(LatLng center, double zoom) { var tileZoom = _clampZoom(zoom.roundToDouble()); - if ((options.maxZoom != null && tileZoom > options.maxZoom)) { - // TODO: minZoom ! + if ((options.maxZoom != null && tileZoom > options.maxZoom) || + (options.minZoom != null && tileZoom < options.minZoom)) { + // TODO: tileZoom = null ?? // tileZoom = null; } From 0293555bd3d317efd88bc074456dfb723974be48 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 12:11:24 +0200 Subject: [PATCH 14/20] remove Tiles when tileZoom is out of zoom limits and restore them when tileZoom is within limits --- lib/src/layer/tile_layer.dart | 69 +++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 20 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 3828a1140..80e1f4661 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -360,7 +360,7 @@ class _TileLayerState extends State { } return Positioned( - key: Key(tile.coordsKey), + key: ValueKey(tile.coordsKey), left: pos.x.toDouble(), top: pos.y.toDouble(), width: width.toDouble(), @@ -455,10 +455,16 @@ class _TileLayerState extends State { return; } - var zoom = map.zoom; - if (zoom > options.maxZoom || zoom < options.minZoom) { - // TODO: _removeAllTiles ?? - // _removeAllTiles(); + var zoom = _tileZoom; + if (zoom == null) { + // TODO: remove diagnostic + print('_removeAllTiles'); + + // TODO: this is fair enough + // however if MapOptions has the same maxZoom as this TileLayer then + // this shouldn't happen (fix at MapOptions! -- double tap ok but two finger scale need some fix) + // TODO: MapOptions need minZoom too + _removeAllTiles(); return; } @@ -592,8 +598,14 @@ class _TileLayerState extends State { var tileZoom = _clampZoom(zoom.roundToDouble()); if ((options.maxZoom != null && tileZoom > options.maxZoom) || (options.minZoom != null && tileZoom < options.minZoom)) { - // TODO: tileZoom = null ?? - // tileZoom = null; + // TODO: this is fair enough + // however if MapOptions has the same maxZoom as this TileLayer then + // this shouldn't happen (fix at MapOptions! -- double tap ok but two finger scale need some fix) + // TODO: MapOptions need minZoom too + tileZoom = null; + + // TODO: remove diagnostic + print('Zoom become null'); } _tileZoom = tileZoom; @@ -665,25 +677,42 @@ class _TileLayerState extends State { } void _handleMove() { - setState(() { - var zoom = _clampZoom(map.zoom); + var tileZoom = _clampZoom(map.zoom.roundToDouble()); + + if (_tileZoom == null) { + // if there is no _tileZoom available it means we are out within zoom level + // we will restory fully via _setView call if we are back on trail + if ((options.maxZoom != null && tileZoom <= options.maxZoom) && + (options.minZoom != null && tileZoom >= options.minZoom)) { + _tileZoom = tileZoom; - if ((zoom - _tileZoom).abs() >= 1) { // It was a zoom lvl change // TODO: remove diagnostic - print('Zoom change: $_tileZoom --> ${zoom.roundToDouble()}'); + print('Zoom restored from null to $tileZoom'); - _setView(map.center, zoom); - } else { - if (null == _throttleUpdate) { - _update(null); + setState(() { + _setView(map.center, tileZoom); + }); + } + } else { + setState(() { + if ((tileZoom - _tileZoom).abs() >= 1) { + // It was a zoom lvl change + // TODO: remove diagnostic + print('Zoom change: $_tileZoom --> ${tileZoom}'); + + _setView(map.center, tileZoom); } else { - _throttleUpdate.add(null); - } + if (null == _throttleUpdate) { + _update(null); + } else { + _throttleUpdate.add(null); + } - _setZoomTransforms(map.center, map.zoom); - } - }); + _setZoomTransforms(map.center, map.zoom); + } + }); + } } Bounds _getTiledPixelBounds(LatLng center) { From dde78a4a1727bb9d659b59dac442ced113c8c1c9 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 13:55:34 +0200 Subject: [PATCH 15/20] fix -- Zooming out very far causes null pointers #59 --- lib/src/gestures/gestures.dart | 7 +++++-- lib/src/layer/tile_layer.dart | 12 ++---------- lib/src/map/map.dart | 7 +++---- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/lib/src/gestures/gestures.dart b/lib/src/gestures/gestures.dart index 58de3f3fc..56146d2a0 100644 --- a/lib/src/gestures/gestures.dart +++ b/lib/src/gestures/gestures.dart @@ -236,8 +236,11 @@ abstract class MapGestureMixin extends State return Offset(point.x.toDouble(), point.y.toDouble()); } - double _getZoomForScale(double startZoom, double scale) => - startZoom + math.log(scale) / math.ln2; + double _getZoomForScale(double startZoom, double scale) { + var resultZoom = startZoom + math.log(scale) / math.ln2; + + return map.fitZoomToBounds(resultZoom); + } @override void dispose() { diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 80e1f4661..a6e1aaf7a 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -460,10 +460,6 @@ class _TileLayerState extends State { // TODO: remove diagnostic print('_removeAllTiles'); - // TODO: this is fair enough - // however if MapOptions has the same maxZoom as this TileLayer then - // this shouldn't happen (fix at MapOptions! -- double tap ok but two finger scale need some fix) - // TODO: MapOptions need minZoom too _removeAllTiles(); return; } @@ -598,14 +594,10 @@ class _TileLayerState extends State { var tileZoom = _clampZoom(zoom.roundToDouble()); if ((options.maxZoom != null && tileZoom > options.maxZoom) || (options.minZoom != null && tileZoom < options.minZoom)) { - // TODO: this is fair enough - // however if MapOptions has the same maxZoom as this TileLayer then - // this shouldn't happen (fix at MapOptions! -- double tap ok but two finger scale need some fix) - // TODO: MapOptions need minZoom too - tileZoom = null; - // TODO: remove diagnostic print('Zoom become null'); + + tileZoom = null; } _tileZoom = tileZoom; diff --git a/lib/src/map/map.dart b/lib/src/map/map.dart index c486bb318..c09932bd3 100644 --- a/lib/src/map/map.dart +++ b/lib/src/map/map.dart @@ -1,14 +1,13 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/core/bounds.dart'; import 'package:flutter_map/src/core/center_zoom.dart'; import 'package:flutter_map/src/core/point.dart'; import 'package:latlong/latlong.dart'; -import 'package:flutter/material.dart'; - class MapControllerImpl implements MapController { final Completer _readyCompleter = Completer(); MapState _state; @@ -109,7 +108,7 @@ class MapState { } void move(LatLng center, double zoom, {hasGesture = false}) { - zoom = _fitZoomToBounds(zoom); + zoom = fitZoomToBounds(zoom); final mapMoved = center != _lastCenter || zoom != _zoom; if (_lastCenter != null && @@ -135,7 +134,7 @@ class MapState { } } - double _fitZoomToBounds(double zoom) { + double fitZoomToBounds(double zoom) { zoom ??= _zoom; // Abide to min/max zoom if (options.maxZoom != null) { From 19380d1c720afd14c1fc92a8d2a7ac6e8f95d32f Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 18:03:47 +0200 Subject: [PATCH 16/20] support zoomReverse / zoomOffset --- lib/src/layer/tile_layer.dart | 3 +++ .../layer/tile_provider/tile_provider.dart | 20 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index a6e1aaf7a..96b503739 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -57,7 +57,10 @@ class TileLayerOptions extends LayerOptions { // from maxNativeZoom level and auto-scaled. final double maxNativeZoom; + // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`) final bool zoomReverse; + + // The zoom number used in tile URLs will be offset with this value. final double zoomOffset; /// List of subdomains for the URL. diff --git a/lib/src/layer/tile_provider/tile_provider.dart b/lib/src/layer/tile_provider/tile_provider.dart index 01c775e5b..d66b03dd6 100644 --- a/lib/src/layer/tile_provider/tile_provider.dart +++ b/lib/src/layer/tile_provider/tile_provider.dart @@ -16,22 +16,36 @@ abstract class TileProvider { void dispose() {} String getTileUrl(Coords coords, TileLayerOptions options) { - if (options.wmsOptions != null) + if (options.wmsOptions != null) { return options.wmsOptions.getUrl(coords, options.tileSize.toInt()); + } + + var z = _getZoomForUrl(coords, options); + var data = { 'x': coords.x.round().toString(), 'y': coords.y.round().toString(), - 'z': coords.z.round().toString(), + 'z': z.round().toString(), 's': getSubdomain(coords, options) }; if (options.tms) { - data['y'] = invertY(coords.y.round(), coords.z.round()).toString(); + data['y'] = invertY(coords.y.round(), z.round()).toString(); } var allOpts = Map.from(data) ..addAll(options.additionalOptions); return util.template(options.urlTemplate, allOpts); } + double _getZoomForUrl(Coords coords, TileLayerOptions options) { + var zoom = coords.z; + + if (options.zoomReverse) { + zoom = options.maxZoom - zoom; + } + + return zoom += options.zoomOffset; + } + int invertY(int y, int z) { return ((1 << z) - 1) - y; } From 24ba1f9bac369dc941915a201b2e86f89fbed76f Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 20:54:09 +0200 Subject: [PATCH 17/20] support error tile image / improve tile fade in (remove hacky solution) --- lib/src/layer/tile_layer.dart | 132 ++++++++++++++++++++++++++++------ 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 96b503739..e4d4286be 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -119,6 +119,9 @@ class TileLayerOptions extends LayerOptions { /// Placeholder to show until tile images are fetched by the provider. final ImageProvider placeholderImage; + /// Tile image to show in place of the tile that failed to load. + final ImageProvider errorImage; + /// Static informations that should replace placeholders in the [urlTemplate]. /// Applying API keys is a good example on how to use this parameter. /// @@ -159,6 +162,7 @@ class TileLayerOptions extends LayerOptions { this.keepBuffer = 2, this.backgroundColor = const Color(0xFFE0E0E0), this.placeholderImage, + this.errorImage, this.tileProvider = const CachedNetworkTileProvider(), this.tms = false, // ignore: avoid_init_to_null @@ -273,7 +277,7 @@ class TileLayer extends StatefulWidget { } } -class _TileLayerState extends State { +class _TileLayerState extends State with TickerProviderStateMixin { MapState get map => widget.mapState; TileLayerOptions get options => widget.options; @@ -347,21 +351,11 @@ class _TileLayerState extends State { var width = tileSize.x * level.scale; var height = tileSize.y * level.scale; - final Widget content = AnimatedOpacity( - key: Key(tile.coordsKey), - opacity: tile.fadeStarted ? 1.0 : 0.0, - duration: options.tileFadeInDuration, - onEnd: tile.onAnimateEnd, - child: RawImage( - image: tile.imageInfo?.image, - fit: BoxFit.fill, - ), + final Widget content = AnimatedTile( + tile: tile, + errorImage: options.errorImage, ); - if (null != tile.imageInfo) { - tile.fadeStarted = true; - } - return Positioned( key: ValueKey(tile.coordsKey), left: pos.x.toDouble(), @@ -846,6 +840,8 @@ class _TileLayerState extends State { void _tileReady(Coords coords, dynamic error, Tile tile) { if (null != error) { print(error); + + tile.loadError = true; } var key = _tileCoordsToKey(coords); @@ -855,8 +851,11 @@ class _TileLayerState extends State { } tile.loaded = DateTime.now(); - if (options.tileFadeInDuration.inMicroseconds == 0) { + if (options.tileFadeInDuration.inMicroseconds == 0 || + (tile.loadError && null == options.errorImage)) { tile.active = true; + } else { + tile.startFadeInAnimation(options.tileFadeInDuration, this); } setState(() {}); @@ -921,15 +920,19 @@ class Tile implements Comparable { bool current; bool retain; bool active; + bool loadError; DateTime loaded; - bool fadeStarted = false; + AnimationController animationController; + double get opacity => animationController == null + ? (active ? 1.0 : 0.0) + : animationController.value; // callback when tile is ready / error occurred // it maybe be null forinstance when download aborted TileReady tileReady; ImageInfo imageInfo; - ImageStream _stream; + ImageStream _imageStream; ImageStreamListener _listener; Tile({ @@ -942,10 +945,11 @@ class Tile implements Comparable { this.current = false, this.active = false, this.retain = false, + this.loadError = false, }) { - _stream = imageProvider.resolve(ImageConfiguration()); + _imageStream = imageProvider.resolve(ImageConfiguration()); _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); - _stream.addListener(_listener); + _imageStream.addListener(_listener); } // call this before GC! @@ -957,11 +961,21 @@ class Tile implements Comparable { .catchError((error) => print('evict tile: $coords -> $error')); } - _stream?.removeListener(_listener); + animationController?.removeStatusListener(_onAnimateEnd); + _imageStream?.removeListener(_listener); } - void onAnimateEnd() { - active = true; + void startFadeInAnimation(Duration duration, TickerProvider vsync) { + animationController = AnimationController(duration: duration, vsync: vsync) + ..addStatusListener(_onAnimateEnd); + + animationController.forward(); + } + + void _onAnimateEnd(AnimationStatus status) { + if (status == AnimationStatus.completed) { + active = true; + } } void _tileOnLoad(ImageInfo imageInfo, bool synchronousCall) { @@ -983,12 +997,84 @@ class Tile implements Comparable { var zIndexB = other.level.zIndex; if (zIndexA == zIndexB) { - // (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt(); <-- no matters since Positioned don't care about this, however we use this when sorting tiles to download return 0; } else { return zIndexB.compareTo(zIndexA); } } + + @override + int get hashCode => coords.hashCode; + + @override + bool operator ==(other) { + return other is Tile && coords == other.coords; + } +} + +class AnimatedTile extends StatefulWidget { + final Tile tile; + final ImageProvider errorImage; + + AnimatedTile({Key key, this.tile, this.errorImage}) + : assert(null != tile), + super(key: key); + + @override + _AnimatedTileState createState() => _AnimatedTileState(); +} + +class _AnimatedTileState extends State { + bool listenerAttached = false; + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: widget.tile.opacity, + child: (widget.tile.loadError && widget.errorImage != null) + ? Image( + image: widget.errorImage, + fit: BoxFit.fill, + ) + : RawImage( + image: widget.tile.imageInfo?.image, + fit: BoxFit.fill, + ), + ); + } + + @override + void initState() { + super.initState(); + + if (null != widget.tile.animationController) { + widget.tile.animationController.addListener(_handleChange); + listenerAttached = true; + } + } + + @override + void dispose() { + if (listenerAttached) { + widget.tile.animationController?.removeListener(_handleChange); + } + + super.dispose(); + } + + @override + void didUpdateWidget(AnimatedTile oldWidget) { + super.didUpdateWidget(oldWidget); + + if (!listenerAttached && null != widget.tile.animationController) { + widget.tile.animationController.addListener(_handleChange); + listenerAttached = true; + } + } + + void _handleChange() { + setState(() {}); + } } class Level { From 89ae17f07f07f2e5aef4ab31b822a8a5479fa1e7 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 22:17:03 +0200 Subject: [PATCH 18/20] use direct milliseconds instead of Duration to avoid microseconds --- lib/src/layer/tile_layer.dart | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index e4d4286be..ce99a2fdb 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -141,11 +141,15 @@ class TileLayerOptions extends LayerOptions { /// final Map additionalOptions; - // Tiles will not update more than once every `updateInterval` when panning. + // Tiles will not update more than once every `updateInterval` milliseconds + // (default 200) when panning. + // It can be 0 (but it will calculating for loading tiles every frame when panning / zooming, flutter is fast) // This can save some fps and even bandwidth // (ie. when fast panning / animating between long distances in short time) final Duration updateInterval; + // Tiles fade in duration in milliseconds (default 100), + // it can 0 to avoid fade in final Duration tileFadeInDuration; TileLayerOptions( @@ -168,11 +172,21 @@ class TileLayerOptions extends LayerOptions { // ignore: avoid_init_to_null this.wmsOptions = null, this.opacity = 1.0, - this.updateInterval = const Duration(milliseconds: 150), - this.tileFadeInDuration = const Duration(milliseconds: 100), + // Tiles will not update more than once every `updateInterval` milliseconds + // (default 200) when panning. + // It can be 0 (but it will calculating for loading tiles every frame when panning / zooming, flutter is fast) + // This can save some fps and even bandwidth + // (ie. when fast panning / animating between long distances in short time) + int updateInterval = 200, + // Tiles fade in duration in milliseconds (default 100), + // it can 0 to avoid fade in + int tileFadeInDuration = 100, rebuild}) - : assert(tileFadeInDuration != null && !tileFadeInDuration.isNegative), - assert(updateInterval != null && !updateInterval.isNegative), + : updateInterval = + updateInterval <= 0 ? null : Duration(milliseconds: updateInterval), + tileFadeInDuration = tileFadeInDuration <= 0 + ? null + : Duration(milliseconds: tileFadeInDuration), super(rebuild: rebuild); } @@ -302,7 +316,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); - if (options.updateInterval.inMicroseconds == 0) { + if (options.updateInterval == null) { _throttleUpdate = null; } else { _throttleUpdate = StreamController(sync: true); @@ -851,7 +865,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } tile.loaded = DateTime.now(); - if (options.tileFadeInDuration.inMicroseconds == 0 || + if (options.tileFadeInDuration == null || (tile.loadError && null == options.errorImage)) { tile.active = true; } else { @@ -864,7 +878,9 @@ class _TileLayerState extends State with TickerProviderStateMixin { // Wait a bit more than tileFadeInDuration (the duration of the tile fade-in) // to trigger a pruning. Future.delayed( - options.tileFadeInDuration + const Duration(milliseconds: 50), + options.tileFadeInDuration != null + ? options.tileFadeInDuration + const Duration(milliseconds: 50) + : const Duration(milliseconds: 50), () { setState(_pruneTiles); }, From 70faa49bfbe50b64c423289db618fbf32f535ff4 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 23:07:46 +0200 Subject: [PATCH 19/20] fix -- Exception caught by image resource service - not handled. #444 --- lib/src/layer/tile_layer.dart | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index ce99a2fdb..f9d3486cc 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -963,9 +963,14 @@ class Tile implements Comparable { this.retain = false, this.loadError = false, }) { - _imageStream = imageProvider.resolve(ImageConfiguration()); - _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); - _imageStream.addListener(_listener); + try { + _imageStream = imageProvider.resolve(ImageConfiguration()); + _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); + _imageStream.addListener(_listener); + } catch (e, s) { + // make sure all exception is handled - #444 + _tileOnError(e, s); + } } // call this before GC! From 380d650948361f67addade76f89577185d52e5c6 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 4 Apr 2020 23:17:02 +0200 Subject: [PATCH 20/20] remove diagnostic prints --- lib/src/layer/tile_layer.dart | 38 +---------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index f9d3486cc..a5be4bfa4 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -392,15 +392,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { } } - // TODO: remove diagnostic - if (toRemove.isNotEmpty) { - print('_prune+abort ---------------------'); - } - for (var key in toRemove) { var tile = _tiles[key]; - // TODO: remove diagnostic - print('_prune+abort: ${tile.coords}'); tile.tileReady = null; tile.dispose(); @@ -468,9 +461,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { var zoom = _tileZoom; if (zoom == null) { - // TODO: remove diagnostic - print('_removeAllTiles'); - _removeAllTiles(); return; } @@ -500,14 +490,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } } - // TODO: remove diagnostic - if (toRemove.isNotEmpty) { - print('_prune ---------------------'); - } - for (var key in toRemove) { - // TODO: remove diagnostic - print('_prune: ${_tiles[key].coords}'); _removeTile(key); } } @@ -605,9 +588,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { var tileZoom = _clampZoom(zoom.roundToDouble()); if ((options.maxZoom != null && tileZoom > options.maxZoom) || (options.minZoom != null && tileZoom < options.minZoom)) { - // TODO: remove diagnostic - print('Zoom become null'); - tileZoom = null; } @@ -688,11 +668,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { if ((options.maxZoom != null && tileZoom <= options.maxZoom) && (options.minZoom != null && tileZoom >= options.minZoom)) { _tileZoom = tileZoom; - - // It was a zoom lvl change - // TODO: remove diagnostic - print('Zoom restored from null to $tileZoom'); - setState(() { _setView(map.center, tileZoom); }); @@ -701,9 +676,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { setState(() { if ((tileZoom - _tileZoom).abs() >= 1) { // It was a zoom lvl change - // TODO: remove diagnostic - print('Zoom change: $_tileZoom --> ${tileZoom}'); - _setView(map.center, tileZoom); } else { if (null == _throttleUpdate) { @@ -785,11 +757,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { queue.sort((a, b) => (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt()); - // TODO: remove diagnostic - if (queue.isNotEmpty) { - print('_addTile ---------------------'); - } - for (var i = 0; i < queue.length; i++) { _addTile(queue[i]); } @@ -835,9 +802,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { } void _addTile(Coords coords) { - // TODO: remove diagnostic - print('_fetch tile: $coords'); - var tileCoordsToKey = _tileCoordsToKey(coords); _tiles[tileCoordsToKey] = Tile( coords: coords, @@ -968,7 +932,7 @@ class Tile implements Comparable { _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); _imageStream.addListener(_listener); } catch (e, s) { - // make sure all exception is handled - #444 + // make sure all exception is handled - #444 / #536 _tileOnError(e, s); } }