|
| 1 | +import 'dart:ui'; |
| 2 | + |
| 3 | +import 'package:flutter/foundation.dart'; |
| 4 | +import 'package:flutter/painting.dart'; |
| 5 | +import 'package:flutter_map/flutter_map.dart'; |
| 6 | +import 'package:flutter_map/src/layer/tile_layer.dart'; |
| 7 | +import './tile_storage_caching_manager.dart'; |
| 8 | +import 'package:flutter_map/src/layer/tile_provider/tile_provider.dart'; |
| 9 | +import 'package:http/http.dart' as http; |
| 10 | +import 'package:tuple/tuple.dart'; |
| 11 | +export './tile_storage_caching_manager.dart'; |
| 12 | + |
| 13 | +///Provider that persist loaded raster tiles inside local sqlite db |
| 14 | +/// [cachedValidDuration] - valid time period since [DateTime.now] |
| 15 | +/// which determines the need for a request for remote tile server. Default value |
| 16 | +/// is one day, that means - all cached tiles today and day before don't need rewriting. |
| 17 | +class StorageCachingTileProvider extends TileProvider { |
| 18 | + static final kMaxPreloadTileAreaCount = 3000; |
| 19 | + final Duration cachedValidDuration; |
| 20 | + |
| 21 | + StorageCachingTileProvider( |
| 22 | + {this.cachedValidDuration = const Duration(days: 1)}); |
| 23 | + |
| 24 | + @override |
| 25 | + ImageProvider getImage(Coords<num> coords, TileLayerOptions options) { |
| 26 | + final tileUrl = getTileUrl(coords, options); |
| 27 | + return CachedTileImageProvider(tileUrl, |
| 28 | + Coords<int>(coords.x.toInt(), coords.y.toInt())..z = coords.z.toInt()); |
| 29 | + } |
| 30 | + |
| 31 | + /// Caching tile area by provided [bounds], zoom edges and [options]. |
| 32 | + /// The maximum number of tiles to load is [kMaxPreloadTileAreaCount]. |
| 33 | + /// To check tiles number before calling this method, use |
| 34 | + /// [approximateTileAmount]. |
| 35 | + /// Return [Tuple3] with uploaded tile index as [Tuple3.item1], |
| 36 | + /// errors count as [Tuple3.item2], and total tiles count need to be downloaded |
| 37 | + /// as [Tuple3.item3] |
| 38 | + Stream<Tuple3<int, int, int>> loadTiles( |
| 39 | + LatLngBounds bounds, int minZoom, int maxZoom, TileLayerOptions options, |
| 40 | + {Function(dynamic) errorHandler}) async* { |
| 41 | + final tilesRange = approximateTileRange( |
| 42 | + bounds: bounds, |
| 43 | + minZoom: minZoom, |
| 44 | + maxZoom: maxZoom, |
| 45 | + tileSize: CustomPoint(options.tileSize, options.tileSize)); |
| 46 | + assert(tilesRange.length <= kMaxPreloadTileAreaCount, |
| 47 | + '${tilesRange.length} to many tiles for caching'); |
| 48 | + var errorsCount = 0; |
| 49 | + for (var i = 0; i < tilesRange.length; i++) { |
| 50 | + try { |
| 51 | + final cord = tilesRange[i]; |
| 52 | + final url = getTileUrl(cord, options); |
| 53 | + // get network tile |
| 54 | + final bytes = (await http.get(Uri.parse(url))).bodyBytes; |
| 55 | + // save tile to cache |
| 56 | + await TileStorageCachingManager.saveTile(bytes, cord); |
| 57 | + } catch (e) { |
| 58 | + errorsCount++; |
| 59 | + if (errorHandler != null) errorHandler(e); |
| 60 | + } |
| 61 | + yield Tuple3(i + 1, errorsCount, tilesRange.length); |
| 62 | + } |
| 63 | + } |
| 64 | + |
| 65 | + ///Get approximate tile amount from bounds and zoom edges. |
| 66 | + ///[crs] and [tileSize] is optional. |
| 67 | + static int approximateTileAmount( |
| 68 | + {@required LatLngBounds bounds, |
| 69 | + @required int minZoom, |
| 70 | + @required int maxZoom, |
| 71 | + Crs crs = const Epsg3857(), |
| 72 | + tileSize = const CustomPoint(256, 256)}) { |
| 73 | + assert(minZoom <= maxZoom, 'minZoom > maxZoom'); |
| 74 | + var amount = 0; |
| 75 | + for (var zoomLevel in List<int>.generate( |
| 76 | + maxZoom - minZoom + 1, (index) => index + minZoom)) { |
| 77 | + final nwPoint = crs |
| 78 | + .latLngToPoint(bounds.northWest, zoomLevel.toDouble()) |
| 79 | + .unscaleBy(tileSize) |
| 80 | + .floor(); |
| 81 | + final sePoint = crs |
| 82 | + .latLngToPoint(bounds.southEast, zoomLevel.toDouble()) |
| 83 | + .unscaleBy(tileSize) |
| 84 | + .ceil() - |
| 85 | + CustomPoint(1, 1); |
| 86 | + final a = sePoint.x - nwPoint.x + 1; |
| 87 | + final b = sePoint.y - nwPoint.y + 1; |
| 88 | + amount += a * b; |
| 89 | + } |
| 90 | + return amount; |
| 91 | + } |
| 92 | + |
| 93 | + ///Get tileRange from bounds and zoom edges. |
| 94 | + ///[crs] and [tileSize] is optional. |
| 95 | + static List<Coords> approximateTileRange( |
| 96 | + {@required LatLngBounds bounds, |
| 97 | + @required int minZoom, |
| 98 | + @required int maxZoom, |
| 99 | + Crs crs = const Epsg3857(), |
| 100 | + tileSize = const CustomPoint(256, 256)}) { |
| 101 | + assert(minZoom <= maxZoom, 'minZoom > maxZoom'); |
| 102 | + final cords = <Coords>[]; |
| 103 | + for (var zoomLevel in List<int>.generate( |
| 104 | + maxZoom - minZoom + 1, (index) => index + minZoom)) { |
| 105 | + final nwPoint = crs |
| 106 | + .latLngToPoint(bounds.northWest, zoomLevel.toDouble()) |
| 107 | + .unscaleBy(tileSize) |
| 108 | + .floor(); |
| 109 | + final sePoint = crs |
| 110 | + .latLngToPoint(bounds.southEast, zoomLevel.toDouble()) |
| 111 | + .unscaleBy(tileSize) |
| 112 | + .ceil() - |
| 113 | + CustomPoint(1, 1); |
| 114 | + for (var x = nwPoint.x; x <= sePoint.x; x++) { |
| 115 | + for (var y = nwPoint.y; y <= sePoint.y; y++) { |
| 116 | + cords.add(Coords(x, y)..z = zoomLevel); |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + return cords; |
| 121 | + } |
| 122 | +} |
| 123 | + |
| 124 | +class CachedTileImageProvider extends ImageProvider<Coords<int>> { |
| 125 | + final Function(dynamic) netWorkErrorHandler; |
| 126 | + final String url; |
| 127 | + final Coords<int> coords; |
| 128 | + final Duration cacheValidDuration; |
| 129 | + |
| 130 | + CachedTileImageProvider(this.url, this.coords, |
| 131 | + {this.cacheValidDuration = const Duration(days: 1), |
| 132 | + this.netWorkErrorHandler}); |
| 133 | + |
| 134 | + @override |
| 135 | + ImageStreamCompleter load(Coords<int> key, decode) => |
| 136 | + MultiFrameImageStreamCompleter( |
| 137 | + codec: _loadAsync(), |
| 138 | + scale: 1, |
| 139 | + informationCollector: () sync* { |
| 140 | + yield DiagnosticsProperty<ImageProvider>('Image provider', this); |
| 141 | + yield DiagnosticsProperty<Coords>('Image key', key); |
| 142 | + }); |
| 143 | + |
| 144 | + @override |
| 145 | + Future<Coords<int>> obtainKey(ImageConfiguration configuration) => |
| 146 | + SynchronousFuture(coords); |
| 147 | + |
| 148 | + Future<Codec> _loadAsync() async { |
| 149 | + final localBytes = await TileStorageCachingManager.getTile(coords); |
| 150 | + var bytes = localBytes?.item1; |
| 151 | + if ((DateTime.now().millisecondsSinceEpoch - |
| 152 | + (localBytes?.item2?.millisecondsSinceEpoch ?? 0)) > |
| 153 | + cacheValidDuration.inMilliseconds) { |
| 154 | + try { |
| 155 | + // get network tile |
| 156 | + bytes = (await http.get(Uri.parse(url))).bodyBytes; |
| 157 | + // save tile to cache |
| 158 | + await TileStorageCachingManager.saveTile(bytes, coords); |
| 159 | + } catch (e) { |
| 160 | + if (netWorkErrorHandler != null) netWorkErrorHandler(e); |
| 161 | + } |
| 162 | + } |
| 163 | + if (bytes == null) { |
| 164 | + return Future<Codec>.error('Failed to load tile for coords: $coords'); |
| 165 | + } |
| 166 | + return await PaintingBinding.instance.instantiateImageCodec(bytes); |
| 167 | + } |
| 168 | +} |
0 commit comments