From 6782f7a07064c27b882de219cd0189d9465e4704 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 26 May 2022 11:10:45 +0200 Subject: [PATCH 01/11] Separate TileLayer in to multiple files for readability --- lib/flutter_map.dart | 3 +- lib/src/layer/tile_builder/tile_builder.dart | 2 +- lib/src/layer/tile_layer/animated_tile.dart | 76 +++ lib/src/layer/tile_layer/coords.dart | 23 + lib/src/layer/tile_layer/level.dart | 9 + lib/src/layer/tile_layer/tile.dart | 139 ++++ .../layer/{ => tile_layer}/tile_layer.dart | 640 +----------------- .../layer/tile_layer/tile_layer_options.dart | 405 +++++++++++ .../tile_provider/file_tile_provider_io.dart | 4 +- .../tile_provider/file_tile_provider_web.dart | 5 +- .../layer/tile_provider/tile_provider.dart | 1 + 11 files changed, 664 insertions(+), 643 deletions(-) create mode 100644 lib/src/layer/tile_layer/animated_tile.dart create mode 100644 lib/src/layer/tile_layer/coords.dart create mode 100644 lib/src/layer/tile_layer/level.dart create mode 100644 lib/src/layer/tile_layer/tile.dart rename lib/src/layer/{ => tile_layer}/tile_layer.dart (51%) create mode 100644 lib/src/layer/tile_layer/tile_layer_options.dart diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index ace11ee65..bd12f37ba 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -32,8 +32,9 @@ export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer.dart'; +export 'package:flutter_map/src/layer/tile_layer/tile.dart'; export 'package:flutter_map/src/layer/tile_builder/tile_builder.dart'; -export 'package:flutter_map/src/layer/tile_layer.dart'; +export 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; export 'package:flutter_map/src/layer/tile_provider/file_tile_provider_io.dart' if (dart.library.html) 'package:flutter_map/src/layer/tile_provider/file_tile_provider_web.dart'; export 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; diff --git a/lib/src/layer/tile_builder/tile_builder.dart b/lib/src/layer/tile_builder/tile_builder.dart index 26f865fe8..27bbd0a8e 100644 --- a/lib/src/layer/tile_builder/tile_builder.dart +++ b/lib/src/layer/tile_builder/tile_builder.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_map/src/layer/tile_layer.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile.dart'; typedef TileBuilder = Widget Function( BuildContext context, Widget tileWidget, Tile tile); diff --git a/lib/src/layer/tile_layer/animated_tile.dart b/lib/src/layer/tile_layer/animated_tile.dart new file mode 100644 index 000000000..13b2a4c26 --- /dev/null +++ b/lib/src/layer/tile_layer/animated_tile.dart @@ -0,0 +1,76 @@ + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; + +class AnimatedTile extends StatefulWidget { + final Tile tile; + final ImageProvider? errorImage; + final TileBuilder? tileBuilder; + + const AnimatedTile({ + Key? key, + required this.tile, + this.errorImage, + required this.tileBuilder, + }) : super(key: key); + + @override + State createState() => _AnimatedTileState(); +} + +class _AnimatedTileState extends State { + bool listenerAttached = false; + + @override + Widget build(BuildContext context) { + final tileWidget = (widget.tile.loadError && widget.errorImage != null) + ? Image( + image: widget.errorImage!, + fit: BoxFit.fill, + ) + : RawImage( + image: widget.tile.imageInfo?.image, + fit: BoxFit.fill, + opacity: widget.tile.animationController); + + return widget.tileBuilder == null + ? tileWidget + : widget.tileBuilder!(context, tileWidget, widget.tile); + } + + @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() { + if (mounted) { + setState(() {}); + } + } +} + diff --git a/lib/src/layer/tile_layer/coords.dart b/lib/src/layer/tile_layer/coords.dart new file mode 100644 index 000000000..68d52d502 --- /dev/null +++ b/lib/src/layer/tile_layer/coords.dart @@ -0,0 +1,23 @@ +import 'dart:ui'; + +import 'package:flutter_map/flutter_map.dart'; + +class Coords extends CustomPoint { + late T z; + + Coords(T x, T y) : super(x, y); + + @override + String toString() => 'Coords($x, $y, $z)'; + + @override + bool operator ==(Object other) { + if (other is Coords) { + return x == other.x && y == other.y && z == other.z; + } + return false; + } + + @override + int get hashCode => hashValues(x.hashCode, y.hashCode, z.hashCode); +} diff --git a/lib/src/layer/tile_layer/level.dart b/lib/src/layer/tile_layer/level.dart new file mode 100644 index 000000000..0d02e821a --- /dev/null +++ b/lib/src/layer/tile_layer/level.dart @@ -0,0 +1,9 @@ +import 'package:flutter_map/flutter_map.dart'; + +class Level { + late double zIndex; + CustomPoint? origin; + late double zoom; + late CustomPoint translatePoint; + late double scale; +} diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart new file mode 100644 index 000000000..4e175dc24 --- /dev/null +++ b/lib/src/layer/tile_layer/tile.dart @@ -0,0 +1,139 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/coords.dart'; +import 'package:flutter_map/src/layer/tile_layer/level.dart'; + +typedef TileReady = void Function( + Coords coords, dynamic error, Tile tile); + +class Tile implements Comparable { + final String coordsKey; + final Coords coords; + final CustomPoint tilePos; + ImageProvider imageProvider; + final Level level; + + bool current; + bool retain; + bool active; + bool loadError; + DateTime? loaded; + late DateTime loadStarted; + + 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 for instance when download aborted + TileReady? tileReady; + ImageInfo? imageInfo; + ImageStream? _imageStream; + late ImageStreamListener _listener; + + Tile({ + required this.coordsKey, + required this.coords, + required this.tilePos, + required this.imageProvider, + this.tileReady, + required this.level, + this.current = false, + this.active = false, + this.retain = false, + this.loadError = false, + }); + + void loadTileImage() { + loadStarted = DateTime.now(); + + try { + final oldImageStream = _imageStream; + _imageStream = imageProvider.resolve(ImageConfiguration.empty); + + 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); + } + } + + // call this before GC! + void dispose([bool evict = false]) { + if (evict) { + try { + // ignore: return_type_invalid_for_catch_error + // ignore: implicit_dynamic_parameter + imageProvider.evict().catchError((e) { + debugPrint(e.toString()); + }); + } catch (e) { + // this may be never called because catchError will handle errors, however + // we want to avoid random crashes like in #444 / #536 + debugPrint(e.toString()); + } + } + + animationController?.removeStatusListener(_onAnimateEnd); + animationController?.dispose(); + _imageStream?.removeListener(_listener); + } + + void startFadeInAnimation(Duration duration, TickerProvider vsync, + {double? from}) { + animationController?.removeStatusListener(_onAnimateEnd); + + animationController = AnimationController(duration: duration, vsync: vsync) + ..addStatusListener(_onAnimateEnd); + + animationController!.forward(from: from); + } + + void _onAnimateEnd(AnimationStatus status) { + if (status == AnimationStatus.completed) { + active = true; + } + } + + void _tileOnLoad(ImageInfo imageInfo, bool synchronousCall) { + if (null != tileReady) { + this.imageInfo = imageInfo; + tileReady!(coords, null, this); + } + } + + void _tileOnError(dynamic exception, StackTrace? stackTrace) { + if (null != tileReady) { + tileReady!( + coords, exception ?? 'Unknown exception during loadTileImage', this); + } + } + + @override + int compareTo(Tile other) { + final zIndexA = level.zIndex; + final zIndexB = other.level.zIndex; + + if (zIndexA == zIndexB) { + return 0; + } else { + return zIndexB.compareTo(zIndexA); + } + } + + @override + int get hashCode => coords.hashCode; + + @override + bool operator ==(Object other) { + return other is Tile && coords == other.coords; + } +} + diff --git a/lib/src/layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart similarity index 51% rename from lib/src/layer/tile_layer.dart rename to lib/src/layer/tile_layer/tile_layer.dart index fc68d2389..f8a699167 100644 --- a/lib/src/layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -7,412 +7,14 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/core/bounds.dart'; import 'package:flutter_map/src/core/util.dart' as util; +import 'package:flutter_map/src/layer/tile_layer/animated_tile.dart'; +import 'package:flutter_map/src/layer/tile_layer/coords.dart'; +import 'package:flutter_map/src/layer/tile_layer/level.dart'; import 'package:flutter_map/src/map/map.dart'; import 'package:latlong2/latlong.dart'; import 'package:tuple/tuple.dart'; -typedef TemplateFunction = String Function( - String str, Map data); - -enum EvictErrorTileStrategy { - // never evict error Tiles - none, - // evict error Tiles during _pruneTiles / _abortLoading calls - dispose, - // evict error Tiles which are not visible anymore but respect margin (see keepBuffer option) - // (Tile's zoom level not equals current _tileZoom or Tile is out of viewport) - notVisibleRespectMargin, - // evict error Tiles which are not visible anymore - // (Tile's zoom level not equals current _tileZoom or Tile is out of viewport) - notVisible, -} - -typedef ErrorTileCallBack = void Function(Tile tile, dynamic error); - -/// Describes the needed properties to create a tile-based layer. A tile is an -/// image bound 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: - /// - /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png - /// - /// Is translated to this: - /// - /// https://a.tile.openstreetmap.org/12/2177/1259.png - final String? urlTemplate; - - /// If `true`, inverses Y axis numbering for tiles (turn this on for - /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services). - final bool tms; - - /// If not `null`, then tiles will pull's WMS protocol requests - final WMSTileLayerOptions? wmsOptions; - - /// Size for the tile. - /// Default is 256 - final double tileSize; - - // 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; - - /// 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. - /// - /// Example: - /// - /// Subdomains = {a,b,c} - /// - /// and the URL is as follows: - /// - /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png - /// - /// then: - /// - /// https://a.tile.openstreetmap.org/{z}/{x}/{y}.png - /// https://b.tile.openstreetmap.org/{z}/{x}/{y}.png - /// https://c.tile.openstreetmap.org/{z}/{x}/{y}.png - final List subdomains; - - /// Color shown behind the tiles. - final Color backgroundColor; - - /// Opacity of the rendered tile - final double opacity; - - /// Provider to load the tiles. The default is `NonCachingNetworkTileProvider()` which - /// doesn't cache tiles and won't retry the HTTP request. Use `NetworkTileProvider()` for - /// a provider which will retry requests. For the best caching implementations, see the - /// flutter_map readme. - /// - /// In order to use images from the asset folder set this option to - /// AssetTileProvider() Note that it requires the urlTemplate to target - /// assets, for example: - /// - /// ```dart - /// urlTemplate: "assets/map/anholt_osmbright/{z}/{x}/{y}.png", - /// ``` - /// - /// In order to use images from the filesystem set this option to - /// FileTileProvider() Note that it requires the urlTemplate to target the - /// file system, for example: - /// - /// ```dart - /// urlTemplate: "/storage/emulated/0/tiles/some_place/{z}/{x}/{y}.png", - /// ``` - /// - /// Furthermore you create your custom implementation by subclassing - /// TileProvider - /// - final TileProvider tileProvider; - - /// When panning the map, keep this many rows and columns of tiles before - /// unloading them. - final int keepBuffer; - - /// 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 information that should replace placeholders in the [urlTemplate]. - /// Applying API keys is a good example on how to use this parameter. - /// - /// Example: - /// - /// ```dart - /// - /// TileLayerOptions( - /// urlTemplate: "https://api.tiles.mapbox.com/v4/" - /// "{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}", - /// additionalOptions: { - /// 'accessToken': '', - /// 'id': 'mapbox.streets', - /// }, - /// ), - /// ``` - /// - final Map additionalOptions; - - /// 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). This 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; - - /// 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: - /// - /// ```dart - /// TileLayerOptions( - /// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0, - /// ), - /// ``` - final bool retinaMode; - - /// This callback will be execute if some errors occur when fetching tiles. - final ErrorTileCallBack? errorTileCallback; - - final TemplateFunction templateFunction; - - /// Function which may Wrap Tile with custom Widget - /// There are predefined examples in 'tile_builder.dart' - final TileBuilder? tileBuilder; - - /// Function which may wrap Tiles Container with custom Widget - /// There are predefined examples in 'tile_builder.dart' - final TilesContainerBuilder? tilesContainerBuilder; - - // If a Tile was loaded with error and if strategy isn't `none` then TileProvider - // will be asked to evict Image based on current strategy - // (see #576 - even Error Images are cached in flutter) - final EvictErrorTileStrategy evictErrorTileStrategy; - - /// This option is useful when you have a transparent layer: rather than - /// keeping the old layer visible when zooming (resulting in both layers - /// being temporarily visible), the old layer is removed as quickly as - /// possible when this is set to `true` (default `false`). - /// - /// This option is likely to cause some flickering of the transparent layer, - /// most noticeable when using pinch-to-zoom. It's best used with maps that - /// have `interactive` set to `false`, and zoom using buttons that call - /// `MapController.move()`. - /// - /// When set to `true`, the `tileFadeIn*` options will be ignored. - final bool fastReplace; - - ///Attribution widget builder - final WidgetBuilder? attributionBuilder; - - ///aligment of the attribution text on the map widget - final Alignment attributionAlignment; - - /// Stream to notify the [TileLayer] that it needs resetting - Stream? reset; - - /// Only load tiles that are within these bounds - LatLngBounds? tileBounds; - - TileLayerOptions( - {this.attributionAlignment = Alignment.bottomRight, - this.attributionBuilder, - Key? key, - // TODO: make required - 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, - Map? additionalOptions, - this.subdomains = const [], - this.keepBuffer = 2, - this.backgroundColor = const Color(0xFFE0E0E0), - this.placeholderImage, - this.errorImage, - this.tileProvider = const NonCachingNetworkTileProvider(), - 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) - // TODO: change to Duration - int updateInterval = 200, - // Tiles fade in duration in milliseconds (default 100). This can be set to - // 0 to avoid fade in - // TODO: change to Duration - int tileFadeInDuration = 100, - this.tileFadeInStart = 0.0, - this.tileFadeInStartWhenOverride = 0.0, - this.overrideTilesWhenUrlChanges = false, - this.retinaMode = false, - this.errorTileCallback, - Stream? rebuild, - this.templateFunction = util.template, - this.tileBuilder, - this.tilesContainerBuilder, - this.evictErrorTileStrategy = EvictErrorTileStrategy.none, - this.fastReplace = false, - this.reset, - this.tileBounds}) - : 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), - 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) - : 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, - // copy additionalOptions Map if not null, so we can safely compare old - // and new Map inside didUpdateWidget with MapEquality. - additionalOptions = additionalOptions == null - ? const {} - : Map.from(additionalOptions), - super(key: key, rebuild: rebuild); -} - -class WMSTileLayerOptions { - final service = 'WMS'; - final request = 'GetMap'; - - /// url of WMS service. - /// Ex.: 'http://ows.mundialis.de/services/service?' - final String baseUrl; - - /// list of WMS layers to show - final List layers; - - /// list of WMS styles - final List styles; - - /// WMS image format (use 'image/png' for layers with transparency) - final String format; - - /// Version of the WMS service to use - final String version; - - /// tile transparency flag - final bool transparent; - - /// Encode boolean values as uppercase in request - final bool uppercaseBoolValue; - - // TODO find a way to implicit pass of current map [Crs] - final Crs crs; - - /// other request parameters - final Map otherParameters; - - late final String _encodedBaseUrl; - - late final double _versionNumber; - - WMSTileLayerOptions({ - required this.baseUrl, - this.layers = const [], - this.styles = const [], - this.format = 'image/png', - this.version = '1.1.1', - this.transparent = true, - this.uppercaseBoolValue = false, - this.crs = const Epsg3857(), - this.otherParameters = const {}, - }) { - _versionNumber = double.tryParse(version.split('.').take(2).join('.')) ?? 0; - _encodedBaseUrl = _buildEncodedBaseUrl(); - } - - String _buildEncodedBaseUrl() { - final projectionKey = _versionNumber >= 1.3 ? 'crs' : 'srs'; - final buffer = StringBuffer(baseUrl) - ..write('&service=$service') - ..write('&request=$request') - ..write('&layers=${layers.map(Uri.encodeComponent).join(',')}') - ..write('&styles=${styles.map(Uri.encodeComponent).join(',')}') - ..write('&format=${Uri.encodeComponent(format)}') - ..write('&$projectionKey=${Uri.encodeComponent(crs.code)}') - ..write('&version=${Uri.encodeComponent(version)}') - ..write( - '&transparent=${uppercaseBoolValue ? transparent.toString().toUpperCase() : transparent}'); - otherParameters - .forEach((k, v) => buffer.write('&$k=${Uri.encodeComponent(v)}')); - return buffer.toString(); - } - - String getUrl(Coords coords, int tileSize, bool retinaMode) { - final tileSizePoint = CustomPoint(tileSize, tileSize); - final nvPoint = coords.scaleBy(tileSizePoint); - final sePoint = nvPoint + tileSizePoint; - final nvCoords = crs.pointToLatLng(nvPoint, coords.z as double)!; - final seCoords = crs.pointToLatLng(sePoint, coords.z as double)!; - final nv = crs.projection.project(nvCoords); - final se = crs.projection.project(seCoords); - final bounds = Bounds(nv, se); - final bbox = (_versionNumber >= 1.3 && crs is Epsg4326) - ? [bounds.min.y, bounds.min.x, bounds.max.y, bounds.max.x] - : [bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y]; - - final buffer = StringBuffer(_encodedBaseUrl); - buffer.write('&width=${retinaMode ? tileSize * 2 : tileSize}'); - buffer.write('&height=${retinaMode ? tileSize * 2 : tileSize}'); - buffer.write('&bbox=${bbox.join(',')}'); - return buffer.toString(); - } -} +part 'tile_layer_options.dart'; class TileLayerWidget extends StatelessWidget { final TileLayerOptions options; @@ -1244,237 +846,3 @@ class _TileLayerState extends State with TickerProviderStateMixin { return true; } } - -typedef TileReady = void Function( - Coords coords, dynamic error, Tile tile); - -class Tile implements Comparable { - final String coordsKey; - final Coords coords; - final CustomPoint tilePos; - ImageProvider imageProvider; - final Level level; - - bool current; - bool retain; - bool active; - bool loadError; - DateTime? loaded; - late DateTime loadStarted; - - 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 for instance when download aborted - TileReady? tileReady; - ImageInfo? imageInfo; - ImageStream? _imageStream; - late ImageStreamListener _listener; - - Tile({ - required this.coordsKey, - required this.coords, - required this.tilePos, - required this.imageProvider, - this.tileReady, - required this.level, - this.current = false, - this.active = false, - this.retain = false, - this.loadError = false, - }); - - void loadTileImage() { - loadStarted = DateTime.now(); - - try { - final oldImageStream = _imageStream; - _imageStream = imageProvider.resolve(ImageConfiguration.empty); - - 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); - } - } - - // call this before GC! - void dispose([bool evict = false]) { - if (evict) { - try { - // ignore: return_type_invalid_for_catch_error - // ignore: implicit_dynamic_parameter - imageProvider.evict().catchError((e) { - debugPrint(e.toString()); - }); - } catch (e) { - // this may be never called because catchError will handle errors, however - // we want to avoid random crashes like in #444 / #536 - debugPrint(e.toString()); - } - } - - animationController?.removeStatusListener(_onAnimateEnd); - animationController?.dispose(); - _imageStream?.removeListener(_listener); - } - - void startFadeInAnimation(Duration duration, TickerProvider vsync, - {double? from}) { - animationController?.removeStatusListener(_onAnimateEnd); - - animationController = AnimationController(duration: duration, vsync: vsync) - ..addStatusListener(_onAnimateEnd); - - animationController!.forward(from: from); - } - - void _onAnimateEnd(AnimationStatus status) { - if (status == AnimationStatus.completed) { - active = true; - } - } - - void _tileOnLoad(ImageInfo imageInfo, bool synchronousCall) { - if (null != tileReady) { - this.imageInfo = imageInfo; - tileReady!(coords, null, this); - } - } - - void _tileOnError(dynamic exception, StackTrace? stackTrace) { - if (null != tileReady) { - tileReady!( - coords, exception ?? 'Unknown exception during loadTileImage', this); - } - } - - @override - int compareTo(Tile other) { - final zIndexA = level.zIndex; - final zIndexB = other.level.zIndex; - - if (zIndexA == zIndexB) { - return 0; - } else { - return zIndexB.compareTo(zIndexA); - } - } - - @override - int get hashCode => coords.hashCode; - - @override - bool operator ==(Object other) { - return other is Tile && coords == other.coords; - } -} - -class AnimatedTile extends StatefulWidget { - final Tile tile; - final ImageProvider? errorImage; - final TileBuilder? tileBuilder; - - const AnimatedTile({ - Key? key, - required this.tile, - this.errorImage, - required this.tileBuilder, - }) : super(key: key); - - @override - State createState() => _AnimatedTileState(); -} - -class _AnimatedTileState extends State { - bool listenerAttached = false; - - @override - Widget build(BuildContext context) { - final tileWidget = (widget.tile.loadError && widget.errorImage != null) - ? Image( - image: widget.errorImage!, - fit: BoxFit.fill, - ) - : RawImage( - image: widget.tile.imageInfo?.image, - fit: BoxFit.fill, - opacity: widget.tile.animationController); - - return widget.tileBuilder == null - ? tileWidget - : widget.tileBuilder!(context, tileWidget, widget.tile); - } - - @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() { - if (mounted) { - setState(() {}); - } - } -} - -class Level { - late double zIndex; - CustomPoint? origin; - late double zoom; - late CustomPoint translatePoint; - late double scale; -} - -class Coords extends CustomPoint { - late T z; - - Coords(T x, T y) : super(x, y); - - @override - String toString() => 'Coords($x, $y, $z)'; - - @override - bool operator ==(Object other) { - if (other is Coords) { - return x == other.x && y == other.y && z == other.z; - } - return false; - } - - @override - int get hashCode => hashValues(x.hashCode, y.hashCode, z.hashCode); -} diff --git a/lib/src/layer/tile_layer/tile_layer_options.dart b/lib/src/layer/tile_layer/tile_layer_options.dart new file mode 100644 index 000000000..829d66de6 --- /dev/null +++ b/lib/src/layer/tile_layer/tile_layer_options.dart @@ -0,0 +1,405 @@ +part of 'tile_layer.dart'; + +typedef TemplateFunction = String Function( + String str, Map data); + +enum EvictErrorTileStrategy { + // never evict error Tiles + none, + // evict error Tiles during _pruneTiles / _abortLoading calls + dispose, + // evict error Tiles which are not visible anymore but respect margin (see keepBuffer option) + // (Tile's zoom level not equals current _tileZoom or Tile is out of viewport) + notVisibleRespectMargin, + // evict error Tiles which are not visible anymore + // (Tile's zoom level not equals current _tileZoom or Tile is out of viewport) + notVisible, +} + +typedef ErrorTileCallBack = void Function(Tile tile, dynamic error); + +/// Describes the needed properties to create a tile-based layer. A tile is an +/// image bound 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: + /// + /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png + /// + /// Is translated to this: + /// + /// https://a.tile.openstreetmap.org/12/2177/1259.png + final String? urlTemplate; + + /// If `true`, inverses Y axis numbering for tiles (turn this on for + /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) services). + final bool tms; + + /// If not `null`, then tiles will pull's WMS protocol requests + final WMSTileLayerOptions? wmsOptions; + + /// Size for the tile. + /// Default is 256 + final double tileSize; + + // 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; + + /// 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. + /// + /// Example: + /// + /// Subdomains = {a,b,c} + /// + /// and the URL is as follows: + /// + /// https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png + /// + /// then: + /// + /// https://a.tile.openstreetmap.org/{z}/{x}/{y}.png + /// https://b.tile.openstreetmap.org/{z}/{x}/{y}.png + /// https://c.tile.openstreetmap.org/{z}/{x}/{y}.png + final List subdomains; + + /// Color shown behind the tiles. + final Color backgroundColor; + + /// Opacity of the rendered tile + final double opacity; + + /// Provider to load the tiles. The default is `NonCachingNetworkTileProvider()` which + /// doesn't cache tiles and won't retry the HTTP request. Use `NetworkTileProvider()` for + /// a provider which will retry requests. For the best caching implementations, see the + /// flutter_map readme. + /// + /// In order to use images from the asset folder set this option to + /// AssetTileProvider() Note that it requires the urlTemplate to target + /// assets, for example: + /// + /// ```dart + /// urlTemplate: "assets/map/anholt_osmbright/{z}/{x}/{y}.png", + /// ``` + /// + /// In order to use images from the filesystem set this option to + /// FileTileProvider() Note that it requires the urlTemplate to target the + /// file system, for example: + /// + /// ```dart + /// urlTemplate: "/storage/emulated/0/tiles/some_place/{z}/{x}/{y}.png", + /// ``` + /// + /// Furthermore you create your custom implementation by subclassing + /// TileProvider + /// + final TileProvider tileProvider; + + /// When panning the map, keep this many rows and columns of tiles before + /// unloading them. + final int keepBuffer; + + /// 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 information that should replace placeholders in the [urlTemplate]. + /// Applying API keys is a good example on how to use this parameter. + /// + /// Example: + /// + /// ```dart + /// + /// TileLayerOptions( + /// urlTemplate: "https://api.tiles.mapbox.com/v4/" + /// "{id}/{z}/{x}/{y}{r}.png?access_token={accessToken}", + /// additionalOptions: { + /// 'accessToken': '', + /// 'id': 'mapbox.streets', + /// }, + /// ), + /// ``` + /// + final Map additionalOptions; + + /// 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). This 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; + + /// 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: + /// + /// ```dart + /// TileLayerOptions( + /// retinaMode: true && MediaQuery.of(context).devicePixelRatio > 1.0, + /// ), + /// ``` + final bool retinaMode; + + /// This callback will be execute if some errors occur when fetching tiles. + final ErrorTileCallBack? errorTileCallback; + + final TemplateFunction templateFunction; + + /// Function which may Wrap Tile with custom Widget + /// There are predefined examples in 'tile_builder.dart' + final TileBuilder? tileBuilder; + + /// Function which may wrap Tiles Container with custom Widget + /// There are predefined examples in 'tile_builder.dart' + final TilesContainerBuilder? tilesContainerBuilder; + + // If a Tile was loaded with error and if strategy isn't `none` then TileProvider + // will be asked to evict Image based on current strategy + // (see #576 - even Error Images are cached in flutter) + final EvictErrorTileStrategy evictErrorTileStrategy; + + /// This option is useful when you have a transparent layer: rather than + /// keeping the old layer visible when zooming (resulting in both layers + /// being temporarily visible), the old layer is removed as quickly as + /// possible when this is set to `true` (default `false`). + /// + /// This option is likely to cause some flickering of the transparent layer, + /// most noticeable when using pinch-to-zoom. It's best used with maps that + /// have `interactive` set to `false`, and zoom using buttons that call + /// `MapController.move()`. + /// + /// When set to `true`, the `tileFadeIn*` options will be ignored. + final bool fastReplace; + + ///Attribution widget builder + final WidgetBuilder? attributionBuilder; + + ///aligment of the attribution text on the map widget + final Alignment attributionAlignment; + + /// Stream to notify the [TileLayer] that it needs resetting + Stream? reset; + + /// Only load tiles that are within these bounds + LatLngBounds? tileBounds; + + TileLayerOptions( + {this.attributionAlignment = Alignment.bottomRight, + this.attributionBuilder, + Key? key, + // TODO: make required + 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, + Map? additionalOptions, + this.subdomains = const [], + this.keepBuffer = 2, + this.backgroundColor = const Color(0xFFE0E0E0), + this.placeholderImage, + this.errorImage, + this.tileProvider = const NonCachingNetworkTileProvider(), + 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) + // TODO: change to Duration + int updateInterval = 200, + // Tiles fade in duration in milliseconds (default 100). This can be set to + // 0 to avoid fade in + // TODO: change to Duration + int tileFadeInDuration = 100, + this.tileFadeInStart = 0.0, + this.tileFadeInStartWhenOverride = 0.0, + this.overrideTilesWhenUrlChanges = false, + this.retinaMode = false, + this.errorTileCallback, + Stream? rebuild, + this.templateFunction = util.template, + this.tileBuilder, + this.tilesContainerBuilder, + this.evictErrorTileStrategy = EvictErrorTileStrategy.none, + this.fastReplace = false, + this.reset, + this.tileBounds}) + : 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), + 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) + : 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, + // copy additionalOptions Map if not null, so we can safely compare old + // and new Map inside didUpdateWidget with MapEquality. + additionalOptions = additionalOptions == null + ? const {} + : Map.from(additionalOptions), + super(key: key, rebuild: rebuild); +} + +class WMSTileLayerOptions { + final service = 'WMS'; + final request = 'GetMap'; + + /// url of WMS service. + /// Ex.: 'http://ows.mundialis.de/services/service?' + final String baseUrl; + + /// list of WMS layers to show + final List layers; + + /// list of WMS styles + final List styles; + + /// WMS image format (use 'image/png' for layers with transparency) + final String format; + + /// Version of the WMS service to use + final String version; + + /// tile transparency flag + final bool transparent; + + /// Encode boolean values as uppercase in request + final bool uppercaseBoolValue; + + // TODO find a way to implicit pass of current map [Crs] + final Crs crs; + + /// other request parameters + final Map otherParameters; + + late final String _encodedBaseUrl; + + late final double _versionNumber; + + WMSTileLayerOptions({ + required this.baseUrl, + this.layers = const [], + this.styles = const [], + this.format = 'image/png', + this.version = '1.1.1', + this.transparent = true, + this.uppercaseBoolValue = false, + this.crs = const Epsg3857(), + this.otherParameters = const {}, + }) { + _versionNumber = double.tryParse(version.split('.').take(2).join('.')) ?? 0; + _encodedBaseUrl = _buildEncodedBaseUrl(); + } + + String _buildEncodedBaseUrl() { + final projectionKey = _versionNumber >= 1.3 ? 'crs' : 'srs'; + final buffer = StringBuffer(baseUrl) + ..write('&service=$service') + ..write('&request=$request') + ..write('&layers=${layers.map(Uri.encodeComponent).join(',')}') + ..write('&styles=${styles.map(Uri.encodeComponent).join(',')}') + ..write('&format=${Uri.encodeComponent(format)}') + ..write('&$projectionKey=${Uri.encodeComponent(crs.code)}') + ..write('&version=${Uri.encodeComponent(version)}') + ..write( + '&transparent=${uppercaseBoolValue ? transparent.toString().toUpperCase() : transparent}'); + otherParameters + .forEach((k, v) => buffer.write('&$k=${Uri.encodeComponent(v)}')); + return buffer.toString(); + } + + String getUrl(Coords coords, int tileSize, bool retinaMode) { + final tileSizePoint = CustomPoint(tileSize, tileSize); + final nvPoint = coords.scaleBy(tileSizePoint); + final sePoint = nvPoint + tileSizePoint; + final nvCoords = crs.pointToLatLng(nvPoint, coords.z as double)!; + final seCoords = crs.pointToLatLng(sePoint, coords.z as double)!; + final nv = crs.projection.project(nvCoords); + final se = crs.projection.project(seCoords); + final bounds = Bounds(nv, se); + final bbox = (_versionNumber >= 1.3 && crs is Epsg4326) + ? [bounds.min.y, bounds.min.x, bounds.max.y, bounds.max.x] + : [bounds.min.x, bounds.min.y, bounds.max.x, bounds.max.y]; + + final buffer = StringBuffer(_encodedBaseUrl); + buffer.write('&width=${retinaMode ? tileSize * 2 : tileSize}'); + buffer.write('&height=${retinaMode ? tileSize * 2 : tileSize}'); + buffer.write('&bbox=${bbox.join(',')}'); + return buffer.toString(); + } +} + diff --git a/lib/src/layer/tile_provider/file_tile_provider_io.dart b/lib/src/layer/tile_provider/file_tile_provider_io.dart index 174f8f518..dced715a2 100644 --- a/lib/src/layer/tile_provider/file_tile_provider_io.dart +++ b/lib/src/layer/tile_provider/file_tile_provider_io.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/coords.dart'; -import 'package:flutter_map/src/layer/tile_layer.dart'; -import 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; /// FileTileProvider class FileTileProvider extends TileProvider { diff --git a/lib/src/layer/tile_provider/file_tile_provider_web.dart b/lib/src/layer/tile_provider/file_tile_provider_web.dart index eab46a5eb..2ddbb8992 100644 --- a/lib/src/layer/tile_provider/file_tile_provider_web.dart +++ b/lib/src/layer/tile_provider/file_tile_provider_web.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; - -import 'package:flutter_map/src/layer/tile_layer.dart'; -import 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/coords.dart'; /// FileTileProvider diff --git a/lib/src/layer/tile_provider/tile_provider.dart b/lib/src/layer/tile_provider/tile_provider.dart index e0b055de4..1b4c124d4 100644 --- a/lib/src/layer/tile_provider/tile_provider.dart +++ b/lib/src/layer/tile_provider/tile_provider.dart @@ -1,5 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/coords.dart'; import 'package:flutter_map/src/layer/tile_provider/network_image_with_retry.dart'; From 438942f9275fab98cf119e64252552b4f3afc5d4 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 26 May 2022 12:53:34 +0200 Subject: [PATCH 02/11] Refactor TileLayer in to separate files/classes to make it more readable --- lib/src/layer/tile_layer/coords.dart | 17 + lib/src/layer/tile_layer/tile.dart | 4 +- lib/src/layer/tile_layer/tile_layer.dart | 357 +++------------------ lib/src/layer/tile_layer/tile_manager.dart | 254 +++++++++++++++ lib/src/layer/tile_layer/tile_widget.dart | 38 +++ 5 files changed, 358 insertions(+), 312 deletions(-) create mode 100644 lib/src/layer/tile_layer/tile_manager.dart create mode 100644 lib/src/layer/tile_layer/tile_widget.dart diff --git a/lib/src/layer/tile_layer/coords.dart b/lib/src/layer/tile_layer/coords.dart index 68d52d502..943117023 100644 --- a/lib/src/layer/tile_layer/coords.dart +++ b/lib/src/layer/tile_layer/coords.dart @@ -1,12 +1,29 @@ import 'dart:ui'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/core/util.dart' as util; +import 'package:tuple/tuple.dart'; class Coords extends CustomPoint { late T z; Coords(T x, T y) : super(x, y); + Coords wrap(Tuple2? wrapX, Tuple2? wrapY) { + final newCoords = Coords( + wrapX != null + ? util.wrapNum(x.toDouble(), wrapX) + : x.toDouble(), + wrapY != null + ? util.wrapNum(y.toDouble(), wrapY) + : y.toDouble(), + ); + newCoords.z = z.toDouble(); + return newCoords; + } + + String get key => '$x:$y:$z'; + @override String toString() => 'Coords($x, $y, $z)'; diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index 4e175dc24..0844c392f 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -7,7 +7,6 @@ typedef TileReady = void Function( Coords coords, dynamic error, Tile tile); class Tile implements Comparable { - final String coordsKey; final Coords coords; final CustomPoint tilePos; ImageProvider imageProvider; @@ -34,7 +33,6 @@ class Tile implements Comparable { late ImageStreamListener _listener; Tile({ - required this.coordsKey, required this.coords, required this.tilePos, required this.imageProvider, @@ -128,6 +126,8 @@ class Tile implements Comparable { } } + String get coordsKey => coords.key; + @override int get hashCode => coords.hashCode; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index f8a699167..1d12bfe01 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -7,9 +7,10 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/core/bounds.dart'; import 'package:flutter_map/src/core/util.dart' as util; -import 'package:flutter_map/src/layer/tile_layer/animated_tile.dart'; import 'package:flutter_map/src/layer/tile_layer/coords.dart'; import 'package:flutter_map/src/layer/tile_layer/level.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_manager.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_widget.dart'; import 'package:flutter_map/src/map/map.dart'; import 'package:latlong2/latlong.dart'; import 'package:tuple/tuple.dart'; @@ -66,7 +67,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { StreamController? _throttleUpdate; late CustomPoint _tileSize; - final Map _tiles = {}; + late final TileManager _tileManager; final Map _levels = {}; Timer? _pruneLater; @@ -74,6 +75,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); + _tileManager = TileManager(); _tileSize = CustomPoint(options.tileSize, options.tileSize); _resetView(); _update(null); @@ -100,7 +102,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { reloadTiles = true; } - reloadTiles |= _isZoomOutsideMinMax(); + reloadTiles |= !_tileManager.allWithinZoom(options.minZoom, options.maxZoom); if (oldWidget.options.updateInterval != options.updateInterval) { _throttleUpdate?.close(); @@ -119,11 +121,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { !(const MapEquality()) .equals(oldOptions, newOptions)) { if (options.overrideTilesWhenUrlChanges) { - for (final tile in _tiles.values) { - tile.imageProvider = options.tileProvider - .getImage(_wrapCoords(tile.coords), options); - tile.loadTileImage(); - } + _tileManager.reloadImages(options, _wrapX, _wrapY); } else { reloadTiles = true; } @@ -131,22 +129,12 @@ class _TileLayerState extends State with TickerProviderStateMixin { } if (reloadTiles) { - _removeAllTiles(); + _tileManager.removeAll(options.evictErrorTileStrategy); _resetView(); _update(null); } } - bool _isZoomOutsideMinMax() { - for (final tile in _tiles.values) { - if (tile.level.zoom > (options.maxZoom) || - tile.level.zoom < (options.minZoom)) { - return true; - } - } - return false; - } - void _initThrottleUpdate() { if (options.updateInterval == null) { _throttleUpdate = null; @@ -164,7 +152,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override void dispose() { - _removeAllTiles(); + _tileManager.removeAll(options.evictErrorTileStrategy); _resetSub?.cancel(); _moveSub?.cancel(); _pruneLater?.cancel(); @@ -176,10 +164,16 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final tilesToRender = _tiles.values.toList()..sort(); - + final tilesToRender = _tileManager.sortedTiles(); final tileWidgets = [ - for (var tile in tilesToRender) _createTileWidget(tile) + for (var tile in tilesToRender) + TileWidget( + tile: tile, + size: _tileSize, + errorImage: options.errorImage, + tileBuilder: options.tileBuilder, + key: ValueKey(tile.coordsKey), + ) ]; final tilesContainer = Stack( @@ -211,66 +205,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { ); } - Widget _createTileWidget(Tile tile) { - final tilePos = tile.tilePos; - final level = tile.level; - final tileSize = getTileSize(); - final pos = (tilePos).multiplyBy(level.scale) + level.translatePoint; - final num width = tileSize.x * level.scale; - final num height = tileSize.y * level.scale; - - final Widget content = AnimatedTile( - tile: tile, - errorImage: options.errorImage, - tileBuilder: options.tileBuilder, - ); - - return Positioned( - key: ValueKey(tile.coordsKey), - left: pos.x.toDouble(), - top: pos.y.toDouble(), - width: width.toDouble(), - height: height.toDouble(), - child: content, - ); - } - - void _abortLoading() { - final toRemove = []; - for (final entry in _tiles.entries) { - final tile = entry.value; - - if (tile.coords.z != _tileZoom) { - if (tile.loaded == null) { - toRemove.add(entry.key); - } - } - } - - for (final key in toRemove) { - final tile = _tiles[key]!; - - tile.tileReady = null; - tile.dispose(tile.loadError && - options.evictErrorTileStrategy != EvictErrorTileStrategy.none); - _tiles.remove(key); - } - } - CustomPoint getTileSize() { return _tileSize; } - bool _hasLevelChildren(double lvl) { - for (final tile in _tiles.values) { - if (tile.coords.z == lvl) { - return true; - } - } - - return false; - } - Level? _updateLevels() { final zoom = _tileZoom; final maxZoom = options.maxZoom; @@ -282,7 +220,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { final z = entry.key; final lvl = entry.value; - if (z == zoom || _hasLevelChildren(z)) { + if (z == zoom || _tileManager.anyWithZoomLevel(z)) { lvl.zIndex = maxZoom - (zoom - z).abs(); } else { toRemove.add(z); @@ -290,7 +228,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { } for (final z in toRemove) { - _removeTilesAtZoom(z); + _tileManager.removeAtZoom(z, options.evictErrorTileStrategy); _levels.remove(z); } @@ -309,122 +247,12 @@ class _TileLayerState extends State with TickerProviderStateMixin { return _level = level; } - void _pruneTiles() { - final zoom = _tileZoom; - if (zoom == null) { - _removeAllTiles(); - return; - } - - for (final entry in _tiles.entries) { - final tile = entry.value; - tile.retain = tile.current; - } - - for (final entry in _tiles.entries) { - final tile = entry.value; - - if (tile.current && !tile.active) { - final coords = tile.coords; - if (!_retainParent(coords.x, coords.y, coords.z, coords.z - 5)) { - _retainChildren(coords.x, coords.y, coords.z, coords.z + 2); - } - } - } - - final toRemove = []; - for (final entry in _tiles.entries) { - final tile = entry.value; - - if (!tile.retain) { - toRemove.add(entry.key); - } - } - - for (final key in toRemove) { - _removeTile(key); - } - } - - void _removeTilesAtZoom(double zoom) { - final toRemove = []; - for (final entry in _tiles.entries) { - if (entry.value.coords.z != zoom) { - continue; - } - toRemove.add(entry.key); - } - - for (final key in toRemove) { - _removeTile(key); - } - } - ///removes all loaded tiles and resets the view void _resetTiles() { - _removeAllTiles(); + _tileManager.removeAll(options.evictErrorTileStrategy); _resetView(); } - void _removeAllTiles() { - final toRemove = Map.from(_tiles); - - for (final key in toRemove.keys) { - _removeTile(key); - } - } - - bool _retainParent(double x, double y, double z, double minZoom) { - final x2 = (x / 2).floorToDouble(); - final y2 = (y / 2).floorToDouble(); - final z2 = z - 1; - final coords2 = Coords(x2, y2); - coords2.z = z2; - - final key = _tileCoordsToKey(coords2); - - final tile = _tiles[key]; - 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++) { - final coords = Coords(i, j); - coords.z = z + 1; - - final key = _tileCoordsToKey(coords); - - final 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); } @@ -449,7 +277,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileZoom = tileZoom; - _abortLoading(); + _tileManager.abortLoading(_tileZoom, options.evictErrorTileStrategy); _updateLevels(); _resetGrid(); @@ -458,7 +286,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _update(center); } - _pruneTiles(); + _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); } void _setZoomTransforms(LatLng center, double zoom) { @@ -575,15 +403,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { tileRange.topRight + CustomPoint(margin, -margin), ); - for (final entry in _tiles.entries) { - final tile = entry.value; - final c = tile.coords; - - if (tile.current == true && - (c.z != _tileZoom || !noPruneRange.contains(CustomPoint(c.x, c.y)))) { - tile.current = false; - } - } + _tileManager.markToPrune(_tileZoom, noPruneRange); // _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. @@ -610,23 +430,32 @@ class _TileLayerState extends State with TickerProviderStateMixin { continue; } - final tile = _tiles[_tileCoordsToKey(coords)]; - if (tile != null) { - tile.current = true; - } else { + if (!_tileManager.markTileWithCoordsAsCurrent(coords)) { queue.add(coords); } } } - _evictErrorTilesBasedOnStrategy(tileRange); + _tileManager.evictErrorTilesBasedOnStrategy(tileRange, options.evictErrorTileStrategy); // sort tile queue to load tiles in order of their distance to center queue.sort((a, b) => (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt()); for (var i = 0; i < queue.length; i++) { - _addTile(queue[i] as Coords); + final coords = queue[i] as Coords; + _tileManager.add( + coords, + Tile( + coords: coords, + tilePos: _getTilePos(coords), + current: true, + level: _levels[coords.z]!, + imageProvider: + options.tileProvider.getImage(coords.wrap(_wrapX, _wrapY), options), + tileReady: _tileReady, + )..loadTileImage(), + ); } } @@ -663,10 +492,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { return pxBounds; } - String _tileCoordsToKey(Coords coords) { - return '${coords.x}:${coords.y}:${coords.z}'; - } - //ignore: unused_element Coords _keyToTileCoords(String key) { final k = key.split(':'); @@ -676,73 +501,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { return coords; } - void _removeTile(String key) { - final tile = _tiles[key]; - if (tile == null) { - return; - } - - tile.dispose(tile.loadError && - options.evictErrorTileStrategy != EvictErrorTileStrategy.none); - _tiles.remove(key); - } - - void _addTile(Coords coords) { - final tileCoordsToKey = _tileCoordsToKey(coords); - final tile = _tiles[tileCoordsToKey] = Tile( - coords: coords, - coordsKey: tileCoordsToKey, - tilePos: _getTilePos(coords), - current: true, - level: _levels[coords.z]!, - imageProvider: - options.tileProvider.getImage(_wrapCoords(coords), options), - tileReady: _tileReady, - ); - - tile.loadTileImage(); - } - - void _evictErrorTilesBasedOnStrategy(Bounds tileRange) { - if (options.evictErrorTileStrategy == - EvictErrorTileStrategy.notVisibleRespectMargin) { - final toRemove = []; - for (final entry in _tiles.entries) { - final tile = entry.value; - - if (tile.loadError && !tile.current) { - toRemove.add(entry.key); - } - } - - for (final key in toRemove) { - final tile = _tiles[key]!; - - tile.dispose(true); - _tiles.remove(key); - } - } else if (options.evictErrorTileStrategy == - EvictErrorTileStrategy.notVisible) { - final toRemove = []; - for (final entry in _tiles.entries) { - final tile = entry.value; - final c = tile.coords; - - if (tile.loadError && - (!tile.current || !tileRange.contains(CustomPoint(c.x, c.y)))) { - toRemove.add(entry.key); - } - } - - for (final key in toRemove) { - final tile = _tiles[key]!; - - tile.dispose(true); - _tiles.remove(key); - } - } - } - void _tileReady(Coords coords, dynamic error, Tile? tile) { if (null != error) { debugPrint(error.toString()); @@ -756,19 +514,17 @@ class _TileLayerState extends State with TickerProviderStateMixin { tile!.loadError = false; } - final key = _tileCoordsToKey(coords); - tile = _tiles[key]; - if (null == tile) { - return; - } + + tile = _tileManager.tileAt(tile.coords); + if (tile == null) return; if (options.fastReplace && mounted) { setState(() { tile!.active = true; - if (_noTilesToLoad()) { + if (_tileManager.allLoaded) { // We're not waiting for anything, prune the tiles immediately. - _pruneTiles(); + _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); } }); return; @@ -794,7 +550,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { setState(() {}); } - if (_noTilesToLoad()) { + if (_tileManager.allLoaded) { // Wait a bit more than tileFadeInDuration (the duration of the tile // fade-in) to trigger a pruning. _pruneLater?.cancel(); @@ -804,7 +560,9 @@ class _TileLayerState extends State with TickerProviderStateMixin { : const Duration(milliseconds: 50), () { if (mounted) { - setState(_pruneTiles); + setState((){ + _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); + }); } }, ); @@ -816,19 +574,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { return coords.scaleBy(getTileSize()) - level.origin!; } - Coords _wrapCoords(Coords coords) { - final newCoords = Coords( - _wrapX != null - ? util.wrapNum(coords.x.toDouble(), _wrapX!) - : coords.x.toDouble(), - _wrapY != null - ? util.wrapNum(coords.y.toDouble(), _wrapY!) - : coords.y.toDouble(), - ); - newCoords.z = coords.z.toDouble(); - return newCoords; - } - Bounds _pxBoundsToTileRange(Bounds bounds) { final tileSize = getTileSize(); return Bounds( @@ -837,12 +582,4 @@ class _TileLayerState extends State with TickerProviderStateMixin { ); } - bool _noTilesToLoad() { - for (final entry in _tiles.entries) { - if (entry.value.loaded == null) { - return false; - } - } - return true; - } } diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart new file mode 100644 index 000000000..f9a1c3a39 --- /dev/null +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -0,0 +1,254 @@ +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/core/bounds.dart'; +import 'package:flutter_map/src/layer/tile_layer/coords.dart'; +import 'package:tuple/tuple.dart'; + +class TileManager { + final Map _tiles = {}; + + void abortLoading(double? tileZoom, EvictErrorTileStrategy evictionStrategy) { + final toRemove = []; + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (tile.coords.z != tileZoom) { + if (tile.loaded == null) { + toRemove.add(entry.key); + } + } + } + + for (final key in toRemove) { + final tile = _tiles[key]!; + + tile.tileReady = null; + tile.dispose(tile.loadError && evictionStrategy != EvictErrorTileStrategy.none); + _tiles.remove(key); + } + } + + void prune(double? zoom, EvictErrorTileStrategy evictStrategy) { + if (zoom == null) { + removeAll(evictStrategy); + return; + } + + for (final entry in _tiles.entries) { + final tile = entry.value; + tile.retain = tile.current; + } + + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (tile.current && !tile.active) { + final coords = tile.coords; + if (!_retainParent(coords.x, coords.y, coords.z, coords.z - 5)) { + _retainChildren(coords.x, coords.y, coords.z, coords.z + 2); + } + } + } + + final toRemove = []; + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (!tile.retain) { + toRemove.add(entry.key); + } + } + + for (final key in toRemove) { + remove(key, evictStrategy); + } + } + + void removeAll(EvictErrorTileStrategy evictStrategy) { + final toRemove = Map.from(_tiles); + + for (final key in toRemove.keys) { + remove(key, evictStrategy); + } + } + + void add(Coords coords, Tile tile) { + _tiles[coords.key] = tile; + } + + void remove(String key, EvictErrorTileStrategy evictStrategy) { + final tile = _tiles[key]; + if (tile == null) { + return; + } + + tile.dispose(tile.loadError && evictStrategy != EvictErrorTileStrategy.none); + _tiles.remove(key); + } + + void removeAtZoom(double zoom, EvictErrorTileStrategy evictStrategy) { + final toRemove = []; + for (final entry in _tiles.entries) { + if (entry.value.coords.z != zoom) { + continue; + } + toRemove.add(entry.key); + } + + for (final key in toRemove) { + remove(key, evictStrategy); + } + } + + + bool anyWithZoomLevel(double zoomLevel) { + for (final tile in _tiles.values) { + if (tile.coords.z == zoomLevel) { + return true; + } + } + + return false; + } + + + void markToPrune(double? currentZoom, Bounds noPruneRange) { + for (final entry in _tiles.entries) { + final tile = entry.value; + final c = tile.coords; + + if (!tile.current) continue; + if (c.z == currentZoom) continue; + if (noPruneRange.contains(CustomPoint(c.x, c.y))) continue; + tile.current = false; + } + } + + Tile? tileAt(Coords coords) => _tiles[coords.key]; + + bool get allLoaded { + for (final entry in _tiles.entries) { + if (entry.value.loaded == null) { + return false; + } + } + return true; + } + + bool allWithinZoom(double minZoom, double maxZoom) { + for (final tile in _tiles.values) { + if (tile.level.zoom > (maxZoom) || + tile.level.zoom < (minZoom)) { + return false; + } + } + return true; + } + + void reloadImages(TileLayerOptions options, Tuple2? wrapX, Tuple2? wrapY,) { + for (final tile in _tiles.values) { + tile.imageProvider = options.tileProvider + .getImage(tile.coords.wrap(wrapX, wrapY), options); + tile.loadTileImage(); + } + } + + bool markTileWithCoordsAsCurrent(Coords coords) { + final tile = _tiles[coords.key]; + if (tile != null) { + tile.current = true; + return true; + } else { + return false; + } + } + + void evictErrorTilesBasedOnStrategy(Bounds tileRange, EvictErrorTileStrategy evictStrategy) { + if (evictStrategy == + EvictErrorTileStrategy.notVisibleRespectMargin) { + final toRemove = []; + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (tile.loadError && !tile.current) { + toRemove.add(entry.key); + } + } + + for (final key in toRemove) { + final tile = _tiles[key]!; + + tile.dispose(true); + _tiles.remove(key); + } + } else if (evictStrategy == EvictErrorTileStrategy.notVisible) { + final toRemove = []; + for (final entry in _tiles.entries) { + final tile = entry.value; + final c = tile.coords; + + if (tile.loadError && + (!tile.current || !tileRange.contains(CustomPoint(c.x, c.y)))) { + toRemove.add(entry.key); + } + } + + for (final key in toRemove) { + final tile = _tiles[key]!; + + tile.dispose(true); + _tiles.remove(key); + } + } + } + + List sortedTiles() { + return _tiles.values.toList()..sort(); + } + + 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++) { + final coords = Coords(i, j); + coords.z = z + 1; + + final tile = _tiles[coords.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); + } + } + } + } + + bool _retainParent(double x, double y, double z, double minZoom) { + final x2 = (x / 2).floorToDouble(); + final y2 = (y / 2).floorToDouble(); + final z2 = z - 1; + final coords2 = Coords(x2, y2); + coords2.z = z2; + + final tile = _tiles[coords2.key]; + 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; + } +} \ No newline at end of file diff --git a/lib/src/layer/tile_layer/tile_widget.dart b/lib/src/layer/tile_layer/tile_widget.dart new file mode 100644 index 000000000..7f95b6400 --- /dev/null +++ b/lib/src/layer/tile_layer/tile_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/tile_layer/animated_tile.dart'; + +class TileWidget extends StatelessWidget { + final Tile tile; + final CustomPoint size; + final ImageProvider? errorImage; + final TileBuilder? tileBuilder; + + const TileWidget({ + required this.tile, + required this.size, + required this.errorImage, + required this.tileBuilder, + Key? key, + }) : super(key: key); + + + @override + Widget build(BuildContext context) { + final pos = tile.tilePos.multiplyBy(tile.level.scale) + tile.level.translatePoint; + final num width = size.x * tile.level.scale; + final num height = size.y * tile.level.scale; + + return Positioned( + left: pos.x.toDouble(), + top: pos.y.toDouble(), + width: width.toDouble(), + height: height.toDouble(), + child: AnimatedTile( + tile: tile, + errorImage: errorImage, + tileBuilder: tileBuilder, + ), + ); + } +} \ No newline at end of file From 8a9462bc7cfc9c326e733a27cc04cbec1e547fe5 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Thu, 26 May 2022 17:55:11 +0200 Subject: [PATCH 03/11] Remove unnecessary code --- lib/src/layer/tile_layer/tile_layer.dart | 51 ++++++++++-------------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 1d12bfe01..4914a1cc5 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -60,8 +60,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { Tuple2? _wrapY; double? _tileZoom; - //ignore: unused_field - Level? _level; StreamSubscription? _moveSub; StreamSubscription? _resetSub; StreamController? _throttleUpdate; @@ -102,7 +100,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { reloadTiles = true; } - reloadTiles |= !_tileManager.allWithinZoom(options.minZoom, options.maxZoom); + reloadTiles |= + !_tileManager.allWithinZoom(options.minZoom, options.maxZoom); if (oldWidget.options.updateInterval != options.updateInterval) { _throttleUpdate?.close(); @@ -244,7 +243,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _setZoomTransform(level, map.center, map.zoom); } - return _level = level; + return level; } ///removes all loaded tiles and resets the view @@ -289,7 +288,9 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); } - void _setZoomTransforms(LatLng center, double zoom) { + void _setZoomTransforms() { + final center = map.center; + final zoom = map.zoom; for (final i in _levels.keys) { _setZoomTransform(_levels[i]!, center, zoom); } @@ -352,7 +353,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { setState(() { _setView(map.center, tileZoom); - _setZoomTransforms(map.center, map.zoom); + _setZoomTransforms(); }); } } else { @@ -361,7 +362,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { // It was a zoom lvl change _setView(map.center, tileZoom); - _setZoomTransforms(map.center, map.zoom); + _setZoomTransforms(); } else { if (_throttleUpdate == null) { _update(null); @@ -369,7 +370,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { _throttleUpdate!.add(null); } - _setZoomTransforms(map.center, map.zoom); + _setZoomTransforms(); } }); } @@ -436,7 +437,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { } } - _tileManager.evictErrorTilesBasedOnStrategy(tileRange, options.evictErrorTileStrategy); + _tileManager.evictErrorTilesBasedOnStrategy( + tileRange, options.evictErrorTileStrategy); // sort tile queue to load tiles in order of their distance to center queue.sort((a, b) => @@ -445,15 +447,15 @@ class _TileLayerState extends State with TickerProviderStateMixin { for (var i = 0; i < queue.length; i++) { final coords = queue[i] as Coords; _tileManager.add( - coords, - Tile( - coords: coords, - tilePos: _getTilePos(coords), - current: true, - level: _levels[coords.z]!, - imageProvider: - options.tileProvider.getImage(coords.wrap(_wrapX, _wrapY), options), - tileReady: _tileReady, + coords, + Tile( + coords: coords, + tilePos: _getTilePos(coords), + current: true, + level: _levels[coords.z]!, + imageProvider: options.tileProvider + .getImage(coords.wrap(_wrapX, _wrapY), options), + tileReady: _tileReady, )..loadTileImage(), ); } @@ -492,15 +494,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { return pxBounds; } - //ignore: unused_element - Coords _keyToTileCoords(String key) { - final k = key.split(':'); - final coords = Coords(double.parse(k[0]), double.parse(k[1])); - coords.z = double.parse(k[2]); - - return coords; - } - void _tileReady(Coords coords, dynamic error, Tile? tile) { if (null != error) { debugPrint(error.toString()); @@ -514,7 +507,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { tile!.loadError = false; } - tile = _tileManager.tileAt(tile.coords); if (tile == null) return; @@ -560,7 +552,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { : const Duration(milliseconds: 50), () { if (mounted) { - setState((){ + setState(() { _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); }); } @@ -581,5 +573,4 @@ class _TileLayerState extends State with TickerProviderStateMixin { bounds.max.unscaleBy(tileSize).ceil() - const CustomPoint(1, 1), ); } - } From d4be9c0b0af31a3bf2ef22edb386c2172256c566 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 27 May 2022 10:40:44 +0200 Subject: [PATCH 04/11] Stop precalculating zindexes for levels, just calculate it during build as it's fast anyways and just complicates the code --- lib/src/layer/tile_layer/level.dart | 10 ++++-- lib/src/layer/tile_layer/tile.dart | 18 +++-------- lib/src/layer/tile_layer/tile_layer.dart | 33 ++++++++------------ lib/src/layer/tile_layer/tile_manager.dart | 36 ++++++++++++++-------- lib/src/map/map.dart | 3 ++ 5 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lib/src/layer/tile_layer/level.dart b/lib/src/layer/tile_layer/level.dart index 0d02e821a..d17d84065 100644 --- a/lib/src/layer/tile_layer/level.dart +++ b/lib/src/layer/tile_layer/level.dart @@ -1,9 +1,13 @@ import 'package:flutter_map/flutter_map.dart'; class Level { - late double zIndex; - CustomPoint? origin; - late double zoom; + final CustomPoint origin; + final double zoom; late CustomPoint translatePoint; late double scale; + + Level({ + required this.origin, + required this.zoom, + }); } diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index 0844c392f..7615ba518 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -6,7 +6,7 @@ import 'package:flutter_map/src/layer/tile_layer/level.dart'; typedef TileReady = void Function( Coords coords, dynamic error, Tile tile); -class Tile implements Comparable { +class Tile { final Coords coords; final CustomPoint tilePos; ImageProvider imageProvider; @@ -114,20 +114,11 @@ class Tile implements Comparable { } } - @override - int compareTo(Tile other) { - final zIndexA = level.zIndex; - final zIndexB = other.level.zIndex; - - if (zIndexA == zIndexB) { - return 0; - } else { - return zIndexB.compareTo(zIndexA); - } - } - String get coordsKey => coords.key; + double zIndex(double maxZoom, double currentZoom) => + maxZoom - (currentZoom - coords.z).abs(); + @override int get hashCode => coords.hashCode; @@ -136,4 +127,3 @@ class Tile implements Comparable { return other is Tile && coords == other.coords; } } - diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 4914a1cc5..b3015e383 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -163,7 +163,10 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final tilesToRender = _tileManager.sortedTiles(); + final tilesToRender = _tileZoom == null + ? _tileManager.all() + : _tileManager.sortedByDistanceToZoomAscending( + options.maxZoom, _tileZoom!); final tileWidgets = [ for (var tile in tilesToRender) TileWidget( @@ -210,18 +213,14 @@ class _TileLayerState extends State with TickerProviderStateMixin { Level? _updateLevels() { final zoom = _tileZoom; - final maxZoom = options.maxZoom; if (zoom == null) return null; final toRemove = []; for (final entry in _levels.entries) { final z = entry.key; - final lvl = entry.value; - if (z == zoom || _tileManager.anyWithZoomLevel(z)) { - lvl.zIndex = maxZoom - (zoom - z).abs(); - } else { + if (z != zoom || !_tileManager.anyWithZoomLevel(z)) { toRemove.add(z); } } @@ -232,13 +231,12 @@ class _TileLayerState extends State with TickerProviderStateMixin { } var level = _levels[zoom]; - final map = this.map; if (level == null) { - level = _levels[zoom] = Level(); - level.zIndex = maxZoom; - level.origin = map.project(map.unproject(map.getPixelOrigin()), zoom); - level.zoom = zoom; + level = _levels[zoom] = Level( + origin: map.project(map.unproject(map.getPixelOrigin()), zoom), + zoom: zoom, + ); _setZoomTransform(level, map.center, map.zoom); } @@ -299,10 +297,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void _setZoomTransform(Level level, LatLng center, double zoom) { final scale = map.getZoomScale(zoom, level.zoom); final pixelOrigin = map.getNewPixelOrigin(center, zoom).round(); - if (level.origin == null) { - return; - } - final translate = level.origin!.multiplyBy(scale) - pixelOrigin; + final translate = level.origin.multiplyBy(scale) - pixelOrigin; level.translatePoint = translate; level.scale = scale; } @@ -369,7 +364,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { } else { _throttleUpdate!.add(null); } - _setZoomTransforms(); } }); @@ -397,7 +391,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { final pixelBounds = _getTiledPixelBounds(center); final tileRange = _pxBoundsToTileRange(pixelBounds); final tileCenter = tileRange.center; - final queue = >[]; + final queue = >[]; final margin = options.keepBuffer; final noPruneRange = Bounds( tileRange.bottomLeft - CustomPoint(margin, -margin), @@ -444,8 +438,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { queue.sort((a, b) => (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt()); - for (var i = 0; i < queue.length; i++) { - final coords = queue[i] as Coords; + for (final coords in queue) { _tileManager.add( coords, Tile( @@ -563,7 +556,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { CustomPoint _getTilePos(Coords coords) { final level = _levels[coords.z as double]!; - return coords.scaleBy(getTileSize()) - level.origin!; + return coords.scaleBy(getTileSize()) - level.origin; } Bounds _pxBoundsToTileRange(Bounds bounds) { diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index f9a1c3a39..71bdfbafc 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -22,7 +22,8 @@ class TileManager { final tile = _tiles[key]!; tile.tileReady = null; - tile.dispose(tile.loadError && evictionStrategy != EvictErrorTileStrategy.none); + tile.dispose( + tile.loadError && evictionStrategy != EvictErrorTileStrategy.none); _tiles.remove(key); } } @@ -81,7 +82,8 @@ class TileManager { return; } - tile.dispose(tile.loadError && evictStrategy != EvictErrorTileStrategy.none); + tile.dispose( + tile.loadError && evictStrategy != EvictErrorTileStrategy.none); _tiles.remove(key); } @@ -99,7 +101,6 @@ class TileManager { } } - bool anyWithZoomLevel(double zoomLevel) { for (final tile in _tiles.values) { if (tile.coords.z == zoomLevel) { @@ -110,7 +111,6 @@ class TileManager { return false; } - void markToPrune(double? currentZoom, Bounds noPruneRange) { for (final entry in _tiles.entries) { final tile = entry.value; @@ -136,15 +136,18 @@ class TileManager { bool allWithinZoom(double minZoom, double maxZoom) { for (final tile in _tiles.values) { - if (tile.level.zoom > (maxZoom) || - tile.level.zoom < (minZoom)) { + if (tile.level.zoom > (maxZoom) || tile.level.zoom < (minZoom)) { return false; } } return true; } - void reloadImages(TileLayerOptions options, Tuple2? wrapX, Tuple2? wrapY,) { + void reloadImages( + TileLayerOptions options, + Tuple2? wrapX, + Tuple2? wrapY, + ) { for (final tile in _tiles.values) { tile.imageProvider = options.tileProvider .getImage(tile.coords.wrap(wrapX, wrapY), options); @@ -162,9 +165,9 @@ class TileManager { } } - void evictErrorTilesBasedOnStrategy(Bounds tileRange, EvictErrorTileStrategy evictStrategy) { - if (evictStrategy == - EvictErrorTileStrategy.notVisibleRespectMargin) { + void evictErrorTilesBasedOnStrategy( + Bounds tileRange, EvictErrorTileStrategy evictStrategy) { + if (evictStrategy == EvictErrorTileStrategy.notVisibleRespectMargin) { final toRemove = []; for (final entry in _tiles.entries) { final tile = entry.value; @@ -201,8 +204,15 @@ class TileManager { } } - List sortedTiles() { - return _tiles.values.toList()..sort(); + List all() { + return _tiles.values.toList(); + } + + List sortedByDistanceToZoomAscending( + double maxZoom, double currentZoom) { + return [..._tiles.values]..sort((a, b) => a + .zIndex(maxZoom, currentZoom) + .compareTo(b.zIndex(maxZoom, currentZoom))); } void _retainChildren(double x, double y, double z, double maxZoom) { @@ -251,4 +261,4 @@ class TileManager { return false; } -} \ No newline at end of file +} diff --git a/lib/src/map/map.dart b/lib/src/map/map.dart index 0b434ef67..8897ef906 100644 --- a/lib/src/map/map.dart +++ b/lib/src/map/map.dart @@ -10,6 +10,7 @@ import 'package:latlong2/latlong.dart'; class MapControllerImpl implements MapController { final Completer _readyCompleter = Completer(); final StreamController _mapEventSink = StreamController.broadcast(); + @override StreamSink get mapEventSink => _mapEventSink.sink; @@ -22,6 +23,7 @@ class MapControllerImpl implements MapController { } late final MapState _state; + @override set state(MapState state) { _state = state; @@ -129,6 +131,7 @@ class MapState { double _rotationRad; double get zoom => _zoom; + double get rotation => _rotation; set rotation(double rotation) { From acf06602f3c041149c6771eb8a24bc844784d41b Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 27 May 2022 11:54:35 +0200 Subject: [PATCH 05/11] Calculate tile translations during build Previously we listened to the movement stream, calculated the translations and then called setState(). This introduced noticible jank/lag where when flinging the map or moving it with an AnimatedController other layers (e.g. markers) would not move together with the map. --- lib/src/layer/tile_layer/level.dart | 2 - lib/src/layer/tile_layer/tile.dart | 3 - lib/src/layer/tile_layer/tile_layer.dart | 64 ++++++------------- lib/src/layer/tile_layer/tile_manager.dart | 2 +- .../layer/tile_layer/tile_transformation.dart | 13 ++++ lib/src/layer/tile_layer/tile_widget.dart | 13 ++-- .../tile_layer/transformation_calculator.dart | 40 ++++++++++++ 7 files changed, 81 insertions(+), 56 deletions(-) create mode 100644 lib/src/layer/tile_layer/tile_transformation.dart create mode 100644 lib/src/layer/tile_layer/transformation_calculator.dart diff --git a/lib/src/layer/tile_layer/level.dart b/lib/src/layer/tile_layer/level.dart index d17d84065..6906ecc73 100644 --- a/lib/src/layer/tile_layer/level.dart +++ b/lib/src/layer/tile_layer/level.dart @@ -3,8 +3,6 @@ import 'package:flutter_map/flutter_map.dart'; class Level { final CustomPoint origin; final double zoom; - late CustomPoint translatePoint; - late double scale; Level({ required this.origin, diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index 7615ba518..316f6c28a 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -1,7 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/tile_layer/coords.dart'; -import 'package:flutter_map/src/layer/tile_layer/level.dart'; typedef TileReady = void Function( Coords coords, dynamic error, Tile tile); @@ -10,7 +9,6 @@ class Tile { final Coords coords; final CustomPoint tilePos; ImageProvider imageProvider; - final Level level; bool current; bool retain; @@ -37,7 +35,6 @@ class Tile { required this.tilePos, required this.imageProvider, this.tileReady, - required this.level, this.current = false, this.active = false, this.retain = false, diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index b3015e383..f2e8a081d 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -10,7 +10,9 @@ import 'package:flutter_map/src/core/util.dart' as util; import 'package:flutter_map/src/layer/tile_layer/coords.dart'; import 'package:flutter_map/src/layer/tile_layer/level.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_manager.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_transformation.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_widget.dart'; +import 'package:flutter_map/src/layer/tile_layer/transformation_calculator.dart'; import 'package:flutter_map/src/map/map.dart'; import 'package:latlong2/latlong.dart'; import 'package:tuple/tuple.dart'; @@ -66,7 +68,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { late CustomPoint _tileSize; late final TileManager _tileManager; - final Map _levels = {}; + late final TransformationCalculator _transformationCalculator; Timer? _pruneLater; @@ -74,6 +76,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { void initState() { super.initState(); _tileManager = TileManager(); + _transformationCalculator = TransformationCalculator(); _tileSize = CustomPoint(options.tileSize, options.tileSize); _resetView(); _update(null); @@ -167,11 +170,20 @@ class _TileLayerState extends State with TickerProviderStateMixin { ? _tileManager.all() : _tileManager.sortedByDistanceToZoomAscending( options.maxZoom, _tileZoom!); + + final Map zoomToTransformation = {}; + final tileWidgets = [ for (var tile in tilesToRender) TileWidget( tile: tile, size: _tileSize, + tileTransformation: zoomToTransformation[tile.coords.z] ?? + (zoomToTransformation[tile.coords.z] = + _transformationCalculator.transformationFor( + tile.coords.z, + map, + )), errorImage: options.errorImage, tileBuilder: options.tileBuilder, key: ValueKey(tile.coordsKey), @@ -216,32 +228,15 @@ class _TileLayerState extends State with TickerProviderStateMixin { if (zoom == null) return null; - final toRemove = []; - for (final entry in _levels.entries) { - final z = entry.key; - - if (z != zoom || !_tileManager.anyWithZoomLevel(z)) { - toRemove.add(z); - } - } + final toRemove = _transformationCalculator.whereLevel((levelZoom) => + levelZoom != zoom || !_tileManager.anyWithZoomLevel(levelZoom)); for (final z in toRemove) { _tileManager.removeAtZoom(z, options.evictErrorTileStrategy); - _levels.remove(z); + _transformationCalculator.removeLevel(z); } - var level = _levels[zoom]; - - if (level == null) { - level = _levels[zoom] = Level( - origin: map.project(map.unproject(map.getPixelOrigin()), zoom), - zoom: zoom, - ); - - _setZoomTransform(level, map.center, map.zoom); - } - - return level; + return _transformationCalculator.getOrCreateLevel(zoom, map); } ///removes all loaded tiles and resets the view @@ -286,22 +281,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileManager.prune(_tileZoom, options.evictErrorTileStrategy); } - void _setZoomTransforms() { - final center = map.center; - final zoom = map.zoom; - for (final i in _levels.keys) { - _setZoomTransform(_levels[i]!, center, zoom); - } - } - - void _setZoomTransform(Level level, LatLng center, double zoom) { - final scale = map.getZoomScale(zoom, level.zoom); - final pixelOrigin = map.getNewPixelOrigin(center, zoom).round(); - final translate = level.origin.multiplyBy(scale) - pixelOrigin; - level.translatePoint = translate; - level.scale = scale; - } - void _resetGrid() { final map = this.map; final crs = map.options.crs; @@ -347,8 +326,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { _tileZoom = tileZoom; setState(() { _setView(map.center, tileZoom); - - _setZoomTransforms(); }); } } else { @@ -356,15 +333,12 @@ class _TileLayerState extends State with TickerProviderStateMixin { if ((tileZoom - _tileZoom!).abs() >= 1) { // It was a zoom lvl change _setView(map.center, tileZoom); - - _setZoomTransforms(); } else { if (_throttleUpdate == null) { _update(null); } else { _throttleUpdate!.add(null); } - _setZoomTransforms(); } }); } @@ -445,7 +419,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { coords: coords, tilePos: _getTilePos(coords), current: true, - level: _levels[coords.z]!, imageProvider: options.tileProvider .getImage(coords.wrap(_wrapX, _wrapY), options), tileReady: _tileReady, @@ -555,7 +528,8 @@ class _TileLayerState extends State with TickerProviderStateMixin { } CustomPoint _getTilePos(Coords coords) { - final level = _levels[coords.z as double]!; + final level = + _transformationCalculator.getOrCreateLevel(coords.z as double, map); return coords.scaleBy(getTileSize()) - level.origin; } diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index 71bdfbafc..48bbd7aaf 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -136,7 +136,7 @@ class TileManager { bool allWithinZoom(double minZoom, double maxZoom) { for (final tile in _tiles.values) { - if (tile.level.zoom > (maxZoom) || tile.level.zoom < (minZoom)) { + if (tile.coords.z > (maxZoom) || tile.coords.z < (minZoom)) { return false; } } diff --git a/lib/src/layer/tile_layer/tile_transformation.dart b/lib/src/layer/tile_layer/tile_transformation.dart new file mode 100644 index 000000000..8cb3fe217 --- /dev/null +++ b/lib/src/layer/tile_layer/tile_transformation.dart @@ -0,0 +1,13 @@ +import 'package:flutter_map/flutter_map.dart'; +import 'package:meta/meta.dart'; + +@immutable +class TileTransformation { + final double scale; + final CustomPoint translate; + + const TileTransformation({ + required this.scale, + required this.translate, + }); +} diff --git a/lib/src/layer/tile_layer/tile_widget.dart b/lib/src/layer/tile_layer/tile_widget.dart index 7f95b6400..68af0cdcc 100644 --- a/lib/src/layer/tile_layer/tile_widget.dart +++ b/lib/src/layer/tile_layer/tile_widget.dart @@ -1,27 +1,30 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/tile_layer/animated_tile.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_transformation.dart'; class TileWidget extends StatelessWidget { final Tile tile; final CustomPoint size; + final TileTransformation tileTransformation; final ImageProvider? errorImage; final TileBuilder? tileBuilder; const TileWidget({ required this.tile, required this.size, + required this.tileTransformation, required this.errorImage, required this.tileBuilder, Key? key, }) : super(key: key); - @override Widget build(BuildContext context) { - final pos = tile.tilePos.multiplyBy(tile.level.scale) + tile.level.translatePoint; - final num width = size.x * tile.level.scale; - final num height = size.y * tile.level.scale; + final pos = tile.tilePos.multiplyBy(tileTransformation.scale) + + tileTransformation.translate; + final num width = size.x * tileTransformation.scale; + final num height = size.y * tileTransformation.scale; return Positioned( left: pos.x.toDouble(), @@ -35,4 +38,4 @@ class TileWidget extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/layer/tile_layer/transformation_calculator.dart b/lib/src/layer/tile_layer/transformation_calculator.dart new file mode 100644 index 000000000..6f1419198 --- /dev/null +++ b/lib/src/layer/tile_layer/transformation_calculator.dart @@ -0,0 +1,40 @@ +import 'package:flutter_map/src/layer/tile_layer/level.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_transformation.dart'; +import 'package:flutter_map/src/map/map.dart'; + +class TransformationCalculator { + final Map _levels = {}; + + Level? levelAt(double zoom) => _levels[zoom]; + + Level getOrCreateLevel(double zoom, MapState map) { + final level = _levels[zoom]; + if (level != null) return level; + + return _levels[zoom] = Level( + origin: map.project(map.unproject(map.getPixelOrigin()), zoom), + zoom: zoom, + ); + } + + List whereLevel(bool Function(double level) test) { + final result = []; + for (final levelZoom in _levels.keys) { + if (test(levelZoom)) result.add(levelZoom); + } + + return result; + } + + void removeLevel(double levelZoom) { + _levels.remove(levelZoom); + } + + TileTransformation transformationFor(double levelZoom, MapState map) { + final level = _levels[levelZoom]!; + final scale = map.getZoomScale(map.zoom, level.zoom); + final pixelOrigin = map.getNewPixelOrigin(map.center, map.zoom).round(); + final translate = level.origin.multiplyBy(scale) - pixelOrigin; + return TileTransformation(scale: scale, translate: translate); + } +} From 6ebf1e11de9556771ed4ad4e12a54b046a2a941d Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 27 May 2022 13:55:05 +0200 Subject: [PATCH 06/11] Fix tile sorting and level removing check --- lib/src/layer/tile_layer/tile_layer.dart | 2 +- lib/src/layer/tile_layer/tile_manager.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index f2e8a081d..710e797e8 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -229,7 +229,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { if (zoom == null) return null; final toRemove = _transformationCalculator.whereLevel((levelZoom) => - levelZoom != zoom || !_tileManager.anyWithZoomLevel(levelZoom)); + levelZoom != zoom && !_tileManager.anyWithZoomLevel(levelZoom)); for (final z in toRemove) { _tileManager.removeAtZoom(z, options.evictErrorTileStrategy); diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index 48bbd7aaf..568cc25fe 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -210,9 +210,9 @@ class TileManager { List sortedByDistanceToZoomAscending( double maxZoom, double currentZoom) { - return [..._tiles.values]..sort((a, b) => a + return [..._tiles.values]..sort((a, b) => b .zIndex(maxZoom, currentZoom) - .compareTo(b.zIndex(maxZoom, currentZoom))); + .compareTo(a.zIndex(maxZoom, currentZoom))); } void _retainChildren(double x, double y, double z, double maxZoom) { From 2b7f5a8e00fdea173b477c8f670a6d69c28cb3fc Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Fri, 27 May 2022 14:14:37 +0200 Subject: [PATCH 07/11] Final changes * Run dart format * Re-fix the tile ordering function, it was putting lower-zoom tiles. above higher zoom ones. * Add the StreamBuilder to trigger builds as a result of map movement. --- lib/src/layer/tile_layer/animated_tile.dart | 14 +-- lib/src/layer/tile_layer/coords.dart | 11 +- lib/src/layer/tile_layer/tile_layer.dart | 106 +++++++++-------- .../layer/tile_layer/tile_layer_options.dart | 111 +++++++++--------- lib/src/layer/tile_layer/tile_manager.dart | 4 +- .../tile_provider/file_tile_provider_io.dart | 1 - 6 files changed, 123 insertions(+), 124 deletions(-) diff --git a/lib/src/layer/tile_layer/animated_tile.dart b/lib/src/layer/tile_layer/animated_tile.dart index 13b2a4c26..53380f6eb 100644 --- a/lib/src/layer/tile_layer/animated_tile.dart +++ b/lib/src/layer/tile_layer/animated_tile.dart @@ -1,4 +1,3 @@ - import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -25,13 +24,13 @@ class _AnimatedTileState extends State { Widget build(BuildContext context) { final tileWidget = (widget.tile.loadError && widget.errorImage != null) ? Image( - image: widget.errorImage!, - fit: BoxFit.fill, - ) + image: widget.errorImage!, + fit: BoxFit.fill, + ) : RawImage( - image: widget.tile.imageInfo?.image, - fit: BoxFit.fill, - opacity: widget.tile.animationController); + image: widget.tile.imageInfo?.image, + fit: BoxFit.fill, + opacity: widget.tile.animationController); return widget.tileBuilder == null ? tileWidget @@ -73,4 +72,3 @@ class _AnimatedTileState extends State { } } } - diff --git a/lib/src/layer/tile_layer/coords.dart b/lib/src/layer/tile_layer/coords.dart index 943117023..f927be44b 100644 --- a/lib/src/layer/tile_layer/coords.dart +++ b/lib/src/layer/tile_layer/coords.dart @@ -9,14 +9,11 @@ class Coords extends CustomPoint { Coords(T x, T y) : super(x, y); - Coords wrap(Tuple2? wrapX, Tuple2? wrapY) { + Coords wrap( + Tuple2? wrapX, Tuple2? wrapY) { final newCoords = Coords( - wrapX != null - ? util.wrapNum(x.toDouble(), wrapX) - : x.toDouble(), - wrapY != null - ? util.wrapNum(y.toDouble(), wrapY) - : y.toDouble(), + wrapX != null ? util.wrapNum(x.toDouble(), wrapX) : x.toDouble(), + wrapY != null ? util.wrapNum(y.toDouble(), wrapY) : y.toDouble(), ); newCoords.z = z.toDouble(); return newCoords; diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 710e797e8..c187a6634 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -166,56 +166,62 @@ class _TileLayerState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final tilesToRender = _tileZoom == null - ? _tileManager.all() - : _tileManager.sortedByDistanceToZoomAscending( - options.maxZoom, _tileZoom!); - - final Map zoomToTransformation = {}; - - final tileWidgets = [ - for (var tile in tilesToRender) - TileWidget( - tile: tile, - size: _tileSize, - tileTransformation: zoomToTransformation[tile.coords.z] ?? - (zoomToTransformation[tile.coords.z] = - _transformationCalculator.transformationFor( - tile.coords.z, - map, - )), - errorImage: options.errorImage, - tileBuilder: options.tileBuilder, - key: ValueKey(tile.coordsKey), - ) - ]; - - final tilesContainer = Stack( - children: tileWidgets, - ); - - final tilesLayer = options.tilesContainerBuilder == null - ? tilesContainer - : options.tilesContainerBuilder!( - context, - tilesContainer, - tilesToRender, - ); - - final attributionLayer = widget.options.attributionBuilder?.call(context); - - return Opacity( - opacity: options.opacity, - child: Container( - color: options.backgroundColor, - child: Stack( - alignment: widget.options.attributionAlignment, - children: [ - tilesLayer, - if (attributionLayer != null) attributionLayer, - ], - ), - ), + return StreamBuilder( + stream: widget.stream, + builder: (context, snapshot) { + final tilesToRender = _tileZoom == null + ? _tileManager.all() + : _tileManager.sortedByDistanceToZoomAscending( + options.maxZoom, _tileZoom!); + + final Map zoomToTransformation = {}; + + final tileWidgets = [ + for (var tile in tilesToRender) + TileWidget( + tile: tile, + size: _tileSize, + tileTransformation: zoomToTransformation[tile.coords.z] ?? + (zoomToTransformation[tile.coords.z] = + _transformationCalculator.transformationFor( + tile.coords.z, + map, + )), + errorImage: options.errorImage, + tileBuilder: options.tileBuilder, + key: ValueKey(tile.coordsKey), + ) + ]; + + final tilesContainer = Stack( + children: tileWidgets, + ); + + final tilesLayer = options.tilesContainerBuilder == null + ? tilesContainer + : options.tilesContainerBuilder!( + context, + tilesContainer, + tilesToRender, + ); + + final attributionLayer = + widget.options.attributionBuilder?.call(context); + + return Opacity( + opacity: options.opacity, + child: Container( + color: options.backgroundColor, + child: Stack( + alignment: widget.options.attributionAlignment, + children: [ + tilesLayer, + if (attributionLayer != null) attributionLayer, + ], + ), + ), + ); + }, ); } diff --git a/lib/src/layer/tile_layer/tile_layer_options.dart b/lib/src/layer/tile_layer/tile_layer_options.dart index 829d66de6..882917942 100644 --- a/lib/src/layer/tile_layer/tile_layer_options.dart +++ b/lib/src/layer/tile_layer/tile_layer_options.dart @@ -238,54 +238,54 @@ class TileLayerOptions extends LayerOptions { TileLayerOptions( {this.attributionAlignment = Alignment.bottomRight, - this.attributionBuilder, - Key? key, - // TODO: make required - 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, - Map? additionalOptions, - this.subdomains = const [], - this.keepBuffer = 2, - this.backgroundColor = const Color(0xFFE0E0E0), - this.placeholderImage, - this.errorImage, - this.tileProvider = const NonCachingNetworkTileProvider(), - 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) - // TODO: change to Duration - int updateInterval = 200, - // Tiles fade in duration in milliseconds (default 100). This can be set to - // 0 to avoid fade in - // TODO: change to Duration - int tileFadeInDuration = 100, - this.tileFadeInStart = 0.0, - this.tileFadeInStartWhenOverride = 0.0, - this.overrideTilesWhenUrlChanges = false, - this.retinaMode = false, - this.errorTileCallback, - Stream? rebuild, - this.templateFunction = util.template, - this.tileBuilder, - this.tilesContainerBuilder, - this.evictErrorTileStrategy = EvictErrorTileStrategy.none, - this.fastReplace = false, - this.reset, - this.tileBounds}) + this.attributionBuilder, + Key? key, + // TODO: make required + 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, + Map? additionalOptions, + this.subdomains = const [], + this.keepBuffer = 2, + this.backgroundColor = const Color(0xFFE0E0E0), + this.placeholderImage, + this.errorImage, + this.tileProvider = const NonCachingNetworkTileProvider(), + 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) + // TODO: change to Duration + int updateInterval = 200, + // Tiles fade in duration in milliseconds (default 100). This can be set to + // 0 to avoid fade in + // TODO: change to Duration + int tileFadeInDuration = 100, + this.tileFadeInStart = 0.0, + this.tileFadeInStartWhenOverride = 0.0, + this.overrideTilesWhenUrlChanges = false, + this.retinaMode = false, + this.errorTileCallback, + Stream? rebuild, + this.templateFunction = util.template, + this.tileBuilder, + this.tilesContainerBuilder, + this.evictErrorTileStrategy = EvictErrorTileStrategy.none, + this.fastReplace = false, + this.reset, + this.tileBounds}) : updateInterval = - updateInterval <= 0 ? null : Duration(milliseconds: updateInterval), + updateInterval <= 0 ? null : Duration(milliseconds: updateInterval), tileFadeInDuration = tileFadeInDuration <= 0 ? null : Duration(milliseconds: tileFadeInDuration), @@ -293,21 +293,21 @@ class TileLayerOptions extends LayerOptions { assert(tileFadeInStartWhenOverride >= 0.0 && tileFadeInStartWhenOverride <= 1.0), maxZoom = - wmsOptions == null && retinaMode && maxZoom > 0.0 && !zoomReverse - ? maxZoom - 1.0 - : 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) - : minZoom, + wmsOptions == null && retinaMode && maxZoom > 0.0 && zoomReverse + ? math.max(minZoom + 1.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, - // copy additionalOptions Map if not null, so we can safely compare old - // and new Map inside didUpdateWidget with MapEquality. + // copy additionalOptions Map if not null, so we can safely compare old + // and new Map inside didUpdateWidget with MapEquality. additionalOptions = additionalOptions == null ? const {} : Map.from(additionalOptions), @@ -402,4 +402,3 @@ class WMSTileLayerOptions { return buffer.toString(); } } - diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index 568cc25fe..48bbd7aaf 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -210,9 +210,9 @@ class TileManager { List sortedByDistanceToZoomAscending( double maxZoom, double currentZoom) { - return [..._tiles.values]..sort((a, b) => b + return [..._tiles.values]..sort((a, b) => a .zIndex(maxZoom, currentZoom) - .compareTo(a.zIndex(maxZoom, currentZoom))); + .compareTo(b.zIndex(maxZoom, currentZoom))); } void _retainChildren(double x, double y, double z, double maxZoom) { diff --git a/lib/src/layer/tile_provider/file_tile_provider_io.dart b/lib/src/layer/tile_provider/file_tile_provider_io.dart index dced715a2..ab982f563 100644 --- a/lib/src/layer/tile_provider/file_tile_provider_io.dart +++ b/lib/src/layer/tile_provider/file_tile_provider_io.dart @@ -4,7 +4,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/tile_layer/coords.dart'; - /// FileTileProvider class FileTileProvider extends TileProvider { const FileTileProvider(); From e38cce6a23b8812c81842715afee19a222ea7e0d Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 28 May 2022 09:37:51 +0200 Subject: [PATCH 08/11] Fix Tile tileReady callback race condition The Tile loading was initiated before adding the Tile to the TileManager which meant that with a very fast connection or cached Tile images the load callback would be called before the Tile was in the TileManager. Therefore when we tried to set the loaded value of the Tile in the callback we would not find the Tile and the Tile was never marked as loaded. This caused the tile to be removed sooner than it should be since we prune tiles that are not loaded yet when zooming in. --- lib/src/layer/tile_layer/tile_layer.dart | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index c187a6634..921724efd 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -173,7 +173,6 @@ class _TileLayerState extends State with TickerProviderStateMixin { ? _tileManager.all() : _tileManager.sortedByDistanceToZoomAscending( options.maxZoom, _tileZoom!); - final Map zoomToTransformation = {}; final tileWidgets = [ @@ -419,17 +418,20 @@ class _TileLayerState extends State with TickerProviderStateMixin { (a.distanceTo(tileCenter) - b.distanceTo(tileCenter)).toInt()); for (final coords in queue) { - _tileManager.add( - coords, - Tile( - coords: coords, - tilePos: _getTilePos(coords), - current: true, - imageProvider: options.tileProvider - .getImage(coords.wrap(_wrapX, _wrapY), options), - tileReady: _tileReady, - )..loadTileImage(), + final newTile = Tile( + coords: coords, + tilePos: _getTilePos(coords), + current: true, + imageProvider: + options.tileProvider.getImage(coords.wrap(_wrapX, _wrapY), options), + tileReady: _tileReady, ); + + _tileManager.add(coords, newTile); + // If we do this before adding the Tile to the TileManager the _tileReady + // callback may be fired very fast and we won't find the Tile in the + // TileManager since it's not added yet. + newTile.loadTileImage(); } } From 9cd6799339c4e498fcf21988a0ee52120a314266 Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 28 May 2022 09:42:50 +0200 Subject: [PATCH 09/11] Small tidy-ups --- lib/src/layer/tile_layer/tile_layer.dart | 8 ++--- .../layer/tile_layer/tile_layer_options.dart | 2 -- lib/src/layer/tile_layer/tile_manager.dart | 36 ++++++++----------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 921724efd..2f454a947 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -48,9 +48,7 @@ class TileLayer extends StatefulWidget { }) : super(key: options.key); @override - State createState() { - return _TileLayerState(); - } + State createState() => _TileLayerState(); } class _TileLayerState extends State with TickerProviderStateMixin { @@ -224,9 +222,7 @@ class _TileLayerState extends State with TickerProviderStateMixin { ); } - CustomPoint getTileSize() { - return _tileSize; - } + CustomPoint getTileSize() => _tileSize; Level? _updateLevels() { final zoom = _tileZoom; diff --git a/lib/src/layer/tile_layer/tile_layer_options.dart b/lib/src/layer/tile_layer/tile_layer_options.dart index 882917942..5dc4aec92 100644 --- a/lib/src/layer/tile_layer/tile_layer_options.dart +++ b/lib/src/layer/tile_layer/tile_layer_options.dart @@ -117,7 +117,6 @@ class TileLayerOptions extends LayerOptions { /// /// Furthermore you create your custom implementation by subclassing /// TileProvider - /// final TileProvider tileProvider; /// When panning the map, keep this many rows and columns of tiles before @@ -146,7 +145,6 @@ class TileLayerOptions extends LayerOptions { /// }, /// ), /// ``` - /// final Map additionalOptions; /// Tiles will not update more than once every `updateInterval` (default 200 diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index 48bbd7aaf..e1e73a920 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -11,10 +11,8 @@ class TileManager { for (final entry in _tiles.entries) { final tile = entry.value; - if (tile.coords.z != tileZoom) { - if (tile.loaded == null) { - toRemove.add(entry.key); - } + if (tile.coords.z != tileZoom && tile.loaded == null) { + toRemove.add(entry.key); } } @@ -64,6 +62,10 @@ class TileManager { } } + void add(Coords coords, Tile tile) { + _tiles[coords.key] = tile; + } + void removeAll(EvictErrorTileStrategy evictStrategy) { final toRemove = Map.from(_tiles); @@ -72,10 +74,6 @@ class TileManager { } } - void add(Coords coords, Tile tile) { - _tiles[coords.key] = tile; - } - void remove(String key, EvictErrorTileStrategy evictStrategy) { final tile = _tiles[key]; if (tile == null) { @@ -84,6 +82,7 @@ class TileManager { tile.dispose( tile.loadError && evictStrategy != EvictErrorTileStrategy.none); + _tiles.remove(key); } @@ -116,10 +115,11 @@ class TileManager { final tile = entry.value; final c = tile.coords; - if (!tile.current) continue; - if (c.z == currentZoom) continue; - if (noPruneRange.contains(CustomPoint(c.x, c.y))) continue; - tile.current = false; + if (tile.current && + (c.z != currentZoom || + !noPruneRange.contains(CustomPoint(c.x, c.y)))) { + tile.current = false; + } } } @@ -178,9 +178,7 @@ class TileManager { } for (final key in toRemove) { - final tile = _tiles[key]!; - - tile.dispose(true); + _tiles[key]!.dispose(true); _tiles.remove(key); } } else if (evictStrategy == EvictErrorTileStrategy.notVisible) { @@ -196,17 +194,13 @@ class TileManager { } for (final key in toRemove) { - final tile = _tiles[key]!; - - tile.dispose(true); + _tiles[key]!.dispose(true); _tiles.remove(key); } } } - List all() { - return _tiles.values.toList(); - } + List all() => _tiles.values.toList(); List sortedByDistanceToZoomAscending( double maxZoom, double currentZoom) { From 58fd61b26888e9138062716f53d154a8565798be Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 28 May 2022 09:48:17 +0200 Subject: [PATCH 10/11] Re-order TileManager functions to a more logical order --- lib/src/layer/tile_layer/tile_manager.dart | 198 ++++++++++----------- 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index e1e73a920..8fcee3b69 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -6,59 +6,52 @@ import 'package:tuple/tuple.dart'; class TileManager { final Map _tiles = {}; - void abortLoading(double? tileZoom, EvictErrorTileStrategy evictionStrategy) { - final toRemove = []; - for (final entry in _tiles.entries) { - final tile = entry.value; + List all() => _tiles.values.toList(); - if (tile.coords.z != tileZoom && tile.loaded == null) { - toRemove.add(entry.key); + List sortedByDistanceToZoomAscending( + double maxZoom, double currentZoom) { + return [..._tiles.values]..sort((a, b) => a + .zIndex(maxZoom, currentZoom) + .compareTo(b.zIndex(maxZoom, currentZoom))); + } + + bool anyWithZoomLevel(double zoomLevel) { + for (final tile in _tiles.values) { + if (tile.coords.z == zoomLevel) { + return true; } } - for (final key in toRemove) { - final tile = _tiles[key]!; - - tile.tileReady = null; - tile.dispose( - tile.loadError && evictionStrategy != EvictErrorTileStrategy.none); - _tiles.remove(key); - } + return false; } - void prune(double? zoom, EvictErrorTileStrategy evictStrategy) { - if (zoom == null) { - removeAll(evictStrategy); - return; - } - - for (final entry in _tiles.entries) { - final tile = entry.value; - tile.retain = tile.current; - } + Tile? tileAt(Coords coords) => _tiles[coords.key]; + bool get allLoaded { for (final entry in _tiles.entries) { - final tile = entry.value; - - if (tile.current && !tile.active) { - final coords = tile.coords; - if (!_retainParent(coords.x, coords.y, coords.z, coords.z - 5)) { - _retainChildren(coords.x, coords.y, coords.z, coords.z + 2); - } + if (entry.value.loaded == null) { + return false; } } + return true; + } - final toRemove = []; - for (final entry in _tiles.entries) { - final tile = entry.value; - - if (!tile.retain) { - toRemove.add(entry.key); + bool allWithinZoom(double minZoom, double maxZoom) { + for (final tile in _tiles.values) { + if (tile.coords.z > (maxZoom) || tile.coords.z < (minZoom)) { + return false; } } + return true; + } - for (final key in toRemove) { - remove(key, evictStrategy); + bool markTileWithCoordsAsCurrent(Coords coords) { + final tile = _tiles[coords.key]; + if (tile != null) { + tile.current = true; + return true; + } else { + return false; } } @@ -66,14 +59,6 @@ class TileManager { _tiles[coords.key] = tile; } - void removeAll(EvictErrorTileStrategy evictStrategy) { - final toRemove = Map.from(_tiles); - - for (final key in toRemove.keys) { - remove(key, evictStrategy); - } - } - void remove(String key, EvictErrorTileStrategy evictStrategy) { final tile = _tiles[key]; if (tile == null) { @@ -86,6 +71,14 @@ class TileManager { _tiles.remove(key); } + void removeAll(EvictErrorTileStrategy evictStrategy) { + final toRemove = Map.from(_tiles); + + for (final key in toRemove.keys) { + remove(key, evictStrategy); + } + } + void removeAtZoom(double zoom, EvictErrorTileStrategy evictStrategy) { final toRemove = []; for (final entry in _tiles.entries) { @@ -100,14 +93,36 @@ class TileManager { } } - bool anyWithZoomLevel(double zoomLevel) { + void reloadImages( + TileLayerOptions options, + Tuple2? wrapX, + Tuple2? wrapY, + ) { for (final tile in _tiles.values) { - if (tile.coords.z == zoomLevel) { - return true; + tile.imageProvider = options.tileProvider + .getImage(tile.coords.wrap(wrapX, wrapY), options); + tile.loadTileImage(); + } + } + + void abortLoading(double? tileZoom, EvictErrorTileStrategy evictionStrategy) { + final toRemove = []; + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (tile.coords.z != tileZoom && tile.loaded == null) { + toRemove.add(entry.key); } } - return false; + for (final key in toRemove) { + final tile = _tiles[key]!; + + tile.tileReady = null; + tile.dispose( + tile.loadError && evictionStrategy != EvictErrorTileStrategy.none); + _tiles.remove(key); + } } void markToPrune(double? currentZoom, Bounds noPruneRange) { @@ -123,48 +138,6 @@ class TileManager { } } - Tile? tileAt(Coords coords) => _tiles[coords.key]; - - bool get allLoaded { - for (final entry in _tiles.entries) { - if (entry.value.loaded == null) { - return false; - } - } - return true; - } - - bool allWithinZoom(double minZoom, double maxZoom) { - for (final tile in _tiles.values) { - if (tile.coords.z > (maxZoom) || tile.coords.z < (minZoom)) { - return false; - } - } - return true; - } - - void reloadImages( - TileLayerOptions options, - Tuple2? wrapX, - Tuple2? wrapY, - ) { - for (final tile in _tiles.values) { - tile.imageProvider = options.tileProvider - .getImage(tile.coords.wrap(wrapX, wrapY), options); - tile.loadTileImage(); - } - } - - bool markTileWithCoordsAsCurrent(Coords coords) { - final tile = _tiles[coords.key]; - if (tile != null) { - tile.current = true; - return true; - } else { - return false; - } - } - void evictErrorTilesBasedOnStrategy( Bounds tileRange, EvictErrorTileStrategy evictStrategy) { if (evictStrategy == EvictErrorTileStrategy.notVisibleRespectMargin) { @@ -200,13 +173,40 @@ class TileManager { } } - List all() => _tiles.values.toList(); + void prune(double? zoom, EvictErrorTileStrategy evictStrategy) { + if (zoom == null) { + removeAll(evictStrategy); + return; + } - List sortedByDistanceToZoomAscending( - double maxZoom, double currentZoom) { - return [..._tiles.values]..sort((a, b) => a - .zIndex(maxZoom, currentZoom) - .compareTo(b.zIndex(maxZoom, currentZoom))); + for (final entry in _tiles.entries) { + final tile = entry.value; + tile.retain = tile.current; + } + + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (tile.current && !tile.active) { + final coords = tile.coords; + if (!_retainParent(coords.x, coords.y, coords.z, coords.z - 5)) { + _retainChildren(coords.x, coords.y, coords.z, coords.z + 2); + } + } + } + + final toRemove = []; + for (final entry in _tiles.entries) { + final tile = entry.value; + + if (!tile.retain) { + toRemove.add(entry.key); + } + } + + for (final key in toRemove) { + remove(key, evictStrategy); + } } void _retainChildren(double x, double y, double z, double maxZoom) { From 876d4f7fca3cb314f86989d78ae1625d8318e7cb Mon Sep 17 00:00:00 2001 From: Rory Stephenson Date: Sat, 28 May 2022 10:03:24 +0200 Subject: [PATCH 11/11] Move all TileLayer code to the tile_layer/ directory. Add Coords to the exports since it's needed for custom TileProvider implementations --- lib/flutter_map.dart | 12 ++++++++---- lib/src/layer/tile_layer/tile.dart | 1 - .../{tile_builder => tile_layer}/tile_builder.dart | 0 lib/src/layer/tile_layer/tile_layer.dart | 1 - lib/src/layer/tile_layer/tile_manager.dart | 1 - .../tile_provider/file_tile_provider_io.dart | 2 +- .../tile_provider/file_tile_provider_web.dart | 2 +- .../tile_provider/network_image_with_retry.dart | 0 .../tile_provider/tile_provider.dart | 6 +++--- 9 files changed, 13 insertions(+), 12 deletions(-) rename lib/src/layer/{tile_builder => tile_layer}/tile_builder.dart (100%) rename lib/src/layer/{ => tile_layer}/tile_provider/file_tile_provider_io.dart (84%) rename lib/src/layer/{ => tile_layer}/tile_provider/file_tile_provider_web.dart (83%) rename lib/src/layer/{ => tile_layer}/tile_provider/network_image_with_retry.dart (100%) rename lib/src/layer/{ => tile_layer}/tile_provider/tile_provider.dart (94%) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index bd12f37ba..fea6ae126 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -32,12 +32,13 @@ export 'package:flutter_map/src/layer/marker_layer.dart'; export 'package:flutter_map/src/layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer.dart'; export 'package:flutter_map/src/layer/polyline_layer.dart'; +export 'package:flutter_map/src/layer/tile_layer/coords.dart'; export 'package:flutter_map/src/layer/tile_layer/tile.dart'; -export 'package:flutter_map/src/layer/tile_builder/tile_builder.dart'; +export 'package:flutter_map/src/layer/tile_layer/tile_builder.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_layer.dart'; -export 'package:flutter_map/src/layer/tile_provider/file_tile_provider_io.dart' - if (dart.library.html) 'package:flutter_map/src/layer/tile_provider/file_tile_provider_web.dart'; -export 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; +export 'package:flutter_map/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart' + if (dart.library.html) 'package:flutter_map/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart'; +export 'package:flutter_map/src/layer/tile_layer/tile_provider/tile_provider.dart'; export 'package:flutter_map/src/plugins/plugin.dart'; /// Renders a map composed of a list of layers powered by [LayerOptions]. @@ -146,8 +147,11 @@ abstract class MapController { double get rotation; Stream get mapEventStream; + StreamSink get mapEventSink; + set state(MapState state); + void dispose(); LatLng? pointToLatLng(CustomPoint point); diff --git a/lib/src/layer/tile_layer/tile.dart b/lib/src/layer/tile_layer/tile.dart index 316f6c28a..157097d90 100644 --- a/lib/src/layer/tile_layer/tile.dart +++ b/lib/src/layer/tile_layer/tile.dart @@ -1,6 +1,5 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/tile_layer/coords.dart'; typedef TileReady = void Function( Coords coords, dynamic error, Tile tile); diff --git a/lib/src/layer/tile_builder/tile_builder.dart b/lib/src/layer/tile_layer/tile_builder.dart similarity index 100% rename from lib/src/layer/tile_builder/tile_builder.dart rename to lib/src/layer/tile_layer/tile_builder.dart diff --git a/lib/src/layer/tile_layer/tile_layer.dart b/lib/src/layer/tile_layer/tile_layer.dart index 2f454a947..bc1e737a7 100644 --- a/lib/src/layer/tile_layer/tile_layer.dart +++ b/lib/src/layer/tile_layer/tile_layer.dart @@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/core/bounds.dart'; import 'package:flutter_map/src/core/util.dart' as util; -import 'package:flutter_map/src/layer/tile_layer/coords.dart'; import 'package:flutter_map/src/layer/tile_layer/level.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_manager.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_transformation.dart'; diff --git a/lib/src/layer/tile_layer/tile_manager.dart b/lib/src/layer/tile_layer/tile_manager.dart index 8fcee3b69..408435a3d 100644 --- a/lib/src/layer/tile_layer/tile_manager.dart +++ b/lib/src/layer/tile_layer/tile_manager.dart @@ -1,6 +1,5 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/core/bounds.dart'; -import 'package:flutter_map/src/layer/tile_layer/coords.dart'; import 'package:tuple/tuple.dart'; class TileManager { diff --git a/lib/src/layer/tile_provider/file_tile_provider_io.dart b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart similarity index 84% rename from lib/src/layer/tile_provider/file_tile_provider_io.dart rename to lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart index ab982f563..564345245 100644 --- a/lib/src/layer/tile_provider/file_tile_provider_io.dart +++ b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_io.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/tile_layer/coords.dart'; /// FileTileProvider class FileTileProvider extends TileProvider { const FileTileProvider(); + @override ImageProvider getImage(Coords coords, TileLayerOptions options) { return FileImage(File(getTileUrl(coords, options))); diff --git a/lib/src/layer/tile_provider/file_tile_provider_web.dart b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart similarity index 83% rename from lib/src/layer/tile_provider/file_tile_provider_web.dart rename to lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart index 2ddbb8992..31da2b069 100644 --- a/lib/src/layer/tile_provider/file_tile_provider_web.dart +++ b/lib/src/layer/tile_layer/tile_provider/file_tile_provider_web.dart @@ -1,11 +1,11 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/tile_layer/coords.dart'; /// FileTileProvider class FileTileProvider extends TileProvider { const FileTileProvider(); + @override ImageProvider getImage(Coords coords, TileLayerOptions options) { return NetworkImage(getTileUrl(coords, options)); diff --git a/lib/src/layer/tile_provider/network_image_with_retry.dart b/lib/src/layer/tile_layer/tile_provider/network_image_with_retry.dart similarity index 100% rename from lib/src/layer/tile_provider/network_image_with_retry.dart rename to lib/src/layer/tile_layer/tile_provider/network_image_with_retry.dart diff --git a/lib/src/layer/tile_provider/tile_provider.dart b/lib/src/layer/tile_layer/tile_provider/tile_provider.dart similarity index 94% rename from lib/src/layer/tile_provider/tile_provider.dart rename to lib/src/layer/tile_layer/tile_provider/tile_provider.dart index 1b4c124d4..4ffddef66 100644 --- a/lib/src/layer/tile_provider/tile_provider.dart +++ b/lib/src/layer/tile_layer/tile_provider/tile_provider.dart @@ -1,8 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/tile_layer/coords.dart'; - -import 'package:flutter_map/src/layer/tile_provider/network_image_with_retry.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_provider/network_image_with_retry.dart'; abstract class TileProvider { const TileProvider(); @@ -66,6 +64,7 @@ class NetworkTileProvider extends TileProvider { class NonCachingNetworkTileProvider extends TileProvider { const NonCachingNetworkTileProvider(); + @override ImageProvider getImage(Coords coords, TileLayerOptions options) { return NetworkImage(getTileUrl(coords, options)); @@ -74,6 +73,7 @@ class NonCachingNetworkTileProvider extends TileProvider { class AssetTileProvider extends TileProvider { const AssetTileProvider(); + @override ImageProvider getImage(Coords coords, TileLayerOptions options) { return AssetImage(getTileUrl(coords, options));