From be00c1645a111aabccd40cf7594c03230efc8694 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 7 Jul 2025 00:46:35 +0100 Subject: [PATCH 01/13] Start of modern tile layer implementation --- example/pubspec.lock | 14 +- lib/src/layer/modern_tile_layer/README.md | 35 +++ .../modern_tile_layer/base_tile_layer.dart | 234 ++++++++++++++++++ .../modern_tile_layer/base_tile_loader.dart | 19 ++ lib/src/layer/modern_tile_layer/options.dart | 30 +++ .../layer/modern_tile_layer/tile_data.dart | 62 +++++ .../tile_layer.dart/raster_tile_layer.dart | 30 +++ .../modern_tile_layer/tile_loader/loader.dart | 141 +++++++++++ .../modern_tile_layer/tile_loader/source.dart | 23 ++ .../source_fetchers/asset/asset.dart | 0 .../source_fetchers/file/file_stub.dart | 17 ++ .../file/tile_provider_io.dart | 23 ++ .../source_fetchers/network/network.dart | 62 +++++ .../tile_loader/source_generator_fetcher.dart | 45 ++++ .../tile_loader/source_generators/slippy.dart | 93 +++++++ .../tile_loader/source_generators/wms.dart | 113 +++++++++ 16 files changed, 934 insertions(+), 7 deletions(-) create mode 100644 lib/src/layer/modern_tile_layer/README.md create mode 100644 lib/src/layer/modern_tile_layer/base_tile_layer.dart create mode 100644 lib/src/layer/modern_tile_layer/base_tile_loader.dart create mode 100644 lib/src/layer/modern_tile_layer/options.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_data.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/loader.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/asset/asset.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart diff --git a/example/pubspec.lock b/example/pubspec.lock index c0d8ff6cc..064312987 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -171,26 +171,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -541,5 +541,5 @@ packages: source: hosted version: "1.1.0" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/src/layer/modern_tile_layer/README.md b/lib/src/layer/modern_tile_layer/README.md new file mode 100644 index 000000000..54dbc7620 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/README.md @@ -0,0 +1,35 @@ +# Modern Tile Layer + +The modern tile layer is a rework of the original tile layer which: + +* should be significantly more flexible (-> provide better integration support for plugins) +* resolve some hard-to-debug bugs +* improve performance in the default case + +It does this by: + +* splitting the logic of the current `TileLayer` & `TileProvider` into 3-5 parts: + * `BaseTileLayer`: responsible for tile management (initial workings provided by @mootw) + + * a tile loader: responsible for getting the data for individual tiles given the coordinates from the manager + In the default implementation, this is further split: + * a source generator: responsible for telling the source fetcher what to fetch for the tile + * a source fetcher: responsible for actually fetching the tile data + + * a tile renderer: responsible for painting tiled data + +* using a canvas implementation for the default raster tile layer + +Significant uestions remaining: + +* Is the default tile loader setup (with two stages) too much frameworking/overly-complicated? +* Should the default tile loader have a third step to statically tie the loader more closely to the renderer? +* Simulating retina mode affects all parts of the system - but only (conceptually/for reasoning) applies to raster tiles (although technically it's no different to a top layer option). How should this be represented? +* How far do we want to provide pre-builts? The raster tile layer boils down to 4 parts - a prebuilt is great for beginners, but providing flexbility at this level basically just makes the raster tile layer pre-build a very loose cover for the underlying base tile layer configuration. +* What should the top-level options be (`TileLayerOptions`)? See also retina mode simulation. +* Who's responsibility is enforcing the max-zoom level? Is max-zoom = native max-zoom or MapOptions.maxZoom? + +This new functionality has no deadline or estimated completion date - although it's something we've been wanting to do for a while, and we have some work in the +background which may be integrating with this. + +Contribution greatly appriciated! diff --git a/lib/src/layer/modern_tile_layer/base_tile_layer.dart b/lib/src/layer/modern_tile_layer/base_tile_layer.dart new file mode 100644 index 000000000..66fc595e6 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/base_tile_layer.dart @@ -0,0 +1,234 @@ +import 'dart:collection'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:meta/meta.dart'; + +/// A map layer formed from adjacent square tiles loaded individually on demand +/// +/// This widget provides the tile management logic, giving responsibility for +/// tile loading and tile rendering to the [tileLoader] & [renderer] delegates +/// respectively. +/// +/// This layer is often used to draw the map itself, for example as raster image +/// tiles. However, it may be used for any reasonable purpose where the contract +/// is met. +class BaseTileLayer extends StatefulWidget { + final TileLayerOptions options; + final TileLoaderBase tileLoader; + final Widget Function( + BuildContext context, + Map<({TileCoordinates coordinates, Object layerKey}), TileData> + visibleTiles, + TileLayerOptions options, + ) renderer; + + const BaseTileLayer({ + super.key, + required this.options, + required this.tileLoader, + required this.renderer, + }); + + Object get layerKey => Object.hash(tileLoader.hashCode, renderer.hashCode); + + @override + State> createState() => _BaseTileLayerState(); +} + +class _BaseTileLayerState extends State> { + final tiles = _TilesTracker(); + + @override + Widget build(BuildContext context) { + final camera = MapCamera.of(context); + final zoom = camera.zoom.round(); + final visibleTileCoordinates = _getVisibleTiles(camera); + + // Load new tiles + for (final coordinates in visibleTileCoordinates) { + final key = (coordinates: coordinates, layerKey: widget.layerKey); + tiles.putIfAbsent( + key, + () => widget.tileLoader.load(coordinates, widget.options) + ..whenLoaded.then((_) => _pruneOnLoadedTile(key)), + ); + } + + // Prune tiles that are at the same zoom level, but not visible to the camera + // These tiles are NEVER visible to the camera regardless of their loading + // status + tiles.removeWhere( + (key, _) => + key.coordinates.z == zoom && + !visibleTileCoordinates.contains(key.coordinates), + ); + + // If all visible tiles are loaded correctly, prune ALL other tiles + // This is mostly a catch-all, as there is likely some weird edge case + // that keeps old tiles loaded when they shouldn't be + final allLoaded = tiles.entries + .where( + (tile) => + visibleTileCoordinates.contains(tile.key.coordinates) && + tile.key.layerKey == widget.layerKey, + ) + .every((tile) => tile.value.isLoaded); + if (allLoaded) { + tiles.removeWhere( + (key, _) => !visibleTileCoordinates.contains(key.coordinates), + ); + } + + return widget.renderer(context, Map.unmodifiable(tiles), widget.options); + } + + /// Eventually pruning could be restricted to tiles if there is an animation + /// phase that needs to be waited for, quite easily! + void _pruneOnLoadedTile(_TileKey key) { + /// PRUNE PHASE + // Remove all identical tiles of other (old) keys. aka replace my ancestor + tiles.removeWhere( + (otherKey, otherData) => + otherData.isLoaded && + otherKey.coordinates == key.coordinates && + otherKey.layerKey != key.layerKey, + ); + + for (final childCoordinates in key.coordinates.children()) { + // Prune all children + // TODO decide if children of different keys should be pruned + // or not + tiles.removeWhere( + (otherKey, otherData) => + otherData.isLoaded && otherKey.coordinates == childCoordinates, + ); + } + + // TODO there is still some minor flickering when zooming quickly + // This appears to be caused by pruning tiles that are loaded but + // then getting replaced with tiles that get loaded but then pruned. + // It seems to only happen when zooming more than 1 level at a time + + if (key.coordinates.z != 0) { + // Ensure that this is not called on the z = 0 tile + final siblingCoordinates = key.coordinates.parent().children(); + siblingCoordinates.remove(key.coordinates); + + // True when all tiles are loaded with the latest key + final allLoaded = tiles.entries + .where( + (other) => + siblingCoordinates.contains(other.key.coordinates) && + other.key.layerKey == key.layerKey, + ) + .every((other) => other.value.isLoaded); + + if (allLoaded) { + // Prune parent if me and my siblings are all loaded + // Key does not matter as the tile is getting replaced by + // tiles with the correct key + tiles.removeWhere( + (otherKey, _) => otherKey.coordinates == key.coordinates.parent(), + ); + } + } + + /// PRUNE COMPLETE + if (mounted) { + setState(() {}); + } + } + + Offset _floor(Offset point) => + Offset(point.dx.floorToDouble(), point.dy.floorToDouble()); + + Offset _ceil(Offset point) => + Offset(point.dx.ceilToDouble(), point.dy.ceilToDouble()); + + Rect _calculatePixelBounds( + MapCamera camera, + LatLng center, + double viewingZoom, + int tileZoom, + ) { + final tileZoomDouble = tileZoom.toDouble(); + final scale = camera.getZoomScale(viewingZoom, tileZoomDouble); + final pixelCenter = camera.projectAtZoom(center, tileZoomDouble); + final halfSize = camera.size / (scale * 2); + + return Rect.fromPoints( + pixelCenter - halfSize.bottomRight(Offset.zero), + pixelCenter + halfSize.bottomRight(Offset.zero), + ); + } + + List _getVisibleTiles(MapCamera camera) { + final pixelBounds = _calculatePixelBounds( + camera, + camera.center, + camera.zoom, + camera.zoom.round(), // TODO: `maxZoom`? + ); + + final tileBounds = Rect.fromPoints( + _floor(pixelBounds.topLeft / widget.options.tileDimension.toDouble()), + _ceil(pixelBounds.bottomRight / widget.options.tileDimension.toDouble()) - + const Offset(1, 1), + ); + + return [ + for (int x = tileBounds.left.round(); x <= tileBounds.right; x++) + for (int y = tileBounds.top.round(); y <= tileBounds.bottom; y++) + TileCoordinates(x, y, camera.zoom.round()), + ]; + } +} + +extension _ParentChildTraversal on TileCoordinates { + /// This tile coordinate zoomed out by one + TileCoordinates parent() => z == 0 + ? throw RangeError.range( + 0, + 0, + null, + null, + 'Tiles at zoom level 0 are orphans', + ) + : TileCoordinates(x ~/ 2, y ~/ 2, z - 1); + + /// This tile coordinate but zoomed in by 1 + Set children() { + final topLeftChild = TileCoordinates(x * 2, y * 2, z + 1); + + return { + topLeftChild, + TileCoordinates(topLeftChild.x + 1, topLeftChild.y, topLeftChild.z), + TileCoordinates(topLeftChild.x, topLeftChild.y + 1, topLeftChild.z), + TileCoordinates(topLeftChild.x + 1, topLeftChild.y + 1, topLeftChild.z), + }; + } +} + +typedef _TileKey = ({TileCoordinates coordinates, Object layerKey}); + +extension type _TilesTracker._( + SplayTreeMap<_TileKey, TileData> map) + implements SplayTreeMap<_TileKey, TileData> { + _TilesTracker() + : this._( + SplayTreeMap<_TileKey, TileData>( + (a, b) => + a.coordinates.z.compareTo(b.coordinates.z) | + a.coordinates.x.compareTo(b.coordinates.x) | + a.coordinates.y.compareTo(b.coordinates.y), + ), + ); + + @redeclare + TileData? remove(Object? key) => map.remove(key)?..abort(); +} diff --git a/lib/src/layer/modern_tile_layer/base_tile_loader.dart b/lib/src/layer/modern_tile_layer/base_tile_loader.dart new file mode 100644 index 000000000..a7b467957 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/base_tile_loader.dart @@ -0,0 +1,19 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +@immutable +abstract interface class TileLoaderBase { + const TileLoaderBase(); + + TileData load(TileCoordinates coordinates, TileLayerOptions options); + + @override + @mustBeOverridden + int get hashCode; + + @override + @mustBeOverridden + bool operator ==(Object other); +} diff --git a/lib/src/layer/modern_tile_layer/options.dart b/lib/src/layer/modern_tile_layer/options.dart new file mode 100644 index 000000000..909087532 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/options.dart @@ -0,0 +1,30 @@ +import 'package:meta/meta.dart'; + +@immutable +class TileLayerOptions { + final double maxZoom; + final double zoomOffset; + final bool zoomReverse; + + final int tileDimension; + + const TileLayerOptions({ + this.maxZoom = double.infinity, + this.zoomOffset = 0, + this.zoomReverse = false, + this.tileDimension = 256, + }); + + @override + int get hashCode => + Object.hash(maxZoom, zoomOffset, zoomReverse, tileDimension); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TileLayerOptions && + other.maxZoom == maxZoom && + other.zoomOffset == zoomOffset && + other.zoomReverse == zoomReverse && + other.tileDimension == tileDimension); +} diff --git a/lib/src/layer/modern_tile_layer/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_data.dart new file mode 100644 index 000000000..5bd1ea930 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_data.dart @@ -0,0 +1,62 @@ +import 'dart:async'; + +import 'package:meta/meta.dart'; + +/// Container for custom-shape data associated with a particular tile coordinate +/// +/// The data carried is usually made available asynchronously, for example as +/// the result of an I/O operation or HTTP request. Alternatively, data may be +/// available synchronously if the data is loaded from prepared memory. This +/// container supports either form of data. +/// +/// The container tracks the status/availability of the data +/// (for asynchronously available data), and optionally provides a handle to +/// enable the request which spawns the data to be aborted if it is no longer +/// required. +/// +/// Association with a tile coordinate is made in the tile layer. +class TileData { + D? _data; + + /// Data + /// + /// This may be `null` if [D] is nullable & the data is `null`. In this case, + /// use [isLoaded] to determine whether this accurately reflects the `null` + /// data. Otherwise, `null` means the data is not yet available. + D? get data => _data; + + final _loadedTracker = Completer.sync(); + + /// Completes with loaded data when the data is loaded successfully + /// + /// This never completes if the data completes to an error. + Future get whenLoaded => _loadedTracker.future; + + /// Whether [data] represents the loaded data + bool get isLoaded => _loadedTracker.isCompleted; + + /// Abort the ongoing request when the [data] is no longer required + /// + /// If called after the data is already available, this should have no effect. + /// + /// If called when the data is not yet available, [data] should never become + /// available, [whenLoaded] should never complete, and [isLoaded] should + /// remain `false`. + /// + /// This may have no effect. + final void Function() abort; + + /// Create a container with the specified data (or the data result of the + /// specified future) + @internal + TileData({ + required FutureOr data, + void Function()? abort, + }) : abort = (abort ?? () {}) { + if (data is Future) { + data.then((data) => _loadedTracker.complete(_data = data)); + } else { + _loadedTracker.complete(_data = data); + } + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart new file mode 100644 index 000000000..fbd521f0b --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart @@ -0,0 +1,30 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart'; + +class RasterTileLayer extends StatefulWidget { + const RasterTileLayer({super.key}); + + @override + State createState() => _RasterTileLayerState(); +} + +class _RasterTileLayerState extends State { + @override + Widget build(BuildContext context) => BaseTileLayer( + options: const TileLayerOptions(), + tileLoader: TileLoader( + sourceGenerator: const SlippyMapGenerator( + urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', + ), + sourceFetcher: + NetworkBytesFetcher.withUAIdentifier('com.example.app'), + ), + renderer: (context, visibleTiles, options) { + throw UnimplementedError(); + }, + ); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/loader.dart new file mode 100644 index 000000000..6186bf4d1 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/loader.dart @@ -0,0 +1,141 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loaders/source.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +/// Default implementation of a tile loader, which delegates loading of data to +/// two seperate stages: +/// +/// 1. The [sourceGenerator] uses a tile's [TileCoordinates] & the ambient +/// [TileLayerOptions] to generate an object, describing the tile's 'source' +/// +/// 2. The [sourceFetcher] uses this 'source' to generate an output data (which +/// is held within a [TileData] for the renderer's benefit) +/// +/// The data ([D]) may be of any shape - but is commonly raw bytes for the +/// renderer to process. The 'source' ([S]) may be of any shape, such as +/// [TileSource]. +@immutable +final class TileLoader + implements TileLoaderBase { + /// Tile source generator + /// + /// See documentation on [TileLoader] & [TileSourceGenerator] for information. + final TileSourceGenerator sourceGenerator; + + /// Tile source fetcher + /// + /// See documentation on [TileLoader] & [TileSourceFetcher] for information. + final TileSourceFetcher sourceFetcher; + + // TODO: Consider whether a 3rd step is useful (for converting bytes -> + // resource), which would add better typing guarantees (tie to specific + // renderers) - but creates more types & may be able to be best handled by the + // renderer + + /// Create a tile loader from a source generator & fetcher + const TileLoader({ + required this.sourceGenerator, + required this.sourceFetcher, + }); + + @override + TileData load(TileCoordinates coordinates, TileLayerOptions options) { + final abortTrigger = Completer(); + return TileData( + abort: abortTrigger.complete, + data: sourceFetcher( + sourceGenerator(coordinates, options), + abortTrigger.future, + ), + ); + } + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TileLoader && + other.sourceFetcher == sourceFetcher && + other.sourceGenerator == sourceGenerator); + + @override + int get hashCode => Object.hash(sourceGenerator, sourceFetcher); + + /// [Uint8List] that forms a fully transparent image + @deprecated + static final transparentImage = Uint8List.fromList([ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, + 0x42, + 0x60, + 0x82, + ]); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source.dart b/lib/src/layer/modern_tile_layer/tile_loader/source.dart new file mode 100644 index 000000000..e322bff5d --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source.dart @@ -0,0 +1,23 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; +import 'package:meta/meta.dart'; + +/// Default tile 'source' implementation for the default [TileLoader] +/// implementation +@internal +@immutable +class TileSource { + final String uri; + final String? fallbackUri; + + const TileSource({required this.uri, this.fallbackUri}); + + @override + int get hashCode => Object.hash(uri, fallbackUri); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TileSource && + other.uri == uri && + other.fallbackUri == fallbackUri); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/asset/asset.dart new file mode 100644 index 000000000..e69de29bb diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart new file mode 100644 index 000000000..88f9a4a88 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; + +class FileBytesFetcher implements TileSourceFetcher { + const FileBytesFetcher(); + + @override + Future call( + TileSource source, + Future abortSignal, + ) { + throw UnsupportedError( + '`FileBytesFetcher` is unsupported on non-native platforms', + ); + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart new file mode 100644 index 000000000..51d0ee068 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart @@ -0,0 +1,23 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; + +class FileBytesFetcher implements TileSourceFetcher { + const FileBytesFetcher(); + + @override + Future call( + TileSource source, + Future abortSignal, + ) async { + try { + return await File(source.uri).readAsBytes(); + } on FileSystemException { + if (source.fallbackUri == null) rethrow; + return await File(source.fallbackUri!).readAsBytes(); + } + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart new file mode 100644 index 000000000..cbafbfaec --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart @@ -0,0 +1,62 @@ +import 'dart:io' show HttpHeaders; // web safe! + +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:http/http.dart'; +import 'package:http/retry.dart'; + +class NetworkBytesFetcher implements TileSourceFetcher { + final Map headers; + final Client httpClient; + + NetworkBytesFetcher({ + Map? headers, + Client? httpClient, + }) : headers = headers ?? {}, + httpClient = httpClient ?? RetryClient(Client()); + + NetworkBytesFetcher.withUAIdentifier( + String identifier, { + Map? headers, + Client? httpClient, + }) : headers = headers ?? {}, + httpClient = httpClient ?? RetryClient(Client()) { + if (!kIsWeb) { + this.headers.putIfAbsent( + HttpHeaders.userAgentHeader, + () => 'flutter_map ($identifier)', + ); + } + } + + @override + Future call( + TileSource source, + Future abortSignal, { + bool useFallback = false, + }) { + // TODO: Replace with #2082 + return httpClient + .readBytes( + Uri.parse(useFallback ? source.fallbackUri! : source.uri), + headers: headers, + ) + .onError((err, _) { + if (useFallback || source.fallbackUri == null) { + throw err; + } + return this(source, abortSignal, useFallback: true); + }); + } + + @override + int get hashCode => Object.hash(headers, httpClient); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is NetworkBytesFetcher && + other.headers == headers && + other.httpClient == httpClient); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart new file mode 100644 index 000000000..ce4197e76 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +/// Generates a tile's 'source' based on its own properties, the ambient +/// [TileLayerOptions], and the tile's [TileCoordinates] +/// +/// The 'source' type must be consumable by the [TileSourceFetcher] used. +/// +/// If this generator accepts other properties/options, it must remain immutable +/// and must set a valid equality operator. +@immutable +abstract interface class TileSourceGenerator { + /// Generates a tile's 'source' + /// + /// See documentation on [TileSourceGenerator] for more information. + S call(TileCoordinates coordinates, TileLayerOptions options); +} + +/// Fetch a tile's data based on its 'source' ([S]) +/// +/// A tile's data ([T]) is often bytes. These may be interpreted by a tile +/// layer's renderer. However, this is not required - it may be preferable to +/// perform some (potentially asynchronous) processing of data before sending it +/// to the renderer. +/// +/// The 'source' type is set by the [TileSourceGenerator]. The fetcher does not +/// have access to the ambient [TileLayerOptions], therefore any required +/// options must appear in the 'source'. +/// +/// Supports an abort signal to abort an ongoing operation, such as a network +/// request. It is not required to respect the signal. +/// +/// If this fetcher accepts other properties/options, it must remain immutable +/// and must set a valid equality operator. +@immutable +abstract interface class TileSourceFetcher { + /// Fetch a tile's data based on its 'source' + /// + /// See documentation on [TileSourceFetcher] for more information. + FutureOr call(S source, Future abortSignal); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart new file mode 100644 index 000000000..670effcbf --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart @@ -0,0 +1,93 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +@immutable +class SlippyMapGenerator implements TileSourceGenerator { + final String urlTemplate; + final String? fallbackUrlTemplate; + final List subdomains; + final Map additionalPlaceholders; + + final bool tms; + + const SlippyMapGenerator({ + required this.urlTemplate, + this.fallbackUrlTemplate, + this.subdomains = const [], + this.additionalPlaceholders = const {}, + this.tms = false, + }); + + @override + TileSource call(TileCoordinates coordinates, TileLayerOptions options) { + final replacementMap = generateReplacementMap(coordinates, options); + + String replacer(Match match) { + final value = replacementMap[match.group(1)!]; + if (value != null) return value; + throw ArgumentError('Missing value for placeholder: {${match.group(1)}}'); + } + + final url = urlTemplate.replaceAllMapped( + templatePlaceholderElement, + replacer, + ); + final fallbackUrl = fallbackUrlTemplate?.replaceAllMapped( + templatePlaceholderElement, + replacer, + ); + + return TileSource(uri: url, fallbackUri: fallbackUrl); + } + + @visibleForOverriding + Map generateReplacementMap( + TileCoordinates coordinates, + TileLayerOptions options, + ) { + final zoom = (options.zoomOffset + + (options.zoomReverse + ? options.maxZoom - coordinates.z.toDouble() + : coordinates.z.toDouble())) + .round(); + + return { + 'x': coordinates.x.toString(), + 'y': (tms ? ((1 << zoom) - 1) - coordinates.y : coordinates.y).toString(), + 'z': zoom.toString(), + 's': subdomains.isEmpty + ? '' + : subdomains[(coordinates.x + coordinates.y) % subdomains.length], + // TODO: Retina mode + // We can easily implement server retina mode: simulated retina mode + // requires cooperation with renderer! + //'r': options.resolvedRetinaMode == RetinaMode.server ? '@2x' : '', + 'd': options.tileDimension.toString(), + ...additionalPlaceholders, + }; + } + + static final templatePlaceholderElement = RegExp('{([^{}]*)}'); + + @override + int get hashCode => Object.hash( + urlTemplate, + fallbackUrlTemplate, + subdomains, + additionalPlaceholders, + tms, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is SlippyMapGenerator && + other.urlTemplate == urlTemplate && + other.fallbackUrlTemplate == fallbackUrlTemplate && + other.subdomains == subdomains && + other.additionalPlaceholders == additionalPlaceholders && + other.tms == tms); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart new file mode 100644 index 000000000..75e6787c5 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart @@ -0,0 +1,113 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_map/src/geo/crs.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:flutter_map/src/misc/extensions.dart'; +import 'package:meta/meta.dart'; + +@immutable +class WMSGenerator implements TileSourceGenerator { + /// WMS service's URL, for example '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; + + /// Whether to make tiles transparent + final bool transparent; + + /// Encode boolean values as uppercase in request + final bool uppercaseBoolValue; + + /// Sets map projection standard + final Crs crs; + + /// The scalar to multiply the calculated width & height for each request by + /// + /// This may be used to simulate retina mode, for example, by setting to 2. + /// + /// Defaults to 1. + // TODO: This is simulating retina mode - see README for questions + final int dimensionsMultiplier; + + /// Other request parameters + final Map otherParameters; + + late final String _encodedBaseUrl; + + late final double _versionNumber; + + /// Create a new [WMSGenerator] instance + WMSGenerator({ + 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.dimensionsMultiplier = 1, + 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=WMS') + ..write('&request=GetMap') + ..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(); + } + + @override + TileSource call(TileCoordinates coordinates, TileLayerOptions options) { + final nwPoint = Offset( + (coordinates.x * options.tileDimension).toDouble(), + (coordinates.y * options.tileDimension).toDouble(), + ); + final sePoint = + nwPoint + (const Offset(1, 1) * options.tileDimension.toDouble()); + + final nwCoords = crs.offsetToLatLng(nwPoint, coordinates.z.toDouble()); + final seCoords = crs.offsetToLatLng(sePoint, coordinates.z.toDouble()); + + final nw = crs.projection.project(nwCoords); + final se = crs.projection.project(seCoords); + + final bounds = Rect.fromPoints(nw, se); + final bbox = (_versionNumber >= 1.3 && crs is Epsg4326) + ? [bounds.min.dy, bounds.min.dx, bounds.max.dy, bounds.max.dx] + : [bounds.min.dx, bounds.min.dy, bounds.max.dx, bounds.max.dy]; + + return TileSource( + uri: (StringBuffer(_encodedBaseUrl) + ..write('&width=${options.tileDimension * dimensionsMultiplier}') + ..write('&height=${options.tileDimension * dimensionsMultiplier}') + ..write('&bbox=${bbox.join(',')}')) + .toString(), + ); + } +} From c3745ad66ae9eeb561c48c34f343bbd3f34afe23 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 7 Jul 2025 22:34:46 +0100 Subject: [PATCH 02/13] Continued implementation, mostly of the tile loader part --- lib/src/layer/modern_tile_layer/README.md | 6 +- .../modern_tile_layer/base_tile_layer.dart | 47 ++++-- .../modern_tile_layer/base_tile_loader.dart | 6 +- lib/src/layer/modern_tile_layer/options.dart | 13 +- .../layer/modern_tile_layer/tile_data.dart | 66 +++++--- .../tile_layer.dart/raster_tile_layer.dart | 110 +++++++++++-- .../modern_tile_layer/tile_loader/loader.dart | 33 +--- .../source_fetchers/asset/asset.dart | 0 .../bytes_fetchers/asset/asset.dart | 47 ++++++ .../bytes_fetchers/bytes_fetcher.dart | 72 +++++++++ .../{ => bytes_fetchers}/file/file_stub.dart | 7 +- .../file/tile_provider_io.dart | 7 +- .../bytes_fetchers/network/network.dart | 97 ++++++++++++ .../source_fetchers/bytes_passthrough.dart | 33 ++++ .../source_fetchers/network/network.dart | 62 -------- .../raster/image_provider.dart | 95 +++++++++++ .../raster/raster_tile_fetcher.dart | 49 ++++++ .../source_fetchers/raster/tile_data.dart | 33 ++++ .../tile_loader/source_generator_fetcher.dart | 27 ++-- .../tile_loader/source_generators/slippy.dart | 93 ----------- .../tile_loader/source_generators/wms.dart | 4 +- .../tile_loader/source_generators/xyz.dart | 149 ++++++++++++++++++ .../{source.dart => tile_source.dart} | 4 +- 23 files changed, 794 insertions(+), 266 deletions(-) delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/asset/asset.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart rename lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/{ => bytes_fetchers}/file/file_stub.dart (72%) rename lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/{ => bytes_fetchers}/file/tile_provider_io.dart (78%) create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart rename lib/src/layer/modern_tile_layer/tile_loader/{source.dart => tile_source.dart} (82%) diff --git a/lib/src/layer/modern_tile_layer/README.md b/lib/src/layer/modern_tile_layer/README.md index 54dbc7620..e3c620977 100644 --- a/lib/src/layer/modern_tile_layer/README.md +++ b/lib/src/layer/modern_tile_layer/README.md @@ -14,7 +14,9 @@ It does this by: * a tile loader: responsible for getting the data for individual tiles given the coordinates from the manager In the default implementation, this is further split: * a source generator: responsible for telling the source fetcher what to fetch for the tile - * a source fetcher: responsible for actually fetching the tile data + * a source fetcher: responsible for actually fetching the tile data + In the default implementation, this is further split: + * a bytes fetcher: responsible for actually fetching the tile data * a tile renderer: responsible for painting tiled data @@ -23,9 +25,7 @@ It does this by: Significant uestions remaining: * Is the default tile loader setup (with two stages) too much frameworking/overly-complicated? -* Should the default tile loader have a third step to statically tie the loader more closely to the renderer? * Simulating retina mode affects all parts of the system - but only (conceptually/for reasoning) applies to raster tiles (although technically it's no different to a top layer option). How should this be represented? -* How far do we want to provide pre-builts? The raster tile layer boils down to 4 parts - a prebuilt is great for beginners, but providing flexbility at this level basically just makes the raster tile layer pre-build a very loose cover for the underlying base tile layer configuration. * What should the top-level options be (`TileLayerOptions`)? See also retina mode simulation. * Who's responsibility is enforcing the max-zoom level? Is max-zoom = native max-zoom or MapOptions.maxZoom? diff --git a/lib/src/layer/modern_tile_layer/base_tile_layer.dart b/lib/src/layer/modern_tile_layer/base_tile_layer.dart index 66fc595e6..c48694842 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_layer.dart @@ -17,32 +17,43 @@ import 'package:meta/meta.dart'; /// This layer is often used to draw the map itself, for example as raster image /// tiles. However, it may be used for any reasonable purpose where the contract /// is met. -class BaseTileLayer extends StatefulWidget { +class BaseTileLayer extends StatefulWidget { final TileLayerOptions options; - final TileLoaderBase tileLoader; + final BaseTileLoader tileLoader; final Widget Function( BuildContext context, - Map<({TileCoordinates coordinates, Object layerKey}), TileData> - visibleTiles, + Object layerKey, TileLayerOptions options, + Map<({TileCoordinates coordinates, Object layerKey}), D> visibleTiles, ) renderer; const BaseTileLayer({ super.key, - required this.options, + this.options = const TileLayerOptions(), required this.tileLoader, required this.renderer, }); - Object get layerKey => Object.hash(tileLoader.hashCode, renderer.hashCode); - @override State> createState() => _BaseTileLayerState(); } -class _BaseTileLayerState extends State> { +class _BaseTileLayerState extends State> { + late Object layerKey = UniqueKey(); + final tiles = _TilesTracker(); + @override + void didUpdateWidget(covariant BaseTileLayer oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.options != widget.options || + oldWidget.tileLoader != widget.tileLoader || + oldWidget.renderer != widget.renderer) { + layerKey = UniqueKey(); + } + } + @override Widget build(BuildContext context) { final camera = MapCamera.of(context); @@ -51,7 +62,7 @@ class _BaseTileLayerState extends State> { // Load new tiles for (final coordinates in visibleTileCoordinates) { - final key = (coordinates: coordinates, layerKey: widget.layerKey); + final key = (coordinates: coordinates, layerKey: layerKey); tiles.putIfAbsent( key, () => widget.tileLoader.load(coordinates, widget.options) @@ -75,7 +86,7 @@ class _BaseTileLayerState extends State> { .where( (tile) => visibleTileCoordinates.contains(tile.key.coordinates) && - tile.key.layerKey == widget.layerKey, + tile.key.layerKey == layerKey, ) .every((tile) => tile.value.isLoaded); if (allLoaded) { @@ -84,7 +95,12 @@ class _BaseTileLayerState extends State> { ); } - return widget.renderer(context, Map.unmodifiable(tiles), widget.options); + return widget.renderer( + context, + layerKey, + widget.options, + Map.unmodifiable(tiles), + ); } /// Eventually pruning could be restricted to tiles if there is an animation @@ -216,12 +232,11 @@ extension _ParentChildTraversal on TileCoordinates { typedef _TileKey = ({TileCoordinates coordinates, Object layerKey}); -extension type _TilesTracker._( - SplayTreeMap<_TileKey, TileData> map) - implements SplayTreeMap<_TileKey, TileData> { +extension type _TilesTracker._( + SplayTreeMap<_TileKey, D> map) implements SplayTreeMap<_TileKey, D> { _TilesTracker() : this._( - SplayTreeMap<_TileKey, TileData>( + SplayTreeMap<_TileKey, D>( (a, b) => a.coordinates.z.compareTo(b.coordinates.z) | a.coordinates.x.compareTo(b.coordinates.x) | @@ -230,5 +245,5 @@ extension type _TilesTracker._( ); @redeclare - TileData? remove(Object? key) => map.remove(key)?..abort(); + D? remove(Object? key) => map.remove(key)?..dispose(); } diff --git a/lib/src/layer/modern_tile_layer/base_tile_loader.dart b/lib/src/layer/modern_tile_layer/base_tile_loader.dart index a7b467957..d0dce69a2 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_loader.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_loader.dart @@ -4,10 +4,10 @@ import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; @immutable -abstract interface class TileLoaderBase { - const TileLoaderBase(); +abstract interface class BaseTileLoader { + const BaseTileLoader(); - TileData load(TileCoordinates coordinates, TileLayerOptions options); + D load(TileCoordinates coordinates, TileLayerOptions options); @override @mustBeOverridden diff --git a/lib/src/layer/modern_tile_layer/options.dart b/lib/src/layer/modern_tile_layer/options.dart index 909087532..aa9934853 100644 --- a/lib/src/layer/modern_tile_layer/options.dart +++ b/lib/src/layer/modern_tile_layer/options.dart @@ -1,13 +1,24 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:meta/meta.dart'; +/// Configuration of a [BaseTileLayer], which can be used by all parts of the +/// tile layer @immutable class TileLayerOptions { - final double maxZoom; + final double maxZoom; // TODO: Is this the same as the old `nativeMaxZoom`? final double zoomOffset; final bool zoomReverse; + /// Size in pixels of each tile image + /// + /// Should be a positive power of 2. Defaults to 256px. + /// + /// If increasing past 256(px) (default), adjust [zoomOffset] as necessary, + /// for example 512px: -1. final int tileDimension; + /// Configuration of a [BaseTileLayer], which can be used by all parts of the + /// tile layer const TileLayerOptions({ this.maxZoom = double.infinity, this.zoomOffset = 0, diff --git a/lib/src/layer/modern_tile_layer/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_data.dart index 5bd1ea930..9ab08654a 100644 --- a/lib/src/layer/modern_tile_layer/tile_data.dart +++ b/lib/src/layer/modern_tile_layer/tile_data.dart @@ -1,24 +1,47 @@ import 'dart:async'; +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:meta/meta.dart'; -/// Container for custom-shape data associated with a particular tile coordinate +/// Data associated with a particular tile coordinate which 'loads' +/// asynchronously +/// +/// These are generated by the [BaseTileLayer.tileLoader] and consumed by the +/// [BaseTileLayer.renderer]. +/// +/// Association with a tile coordinate is made in the tile layer. +/// +/// It is up to the implementation as to what 'loads' means. However, the +/// [BaseTileLayer] will use [whenLoaded], [isLoaded], and [dispose] to manage +/// (such as pruning) the tile for the renderer. +abstract interface class TileData { + /// Completes when the underlying resource is 'loaded' + Future get whenLoaded; + + /// Whether the underlying resource is 'loaded' + bool get isLoaded; + + /// Called when a tile is removed from the map of visible tiles + /// + /// This should usually be used to abort loading of the underlying resource + /// if it has not yet loaded, or release the resources held by it if already + /// loaded. + /// + /// This should not usually be called externally. + @internal + void dispose(); +} + +/// Wrapper for custom-shape data as a [TileData] /// /// The data carried is usually made available asynchronously, for example as /// the result of an I/O operation or HTTP request. Alternatively, data may be /// available synchronously if the data is loaded from prepared memory. This /// container supports either form of data. -/// -/// The container tracks the status/availability of the data -/// (for asynchronously available data), and optionally provides a handle to -/// enable the request which spawns the data to be aborted if it is no longer -/// required. -/// -/// Association with a tile coordinate is made in the tile layer. -class TileData { +class WrapperTileData implements TileData { D? _data; - /// Data + /// Data resource /// /// This may be `null` if [D] is nullable & the data is `null`. In this case, /// use [isLoaded] to determine whether this accurately reflects the `null` @@ -30,29 +53,24 @@ class TileData { /// Completes with loaded data when the data is loaded successfully /// /// This never completes if the data completes to an error. + @override Future get whenLoaded => _loadedTracker.future; /// Whether [data] represents the loaded data + @override bool get isLoaded => _loadedTracker.isCompleted; - /// Abort the ongoing request when the [data] is no longer required - /// - /// If called after the data is already available, this should have no effect. - /// - /// If called when the data is not yet available, [data] should never become - /// available, [whenLoaded] should never complete, and [isLoaded] should - /// remain `false`. - /// - /// This may have no effect. - final void Function() abort; + @internal + @override + void dispose() => _dispose?.call(); + final void Function()? _dispose; /// Create a container with the specified data (or the data result of the /// specified future) - @internal - TileData({ + WrapperTileData({ required FutureOr data, - void Function()? abort, - }) : abort = (abort ?? () {}) { + void Function()? dispose, + }) : _dispose = dispose { if (data is Future) { data.then((data) => _loadedTracker.complete(_data = data)); } else { diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart index fbd521f0b..32be2d5ea 100644 --- a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart @@ -2,11 +2,34 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; class RasterTileLayer extends StatefulWidget { - const RasterTileLayer({super.key}); + const RasterTileLayer({ + super.key, + this.options = const TileLayerOptions(), + required this.sourceGenerator, + required this.bytesFetcher, + }); + + RasterTileLayer.simple({ + super.key, + this.options = const TileLayerOptions(), + required String urlTemplate, + required String uaIdentifier, + }) : sourceGenerator = XYZGenerator(uriTemplate: urlTemplate), + bytesFetcher = NetworkBytesFetcher(uaIdentifier: uaIdentifier); + + final TileLayerOptions options; + final TileSourceGenerator sourceGenerator; + final TileBytesFetcher bytesFetcher; @override State createState() => _RasterTileLayerState(); @@ -15,16 +38,79 @@ class RasterTileLayer extends StatefulWidget { class _RasterTileLayerState extends State { @override Widget build(BuildContext context) => BaseTileLayer( - options: const TileLayerOptions(), + options: widget.options, tileLoader: TileLoader( - sourceGenerator: const SlippyMapGenerator( - urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', - ), - sourceFetcher: - NetworkBytesFetcher.withUAIdentifier('com.example.app'), + sourceGenerator: widget.sourceGenerator, + sourceFetcher: RasterTileFetcher(bytesFetcher: widget.bytesFetcher), + ), + renderer: (context, layerKey, options, visibleTiles) => _RasterRenderer( + layerKey: layerKey, + options: options, + visibleTiles: visibleTiles, ), - renderer: (context, visibleTiles, options) { - throw UnimplementedError(); - }, ); } + +class _RasterRenderer extends StatefulWidget { + _RasterRenderer({ + required Object layerKey, + required this.options, + required this.visibleTiles, + }) : super(key: ValueKey(layerKey)); + + final TileLayerOptions options; + final Map<({TileCoordinates coordinates, Object layerKey}), RasterTileData> + visibleTiles; + + @override + State<_RasterRenderer> createState() => __RasterRendererState(); +} + +class __RasterRendererState extends State<_RasterRenderer> { + //final Map<({TileCoordinates coordinates, Object layerKey}), + // TileData> visibleTiles = {}; + + @override + void didUpdateWidget(covariant _RasterRenderer oldWidget) { + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size.infinite, + willChange: true, + painter: _RasterPainter( + options: widget.options, + visibleTiles: widget.visibleTiles, + //tiles: tiles..sort(renderOrder), + //tilePaint: widget.tilePaint, + //tileOverlayPainter: widget.tileOverlayPainter, + ), + ); + } +} + +class _RasterPainter extends CustomPainter { + final TileLayerOptions options; + final Map<({TileCoordinates coordinates, Object layerKey}), RasterTileData> + visibleTiles; + + _RasterPainter({ + super.repaint, + required this.options, + required this.visibleTiles, + }); + + @override + void paint(Canvas canvas, Size size) { + for (final MapEntry(key: (:coordinates, layerKey: _), value: tile) + in visibleTiles.entries) {} + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + // TODO: implement shouldRepaint + throw UnimplementedError(); + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/loader.dart index 6186bf4d1..adf1e9fd7 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/loader.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/loader.dart @@ -1,11 +1,9 @@ -import 'dart:async'; import 'dart:typed_data'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loaders/source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; @@ -14,16 +12,13 @@ import 'package:meta/meta.dart'; /// /// 1. The [sourceGenerator] uses a tile's [TileCoordinates] & the ambient /// [TileLayerOptions] to generate an object, describing the tile's 'source' +/// ([S]) /// -/// 2. The [sourceFetcher] uses this 'source' to generate an output data (which -/// is held within a [TileData] for the renderer's benefit) -/// -/// The data ([D]) may be of any shape - but is commonly raw bytes for the -/// renderer to process. The 'source' ([S]) may be of any shape, such as -/// [TileSource]. +/// 2. The [sourceFetcher] uses this 'source' to generate an output [TileData] +/// ([D]) @immutable -final class TileLoader - implements TileLoaderBase { +final class TileLoader + implements BaseTileLoader { /// Tile source generator /// /// See documentation on [TileLoader] & [TileSourceGenerator] for information. @@ -34,11 +29,6 @@ final class TileLoader /// See documentation on [TileLoader] & [TileSourceFetcher] for information. final TileSourceFetcher sourceFetcher; - // TODO: Consider whether a 3rd step is useful (for converting bytes -> - // resource), which would add better typing guarantees (tie to specific - // renderers) - but creates more types & may be able to be best handled by the - // renderer - /// Create a tile loader from a source generator & fetcher const TileLoader({ required this.sourceGenerator, @@ -46,16 +36,8 @@ final class TileLoader }); @override - TileData load(TileCoordinates coordinates, TileLayerOptions options) { - final abortTrigger = Completer(); - return TileData( - abort: abortTrigger.complete, - data: sourceFetcher( - sourceGenerator(coordinates, options), - abortTrigger.future, - ), - ); - } + D load(TileCoordinates coordinates, TileLayerOptions options) => + sourceFetcher(sourceGenerator(coordinates, options)); @override bool operator ==(Object other) => @@ -68,7 +50,6 @@ final class TileLoader int get hashCode => Object.hash(sourceGenerator, sourceFetcher); /// [Uint8List] that forms a fully transparent image - @deprecated static final transparentImage = Uint8List.fromList([ 0x89, 0x50, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/asset/asset.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart new file mode 100644 index 000000000..5b64bd89b --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; + +/// A tile bytes fetcher which fetches from the app's shipped assets, based on +/// their [TileSource] +/// +/// In normal usage, all tiles (or at least each individual lowest-level +/// directory) must be listed as normal in the pubspec. +// TODO: This a considerably different implementation - check performance +// If adjustment is needed, it's likely to really mess up the contracts I've +// set up. +@immutable +class AssetBytesFetcher implements TileBytesFetcher { + /// Asset bundle to retrieve tiles from + final AssetBundle? assetBundle; + + /// A tile bytes fetcher which fetches from the app's shipped assets, based on + /// their [TileSource] + /// + /// By default, this uses the default [rootBundle]. If a different bundle is + /// required, either specify it manually, or use the + /// [AssetBytesFetcher.fromContext] constructor. + const AssetBytesFetcher({this.assetBundle}); + + /// A tile bytes fetcher which fetches from the app's shipped assets, based on + /// their [TileSource] + /// + /// Gets the asset bundle from the [DefaultAssetBundle] depending on the + /// provided context. + AssetBytesFetcher.fromContext(BuildContext context) + : assetBundle = DefaultAssetBundle.of(context); + + @override + Future call(TileSource source, Future abortSignal) async { + final bundle = assetBundle ?? rootBundle; + try { + return Uint8List.sublistView(await bundle.load(source.uri)); + } on Exception { + if (source.fallbackUri == null) rethrow; + return Uint8List.sublistView(await bundle.load(source.fallbackUri!)); + } + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart new file mode 100644 index 000000000..73a5282f0 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart @@ -0,0 +1,72 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:meta/meta.dart'; + +/// Fetches a tile's bytes based on its 'source' ([S]) +/// +/// Implementations make no assumption as to what the bytes may represent. For +/// example, it is the [RasterTileFetcher]s responsibility to assume and decode +/// the bytes to a raster image. Therefore, [TileSourceFetcher]s should delegate +/// byte fetching to an implementation if the resource can be represented in +/// bytes, to promote reusability and compatibility. +/// +/// Implementers should implement longer-term caching where necessary, or +/// delegate to a cacher. Note that some [TileSourceFetcher]s may also perform +/// caching of the resulting resource, often in the short-term - such as the +/// [RasterTileFetcher] using the Flutter [ImageCache]. +/// +/// Implementations which work with the [RasterTileFetcher] should consider +/// mixing-in [ImageChunkEventsSupport]. +abstract interface class TileBytesFetcher { + /// Fetches a tile's bytes based on its 'source' ([S]) + /// + /// The [abortSignal] completes when the tile is no longer required. If + /// possible, any ongoing work (such as an HTTP request) should be aborted. + /// If aborting and a result is unavailable, [TileAbortedException] should be + /// thrown. + FutureOr call(S source, Future abortSignal); +} + +/// Allows a [TileBytesFetcher] to integrate more closely with the raster tile +/// stack by reporting progress events to the underlying [ImageProvider] +abstract mixin class ImageChunkEventsSupport + implements TileBytesFetcher { + /// Redirects to [withImageChunkEventsSink] + @override + @nonVirtual + FutureOr call(S source, Future abortSignal) => + withImageChunkEventsSink(source, abortSignal); + + /// Fetches a tile's bytes based on its 'source' ([S]) + /// + /// The [abortSignal] completes when the tile is no longer required. If + /// possible, any ongoing work (such as an HTTP request) should be aborted. + /// If aborting and a result is unavailable, [TileAbortedException] should be + /// thrown. + /// + /// [chunkEvents] should be used when consolidating a stream of bytes to + /// report progress notifications to the underlying [ImageProvider]. + FutureOr withImageChunkEventsSink( + S source, + Future abortSignal, { + StreamSink? chunkEvents, + }); +} + +/// Exception thrown when a tile was loading but aborted early as it was no +/// longer required +class TileAbortedException implements Exception { + /// Optional description of the tile + final Object? source; + + /// Exception thrown when a tile was loading but aborted early as it was no + /// longer required + const TileAbortedException({this.source}); + + @override + String toString() => 'TileAbortedException: $source'; +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart similarity index 72% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart index 88f9a4a88..527a9b7e4 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/file_stub.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart @@ -1,8 +1,9 @@ import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -class FileBytesFetcher implements TileSourceFetcher { +@immutable +class FileBytesFetcher implements TileBytesFetcher { const FileBytesFetcher(); @override diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart similarity index 78% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart index 51d0ee068..5b4d3b930 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/file/tile_provider_io.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart @@ -2,10 +2,11 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -class FileBytesFetcher implements TileSourceFetcher { +@immutable +class FileBytesFetcher implements TileBytesFetcher { const FileBytesFetcher(); @override diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart new file mode 100644 index 000000000..e8b5b1f35 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart @@ -0,0 +1,97 @@ +import 'dart:async'; +import 'dart:io' show HttpHeaders; // web safe! + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +import 'package:http/http.dart'; +import 'package:http/retry.dart'; + +/// A tile bytes fetcher which fetches from the network using HTTP, based on +/// their [TileSource] +@immutable +class NetworkBytesFetcher + with ImageChunkEventsSupport + implements TileBytesFetcher { + /// HTTP headers to send with each request + final Map headers; + + /// HTTP client used to make each request + /// + /// It is much more efficient if a single client is used repeatedly, as it + /// can maintain an open socket connection to the server. + /// + /// Where possible, clients should support aborting of requests when the + /// response is no longer required. + final Client httpClient; + + // TODO: Add caching provider integration + + /// A tile bytes fetcher which fetches from the network using HTTP, based on + /// their [TileSource] + /// + /// The string "flutter_map ([uaIdentifier])" is set as the 'User-Agent' HTTP + /// header on non-web platforms, if the UA header is not specified manually. + /// If not provided, the string "flutter_map (unknown)" is used. + /// [uaIdentifier] should uniquely identify your app or project - for example, + /// 'com.example.app'. + /// + /// > [!TIP] + /// > Setting a [uaIdentifier] (or a custom UA header) is strongly recommended + /// > for all projects. It helps the server differentiate your traffic from + /// > other flutter_map traffic. + /// > + /// > A useful UA header is required by the terms of service of many tile + /// > servers. flutter_map places some restrictions on projects if a UA header + /// > is left unset. + NetworkBytesFetcher({ + String? uaIdentifier, + Map? headers, + Client? httpClient, + }) : headers = headers ?? {}, + httpClient = httpClient ?? RetryClient(Client()) { + if (!kIsWeb) { + this.headers.putIfAbsent( + HttpHeaders.userAgentHeader, + () => 'flutter_map ($uaIdentifier)', + ); + } + } + + @override + FutureOr withImageChunkEventsSink( + TileSource source, + Future abortSignal, { + StreamSink? chunkEvents, + bool useFallback = false, + }) { + // TODO: Replace with #2082 + return httpClient + .readBytes( + Uri.parse(useFallback ? source.fallbackUri! : source.uri), + headers: headers, + ) + .onError((err, _) { + if (useFallback || source.fallbackUri == null) { + throw err; + } + return withImageChunkEventsSink( + source, + abortSignal, + chunkEvents: chunkEvents, + useFallback: true, + ); + }); + } + + @override + int get hashCode => Object.hash(headers, httpClient); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is NetworkBytesFetcher && + other.headers == headers && + other.httpClient == httpClient); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart new file mode 100644 index 000000000..a14415daf --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart @@ -0,0 +1,33 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; + +/// A tile source fetcher which delgates fetching of a raster image's bytes to +/// a [TileBytesFetcher], then passes it directly to the renderer (wrapping +/// it in a [WrapperTileData]) +/// +/// Users should consider whether it would be more efficient or better practise +/// to avoid this class and implement a more custom fetcher for their use-case. +class RawBytesTileFetcher + implements TileSourceFetcher> { + /// The delegate which provides the bytes for the this tile + final TileBytesFetcher bytesFetcher; + + /// A tile source fetcher which delgates fetching of a raster image's bytes to + /// a [TileBytesFetcher], then passes it directly to the renderer (wrapping + /// it in a [WrapperTileData]) + const RawBytesTileFetcher({required this.bytesFetcher}); + + @override + WrapperTileData call(S source) { + final abortTrigger = Completer.sync(); + + return WrapperTileData( + data: bytesFetcher(source, abortTrigger.future), + dispose: abortTrigger.complete, + ); + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart deleted file mode 100644 index cbafbfaec..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/network/network.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io' show HttpHeaders; // web safe! - -import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; -import 'package:http/http.dart'; -import 'package:http/retry.dart'; - -class NetworkBytesFetcher implements TileSourceFetcher { - final Map headers; - final Client httpClient; - - NetworkBytesFetcher({ - Map? headers, - Client? httpClient, - }) : headers = headers ?? {}, - httpClient = httpClient ?? RetryClient(Client()); - - NetworkBytesFetcher.withUAIdentifier( - String identifier, { - Map? headers, - Client? httpClient, - }) : headers = headers ?? {}, - httpClient = httpClient ?? RetryClient(Client()) { - if (!kIsWeb) { - this.headers.putIfAbsent( - HttpHeaders.userAgentHeader, - () => 'flutter_map ($identifier)', - ); - } - } - - @override - Future call( - TileSource source, - Future abortSignal, { - bool useFallback = false, - }) { - // TODO: Replace with #2082 - return httpClient - .readBytes( - Uri.parse(useFallback ? source.fallbackUri! : source.uri), - headers: headers, - ) - .onError((err, _) { - if (useFallback || source.fallbackUri == null) { - throw err; - } - return this(source, abortSignal, useFallback: true); - }); - } - - @override - int get hashCode => Object.hash(headers, httpClient); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is NetworkBytesFetcher && - other.headers == headers && - other.httpClient == httpClient); -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart new file mode 100644 index 000000000..0588234ab --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart @@ -0,0 +1,95 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +/// Similar to [MemoryImage], but requires a [key] to identify and cache the +/// image, and supports lazily getting the image bytes with chunk support +class KeyedGeneratedBytesImage extends ImageProvider { + /// Similar to [MemoryImage], but requires a [key] to identify and cache the + /// image, and supports lazily getting the image bytes with chunk support + const KeyedGeneratedBytesImage({ + required this.key, + required this.bytesGetter, + this.scale = 1.0, + }); + + /// Identifier for this image + /// + /// This is used (alongside [scale]) to identify this image in the image + /// cache. Therefore, two requirements must be met: + /// + /// * The same key must not be used for two different images + /// * The same image should always use the same key + final Object key; + + /// Callback which returns the bytes to decode into an image. + /// + /// Using the provided `chunkEvents` stream is optional, but may be used to + /// report image loading progress. + /// + /// The bytes represent encoded image bytes and can be encoded in any of the + /// following supported image formats: {@macro dart.ui.imageFormats} + /// + /// See also: + /// + /// * [PaintingBinding.instantiateImageCodecWithSize] + final FutureOr Function(StreamSink chunkEvents) + bytesGetter; + + /// The scale to place in the [ImageInfo] object of the image. + /// + /// See also: + /// + /// * [ImageInfo.scale], which gives more information on how this scale is + /// applied. + final double scale; + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + ImageStreamCompleter loadImage( + KeyedGeneratedBytesImage key, + ImageDecoderCallback decode, + ) { + final chunkEvents = StreamController(); + + return MultiFrameImageStreamCompleter( + codec: _loadAsync(key, chunkEvents: chunkEvents.sink, decode: decode) + ..then( + (_) => unawaited(chunkEvents.close()), + onError: (_) => unawaited(chunkEvents.close()), + ), + chunkEvents: chunkEvents.stream, + scale: key.scale, + debugLabel: 'KeyedGeneratedBytesImage($key)', + ); + } + + Future _loadAsync( + KeyedGeneratedBytesImage key, { + required StreamSink chunkEvents, + required ImageDecoderCallback decode, + }) async => + await decode( + await ImmutableBuffer.fromUint8List(await bytesGetter(chunkEvents)), + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is KeyedGeneratedBytesImage && + other.key == key && + other.scale == scale); + + @override + int get hashCode => Object.hash(key, scale); + + @override + String toString() => + '${objectRuntimeType(this, 'KeyedGeneratedBytesImage')}(key: $key, scale: ${scale.toStringAsFixed(1)})'; +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart new file mode 100644 index 000000000..e03a75285 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart @@ -0,0 +1,49 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; + +/// A tile source fetcher which delgates fetching of a raster image's bytes to +/// a [TileBytesFetcher], then creates an [ImageProvider] by decoding the bytes +/// +/// This is used instead of directly sending the bytes to the renderer, as it +/// hooks into the Flutter image cache, meaning that tiles are cached in memory. +/// Additionally, it is easier for the renderer canvas to work with. +class RasterTileFetcher + implements TileSourceFetcher { + /// The delegate which provides the bytes for the this tile + /// + /// This may not be called for every tile, if the tile was already present in + /// the ambient [ImageCache]. + final TileBytesFetcher bytesFetcher; + + /// A tile source fetcher which delgates fetching of a raster image's bytes + /// to a [TileBytesFetcher], then creates an [ImageProvider] by decoding the + /// bytes + const RasterTileFetcher({required this.bytesFetcher}); + + @override + RasterTileData call(S source) { + final abortTrigger = Completer.sync(); + + return RasterTileData( + image: KeyedGeneratedBytesImage( + key: source, + bytesGetter: (chunkEvents) { + if (bytesFetcher case final ImageChunkEventsSupport bytesFetcher) { + return bytesFetcher.withImageChunkEventsSink( + source, + abortTrigger.future, + chunkEvents: chunkEvents, + ); + } + return bytesFetcher(source, abortTrigger.future); + }, + ), + dispose: abortTrigger.complete, + ); + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart new file mode 100644 index 000000000..5870a342c --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart @@ -0,0 +1,33 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; +import 'package:meta/meta.dart'; + +/// Raster tile data associated with a particular tile coordinate +/// +/// This is used for communication between the [RasterTileFetcher] and the +/// raster tile renderer. +/// +/// It is not usually necessary to consume this externally. +class RasterTileData implements TileData { + /// Actual raster image resource + final ImageProvider image; + + /// Raster tile data associated with a particular tile coordinate + const RasterTileData({required this.image, required void Function() dispose}) + : _dispose = dispose; + + @override + bool get isLoaded => throw UnimplementedError(); + + @override + Future get whenLoaded => throw UnimplementedError(); + + @internal + @override + void dispose() { + _dispose(); + } + + final void Function() _dispose; +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart index ce4197e76..cd51e9da3 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart @@ -1,45 +1,36 @@ -import 'dart:async'; - import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; /// Generates a tile's 'source' based on its own properties, the ambient /// [TileLayerOptions], and the tile's [TileCoordinates] /// -/// The 'source' type must be consumable by the [TileSourceFetcher] used. +/// The source type must be consumable by the [TileSourceFetcher] used. /// /// If this generator accepts other properties/options, it must remain immutable /// and must set a valid equality operator. @immutable abstract interface class TileSourceGenerator { - /// Generates a tile's 'source' + /// Generates a tile's source /// /// See documentation on [TileSourceGenerator] for more information. S call(TileCoordinates coordinates, TileLayerOptions options); } -/// Fetch a tile's data based on its 'source' ([S]) -/// -/// A tile's data ([T]) is often bytes. These may be interpreted by a tile -/// layer's renderer. However, this is not required - it may be preferable to -/// perform some (potentially asynchronous) processing of data before sending it -/// to the renderer. +/// Fetch a tile's data ([D]) based on its 'source' ([S]) /// -/// The 'source' type is set by the [TileSourceGenerator]. The fetcher does not +/// The source type is set by the [TileSourceGenerator]. The fetcher does not /// have access to the ambient [TileLayerOptions], therefore any required -/// options must appear in the 'source'. -/// -/// Supports an abort signal to abort an ongoing operation, such as a network -/// request. It is not required to respect the signal. +/// options must appear in the source. /// /// If this fetcher accepts other properties/options, it must remain immutable /// and must set a valid equality operator. @immutable abstract interface class TileSourceFetcher { - /// Fetch a tile's data based on its 'source' + D extends TileData> { + /// Fetch a tile's data based on its source /// /// See documentation on [TileSourceFetcher] for more information. - FutureOr call(S source, Future abortSignal); + D call(S source); } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart deleted file mode 100644 index 670effcbf..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/slippy.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; -import 'package:meta/meta.dart'; - -@immutable -class SlippyMapGenerator implements TileSourceGenerator { - final String urlTemplate; - final String? fallbackUrlTemplate; - final List subdomains; - final Map additionalPlaceholders; - - final bool tms; - - const SlippyMapGenerator({ - required this.urlTemplate, - this.fallbackUrlTemplate, - this.subdomains = const [], - this.additionalPlaceholders = const {}, - this.tms = false, - }); - - @override - TileSource call(TileCoordinates coordinates, TileLayerOptions options) { - final replacementMap = generateReplacementMap(coordinates, options); - - String replacer(Match match) { - final value = replacementMap[match.group(1)!]; - if (value != null) return value; - throw ArgumentError('Missing value for placeholder: {${match.group(1)}}'); - } - - final url = urlTemplate.replaceAllMapped( - templatePlaceholderElement, - replacer, - ); - final fallbackUrl = fallbackUrlTemplate?.replaceAllMapped( - templatePlaceholderElement, - replacer, - ); - - return TileSource(uri: url, fallbackUri: fallbackUrl); - } - - @visibleForOverriding - Map generateReplacementMap( - TileCoordinates coordinates, - TileLayerOptions options, - ) { - final zoom = (options.zoomOffset + - (options.zoomReverse - ? options.maxZoom - coordinates.z.toDouble() - : coordinates.z.toDouble())) - .round(); - - return { - 'x': coordinates.x.toString(), - 'y': (tms ? ((1 << zoom) - 1) - coordinates.y : coordinates.y).toString(), - 'z': zoom.toString(), - 's': subdomains.isEmpty - ? '' - : subdomains[(coordinates.x + coordinates.y) % subdomains.length], - // TODO: Retina mode - // We can easily implement server retina mode: simulated retina mode - // requires cooperation with renderer! - //'r': options.resolvedRetinaMode == RetinaMode.server ? '@2x' : '', - 'd': options.tileDimension.toString(), - ...additionalPlaceholders, - }; - } - - static final templatePlaceholderElement = RegExp('{([^{}]*)}'); - - @override - int get hashCode => Object.hash( - urlTemplate, - fallbackUrlTemplate, - subdomains, - additionalPlaceholders, - tms, - ); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is SlippyMapGenerator && - other.urlTemplate == urlTemplate && - other.fallbackUrlTemplate == fallbackUrlTemplate && - other.subdomains == subdomains && - other.additionalPlaceholders == additionalPlaceholders && - other.tms == tms); -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart index 75e6787c5..9ebdc2b78 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart @@ -1,12 +1,14 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:meta/meta.dart'; +/// A tile source generator which generates tiles for the +/// [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) referencing system @immutable class WMSGenerator implements TileSourceGenerator { /// WMS service's URL, for example 'http://ows.mundialis.de/services/service?' diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart new file mode 100644 index 000000000..2c28d6d2a --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart @@ -0,0 +1,149 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +/// A tile source generator which generates tiles for slippy map tile servers +/// following the standard XYZ tile referencing system +/// +/// [Slippy maps](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) are +/// also known as [tiled web maps](https://en.wikipedia.org/wiki/Tiled_web_map) +/// more generally, or sometimes as 'CARTO'. This is the most common map tile +/// referencing system in use. +/// +/// This generator can also support part of the alternative +/// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) standard by flipping +/// the Y axis. +@immutable +class XYZGenerator implements TileSourceGenerator { + /// Template string for tile resources (containing placeholders) + /// + /// The following placeholders are supported, in addition to any described in + /// [additionalPlaceholders] : + /// + /// * `{z}`, `{x}`, `{z}`: tile coordinates + /// * `{s}`: subdomain chosen from [subdomains] + /// * `{r}`: retina mode (filled with "@2x" when enabled) + /// * `{d}`: current [TileLayerOptions.tileDimension] + final String uriTemplate; + + /// Template string for tile resources used by some [TileSourceFetcher]s if + /// the request/response to/from the primary [uriTemplate] fails + /// + /// > [!WARNING] + /// > Not all fetchers support falling-back. Note that failing the primary + /// > template may take some time (such as a HTTP timeout elapsing). + /// > Additionally, using fallbacks may have negative performance and tile + /// > usage consequences. See online documentation for more information. + final String? fallbackUriTemplate; + + /// List of subdomains for the [uriTemplate] (to replace the `{s}` + /// placeholder) + /// + /// > [!NOTE] + /// > This may no longer be necessary for many tile servers in many cases. + /// > See online documentation for more information. + final List subdomains; + + /// Static information that should replace associated placeholders in the + /// [uriTemplate] + /// + /// For example, this could be used to more easily apply API keys to + /// templates. + /// + /// Override [generateReplacementMap] to dynamically generate placeholders. + final Map additionalPlaceholders; + + /// Whether to invert Y axis numbering for tiles + final bool tms; + + /// A tile source generator which generates tiles for slippy map tile servers + /// following the standard XYZ tile referencing system + const XYZGenerator({ + required this.uriTemplate, + this.fallbackUriTemplate, + this.subdomains = const [], + this.additionalPlaceholders = const {}, + this.tms = false, + }); + + @override + TileSource call(TileCoordinates coordinates, TileLayerOptions options) { + final replacementMap = generateReplacementMap(coordinates, options); + + String replacer(Match match) { + final value = replacementMap[match.group(1)!]; + if (value != null) return value; + throw ArgumentError('Missing value for placeholder: {${match.group(1)}}'); + } + + final uri = uriTemplate.replaceAllMapped( + templatePlaceholderElement, + replacer, + ); + final fallbackUri = fallbackUriTemplate?.replaceAllMapped( + templatePlaceholderElement, + replacer, + ); + + return TileSource(uri: uri, fallbackUri: fallbackUri); + } + + /// Generates the mapping of [uriTemplate] placeholders to replacements + @visibleForOverriding + Map generateReplacementMap( + TileCoordinates coordinates, + TileLayerOptions options, + ) { + final zoom = (options.zoomOffset + + (options.zoomReverse + ? options.maxZoom - coordinates.z.toDouble() + : coordinates.z.toDouble())) + .round(); + + return { + 'x': coordinates.x.toString(), + 'y': (tms ? ((1 << zoom) - 1) - coordinates.y : coordinates.y).toString(), + 'z': zoom.toString(), + 's': subdomains.isEmpty + ? '' + : subdomains[(coordinates.x + coordinates.y) % subdomains.length], + // TODO: Retina mode + // We can easily implement server retina mode: simulated retina mode + // requires cooperation with renderer! + //'r': options.resolvedRetinaMode == RetinaMode.server ? '@2x' : '', + 'd': options.tileDimension.toString(), + ...additionalPlaceholders, + }; + } + + /// Regex that describes the format of placeholders in a `uriTemplate` + /// + /// The regex used prior to v6 originated from leaflet.js, specifically from + /// commit [dc79b10683d2](https://github.com/Leaflet/Leaflet/commit/dc79b10683d232b9637cbe4d65567631f4fa5a0b). + /// Prior to that, a more permissive regex was used, starting from commit + /// [70339807ed6b](https://github.com/Leaflet/Leaflet/commit/70339807ed6bec630ee9c2e96a9cb8356fa6bd86). + /// It is never mentioned why this regex was used or changed in Leaflet. + /// This regex is more permissive of the characters it allows. + static final templatePlaceholderElement = RegExp('{([^{}]*)}'); + + @override + int get hashCode => Object.hash( + uriTemplate, + fallbackUriTemplate, + subdomains, + additionalPlaceholders, + tms, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is XYZGenerator && + other.uriTemplate == uriTemplate && + other.fallbackUriTemplate == fallbackUriTemplate && + other.subdomains == subdomains && + other.additionalPlaceholders == additionalPlaceholders && + other.tms == tms); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart similarity index 82% rename from lib/src/layer/modern_tile_layer/tile_loader/source.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart index e322bff5d..13a0b7b93 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart @@ -3,7 +3,6 @@ import 'package:meta/meta.dart'; /// Default tile 'source' implementation for the default [TileLoader] /// implementation -@internal @immutable class TileSource { final String uri; @@ -11,6 +10,9 @@ class TileSource { const TileSource({required this.uri, this.fallbackUri}); + //! It is very important that these remain correct - they uniquely identify + //! a resulting image in the raster fetcher. + @override int get hashCode => Object.hash(uri, fallbackUri); From 59eaa6aa62853837d05a66f59d30d05e1ba47c1b Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 8 Jul 2025 18:59:31 +0100 Subject: [PATCH 03/13] Started transplanting `TileImage` to `RasterTileData` --- .../modern_tile_layer/base_tile_layer.dart | 1 + .../tile_layer.dart/raster_tile_layer.dart | 4 +- .../source_fetchers/raster/tile_data.dart | 82 +++++++++++++++++-- 3 files changed, 78 insertions(+), 9 deletions(-) diff --git a/lib/src/layer/modern_tile_layer/base_tile_layer.dart b/lib/src/layer/modern_tile_layer/base_tile_layer.dart index c48694842..db766b6ca 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_layer.dart @@ -67,6 +67,7 @@ class _BaseTileLayerState extends State> { key, () => widget.tileLoader.load(coordinates, widget.options) ..whenLoaded.then((_) => _pruneOnLoadedTile(key)), + // TODO: Consider how to handle errors ); } diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart index 32be2d5ea..c947478c7 100644 --- a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart @@ -105,7 +105,9 @@ class _RasterPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { for (final MapEntry(key: (:coordinates, layerKey: _), value: tile) - in visibleTiles.entries) {} + in visibleTiles.entries) { + //final image = tile.imageInfo. + } } @override diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart index 5870a342c..74ab55d76 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; @@ -10,24 +12,88 @@ import 'package:meta/meta.dart'; /// /// It is not usually necessary to consume this externally. class RasterTileData implements TileData { - /// Actual raster image resource + /// Actual raster [ImageProvider] final ImageProvider image; + final void Function() _dispose; + /// Raster tile data associated with a particular tile coordinate - const RasterTileData({required this.image, required void Function() dispose}) + RasterTileData({required this.image, required void Function() dispose}) : _dispose = dispose; + bool _isDisposed = false; + @internal @override - bool get isLoaded => throw UnimplementedError(); + void dispose() { + _dispose(); + _isDisposed = true; + } + + DateTime? loadStartedTime; + final _loadedTracker = Completer(); @override - Future get whenLoaded => throw UnimplementedError(); + Future get whenLoaded => _loadedTracker.future; - @internal @override - void dispose() { - _dispose(); + bool get isLoaded => loaded != null; + ({ + DateTime time, + ImageInfo? successfulImageInfo, + ({Object exception, StackTrace? stackTrace})? failureInfo, + })? loaded; + + ImageStream? _imageStream; + late ImageStreamListener _imageStreamListener; + + void load() { + // TODO: Consider whether `load` can be called multiple times + if (_isDisposed) return; + + loadStartedTime = DateTime.now(); + + try { + final oldImageStream = _imageStream; + _imageStream = image.resolve(ImageConfiguration.empty); + + if (_imageStream!.key != oldImageStream?.key) { + oldImageStream?.removeListener(_imageStreamListener); + + _imageStreamListener = ImageStreamListener( + _onImageLoadSuccess, + onError: _onImageLoadError, + ); + _imageStream!.addListener(_imageStreamListener); + } + } catch (e, s) { + // Make sure all exceptions are handled - #444 / #536 + _onImageLoadError(e, s); + } } - final void Function() _dispose; + void _onImageLoadSuccess(ImageInfo imageInfo, bool synchronousCall) { + if (_isDisposed) return; + + loaded = ( + time: DateTime.now(), + successfulImageInfo: imageInfo, + failureInfo: null + ); + _loadedTracker.complete(); + _display(); + } + + void _onImageLoadError(Object exception, StackTrace? stackTrace) { + if (_isDisposed) return; + + loaded = ( + time: DateTime.now(), + successfulImageInfo: null, + failureInfo: (exception: exception, stackTrace: stackTrace), + ); + _loadedTracker.completeError(exception, stackTrace); + + // TODO: Was `if (errorImage != null) _display();`? + _display(); + } } From 7dfd59dab4acce9f79eefffcf208320f4a3d1fb6 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 8 Jul 2025 22:18:49 +0100 Subject: [PATCH 04/13] Add TODO --- .../tile_loader/source_fetchers/raster/raster_tile_fetcher.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart index e03a75285..d70923027 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart @@ -31,6 +31,7 @@ class RasterTileFetcher return RasterTileData( image: KeyedGeneratedBytesImage( + // TODO: Include properties of bytes fetcher (hashcode of source + bytesFetcher)? key: source, bytesGetter: (chunkEvents) { if (bytesFetcher case final ImageChunkEventsSupport bytesFetcher) { From 21d2945b7bf54cf59ec96d91c1feaabe4163d2d4 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 11 Jul 2025 15:24:02 +0100 Subject: [PATCH 05/13] Integrated caching --- lib/flutter_map.dart | 10 +- .../tile_layer.dart/raster_tile_layer.dart | 8 +- .../bytes_fetchers/asset/asset.dart | 37 +- .../bytes_fetchers/bytes_fetcher.dart | 83 +++-- .../bytes_fetchers/file/file_io.dart | 39 +++ .../bytes_fetchers/file/file_stub.dart | 17 +- .../bytes_fetchers/file/tile_provider_io.dart | 24 -- .../built_in/built_in_caching_provider.dart | 9 +- .../caching/built_in/impl/native/README.md | 0 .../caching/built_in/impl/native/native.dart | 2 +- .../impl/native/workers/size_reducer.dart | 2 +- .../workers/tile_and_size_monitor_writer.dart | 6 +- .../network/caching/built_in/impl/stub.dart | 0 .../caching/built_in/impl/web/web.dart | 0 .../network/caching/caching_provider.dart | 6 +- .../disabled/disabled_caching_provider.dart | 0 .../network/caching/tile_metadata.dart | 0 .../caching/tile_read_failure_exception.dart | 0 .../network/fetcher/consolidate_response.dart | 83 +++++ .../network/fetcher/network.dart | 317 ++++++++++++++++++ .../bytes_fetchers/network/network.dart | 97 ------ .../source_fetchers/bytes_passthrough.dart | 33 -- .../raster/image_provider.dart | 51 ++- .../raster/raster_tile_fetcher.dart | 54 ++- .../source_fetchers/raster/tile_data.dart | 44 ++- 25 files changed, 658 insertions(+), 264 deletions(-) create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/built_in_caching_provider.dart (91%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/impl/native/README.md (100%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/impl/native/native.dart (97%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/impl/native/workers/size_reducer.dart (95%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart (96%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/impl/stub.dart (100%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/built_in/impl/web/web.dart (100%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/caching_provider.dart (89%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/disabled/disabled_caching_provider.dart (100%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/tile_metadata.dart (100%) rename lib/src/layer/{tile_layer/tile_provider => modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers}/network/caching/tile_read_failure_exception.dart (100%) create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 3ae100b98..db0a9d6c6 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -29,6 +29,11 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart'; export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer/label/deprecated_placements.dart'; export 'package:flutter_map/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart'; @@ -49,11 +54,6 @@ export 'package:flutter_map/src/layer/tile_layer/tile_provider/asset/provider.da export 'package:flutter_map/src/layer/tile_layer/tile_provider/base_tile_provider.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_provider/file/stub_tile_provider.dart' if (dart.library.io) 'package:flutter_map/src/layer/tile_layer/tile_provider/file/native_tile_provider.dart'; -export 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/built_in_caching_provider.dart'; -export 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/caching_provider.dart'; -export 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/disabled/disabled_caching_provider.dart'; -export 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/tile_metadata.dart'; -export 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/tile_read_failure_exception.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_provider/network/tile_provider.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_event.dart'; export 'package:flutter_map/src/layer/tile_layer/tile_update_transformer.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart index c947478c7..3b03b49f0 100644 --- a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart @@ -3,7 +3,7 @@ import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; @@ -29,7 +29,7 @@ class RasterTileLayer extends StatefulWidget { final TileLayerOptions options; final TileSourceGenerator sourceGenerator; - final TileBytesFetcher bytesFetcher; + final SourceBytesFetcher bytesFetcher; @override State createState() => _RasterTileLayerState(); @@ -105,9 +105,7 @@ class _RasterPainter extends CustomPainter { @override void paint(Canvas canvas, Size size) { for (final MapEntry(key: (:coordinates, layerKey: _), value: tile) - in visibleTiles.entries) { - //final image = tile.imageInfo. - } + in visibleTiles.entries) {} } @override diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart index 5b64bd89b..ee546ad22 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart @@ -5,29 +5,27 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -/// A tile bytes fetcher which fetches from the app's shipped assets, based on +/// A [SourceBytesFetcher] which fetches from the app's shipped assets, based on /// their [TileSource] /// /// In normal usage, all tiles (or at least each individual lowest-level /// directory) must be listed as normal in the pubspec. // TODO: This a considerably different implementation - check performance -// If adjustment is needed, it's likely to really mess up the contracts I've -// set up. @immutable -class AssetBytesFetcher implements TileBytesFetcher { +class AssetBytesFetcher implements SourceBytesFetcher { /// Asset bundle to retrieve tiles from final AssetBundle? assetBundle; - /// A tile bytes fetcher which fetches from the app's shipped assets, based on - /// their [TileSource] + /// A [SourceBytesFetcher] which fetches from the app's shipped assets, based + /// on their [TileSource] /// /// By default, this uses the default [rootBundle]. If a different bundle is /// required, either specify it manually, or use the /// [AssetBytesFetcher.fromContext] constructor. const AssetBytesFetcher({this.assetBundle}); - /// A tile bytes fetcher which fetches from the app's shipped assets, based on - /// their [TileSource] + /// A [SourceBytesFetcher] which fetches from the app's shipped assets, based + /// on their [TileSource] /// /// Gets the asset bundle from the [DefaultAssetBundle] depending on the /// provided context. @@ -35,13 +33,28 @@ class AssetBytesFetcher implements TileBytesFetcher { : assetBundle = DefaultAssetBundle.of(context); @override - Future call(TileSource source, Future abortSignal) async { + Future call({ + required TileSource source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + bool useFallback = false, + }) async { final bundle = assetBundle ?? rootBundle; + final resolvedUri = useFallback ? source.fallbackUri ?? '' : source.uri; + try { - return Uint8List.sublistView(await bundle.load(source.uri)); + final bytes = await bundle.load(resolvedUri); + return await transformer(Uint8List.sublistView(bytes)); } on Exception { - if (source.fallbackUri == null) rethrow; - return Uint8List.sublistView(await bundle.load(source.fallbackUri!)); + if (useFallback || source.fallbackUri == null) rethrow; + return this( + source: source, + abortSignal: abortSignal, + // In fallback scenarios, we never reuse bytes + transformer: (bytes, {allowReuse = true}) => + transformer(bytes, allowReuse: false), + useFallback: useFallback, + ); } } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart index 73a5282f0..5bc4769ae 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart @@ -6,53 +6,62 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetch import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; import 'package:meta/meta.dart'; -/// Fetches a tile's bytes based on its 'source' ([S]) -/// -/// Implementations make no assumption as to what the bytes may represent. For -/// example, it is the [RasterTileFetcher]s responsibility to assume and decode -/// the bytes to a raster image. Therefore, [TileSourceFetcher]s should delegate -/// byte fetching to an implementation if the resource can be represented in -/// bytes, to promote reusability and compatibility. +/// Fetches a tile's bytes based on its source ([S]), transforming it into a +/// desired resource using a supplied [BytesToResourceTransformer] /// /// Implementers should implement longer-term caching where necessary, or -/// delegate to a cacher. Note that some [TileSourceFetcher]s may also perform +/// delegate to a cacher. Note that [TileSourceFetcher]s may also perform /// caching of the resulting resource, often in the short-term - such as the /// [RasterTileFetcher] using the Flutter [ImageCache]. /// /// Implementations which work with the [RasterTileFetcher] should consider /// mixing-in [ImageChunkEventsSupport]. -abstract interface class TileBytesFetcher { - /// Fetches a tile's bytes based on its 'source' ([S]) +abstract interface class SourceBytesFetcher { + /// {@template fm.tilelayer.tilebytesfetcher.call} + /// Fetches a tile's bytes based on its source ([S]), transforming it into a + /// desired resource ([R]) using a supplied transformer /// /// The [abortSignal] completes when the tile is no longer required. If /// possible, any ongoing work (such as an HTTP request) should be aborted. /// If aborting and a result is unavailable, [TileAbortedException] should be /// thrown. - FutureOr call(S source, Future abortSignal); + /// + /// See [BytesToResourceTransformer] for more information about handling the + /// [transformer]. + /// {@endtemplate} + FutureOr call({ + required S source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + }); } -/// Allows a [TileBytesFetcher] to integrate more closely with the raster tile +/// Allows a [SourceBytesFetcher] to integrate more closely with the raster tile /// stack by reporting progress events to the underlying [ImageProvider] abstract mixin class ImageChunkEventsSupport - implements TileBytesFetcher { + implements SourceBytesFetcher { /// Redirects to [withImageChunkEventsSink] @override @nonVirtual - FutureOr call(S source, Future abortSignal) => - withImageChunkEventsSink(source, abortSignal); + FutureOr call({ + required S source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + }) => + withImageChunkEventsSink( + source: source, + abortSignal: abortSignal, + transformer: transformer, + ); - /// Fetches a tile's bytes based on its 'source' ([S]) - /// - /// The [abortSignal] completes when the tile is no longer required. If - /// possible, any ongoing work (such as an HTTP request) should be aborted. - /// If aborting and a result is unavailable, [TileAbortedException] should be - /// thrown. + /// {@macro fm.tilelayer.tilebytesfetcher.call} /// /// [chunkEvents] should be used when consolidating a stream of bytes to /// report progress notifications to the underlying [ImageProvider]. - FutureOr withImageChunkEventsSink( - S source, - Future abortSignal, { + FutureOr withImageChunkEventsSink({ + required S source, + required Future abortSignal, + required BytesToResourceTransformer transformer, StreamSink? chunkEvents, }); } @@ -70,3 +79,29 @@ class TileAbortedException implements Exception { @override String toString() => 'TileAbortedException: $source'; } + +/// Callback provided to a [SourceBytesFetcher] by a root [TileSourceFetcher], +/// which converts fetched bytes into the desired [Resource] +/// +/// This may throw if the bytes could not be correctly transformed, for example +/// because they were corrupted or otherwise undecodable. In this case, it is +/// the bytes fetcher's responsibility to catch the error and act accordingly, +/// potentially by returning another (for example, a fallback) resource and/or +/// disabling the long-term caching of this tile. Therefore, it is recommended +/// to always await the result of the callback. +/// +/// The [SourceBytesFetcher] should also indicate whether it is acceptable for +/// other parts of the stack (such as the [TileSourceFetcher]) to reuse the +/// resource for tile in the short-term, avoiding having to re-fetch bytes. +/// Other parts of the stack may perform short-term caching (whilst it is the +/// bytes fetcher's responsibility to provide long-term caching) to improve +/// efficiency, for example when the same tile is re-requested for display in +/// the same session. For example, a raster image resource may be cached in +/// memory using the [ImageCache]. However, if this should not occur, because +/// the bytes create a resource different to what is desired (for example, a +/// fallback resource), then `allowReuse` should be set `false`. +typedef BytesToResourceTransformer + = FutureOr Function( + Uint8List bytes, { + bool allowReuse, +}); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart new file mode 100644 index 000000000..ea50a2f25 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart @@ -0,0 +1,39 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; + +/// A [SourceBytesFetcher] which fetches from the local filesystem, based on +/// their [TileSource] +@immutable +class FileBytesFetcher implements SourceBytesFetcher { + /// A [SourceBytesFetcher] which fetches from the local filesystem, based on + /// their [TileSource] + const FileBytesFetcher(); + + @override + Future call({ + required TileSource source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + bool useFallback = false, + }) async { + final resolvedUri = useFallback ? source.fallbackUri ?? '' : source.uri; + + try { + final bytes = await File(resolvedUri).readAsBytes(); + return await transformer(bytes); + } on Exception { + if (useFallback || source.fallbackUri == null) rethrow; + return this( + source: source, + abortSignal: abortSignal, + // In fallback scenarios, we never reuse bytes + transformer: (bytes, {allowReuse = true}) => + transformer(bytes, allowReuse: false), + useFallback: useFallback, + ); + } + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart index 527a9b7e4..f0b419779 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart @@ -1,16 +1,23 @@ +import 'dart:async'; + import 'package:flutter/foundation.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +/// A [SourceBytesFetcher] which fetches from the local filesystem, based on +/// their [TileSource] @immutable -class FileBytesFetcher implements TileBytesFetcher { +class FileBytesFetcher implements SourceBytesFetcher { + /// A [SourceBytesFetcher] which fetches from the local filesystem, based on + /// their [TileSource] const FileBytesFetcher(); @override - Future call( - TileSource source, - Future abortSignal, - ) { + Future call({ + required TileSource source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + }) { throw UnsupportedError( '`FileBytesFetcher` is unsupported on non-native platforms', ); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart deleted file mode 100644 index 5b4d3b930..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/tile_provider_io.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; - -@immutable -class FileBytesFetcher implements TileBytesFetcher { - const FileBytesFetcher(); - - @override - Future call( - TileSource source, - Future abortSignal, - ) async { - try { - return await File(source.uri).readAsBytes(); - } on FileSystemException { - if (source.fallbackUri == null) rethrow; - return await File(source.fallbackUri!).readAsBytes(); - } - } -} diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/built_in_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart similarity index 91% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/built_in_caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart index e29b6fbf8..9a333e439 100644 --- a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/built_in_caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart @@ -1,7 +1,8 @@ import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/stub.dart' - if (dart.library.io) 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/native.dart' - if (dart.library.js_interop) 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/web/web.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/stub.dart' + if (dart.library.io) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart' + if (dart.library.js_interop) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; import 'package:uuid/data.dart'; import 'package:uuid/rng.dart'; import 'package:uuid/uuid.dart'; @@ -17,7 +18,7 @@ import 'package:uuid/uuid.dart'; /// `overrideFreshAge` can override this. /// /// This is enabled by default in flutter_map, when using the -/// [NetworkTileProvider] (or cancellable version). +/// [NetworkBytesFetcher]. /// /// It is safe to use all public methods when running on web - they will noop. /// diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/README.md b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/README.md similarity index 100% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/README.md rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/README.md diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/native.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart similarity index 97% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/native.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart index f073aa3e0..7f008afc6 100644 --- a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/native.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart @@ -6,7 +6,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/size_reducer.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart similarity index 95% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/size_reducer.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart index b4f8e9391..0a3bffb50 100644 --- a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/size_reducer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/native.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart similarity index 96% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart index 7e2a67117..1881a8817 100644 --- a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart @@ -4,9 +4,9 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/native.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/native/workers/size_reducer.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_provider/network/caching/tile_metadata.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/stub.dart similarity index 100% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/stub.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/stub.dart diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/web/web.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart similarity index 100% rename from lib/src/layer/tile_layer/tile_provider/network/caching/built_in/impl/web/web.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart similarity index 89% rename from lib/src/layer/tile_layer/tile_provider/network/caching/caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart index 1335b2a88..bf5c2fcd3 100644 --- a/lib/src/layer/tile_layer/tile_provider/network/caching/caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart @@ -2,11 +2,7 @@ import 'dart:typed_data'; import 'package:flutter_map/flutter_map.dart'; -/// Provides tile caching facilities to [TileProvider]s -/// -/// Some caching plugins may choose instead to provide a dedicated -/// [TileProvider], in which case the flutter_map-provided caching facilities -/// are irrelevant. +/// Provides tile caching facilities /// /// The [CachedMapTileMetadata] object is used to store metadata alongside /// cached tiles. Its intended purpose is primarily for caching based on HTTP diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/disabled/disabled_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart similarity index 100% rename from lib/src/layer/tile_layer/tile_provider/network/caching/disabled/disabled_caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/tile_metadata.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart similarity index 100% rename from lib/src/layer/tile_layer/tile_provider/network/caching/tile_metadata.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart diff --git a/lib/src/layer/tile_layer/tile_provider/network/caching/tile_read_failure_exception.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart similarity index 100% rename from lib/src/layer/tile_layer/tile_provider/network/caching/tile_read_failure_exception.dart rename to lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart new file mode 100644 index 000000000..eacf725b4 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart @@ -0,0 +1,83 @@ +// Adapted from Flutter (c 2014 BSD The Flutter Authors) method to work without +// `dart:io` using a `StreamedResponse` + +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart'; +import 'package:meta/meta.dart'; + +/// Efficiently converts the response body of an [Response] into a +/// [Uint8List]. +/// +/// Assumes response has been uncompressed automatically. +/// +/// See [consolidateHttpClientResponseBytes] for more info. +@internal +Future consolidateStreamedResponseBytes( + StreamedResponse response, { + BytesReceivedCallback? onBytesReceived, +}) { + final completer = Completer.sync(); + final output = _OutputBuffer(); + + int? expectedContentLength = response.contentLength; + if (expectedContentLength == -1) expectedContentLength = null; + + int bytesReceived = 0; + late final StreamSubscription> subscription; + subscription = response.stream.listen( + (chunk) { + output.add(chunk); + if (onBytesReceived != null) { + bytesReceived += chunk.length; + try { + onBytesReceived(bytesReceived, expectedContentLength); + } catch (error, stackTrace) { + completer.completeError(error, stackTrace); + subscription.cancel(); + return; + } + } + }, + onDone: () { + output.close(); + completer.complete(output.bytes); + }, + onError: completer.completeError, + cancelOnError: true, + ); + + return completer.future; +} + +class _OutputBuffer extends ByteConversionSinkBase { + List>? _chunks = >[]; + int _contentLength = 0; + Uint8List? _bytes; + + @override + void add(List chunk) { + assert(_bytes == null, '`_bytes` must be `null`'); + _chunks!.add(chunk); + _contentLength += chunk.length; + } + + @override + void close() { + if (_bytes != null) { + // We've already been closed; this is a no-op + return; + } + _bytes = Uint8List(_contentLength); + int offset = 0; + for (final List chunk in _chunks!) { + _bytes!.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + _chunks = null; + } + + Uint8List get bytes => _bytes!; +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart new file mode 100644 index 000000000..50d6996eb --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart @@ -0,0 +1,317 @@ +import 'dart:async'; +import 'dart:io' show HttpHeaders, HttpDate, HttpStatus; // web safe! + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +import 'package:http/http.dart'; +import 'package:http/retry.dart'; + +/// A [SourceBytesFetcher] which fetches from the network using HTTP, based on +/// their [TileSource] +@immutable +class NetworkBytesFetcher + with ImageChunkEventsSupport + implements SourceBytesFetcher { + /// HTTP headers to send with each request + final Map headers; + + /// HTTP client used to make each request + /// + /// It is much more efficient if a single client is used repeatedly, as it + /// can maintain an open socket connection to the server. + /// + /// Where possible, clients should support aborting of requests when the + /// response is no longer required. + final Client httpClient; + + /// Provider used to perform long-term tile caching. + /// + /// See online documentation for more information about built-in caching. + /// + /// Defaults to [BuiltInMapCachingProvider]. Set to + /// [DisabledMapCachingProvider] to disable. + final MapCachingProvider? cachingProvider; + + /// Whether to optimistically attempt to decode HTTP responses that have a + /// non-successful status code as an image + /// + /// Defaults to `true`. + final bool attemptDecodeOfHttpErrorResponses; + + /// Whether to abort HTTP requests for tiles that will no longer be displayed. + /// + /// For example, tiles may be pruned from an intermediate zoom level during a + /// user's fast zoom. When disabled, the request for each tile that has been + /// pruned still needs to complete and be processed. When enabled, those + /// tiles' requests can be aborted before they are fully loaded. + /// + /// > [!TIP] + /// > This functionality replaces the 'flutter_map_cancellable_tile_provider' + /// > plugin package. + /// + /// This may have multiple advantages: + /// * It may improve tile loading speeds + /// * It may reduce the user's consumption of a metered network connection + /// * It may reduce the user's consumption of storage capacity in the + /// [cachingProvider] + /// * It may reduce unnecessary tile requests, reducing tile server costs + /// * It may negligibly improve app performance in general + /// + /// This is likely to be more effective on web platforms (where + /// `BrowserClient` is used) and with clients or servers with limited numbers + /// of simultaneous connections or slow traffic speeds, but is also likely to + /// have a positive effect everywhere. If an HTTP client is used which does + /// not support the standard method of request aborting, this has no effect. + /// + /// Defaults to `true`. It is recommended to enable this functionality, unless + /// you suspect it is causing problems; in this case, please report the issue + /// to flutter_map. + final bool abortObsoleteRequests; + + /// A tile bytes fetcher which fetches from the network using HTTP, based on + /// their [TileSource] + /// + /// The string "flutter_map ([uaIdentifier])" is set as the 'User-Agent' HTTP + /// header on non-web platforms, if the UA header is not specified manually. + /// If not provided, the string "flutter_map (unknown)" is used. + /// [uaIdentifier] should uniquely identify your app or project - for example, + /// 'com.example.app'. + /// + /// > [!TIP] + /// > Setting a [uaIdentifier] (or a custom UA header) is strongly recommended + /// > for all projects. It helps the server differentiate your traffic from + /// > other flutter_map traffic. + /// > + /// > A useful UA header is required by the terms of service of many tile + /// > servers. flutter_map places some restrictions on projects if a UA header + /// > is left unset. + NetworkBytesFetcher({ + String? uaIdentifier, + Map? headers, + Client? httpClient, + this.cachingProvider, + this.attemptDecodeOfHttpErrorResponses = true, + this.abortObsoleteRequests = true, + }) : headers = headers ?? {}, + httpClient = httpClient ?? RetryClient(Client()) { + if (!kIsWeb) { + this.headers.putIfAbsent( + HttpHeaders.userAgentHeader, + () => 'flutter_map ($uaIdentifier)', + ); + } + } + + @override + Future withImageChunkEventsSink({ + required TileSource source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + StreamSink? chunkEvents, + bool useFallback = false, + }) async { + // Resolve URIs + final resolvedUri = useFallback ? source.fallbackUri ?? '' : source.uri; + final parsedUri = Uri.parse(resolvedUri); + + // Create method to get bytes from server + Future<({Uint8List bytes, StreamedResponse response})> get({ + Map? additionalHeaders, + }) async { + final request = AbortableRequest( + 'GET', + parsedUri, + abortTrigger: abortObsoleteRequests ? abortSignal : null, + ); + + request.headers.addAll(headers); + if (additionalHeaders != null) request.headers.addAll(additionalHeaders); + + final response = await httpClient.send(request); + + final bytes = await consolidateStreamedResponseBytes( + response, + onBytesReceived: chunkEvents == null + ? null + : (cumulative, total) => chunkEvents.add( + ImageChunkEvent( + cumulativeBytesLoaded: cumulative, + expectedTotalBytes: total, + ), + ), + ); + + return (bytes: bytes, response: response); + } + + // Prepare caching provider & load cached tile if available + CachedMapTile? cachedTile; + final cachingProvider = + this.cachingProvider ?? BuiltInMapCachingProvider.getOrCreateInstance(); + if (cachingProvider.isSupported) { + try { + cachedTile = await cachingProvider.getTile(resolvedUri); + } on CachedMapTileReadFailure { + // This could occur due to a corrupt tile - we just try to overwrite it + // with fresh data + cachedTile = null; + } + } + + // Create method to write response to cache when applicable + void cachePut({ + required Uint8List? bytes, + required Map headers, + }) { + if (useFallback || !cachingProvider.isSupported) return; + cachingProvider.putTile( + url: resolvedUri, + metadata: CachedMapTileMetadata.fromHttpHeaders(headers), + bytes: bytes, + ); + } + + // Main logic + // All `transformer` calls should be awaited so errors may be handled + try { + bool forceFromServer = false; + if (cachedTile != null && !cachedTile.metadata.isStale) { + try { + // If we have a cached tile that's not stale, return it + return await transformer(cachedTile.bytes); + } on Exception { + // If the cached tile is corrupt, we proceed and get from the server + forceFromServer = true; + } + } + + // Otherwise, ask the server what's going on - supply any details we have + var (:bytes, :response) = await get( + additionalHeaders: forceFromServer + ? null + : { + if (cachedTile?.metadata.lastModified case final lastModified?) + HttpHeaders.ifModifiedSinceHeader: + HttpDate.format(lastModified), + if (cachedTile?.metadata.etag case final etag?) + HttpHeaders.ifNoneMatchHeader: etag, + }, + ); + + // Server says nothing's changed - but might return new useful headers + if (!forceFromServer && + cachedTile != null && + response.statusCode == HttpStatus.notModified) { + late final R transformedCacheBytes; + try { + transformedCacheBytes = await transformer(cachedTile.bytes); + } on Exception { + // If the cached tile is corrupt, we get fresh from the server without + // caching, then continue + forceFromServer = true; + (:bytes, :response) = await get(); + } + if (!forceFromServer) { + cachePut(bytes: null, headers: response.headers); + return transformedCacheBytes; + } + } + + // Server says the image has changed - store it new + if (response.statusCode == HttpStatus.ok) { + cachePut(bytes: bytes, headers: response.headers); + return await transformer(bytes); + } + + // It's likely an error at this point + // However, some servers may produce error responses with useful bodies, + // perhaps intentionally (such as an "API Key Required" message) + // Therefore, if there is a body, and the user allows it, we attempt to + // decode the body bytes as an image (although we don't cache if + // successful) + // Otherwise, we just throw early + if (!attemptDecodeOfHttpErrorResponses || bytes.isEmpty) { + throw NetworkImageLoadException( + statusCode: response.statusCode, + uri: parsedUri, + ); + } + + try { + return await transformer(bytes, allowReuse: false); + } catch (_, stackTrace) { + // If it throws, we don't want to throw the decode error, as that's not + // useful for users + // Instead, we throw an exception reporting the failed HTTP request, + // which is caught by the non-specific catch block below to initiate the + // retry/silence mechanisms if applicable + // We do retain the stack trace, so that it might be clear we attempted + // to decode it + // We piggyback off of an error meant for `NetworkImage` - it's the same + // as we need + Error.throwWithStackTrace( + NetworkImageLoadException( + statusCode: response.statusCode, + uri: parsedUri, + ), + stackTrace, + ); + } + } on RequestAbortedException catch (_, stackTrace) { + // This is a planned exception, we convert the error + + Error.throwWithStackTrace( + TileAbortedException(source: source), + stackTrace, + ); + } on ClientException catch (err, stackTrace) { + // This could be a wide range of issues, potentially ours, potentially + // network, etc. + + // Try to detect errors thrown from requests being aborted due to the + // client being closed + // This can occur when the map/tile layer is disposed early - in older + // versions, we used manual tracking to avoid disposing too early, but now + // we just attempt to catch (it's cleaner & easier) + if (err.message.contains('closed') || err.message.contains('cancel')) { + Error.throwWithStackTrace( + TileAbortedException(source: source), + stackTrace, + ); + } + + if (useFallback || source.fallbackUri == null) rethrow; + return withImageChunkEventsSink( + source: source, + abortSignal: abortSignal, + // In fallback scenarios, we never reuse bytes + transformer: (bytes, {allowReuse = true}) => + transformer(bytes, allowReuse: false), + chunkEvents: chunkEvents, + useFallback: true, + ); + } on Exception { + // Non-specific catch to catch decoding errors, the manually thrown HTTP + // exception, etc. + + if (useFallback || source.fallbackUri == null) rethrow; + return withImageChunkEventsSink( + source: source, + abortSignal: abortSignal, + // In fallback scenarios, we never reuse bytes + transformer: (bytes, {allowReuse = true}) => + transformer(bytes, allowReuse: false), + chunkEvents: chunkEvents, + useFallback: true, + ); + } + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart deleted file mode 100644 index e8b5b1f35..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/network.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:async'; -import 'dart:io' show HttpHeaders; // web safe! - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -import 'package:http/http.dart'; -import 'package:http/retry.dart'; - -/// A tile bytes fetcher which fetches from the network using HTTP, based on -/// their [TileSource] -@immutable -class NetworkBytesFetcher - with ImageChunkEventsSupport - implements TileBytesFetcher { - /// HTTP headers to send with each request - final Map headers; - - /// HTTP client used to make each request - /// - /// It is much more efficient if a single client is used repeatedly, as it - /// can maintain an open socket connection to the server. - /// - /// Where possible, clients should support aborting of requests when the - /// response is no longer required. - final Client httpClient; - - // TODO: Add caching provider integration - - /// A tile bytes fetcher which fetches from the network using HTTP, based on - /// their [TileSource] - /// - /// The string "flutter_map ([uaIdentifier])" is set as the 'User-Agent' HTTP - /// header on non-web platforms, if the UA header is not specified manually. - /// If not provided, the string "flutter_map (unknown)" is used. - /// [uaIdentifier] should uniquely identify your app or project - for example, - /// 'com.example.app'. - /// - /// > [!TIP] - /// > Setting a [uaIdentifier] (or a custom UA header) is strongly recommended - /// > for all projects. It helps the server differentiate your traffic from - /// > other flutter_map traffic. - /// > - /// > A useful UA header is required by the terms of service of many tile - /// > servers. flutter_map places some restrictions on projects if a UA header - /// > is left unset. - NetworkBytesFetcher({ - String? uaIdentifier, - Map? headers, - Client? httpClient, - }) : headers = headers ?? {}, - httpClient = httpClient ?? RetryClient(Client()) { - if (!kIsWeb) { - this.headers.putIfAbsent( - HttpHeaders.userAgentHeader, - () => 'flutter_map ($uaIdentifier)', - ); - } - } - - @override - FutureOr withImageChunkEventsSink( - TileSource source, - Future abortSignal, { - StreamSink? chunkEvents, - bool useFallback = false, - }) { - // TODO: Replace with #2082 - return httpClient - .readBytes( - Uri.parse(useFallback ? source.fallbackUri! : source.uri), - headers: headers, - ) - .onError((err, _) { - if (useFallback || source.fallbackUri == null) { - throw err; - } - return withImageChunkEventsSink( - source, - abortSignal, - chunkEvents: chunkEvents, - useFallback: true, - ); - }); - } - - @override - int get hashCode => Object.hash(headers, httpClient); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is NetworkBytesFetcher && - other.headers == headers && - other.httpClient == httpClient); -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart deleted file mode 100644 index a14415daf..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_passthrough.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; - -/// A tile source fetcher which delgates fetching of a raster image's bytes to -/// a [TileBytesFetcher], then passes it directly to the renderer (wrapping -/// it in a [WrapperTileData]) -/// -/// Users should consider whether it would be more efficient or better practise -/// to avoid this class and implement a more custom fetcher for their use-case. -class RawBytesTileFetcher - implements TileSourceFetcher> { - /// The delegate which provides the bytes for the this tile - final TileBytesFetcher bytesFetcher; - - /// A tile source fetcher which delgates fetching of a raster image's bytes to - /// a [TileBytesFetcher], then passes it directly to the renderer (wrapping - /// it in a [WrapperTileData]) - const RawBytesTileFetcher({required this.bytesFetcher}); - - @override - WrapperTileData call(S source) { - final abortTrigger = Completer.sync(); - - return WrapperTileData( - data: bytesFetcher(source, abortTrigger.future), - dispose: abortTrigger.complete, - ); - } -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart index 0588234ab..eb7a70b42 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart @@ -6,16 +6,16 @@ import 'package:flutter/widgets.dart'; /// Similar to [MemoryImage], but requires a [key] to identify and cache the /// image, and supports lazily getting the image bytes with chunk support -class KeyedGeneratedBytesImage extends ImageProvider { +class KeyedDelegatedImage extends ImageProvider { /// Similar to [MemoryImage], but requires a [key] to identify and cache the /// image, and supports lazily getting the image bytes with chunk support - const KeyedGeneratedBytesImage({ + const KeyedDelegatedImage({ required this.key, - required this.bytesGetter, + required this.delegate, this.scale = 1.0, }); - /// Identifier for this image + /// Identifier for this image. /// /// This is used (alongside [scale]) to identify this image in the image /// cache. Therefore, two requirements must be met: @@ -24,19 +24,24 @@ class KeyedGeneratedBytesImage extends ImageProvider { /// * The same image should always use the same key final Object key; - /// Callback which returns the bytes to decode into an image. + /// Callback which returns the codec to use as an image. /// /// Using the provided `chunkEvents` stream is optional, but may be used to /// report image loading progress. /// - /// The bytes represent encoded image bytes and can be encoded in any of the - /// following supported image formats: {@macro dart.ui.imageFormats} + /// The `decode` callback provides the logic to obtain the codec for the + /// image. It works on image bytes encoded in any of the following supported + /// image formats: + /// {@macro dart.ui.imageFormats} /// /// See also: /// /// * [PaintingBinding.instantiateImageCodecWithSize] - final FutureOr Function(StreamSink chunkEvents) - bytesGetter; + final Future Function( + KeyedDelegatedImage key, { + required StreamSink chunkEvents, + required ImageDecoderCallback decode, + }) delegate; /// The scale to place in the [ImageInfo] object of the image. /// @@ -47,42 +52,30 @@ class KeyedGeneratedBytesImage extends ImageProvider { final double scale; @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); } @override ImageStreamCompleter loadImage( - KeyedGeneratedBytesImage key, + KeyedDelegatedImage key, ImageDecoderCallback decode, ) { final chunkEvents = StreamController(); return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, chunkEvents: chunkEvents.sink, decode: decode) - ..then( - (_) => unawaited(chunkEvents.close()), - onError: (_) => unawaited(chunkEvents.close()), - ), + codec: delegate(key, chunkEvents: chunkEvents.sink, decode: decode) + ..whenComplete(chunkEvents.close), chunkEvents: chunkEvents.stream, scale: key.scale, - debugLabel: 'KeyedGeneratedBytesImage($key)', + debugLabel: 'KeyedDelegatedImage($key)', ); } - Future _loadAsync( - KeyedGeneratedBytesImage key, { - required StreamSink chunkEvents, - required ImageDecoderCallback decode, - }) async => - await decode( - await ImmutableBuffer.fromUint8List(await bytesGetter(chunkEvents)), - ); - @override bool operator ==(Object other) => identical(this, other) || - (other is KeyedGeneratedBytesImage && + (other is KeyedDelegatedImage && other.key == key && other.scale == scale); @@ -91,5 +84,5 @@ class KeyedGeneratedBytesImage extends ImageProvider { @override String toString() => - '${objectRuntimeType(this, 'KeyedGeneratedBytesImage')}(key: $key, scale: ${scale.toStringAsFixed(1)})'; + '${objectRuntimeType(this, 'KeyedDelegatedImage')}(key: $key, scale: ${scale.toStringAsFixed(1)})'; } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart index d70923027..09ce75d4a 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart @@ -1,13 +1,16 @@ import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; import 'package:flutter/widgets.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; /// A tile source fetcher which delgates fetching of a raster image's bytes to -/// a [TileBytesFetcher], then creates an [ImageProvider] by decoding the bytes +/// a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the bytes /// /// This is used instead of directly sending the bytes to the renderer, as it /// hooks into the Flutter image cache, meaning that tiles are cached in memory. @@ -18,10 +21,10 @@ class RasterTileFetcher /// /// This may not be called for every tile, if the tile was already present in /// the ambient [ImageCache]. - final TileBytesFetcher bytesFetcher; + final SourceBytesFetcher bytesFetcher; /// A tile source fetcher which delgates fetching of a raster image's bytes - /// to a [TileBytesFetcher], then creates an [ImageProvider] by decoding the + /// to a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the /// bytes const RasterTileFetcher({required this.bytesFetcher}); @@ -30,21 +33,44 @@ class RasterTileFetcher final abortTrigger = Completer.sync(); return RasterTileData( - image: KeyedGeneratedBytesImage( - // TODO: Include properties of bytes fetcher (hashcode of source + bytesFetcher)? - key: source, - bytesGetter: (chunkEvents) { - if (bytesFetcher case final ImageChunkEventsSupport bytesFetcher) { - return bytesFetcher.withImageChunkEventsSink( - source, - abortTrigger.future, - chunkEvents: chunkEvents, + image: KeyedDelegatedImage( + key: (source, bytesFetcher), + delegate: (key, {required chunkEvents, required decode}) async { + void evict() => scheduleMicrotask( + () => PaintingBinding.instance.imageCache.evict(key), + ); + + Future transformer(Uint8List bytes, {bool allowReuse = true}) { + if (!allowReuse) evict(); + return ImmutableBuffer.fromUint8List(bytes).then(decode); + } + + try { + // await to handle errors + if (bytesFetcher case final ImageChunkEventsSupport bytesFetcher) { + return await bytesFetcher.withImageChunkEventsSink( + source: source, + abortSignal: abortTrigger.future, + transformer: transformer, + chunkEvents: chunkEvents, + ); + } + return await bytesFetcher( + source: source, + abortSignal: abortTrigger.future, + transformer: transformer, ); + } on TileAbortedException { + evict(); + return ImmutableBuffer.fromUint8List(TileLoader.transparentImage) + .then(decode); + } catch (e) { + evict(); + rethrow; } - return bytesFetcher(source, abortTrigger.future); }, ), dispose: abortTrigger.complete, - ); + )..load(); } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart index 74ab55d76..3186effa2 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart @@ -74,18 +74,23 @@ class RasterTileData implements TileData { void _onImageLoadSuccess(ImageInfo imageInfo, bool synchronousCall) { if (_isDisposed) return; + final isPreviouslyLoaded = loaded != null; + loaded = ( time: DateTime.now(), successfulImageInfo: imageInfo, failureInfo: null ); _loadedTracker.complete(); - _display(); + + _display(isPreviouslyLoaded); } void _onImageLoadError(Object exception, StackTrace? stackTrace) { if (_isDisposed) return; + final isPreviouslyLoaded = loaded != null; + loaded = ( time: DateTime.now(), successfulImageInfo: null, @@ -94,6 +99,41 @@ class RasterTileData implements TileData { _loadedTracker.completeError(exception, stackTrace); // TODO: Was `if (errorImage != null) _display();`? - _display(); + _display(isPreviouslyLoaded); + } + + void _display(bool isPreviouslyLoaded) { + /*if (loadError) { + assert( + errorImage != null, + 'A TileImage should not be displayed if loading errors and there is no ' + 'errorImage to show.', + ); + _readyToDisplay = true; + if (!_disposed) notifyListeners(); + return; + }*/ + + /*_tileDisplay.when( + instantaneous: (_) { + _readyToDisplay = true; + if (!_disposed) notifyListeners(); + }, + fadeIn: (fadeIn) { + final fadeStartOpacity = + previouslyLoaded ? fadeIn.reloadStartOpacity : fadeIn.startOpacity; + + if (fadeStartOpacity == 1.0) { + _readyToDisplay = true; + if (!_disposed) notifyListeners(); + } else { + _animationController!.reset(); + _animationController!.forward(from: fadeStartOpacity).then((_) { + _readyToDisplay = true; + if (!_disposed) notifyListeners(); + }); + } + }, + );*/ } } From f65d3e34b4d4552f1724f76e8f7ed6a223de026a Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 11 Jul 2025 15:30:31 +0100 Subject: [PATCH 06/13] Integrate fix from #2125 --- .../network/fetcher/network.dart | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart index 50d6996eb..253bf9e7d 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart @@ -13,6 +13,7 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetch import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:http/http.dart'; import 'package:http/retry.dart'; +import 'package:logger/logger.dart'; /// A [SourceBytesFetcher] which fetches from the network using HTTP, based on /// their [TileSource] @@ -172,9 +173,28 @@ class NetworkBytesFetcher required Map headers, }) { if (useFallback || !cachingProvider.isSupported) return; + + // TODO: Consider best way to silence these 2 logs + late final CachedMapTileMetadata metadata; + try { + metadata = CachedMapTileMetadata.fromHttpHeaders( + headers, + warnOnFallbackUsage: parsedUri, + ); + } on Exception catch (e) { + if (kDebugMode) { + Logger(printer: SimplePrinter()).w( + '[flutter_map cache] Failed to cache ${parsedUri.path}: $e\n\tThis ' + 'may indicate a HTTP spec non-conformance issue with the tile ' + 'server. ', + ); + } + return; + } + cachingProvider.putTile( url: resolvedUri, - metadata: CachedMapTileMetadata.fromHttpHeaders(headers), + metadata: metadata, bytes: bytes, ); } From 96ed2ca5344e2dd3a935d2ee44ac1419ad40ab2c Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 13 Jul 2025 10:00:48 +0100 Subject: [PATCH 07/13] Start allowing multiple fallbacks --- ...raster_tile_layer.dart => tile_layer.dart} | 9 +- .../network/fetcher/network.dart | 94 +++++++++++-------- .../raster/raster_tile_fetcher.dart | 19 +++- .../tile_loader/source_generators/wms.dart | 11 +-- .../tile_loader/source_generators/xyz.dart | 70 +++++++------- .../{loader.dart => tile_loader.dart} | 0 .../tile_loader/tile_source.dart | 25 ----- 7 files changed, 114 insertions(+), 114 deletions(-) rename lib/src/layer/modern_tile_layer/tile_layers/raster/{tile_layer.dart/raster_tile_layer.dart => tile_layer.dart} (93%) rename lib/src/layer/modern_tile_layer/tile_loader/{loader.dart => tile_loader.dart} (100%) delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart similarity index 93% rename from lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart rename to lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart index 3b03b49f0..ec9791ea0 100644 --- a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart/raster_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart @@ -1,14 +1,13 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; class RasterTileLayer extends StatefulWidget { @@ -24,12 +23,12 @@ class RasterTileLayer extends StatefulWidget { this.options = const TileLayerOptions(), required String urlTemplate, required String uaIdentifier, - }) : sourceGenerator = XYZGenerator(uriTemplate: urlTemplate), + }) : sourceGenerator = XYZGenerator(uriTemplates: [urlTemplate]), bytesFetcher = NetworkBytesFetcher(uaIdentifier: uaIdentifier); final TileLayerOptions options; - final TileSourceGenerator sourceGenerator; - final SourceBytesFetcher bytesFetcher; + final TileSourceGenerator> sourceGenerator; + final SourceBytesFetcher> bytesFetcher; @override State createState() => _RasterTileLayerState(); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart index 253bf9e7d..c7e43921d 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart @@ -10,7 +10,6 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetch import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:http/http.dart'; import 'package:http/retry.dart'; import 'package:logger/logger.dart'; @@ -19,8 +18,8 @@ import 'package:logger/logger.dart'; /// their [TileSource] @immutable class NetworkBytesFetcher - with ImageChunkEventsSupport - implements SourceBytesFetcher { + with ImageChunkEventsSupport> + implements SourceBytesFetcher> { /// HTTP headers to send with each request final Map headers; @@ -113,15 +112,52 @@ class NetworkBytesFetcher @override Future withImageChunkEventsSink({ - required TileSource source, + required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, StreamSink? chunkEvents, - bool useFallback = false, }) async { - // Resolve URIs - final resolvedUri = useFallback ? source.fallbackUri ?? '' : source.uri; - final parsedUri = Uri.parse(resolvedUri); + final iterator = source.iterator; + + if (!iterator.moveNext()) { + throw ArgumentError('At least one URI must be provided', 'source'); + } + + for (bool isPrimary = true;; isPrimary = false) { + try { + return await _fetch( + uri: iterator.current, + abortSignal: abortSignal, + transformer: isPrimary + ? transformer + : (bytes, {allowReuse = true}) => + // In fallback scenarios, we never allow reuse of bytes in the + // short-term cache (or long-term cache) + transformer(bytes, allowReuse: false), + chunkEvents: chunkEvents, + performLongTermCaching: !isPrimary, + ); + } on TileAbortedException { + rethrow; // Never try fallbacks on abortion + } on Exception { + if (iterator.moveNext()) { + // Attempt fallbacks + // TODO: Consider logging + continue; + } + rethrow; // No more fallbacks available + } + } + } + + Future _fetch({ + required String uri, + required Future abortSignal, + required BytesToResourceTransformer transformer, + required StreamSink? chunkEvents, + required bool performLongTermCaching, + }) async { + final parsedUri = Uri.parse(uri); // Create method to get bytes from server Future<({Uint8List bytes, StreamedResponse response})> get({ @@ -159,7 +195,7 @@ class NetworkBytesFetcher this.cachingProvider ?? BuiltInMapCachingProvider.getOrCreateInstance(); if (cachingProvider.isSupported) { try { - cachedTile = await cachingProvider.getTile(resolvedUri); + cachedTile = await cachingProvider.getTile(uri); } on CachedMapTileReadFailure { // This could occur due to a corrupt tile - we just try to overwrite it // with fresh data @@ -172,7 +208,7 @@ class NetworkBytesFetcher required Uint8List? bytes, required Map headers, }) { - if (useFallback || !cachingProvider.isSupported) return; + if (performLongTermCaching || !cachingProvider.isSupported) return; // TODO: Consider best way to silence these 2 logs late final CachedMapTileMetadata metadata; @@ -192,11 +228,7 @@ class NetworkBytesFetcher return; } - cachingProvider.putTile( - url: resolvedUri, - metadata: metadata, - bytes: bytes, - ); + cachingProvider.putTile(url: uri, metadata: metadata, bytes: bytes); } // Main logic @@ -289,7 +321,7 @@ class NetworkBytesFetcher // This is a planned exception, we convert the error Error.throwWithStackTrace( - TileAbortedException(source: source), + TileAbortedException(source: parsedUri), stackTrace, ); } on ClientException catch (err, stackTrace) { @@ -303,35 +335,15 @@ class NetworkBytesFetcher // we just attempt to catch (it's cleaner & easier) if (err.message.contains('closed') || err.message.contains('cancel')) { Error.throwWithStackTrace( - TileAbortedException(source: source), + TileAbortedException(source: parsedUri), stackTrace, ); } - if (useFallback || source.fallbackUri == null) rethrow; - return withImageChunkEventsSink( - source: source, - abortSignal: abortSignal, - // In fallback scenarios, we never reuse bytes - transformer: (bytes, {allowReuse = true}) => - transformer(bytes, allowReuse: false), - chunkEvents: chunkEvents, - useFallback: true, - ); - } on Exception { - // Non-specific catch to catch decoding errors, the manually thrown HTTP - // exception, etc. - - if (useFallback || source.fallbackUri == null) rethrow; - return withImageChunkEventsSink( - source: source, - abortSignal: abortSignal, - // In fallback scenarios, we never reuse bytes - transformer: (bytes, {allowReuse = true}) => - transformer(bytes, allowReuse: false), - chunkEvents: chunkEvents, - useFallback: true, - ); + rethrow; // Otherwise, attempt fallbacks } + // We may also get exceptions otherwise, for example from failing to + // transform/decode bytes or `NetworkImageLoadException` - we pass these + // through to the caller to allow attempting of fallbacks implicitly } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart index 09ce75d4a..8071ec457 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart @@ -2,8 +2,9 @@ import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; @@ -32,9 +33,23 @@ class RasterTileFetcher RasterTileData call(S source) { final abortTrigger = Completer.sync(); + // PaintingBinding.instance.imageCache.containsKey(key) + return RasterTileData( image: KeyedDelegatedImage( - key: (source, bytesFetcher), + key: ( + source, // TODO: This cannot be used. Maybe .first somehow if we only + // want to cache primary target (or evict otherwise). Otherwise more + // complex system required. + bytesFetcher + ), + // TODO: Ideal if we can return a key from the delegate - but need to + // ensure that key can be looked up in cache without async or waiting + // for bytes. But then need to make a decision about traversing source + // with `imageCache.containsKey` vs getting bytes. And, we check cache + // for primary -> not present, try to fetch primary -> fail, might as + // well check cache for secondary - but that requires looping outside + // this constructor! delegate: (key, {required chunkEvents, required decode}) async { void evict() => scheduleMicrotask( () => PaintingBinding.instance.imageCache.evict(key), diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart index 9ebdc2b78..b2fc873a7 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart @@ -2,7 +2,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:meta/meta.dart'; @@ -10,7 +9,7 @@ import 'package:meta/meta.dart'; /// A tile source generator which generates tiles for the /// [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) referencing system @immutable -class WMSGenerator implements TileSourceGenerator { +class WMSGenerator implements TileSourceGenerator> { /// WMS service's URL, for example 'http://ows.mundialis.de/services/service?' final String baseUrl; @@ -85,7 +84,7 @@ class WMSGenerator implements TileSourceGenerator { } @override - TileSource call(TileCoordinates coordinates, TileLayerOptions options) { + List call(TileCoordinates coordinates, TileLayerOptions options) { final nwPoint = Offset( (coordinates.x * options.tileDimension).toDouble(), (coordinates.y * options.tileDimension).toDouble(), @@ -104,12 +103,12 @@ class WMSGenerator implements TileSourceGenerator { ? [bounds.min.dy, bounds.min.dx, bounds.max.dy, bounds.max.dx] : [bounds.min.dx, bounds.min.dy, bounds.max.dx, bounds.max.dy]; - return TileSource( - uri: (StringBuffer(_encodedBaseUrl) + return [ + (StringBuffer(_encodedBaseUrl) ..write('&width=${options.tileDimension * dimensionsMultiplier}') ..write('&height=${options.tileDimension * dimensionsMultiplier}') ..write('&bbox=${bbox.join(',')}')) .toString(), - ); + ]; } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart index 2c28d6d2a..e4c675649 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart @@ -1,6 +1,8 @@ import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; @@ -16,29 +18,34 @@ import 'package:meta/meta.dart'; /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) standard by flipping /// the Y axis. @immutable -class XYZGenerator implements TileSourceGenerator { - /// Template string for tile resources (containing placeholders) +class XYZGenerator implements TileSourceGenerator> { + /// List of endpoints for tile resources, in XYZ template format + /// + /// Endpoints are used by the [TileSourceFetcher] in use, and so their meaning + /// is context dependent. For example, a HTTP URL would likely be used with + /// the [NetworkBytesFetcher], whilst a file URI would be used with the + /// [FileBytesFetcher]. + /// + /// In all 3 default [SourceBytesFetcher]s, the first endpoint is used for + /// requests, unless it fails, in which case following endpoints are used as + /// fallbacks. + /// + /// > [!WARNING] + /// > Using fallbacks may incur a (potentially significant) performance + /// > penalty, and may not be understood by all [TileSourceFetcher]s. + /// > Note that failing each endpoint may take some time (such as a HTTP + /// > timeout elapsing). /// /// The following placeholders are supported, in addition to any described in /// [additionalPlaceholders] : /// - /// * `{z}`, `{x}`, `{z}`: tile coordinates + /// * `{z}`, `{x}`, `{y}`: tile coordinates /// * `{s}`: subdomain chosen from [subdomains] /// * `{r}`: retina mode (filled with "@2x" when enabled) /// * `{d}`: current [TileLayerOptions.tileDimension] - final String uriTemplate; + final List uriTemplates; - /// Template string for tile resources used by some [TileSourceFetcher]s if - /// the request/response to/from the primary [uriTemplate] fails - /// - /// > [!WARNING] - /// > Not all fetchers support falling-back. Note that failing the primary - /// > template may take some time (such as a HTTP timeout elapsing). - /// > Additionally, using fallbacks may have negative performance and tile - /// > usage consequences. See online documentation for more information. - final String? fallbackUriTemplate; - - /// List of subdomains for the [uriTemplate] (to replace the `{s}` + /// List of subdomains for the [uriTemplates] (to replace the `{s}` /// placeholder) /// /// > [!NOTE] @@ -47,7 +54,7 @@ class XYZGenerator implements TileSourceGenerator { final List subdomains; /// Static information that should replace associated placeholders in the - /// [uriTemplate] + /// [uriTemplates] /// /// For example, this could be used to more easily apply API keys to /// templates. @@ -61,15 +68,17 @@ class XYZGenerator implements TileSourceGenerator { /// A tile source generator which generates tiles for slippy map tile servers /// following the standard XYZ tile referencing system const XYZGenerator({ - required this.uriTemplate, - this.fallbackUriTemplate, + required this.uriTemplates, this.subdomains = const [], this.additionalPlaceholders = const {}, this.tms = false, }); @override - TileSource call(TileCoordinates coordinates, TileLayerOptions options) { + Iterable call( + TileCoordinates coordinates, + TileLayerOptions options, + ) { final replacementMap = generateReplacementMap(coordinates, options); String replacer(Match match) { @@ -78,19 +87,12 @@ class XYZGenerator implements TileSourceGenerator { throw ArgumentError('Missing value for placeholder: {${match.group(1)}}'); } - final uri = uriTemplate.replaceAllMapped( - templatePlaceholderElement, - replacer, - ); - final fallbackUri = fallbackUriTemplate?.replaceAllMapped( - templatePlaceholderElement, - replacer, - ); - - return TileSource(uri: uri, fallbackUri: fallbackUri); + // Lazily generate URIs as required + return uriTemplates + .map((t) => t.replaceAllMapped(templatePlaceholderElement, replacer)); } - /// Generates the mapping of [uriTemplate] placeholders to replacements + /// Generates the mapping of [uriTemplates] placeholders to replacements @visibleForOverriding Map generateReplacementMap( TileCoordinates coordinates, @@ -130,8 +132,7 @@ class XYZGenerator implements TileSourceGenerator { @override int get hashCode => Object.hash( - uriTemplate, - fallbackUriTemplate, + uriTemplates, subdomains, additionalPlaceholders, tms, @@ -141,8 +142,7 @@ class XYZGenerator implements TileSourceGenerator { bool operator ==(Object other) => identical(this, other) || (other is XYZGenerator && - other.uriTemplate == uriTemplate && - other.fallbackUriTemplate == fallbackUriTemplate && + other.uriTemplates == uriTemplates && other.subdomains == subdomains && other.additionalPlaceholders == additionalPlaceholders && other.tms == tms); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/loader.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart deleted file mode 100644 index 13a0b7b93..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/loader.dart'; -import 'package:meta/meta.dart'; - -/// Default tile 'source' implementation for the default [TileLoader] -/// implementation -@immutable -class TileSource { - final String uri; - final String? fallbackUri; - - const TileSource({required this.uri, this.fallbackUri}); - - //! It is very important that these remain correct - they uniquely identify - //! a resulting image in the raster fetcher. - - @override - int get hashCode => Object.hash(uri, fallbackUri); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is TileSource && - other.uri == uri && - other.fallbackUri == fallbackUri); -} From 3f995e7e41030048685497bf4dbcdace82190692 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 20 Jul 2025 14:57:57 +0100 Subject: [PATCH 08/13] Re-introduce `TileSource` as an extension of `Iterable` Finish support for multi-fallback --- .../bytes_fetchers/asset/asset.dart | 56 ++++---- .../bytes_fetchers/bytes_fetcher.dart | 21 ++- .../bytes_fetchers/file/file_io.dart | 49 ++++--- .../bytes_fetchers/file/file_stub.dart | 13 +- .../network/fetcher/network.dart | 29 ++-- .../raster/image_provider.dart | 4 +- .../raster/raster_tile_fetcher.dart | 128 ++++++++++-------- .../tile_loader/source_generator_fetcher.dart | 8 +- .../tile_loader/source_generators/wms.dart | 15 +- .../tile_loader/source_generators/xyz.dart | 36 ++--- .../tile_loader/tile_loader.dart | 8 +- .../tile_loader/tile_source.dart | 74 ++++++++++ 12 files changed, 274 insertions(+), 167 deletions(-) create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart index ee546ad22..add2655e8 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart @@ -3,29 +3,27 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -/// A [SourceBytesFetcher] which fetches from the app's shipped assets, based on -/// their [TileSource] +/// A [SourceBytesFetcher] which fetches from the app's shipped assets. +/// +/// {@macro fm.sbf.default.sourceConsumption} /// /// In normal usage, all tiles (or at least each individual lowest-level /// directory) must be listed as normal in the pubspec. // TODO: This a considerably different implementation - check performance @immutable -class AssetBytesFetcher implements SourceBytesFetcher { - /// Asset bundle to retrieve tiles from +class AssetBytesFetcher implements SourceBytesFetcher> { + /// Asset bundle to retrieve tiles from. final AssetBundle? assetBundle; - /// A [SourceBytesFetcher] which fetches from the app's shipped assets, based - /// on their [TileSource] + /// A [SourceBytesFetcher] which fetches from the app's shipped assets. /// /// By default, this uses the default [rootBundle]. If a different bundle is /// required, either specify it manually, or use the /// [AssetBytesFetcher.fromContext] constructor. const AssetBytesFetcher({this.assetBundle}); - /// A [SourceBytesFetcher] which fetches from the app's shipped assets, based - /// on their [TileSource] + /// A [SourceBytesFetcher] which fetches from the app's shipped assets. /// /// Gets the asset bundle from the [DefaultAssetBundle] depending on the /// provided context. @@ -34,27 +32,33 @@ class AssetBytesFetcher implements SourceBytesFetcher { @override Future call({ - required TileSource source, + required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, - bool useFallback = false, }) async { final bundle = assetBundle ?? rootBundle; - final resolvedUri = useFallback ? source.fallbackUri ?? '' : source.uri; - - try { - final bytes = await bundle.load(resolvedUri); - return await transformer(Uint8List.sublistView(bytes)); - } on Exception { - if (useFallback || source.fallbackUri == null) rethrow; - return this( - source: source, - abortSignal: abortSignal, - // In fallback scenarios, we never reuse bytes - transformer: (bytes, {allowReuse = true}) => - transformer(bytes, allowReuse: false), - useFallback: useFallback, - ); + + final iterator = source.iterator; + + if (!iterator.moveNext()) { + throw ArgumentError('At least one URI must be provided', 'source'); + } + + for (bool isPrimary = true;; isPrimary = false) { + try { + return await transformer( + Uint8List.sublistView(await bundle.load(iterator.current)), + // In fallback scenarios, we never allow reuse of bytes in the + // short-term cache (or long-term cache) + allowReuse: isPrimary, + ); + } on Exception { + if (!iterator.moveNext()) rethrow; // No (more) fallbacks available + + // Attempt fallbacks + // TODO: Consider logging + continue; + } } } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart index 5bc4769ae..3feb4c487 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart @@ -7,7 +7,7 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_gener import 'package:meta/meta.dart'; /// Fetches a tile's bytes based on its source ([S]), transforming it into a -/// desired resource using a supplied [BytesToResourceTransformer] +/// desired resource using a supplied [BytesToResourceTransformer]. /// /// Implementers should implement longer-term caching where necessary, or /// delegate to a cacher. Note that [TileSourceFetcher]s may also perform @@ -19,7 +19,7 @@ import 'package:meta/meta.dart'; abstract interface class SourceBytesFetcher { /// {@template fm.tilelayer.tilebytesfetcher.call} /// Fetches a tile's bytes based on its source ([S]), transforming it into a - /// desired resource ([R]) using a supplied transformer + /// desired resource ([R]) using a supplied transformer. /// /// The [abortSignal] completes when the tile is no longer required. If /// possible, any ongoing work (such as an HTTP request) should be aborted. @@ -37,10 +37,10 @@ abstract interface class SourceBytesFetcher { } /// Allows a [SourceBytesFetcher] to integrate more closely with the raster tile -/// stack by reporting progress events to the underlying [ImageProvider] +/// stack by reporting progress events to the underlying [ImageProvider]. abstract mixin class ImageChunkEventsSupport implements SourceBytesFetcher { - /// Redirects to [withImageChunkEventsSink] + /// Redirects to [withImageChunkEventsSink]. @override @nonVirtual FutureOr call({ @@ -67,13 +67,13 @@ abstract mixin class ImageChunkEventsSupport } /// Exception thrown when a tile was loading but aborted early as it was no -/// longer required +/// longer required. class TileAbortedException implements Exception { - /// Optional description of the tile + /// Optional description of the tile. final Object? source; /// Exception thrown when a tile was loading but aborted early as it was no - /// longer required + /// longer required. const TileAbortedException({this.source}); @override @@ -81,7 +81,7 @@ class TileAbortedException implements Exception { } /// Callback provided to a [SourceBytesFetcher] by a root [TileSourceFetcher], -/// which converts fetched bytes into the desired [Resource] +/// which converts fetched bytes into the desired [Resource]. /// /// This may throw if the bytes could not be correctly transformed, for example /// because they were corrupted or otherwise undecodable. In this case, it is @@ -101,7 +101,4 @@ class TileAbortedException implements Exception { /// the bytes create a resource different to what is desired (for example, a /// fallback resource), then `allowReuse` should be set `false`. typedef BytesToResourceTransformer - = FutureOr Function( - Uint8List bytes, { - bool allowReuse, -}); + = FutureOr Function(Uint8List bytes, {bool allowReuse}); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart index ea50a2f25..1f9569916 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart @@ -2,38 +2,43 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -/// A [SourceBytesFetcher] which fetches from the local filesystem, based on -/// their [TileSource] +/// A [SourceBytesFetcher] which fetches from the local filesystem. +/// +/// {@macro fm.sbf.default.sourceConsumption} @immutable -class FileBytesFetcher implements SourceBytesFetcher { - /// A [SourceBytesFetcher] which fetches from the local filesystem, based on - /// their [TileSource] +class FileBytesFetcher implements SourceBytesFetcher> { + /// A [SourceBytesFetcher] which fetches from the local filesystem. const FileBytesFetcher(); @override Future call({ - required TileSource source, + required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, - bool useFallback = false, }) async { - final resolvedUri = useFallback ? source.fallbackUri ?? '' : source.uri; + final iterator = source.iterator; - try { - final bytes = await File(resolvedUri).readAsBytes(); - return await transformer(bytes); - } on Exception { - if (useFallback || source.fallbackUri == null) rethrow; - return this( - source: source, - abortSignal: abortSignal, - // In fallback scenarios, we never reuse bytes - transformer: (bytes, {allowReuse = true}) => - transformer(bytes, allowReuse: false), - useFallback: useFallback, - ); + if (!iterator.moveNext()) { + throw ArgumentError('At least one URI must be provided', 'source'); + } + + for (bool isPrimary = true;; isPrimary = false) { + // TODO: Consider abortable streaming of bytes + try { + return await transformer( + await File(iterator.current).readAsBytes(), + // In fallback scenarios, we never allow reuse of bytes in the + // short-term cache (or long-term cache) + allowReuse: isPrimary, + ); + } on Exception { + if (!iterator.moveNext()) rethrow; // No (more) fallbacks available + + // Attempt fallbacks + // TODO: Consider logging + continue; + } } } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart index f0b419779..10e80751e 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart @@ -2,19 +2,18 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -/// A [SourceBytesFetcher] which fetches from the local filesystem, based on -/// their [TileSource] +/// A [SourceBytesFetcher] which fetches from the local filesystem. +/// +/// {@macro fm.sbf.default.sourceConsumption} @immutable -class FileBytesFetcher implements SourceBytesFetcher { - /// A [SourceBytesFetcher] which fetches from the local filesystem, based on - /// their [TileSource] +class FileBytesFetcher implements SourceBytesFetcher> { + /// A [SourceBytesFetcher] which fetches from the local filesystem. const FileBytesFetcher(); @override Future call({ - required TileSource source, + required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, }) { diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart index c7e43921d..6232c3ab0 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart @@ -14,16 +14,21 @@ import 'package:http/http.dart'; import 'package:http/retry.dart'; import 'package:logger/logger.dart'; -/// A [SourceBytesFetcher] which fetches from the network using HTTP, based on -/// their [TileSource] +/// A [SourceBytesFetcher] which fetches from the network using HTTP. +/// +/// {@template fm.sbf.default.sourceConsumption} +/// Consumes an [Iterable] of [String] URIs, which must not be empty and +/// iterates in an order. If the first URI cannot be used to fetch bytes, the +/// next URI is used as a fallback if available, and so on. +/// {@endtemplate} @immutable class NetworkBytesFetcher with ImageChunkEventsSupport> implements SourceBytesFetcher> { - /// HTTP headers to send with each request + /// HTTP headers to send with each request. final Map headers; - /// HTTP client used to make each request + /// HTTP client used to make each request. /// /// It is much more efficient if a single client is used repeatedly, as it /// can maintain an open socket connection to the server. @@ -41,7 +46,7 @@ class NetworkBytesFetcher final MapCachingProvider? cachingProvider; /// Whether to optimistically attempt to decode HTTP responses that have a - /// non-successful status code as an image + /// non-successful status code as an image. /// /// Defaults to `true`. final bool attemptDecodeOfHttpErrorResponses; @@ -76,8 +81,7 @@ class NetworkBytesFetcher /// to flutter_map. final bool abortObsoleteRequests; - /// A tile bytes fetcher which fetches from the network using HTTP, based on - /// their [TileSource] + /// A [SourceBytesFetcher] which fetches from the network using HTTP. /// /// The string "flutter_map ([uaIdentifier])" is set as the 'User-Agent' HTTP /// header on non-web platforms, if the UA header is not specified manually. @@ -140,12 +144,11 @@ class NetworkBytesFetcher } on TileAbortedException { rethrow; // Never try fallbacks on abortion } on Exception { - if (iterator.moveNext()) { - // Attempt fallbacks - // TODO: Consider logging - continue; - } - rethrow; // No more fallbacks available + if (!iterator.moveNext()) rethrow; // No (more) fallbacks available + + // Attempt fallbacks + // TODO: Consider logging + continue; } } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart index eb7a70b42..6687f65d9 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart @@ -5,10 +5,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; /// Similar to [MemoryImage], but requires a [key] to identify and cache the -/// image, and supports lazily getting the image bytes with chunk support +/// image, and supports lazily getting the image bytes with chunk support. class KeyedDelegatedImage extends ImageProvider { /// Similar to [MemoryImage], but requires a [key] to identify and cache the - /// image, and supports lazily getting the image bytes with chunk support + /// image, and supports lazily getting the image bytes with chunk support. const KeyedDelegatedImage({ required this.key, required this.delegate, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart index 8071ec457..11ca808a9 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart @@ -1,24 +1,49 @@ import 'dart:async'; -import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; /// A tile source fetcher which delgates fetching of a raster image's bytes to -/// a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the bytes +/// a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the +/// bytes. +/// +/// The source ([S]) is used as the short-term caching key for the +/// [ImageProvider] (and Flutter's [ImageCache]) - therefore, it must meet the +/// necessary conditions as described by [ImageProvider.obtainKey] +/// (particularly, it must be an object with a useful equality defined). /// /// This is used instead of directly sending the bytes to the renderer, as it /// hooks into the Flutter image cache, meaning that tiles are cached in memory. /// Additionally, it is easier for the renderer canvas to work with. +/// +/// --- +/// +/// The pre-provided [SourceBytesFetcher]s (such as [NetworkBytesFetcher]) +/// consume [Iterable]s of [String]s as a source ([S]), and use the transformer +/// provided by this object to output a [RasterTileData]. This is so that they +/// may be used outside of the context of [TileSource] & [RasterTileFetcher] +/// (for example, in a different stack). +/// +/// It is not suitable to use an [Iterable] directly, as it does not meet the +/// criteria for a key. Instead, the pre-provided [TileSourceGenerator]s (such +/// as [XYZGenerator]) output a [TileSource], which meets all necessary +/// requirements. +/// +/// However, this fetcher could be used with any source. For example, if a +/// different [SourceBytesFetcher] is used, it doesn't necessarily need to use +/// [TileSource] or any other contract described above. class RasterTileFetcher implements TileSourceFetcher { - /// The delegate which provides the bytes for the this tile + /// The delegate which provides the bytes for the this tile. /// /// This may not be called for every tile, if the tile was already present in /// the ambient [ImageCache]. @@ -26,64 +51,59 @@ class RasterTileFetcher /// A tile source fetcher which delgates fetching of a raster image's bytes /// to a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the - /// bytes + /// bytes. const RasterTileFetcher({required this.bytesFetcher}); @override RasterTileData call(S source) { - final abortTrigger = Completer.sync(); + final abortTrigger = Completer(); - // PaintingBinding.instance.imageCache.containsKey(key) + Future imageDelegate( + KeyedDelegatedImage key, { + required StreamSink chunkEvents, + required ImageDecoderCallback decode, + }) async { + void evict() => scheduleMicrotask( + () => PaintingBinding.instance.imageCache.evict(key), + ); - return RasterTileData( - image: KeyedDelegatedImage( - key: ( - source, // TODO: This cannot be used. Maybe .first somehow if we only - // want to cache primary target (or evict otherwise). Otherwise more - // complex system required. - bytesFetcher - ), - // TODO: Ideal if we can return a key from the delegate - but need to - // ensure that key can be looked up in cache without async or waiting - // for bytes. But then need to make a decision about traversing source - // with `imageCache.containsKey` vs getting bytes. And, we check cache - // for primary -> not present, try to fetch primary -> fail, might as - // well check cache for secondary - but that requires looping outside - // this constructor! - delegate: (key, {required chunkEvents, required decode}) async { - void evict() => scheduleMicrotask( - () => PaintingBinding.instance.imageCache.evict(key), - ); + Future transformer(Uint8List bytes, {bool allowReuse = true}) { + if (!allowReuse) evict(); + return ImmutableBuffer.fromUint8List(bytes).then(decode); + } - Future transformer(Uint8List bytes, {bool allowReuse = true}) { - if (!allowReuse) evict(); - return ImmutableBuffer.fromUint8List(bytes).then(decode); - } + try { + // Must await to handle errors + if (bytesFetcher + case ImageChunkEventsSupport( + withImageChunkEventsSink: final bytesFetcher + )) { + return await bytesFetcher( + source: source, + abortSignal: abortTrigger.future, + transformer: transformer, + chunkEvents: chunkEvents, + ); + } + return await bytesFetcher( + source: source, + abortSignal: abortTrigger.future, + transformer: transformer, + ); + } on TileAbortedException { + evict(); + return ImmutableBuffer.fromUint8List(TileLoader.transparentImage) + .then(decode); + } on Exception { + evict(); + rethrow; + } + } - try { - // await to handle errors - if (bytesFetcher case final ImageChunkEventsSupport bytesFetcher) { - return await bytesFetcher.withImageChunkEventsSink( - source: source, - abortSignal: abortTrigger.future, - transformer: transformer, - chunkEvents: chunkEvents, - ); - } - return await bytesFetcher( - source: source, - abortSignal: abortTrigger.future, - transformer: transformer, - ); - } on TileAbortedException { - evict(); - return ImmutableBuffer.fromUint8List(TileLoader.transparentImage) - .then(decode); - } catch (e) { - evict(); - rethrow; - } - }, + return RasterTileData( + image: KeyedDelegatedImage( + key: source, + delegate: imageDelegate, ), dispose: abortTrigger.complete, )..load(); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart index cd51e9da3..31d491e8a 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart @@ -4,7 +4,7 @@ import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; /// Generates a tile's 'source' based on its own properties, the ambient -/// [TileLayerOptions], and the tile's [TileCoordinates] +/// [TileLayerOptions], and the tile's [TileCoordinates]. /// /// The source type must be consumable by the [TileSourceFetcher] used. /// @@ -12,13 +12,13 @@ import 'package:meta/meta.dart'; /// and must set a valid equality operator. @immutable abstract interface class TileSourceGenerator { - /// Generates a tile's source + /// Generates a tile's source. /// /// See documentation on [TileSourceGenerator] for more information. S call(TileCoordinates coordinates, TileLayerOptions options); } -/// Fetch a tile's data ([D]) based on its 'source' ([S]) +/// Fetch a tile's data ([D]) based on its 'source' ([S]). /// /// The source type is set by the [TileSourceGenerator]. The fetcher does not /// have access to the ambient [TileLayerOptions], therefore any required @@ -29,7 +29,7 @@ abstract interface class TileSourceGenerator { @immutable abstract interface class TileSourceFetcher { - /// Fetch a tile's data based on its source + /// Fetch a tile's data based on its source. /// /// See documentation on [TileSourceFetcher] for more information. D call(S source); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart index b2fc873a7..dac30ab92 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart @@ -2,14 +2,15 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/misc/extensions.dart'; import 'package:meta/meta.dart'; /// A tile source generator which generates tiles for the -/// [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) referencing system +/// [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) referencing system. @immutable -class WMSGenerator implements TileSourceGenerator> { +class WMSGenerator implements TileSourceGenerator { /// WMS service's URL, for example 'http://ows.mundialis.de/services/service?' final String baseUrl; @@ -34,7 +35,7 @@ class WMSGenerator implements TileSourceGenerator> { /// Sets map projection standard final Crs crs; - /// The scalar to multiply the calculated width & height for each request by + /// The scalar to multiply the calculated width & height for each request by. /// /// This may be used to simulate retina mode, for example, by setting to 2. /// @@ -49,7 +50,7 @@ class WMSGenerator implements TileSourceGenerator> { late final double _versionNumber; - /// Create a new [WMSGenerator] instance + /// Create a new [WMSGenerator] instance. WMSGenerator({ required this.baseUrl, this.layers = const [], @@ -84,7 +85,7 @@ class WMSGenerator implements TileSourceGenerator> { } @override - List call(TileCoordinates coordinates, TileLayerOptions options) { + TileSource call(TileCoordinates coordinates, TileLayerOptions options) { final nwPoint = Offset( (coordinates.x * options.tileDimension).toDouble(), (coordinates.y * options.tileDimension).toDouble(), @@ -103,12 +104,12 @@ class WMSGenerator implements TileSourceGenerator> { ? [bounds.min.dy, bounds.min.dx, bounds.max.dy, bounds.max.dx] : [bounds.min.dx, bounds.min.dy, bounds.max.dx, bounds.max.dy]; - return [ + return TileSource( (StringBuffer(_encodedBaseUrl) ..write('&width=${options.tileDimension * dimensionsMultiplier}') ..write('&height=${options.tileDimension * dimensionsMultiplier}') ..write('&bbox=${bbox.join(',')}')) .toString(), - ]; + ); } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart index e4c675649..f45f2b961 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart @@ -3,11 +3,12 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetch import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; /// A tile source generator which generates tiles for slippy map tile servers -/// following the standard XYZ tile referencing system +/// following the standard XYZ tile referencing system. /// /// [Slippy maps](https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames) are /// also known as [tiled web maps](https://en.wikipedia.org/wiki/Tiled_web_map) @@ -18,8 +19,8 @@ import 'package:meta/meta.dart'; /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) standard by flipping /// the Y axis. @immutable -class XYZGenerator implements TileSourceGenerator> { - /// List of endpoints for tile resources, in XYZ template format +class XYZGenerator implements TileSourceGenerator { + /// List of endpoints for tile resources, in XYZ template format. /// /// Endpoints are used by the [TileSourceFetcher] in use, and so their meaning /// is context dependent. For example, a HTTP URL would likely be used with @@ -46,7 +47,7 @@ class XYZGenerator implements TileSourceGenerator> { final List uriTemplates; /// List of subdomains for the [uriTemplates] (to replace the `{s}` - /// placeholder) + /// placeholder). /// /// > [!NOTE] /// > This may no longer be necessary for many tile servers in many cases. @@ -54,7 +55,7 @@ class XYZGenerator implements TileSourceGenerator> { final List subdomains; /// Static information that should replace associated placeholders in the - /// [uriTemplates] + /// [uriTemplates]. /// /// For example, this could be used to more easily apply API keys to /// templates. @@ -62,11 +63,11 @@ class XYZGenerator implements TileSourceGenerator> { /// Override [generateReplacementMap] to dynamically generate placeholders. final Map additionalPlaceholders; - /// Whether to invert Y axis numbering for tiles + /// Whether to invert Y axis numbering for tiles. final bool tms; /// A tile source generator which generates tiles for slippy map tile servers - /// following the standard XYZ tile referencing system + /// following the standard XYZ tile referencing system. const XYZGenerator({ required this.uriTemplates, this.subdomains = const [], @@ -75,10 +76,9 @@ class XYZGenerator implements TileSourceGenerator> { }); @override - Iterable call( - TileCoordinates coordinates, - TileLayerOptions options, - ) { + TileSource call(TileCoordinates coordinates, TileLayerOptions options) { + assert(uriTemplates.isNotEmpty, '`uriTemplates` must not be empty'); + final replacementMap = generateReplacementMap(coordinates, options); String replacer(Match match) { @@ -87,12 +87,16 @@ class XYZGenerator implements TileSourceGenerator> { throw ArgumentError('Missing value for placeholder: {${match.group(1)}}'); } - // Lazily generate URIs as required - return uriTemplates - .map((t) => t.replaceAllMapped(templatePlaceholderElement, replacer)); + return TileSource( + uriTemplates[0].replaceAllMapped(templatePlaceholderElement, replacer), + fallbackUris: uriTemplates + .skip(1) + // Lazily generate fallback URIs as required + .map((t) => t.replaceAllMapped(templatePlaceholderElement, replacer)), + ); } - /// Generates the mapping of [uriTemplates] placeholders to replacements + /// Generates the mapping of [uriTemplates] placeholders to replacements. @visibleForOverriding Map generateReplacementMap( TileCoordinates coordinates, @@ -120,7 +124,7 @@ class XYZGenerator implements TileSourceGenerator> { }; } - /// Regex that describes the format of placeholders in a `uriTemplate` + /// Regex that describes the format of placeholders in a `uriTemplate`. /// /// The regex used prior to v6 originated from leaflet.js, specifically from /// commit [dc79b10683d2](https://github.com/Leaflet/Leaflet/commit/dc79b10683d232b9637cbe4d65567631f4fa5a0b). diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart index adf1e9fd7..4e7cf57af 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart @@ -19,17 +19,17 @@ import 'package:meta/meta.dart'; @immutable final class TileLoader implements BaseTileLoader { - /// Tile source generator + /// Tile source generator. /// /// See documentation on [TileLoader] & [TileSourceGenerator] for information. final TileSourceGenerator sourceGenerator; - /// Tile source fetcher + /// Tile source fetcher. /// /// See documentation on [TileLoader] & [TileSourceFetcher] for information. final TileSourceFetcher sourceFetcher; - /// Create a tile loader from a source generator & fetcher + /// Create a tile loader from a source generator & fetcher. const TileLoader({ required this.sourceGenerator, required this.sourceFetcher, @@ -49,7 +49,7 @@ final class TileLoader @override int get hashCode => Object.hash(sourceGenerator, sourceFetcher); - /// [Uint8List] that forms a fully transparent image + /// [Uint8List] that forms a fully transparent image. static final transparentImage = Uint8List.fromList([ 0x89, 0x50, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart new file mode 100644 index 000000000..91a8cb9d9 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart @@ -0,0 +1,74 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; + +/// Default tile source, which supports a primary URI & potential fallback URIs. +/// +/// Iterating this will first yield the [primaryUri], followed by any +/// [fallbackUris] in order. +/// +/// Generated by the default [TileSourceGenerator]s. Consumed by the default +/// [SourceBytesFetcher]s as a standard [Iterable]. [TileSourceFetcher]s may +/// use this as a key for short-term caching, asssuming the following conditions +/// are met... +/// +/// To enable short-term caching and ensure it is accurate, this object's +/// equality depends only on the [primaryUri]. +/// +/// > [!WARNING] +/// > If in use, [SourceBytesFetcher]s should not allow re-use of any generated +/// > resources from [fallbackUris] (as the wrong resource would be cached under +/// > the [primaryUri]) . +class TileSource extends Iterable { + /// Primary URI of the tile. + final String primaryUri; + + /// Lazily generated URIs of the tile which may be used in the event that the + /// [primaryUri] cannot be used to retrieve the tile. + /// + /// This is not included in the equality of this object. See the documentation + /// on this class for more info. + /// + /// This may be empty or not provided. + final Iterable? fallbackUris; + + /// Default tile source, which supports a primary URI & potential fallback + /// URIs. + const TileSource(this.primaryUri, {this.fallbackUris}); + + @override + int get hashCode => primaryUri.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is TileSource && primaryUri == other.primaryUri); + + @override + Iterator get iterator => + _TileSourceIterator(primaryUri, fallbackUris?.iterator); +} + +class _TileSourceIterator implements Iterator { + String? _current; + final String primaryUri; + final Iterator? fallbackUris; + + _TileSourceIterator(this.primaryUri, this.fallbackUris); + + @override + bool moveNext() { + if (_current == null) { + _current = primaryUri; + return true; + } + if (fallbackUris == null || !fallbackUris!.moveNext()) { + _current = null; + return false; + } + _current = fallbackUris!.current; + return true; + } + + @override + String get current => _current!; +} From fb8fe83560e9bd2a28a1963982a0d5f44b76fdf8 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Sun, 20 Jul 2025 21:55:59 +0100 Subject: [PATCH 09/13] Refactoring & renaming --- lib/flutter_map.dart | 10 +- .../{tile_data.dart => base_tile_data.dart} | 6 +- .../modern_tile_layer/base_tile_layer.dart | 11 +- .../modern_tile_layer/base_tile_loader.dart | 24 ++- .../tile_layers/raster/tile_layer.dart | 19 +- .../bytes_fetchers/bytes_fetcher.dart | 104 ---------- .../bytes_fetchers/file/file_io.dart | 44 ----- .../tile_loader/source_generator_fetcher.dart | 36 ---- .../tile_loader/source_generators/wms.dart | 8 +- .../tile_loader/source_generators/xyz.dart | 18 +- .../tile_loader/source_tile_generators.dart | 36 ++++ .../bytes_fetchers/asset/asset.dart | 35 +--- .../bytes_fetchers/bytes_fetcher.dart | 185 ++++++++++++++++++ .../bytes_fetchers/file/file_io.dart | 26 +++ .../bytes_fetchers/file/file_stub.dart | 2 +- .../built_in/built_in_caching_provider.dart | 4 +- .../caching/built_in/impl/native/README.md | 0 .../caching/built_in/impl/native/native.dart | 2 +- .../impl/native/workers/size_reducer.dart | 2 +- .../workers/tile_and_size_monitor_writer.dart | 4 +- .../network/caching/built_in/impl/stub.dart | 0 .../caching/built_in/impl/web/web.dart | 0 .../network/caching/caching_provider.dart | 0 .../disabled/disabled_caching_provider.dart | 0 .../network/caching/tile_metadata.dart | 0 .../caching/tile_read_failure_exception.dart | 0 .../network/fetcher/consolidate_response.dart | 0 .../network/fetcher/network.dart | 76 +++---- .../raster/generator.dart} | 28 +-- .../raster/image_provider.dart | 0 .../raster/tile_data.dart | 8 +- .../tile_loader/tile_loader.dart | 42 ++-- .../tile_loader/tile_source.dart | 68 ++++--- 33 files changed, 424 insertions(+), 374 deletions(-) rename lib/src/layer/modern_tile_layer/{tile_data.dart => base_tile_data.dart} (93%) delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/asset/asset.dart (62%) create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/file/file_stub.dart (92%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart (97%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/impl/native/README.md (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/impl/native/native.dart (99%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart (97%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart (98%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/impl/stub.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/built_in/impl/web/web.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/caching_provider.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/tile_metadata.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/caching/tile_read_failure_exception.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/fetcher/consolidate_response.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/bytes_fetchers/network/fetcher/network.dart (87%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers/raster/raster_tile_fetcher.dart => tile_generators/raster/generator.dart} (82%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/raster/image_provider.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{source_fetchers => tile_generators}/raster/tile_data.dart (93%) diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index db0a9d6c6..7686c240d 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -29,11 +29,11 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart'; export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer/label/deprecated_placements.dart'; export 'package:flutter_map/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_data.dart b/lib/src/layer/modern_tile_layer/base_tile_data.dart similarity index 93% rename from lib/src/layer/modern_tile_layer/tile_data.dart rename to lib/src/layer/modern_tile_layer/base_tile_data.dart index 9ab08654a..7cf16b2f2 100644 --- a/lib/src/layer/modern_tile_layer/tile_data.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_data.dart @@ -14,7 +14,7 @@ import 'package:meta/meta.dart'; /// It is up to the implementation as to what 'loads' means. However, the /// [BaseTileLayer] will use [whenLoaded], [isLoaded], and [dispose] to manage /// (such as pruning) the tile for the renderer. -abstract interface class TileData { +abstract interface class BaseTileData { /// Completes when the underlying resource is 'loaded' Future get whenLoaded; @@ -32,13 +32,13 @@ abstract interface class TileData { void dispose(); } -/// Wrapper for custom-shape data as a [TileData] +/// Wrapper for custom-shape data as a [BaseTileData] /// /// The data carried is usually made available asynchronously, for example as /// the result of an I/O operation or HTTP request. Alternatively, data may be /// available synchronously if the data is loaded from prepared memory. This /// container supports either form of data. -class WrapperTileData implements TileData { +class WrapperTileData implements BaseTileData { D? _data; /// Data resource diff --git a/lib/src/layer/modern_tile_layer/base_tile_layer.dart b/lib/src/layer/modern_tile_layer/base_tile_layer.dart index db766b6ca..d3b882e3d 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_layer.dart @@ -2,9 +2,9 @@ import 'dart:collection'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -17,7 +17,7 @@ import 'package:meta/meta.dart'; /// This layer is often used to draw the map itself, for example as raster image /// tiles. However, it may be used for any reasonable purpose where the contract /// is met. -class BaseTileLayer extends StatefulWidget { +class BaseTileLayer extends StatefulWidget { final TileLayerOptions options; final BaseTileLoader tileLoader; final Widget Function( @@ -38,7 +38,8 @@ class BaseTileLayer extends StatefulWidget { State> createState() => _BaseTileLayerState(); } -class _BaseTileLayerState extends State> { +class _BaseTileLayerState + extends State> { late Object layerKey = UniqueKey(); final tiles = _TilesTracker(); @@ -65,7 +66,7 @@ class _BaseTileLayerState extends State> { final key = (coordinates: coordinates, layerKey: layerKey); tiles.putIfAbsent( key, - () => widget.tileLoader.load(coordinates, widget.options) + () => widget.tileLoader(coordinates, widget.options) ..whenLoaded.then((_) => _pruneOnLoadedTile(key)), // TODO: Consider how to handle errors ); @@ -233,7 +234,7 @@ extension _ParentChildTraversal on TileCoordinates { typedef _TileKey = ({TileCoordinates coordinates, Object layerKey}); -extension type _TilesTracker._( +extension type _TilesTracker._( SplayTreeMap<_TileKey, D> map) implements SplayTreeMap<_TileKey, D> { _TilesTracker() : this._( diff --git a/lib/src/layer/modern_tile_layer/base_tile_loader.dart b/lib/src/layer/modern_tile_layer/base_tile_loader.dart index d0dce69a2..2e2187103 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_loader.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_loader.dart @@ -1,19 +1,17 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; +/// Responsible for generating data ([D]) for tiles given the tile's +/// [TileCoordinates] and ambient [TileLayerOptions]. +/// +/// See [TileLoader] for an implementation which delegates its responsibility +/// into two parts. @immutable -abstract interface class BaseTileLoader { - const BaseTileLoader(); - - D load(TileCoordinates coordinates, TileLayerOptions options); - - @override - @mustBeOverridden - int get hashCode; - - @override - @mustBeOverridden - bool operator ==(Object other); +abstract interface class BaseTileLoader { + /// Generate data for the tile at [coordinates], with the ambient layer + /// [options]. + D call(TileCoordinates coordinates, TileLayerOptions options); } diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart index ec9791ea0..1f6131157 100644 --- a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart @@ -1,13 +1,14 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; class RasterTileLayer extends StatefulWidget { @@ -23,11 +24,11 @@ class RasterTileLayer extends StatefulWidget { this.options = const TileLayerOptions(), required String urlTemplate, required String uaIdentifier, - }) : sourceGenerator = XYZGenerator(uriTemplates: [urlTemplate]), + }) : sourceGenerator = XYZSourceGenerator(uriTemplates: [urlTemplate]), bytesFetcher = NetworkBytesFetcher(uaIdentifier: uaIdentifier); final TileLayerOptions options; - final TileSourceGenerator> sourceGenerator; + final SourceGenerator sourceGenerator; final SourceBytesFetcher> bytesFetcher; @override @@ -40,7 +41,7 @@ class _RasterTileLayerState extends State { options: widget.options, tileLoader: TileLoader( sourceGenerator: widget.sourceGenerator, - sourceFetcher: RasterTileFetcher(bytesFetcher: widget.bytesFetcher), + tileGenerator: RasterTileGenerator(bytesFetcher: widget.bytesFetcher), ), renderer: (context, layerKey, options, visibleTiles) => _RasterRenderer( layerKey: layerKey, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart deleted file mode 100644 index 3feb4c487..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart +++ /dev/null @@ -1,104 +0,0 @@ -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; -import 'package:meta/meta.dart'; - -/// Fetches a tile's bytes based on its source ([S]), transforming it into a -/// desired resource using a supplied [BytesToResourceTransformer]. -/// -/// Implementers should implement longer-term caching where necessary, or -/// delegate to a cacher. Note that [TileSourceFetcher]s may also perform -/// caching of the resulting resource, often in the short-term - such as the -/// [RasterTileFetcher] using the Flutter [ImageCache]. -/// -/// Implementations which work with the [RasterTileFetcher] should consider -/// mixing-in [ImageChunkEventsSupport]. -abstract interface class SourceBytesFetcher { - /// {@template fm.tilelayer.tilebytesfetcher.call} - /// Fetches a tile's bytes based on its source ([S]), transforming it into a - /// desired resource ([R]) using a supplied transformer. - /// - /// The [abortSignal] completes when the tile is no longer required. If - /// possible, any ongoing work (such as an HTTP request) should be aborted. - /// If aborting and a result is unavailable, [TileAbortedException] should be - /// thrown. - /// - /// See [BytesToResourceTransformer] for more information about handling the - /// [transformer]. - /// {@endtemplate} - FutureOr call({ - required S source, - required Future abortSignal, - required BytesToResourceTransformer transformer, - }); -} - -/// Allows a [SourceBytesFetcher] to integrate more closely with the raster tile -/// stack by reporting progress events to the underlying [ImageProvider]. -abstract mixin class ImageChunkEventsSupport - implements SourceBytesFetcher { - /// Redirects to [withImageChunkEventsSink]. - @override - @nonVirtual - FutureOr call({ - required S source, - required Future abortSignal, - required BytesToResourceTransformer transformer, - }) => - withImageChunkEventsSink( - source: source, - abortSignal: abortSignal, - transformer: transformer, - ); - - /// {@macro fm.tilelayer.tilebytesfetcher.call} - /// - /// [chunkEvents] should be used when consolidating a stream of bytes to - /// report progress notifications to the underlying [ImageProvider]. - FutureOr withImageChunkEventsSink({ - required S source, - required Future abortSignal, - required BytesToResourceTransformer transformer, - StreamSink? chunkEvents, - }); -} - -/// Exception thrown when a tile was loading but aborted early as it was no -/// longer required. -class TileAbortedException implements Exception { - /// Optional description of the tile. - final Object? source; - - /// Exception thrown when a tile was loading but aborted early as it was no - /// longer required. - const TileAbortedException({this.source}); - - @override - String toString() => 'TileAbortedException: $source'; -} - -/// Callback provided to a [SourceBytesFetcher] by a root [TileSourceFetcher], -/// which converts fetched bytes into the desired [Resource]. -/// -/// This may throw if the bytes could not be correctly transformed, for example -/// because they were corrupted or otherwise undecodable. In this case, it is -/// the bytes fetcher's responsibility to catch the error and act accordingly, -/// potentially by returning another (for example, a fallback) resource and/or -/// disabling the long-term caching of this tile. Therefore, it is recommended -/// to always await the result of the callback. -/// -/// The [SourceBytesFetcher] should also indicate whether it is acceptable for -/// other parts of the stack (such as the [TileSourceFetcher]) to reuse the -/// resource for tile in the short-term, avoiding having to re-fetch bytes. -/// Other parts of the stack may perform short-term caching (whilst it is the -/// bytes fetcher's responsibility to provide long-term caching) to improve -/// efficiency, for example when the same tile is re-requested for display in -/// the same session. For example, a raster image resource may be cached in -/// memory using the [ImageCache]. However, if this should not occur, because -/// the bytes create a resource different to what is desired (for example, a -/// fallback resource), then `allowReuse` should be set `false`. -typedef BytesToResourceTransformer - = FutureOr Function(Uint8List bytes, {bool allowReuse}); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart deleted file mode 100644 index 1f9569916..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_io.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; - -/// A [SourceBytesFetcher] which fetches from the local filesystem. -/// -/// {@macro fm.sbf.default.sourceConsumption} -@immutable -class FileBytesFetcher implements SourceBytesFetcher> { - /// A [SourceBytesFetcher] which fetches from the local filesystem. - const FileBytesFetcher(); - - @override - Future call({ - required Iterable source, - required Future abortSignal, - required BytesToResourceTransformer transformer, - }) async { - final iterator = source.iterator; - - if (!iterator.moveNext()) { - throw ArgumentError('At least one URI must be provided', 'source'); - } - - for (bool isPrimary = true;; isPrimary = false) { - // TODO: Consider abortable streaming of bytes - try { - return await transformer( - await File(iterator.current).readAsBytes(), - // In fallback scenarios, we never allow reuse of bytes in the - // short-term cache (or long-term cache) - allowReuse: isPrimary, - ); - } on Exception { - if (!iterator.moveNext()) rethrow; // No (more) fallbacks available - - // Attempt fallbacks - // TODO: Consider logging - continue; - } - } - } -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart deleted file mode 100644 index 31d491e8a..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; -import 'package:meta/meta.dart'; - -/// Generates a tile's 'source' based on its own properties, the ambient -/// [TileLayerOptions], and the tile's [TileCoordinates]. -/// -/// The source type must be consumable by the [TileSourceFetcher] used. -/// -/// If this generator accepts other properties/options, it must remain immutable -/// and must set a valid equality operator. -@immutable -abstract interface class TileSourceGenerator { - /// Generates a tile's source. - /// - /// See documentation on [TileSourceGenerator] for more information. - S call(TileCoordinates coordinates, TileLayerOptions options); -} - -/// Fetch a tile's data ([D]) based on its 'source' ([S]). -/// -/// The source type is set by the [TileSourceGenerator]. The fetcher does not -/// have access to the ambient [TileLayerOptions], therefore any required -/// options must appear in the source. -/// -/// If this fetcher accepts other properties/options, it must remain immutable -/// and must set a valid equality operator. -@immutable -abstract interface class TileSourceFetcher { - /// Fetch a tile's data based on its source. - /// - /// See documentation on [TileSourceFetcher] for more information. - D call(S source); -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart index dac30ab92..f43c2c780 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart @@ -1,7 +1,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/misc/extensions.dart'; @@ -10,7 +10,7 @@ import 'package:meta/meta.dart'; /// A tile source generator which generates tiles for the /// [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) referencing system. @immutable -class WMSGenerator implements TileSourceGenerator { +class WMSSourceGenerator implements SourceGenerator { /// WMS service's URL, for example 'http://ows.mundialis.de/services/service?' final String baseUrl; @@ -50,8 +50,8 @@ class WMSGenerator implements TileSourceGenerator { late final double _versionNumber; - /// Create a new [WMSGenerator] instance. - WMSGenerator({ + /// Create a new [WMSSourceGenerator] instance. + WMSSourceGenerator({ required this.baseUrl, this.layers = const [], this.styles = const [], diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart index f45f2b961..c3b4b16d1 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart @@ -1,8 +1,8 @@ import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; @@ -19,10 +19,10 @@ import 'package:meta/meta.dart'; /// [TMS](https://en.wikipedia.org/wiki/Tile_Map_Service) standard by flipping /// the Y axis. @immutable -class XYZGenerator implements TileSourceGenerator { +class XYZSourceGenerator implements SourceGenerator { /// List of endpoints for tile resources, in XYZ template format. /// - /// Endpoints are used by the [TileSourceFetcher] in use, and so their meaning + /// Endpoints are used by the [TileGenerator] in use, and so their meaning /// is context dependent. For example, a HTTP URL would likely be used with /// the [NetworkBytesFetcher], whilst a file URI would be used with the /// [FileBytesFetcher]. @@ -33,7 +33,7 @@ class XYZGenerator implements TileSourceGenerator { /// /// > [!WARNING] /// > Using fallbacks may incur a (potentially significant) performance - /// > penalty, and may not be understood by all [TileSourceFetcher]s. + /// > penalty, and may not be understood by all [TileGenerator]s. /// > Note that failing each endpoint may take some time (such as a HTTP /// > timeout elapsing). /// @@ -68,7 +68,7 @@ class XYZGenerator implements TileSourceGenerator { /// A tile source generator which generates tiles for slippy map tile servers /// following the standard XYZ tile referencing system. - const XYZGenerator({ + const XYZSourceGenerator({ required this.uriTemplates, this.subdomains = const [], this.additionalPlaceholders = const {}, @@ -145,7 +145,7 @@ class XYZGenerator implements TileSourceGenerator { @override bool operator ==(Object other) => identical(this, other) || - (other is XYZGenerator && + (other is XYZSourceGenerator && other.uriTemplates == uriTemplates && other.subdomains == subdomains && other.additionalPlaceholders == additionalPlaceholders && diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart new file mode 100644 index 000000000..6f081cea2 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart @@ -0,0 +1,36 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +/// Generates a tile's 'source' based on its own properties, the ambient +/// [TileLayerOptions], and the tile's [TileCoordinates]. +/// +/// The source type must be consumable by the [TileGenerator] used. +@immutable +abstract interface class SourceGenerator { + /// Generates a tile's 'source' based on its own properties, the ambient + /// [TileLayerOptions], and the tile's [TileCoordinates]. + /// + /// See documentation on [SourceGenerator] for more information. + S call(TileCoordinates coordinates, TileLayerOptions options); +} + +/// Generates a tile's data ([D]) based on its 'source' ([S]). +/// +/// The source type is set by the [SourceGenerator]. The fetcher does not have +/// access to the ambient [TileLayerOptions], therefore any required options +/// must appear in the source. +/// +/// To promote flexibility and re-usability, it is recommended to further +/// delegate parts of the tile generator, such as I/O fetching operations. It +/// may depend on a [SourceBytesFetcher] to do this. +@immutable +abstract interface class TileGenerator { + /// Generates a tile's data ([D]) based on its 'source' ([S]). + /// + /// See documentation on [TileGenerator] for more information. + D call(S source); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/asset/asset.dart similarity index 62% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/asset/asset.dart index add2655e8..44d5ae59d 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/asset/asset.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/asset/asset.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -/// A [SourceBytesFetcher] which fetches from the app's shipped assets. +/// A [SourceBytesFetcher] which fetches a URI from the app's shipped assets. /// /// {@macro fm.sbf.default.sourceConsumption} /// @@ -35,30 +35,13 @@ class AssetBytesFetcher implements SourceBytesFetcher> { required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, - }) async { + }) { final bundle = assetBundle ?? rootBundle; - - final iterator = source.iterator; - - if (!iterator.moveNext()) { - throw ArgumentError('At least one URI must be provided', 'source'); - } - - for (bool isPrimary = true;; isPrimary = false) { - try { - return await transformer( - Uint8List.sublistView(await bundle.load(iterator.current)), - // In fallback scenarios, we never allow reuse of bytes in the - // short-term cache (or long-term cache) - allowReuse: isPrimary, - ); - } on Exception { - if (!iterator.moveNext()) rethrow; // No (more) fallbacks available - - // Attempt fallbacks - // TODO: Consider logging - continue; - } - } + return fetchFromSourceIterable( + (uri, transformer, isFirst) => + bundle.load(uri).then(Uint8List.sublistView).then(transformer), + source: source, + transformer: transformer, + ); } } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart new file mode 100644 index 000000000..7443487a3 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart @@ -0,0 +1,185 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +import 'package:logger/logger.dart'; + +/// Fetches a tile's bytes based on its source ([S]), transforming it into a +/// desired resource using a supplied [BytesToResourceTransformer]. +/// +/// Implementers should implement longer-term caching where necessary, or +/// delegate to a cacher. Note that [TileGenerator]s may also perform caching of +/// the resulting resource, often in the short-term - such as the +/// [RasterTileGenerator] using the Flutter [ImageCache]. +/// +/// Implementations which work with the [RasterTileGenerator] should consider +/// mixing-in [ImageChunkEventsSupport]. +abstract interface class SourceBytesFetcher { + /// {@template fm.tilelayer.tilebytesfetcher.call} + /// Fetches a tile's bytes based on its source ([S]), transforming it into a + /// desired resource ([R]) using a supplied transformer. + /// + /// The [abortSignal] completes when the tile is no longer required. If + /// possible, any ongoing work (such as an HTTP request) should be aborted. + /// If aborting and a result is unavailable, [TileAbortedException] should be + /// thrown. + /// + /// See [BytesToResourceTransformer] for more information about handling the + /// [transformer]. + /// {@endtemplate} + FutureOr call({ + required S source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + }); +} + +/// Allows a [SourceBytesFetcher] to integrate more closely with the raster tile +/// stack by reporting progress events to the underlying [ImageProvider]. +abstract mixin class ImageChunkEventsSupport + implements SourceBytesFetcher { + /// Redirects to [withImageChunkEventsSink]. + @override + @nonVirtual + FutureOr call({ + required S source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + }) => + withImageChunkEventsSink( + source: source, + abortSignal: abortSignal, + transformer: transformer, + ); + + /// {@macro fm.tilelayer.tilebytesfetcher.call} + /// + /// [chunkEvents] should be used when consolidating a stream of bytes to + /// report progress notifications to the underlying [ImageProvider]. + FutureOr withImageChunkEventsSink({ + required S source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + StreamSink? chunkEvents, + }); +} + +/// Exception thrown when a tile was loading but aborted early as it was no +/// longer required. +class TileAbortedException implements Exception { + /// Optional description of the tile. + final Object? source; + + /// Exception thrown when a tile was loading but aborted early as it was no + /// longer required. + const TileAbortedException({this.source}); + + @override + String toString() => 'TileAbortedException: $source'; +} + +/// Callback provided to a [SourceBytesFetcher] by a root [TileGenerator], +/// which converts fetched bytes into the desired [Resource]. +/// +/// This may throw if the bytes could not be correctly transformed, for example +/// because they were corrupted or otherwise undecodable. In this case, it is +/// the bytes fetcher's responsibility to catch the error and act accordingly, +/// potentially by returning another (for example, a fallback) resource and/or +/// disabling the long-term caching of this tile. +/// +/// --- +/// +/// Whilst it is the [SourceBytesFetcher]s or [TileGenerator]s responsibility to +/// implement long-term caching where necessary, other parts of the stack (such +/// as the [TileGenerator]) may also perform short-term caching, which requires +/// a key. +/// +/// If the resulting resource differs to what is expected and used as the key +/// - for example, in the case of a fallback being used whilst the only stable +/// key is the primary endpoint - then this must indicate that the resource may +/// not be reused under the key (i.e. not cached). This is done by setting +/// [allowReuse] `false`. +/// +/// Implementers should make the default of [allowReuse] `true`. +typedef BytesToResourceTransformer + = FutureOr Function(Uint8List bytes, {bool allowReuse}); + +/// Provides utilities to [SourceBytesFetcher]s which consume [Iterable] +/// sources. +extension IterableSourceConsumer on SourceBytesFetcher> { + /// Consecutively execute a callback ([fetcher]) on each element of the + /// non-empty [source] in iteration order, until the result (as returned by + /// the [transformer]) is not an error. + /// + /// --- + /// + /// 'Fallbacks' are all elements of the source except the first (mandatory) + /// element. + /// + /// If any result is a [TileAbortedException], (further) fallbacks are not + /// attempted. If the first result was [TileAbortedException], it is rethrown. + /// + /// If all fallbacks fail or a fallback is aborted, then the error thrown by + /// the first element is thrown. + /// + /// For all fallbacks, the [transformer] is automatically modified to disable + /// re-use of the bytes. See [BytesToResourceTransformer] for more info. This + /// meets [TileSource]'s requirements. + /// + /// Emits a log for each fallback attempted. + @protected + Future fetchFromSourceIterable( + Future Function( + T element, + BytesToResourceTransformer transformer, + bool isFirst, + ) fetcher, { + required Iterable source, + required BytesToResourceTransformer transformer, + }) async { + final firstElement = source.firstOrNull ?? + (throw ArgumentError('must have at least one element', 'source')); + + try { + return await fetcher(firstElement, transformer, true); + } on TileAbortedException { + rethrow; // Don't try fallbacks when aborted + } on Exception { + // Lazily initialise logger + late final logger = Logger(printer: SimplePrinter()); + + // Iterate through fallbacks + for (final fallbackUri in source.skip(1)) { + if (kDebugMode) { + logger.w( + '[flutter_map] Attempting fallback URI ($fallbackUri) instead of ' + '$firstElement', + ); + } + + try { + return await fetcher( + fallbackUri, + (bytes, {allowReuse = true}) => + transformer(bytes, allowReuse: false), + false, + ); + } on TileAbortedException { + // Don't try any further fallbacks when aborted, but still throw + // the primary URI's exception instead of `TileAbortedException` + break; + } on Exception { + // Attempt further fallbacks + continue; + } + } + + // This means we always throw the exception from the primary URI, when + // either there are no fallbacks or they have all been exhausted + rethrow; + } + } +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart new file mode 100644 index 000000000..c732542f0 --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart @@ -0,0 +1,26 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; + +/// A [SourceBytesFetcher] which fetches a URI from the local filesystem. +/// +/// {@macro fm.sbf.default.sourceConsumption} +@immutable +class FileBytesFetcher implements SourceBytesFetcher> { + /// A [SourceBytesFetcher] which fetches from the local filesystem. + const FileBytesFetcher(); + + @override + Future call({ + required Iterable source, + required Future abortSignal, + required BytesToResourceTransformer transformer, + }) => + fetchFromSourceIterable( + (uri, transformer, isFirst) => + File(uri).readAsBytes().then(transformer), + source: source, + transformer: transformer, + ); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart similarity index 92% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart index 10e80751e..a06e5ba11 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/file/file_stub.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; /// A [SourceBytesFetcher] which fetches from the local filesystem. /// diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart similarity index 97% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart index 9a333e439..19d88e9ee 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/stub.dart' +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/stub.dart' if (dart.library.io) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart' if (dart.library.js_interop) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; import 'package:uuid/data.dart'; import 'package:uuid/rng.dart'; import 'package:uuid/uuid.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/README.md b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/README.md similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/README.md rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/README.md diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart similarity index 99% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart index 7f008afc6..bbe129a17 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart @@ -6,7 +6,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart similarity index 97% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart index 0a3bffb50..24bc8953c 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart similarity index 98% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart index 1881a8817..4dc65b7e6 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart @@ -5,8 +5,8 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/stub.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/stub.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/stub.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/web/web.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/web/web.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/consolidate_response.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/consolidate_response.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart similarity index 87% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart index 6232c3ab0..ea7bf3c50 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart @@ -3,18 +3,19 @@ import 'dart:io' show HttpHeaders, HttpDate, HttpStatus; // web safe! import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/caching_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_metadata.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/consolidate_response.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/consolidate_response.dart'; import 'package:http/http.dart'; import 'package:http/retry.dart'; import 'package:logger/logger.dart'; -/// A [SourceBytesFetcher] which fetches from the network using HTTP. +/// A [SourceBytesFetcher] which fetches a URI from the network using HTTP & +/// supports caching via a [MapCachingProvider]. /// /// {@template fm.sbf.default.sourceConsumption} /// Consumes an [Iterable] of [String] URIs, which must not be empty and @@ -120,45 +121,29 @@ class NetworkBytesFetcher required Future abortSignal, required BytesToResourceTransformer transformer, StreamSink? chunkEvents, - }) async { - final iterator = source.iterator; - - if (!iterator.moveNext()) { - throw ArgumentError('At least one URI must be provided', 'source'); - } - - for (bool isPrimary = true;; isPrimary = false) { - try { - return await _fetch( - uri: iterator.current, + }) => + fetchFromSourceIterable( + (uri, transformer, isFirst) => fetchSingle( + uri: uri, abortSignal: abortSignal, - transformer: isPrimary - ? transformer - : (bytes, {allowReuse = true}) => - // In fallback scenarios, we never allow reuse of bytes in the - // short-term cache (or long-term cache) - transformer(bytes, allowReuse: false), + transformer: transformer, chunkEvents: chunkEvents, - performLongTermCaching: !isPrimary, - ); - } on TileAbortedException { - rethrow; // Never try fallbacks on abortion - } on Exception { - if (!iterator.moveNext()) rethrow; // No (more) fallbacks available - - // Attempt fallbacks - // TODO: Consider logging - continue; - } - } - } + ), + source: source, + transformer: transformer, + ); - Future _fetch({ + /// Fetch a single URI's resource + /// + /// This is used internally but exposed for convenience. + /// + /// This throws when an error is encountered attempting to access the + /// resource. + Future fetchSingle({ required String uri, required Future abortSignal, required BytesToResourceTransformer transformer, - required StreamSink? chunkEvents, - required bool performLongTermCaching, + StreamSink? chunkEvents, }) async { final parsedUri = Uri.parse(uri); @@ -207,13 +192,16 @@ class NetworkBytesFetcher } // Create method to write response to cache when applicable + // Even when fetching a fallback, we can still use the long-term cache, as + // it safely associates it with the resolved URI. This is not possible for + // the short-term cache, as it would require the I/O work to occur before + // the short-term cache key could be resolved. void cachePut({ required Uint8List? bytes, required Map headers, }) { - if (performLongTermCaching || !cachingProvider.isSupported) return; + if (!cachingProvider.isSupported) return; - // TODO: Consider best way to silence these 2 logs late final CachedMapTileMetadata metadata; try { metadata = CachedMapTileMetadata.fromHttpHeaders( @@ -302,7 +290,7 @@ class NetworkBytesFetcher try { return await transformer(bytes, allowReuse: false); - } catch (_, stackTrace) { + } on Exception catch (_, stackTrace) { // If it throws, we don't want to throw the decode error, as that's not // useful for users // Instead, we throw an exception reporting the failed HTTP request, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart similarity index 82% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart index 11ca808a9..4222b07f4 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart @@ -3,16 +3,16 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/fetcher/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/image_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; -/// A tile source fetcher which delgates fetching of a raster image's bytes to +/// A tile generator which delgates fetching of a raster image's bytes to /// a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the /// bytes. /// @@ -30,29 +30,29 @@ import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source. /// The pre-provided [SourceBytesFetcher]s (such as [NetworkBytesFetcher]) /// consume [Iterable]s of [String]s as a source ([S]), and use the transformer /// provided by this object to output a [RasterTileData]. This is so that they -/// may be used outside of the context of [TileSource] & [RasterTileFetcher] +/// may be used outside of the context of [TileSource] & [RasterTileGenerator] /// (for example, in a different stack). /// /// It is not suitable to use an [Iterable] directly, as it does not meet the -/// criteria for a key. Instead, the pre-provided [TileSourceGenerator]s (such -/// as [XYZGenerator]) output a [TileSource], which meets all necessary +/// criteria for a key. Instead, the pre-provided [SourceGenerator]s (such +/// as [XYZSourceGenerator]) output a [TileSource], which meets all necessary /// requirements. /// /// However, this fetcher could be used with any source. For example, if a /// different [SourceBytesFetcher] is used, it doesn't necessarily need to use /// [TileSource] or any other contract described above. -class RasterTileFetcher - implements TileSourceFetcher { +class RasterTileGenerator + implements TileGenerator { /// The delegate which provides the bytes for the this tile. /// /// This may not be called for every tile, if the tile was already present in /// the ambient [ImageCache]. final SourceBytesFetcher bytesFetcher; - /// A tile source fetcher which delgates fetching of a raster image's bytes - /// to a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the + /// A tile generator which delgates fetching of a raster image's bytes to a + /// [SourceBytesFetcher], then creates an [ImageProvider] by decoding the /// bytes. - const RasterTileFetcher({required this.bytesFetcher}); + const RasterTileGenerator({required this.bytesFetcher}); @override RasterTileData call(S source) { diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/image_provider.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/image_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/image_provider.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart similarity index 93% rename from lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart rename to lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart index 3186effa2..f369a3dc0 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/tile_data.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart @@ -1,17 +1,17 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/raster/raster_tile_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; import 'package:meta/meta.dart'; /// Raster tile data associated with a particular tile coordinate /// -/// This is used for communication between the [RasterTileFetcher] and the +/// This is used for communication between the [RasterTileGenerator] and the /// raster tile renderer. /// /// It is not usually necessary to consume this externally. -class RasterTileData implements TileData { +class RasterTileData implements BaseTileData { /// Actual raster [ImageProvider] final ImageProvider image; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart index 4e7cf57af..a4072c94a 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart @@ -1,53 +1,57 @@ import 'dart:typed_data'; +import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; -/// Default implementation of a tile loader, which delegates loading of data to -/// two seperate stages: +/// Default implementation of a tile loader, which delegates loading of tile +/// data to two seperate stages: /// /// 1. The [sourceGenerator] uses a tile's [TileCoordinates] & the ambient /// [TileLayerOptions] to generate an object, describing the tile's 'source' /// ([S]) /// -/// 2. The [sourceFetcher] uses this 'source' to generate an output [TileData] -/// ([D]) +/// 2. The [tileGenerator] uses this 'source' to generate an output +/// [BaseTileData] ([D]) @immutable -final class TileLoader +final class TileLoader implements BaseTileLoader { - /// Tile source generator. + /// Generates a 'source' ([S]) for a tile given its [TileCoordinates] & the + /// ambient [TileLayerOptions] /// - /// See documentation on [TileLoader] & [TileSourceGenerator] for information. - final TileSourceGenerator sourceGenerator; + /// For example, see [XYZSourceGenerator]. + final SourceGenerator sourceGenerator; - /// Tile source fetcher. + /// Generates a tile's data ([D]) based on the 'source' generated by + /// [sourceGenerator] /// - /// See documentation on [TileLoader] & [TileSourceFetcher] for information. - final TileSourceFetcher sourceFetcher; + /// For example, see [RasterTileGenerator]. + final TileGenerator tileGenerator; - /// Create a tile loader from a source generator & fetcher. + /// Create a tile loader from a source generator & tile generator. const TileLoader({ required this.sourceGenerator, - required this.sourceFetcher, + required this.tileGenerator, }); @override - D load(TileCoordinates coordinates, TileLayerOptions options) => - sourceFetcher(sourceGenerator(coordinates, options)); + D call(TileCoordinates coordinates, TileLayerOptions options) => + tileGenerator(sourceGenerator(coordinates, options)); @override bool operator ==(Object other) => identical(this, other) || (other is TileLoader && - other.sourceFetcher == sourceFetcher && + other.tileGenerator == tileGenerator && other.sourceGenerator == sourceGenerator); @override - int get hashCode => Object.hash(sourceGenerator, sourceFetcher); + int get hashCode => Object.hash(sourceGenerator, tileGenerator); /// [Uint8List] that forms a fully transparent image. static final transparentImage = Uint8List.fromList([ diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart index 91a8cb9d9..41f80d488 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart @@ -1,23 +1,28 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generator_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; +import 'package:meta/meta.dart'; -/// Default tile source, which supports a primary URI & potential fallback URIs. +/// Data class for communicating URIs between [SourceGenerator]s +/// (such as [XYZSourceGenerator]) and [TileGenerator]s (such as +/// [RasterTileGenerator]), with ability to be used directly as a short-term +/// cache key* and by [SourceBytesFetcher]s (such as [NetworkBytesFetcher]). /// -/// Iterating this will first yield the [primaryUri], followed by any +/// This carries a [primaryUri] and potentially multiple ordered [fallbackUris]. +/// When iterated, this will first yield the [primaryUri], followed by any /// [fallbackUris] in order. /// -/// Generated by the default [TileSourceGenerator]s. Consumed by the default -/// [SourceBytesFetcher]s as a standard [Iterable]. [TileSourceFetcher]s may -/// use this as a key for short-term caching, asssuming the following conditions -/// are met... -/// -/// To enable short-term caching and ensure it is accurate, this object's -/// equality depends only on the [primaryUri]. -/// /// > [!WARNING] -/// > If in use, [SourceBytesFetcher]s should not allow re-use of any generated -/// > resources from [fallbackUris] (as the wrong resource would be cached under -/// > the [primaryUri]) . +/// > The equality of these objects depends only on [primaryUri]. +/// > Therefore, where used as a short-term cache key, resources at +/// > [fallbackUris] must not automatically be re-used/cached under the +/// > [primaryUri]. +/// +/// This is provided as it is used internally and may be used externally by +/// layer implementations for convienience, however it is not required. +@immutable class TileSource extends Iterable { /// Primary URI of the tile. final String primaryUri; @@ -31,10 +36,14 @@ class TileSource extends Iterable { /// This may be empty or not provided. final Iterable? fallbackUris; - /// Default tile source, which supports a primary URI & potential fallback - /// URIs. + /// Construct a data class for communicating URIs between [SourceGenerator]s + /// and [TileGenerator]s. const TileSource(this.primaryUri, {this.fallbackUris}); + @override + Iterator get iterator => + _TileSourceIterator(primaryUri, fallbackUris?.iterator); + @override int get hashCode => primaryUri.hashCode; @@ -42,30 +51,33 @@ class TileSource extends Iterable { bool operator ==(Object other) => identical(this, other) || (other is TileSource && primaryUri == other.primaryUri); - - @override - Iterator get iterator => - _TileSourceIterator(primaryUri, fallbackUris?.iterator); } class _TileSourceIterator implements Iterator { - String? _current; - final String primaryUri; - final Iterator? fallbackUris; + final String _primaryUri; + final Iterator? _fallbackUris; - _TileSourceIterator(this.primaryUri, this.fallbackUris); + _TileSourceIterator(this._primaryUri, this._fallbackUris); + + String? _current; + bool _finished = false; @override bool moveNext() { + if (_finished) return false; + if (_current == null) { - _current = primaryUri; + _current = _primaryUri; return true; } - if (fallbackUris == null || !fallbackUris!.moveNext()) { + + if (_fallbackUris == null || !_fallbackUris.moveNext()) { _current = null; + _finished = true; return false; } - _current = fallbackUris!.current; + + _current = _fallbackUris.current; return true; } From bcd51f9aa70eaf6f221d8a97d1edf5ff5d0df7be Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Mon, 11 Aug 2025 18:15:08 +0100 Subject: [PATCH 10/13] Removed generic `TileLoader`, collapsing responsibilities into `RasterTileLoader` --- .gitignore | 2 +- example/pubspec.lock | 2 +- lib/flutter_map.dart | 10 +- .../modern_tile_layer/base_tile_data.dart | 4 +- .../modern_tile_layer/base_tile_layer.dart | 4 +- .../modern_tile_layer/base_tile_loader.dart | 17 -- lib/src/layer/modern_tile_layer/options.dart | 6 +- .../source_generators/source_generator.dart | 13 ++ .../source_generators/wms.dart | 2 +- .../source_generators/xyz.dart | 8 +- .../tile_layers/raster/tile_layer.dart | 19 +- .../bytes_fetchers/asset/asset.dart | 4 +- .../bytes_fetchers/bytes_fetcher.dart | 64 ++----- .../bytes_fetchers/file/file_io.dart | 3 +- .../bytes_fetchers/file/file_stub.dart | 3 +- .../built_in/built_in_caching_provider.dart | 4 +- .../caching/built_in/impl/native/README.md | 0 .../caching/built_in/impl/native/native.dart | 2 +- .../impl/native/workers/size_reducer.dart | 2 +- .../workers/tile_and_size_monitor_writer.dart | 4 +- .../network/caching/built_in/impl/stub.dart | 0 .../caching/built_in/impl/web/web.dart | 0 .../network/caching/caching_provider.dart | 0 .../disabled/disabled_caching_provider.dart | 0 .../network/caching/tile_metadata.dart | 0 .../caching/tile_read_failure_exception.dart | 0 .../network/fetcher/consolidate_response.dart | 0 .../network/fetcher/network.dart | 35 ++-- .../raster/image_provider.dart | 0 .../raster/tile_data.dart | 10 +- .../tile_loader/raster/tile_loader.dart | 173 ++++++++++++++++++ .../tile_loader/source_tile_generators.dart | 36 ---- .../tile_generators/raster/generator.dart | 111 ----------- .../tile_loader/tile_loader.dart | 125 +------------ .../tile_loader/tile_source.dart | 30 ++- 35 files changed, 282 insertions(+), 411 deletions(-) delete mode 100644 lib/src/layer/modern_tile_layer/base_tile_loader.dart create mode 100644 lib/src/layer/modern_tile_layer/source_generators/source_generator.dart rename lib/src/layer/modern_tile_layer/{tile_loader => }/source_generators/wms.dart (97%) rename lib/src/layer/modern_tile_layer/{tile_loader => }/source_generators/xyz.dart (95%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/asset/asset.dart (92%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/bytes_fetcher.dart (73%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/file/file_io.dart (90%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/file/file_stub.dart (89%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart (97%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/impl/native/README.md (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/impl/native/native.dart (98%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart (97%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart (98%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/impl/stub.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/built_in/impl/web/web.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/caching_provider.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/tile_metadata.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/caching/tile_read_failure_exception.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/fetcher/consolidate_response.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/bytes_fetchers/network/fetcher/network.dart (91%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/raster/image_provider.dart (100%) rename lib/src/layer/modern_tile_layer/tile_loader/{tile_generators => }/raster/tile_data.dart (92%) create mode 100644 lib/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart delete mode 100644 lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart diff --git a/.gitignore b/.gitignore index f6b7bdaa8..370a69658 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. diff --git a/example/pubspec.lock b/example/pubspec.lock index eccc52347..8d34186a6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -116,7 +116,7 @@ packages: path: ".." relative: true source: path - version: "8.2.1" + version: "8.2.2" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/flutter_map.dart b/lib/flutter_map.dart index 7686c240d..72bfc53dd 100644 --- a/lib/flutter_map.dart +++ b/lib/flutter_map.dart @@ -29,11 +29,11 @@ export 'package:flutter_map/src/layer/attribution_layer/rich/widget.dart'; export 'package:flutter_map/src/layer/attribution_layer/simple.dart'; export 'package:flutter_map/src/layer/circle_layer/circle_layer.dart'; export 'package:flutter_map/src/layer/marker_layer/marker_layer.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart'; -export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart'; +export 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; export 'package:flutter_map/src/layer/overlay_image_layer/overlay_image_layer.dart'; export 'package:flutter_map/src/layer/polygon_layer/label/deprecated_placements.dart'; export 'package:flutter_map/src/layer/polygon_layer/label/placement_calculators/placement_calculator.dart'; diff --git a/lib/src/layer/modern_tile_layer/base_tile_data.dart b/lib/src/layer/modern_tile_layer/base_tile_data.dart index 7cf16b2f2..723fc1583 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_data.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_data.dart @@ -4,7 +4,7 @@ import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:meta/meta.dart'; /// Data associated with a particular tile coordinate which 'loads' -/// asynchronously +/// asynchronously. /// /// These are generated by the [BaseTileLayer.tileLoader] and consumed by the /// [BaseTileLayer.renderer]. @@ -32,7 +32,7 @@ abstract interface class BaseTileData { void dispose(); } -/// Wrapper for custom-shape data as a [BaseTileData] +/// Wrapper for custom-shape data as a [BaseTileData]. /// /// The data carried is usually made available asynchronously, for example as /// the result of an I/O operation or HTTP request. Alternatively, data may be diff --git a/lib/src/layer/modern_tile_layer/base_tile_layer.dart b/lib/src/layer/modern_tile_layer/base_tile_layer.dart index d3b882e3d..edcc590af 100644 --- a/lib/src/layer/modern_tile_layer/base_tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/base_tile_layer.dart @@ -3,8 +3,8 @@ import 'dart:collection'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:latlong2/latlong.dart'; import 'package:meta/meta.dart'; @@ -19,7 +19,7 @@ import 'package:meta/meta.dart'; /// is met. class BaseTileLayer extends StatefulWidget { final TileLayerOptions options; - final BaseTileLoader tileLoader; + final TileLoader tileLoader; final Widget Function( BuildContext context, Object layerKey, diff --git a/lib/src/layer/modern_tile_layer/base_tile_loader.dart b/lib/src/layer/modern_tile_layer/base_tile_loader.dart deleted file mode 100644 index 2e2187103..000000000 --- a/lib/src/layer/modern_tile_layer/base_tile_loader.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; -import 'package:meta/meta.dart'; - -/// Responsible for generating data ([D]) for tiles given the tile's -/// [TileCoordinates] and ambient [TileLayerOptions]. -/// -/// See [TileLoader] for an implementation which delegates its responsibility -/// into two parts. -@immutable -abstract interface class BaseTileLoader { - /// Generate data for the tile at [coordinates], with the ambient layer - /// [options]. - D call(TileCoordinates coordinates, TileLayerOptions options); -} diff --git a/lib/src/layer/modern_tile_layer/options.dart b/lib/src/layer/modern_tile_layer/options.dart index aa9934853..b233ebcdd 100644 --- a/lib/src/layer/modern_tile_layer/options.dart +++ b/lib/src/layer/modern_tile_layer/options.dart @@ -2,14 +2,14 @@ import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:meta/meta.dart'; /// Configuration of a [BaseTileLayer], which can be used by all parts of the -/// tile layer +/// tile layer. @immutable class TileLayerOptions { final double maxZoom; // TODO: Is this the same as the old `nativeMaxZoom`? final double zoomOffset; final bool zoomReverse; - /// Size in pixels of each tile image + /// Size in pixels of each tile image. /// /// Should be a positive power of 2. Defaults to 256px. /// @@ -18,7 +18,7 @@ class TileLayerOptions { final int tileDimension; /// Configuration of a [BaseTileLayer], which can be used by all parts of the - /// tile layer + /// tile layer. const TileLayerOptions({ this.maxZoom = double.infinity, this.zoomOffset = 0, diff --git a/lib/src/layer/modern_tile_layer/source_generators/source_generator.dart b/lib/src/layer/modern_tile_layer/source_generators/source_generator.dart new file mode 100644 index 000000000..75f19e6db --- /dev/null +++ b/lib/src/layer/modern_tile_layer/source_generators/source_generator.dart @@ -0,0 +1,13 @@ +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; +import 'package:meta/meta.dart'; + +/// Responsible for generating a tile's 'source' ([S]), which is later used to +/// get the tile's data, given the [TileCoordinates] and ambient +/// [TileLayerOptions]. +@immutable +abstract interface class SourceGenerator { + /// Generate the 'source' ([S]) for the tile at [coordinates], with the + /// ambient layer [options]. + S call(TileCoordinates coordinates, TileLayerOptions options); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart b/lib/src/layer/modern_tile_layer/source_generators/wms.dart similarity index 97% rename from lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart rename to lib/src/layer/modern_tile_layer/source_generators/wms.dart index f43c2c780..4a084f78a 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/wms.dart +++ b/lib/src/layer/modern_tile_layer/source_generators/wms.dart @@ -1,7 +1,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_map/src/geo/crs.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:flutter_map/src/misc/extensions.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart b/lib/src/layer/modern_tile_layer/source_generators/xyz.dart similarity index 95% rename from lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart rename to lib/src/layer/modern_tile_layer/source_generators/xyz.dart index c3b4b16d1..258fead1e 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart +++ b/lib/src/layer/modern_tile_layer/source_generators/xyz.dart @@ -1,8 +1,8 @@ import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_stub.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart index 1f6131157..9741ed1da 100644 --- a/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart +++ b/lib/src/layer/modern_tile_layer/tile_layers/raster/tile_layer.dart @@ -1,13 +1,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_layer.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; @@ -39,9 +38,9 @@ class _RasterTileLayerState extends State { @override Widget build(BuildContext context) => BaseTileLayer( options: widget.options, - tileLoader: TileLoader( - sourceGenerator: widget.sourceGenerator, - tileGenerator: RasterTileGenerator(bytesFetcher: widget.bytesFetcher), + tileLoader: RasterTileLoader( + sourceGenerator: const XYZSourceGenerator(uriTemplates: ['']), + bytesFetcher: widget.bytesFetcher, ), renderer: (context, layerKey, options, visibleTiles) => _RasterRenderer( layerKey: layerKey, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/asset/asset.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/asset/asset.dart similarity index 92% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/asset/asset.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/asset/asset.dart index 44d5ae59d..2183f68c8 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/asset/asset.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/asset/asset.dart @@ -1,8 +1,9 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; /// A [SourceBytesFetcher] which fetches a URI from the app's shipped assets. /// @@ -35,6 +36,7 @@ class AssetBytesFetcher implements SourceBytesFetcher> { required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, + BytesReceivedCallback? bytesLoadedCallback, }) { final bundle = assetBundle ?? rootBundle; return fetchFromSourceIterable( diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart similarity index 73% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart index 7443487a3..81f7e8d54 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; import 'package:logger/logger.dart'; @@ -11,16 +11,12 @@ import 'package:logger/logger.dart'; /// desired resource using a supplied [BytesToResourceTransformer]. /// /// Implementers should implement longer-term caching where necessary, or -/// delegate to a cacher. Note that [TileGenerator]s may also perform caching of +/// delegate to a cacher. Note that [TileLoader]s may also perform caching of /// the resulting resource, often in the short-term - such as the -/// [RasterTileGenerator] using the Flutter [ImageCache]. -/// -/// Implementations which work with the [RasterTileGenerator] should consider -/// mixing-in [ImageChunkEventsSupport]. +/// [RasterTileLoader] using the Flutter [ImageCache]. abstract interface class SourceBytesFetcher { - /// {@template fm.tilelayer.tilebytesfetcher.call} /// Fetches a tile's bytes based on its source ([S]), transforming it into a - /// desired resource ([R]) using a supplied transformer. + /// desired resource ([R]) using a supplied [transformer]. /// /// The [abortSignal] completes when the tile is no longer required. If /// possible, any ongoing work (such as an HTTP request) should be aborted. @@ -29,41 +25,15 @@ abstract interface class SourceBytesFetcher { /// /// See [BytesToResourceTransformer] for more information about handling the /// [transformer]. - /// {@endtemplate} - FutureOr call({ - required S source, - required Future abortSignal, - required BytesToResourceTransformer transformer, - }); -} - -/// Allows a [SourceBytesFetcher] to integrate more closely with the raster tile -/// stack by reporting progress events to the underlying [ImageProvider]. -abstract mixin class ImageChunkEventsSupport - implements SourceBytesFetcher { - /// Redirects to [withImageChunkEventsSink]. - @override - @nonVirtual - FutureOr call({ - required S source, - required Future abortSignal, - required BytesToResourceTransformer transformer, - }) => - withImageChunkEventsSink( - source: source, - abortSignal: abortSignal, - transformer: transformer, - ); - - /// {@macro fm.tilelayer.tilebytesfetcher.call} /// - /// [chunkEvents] should be used when consolidating a stream of bytes to - /// report progress notifications to the underlying [ImageProvider]. - FutureOr withImageChunkEventsSink({ + /// [bytesLoadedCallback] (if provided), may be called as/when bytes are + /// loaded (before [transformer] is called). See [BytesReceivedCallback] for + /// more information. + FutureOr call({ required S source, required Future abortSignal, required BytesToResourceTransformer transformer, - StreamSink? chunkEvents, + BytesReceivedCallback? bytesLoadedCallback, }); } @@ -81,8 +51,8 @@ class TileAbortedException implements Exception { String toString() => 'TileAbortedException: $source'; } -/// Callback provided to a [SourceBytesFetcher] by a root [TileGenerator], -/// which converts fetched bytes into the desired [Resource]. +/// Callback provided to a [SourceBytesFetcher] by a [TileLoader], which +/// converts fetched bytes into the desired [Resource]. /// /// This may throw if the bytes could not be correctly transformed, for example /// because they were corrupted or otherwise undecodable. In this case, it is @@ -92,10 +62,9 @@ class TileAbortedException implements Exception { /// /// --- /// -/// Whilst it is the [SourceBytesFetcher]s or [TileGenerator]s responsibility to -/// implement long-term caching where necessary, other parts of the stack (such -/// as the [TileGenerator]) may also perform short-term caching, which requires -/// a key. +/// Whilst it is the [SourceBytesFetcher]s responsibility to implement long-term +/// caching where necessary, other parts of the stack (such as the [TileLoader]) +/// may also perform short-term caching, which requires a key. /// /// If the resulting resource differs to what is expected and used as the key /// - for example, in the case of a fallback being used whilst the only stable @@ -103,6 +72,9 @@ class TileAbortedException implements Exception { /// not be reused under the key (i.e. not cached). This is done by setting /// [allowReuse] `false`. /// +/// For example, the [TileSource] object is suitable as a key - but where one +/// of its [TileSource.fallbackUris] was used, [allowReuse] must be set `false`. +/// /// Implementers should make the default of [allowReuse] `true`. typedef BytesToResourceTransformer = FutureOr Function(Uint8List bytes, {bool allowReuse}); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_io.dart similarity index 90% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_io.dart index c732542f0..5269e8294 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_io.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_io.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; /// A [SourceBytesFetcher] which fetches a URI from the local filesystem. /// @@ -16,6 +16,7 @@ class FileBytesFetcher implements SourceBytesFetcher> { required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, + BytesReceivedCallback? bytesLoadedCallback, }) => fetchFromSourceIterable( (uri, transformer, isFirst) => diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_stub.dart similarity index 89% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_stub.dart index a06e5ba11..6bd5cfd7b 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/file/file_stub.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/file/file_stub.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; /// A [SourceBytesFetcher] which fetches from the local filesystem. /// @@ -16,6 +16,7 @@ class FileBytesFetcher implements SourceBytesFetcher> { required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, + BytesReceivedCallback? bytesLoadedCallback, }) { throw UnsupportedError( '`FileBytesFetcher` is unsupported on non-native platforms', diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart similarity index 97% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart index 19d88e9ee..f162cff5f 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart @@ -1,8 +1,8 @@ import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/stub.dart' +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart' if (dart.library.io) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/native/native.dart' if (dart.library.js_interop) 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_fetchers/bytes_fetchers/network/caching/built_in/impl/web/web.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart'; import 'package:uuid/data.dart'; import 'package:uuid/rng.dart'; import 'package:uuid/uuid.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/README.md b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/README.md similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/README.md rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/README.md diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart similarity index 98% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart index bbe129a17..2b749c207 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart @@ -6,7 +6,7 @@ import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart similarity index 97% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart index 24bc8953c..7ef159f80 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart similarity index 98% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart index 4dc65b7e6..7b04e7ec2 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart @@ -5,8 +5,8 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'package:flutter_map/flutter_map.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/size_reducer.dart'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/stub.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/web/web.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/impl/web/web.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_read_failure_exception.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_read_failure_exception.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/consolidate_response.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/consolidate_response.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/consolidate_response.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/consolidate_response.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart similarity index 91% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart rename to lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart index ea7bf3c50..041899644 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart @@ -3,13 +3,13 @@ import 'dart:io' show HttpHeaders, HttpDate, HttpStatus; // web safe! import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/caching_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_metadata.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/consolidate_response.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_read_failure_exception.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/consolidate_response.dart'; import 'package:http/http.dart'; import 'package:http/retry.dart'; import 'package:logger/logger.dart'; @@ -23,9 +23,7 @@ import 'package:logger/logger.dart'; /// next URI is used as a fallback if available, and so on. /// {@endtemplate} @immutable -class NetworkBytesFetcher - with ImageChunkEventsSupport> - implements SourceBytesFetcher> { +class NetworkBytesFetcher implements SourceBytesFetcher> { /// HTTP headers to send with each request. final Map headers; @@ -116,18 +114,18 @@ class NetworkBytesFetcher } @override - Future withImageChunkEventsSink({ + Future call({ required Iterable source, required Future abortSignal, required BytesToResourceTransformer transformer, - StreamSink? chunkEvents, + BytesReceivedCallback? bytesLoadedCallback, }) => fetchFromSourceIterable( (uri, transformer, isFirst) => fetchSingle( uri: uri, abortSignal: abortSignal, transformer: transformer, - chunkEvents: chunkEvents, + bytesLoadedCallback: bytesLoadedCallback, ), source: source, transformer: transformer, @@ -143,7 +141,7 @@ class NetworkBytesFetcher required String uri, required Future abortSignal, required BytesToResourceTransformer transformer, - StreamSink? chunkEvents, + BytesReceivedCallback? bytesLoadedCallback, }) async { final parsedUri = Uri.parse(uri); @@ -164,14 +162,7 @@ class NetworkBytesFetcher final bytes = await consolidateStreamedResponseBytes( response, - onBytesReceived: chunkEvents == null - ? null - : (cumulative, total) => chunkEvents.add( - ImageChunkEvent( - cumulativeBytesLoaded: cumulative, - expectedTotalBytes: total, - ), - ), + onBytesReceived: bytesLoadedCallback, ); return (bytes: bytes, response: response); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/image_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/raster/image_provider.dart similarity index 100% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/image_provider.dart rename to lib/src/layer/modern_tile_layer/tile_loader/raster/image_provider.dart diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart b/lib/src/layer/modern_tile_layer/tile_loader/raster/tile_data.dart similarity index 92% rename from lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart rename to lib/src/layer/modern_tile_layer/tile_loader/raster/tile_data.dart index f369a3dc0..a1bd91a85 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/raster/tile_data.dart @@ -2,13 +2,11 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart'; import 'package:meta/meta.dart'; -/// Raster tile data associated with a particular tile coordinate -/// -/// This is used for communication between the [RasterTileGenerator] and the -/// raster tile renderer. +/// Raster tile data associated with a particular tile, used for communication +/// between the [RasterTileLoader] and the raster tile renderer. /// /// It is not usually necessary to consume this externally. class RasterTileData implements BaseTileData { @@ -17,7 +15,7 @@ class RasterTileData implements BaseTileData { final void Function() _dispose; - /// Raster tile data associated with a particular tile coordinate + /// Raster tile data associated with a particular tile. RasterTileData({required this.image, required void Function() dispose}) : _dispose = dispose; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart new file mode 100644 index 000000000..5774fe3bc --- /dev/null +++ b/lib/src/layer/modern_tile_layer/tile_loader/raster/tile_loader.dart @@ -0,0 +1,173 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/wms.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/image_provider.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/raster/tile_data.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; +import 'package:meta/meta.dart'; + +/// A tile loader implementation which: +/// +/// 1. delegates to a [SourceGenerator] to output the tile's 'source' ([S]) +/// 2. fetches the 'source's bytes by delegating to a [SourceBytesFetcher] +/// 3. outputs a [RasterTileData], containing the [ImageProvider] created by +/// decoding the tile's image bytes +/// +/// The source ([S]) is used as the short-term caching key for the +/// [ImageProvider] (in Flutter's [ImageCache]) - therefore, it must meet the +/// necessary conditions as described by [ImageProvider.obtainKey] +/// (particularly, it must be an object with a useful equality defined). The +/// [TileSource] generated by the [XYZSourceGenerator] & [WMSSourceGenerator] +/// meets this requirement, although it is not a requirement to use this. +@immutable +final class RasterTileLoader + implements TileLoader { + /// Generates a 'source' ([S]) for a tile, given its [TileCoordinates] & the + /// ambient [TileLayerOptions] + /// + /// For example, see [XYZSourceGenerator]. + final SourceGenerator sourceGenerator; + + /// The delegate which provides the bytes for the this tile, based on its + /// 'source' ([S]). + /// + /// This may not be called for every tile, if the tile was already present in + /// the ambient [ImageCache]. + /// + /// For example, see [NetworkBytesFetcher]. + final SourceBytesFetcher bytesFetcher; + + /// Tile loader which loads raster image tiles + const RasterTileLoader({ + required this.sourceGenerator, + required this.bytesFetcher, + }); + + @override + RasterTileData call(TileCoordinates coordinates, TileLayerOptions options) { + final source = sourceGenerator(coordinates, options); + + final abortTrigger = Completer(); + + Future imageDelegate( + KeyedDelegatedImage key, { + required StreamSink chunkEvents, + required ImageDecoderCallback decode, + }) async { + void evict() => scheduleMicrotask( + () => PaintingBinding.instance.imageCache.evict(key), + ); + + Future transformer(Uint8List bytes, {bool allowReuse = true}) { + if (!allowReuse) evict(); + return ImmutableBuffer.fromUint8List(bytes).then(decode); + } + + try { + return await bytesFetcher( + source: source, + abortSignal: abortTrigger.future, + transformer: transformer, + bytesLoadedCallback: (c, t) => chunkEvents.add( + ImageChunkEvent(cumulativeBytesLoaded: c, expectedTotalBytes: t), + ), + ); + } on TileAbortedException { + evict(); + return ImmutableBuffer.fromUint8List(transparentImage).then(decode); + } on Exception { + evict(); + rethrow; + } + } + + return RasterTileData( + image: KeyedDelegatedImage( + key: source, + delegate: imageDelegate, + ), + dispose: abortTrigger.complete, + )..load(); + } + + /// [Uint8List] that forms a fully transparent image. + static final transparentImage = Uint8List.fromList([ + 0x89, + 0x50, + 0x4E, + 0x47, + 0x0D, + 0x0A, + 0x1A, + 0x0A, + 0x00, + 0x00, + 0x00, + 0x0D, + 0x49, + 0x48, + 0x44, + 0x52, + 0x00, + 0x00, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x01, + 0x08, + 0x06, + 0x00, + 0x00, + 0x00, + 0x1F, + 0x15, + 0xC4, + 0x89, + 0x00, + 0x00, + 0x00, + 0x0A, + 0x49, + 0x44, + 0x41, + 0x54, + 0x78, + 0x9C, + 0x63, + 0x00, + 0x01, + 0x00, + 0x00, + 0x05, + 0x00, + 0x01, + 0x0D, + 0x0A, + 0x2D, + 0xB4, + 0x00, + 0x00, + 0x00, + 0x00, + 0x49, + 0x45, + 0x4E, + 0x44, + 0xAE, + 0x42, + 0x60, + 0x82, + ]); +} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart b/lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart deleted file mode 100644 index 6f081cea2..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; -import 'package:meta/meta.dart'; - -/// Generates a tile's 'source' based on its own properties, the ambient -/// [TileLayerOptions], and the tile's [TileCoordinates]. -/// -/// The source type must be consumable by the [TileGenerator] used. -@immutable -abstract interface class SourceGenerator { - /// Generates a tile's 'source' based on its own properties, the ambient - /// [TileLayerOptions], and the tile's [TileCoordinates]. - /// - /// See documentation on [SourceGenerator] for more information. - S call(TileCoordinates coordinates, TileLayerOptions options); -} - -/// Generates a tile's data ([D]) based on its 'source' ([S]). -/// -/// The source type is set by the [SourceGenerator]. The fetcher does not have -/// access to the ambient [TileLayerOptions], therefore any required options -/// must appear in the source. -/// -/// To promote flexibility and re-usability, it is recommended to further -/// delegate parts of the tile generator, such as I/O fetching operations. It -/// may depend on a [SourceBytesFetcher] to do this. -@immutable -abstract interface class TileGenerator { - /// Generates a tile's data ([D]) based on its 'source' ([S]). - /// - /// See documentation on [TileGenerator] for more information. - D call(S source); -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart deleted file mode 100644 index 4222b07f4..000000000 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:async'; -import 'dart:ui'; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/image_provider.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_loader.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_source.dart'; - -/// A tile generator which delgates fetching of a raster image's bytes to -/// a [SourceBytesFetcher], then creates an [ImageProvider] by decoding the -/// bytes. -/// -/// The source ([S]) is used as the short-term caching key for the -/// [ImageProvider] (and Flutter's [ImageCache]) - therefore, it must meet the -/// necessary conditions as described by [ImageProvider.obtainKey] -/// (particularly, it must be an object with a useful equality defined). -/// -/// This is used instead of directly sending the bytes to the renderer, as it -/// hooks into the Flutter image cache, meaning that tiles are cached in memory. -/// Additionally, it is easier for the renderer canvas to work with. -/// -/// --- -/// -/// The pre-provided [SourceBytesFetcher]s (such as [NetworkBytesFetcher]) -/// consume [Iterable]s of [String]s as a source ([S]), and use the transformer -/// provided by this object to output a [RasterTileData]. This is so that they -/// may be used outside of the context of [TileSource] & [RasterTileGenerator] -/// (for example, in a different stack). -/// -/// It is not suitable to use an [Iterable] directly, as it does not meet the -/// criteria for a key. Instead, the pre-provided [SourceGenerator]s (such -/// as [XYZSourceGenerator]) output a [TileSource], which meets all necessary -/// requirements. -/// -/// However, this fetcher could be used with any source. For example, if a -/// different [SourceBytesFetcher] is used, it doesn't necessarily need to use -/// [TileSource] or any other contract described above. -class RasterTileGenerator - implements TileGenerator { - /// The delegate which provides the bytes for the this tile. - /// - /// This may not be called for every tile, if the tile was already present in - /// the ambient [ImageCache]. - final SourceBytesFetcher bytesFetcher; - - /// A tile generator which delgates fetching of a raster image's bytes to a - /// [SourceBytesFetcher], then creates an [ImageProvider] by decoding the - /// bytes. - const RasterTileGenerator({required this.bytesFetcher}); - - @override - RasterTileData call(S source) { - final abortTrigger = Completer(); - - Future imageDelegate( - KeyedDelegatedImage key, { - required StreamSink chunkEvents, - required ImageDecoderCallback decode, - }) async { - void evict() => scheduleMicrotask( - () => PaintingBinding.instance.imageCache.evict(key), - ); - - Future transformer(Uint8List bytes, {bool allowReuse = true}) { - if (!allowReuse) evict(); - return ImmutableBuffer.fromUint8List(bytes).then(decode); - } - - try { - // Must await to handle errors - if (bytesFetcher - case ImageChunkEventsSupport( - withImageChunkEventsSink: final bytesFetcher - )) { - return await bytesFetcher( - source: source, - abortSignal: abortTrigger.future, - transformer: transformer, - chunkEvents: chunkEvents, - ); - } - return await bytesFetcher( - source: source, - abortSignal: abortTrigger.future, - transformer: transformer, - ); - } on TileAbortedException { - evict(); - return ImmutableBuffer.fromUint8List(TileLoader.transparentImage) - .then(decode); - } on Exception { - evict(); - rethrow; - } - } - - return RasterTileData( - image: KeyedDelegatedImage( - key: source, - delegate: imageDelegate, - ), - dispose: abortTrigger.complete, - )..load(); - } -} diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart index a4072c94a..cc507e534 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_loader.dart @@ -1,126 +1,13 @@ -import 'dart:typed_data'; - import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_data.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/base_tile_loader.dart'; import 'package:flutter_map/src/layer/modern_tile_layer/options.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; import 'package:flutter_map/src/layer/tile_layer/tile_coordinates.dart'; import 'package:meta/meta.dart'; -/// Default implementation of a tile loader, which delegates loading of tile -/// data to two seperate stages: -/// -/// 1. The [sourceGenerator] uses a tile's [TileCoordinates] & the ambient -/// [TileLayerOptions] to generate an object, describing the tile's 'source' -/// ([S]) -/// -/// 2. The [tileGenerator] uses this 'source' to generate an output -/// [BaseTileData] ([D]) +/// Responsible for generating a tile's data ([D]), given the [TileCoordinates] +/// and ambient [TileLayerOptions]. @immutable -final class TileLoader - implements BaseTileLoader { - /// Generates a 'source' ([S]) for a tile given its [TileCoordinates] & the - /// ambient [TileLayerOptions] - /// - /// For example, see [XYZSourceGenerator]. - final SourceGenerator sourceGenerator; - - /// Generates a tile's data ([D]) based on the 'source' generated by - /// [sourceGenerator] - /// - /// For example, see [RasterTileGenerator]. - final TileGenerator tileGenerator; - - /// Create a tile loader from a source generator & tile generator. - const TileLoader({ - required this.sourceGenerator, - required this.tileGenerator, - }); - - @override - D call(TileCoordinates coordinates, TileLayerOptions options) => - tileGenerator(sourceGenerator(coordinates, options)); - - @override - bool operator ==(Object other) => - identical(this, other) || - (other is TileLoader && - other.tileGenerator == tileGenerator && - other.sourceGenerator == sourceGenerator); - - @override - int get hashCode => Object.hash(sourceGenerator, tileGenerator); - - /// [Uint8List] that forms a fully transparent image. - static final transparentImage = Uint8List.fromList([ - 0x89, - 0x50, - 0x4E, - 0x47, - 0x0D, - 0x0A, - 0x1A, - 0x0A, - 0x00, - 0x00, - 0x00, - 0x0D, - 0x49, - 0x48, - 0x44, - 0x52, - 0x00, - 0x00, - 0x00, - 0x01, - 0x00, - 0x00, - 0x00, - 0x01, - 0x08, - 0x06, - 0x00, - 0x00, - 0x00, - 0x1F, - 0x15, - 0xC4, - 0x89, - 0x00, - 0x00, - 0x00, - 0x0A, - 0x49, - 0x44, - 0x41, - 0x54, - 0x78, - 0x9C, - 0x63, - 0x00, - 0x01, - 0x00, - 0x00, - 0x05, - 0x00, - 0x01, - 0x0D, - 0x0A, - 0x2D, - 0xB4, - 0x00, - 0x00, - 0x00, - 0x00, - 0x49, - 0x45, - 0x4E, - 0x44, - 0xAE, - 0x42, - 0x60, - 0x82, - ]); +abstract interface class TileLoader { + /// Generate data ([D]) for the tile at [coordinates], with the ambient layer + /// [options]. + D call(TileCoordinates coordinates, TileLayerOptions options); } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart index 41f80d488..6a5ce3001 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/tile_source.dart @@ -1,27 +1,25 @@ -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_generators/xyz.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/source_tile_generators.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/bytes_fetcher.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/bytes_fetchers/network/fetcher/network.dart'; -import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/tile_generators/raster/generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/source_generator.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/source_generators/xyz.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/bytes_fetcher.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart'; import 'package:meta/meta.dart'; -/// Data class for communicating URIs between [SourceGenerator]s -/// (such as [XYZSourceGenerator]) and [TileGenerator]s (such as -/// [RasterTileGenerator]), with ability to be used directly as a short-term -/// cache key* and by [SourceBytesFetcher]s (such as [NetworkBytesFetcher]). +/// Data class for communicating URIs returned by some [SourceGenerator] +/// implementations (such as [XYZSourceGenerator]). /// -/// This carries a [primaryUri] and potentially multiple ordered [fallbackUris]. +/// Carries a [primaryUri] and potentially multiple ordered [fallbackUris]. /// When iterated, this will first yield the [primaryUri], followed by any /// [fallbackUris] in order. /// +/// This is suitable to be used directly as a short-term cache key*. This may be +/// consumed directly by some [SourceBytesFetcher] implementations (such as +/// [NetworkBytesFetcher]). +/// /// > [!WARNING] /// > The equality of these objects depends only on [primaryUri]. /// > Therefore, where used as a short-term cache key, resources at -/// > [fallbackUris] must not automatically be re-used/cached under the +/// > [fallbackUris] **must not** automatically be re-used/cached under the /// > [primaryUri]. -/// -/// This is provided as it is used internally and may be used externally by -/// layer implementations for convienience, however it is not required. @immutable class TileSource extends Iterable { /// Primary URI of the tile. @@ -36,8 +34,8 @@ class TileSource extends Iterable { /// This may be empty or not provided. final Iterable? fallbackUris; - /// Construct a data class for communicating URIs between [SourceGenerator]s - /// and [TileGenerator]s. + /// Construct a data class for communicating URIs returned by some + /// [SourceGenerator] implementations. const TileSource(this.primaryUri, {this.fallbackUris}); @override From cac5393f9675aba09bb8f5f2dc9de6e4ef62fffa Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 12 Aug 2025 14:31:39 +0100 Subject: [PATCH 11/13] Fixed leaky abstraction of caching metadata by decomposing metadata classes and using interfaces for caching providers to support put ops Allow stale cached tiles to be used when network fetch fails in modern implementation --- example/pubspec.lock | 2 +- .../built_in/built_in_caching_provider.dart | 18 ++- .../caching/built_in/impl/native/native.dart | 16 +-- .../workers/tile_and_size_monitor_writer.dart | 8 +- .../network/caching/built_in/impl/stub.dart | 8 +- .../caching/built_in/impl/web/web.dart | 2 +- .../network/caching/caching_provider.dart | 68 ++++++++--- .../disabled/disabled_caching_provider.dart | 15 ++- .../network/caching/tile_metadata.dart | 97 +++++++++++----- .../network/fetcher/network.dart | 109 ++++++++++++++---- .../image_provider/image_provider.dart | 79 ++++++++++--- 11 files changed, 317 insertions(+), 105 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index 8d34186a6..eccc52347 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -116,7 +116,7 @@ packages: path: ".." relative: true source: path - version: "8.2.2" + version: "8.2.1" flutter_test: dependency: "direct dev" description: flutter diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart index f162cff5f..19f98c6c5 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/built_in_caching_provider.dart @@ -7,24 +7,27 @@ import 'package:uuid/data.dart'; import 'package:uuid/rng.dart'; import 'package:uuid/uuid.dart'; -/// Simple built-in map caching using an I/O storage mechanism, for native -/// (non-web) platforms only +/// Simple HTTP-based built-in map caching using an I/O storage mechanism, for +/// native (non-web) platforms only /// /// Stores tiles as files identified with keys, containing some metadata headers /// followed by the tile bytes, alongside a file used to track the size of the /// cache. /// +/// This is enabled by default in flutter_map, when using the +/// [NetworkBytesFetcher]. Consumers must support putting +/// [HttpControlledCachedTileMetadata] to use this provider. +/// /// Usually uses HTTP headers to determine tile freshness, although /// `overrideFreshAge` can override this. /// -/// This is enabled by default in flutter_map, when using the -/// [NetworkBytesFetcher]. -/// /// It is safe to use all public methods when running on web - they will noop. /// /// For more information, see the online documentation. abstract interface class BuiltInMapCachingProvider - implements MapCachingProvider { + implements + MapCachingProvider, + PutTileAndMetadataCapability { /// If an instance exists, return it, otherwise create a new instance /// /// The provided configuration will only be respected if an instance does not @@ -149,4 +152,7 @@ abstract interface class BuiltInMapCachingProvider static String uuidTileKeyGenerator(String url) => _uuid.v5(Namespace.url.value, url); static final _uuid = Uuid(goptions: GlobalOptions(MathRNG())); + + @override + Future?> getTile(String url); } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart index 2b749c207..6aceeb21b 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/native.dart @@ -83,7 +83,7 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { sizeMonitorFilePath: sizeMonitorFilePath, maxCacheSize: maxCacheSize, ), - debugName: '[flutter_map: cache] Tile & Size Monitor Writer', + debugName: '[flutter_map: BIC] Tile & Size Monitor Writer', ); workerReceivePort.listen( @@ -111,7 +111,7 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { late final void Function( String path, - CachedMapTileMetadata metadata, + HttpControlledCachedTileMetadata metadata, Uint8List? tileBytes, ) _writeTileFile; late final void Function() @@ -133,7 +133,9 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { } @override - Future getTile(String url) async { + Future?> getTile( + String url, + ) async { final key = tileKeyGenerator(url); final tileFile = File( p.join(_cacheDirectoryPath ?? await _cacheDirectoryPathReady.future, key), @@ -210,7 +212,7 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { } return ( - metadata: CachedMapTileMetadata( + metadata: HttpControlledCachedTileMetadata( staleAt: staleAt, lastModified: lastModified, etag: etag, @@ -230,9 +232,9 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { } @override - Future putTile({ + Future putTileWithMetadata({ required String url, - required CachedMapTileMetadata metadata, + required HttpControlledCachedTileMetadata metadata, Uint8List? bytes, }) async { if (readOnly) return; @@ -246,7 +248,7 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { _writeTileFile( path, overrideFreshAge != null - ? CachedMapTileMetadata( + ? HttpControlledCachedTileMetadata( staleAt: DateTime.timestamp().add(overrideFreshAge!), lastModified: metadata.lastModified, etag: metadata.etag, diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart index 7b04e7ec2..f7a433ac2 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/native/workers/tile_and_size_monitor_writer.dart @@ -124,7 +124,7 @@ Future tileAndSizeMonitorWriterWorker( sizeMonitorFilePath: sizeMonitorFilePath, minSizeToDelete: minSizeToDelete, ), - debugName: '[flutter_map: cache] Size Reducer', + debugName: '[flutter_map: BIC] Size Reducer', ); runSizeReducer( @@ -140,10 +140,10 @@ Future tileAndSizeMonitorWriterWorker( final allocInt64BufferTileWrite = Uint8List(8); final allocUint32BufferTileWrite = Uint8List(4); final allocUint16BufferTileWrite = Uint8List(2); - final asciiEncoder = const AsciiEncoder(); + const asciiEncoder = AsciiEncoder(); void writeTile({ required final String path, - required final CachedMapTileMetadata metadata, + required final HttpControlledCachedTileMetadata metadata, Uint8List? tileBytes, }) { final tileFile = File(path); @@ -312,7 +312,7 @@ Future tileAndSizeMonitorWriterWorker( if (val case ( :final String path, - :final CachedMapTileMetadata metadata, + :final HttpControlledCachedTileMetadata metadata, :final Uint8List? tileBytes, )) { writeTile(path: path, metadata: metadata, tileBytes: tileBytes); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart index ff1e250e9..3208f3cca 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/stub.dart @@ -34,12 +34,14 @@ class BuiltInMapCachingProviderImpl implements BuiltInMapCachingProvider { external bool get isSupported; @override - external Future getTile(String url); + external Future?> getTile( + String url, + ); @override - external Future putTile({ + external Future putTileWithMetadata({ required String url, - required CachedMapTileMetadata metadata, + required HttpControlledCachedTileMetadata metadata, Uint8List? bytes, }); } diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart index 5d2fc3252..33bc501f7 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/built_in/impl/web/web.dart @@ -3,7 +3,7 @@ import 'package:meta/meta.dart'; @internal class BuiltInMapCachingProviderImpl - with DisabledMapCachingProvider + with DisabledMapCachingProvider implements BuiltInMapCachingProvider { final String? cacheDirectory; final int? maxCacheSize; diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart index bf5c2fcd3..c4dfbf261 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/caching_provider.dart @@ -1,26 +1,37 @@ import 'dart:typed_data'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart'; +import 'package:meta/meta.dart'; -/// Provides tile caching facilities +/// Provides tile caching facilities. /// -/// The [CachedMapTileMetadata] object is used to store metadata alongside -/// cached tiles. Its intended purpose is primarily for caching based on HTTP -/// headers - however, this is not a requirement. +/// A cached tile is considered to be at least bytes representing the tile +/// resource, usually paired with metadata about the tile resource. +/// +/// Implementations usually mix-in/implement at least one of [PutTileCapability] +/// and/or [PutTileAndMetadataCapability], to allow tiles to be added to the +/// cache by compatible external consumers. +/// +/// To be supported by the [NetworkBytesFetcher], at least one of the following +/// must be mixed-in/implemented: +/// * [PutTileCapability] +/// * [PutTileAndMetadataCapability] with a metadata type parameter of +/// [HttpControlledCachedTileMetadata] abstract interface class MapCachingProvider { /// Whether this caching provider is "currently supported": whether the /// tile provider should attempt to use it, or fallback to a non-caching - /// alternative + /// alternative. /// - /// Tile providers must not call [getTile] or [putTile] if this is `false`. - /// [getTile] and [putTile] should gracefully throw if this is `false`. - /// This should not throw. + /// Tile providers must not use any other members if this is `false`. Where + /// possible, other methods should gracefully throw if this is `false`. This + /// should not throw. /// /// If this is always `false`, consider mixing in or using /// [DisabledMapCachingProvider] directly. bool get isSupported; - /// Retrieve a tile from the cache, if it exists + /// Retrieve a tile from the cache, if it exists. /// /// Returns `null` if the tile was not present in the cache. /// @@ -31,18 +42,49 @@ abstract interface class MapCachingProvider { /// Tile providers should anticipate these exceptions and fallback to a /// non-caching alternative, wherever possible repairing or replacing the tile /// with a fresh & valid one. + /// + /// If this method throws an error/exception other than + /// [CachedMapTileReadFailure], consumers should rethrow the error. + /// + /// If the tile is available, the metadata at least gives an indication as to + /// whether the tile is 'stale'. The metadata may also be a more informative + /// subclass, such as [HttpControlledCachedTileMetadata]. Future getTile(String url); +} + +/// Allows a [MapCachingProvider] to have tiles added externally, without +/// metadata. +abstract interface class PutTileCapability implements MapCachingProvider { + /// Add or update a tile in the cache. + /// + /// [bytes] is required if the tile is not already cached. The behaviour is + /// implementation specific if bytes are not supplied when required. + void putTile({ + required String url, + Uint8List? bytes, + }); +} - /// Add or update a tile in the cache +/// Allows a [MapCachingProvider] to have tiles added externally, with +/// metadata. +abstract interface class PutTileAndMetadataCapability + implements MapCachingProvider { + /// Add or update a tile & its metadata in the cache /// /// [bytes] is required if the tile is not already cached. The behaviour is /// implementation specific if bytes are not supplied when required. - Future putTile({ + void putTileWithMetadata({ required String url, - required CachedMapTileMetadata metadata, + required IM metadata, Uint8List? bytes, }); } /// A tile's bytes and metadata returned from [MapCachingProvider.getTile] -typedef CachedMapTile = ({Uint8List bytes, CachedMapTileMetadata metadata}); +/// +/// Depending on the caching provider, `metadata` may be a more specific subtype. +@optionalTypeArgs +typedef CachedMapTile = ({ + Uint8List bytes, + OM metadata, +}); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart index cb947dbe4..6ae24ef71 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/disabled/disabled_caching_provider.dart @@ -3,7 +3,11 @@ import 'dart:typed_data'; import 'package:flutter_map/flutter_map.dart'; /// Caching provider which disables built-in caching -mixin class DisabledMapCachingProvider implements MapCachingProvider { +mixin class DisabledMapCachingProvider + implements + MapCachingProvider, + PutTileCapability, + PutTileAndMetadataCapability { /// Disable built-in map caching const DisabledMapCachingProvider(); @@ -17,7 +21,14 @@ mixin class DisabledMapCachingProvider implements MapCachingProvider { @override Never putTile({ required String url, - required CachedMapTileMetadata metadata, + Uint8List? bytes, + }) => + throw UnsupportedError('Must not be called if `isSupported` is `false`'); + + @override + Never putTileWithMetadata({ + required String url, + required IM metadata, Uint8List? bytes, }) => throw UnsupportedError('Must not be called if `isSupported` is `false`'); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart index 937577914..85fbb7cd1 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart @@ -5,31 +5,78 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:logger/logger.dart'; -/// Metadata about a tile cached with a [MapCachingProvider] +/// Metadata about a tile cached with a [MapCachingProvider]. /// -/// Caching is usually determined with HTTP headers. However, if a specific -/// implementation chooses to, it can solely use [isStale] and set the other -/// properties to `null`. +/// For output, implementers of a [MapCachingProvider] may: +/// * implement this interface +/// * use or extend [HttpControlledCachedTileMetadata], if the provider makes +/// use of HTTP headers +/// * construct this directly, if the provider does not consider HTTP caching /// -/// External usage of this class is not usually necessary. It is visible so -/// other tile providers may make use of it. +/// For input, implementers of a [MapCachingProvider] may: +/// * accept no metadata/ignore any provided metadata +/// * accept a subclass implementation if metadata is useful +/// +/// Consumers of a [MapCachingProvider] should: +/// * (preferably) be accepting of many providers, and +/// * expect this interface to be returned as metadata +/// * provide whatever metadata is likely to be useful (such as +/// [HttpControlledCachedTileMetadata] to be compatible with the +/// [BuiltInMapCachingProvider]), and ensure that type safety is preserved +/// * or, tie to a specific provider implementation and specific metadata +/// implementation @immutable -class CachedMapTileMetadata { - /// Create new metadata - const CachedMapTileMetadata({ +interface class CachedTileMetadata { + /// Create new non-specific metadata. + /// + /// This method is likely only to be useful for [MapCachingProvider] + /// implementations as an output. + const CachedTileMetadata({required this.isStale}); + + /// Whether to consider this tile as stale. + /// + /// If `true`, consumers should: + /// * attempt to update the tile + /// * only use the tile as a fallback + /// + /// The meaning & interpretation of `false` depends on the implementation of + /// the consumer and the caching provider. For example, it may indicate that + /// the tile should be used without deferring to a second source (network), or + /// the network may still be attempted anyway - and this may be set on either + /// implementation. + final bool isStale; +} + +/// Implementation of [CachedTileMetadata] which uses properties commonly found +/// in the HTTP Caching specification. +/// +/// [isStale] is determined by whether the tile is 'stale', as determined +/// by [staleAt] (which may be calculated from multiple HTTP headers). +/// +/// [lastModified] & [etag] are common metadata components which caches may +/// choose to support, which makes HTTP Caching more efficient. +@immutable +base class HttpControlledCachedTileMetadata implements CachedTileMetadata { + /// Create new metadata based on properties commonly found in the HTTP Caching + /// specification. + /// + /// If constructing from a HTTP response, consider + /// [HttpControlledCachedTileMetadata.fromHttpHeaders] to automatically + /// calculate and parse these properties. + const HttpControlledCachedTileMetadata({ required this.staleAt, - required this.lastModified, - required this.etag, + this.lastModified, + this.etag, }); - /// Create new metadata based off an HTTP response's headers + /// Create new metadata based off an HTTP response's headers. /// /// Where a response does not include enough information to calculate the /// freshness age, [fallbackFreshnessAge] is used. This will emit a console /// log in debug mode if [warnOnFallbackUsage] is is set. /// /// This may throw if the required headers were in an unexpected format. - factory CachedMapTileMetadata.fromHttpHeaders( + factory HttpControlledCachedTileMetadata.fromHttpHeaders( Map headers, { Uri? warnOnFallbackUsage, Duration fallbackFreshnessAge = const Duration(days: 7), @@ -37,11 +84,10 @@ class CachedMapTileMetadata { void warnFallbackUsage() { if (kDebugMode && warnOnFallbackUsage != null) { Logger(printer: SimplePrinter()).w( - '[flutter_map cache] Using fallback freshness age ' - '($fallbackFreshnessAge) for ${warnOnFallbackUsage.path}\n' - '\tThis indicates the tile server did not send enough ' - 'information to calculate a freshness age. Optionally override ' - "in the caching provider's config.", + '[flutter_map] Using fallback freshness age ($fallbackFreshnessAge) ' + 'for ${warnOnFallbackUsage.path}\n\tThis indicates the tile server ' + 'did not send enough information to calculate a freshness age. ' + "Optionally override in the caching provider's config.", ); } } @@ -87,7 +133,7 @@ class CachedMapTileMetadata { final lastModified = headers[HttpHeaders.lastModifiedHeader]; final etag = headers[HttpHeaders.etagHeader]; - return CachedMapTileMetadata( + return HttpControlledCachedTileMetadata( staleAt: calculateStaleAt(), lastModified: lastModified != null ? HttpDate.parse(lastModified) : null, etag: etag, @@ -96,11 +142,11 @@ class CachedMapTileMetadata { /// The calculated time at which this tile becomes stale (UTC) /// - /// Tile providers should use [isStale] to check whether a tile is stale, - /// instead of manually comparing this to the current timestamp. + /// Consumers should refer to [isStale] to determine whether the tile is + /// stale. /// /// This may have been calculated based off an HTTP response's headers using - /// [CachedMapTileMetadata.fromHttpHeaders], or it may be custom. + /// [HttpControlledCachedTileMetadata.fromHttpHeaders], or it may be custom. final DateTime staleAt; /// If available, the value in [HttpHeaders.lastModifiedHeader] (UTC) @@ -109,10 +155,7 @@ class CachedMapTileMetadata { /// If available, the value in [HttpHeaders.etagHeader] final String? etag; - /// Whether this tile should be considered stale - /// - /// Usually this is implemented by storing the timestamp at which the tile - /// becomes stale, and comparing that to the current timestamp. + @override bool get isStale => DateTime.timestamp().isAfter(staleAt); @override @@ -121,7 +164,7 @@ class CachedMapTileMetadata { @override bool operator ==(Object other) => identical(this, other) || - (other is CachedMapTileMetadata && + (other is HttpControlledCachedTileMetadata && staleAt == other.staleAt && lastModified == other.lastModified && etag == other.etag); diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart index 041899644..d11708e7b 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart @@ -14,14 +14,18 @@ import 'package:http/http.dart'; import 'package:http/retry.dart'; import 'package:logger/logger.dart'; -/// A [SourceBytesFetcher] which fetches a URI from the network using HTTP & -/// supports caching via a [MapCachingProvider]. +/// A [SourceBytesFetcher] which fetches a URI from the network using HTTP. /// /// {@template fm.sbf.default.sourceConsumption} /// Consumes an [Iterable] of [String] URIs, which must not be empty and /// iterates in an order. If the first URI cannot be used to fetch bytes, the /// next URI is used as a fallback if available, and so on. /// {@endtemplate} +/// +/// Supports caching, delegating to a [MapCachingProvider]. +/// * If a non-stale tile is available, it is used without using the network +/// * If a stale tile is available, it is updated if possible, otherwise the +/// behaviour depends on [fallbackToStaleCachedTiles] @immutable class NetworkBytesFetcher implements SourceBytesFetcher> { /// HTTP headers to send with each request. @@ -40,14 +44,29 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { /// /// See online documentation for more information about built-in caching. /// + /// If a cached tile is available and not stale, it will be used without + /// attempting the network. + /// /// Defaults to [BuiltInMapCachingProvider]. Set to /// [DisabledMapCachingProvider] to disable. final MapCachingProvider? cachingProvider; + /// Whether to use a potentially stale cached tile if it could not be + /// retrieved from the network. + /// + /// Only applicable if [cachingProvider] is in use. + /// + /// Defaults to `true`. + final bool fallbackToStaleCachedTiles; + /// Whether to optimistically attempt to decode HTTP responses that have a /// non-successful status code as an image. /// - /// Defaults to `true`. + /// Some servers return useful information embedded in an image returned in + /// the HTTP body of a non-successful response, such as an instruction to use + /// an API key. This can make it easier to debug issues. + /// + /// Defaults to `true` in debug mode, `false` otherwise. final bool attemptDecodeOfHttpErrorResponses; /// Whether to abort HTTP requests for tiles that will no longer be displayed. @@ -101,7 +120,8 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { Map? headers, Client? httpClient, this.cachingProvider, - this.attemptDecodeOfHttpErrorResponses = true, + this.fallbackToStaleCachedTiles = true, + this.attemptDecodeOfHttpErrorResponses = kDebugMode, this.abortObsoleteRequests = true, }) : headers = headers ?? {}, httpClient = httpClient ?? RetryClient(Client()) { @@ -180,6 +200,7 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { // with fresh data cachedTile = null; } + // If any other error is thrown, fetching is stopped & rethrown } // Create method to write response to cache when applicable @@ -193,24 +214,64 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { }) { if (!cachingProvider.isSupported) return; - late final CachedMapTileMetadata metadata; - try { - metadata = CachedMapTileMetadata.fromHttpHeaders( - headers, - warnOnFallbackUsage: parsedUri, + if (cachingProvider + case final PutTileAndMetadataCapability< + HttpControlledCachedTileMetadata> cachingProvider) { + late final HttpControlledCachedTileMetadata metadata; + try { + metadata = HttpControlledCachedTileMetadata.fromHttpHeaders( + headers, + warnOnFallbackUsage: parsedUri, + ); + } on Exception catch (e) { + if (kDebugMode) { + Logger(printer: SimplePrinter()).w( + '[flutter_map] Failed to cache ${parsedUri.path}: $e\n\tThis ' + 'may indicate a HTTP spec non-conformance issue with the tile ' + 'server. ', + ); + } + return; + } + + cachingProvider.putTileWithMetadata( + url: uri, + metadata: metadata, + bytes: bytes, + ); + } else if (cachingProvider case final PutTileCapability cachingProvider) { + cachingProvider.putTile(url: uri, bytes: bytes); + } else if (kDebugMode) { + Logger(printer: SimplePrinter()).w( + '[flutter_map] Caching provider incompatible with ' + '`NetworkBytesFetcher` for put operations', ); - } on Exception catch (e) { + } + } + + // Create the exception exit method + // In the event that a tile cannot be fetched from the network, and a + // (stale) cached tile is available, and the behaviour is allowed, attempt + // to use the cached resource. This method is used on exit when a + // non-abortion exception occurs. Otherwise, it rethrows the original + // exception to the caller, which may attempt fallbacks. + Future fallbackToCachedTile(Object err, StackTrace stackTrace) async { + if (cachedTile == null || !fallbackToStaleCachedTiles) { + Error.throwWithStackTrace(err, stackTrace); + } + try { + final cachedResource = + await transformer(cachedTile.bytes, allowReuse: false); if (kDebugMode) { Logger(printer: SimplePrinter()).w( - '[flutter_map cache] Failed to cache ${parsedUri.path}: $e\n\tThis ' - 'may indicate a HTTP spec non-conformance issue with the tile ' - 'server. ', + '[flutter_map] Failed to fetch ${parsedUri.path} from network; ' + 'using (stale) cached tile', ); } - return; + return cachedResource; + } on Exception { + Error.throwWithStackTrace(err, stackTrace); } - - cachingProvider.putTile(url: uri, metadata: metadata, bytes: bytes); } // Main logic @@ -232,10 +293,12 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { additionalHeaders: forceFromServer ? null : { - if (cachedTile?.metadata.lastModified case final lastModified?) + if (cachedTile?.metadata + case HttpControlledCachedTileMetadata(:final lastModified?)) HttpHeaders.ifModifiedSinceHeader: HttpDate.format(lastModified), - if (cachedTile?.metadata.etag case final etag?) + if (cachedTile?.metadata + case HttpControlledCachedTileMetadata(:final etag?)) HttpHeaders.ifNoneMatchHeader: etag, }, ); @@ -322,10 +385,12 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { ); } - rethrow; // Otherwise, attempt fallbacks + return await fallbackToCachedTile(err, stackTrace); + } on Exception catch (err, stackTrace) { + // We may also get exceptions otherwise, for example from failing to + // transform/decode bytes or `NetworkImageLoadException` + + return await fallbackToCachedTile(err, stackTrace); } - // We may also get exceptions otherwise, for example from failing to - // transform/decode bytes or `NetworkImageLoadException` - we pass these - // through to the caller to allow attempting of fallbacks implicitly } } diff --git a/lib/src/layer/tile_layer/tile_provider/network/image_provider/image_provider.dart b/lib/src/layer/tile_layer/tile_provider/network/image_provider/image_provider.dart index 4e7c931d8..3bfd20a65 100644 --- a/lib/src/layer/tile_layer/tile_provider/network/image_provider/image_provider.dart +++ b/lib/src/layer/tile_layer/tile_provider/network/image_provider/image_provider.dart @@ -10,6 +10,9 @@ import 'package:http/http.dart'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; +// TODO: This does not match the modern implementation in fetcher/network.dart, +// and may be broken. + /// Dedicated [ImageProvider] to fetch tiles from the network /// /// Supports falling back to a secondary URL, if the primary URL fetch fails. @@ -179,28 +182,64 @@ class NetworkTileImageProvider extends ImageProvider { }) { if (useFallback || !cachingProvider.isSupported) return; - late final CachedMapTileMetadata metadata; - try { - metadata = CachedMapTileMetadata.fromHttpHeaders( - headers, - warnOnFallbackUsage: silenceExceptions ? null : uri, + if (cachingProvider + case final PutTileAndMetadataCapability< + HttpControlledCachedTileMetadata> cachingProvider) { + late final HttpControlledCachedTileMetadata metadata; + try { + metadata = HttpControlledCachedTileMetadata.fromHttpHeaders( + headers, + warnOnFallbackUsage: silenceExceptions ? null : uri, + ); + } on Exception catch (e) { + if (kDebugMode) { + Logger(printer: SimplePrinter()).w( + '[flutter_map] Failed to cache ${uri.path}: $e\n\tThis ' + 'may indicate a HTTP spec non-conformance issue with the tile ' + 'server. ', + ); + } + return; + } + + cachingProvider.putTileWithMetadata( + url: resolvedUrl, + metadata: metadata, + bytes: bytes, ); - } catch (e) { - if (kDebugMode && !silenceExceptions) { + } else if (cachingProvider case final PutTileCapability cachingProvider) { + cachingProvider.putTile(url: resolvedUrl, bytes: bytes); + } else if (kDebugMode && !silenceExceptions) { + Logger(printer: SimplePrinter()).w( + '[flutter_map] Caching provider incompatible with ' + '`NetworkBytesFetcher` for put operations', + ); + } + } + + // Create the exception exit method + // In the event that a tile cannot be fetched from the network, and a + // (stale) cached tile is available, and the behaviour is allowed, attempt + // to use the cached resource. This method is used on exit when a + // non-abortion exception occurs. Otherwise, it rethrows the original + // exception to the caller, which may attempt fallbacks. + /*Future fallbackToCachedTile(Object err, StackTrace stackTrace) async { + if (cachedTile == null) { + Error.throwWithStackTrace(err, stackTrace); + } + try { + final cachedResource = await decodeBytes(cachedTile.bytes); + if (kDebugMode) { Logger(printer: SimplePrinter()).w( - '[flutter_map cache] Failed to cache ${uri.path}: $e\n\tThis may ' - 'indicate a HTTP spec non-conformance issue with the tile server. ', + '[flutter_map] Failed to fetch ${uri.path} from network; ' + 'using (stale) cached tile', ); } - return; + return cachedResource; + } on Exception { + Error.throwWithStackTrace(err, stackTrace); } - - cachingProvider.putTile( - url: resolvedUrl, - metadata: metadata, - bytes: bytes, - ); - } + }*/ // Main logic // All `decodeBytes` calls should be awaited so errors may be handled @@ -221,10 +260,12 @@ class NetworkTileImageProvider extends ImageProvider { additionalHeaders: forceFromServer ? null : { - if (cachedTile?.metadata.lastModified case final lastModified?) + if (cachedTile?.metadata + case HttpControlledCachedTileMetadata(:final lastModified?)) HttpHeaders.ifModifiedSinceHeader: HttpDate.format(lastModified), - if (cachedTile?.metadata.etag case final etag?) + if (cachedTile?.metadata + case HttpControlledCachedTileMetadata(:final etag?)) HttpHeaders.ifNoneMatchHeader: etag, }, ); From 2d7f644940c4dba9e30db5fa8b4a2aa9aacf1a88 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Tue, 12 Aug 2025 14:38:12 +0100 Subject: [PATCH 12/13] Replaced public `CachedTileMetadata` constructor with `stale` & `fresh` static const instances --- .../network/caching/tile_metadata.dart | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart index 85fbb7cd1..8ba3ddb20 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/caching/tile_metadata.dart @@ -27,11 +27,7 @@ import 'package:logger/logger.dart'; /// implementation @immutable interface class CachedTileMetadata { - /// Create new non-specific metadata. - /// - /// This method is likely only to be useful for [MapCachingProvider] - /// implementations as an output. - const CachedTileMetadata({required this.isStale}); + const CachedTileMetadata._({required this.isStale}); /// Whether to consider this tile as stale. /// @@ -45,6 +41,18 @@ interface class CachedTileMetadata { /// the network may still be attempted anyway - and this may be set on either /// implementation. final bool isStale; + + /// Non-specific metadata indicating a stale tile. + /// + /// This method is likely only to be useful for [MapCachingProvider] + /// implementations as an output. + static const stale = CachedTileMetadata._(isStale: true); + + /// Non-specific metadata indicating a non-stale tile. + /// + /// This method is likely only to be useful for [MapCachingProvider] + /// implementations as an output. + static const fresh = CachedTileMetadata._(isStale: false); } /// Implementation of [CachedTileMetadata] which uses properties commonly found From 8de1f3c5d3b603c084883ab6e7919961607d6209 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Thu, 11 Sep 2025 16:23:21 +0100 Subject: [PATCH 13/13] Avoid putting bytes which aren't a valid resource into the cache Remove assumption that a HTTP 304 Not Modified response can only occur if `forceFromServer` is `false` --- .../bytes_fetchers/network/fetcher/network.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart index d11708e7b..a154a1e03 100644 --- a/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart +++ b/lib/src/layer/modern_tile_layer/tile_loader/bytes_fetchers/network/fetcher/network.dart @@ -304,9 +304,8 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { ); // Server says nothing's changed - but might return new useful headers - if (!forceFromServer && - cachedTile != null && - response.statusCode == HttpStatus.notModified) { + // This should usually only happen when `!forceFromServer` + if (cachedTile != null && response.statusCode == HttpStatus.notModified) { late final R transformedCacheBytes; try { transformedCacheBytes = await transformer(cachedTile.bytes); @@ -322,10 +321,13 @@ class NetworkBytesFetcher implements SourceBytesFetcher> { } } - // Server says the image has changed - store it new + // Server says the image has changed if (response.statusCode == HttpStatus.ok) { + final resource = await transformer(bytes); + // If the transformer fails, the error will be caught by an outer + // try/catch block, and the bytes won't be put to the cache cachePut(bytes: bytes, headers: response.headers); - return await transformer(bytes); + return resource; } // It's likely an error at this point