diff --git a/example/lib/src/screens/main/map_view/map_view.dart b/example/lib/src/screens/main/map_view/map_view.dart index ec53dc6b..bce63ed8 100644 --- a/example/lib/src/screens/main/map_view/map_view.dart +++ b/example/lib/src/screens/main/map_view/map_view.dart @@ -16,6 +16,7 @@ import '../../../shared/misc/store_metadata_keys.dart'; import '../../../shared/state/download_provider.dart'; import '../../../shared/state/general_provider.dart'; import '../../../shared/state/region_selection_provider.dart'; +import '../../../shared/state/selected_tab_state.dart'; import 'components/additional_overlay/additional_overlay.dart'; import 'components/debugging_tile_builder/debugging_tile_builder.dart'; import 'components/download_progress/download_progress_masker.dart'; @@ -312,8 +313,8 @@ class _MapViewState extends State with TickerProviderStateMixin { maxNativeZoom: 20, tileProvider: widget.mode != MapViewMode.standard ? NetworkTileProvider() - : FMTCTileProvider.multipleStores( - storeNames: compiledStoreNames, + : FMTCTileProvider( + stores: compiledStoreNames, otherStoresStrategy: otherStoresStrategy, loadingStrategy: provider.loadingStrategy, useOtherStoresAsFallbackOnly: @@ -340,6 +341,7 @@ class _MapViewState extends State with TickerProviderStateMixin { context.select((p) => p.isFocused); final map = FlutterMap( + key: ValueKey(selectedTabState.value), mapController: _mapController.mapController, options: mapOptions, children: [ diff --git a/lib/flutter_map_tile_caching.dart b/lib/flutter_map_tile_caching.dart index 1111774d..d90df1d6 100644 --- a/lib/flutter_map_tile_caching.dart +++ b/lib/flutter_map_tile_caching.dart @@ -47,7 +47,7 @@ part 'src/providers/tile_loading_interceptor/result.dart'; part 'src/providers/tile_loading_interceptor/map_typedef.dart'; part 'src/providers/tile_loading_interceptor/result_path.dart'; part 'src/providers/image_provider/image_provider.dart'; -part 'src/providers/image_provider/internal_get_bytes.dart'; +part 'src/providers/image_provider/internal_tile_browser.dart'; part 'src/providers/tile_provider/custom_user_agent_compat_map.dart'; part 'src/providers/tile_provider/strategies.dart'; part 'src/providers/tile_provider/tile_provider.dart'; diff --git a/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart b/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart index 9b2911c4..053b05e0 100644 --- a/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart +++ b/lib/src/backend/impls/objectbox/backend/internal_workers/shared.dart @@ -14,13 +14,21 @@ Map _sharedWriteSingleTile({ final storesBox = root.box(); final rootBox = root.box(); + final availableStoreNames = storesBox.getAll().map((e) => e.name); + + for (final storeName in storeNames) { + if (!availableStoreNames.contains(storeName)) { + throw StoreNotExists(storeName: storeName); + } + } + final compiledStoreNames = writeAllNotIn == null ? storeNames : [ ...storeNames, - ...storesBox.getAll().map((e) => e.name).where( - (e) => !writeAllNotIn.contains(e) && !storeNames.contains(e), - ), + ...availableStoreNames.whereNot( + (e) => writeAllNotIn.contains(e) || storeNames.contains(e), + ), ]; final tilesQuery = tiles.query(ObjectBoxTile_.url.equals(url)).build(); diff --git a/lib/src/backend/interfaces/models.dart b/lib/src/backend/interfaces/models.dart index a738dc40..b0ab75bf 100644 --- a/lib/src/backend/interfaces/models.dart +++ b/lib/src/backend/interfaces/models.dart @@ -1,13 +1,7 @@ // Copyright © Luka S (JaffaKetchup) under GPL-v3 // A full license can be found at .\LICENSE -// TODO: Maybe bad design - do we really want inheritance? -// ignore_for_file: avoid_equals_and_hash_code_on_mutable_classes - import 'dart:typed_data'; - -import 'package:meta/meta.dart'; - import '../../../flutter_map_tile_caching.dart'; /// Represents a tile (which is never directly exposed to the user) @@ -28,18 +22,4 @@ abstract base class BackendTile { /// The raw bytes of the image of this tile Uint8List get bytes; - - /// Uses [url] for equality comparisons only (unless the two objects are - /// [identical]) - /// - /// Overriding this in an implementation may cause FMTC logic to break, and is - /// therefore not recommended. - @override - @nonVirtual - bool operator ==(Object other) => - identical(this, other) || (other is BackendTile && url == other.url); - - @override - @nonVirtual - int get hashCode => url.hashCode; } diff --git a/lib/src/providers/image_provider/browsing_errors.dart b/lib/src/providers/image_provider/browsing_errors.dart index dd29109e..d91935b0 100644 --- a/lib/src/providers/image_provider/browsing_errors.dart +++ b/lib/src/providers/image_provider/browsing_errors.dart @@ -3,7 +3,6 @@ import 'package:flutter_map/flutter_map.dart'; import 'package:http/http.dart'; -import 'package:http/io_client.dart'; import 'package:meta/meta.dart'; import '../../../flutter_map_tile_caching.dart'; @@ -109,11 +108,6 @@ enum FMTCBrowsingErrorType { /// Failed to load the tile from the cache or network because it was missing /// from the cache and there was an unexpected error when requesting from the /// server - /// - /// Try specifying a normal HTTP/1.1 [IOClient] when using - /// [FMTCStore.getTileProvider]. Check that the [TileLayer.urlTemplate] is - /// correct, that any necessary authorization data is correctly included, and - /// that the server serves the viewed region. unknownFetchException( 'Failed to load the tile from the cache or network because it was missing ' 'from the cache and there was an unexpected error when requesting from ' diff --git a/lib/src/providers/image_provider/image_provider.dart b/lib/src/providers/image_provider/image_provider.dart index e153f03c..bdad0f15 100644 --- a/lib/src/providers/image_provider/image_provider.dart +++ b/lib/src/providers/image_provider/image_provider.dart @@ -44,7 +44,7 @@ class _FMTCImageProvider extends ImageProvider<_FMTCImageProvider> { ImageDecoderCallback decode, ) => MultiFrameImageStreamCompleter( - codec: getBytes( + codec: provideTile( coords: coords, options: options, provider: provider, @@ -59,19 +59,19 @@ class _FMTCImageProvider extends ImageProvider<_FMTCImageProvider> { final tileUrl = provider.getTileUrl(coords, options); return [ - DiagnosticsProperty('Store names', provider.storeNames), + DiagnosticsProperty('Stores', provider.stores), DiagnosticsProperty('Tile coordinates', coords), DiagnosticsProperty('Tile URL', tileUrl), DiagnosticsProperty( 'Tile storage-suitable UID', - provider.urlTransformer(tileUrl), + provider.urlTransformer?.call(tileUrl) ?? tileUrl, ), ]; }, ); - /// {@macro fmtc.tileProvider.getBytes} - static Future getBytes({ + /// {@macro fmtc.tileProvider.provideTile} + static Future provideTile({ required TileCoordinates coords, required TileLayer options, required FMTCTileProvider provider, @@ -80,6 +80,8 @@ class _FMTCImageProvider extends ImageProvider<_FMTCImageProvider> { void Function()? finishedLoadingBytes, bool requireValidImage = false, }) async { + startedLoading?.call(); + final currentTLIR = provider.tileLoadingInterceptor != null ? _TLIRConstructor._() : null; @@ -116,11 +118,9 @@ class _FMTCImageProvider extends ImageProvider<_FMTCImageProvider> { } } - startedLoading?.call(); - final Uint8List bytes; try { - bytes = await _internalGetBytes( + bytes = await _internalTileBrowser( coords: coords, options: options, provider: provider, @@ -146,12 +146,13 @@ class _FMTCImageProvider extends ImageProvider<_FMTCImageProvider> { Future<_FMTCImageProvider> obtainKey(ImageConfiguration configuration) => SynchronousFuture(this); - // TODO: Incorporate tile provider & tile layer options @override bool operator ==(Object other) => identical(this, other) || - (other is _FMTCImageProvider && other.coords == coords); + (other is _FMTCImageProvider && + other.coords == coords && + other.provider == provider); @override - int get hashCode => coords.hashCode; + int get hashCode => Object.hash(coords, provider); } diff --git a/lib/src/providers/image_provider/internal_get_bytes.dart b/lib/src/providers/image_provider/internal_tile_browser.dart similarity index 96% rename from lib/src/providers/image_provider/internal_get_bytes.dart rename to lib/src/providers/image_provider/internal_tile_browser.dart index 65486c96..db91667a 100644 --- a/lib/src/providers/image_provider/internal_get_bytes.dart +++ b/lib/src/providers/image_provider/internal_tile_browser.dart @@ -3,7 +3,7 @@ part of '../../../flutter_map_tile_caching.dart'; -Future _internalGetBytes({ +Future _internalTileBrowser({ required TileCoordinates coords, required TileLayer options, required FMTCTileProvider provider, @@ -29,7 +29,7 @@ Future _internalGetBytes({ } final networkUrl = provider.getTileUrl(coords, options); - final matcherUrl = provider.urlTransformer(networkUrl); + final matcherUrl = provider.urlTransformer?.call(networkUrl) ?? networkUrl; currentTLIR?.networkUrl = networkUrl; currentTLIR?.storageSuitableUID = matcherUrl; @@ -55,7 +55,7 @@ Future _internalGetBytes({ final tileRetrievableFromOtherStoresAsFallback = existingTile != null && provider.useOtherStoresAsFallbackOnly && - provider.storeNames.keys + provider.stores.keys .toSet() .intersection(allExistingStores.toSet()) .isEmpty; @@ -206,7 +206,7 @@ Future _internalGetBytes({ // their read/write settings // At this point, we've downloaded the tile anyway, so we might as well // write the stores that allow it, even if the existing tile hasn't expired - final writeTileToSpecified = provider.storeNames.entries + final writeTileToSpecified = provider.stores.entries .where( (e) => switch (e.value) { null => false, @@ -223,7 +223,7 @@ Future _internalGetBytes({ existingTile != null ? writeTileToSpecified.followedBy( intersectedExistingStores - .whereNot((e) => provider.storeNames.containsKey(e)), + .whereNot((e) => provider.stores.containsKey(e)), ) : writeTileToSpecified) .toSet() @@ -236,7 +236,7 @@ Future _internalGetBytes({ storeNames: writeTileToIntermediate, writeAllNotIn: provider.otherStoresStrategy == BrowseStoreStrategy.readUpdateCreate - ? provider.storeNames.keys.toList(growable: false) + ? provider.stores.keys.toList(growable: false) : null, url: matcherUrl, bytes: response.bodyBytes, diff --git a/lib/src/providers/tile_loading_interceptor/result.dart b/lib/src/providers/tile_loading_interceptor/result.dart index bc17fccc..a7b98836 100644 --- a/lib/src/providers/tile_loading_interceptor/result.dart +++ b/lib/src/providers/tile_loading_interceptor/result.dart @@ -3,8 +3,8 @@ part of '../../../flutter_map_tile_caching.dart'; -/// A 'temporary' object that collects information from [_internalGetBytes] to -/// be used to construct a [TileLoadingInterceptorResult] +/// A 'temporary' object that collects information from [_internalTileBrowser] +/// to be used to construct a [TileLoadingInterceptorResult] /// /// See documentation on [TileLoadingInterceptorResult] for more information class _TLIRConstructor { @@ -24,7 +24,7 @@ class _TLIRConstructor { } /// Information useful to debug and record detailed statistics for the loading -/// mechanisms and paths of a tile +/// mechanisms and paths of a browsed tile load @immutable class TileLoadingInterceptorResult { const TileLoadingInterceptorResult._({ diff --git a/lib/src/providers/tile_provider/custom_user_agent_compat_map.dart b/lib/src/providers/tile_provider/custom_user_agent_compat_map.dart index 492cf683..454d3a50 100644 --- a/lib/src/providers/tile_provider/custom_user_agent_compat_map.dart +++ b/lib/src/providers/tile_provider/custom_user_agent_compat_map.dart @@ -20,7 +20,7 @@ class _CustomUserAgentCompatMap extends MapView { /// /// The identifying mark is injected to seperate traffic sent via FMTC from /// standard flutter_map traffic, as it significantly changes the behaviour of - /// tile retrieval, and could generate more traffic. + /// tile retrieval. @override String putIfAbsent(String key, String Function() ifAbsent) { if (key != 'User-Agent') return super.putIfAbsent(key, ifAbsent); diff --git a/lib/src/providers/tile_provider/strategies.dart b/lib/src/providers/tile_provider/strategies.dart index 6b7b30c2..7dc2a538 100644 --- a/lib/src/providers/tile_provider/strategies.dart +++ b/lib/src/providers/tile_provider/strategies.dart @@ -4,7 +4,14 @@ part of '../../../flutter_map_tile_caching.dart'; /// Alias for [BrowseLoadingStrategy], to ease migration from v9 -> v10 -@Deprecated('`CacheBehavior` has been renamed to `BrowseLoadingStrategy`') +@Deprecated( + 'Rename all references to `BrowseLoadingStrategy` instead. ' + 'The new name is less ambiguous in the context of the new ' + '`BrowseStoreStrategy`, and does not depend on a British or American ' + 'spelling. ' + 'This feature was deprecated in v10, and will be removed in a future ' + 'version.', +) typedef CacheBehavior = BrowseLoadingStrategy; /// Determines whether the network or cache is preferred during browse caching, diff --git a/lib/src/providers/tile_provider/tile_provider.dart b/lib/src/providers/tile_provider/tile_provider.dart index dbf70e3c..cb41f603 100644 --- a/lib/src/providers/tile_provider/tile_provider.dart +++ b/lib/src/providers/tile_provider/tile_provider.dart @@ -6,64 +6,92 @@ part of '../../../flutter_map_tile_caching.dart'; /// Specialised [TileProvider] that uses a specialised [ImageProvider] to /// connect to FMTC internals and enable advanced caching/retrieval logic /// -/// To use a single store, use [FMTCStore.getTileProvider]. -/// -/// To use multiple stores, use the [FMTCTileProvider.multipleStores] -/// constructor. See documentation on [storeNames] and [otherStoresStrategy] +/// To use a single or multiple stores, use the [FMTCTileProvider.new] +/// constructor. See documentation on [stores] and [otherStoresStrategy] /// for information on usage. /// /// To use all stores, use the [FMTCTileProvider.allStores] constructor. See /// documentation on [otherStoresStrategy] for information on usage. /// -/// An "FMTC" identifying mark is injected into the "User-Agent" header -/// generated by flutter_map, except if specified in the constructor. For -/// technical details, see [_CustomUserAgentCompatMap]. -/// -/// Can be constructed alternatively with [FMTCStore.getTileProvider] to -/// support a single store. +/// {@template fmtc.fmtcTileProvider.constructionTip} +/// > [!TIP] +/// > +/// > **Minimize reconstructions of this provider by constructing it outside of +/// > the `build` method of a widget wherever possible.** +/// > +/// > If this is not possible, because one or more properties depend on +/// > inherited data (ie. via an `InheritedWidget`, `Provider`, etc.), define +/// > and construct as many properties as possible outside of the `build` +/// > method. +/// > +/// > * Manually constructing and initialising an [httpClient] once is much +/// > cheaper than the [FMTCTileProvider]'s constructors doing it automatically +/// > on every construction (every rebuild), and allows a single connection to +/// > the server to be maintained, massively improving tile loading speeds. Also +/// > see [httpClient]'s documentation. +/// > +/// > * Properties that use objects without a useful equality and hash code +/// > should always be defined once outside of the build method so that their +/// > identity (by [identical]) is not changed - for example, [httpClient], +/// > [tileLoadingInterceptor], [errorHandler], and [urlTransformer]. +/// > All properties comprise part of the [hashCode] & [operator ==], which are +/// > used to form the Flutter session [ImageCache] key in the internal image +/// > provider (alongside the tile coordinates). This key should not change for +/// > a tile unless the configuration is actually changed meaningfully, as this +/// > will disrupt the session cache, and mean tiles may need to be fetched +/// > unnecessarily. +/// > +/// > See the online documentation for an example of the recommended usage. +/// {@endtemplate} @immutable class FMTCTileProvider extends TileProvider { - /// See [FMTCTileProvider] for information - FMTCTileProvider.multipleStores({ - required this.storeNames, + /// Create an [FMTCTileProvider] that interacts with a subset of all available + /// stores + /// + /// See [stores] & [otherStoresStrategy] for information. + /// + /// {@macro fmtc.fmtcTileProvider.constructionTip} + FMTCTileProvider({ + required this.stores, this.otherStoresStrategy, this.loadingStrategy = BrowseLoadingStrategy.cacheFirst, this.useOtherStoresAsFallbackOnly = false, this.recordHitsAndMisses = true, this.cachedValidDuration = Duration.zero, - UrlTransformer? urlTransformer, + this.urlTransformer, this.errorHandler, this.tileLoadingInterceptor, Client? httpClient, @visibleForTesting this.fakeNetworkDisconnect = false, Map? headers, - }) : urlTransformer = (urlTransformer ?? (u) => u), + }) : _wasClientAutomaticallyGenerated = httpClient == null, httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), - _wasClientAutomaticallyGenerated = httpClient == null, super( headers: (headers?.containsKey('User-Agent') ?? false) ? headers : _CustomUserAgentCompatMap(headers ?? {}), ); - /// See [FMTCTileProvider] for information + /// Create an [FMTCTileProvider] that interacts with all available stores, + /// using one [BrowseStoreStrategy] efficiently + /// + /// {@macro fmtc.fmtcTileProvider.constructionTip} FMTCTileProvider.allStores({ required BrowseStoreStrategy allStoresStrategy, this.loadingStrategy = BrowseLoadingStrategy.cacheFirst, - this.useOtherStoresAsFallbackOnly = false, this.recordHitsAndMisses = true, this.cachedValidDuration = Duration.zero, - UrlTransformer? urlTransformer, + this.urlTransformer, this.errorHandler, this.tileLoadingInterceptor, Client? httpClient, @visibleForTesting this.fakeNetworkDisconnect = false, Map? headers, - }) : storeNames = const {}, + }) : stores = const {}, otherStoresStrategy = allStoresStrategy, - urlTransformer = (urlTransformer ?? (u) => u), - httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), + useOtherStoresAsFallbackOnly = false, _wasClientAutomaticallyGenerated = httpClient == null, + httpClient = httpClient ?? IOClient(HttpClient()..userAgent = null), super( headers: (headers?.containsKey('User-Agent') ?? false) ? headers @@ -77,11 +105,15 @@ class FMTCTileProvider extends TileProvider { /// /// Stores not included will not be used by default. However, /// [otherStoresStrategy] determines whether & how all other unspecified - /// stores should be used. Stores included but with a `null` value will be - /// exempt from [otherStoresStrategy]. - final Map storeNames; + /// stores should be used. Stores included in this mapping but with a `null` + /// value will be exempted from [otherStoresStrategy] (ie. unused). + /// + /// All specified store names should correspond to existing stores. + /// Non-existant stores may cause unexpected read behaviour and will throw a + /// [StoreNotExists] error if a tile is attempted to be written to it. + final Map stores; - /// The behaviour of all other stores not specified in [storeNames] + /// The behaviour of all other stores not specified in [stores] /// /// `null` means that all other stores will not be used. /// @@ -92,8 +124,8 @@ class FMTCTileProvider extends TileProvider { /// stores should only be used as a last resort or in addition to the /// specified stores as normal. /// - /// Stores specified in [storeNames] but associated with a `null` value will - /// not not gain this behaviour. + /// Stores specified in [stores] but associated with a `null` value will not + /// gain this behaviour. final BrowseStoreStrategy? otherStoresStrategy; /// Determines whether the network or cache is preferred during browse @@ -126,7 +158,7 @@ class FMTCTileProvider extends TileProvider { /// Whether to record the [StoreStats.hits] and [StoreStats.misses] statistics /// /// When enabled, hits will be recorded for all stores that the tile belonged - /// to and were present in [FMTCTileProvider.storeNames], when necessary. + /// to and were present in [FMTCTileProvider.stores], when necessary. /// Misses will be recorded for all stores specified in the tile provided, /// where necessary /// @@ -165,7 +197,7 @@ class FMTCTileProvider extends TileProvider { /// /// By default, the output string is the input string - that is, the /// storage-suitable UID is the tile's real URL. - final UrlTransformer urlTransformer; + final UrlTransformer? urlTransformer; /// A custom callback that will be called when an [FMTCBrowsingError] is /// thrown @@ -189,6 +221,7 @@ class FMTCTileProvider extends TileProvider { /// parameter: /// /// ```dart + /// // outside of the `build` method /// final tileLoadingInterceptor = /// ValueNotifier({}); // Do not use `const {}` /// ``` @@ -200,9 +233,32 @@ class FMTCTileProvider extends TileProvider { /// [Client] (such as a [IOClient]) used to make all network requests /// - /// If specified, then it will not be closed automatically on [dispose]al. - /// When closing manually, ensure no requests are currently underway, else - /// they will throw [ClientException]s. + /// If this provider could be rebuild frequently (ie. it is constructed in a + /// build method), a client should always be defined manually outside of the + /// build method and passed into the constructor. See the documentation tip on + /// [FMTCTileProvider] for more information. For example (this is also the + /// same client as created automatically by the constructor if no argument + /// is passed): + /// + /// ```dart + /// // `StatefulWidget` class definition + /// + /// class _...State extends State<...> { + /// late final _httpClient = IOClient(HttpClient()..userAgent = null); + /// // followed by other state contents, such as `build` + /// } + /// ``` + /// + /// Any specified user agent defined on the client will be overriden. + /// If a "User-Agent" header is specified in [headers] it will be used. + /// Otherwise, the default flutter_map user agent logic is used, followed by + /// an injected "FMTC" identifying mark (see [_CustomUserAgentCompatMap]). + /// + /// If a client is passed in, it should not be closed manually unless certain + /// that all tile requests have finished, else they will throw + /// [ClientException]s. If the constructor automatically creates a client ( + /// because one was not passed as an argument), it will be closed safely + /// automatically on [dispose]al. /// /// Defaults to a standard [IOClient]/[HttpClient]. final Client httpClient; @@ -251,18 +307,25 @@ class FMTCTileProvider extends TileProvider { super.dispose(); } - /// {@template fmtc.tileProvider.getBytes} + /// {@template fmtc.tileProvider.provideTile} /// Use FMTC's caching logic to get the bytes of the specific tile (at /// [coords]) with the specified [TileLayer] options and [FMTCTileProvider] /// provider /// - /// Used internally by [_FMTCImageProvider.loadImage]. `loadImage` provides - /// a decoding wrapper, but is only suitable for codecs Flutter can render. + /// > [!IMPORTANT] + /// > Note that this will actuate the cache writing mechanism as if a normal + /// > tile browse request was made - ie. the bytes returned may be written to + /// > the cache. /// - /// Therefore, this method does not make any assumptions about the format - /// of the bytes, and it is up to the user to decode/render appropriately. - /// For example, this could be incorporated into another [ImageProvider] (via - /// a [TileProvider]) to integrate FMTC caching for vector tiles. + /// Used internally by [_FMTCImageProvider.loadImage]. `loadImage` provides a + /// decoding wrapper to display the bytes as an image, but is only suitable + /// for codecs Flutter can render. + /// + /// > [!TIP] + /// > This method does not make any assumptions about theformat of the bytes, + /// > and it is up to the user to decode/render appropriately. For example, this + /// > could be incorporated into another [ImageProvider] (via a + /// > [TileProvider]) to integrate FMTC caching for vector tiles. /// /// --- /// @@ -286,7 +349,7 @@ class FMTCTileProvider extends TileProvider { /// FMTC will not throw an error, but Flutter will if the bytes are attempted /// to be decoded (now or at a later time). /// {@endtemplate} - Future getBytes({ + Future provideTile({ required TileCoordinates coords, required TileLayer options, Object? key, @@ -294,7 +357,7 @@ class FMTCTileProvider extends TileProvider { void Function()? finishedLoadingBytes, bool requireValidImage = false, }) => - _FMTCImageProvider.getBytes( + _FMTCImageProvider.provideTile( coords: coords, options: options, provider: this, @@ -311,11 +374,13 @@ class FMTCTileProvider extends TileProvider { Future isTileCached({ required TileCoordinates coords, required TileLayer options, - }) => - FMTCBackendAccess.internal.tileExists( - storeNames: _getSpecifiedStoresOrNull(), - url: urlTransformer(getTileUrl(coords, options)), - ); + }) { + final networkUrl = getTileUrl(coords, options); + return FMTCBackendAccess.internal.tileExists( + storeNames: _getSpecifiedStoresOrNull(), + url: urlTransformer?.call(networkUrl) ?? networkUrl, + ); + } /// Removes key-value pairs from the specified [url], given only the [keys] /// @@ -354,16 +419,15 @@ class FMTCTileProvider extends TileProvider { return mutableUrl; } - /// If [storeNames] contains `null`, returns `null`, otherwise returns all + /// If [stores] contains `null`, returns `null`, otherwise returns all /// non-null names (which cannot be empty) List? _getSpecifiedStoresOrNull() => - otherStoresStrategy != null ? null : storeNames.keys.toList(); + otherStoresStrategy != null ? null : stores.keys.toList(); @override bool operator ==(Object other) => identical(this, other) || (other is FMTCTileProvider && - mapEquals(other.storeNames, storeNames) && other.otherStoresStrategy == otherStoresStrategy && other.loadingStrategy == loadingStrategy && other.useOtherStoresAsFallbackOnly == useOtherStoresAsFallbackOnly && @@ -373,20 +437,23 @@ class FMTCTileProvider extends TileProvider { other.errorHandler == errorHandler && other.tileLoadingInterceptor == tileLoadingInterceptor && other.httpClient == httpClient && - other.headers == headers); + mapEquals(other.stores, stores) && + mapEquals(other.headers, headers)); @override - int get hashCode => Object.hash( - storeNames, - otherStoresStrategy, - loadingStrategy, - useOtherStoresAsFallbackOnly, - recordHitsAndMisses, - cachedValidDuration, - urlTransformer, - errorHandler, - tileLoadingInterceptor, - httpClient, - headers, + int get hashCode => Object.hashAllUnordered( + [ + otherStoresStrategy, + loadingStrategy, + useOtherStoresAsFallbackOnly, + recordHitsAndMisses, + cachedValidDuration, + urlTransformer, + errorHandler, + tileLoadingInterceptor, + httpClient, + ...stores.entries.map((e) => (e.key, e.value)), + ...headers.entries.map((e) => (e.key, e.value)), + ], ); } diff --git a/lib/src/regions/base_region.dart b/lib/src/regions/base_region.dart index 2b309af3..43c2a233 100644 --- a/lib/src/regions/base_region.dart +++ b/lib/src/regions/base_region.dart @@ -8,12 +8,6 @@ part of '../../flutter_map_tile_caching.dart'; /// It can be converted to a: /// - [DownloadableRegion] for downloading: [toDownloadable] /// - list of [LatLng]s forming the outline: [toOutline] -/// -/// Extended/implemented by: -/// - [RectangleRegion] -/// - [CircleRegion] -/// - [LineRegion] -/// - [CustomPolygonRegion] @immutable sealed class BaseRegion { /// Create a geographical region that forms a particular shape @@ -21,12 +15,6 @@ sealed class BaseRegion { /// It can be converted to a: /// - [DownloadableRegion] for downloading: [toDownloadable] /// - list of [LatLng]s forming the outline: [toOutline] - /// - /// Extended/implemented by: - /// - [RectangleRegion] - /// - [CircleRegion] - /// - [LineRegion] - /// - [CustomPolygonRegion] const BaseRegion(); /// Output a value of type [T] the type of this region @@ -34,8 +22,12 @@ sealed class BaseRegion { /// Requires all region types to have a defined handler. See [maybeWhen] for /// the equivalent where this is not required. @Deprecated( - 'Prefer using a pattern matching selection (such as `if case` or ' - '`switch`). This will be removed in a future version.', + 'Use a pattern matching selection pattern (such as `if case` or `switch`) ' + 'instead. ' + 'This is now a redundant method as the `BaseRegion` inheritance tree is ' + 'sealed and modern Dart supports the intended purpose of this natively. ' + 'This feature was deprecated in v10, and will be removed in a future ' + 'version.', ) T when({ required T Function(RectangleRegion rectangle) rectangle, @@ -57,8 +49,12 @@ sealed class BaseRegion { /// If the specified method is not defined for the type of region which this /// region is, `null` will be returned. @Deprecated( - 'Prefer using a pattern matching selection (such as `if case` or ' - '`switch`). This will be removed in a future version.', + 'Use a pattern matching selection pattern (such as `if case` or `switch`) ' + 'instead. ' + 'This is now a redundant method as the `BaseRegion` inheritance tree is ' + 'sealed and modern Dart supports the intended purpose of this natively. ' + 'This feature was deprecated in v10, and will be removed in a future ' + 'version.', ) T? maybeWhen({ T Function(RectangleRegion rectangle)? rectangle, diff --git a/lib/src/root/recovery.dart b/lib/src/root/recovery.dart index 17622bd2..04f5a8ee 100644 --- a/lib/src/root/recovery.dart +++ b/lib/src/root/recovery.dart @@ -31,7 +31,7 @@ part of '../../flutter_map_tile_caching.dart'; /// been successfully downloaded. Therefore, no unnecessary tiles are downloaded /// again. /// -/// > [!NOTE] +/// > [!IMPORTANT] /// > Options set at download time, in [StoreDownload.startForeground], are not /// > included. class RootRecovery { @@ -46,12 +46,10 @@ class RootRecovery { /// {@macro fmtc.backend.watchRecovery} Stream watch({ bool triggerImmediately = false, - }) async* { - final stream = FMTCBackendAccess.internal.watchRecovery( - triggerImmediately: triggerImmediately, - ); - yield* stream; - } + }) => + FMTCBackendAccess.internal.watchRecovery( + triggerImmediately: triggerImmediately, + ); /// List all recoverable regions, and whether each one has failed /// diff --git a/lib/src/root/statistics.dart b/lib/src/root/statistics.dart index 88ec61f0..6b6055d9 100644 --- a/lib/src/root/statistics.dart +++ b/lib/src/root/statistics.dart @@ -22,7 +22,12 @@ class RootStats { Future get length => FMTCBackendAccess.internal.rootLength(); /// {@macro fmtc.backend.watchRecovery} - @Deprecated('This has been moved to `FMTCRoot.recovery` & renamed `.watch`') + @Deprecated( + 'Use `FMTCRoot.recovery.watch()` instead. ' + 'This is more suited to the context of the recovery methods. ' + 'This feature was deprecated in v10, and will be removed in a future ' + 'version.', + ) Stream watchRecovery({ bool triggerImmediately = false, }) => diff --git a/lib/src/store/download.dart b/lib/src/store/download.dart index ad74338e..73f63b07 100644 --- a/lib/src/store/download.dart +++ b/lib/src/store/download.dart @@ -7,7 +7,7 @@ part of '../../flutter_map_tile_caching.dart'; /// /// --- /// -/// {@template num_instances} +/// {@template fmtc.bulkDownload.numInstances} /// By default, only one download is allowed at any one time, across all stores. /// /// However, if necessary, multiple can be started by setting methods' @@ -50,10 +50,12 @@ class StoreDownload { /// /// The first stream (of [DownloadProgress]s) will emit events: /// * once per [TileEvent] emitted on the second stream - /// * at intervals of no longer than [maxReportInterval] - /// * once at the start of the download indicating setup is complete and the - /// first tile is being downloaded - /// * once additionally at the end of the download after the last tile + /// * additionally at intervals of no longer than [maxReportInterval] + /// (defaulting to 1 second, to allow time-based statistics to remain + /// up-to-date if no [TileEvent]s are emitted for a while) + /// * additionally once at the start of the download indicating setup is + /// complete and the first tile is being downloaded + /// * additionally once at the end of the download after the last tile /// setting some final statistics (such as tiles per second to 0) /// /// Once the stream of [DownloadProgress]s completes/finishes, the download @@ -122,16 +124,6 @@ class StoreDownload { /// /// --- /// - /// A fresh [DownloadProgress] event will always be emitted every - /// [maxReportInterval] (if specified), which defaults to every 1 second, - /// regardless of whether any more tiles have been attempted/downloaded/failed. - /// This is to enable the [DownloadProgress.elapsedDuration] to be accurately - /// presented to the end user. - /// - /// {@macro fmtc.tileevent.extraConsiderations} - /// - /// --- - /// /// When this download is started, assuming [disableRecovery] is `false` (as /// default), the recovery system will register this download, to allow it to /// be recovered if it unexpectedly fails. @@ -150,7 +142,7 @@ class StoreDownload { /// /// --- /// - /// {@macro num_instances} + /// {@macro fmtc.bulkDownload.numInstances} ({ Stream tileEvents, Stream downloadProgress, @@ -203,13 +195,11 @@ class StoreDownload { final UrlTransformer resolvedUrlTransformer; if (urlTransformer != null) { resolvedUrlTransformer = urlTransformer; + } else if (region.options.tileProvider + case final FMTCTileProvider tileProvider) { + resolvedUrlTransformer = tileProvider.urlTransformer ?? (u) => u; } else { - if (region.options.tileProvider - case final FMTCTileProvider tileProvider) { - resolvedUrlTransformer = tileProvider.urlTransformer; - } else { - resolvedUrlTransformer = (u) => u; - } + resolvedUrlTransformer = (u) => u; } // Create download instance @@ -346,7 +336,13 @@ class StoreDownload { /// /// Note that this does not require an existing/ready store, or a sensical /// [DownloadableRegion.options]. - @Deprecated('`check` has been renamed to `countTiles`') + @Deprecated( + 'Use `countTiles()` instead. ' + 'The new name is less ambiguous and aligns better with recommended Dart ' + 'code style. ' + 'This feature was deprecated in v10, and will be removed in a future ' + 'version.', + ) Future check(DownloadableRegion region) => countTiles(region); /// Cancel the ongoing foreground download and recovery session @@ -357,7 +353,7 @@ class StoreDownload { /// cancel the download immediately, as this would likely cause unwanted /// behaviour. /// - /// {@macro num_instances} + /// {@macro fmtc.bulkDownload.numInstances} /// /// Does nothing (returns immediately) if there is no ongoing download. Future cancel({Object instanceId = 0}) async => @@ -372,7 +368,7 @@ class StoreDownload { /// parallel download threads will be allowed to finish their *current* tile /// download. Any buffered tiles are not written. /// - /// {@macro num_instances} + /// {@macro fmtc.bulkDownload.numInstances} /// /// Does nothing (returns immediately) if there is no ongoing download or the /// download is already paused. @@ -384,7 +380,7 @@ class StoreDownload { /// Resume (after a [pause]) the ongoing foreground download /// - /// {@macro num_instances} + /// {@macro fmtc.bulkDownload.numInstances} /// /// Does nothing if there is no ongoing download or the download is already /// running. @@ -397,7 +393,7 @@ class StoreDownload { /// Whether the ongoing foreground download is currently paused after a call /// to [pause] (and prior to [resume]) /// - /// {@macro num_instances} + /// {@macro fmtc.bulkDownload.numInstances} /// /// Also returns `false` if there is no ongoing download. bool isPaused({Object instanceId = 0}) => diff --git a/lib/src/store/statistics.dart b/lib/src/store/statistics.dart index 67f01e02..dfe6f234 100644 --- a/lib/src/store/statistics.dart +++ b/lib/src/store/statistics.dart @@ -14,7 +14,7 @@ class StoreStats { /// {@macro fmtc.backend.getStoreStats} /// - /// {@template fmtc.frontend.storestats.efficiency} + /// {@template fmtc.storeStats.efficiency} /// Prefer using [all] when multiple statistics are required instead of /// getting them individually. Only one backend operation is required to get /// all the stats, and so is more efficient. @@ -25,19 +25,19 @@ class StoreStats { /// Retrieve the total number of KiBs of all tiles' bytes (not 'real total' /// size) /// - /// {@macro fmtc.frontend.storestats.efficiency} + /// {@macro fmtc.storeStats.efficiency} Future get size => all.then((a) => a.size); /// Retrieve the number of tiles belonging to this store /// - /// {@macro fmtc.frontend.storestats.efficiency} + /// {@macro fmtc.storeStats.efficiency} Future get length => all.then((a) => a.length); /// Retrieve the number of successful tile retrievals when browsing /// /// A hit is only counted when an unexpired tile is retrieved from the store. /// - /// {@macro fmtc.frontend.storestats.efficiency} + /// {@macro fmtc.storeStats.efficiency} Future get hits => all.then((a) => a.hits); /// Retrieve the number of unsuccessful tile retrievals when browsing @@ -45,7 +45,7 @@ class StoreStats { /// A miss is counted whenever a tile is retrieved anywhere else but from this /// store, or is retrieved from this store, but only as a fallback. /// - /// {@macro fmtc.frontend.storestats.efficiency} + /// {@macro fmtc.storeStats.efficiency} Future get misses => all.then((a) => a.misses); /// {@macro fmtc.backend.watchStores} diff --git a/lib/src/store/store.dart b/lib/src/store/store.dart index 1ab6c5c4..6b89217c 100644 --- a/lib/src/store/store.dart +++ b/lib/src/store/store.dart @@ -38,11 +38,17 @@ class FMTCStore { /// Generate an [FMTCTileProvider] that only specifies this store /// - /// See other available [FMTCTileProvider] contructors to use multiple stores - /// at once. See [FMTCTileProvider] for more info. + /// Prefer/migrate to the [FMTCTileProvider.new] constructor. /// - /// [FMTCTileProvider.fakeNetworkDisconnect] cannot be set through this - /// shorthand for [FMTCTileProvider.multipleStores]. + /// {@macro fmtc.fmtcTileProvider.constructionTip} + @Deprecated( + 'Use the `FMTCTileProvider` default constructor instead. ' + 'This will reduce internal codebase complexity and maximise external ' + 'flexibility, and works toward a potential future decentralised API ' + 'design. ' + 'This feature was deprecated in v10, and will be removed in a future ' + 'version.', + ) FMTCTileProvider getTileProvider({ BrowseStoreStrategy storeStrategy = BrowseStoreStrategy.readUpdateCreate, BrowseStoreStrategy? otherStoresStrategy, @@ -56,8 +62,8 @@ class FMTCStore { Map? headers, Client? httpClient, }) => - FMTCTileProvider.multipleStores( - storeNames: {storeName: storeStrategy}, + FMTCTileProvider( + stores: {storeName: storeStrategy}, otherStoresStrategy: otherStoresStrategy, loadingStrategy: loadingStrategy, useOtherStoresAsFallbackOnly: useOtherStoresAsFallbackOnly,