From 646912e30075032473b3fce6aea5ab6f2d8b1359 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Sat, 11 Apr 2020 19:31:18 +0200 Subject: [PATCH 1/5] Observe TileLayerOptions changes --- lib/src/layer/tile_layer.dart | 40 +++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 0b0aaaed9..3c8b2a579 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -317,6 +317,42 @@ class _TileLayerState extends State with TickerProviderStateMixin { _update(null); _moveSub = widget.stream.listen((_) => _handleMove()); + _initThrottleUpdate(); + } + + @override + void didUpdateWidget(TileLayer oldWidget) { + super.didUpdateWidget(oldWidget); + var reloadTiles = false; + + final oldUrl = oldWidget.options.urlTemplate ?? + oldWidget.options?.wmsOptions?._encodedBaseUrl; + final newUrl = options.urlTemplate ?? options?.wmsOptions?._encodedBaseUrl; + if (oldUrl != newUrl) { + // URL has been changed drop all Tiles and reload them + reloadTiles = true; + } + + if (oldWidget.options.tileSize != options.tileSize) { + // tileSize has been changed drop all Tiles and reload them + _tileSize = CustomPoint(options.tileSize, options.tileSize); + reloadTiles = true; + } + + if (oldWidget.options.updateInterval != options.updateInterval) { + // updateInterval has been changed + _throttleUpdate?.close(); + _initThrottleUpdate(); + } + + if (reloadTiles) { + _removeAllTiles(); + _resetView(); + _update(null); + } + } + + void _initThrottleUpdate() { if (options.updateInterval == null) { _throttleUpdate = null; } else { @@ -331,12 +367,12 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override void dispose() { - super.dispose(); - _removeAllTiles(); _moveSub?.cancel(); options.tileProvider.dispose(); _throttleUpdate?.close(); + + super.dispose(); } @override From 16c43dcf1eb85c8fa347e8427647ffe142004b65 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Mon, 13 Apr 2020 20:05:06 +0200 Subject: [PATCH 2/5] Support retina mode --- lib/src/layer/tile_layer.dart | 132 +++++++++++------- .../layer/tile_provider/tile_provider.dart | 6 +- 2 files changed, 85 insertions(+), 53 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 3c8b2a579..6fa116bac 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -47,20 +48,20 @@ class TileLayerOptions extends LayerOptions { /// 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. + /// 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. + /// 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; - // If set to true, the zoom number used in tile URLs will be reversed (`maxZoom - zoom` instead of `zoom`) + /// 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. + /// The zoom number used in tile URLs will be offset with this value. final double zoomOffset; /// List of subdomains for the URL. @@ -80,10 +81,10 @@ class TileLayerOptions extends LayerOptions { /// https://c.tile.openstreetmap.org/{z}/{x}/{y}.png final List subdomains; - ///Color shown behind the tiles. + /// Color shown behind the tiles. final Color backgroundColor; - ///Opacity of the rendered tile + /// Opacity of the rendered tile final double opacity; /// Provider to load the tiles. The default is CachedNetworkTileProvider, @@ -141,52 +142,75 @@ class TileLayerOptions extends LayerOptions { /// final Map additionalOptions; - // 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) + /// 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 + /// Tiles fade in duration in milliseconds (default 100), + /// Use 0 to avoid fade in final Duration tileFadeInDuration; - 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 {}, - this.subdomains = const [], - this.keepBuffer = 2, - this.backgroundColor = const Color(0xFFE0E0E0), - this.placeholderImage, - this.errorImage, - this.tileProvider = const CachedNetworkTileProvider(), - this.tms = false, - // ignore: avoid_init_to_null - this.wmsOptions = null, - this.opacity = 1.0, - // 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}) - : updateInterval = + /// If `true`, it will request four tiles of half the specified size and a + /// bigger zoom level in place of one to utilize the high resolution. + /// It is advised to use retinaMode if display supports it, write code like this: + /// TileLayerOptions( + /// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0, + /// ), + final bool retinaMode; + + TileLayerOptions({ + this.urlTemplate, + double tileSize = 256.0, + double minZoom = 0.0, + double maxZoom = 18.0, + this.minNativeZoom, + this.maxNativeZoom, + this.zoomReverse = false, + double zoomOffset = 0.0, + this.additionalOptions = const {}, + this.subdomains = const [], + this.keepBuffer = 2, + this.backgroundColor = const Color(0xFFE0E0E0), + this.placeholderImage, + this.errorImage, + this.tileProvider = const CachedNetworkTileProvider(), + this.tms = false, + // ignore: avoid_init_to_null + this.wmsOptions = null, + this.opacity = 1.0, + // 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, + this.retinaMode = false, + rebuild, + }) : updateInterval = updateInterval <= 0 ? null : Duration(milliseconds: updateInterval), tileFadeInDuration = tileFadeInDuration <= 0 ? null : Duration(milliseconds: tileFadeInDuration), + maxZoom = + wmsOptions == null && retinaMode && maxZoom > 0.0 && !zoomReverse + ? maxZoom - 1.0 + : maxZoom, + minZoom = + wmsOptions == null && retinaMode && maxZoom > 0.0 && zoomReverse + ? math.max(minZoom + 1.0, 0.0) + : minZoom, + zoomOffset = wmsOptions == null && retinaMode && maxZoom > 0.0 + ? (zoomReverse ? zoomOffset - 1.0 : zoomOffset + 1.0) + : zoomOffset, + tileSize = wmsOptions == null && retinaMode && maxZoom > 0.0 + ? (tileSize / 2.0).floorToDouble() + : tileSize, super(rebuild: rebuild); } @@ -253,7 +277,7 @@ class WMSTileLayerOptions { return buffer.toString(); } - String getUrl(Coords coords, int tileSize) { + String getUrl(Coords coords, int tileSize, bool retinaMode) { final tileSizePoint = CustomPoint(tileSize, tileSize); final nvPoint = coords.scaleBy(tileSizePoint); final sePoint = nvPoint + tileSizePoint; @@ -267,8 +291,8 @@ class WMSTileLayerOptions { : [bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y]; final buffer = StringBuffer(_encodedBaseUrl); - buffer.write('&width=$tileSize'); - buffer.write('&height=$tileSize'); + buffer.write('&width=${retinaMode ? tileSize * 2 : tileSize}'); + buffer.write('&height=${retinaMode ? tileSize * 2 : tileSize}'); buffer.write('&bbox=${bbox.join(',')}'); return buffer.toString(); } @@ -345,6 +369,12 @@ class _TileLayerState extends State with TickerProviderStateMixin { _initThrottleUpdate(); } + if (oldWidget.options.retinaMode != options.retinaMode) { + // retinaMode has been changed drop all Tiles and reload them. + // Note: tileSize is changed too so reloadTiles should be `true` at this point + reloadTiles = true; + } + if (reloadTiles) { _removeAllTiles(); _resetView(); diff --git a/lib/src/layer/tile_provider/tile_provider.dart b/lib/src/layer/tile_provider/tile_provider.dart index d66b03dd6..01912066e 100644 --- a/lib/src/layer/tile_provider/tile_provider.dart +++ b/lib/src/layer/tile_provider/tile_provider.dart @@ -17,7 +17,8 @@ abstract class TileProvider { String getTileUrl(Coords coords, TileLayerOptions options) { if (options.wmsOptions != null) { - return options.wmsOptions.getUrl(coords, options.tileSize.toInt()); + return options.wmsOptions + .getUrl(coords, options.tileSize.toInt(), options.retinaMode); } var z = _getZoomForUrl(coords, options); @@ -26,7 +27,8 @@ abstract class TileProvider { 'x': coords.x.round().toString(), 'y': coords.y.round().toString(), 'z': z.round().toString(), - 's': getSubdomain(coords, options) + 's': getSubdomain(coords, options), + // 'r': options.retinaMode ? '@2x' : '', }; if (options.tms) { data['y'] = invertY(coords.y.round(), z.round()).toString(); From 0f4f255a3d79ef8c0b9b38442ba3bcb1ee38a357 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Tue, 14 Apr 2020 19:10:38 +0200 Subject: [PATCH 3/5] Update doc --- lib/src/layer/tile_layer.dart | 12 +++++++++++- lib/src/layer/tile_provider/tile_provider.dart | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 6fa116bac..154bb3a15 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -18,6 +18,10 @@ import 'layer.dart'; /// A tile is an image binded to a specific geographical position. class TileLayerOptions extends LayerOptions { /// Defines the structure to create the URLs for the tiles. + /// `{s}` means one of the available subdomains (can be omitted) + /// `{z}` zoom level + /// `{x}` and `{y}` — tile coordinates + /// `{r}` can be used to add "@2x" to the URL to load retina tiles (can be omitted) /// /// Example: /// @@ -132,7 +136,7 @@ class TileLayerOptions extends LayerOptions { /// /// TileLayerOptions( /// urlTemplate: "https://api.tiles.mapbox.com/v4/" - /// "{id}/{z}/{x}/{y}@2x.png?access_token={accessToken}", + /// "{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}", /// additionalOptions: { /// 'accessToken': '', /// 'id': 'mapbox.streets', @@ -155,6 +159,12 @@ class TileLayerOptions extends LayerOptions { /// If `true`, it will request four tiles of half the specified size and a /// bigger zoom level in place of one to utilize the high resolution. + /// + /// If `true` then MapOptions's `maxZoom` should be `maxZoom - 1` since retinaMode + /// just simulates retina display by playing with `zoomOffset`. + /// If geoserver supports retina `@2` tiles then it it advised to use them + /// instead of simulating it (use {r} in the [urlTemplate]) + /// /// It is advised to use retinaMode if display supports it, write code like this: /// TileLayerOptions( /// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0, diff --git a/lib/src/layer/tile_provider/tile_provider.dart b/lib/src/layer/tile_provider/tile_provider.dart index 01912066e..39d349b29 100644 --- a/lib/src/layer/tile_provider/tile_provider.dart +++ b/lib/src/layer/tile_provider/tile_provider.dart @@ -28,7 +28,7 @@ abstract class TileProvider { 'y': coords.y.round().toString(), 'z': z.round().toString(), 's': getSubdomain(coords, options), - // 'r': options.retinaMode ? '@2x' : '', + 'r': '@2x', }; if (options.tms) { data['y'] = invertY(coords.y.round(), z.round()).toString(); From c590ab12edbf5b6fce30b13a42e974c0abadd506 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Mon, 4 May 2020 23:39:01 +0200 Subject: [PATCH 4/5] Override old Tile when new one is avaible --- lib/src/layer/tile_layer.dart | 96 ++++++++++++++++++++++++++--------- 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 3c8b2a579..8d4e5e09e 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -141,17 +141,30 @@ class TileLayerOptions extends LayerOptions { /// final Map additionalOptions; - // 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) + /// Tiles will not update more than once every `updateInterval` + /// (default 200 milliseconds) when panning. + /// It can be null (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 + /// Tiles fade in duration in milliseconds (default 100), + /// it can be null to avoid fade in final Duration tileFadeInDuration; + /// Opacity start value when Tile starts fade in (0.0 - 1.0) + /// Takes effect if `tileFadeInDuration` is not null + final double tileFadeInStart; + + /// Opacity start value when an exists Tile starts fade in with different Url (0.0 - 1.0) + /// Takes effect when `tileFadeInDuration` is not null and if `overrideTilesWhenUrlChanges` if true + final double tileFadeInStartWhenOverride; + + /// `false`: current Tiles will be first dropped and then reload via new url (default) + /// `true`: current Tiles will be visible until new ones aren't loaded (new Tiles are loaded independently) + /// @see https://github.com/johnpryan/flutter_map/issues/583 + final bool overrideTilesWhenUrlChanges; + TileLayerOptions( {this.urlTemplate, this.tileSize = 256.0, @@ -181,12 +194,18 @@ class TileLayerOptions extends LayerOptions { // Tiles fade in duration in milliseconds (default 100), // it can 0 to avoid fade in int tileFadeInDuration = 100, + this.tileFadeInStart = 0.0, + this.tileFadeInStartWhenOverride = 0.0, + this.overrideTilesWhenUrlChanges = false, rebuild}) : updateInterval = updateInterval <= 0 ? null : Duration(milliseconds: updateInterval), tileFadeInDuration = tileFadeInDuration <= 0 ? null : Duration(milliseconds: tileFadeInDuration), + assert(tileFadeInStart >= 0.0 && tileFadeInStart <= 1.0), + assert(tileFadeInStartWhenOverride >= 0.0 && + tileFadeInStartWhenOverride <= 1.0), super(rebuild: rebuild); } @@ -325,26 +344,33 @@ class _TileLayerState extends State with TickerProviderStateMixin { super.didUpdateWidget(oldWidget); var reloadTiles = false; - final oldUrl = oldWidget.options.urlTemplate ?? - oldWidget.options?.wmsOptions?._encodedBaseUrl; - final newUrl = options.urlTemplate ?? options?.wmsOptions?._encodedBaseUrl; - if (oldUrl != newUrl) { - // URL has been changed drop all Tiles and reload them - reloadTiles = true; - } - if (oldWidget.options.tileSize != options.tileSize) { - // tileSize has been changed drop all Tiles and reload them _tileSize = CustomPoint(options.tileSize, options.tileSize); reloadTiles = true; } if (oldWidget.options.updateInterval != options.updateInterval) { - // updateInterval has been changed _throttleUpdate?.close(); _initThrottleUpdate(); } + if (!reloadTiles) { + final oldUrl = oldWidget.options.wmsOptions?._encodedBaseUrl ?? + oldWidget.options.urlTemplate; + final newUrl = options.wmsOptions?._encodedBaseUrl ?? options.urlTemplate; + if (oldUrl != newUrl) { + if (options.overrideTilesWhenUrlChanges) { + for (var tile in _tiles.values) { + tile.imageProvider = options.tileProvider + .getImage(_wrapCoords(tile.coords), options); + tile.loadTileImage(); + } + } else { + reloadTiles = true; + } + } + } + if (reloadTiles) { _removeAllTiles(); _resetView(); @@ -866,12 +892,20 @@ class _TileLayerState extends State with TickerProviderStateMixin { return; } + var fadeInStart = tile.loaded == null + ? options.tileFadeInStart + : options.tileFadeInStartWhenOverride; tile.loaded = DateTime.now(); if (options.tileFadeInDuration == null || + fadeInStart == 1.0 || (tile.loadError && null == options.errorImage)) { tile.active = true; } else { - tile.startFadeInAnimation(options.tileFadeInDuration, this); + tile.startFadeInAnimation( + options.tileFadeInDuration, + this, + from: fadeInStart, + ); } setState(() {}); @@ -932,7 +966,7 @@ class Tile implements Comparable { final String coordsKey; final Coords coords; final CustomPoint tilePos; - final ImageProvider imageProvider; + ImageProvider imageProvider; final Level level; bool current; @@ -965,10 +999,20 @@ class Tile implements Comparable { this.retain = false, this.loadError = false, }) { + loadTileImage(); + } + + void loadTileImage() { try { + final oldImageStream = _imageStream; _imageStream = imageProvider.resolve(ImageConfiguration()); - _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); - _imageStream.addListener(_listener); + + if (_imageStream.key != oldImageStream?.key) { + oldImageStream?.removeListener(_listener); + + _listener = ImageStreamListener(_tileOnLoad, onError: _tileOnError); + _imageStream.addListener(_listener); + } } catch (e, s) { // make sure all exception is handled - #444 / #536 _tileOnError(e, s); @@ -988,11 +1032,14 @@ class Tile implements Comparable { _imageStream?.removeListener(_listener); } - void startFadeInAnimation(Duration duration, TickerProvider vsync) { + void startFadeInAnimation(Duration duration, TickerProvider vsync, + {double from}) { + animationController?.removeStatusListener(_onAnimateEnd); + animationController = AnimationController(duration: duration, vsync: vsync) ..addStatusListener(_onAnimateEnd); - animationController.forward(); + animationController.forward(from: from); } void _onAnimateEnd(AnimationStatus status) { @@ -1010,7 +1057,8 @@ class Tile implements Comparable { void _tileOnError(dynamic exception, StackTrace stackTrace) { if (null != tileReady) { - tileReady(coords, exception, this); + tileReady( + coords, exception ?? 'Unknown exception during loadTileImage', this); } } From 381afd019d34f29e12e997018c65d41e79384964 Mon Sep 17 00:00:00 2001 From: maRci002 Date: Thu, 7 May 2020 00:50:24 +0200 Subject: [PATCH 5/5] make sure loadError resets before override --- lib/src/layer/tile_layer.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer.dart index 8d4e5e09e..9d4748f07 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer.dart @@ -884,6 +884,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { print(error); tile.loadError = true; + } else { + tile.loadError = false; } var key = _tileCoordsToKey(coords);