From ec2130ea4d37d8d0ca63ba79558d6877bc25a8a1 Mon Sep 17 00:00:00 2001 From: JaffaKetchup Date: Fri, 4 Oct 2024 00:02:23 +0100 Subject: [PATCH] Example app experimentation --- .../components/greyscale_masker.dart | 483 ++++++++++-------- .../src/screens/main/map_view/map_view.dart | 120 ++++- lib/src/bulk_download/internal/manager.dart | 1 + 3 files changed, 376 insertions(+), 228 deletions(-) diff --git a/example/lib/src/screens/main/map_view/components/download_progress/components/greyscale_masker.dart b/example/lib/src/screens/main/map_view/components/download_progress/components/greyscale_masker.dart index 70c5a0fa..04efd474 100644 --- a/example/lib/src/screens/main/map_view/components/download_progress/components/greyscale_masker.dart +++ b/example/lib/src/screens/main/map_view/components/download_progress/components/greyscale_masker.dart @@ -6,21 +6,21 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_map/flutter_map.dart'; -import 'package:latlong2/latlong.dart'; +import 'package:latlong2/latlong.dart' hide Path; class GreyscaleMasker extends SingleChildRenderObjectWidget { const GreyscaleMasker({ super.key, - super.child, + required super.child, + required this.tileCoordinatesStream, required this.mapCamera, - required this.tileCoordinates, required this.minZoom, required this.maxZoom, - required this.tileSize, + this.tileSize = 256, }); + final Stream tileCoordinatesStream; final MapCamera mapCamera; - final Stream tileCoordinates; final int minZoom; final int maxZoom; final int tileSize; @@ -29,7 +29,7 @@ class GreyscaleMasker extends SingleChildRenderObjectWidget { RenderObject createRenderObject(BuildContext context) => _GreyscaleMaskerRenderer( mapCamera: mapCamera, - tileCoordinates: tileCoordinates, + tileCoordinatesStream: tileCoordinatesStream, minZoom: minZoom, maxZoom: maxZoom, tileSize: tileSize, @@ -42,74 +42,116 @@ class GreyscaleMasker extends SingleChildRenderObjectWidget { _GreyscaleMaskerRenderer renderObject, ) { renderObject.mapCamera = mapCamera; + // We don't support changing the other properties. They should not change + // during a download. } } class _GreyscaleMaskerRenderer extends RenderProxyBox { _GreyscaleMaskerRenderer({ + required Stream tileCoordinatesStream, required MapCamera mapCamera, - required Stream tileCoordinates, required this.minZoom, required this.maxZoom, required this.tileSize, }) : assert( maxZoom - minZoom < 32, - 'Unable to store large numbers that result from handling `maxZoom` ' - '- `minZoom`', + 'Unable to work with the large numbers that result from handling the ' + 'difference of `maxZoom` & `minZoom`', ), _mapCamera = mapCamera { - // Precalculate for more efficient percentage calculations later - _possibleSubtilesCountPerZoomLevel = Uint64List((maxZoom - minZoom) + 1); + // Precalculate for more efficient greyscale amount calculations later + _maxSubtilesCountPerZoomLevel = Uint64List((maxZoom - minZoom) + 1); int p = 0; for (int i = minZoom; i < maxZoom; i++) { - _possibleSubtilesCountPerZoomLevel[p] = pow(4, maxZoom - i).toInt(); + _maxSubtilesCountPerZoomLevel[p] = pow(4, maxZoom - i).toInt(); p++; } - _possibleSubtilesCountPerZoomLevel[p] = 0; + _maxSubtilesCountPerZoomLevel[p] = 0; // Handle incoming tile coordinates - tileCoordinates.listen(_incomingTileHandler); + tileCoordinatesStream.listen(_incomingTileHandler); } + //! PROPERTIES + MapCamera _mapCamera; MapCamera get mapCamera => _mapCamera; set mapCamera(MapCamera value) { if (value == mapCamera) return; _mapCamera = value; + _recompileGreyscalePathCache(); markNeedsPaint(); } + /// Minimum zoom level of the download + /// + /// The difference of [maxZoom] & [minZoom] must be less than 32, due to + /// limitations with 64-bit integers. final int minZoom; + + /// Maximum zoom level of the download + /// + /// The difference of [maxZoom] & [minZoom] must be less than 32, due to + /// limitations with 64-bit integers. final int maxZoom; + + /// Size of each tile in pixels final int tileSize; + //! STATE + + /// Stream subscription for input `tileCoordinates` stream late final StreamSubscription _tileCoordinatesSub; - /// Maps tiles of a download to the number of subtiles downloaded + /// Maps tiles of a download to a [_TileMappingValue], which contains: + /// * the number of subtiles downloaded + /// * the lat/lng coordinates of the tile's top-left (North-West) & + /// bottom-left (South-East) corners, which is cached to improve + /// performance when re-projecting to screen space /// /// Due to the multi-threaded nature of downloading, it is important to note - /// when modifying this map that the parentist tile may not yet be - /// registered in the map if it has been queued for another thread. In this - /// case, the value should be initialised to 0, then the thread which - /// eventually downloads the parentist tile should increment the value. With - /// the exception of this case, the existence of a tile key is an indication - /// that that parent tile has been downloaded. - /// - /// TODO: Use minZoom system and another 'temp' mapping to prevent the issue - /// above by treating as minZoom until minnerZoom. - /// - /// The map assigned must be immutable: it must be reconstructed for every - /// update. - final Map _tileMapping = SplayTreeMap( + /// when modifying this map that the root tile may not yet be registered in + /// the map if it has been queued for another thread. In this case, the value + /// should be initialised to 0, then the thread which eventually downloads the + /// root tile should increment the value. With the exception of this case, the + /// existence of a tile key is an indication that that parent tile has been + /// downloaded. + final Map _tileMapping = SplayTreeMap( (a, b) => a.z.compareTo(b.z) | a.x.compareTo(b.x) | a.y.compareTo(b.y), ); - //final Set _tempTileStorage = {}; - /// The number of subtiles a tile at the zoom level (index) may have - late final Uint64List _possibleSubtilesCountPerZoomLevel; + late final Uint64List _maxSubtilesCountPerZoomLevel; + + /// Cache for a greyscale amount to the path that should be painted with that + /// greyscale level + /// + /// The key is multiplied by 1/[_greyscaleLevelsCount] to give the greyscale + /// percentage. This means there are [_greyscaleLevelsCount] levels of + /// greyscale available. Because the difference between close greyscales is + /// very difficult to percieve with the eye, this is acceptable, and improves + /// performance drastically. The ideal amount is calculated and rounded to the + /// nearest level. + static const _greyscaleLevelsCount = 25; + final Map _greyscalePathCache = Map.unmodifiable({ + for (int i = 0; i <= _greyscaleLevelsCount; i++) i: Path(), + }); - static ColorFilter _grayscale(double percentage) { + @override + void dispose() { + _tileCoordinatesSub.cancel(); + super.dispose(); + } + + //! GREYSCALE HANDLING + + /// Calculate the grayscale color filter given a percentage + /// + /// 1 is fully greyscale, 0 is fully original color. + /// + /// From https://www.w3.org/TR/filter-effects-1/#grayscaleEquivalent. + static ColorFilter _generateGreyscaleFilter(double percentage) { final amount = 1 - percentage; return ColorFilter.matrix([ (0.2126 + 0.7874 * amount), @@ -135,29 +177,33 @@ class _GreyscaleMaskerRenderer extends RenderProxyBox { ]); } - /*final targetCoord = const LatLng(45.3271, 14.4422); - late final targetTiles = List.generate( - 7, - (z) { - final zoom = z + 12; - final (x, y) = mapCamera.crs - .latLngToXY(targetCoord, mapCamera.crs.scale(zoom.toDouble())); - return TileCoordinates( - (x / tileSize).floor(), - (y / tileSize).floor(), - zoom, - ); - }, - );*/ + /// Calculate the greyscale level given the number of subtiles actually + /// downloaded and the possible number of subtiles + /// + /// Multiply by 1/[_greyscaleLevelsCount] to pass to [_generateGreyscaleFilter] + /// to generate [ColorFilter]. + int _calculateGreyscaleLevel(int subtilesCount, int maxSubtilesCount) { + assert( + subtilesCount <= maxSubtilesCount, + '`subtilesCount` must be less than or equal to `maxSubtilesCount`', + ); - // Generate fresh layer handles lazily, as many as is needed - // - // Required to allow the child to be painted multiple times. - final _layerHandles = Iterable.generate( - double.maxFinite.toInt(), - (_) => LayerHandle(), - ); + final invGreyscalePercentage = + (subtilesCount + 1) / (maxSubtilesCount + 1); // +1 to count self + return _greyscaleLevelsCount - + (invGreyscalePercentage * _greyscaleLevelsCount).round(); + } + //! INPUT STREAM HANDLING + + /// Recursively work towards the root tile at the [absMinZoom] (usually + /// [minZoom]) given a [tile] + /// + /// [zoomLevelCallback] is invoked with the tile at each recursed zoom level, + /// including the original and [absMinZoom] level. + /// + /// In general we recurse towards the root tile because the download occurs + /// from the root tile towards the leaf tiles. static TileCoordinates _recurseTileToMinZoomLevelParentWithCallback( TileCoordinates tile, int absMinZoom, @@ -179,217 +225,220 @@ class _GreyscaleMaskerRenderer extends RenderProxyBox { ); } + /// Project specified coordinates to a screen space [Rect] + Rect _calculateRectOfCoords(LatLng nwCoord, LatLng seCoord) { + final nwScreen = mapCamera.latLngToScreenPoint(nwCoord); + final seScreen = mapCamera.latLngToScreenPoint(seCoord); + return Rect.fromPoints(nwScreen.toOffset(), seScreen.toOffset()); + } + + /// Handles incoming tiles from the input stream, modifying the [_tileMapping] + /// and [_greyscalePathCache] as necessary + /// + /// Tiles are pruned from the tile mapping where the parent tile has maxed out + /// the number of subtiles (ie. all this tile's neighbours within the quad of + /// the parent are also downloaded), to save memory space. However, it is + /// not possible to prune the path cache, so this will slowly become + /// out-of-sync and less efficient. See [_recompileGreyscalePathCache] + /// for details. void _incomingTileHandler(TileCoordinates tile) { assert(tile.z >= minZoom, 'Incoming `tile` has zoom level below minimum'); assert(tile.z <= maxZoom, 'Incoming `tile` has zoom level above maximum'); - //print(tile); - _recurseTileToMinZoomLevelParentWithCallback( tile, minZoom, (intermediateZoomTile) { - final maxSubtilesCount = _possibleSubtilesCountPerZoomLevel[ - intermediateZoomTile.z - minZoom]; - //print('${intermediateZoomTile.z}: $maxSubtilesCount'); - - if (_tileMapping[intermediateZoomTile] case final existingValue?) { - /*assert( - existingValue < maxSubtilesCount, - 'Existing subtiles count cannot be larger than theoretical max ' - 'subtiles count ($intermediateZoomTile: $existingValue >= ' - '$maxSubtilesCount)', - );*/ - - /*if (existingValue + 1 == maxSubtilesCount && - _tileMapping[TileCoordinates( - tile.x ~/ 2, - tile.y ~/ 2, - tile.z - 1, - )] == - _possibleSubtilesCountPerZoomLevel[tile.z - 1 - minZoom] - - 1) { - _tileMapping.remove(intermediateZoomTile); - debugPrint( - 'Removing $intermediateZoomTile, reached max subtiles count of ' - '$maxSubtilesCount & parent contains max tiles', + final maxSubtilesCount = + _maxSubtilesCountPerZoomLevel[intermediateZoomTile.z - minZoom]; + + final _TileMappingValue tmv; + if (_tileMapping[intermediateZoomTile] case final existingTMV?) { + try { + assert( + existingTMV.subtilesCount < maxSubtilesCount, + 'Existing subtiles count must be smaller than max subtiles count ' + '($intermediateZoomTile: ${existingTMV.subtilesCount} !< ' + '$maxSubtilesCount)', ); - } else {*/ - _tileMapping[intermediateZoomTile] = existingValue + 1; - //} + } catch (e) { + print(tile); + print(intermediateZoomTile); + rethrow; + } + + existingTMV.subtilesCount += 1; + tmv = existingTMV; } else { - /*if (maxSubtilesCount == 0 && - _tileMapping[TileCoordinates( - tile.x ~/ 2, - tile.y ~/ 2, - tile.z - 1, - )] == - _possibleSubtilesCountPerZoomLevel[tile.z - 1 - minZoom] - - 1) { - debugPrint('Not making new key $intermediateZoomTile'); - } else {*/ - _tileMapping[intermediateZoomTile] = 0; - //debugPrint('Making new key $intermediateZoomTile'); - //} + final zoom = tile.z.toDouble(); + _tileMapping[intermediateZoomTile] = tmv = _TileMappingValue.newTile( + nwCoord: mapCamera.crs.pointToLatLng(tile * tileSize, zoom), + seCoord: mapCamera.crs + .pointToLatLng((tile + const Point(1, 1)) * tileSize, zoom), + ); } - /*if (_tileMapping[intermediateZoomTile] case final existingValue?) { - _tileMapping[intermediateZoomTile] = existingValue + 1; - } else { - _tempTileStorage.add(intermediateZoomTile); - assert( - _tempTileStorage.length < 50, - 'CAUTION! Temp buffer too full. Likely a bug, or too many threads & small region.', - ); - }*/ + _greyscalePathCache[ + _calculateGreyscaleLevel(tmv.subtilesCount, maxSubtilesCount)]! + .addRect(_calculateRectOfCoords(tmv.nwCoord, tmv.seCoord)); + + late final isParentMaxedOut = _tileMapping[TileCoordinates( + intermediateZoomTile.x ~/ 2, + intermediateZoomTile.y ~/ 2, + intermediateZoomTile.z - 1, + )] + ?.subtilesCount == + _maxSubtilesCountPerZoomLevel[ + intermediateZoomTile.z - 1 - minZoom] - + 1; + if (intermediateZoomTile.z != minZoom && isParentMaxedOut) { + _tileMapping.remove(intermediateZoomTile); // self + + if (intermediateZoomTile.x.isOdd) { + _tileMapping.remove( + TileCoordinates( + intermediateZoomTile.x - 1, + intermediateZoomTile.y, + intermediateZoomTile.z, + ), + ); + } + if (intermediateZoomTile.y.isOdd) { + _tileMapping.remove( + TileCoordinates( + intermediateZoomTile.x, + intermediateZoomTile.y - 1, + intermediateZoomTile.z, + ), + ); + } + if (intermediateZoomTile.x.isOdd && intermediateZoomTile.y.isOdd) { + _tileMapping.remove( + TileCoordinates( + intermediateZoomTile.x - 1, + intermediateZoomTile.y - 1, + intermediateZoomTile.z, + ), + ); + } + } }, ); - /*final (int, int) parentistTile; + markNeedsPaint(); + } - if (tile.z == minZoom) { - parentistTile = (tile.x, tile.y); - } else { - final parentistTileWithZoom = - _recurseTileToMinZoomLevelParent(tile, minZoom); - parentistTile = (parentistTileWithZoom.x, parentistTileWithZoom.y); + /// Recompile the [_greyscalePathCache] ready for repainting based on the + /// single source-of-truth of the [_tileMapping] + /// + /// --- + /// + /// To avoid mutating the cache directly, for performance, we simply reset + /// all paths, which has the same effect but with less work. + /// + /// Then, for every tile, we calculate its greyscale level using its subtiles + /// count and the maximum number of subtiles in its zoom level, and add to + /// that level's `Path` the new rect. + /// + /// The lat/lng coords for the tile are cached and so do not need to be + /// recalculated. They only need to be reprojected to screen space to handle + /// changes to the map camera. This is more performant. + /// + /// We do not ever need to recurse towards the maximum zoom level. We go in + /// order from highest to lowest zoom level when painting, and if a tile at + /// the highest zoom level is fully downloaded (maxed subtiles), then all + /// subtiles will be 0% greyscale anyway, when this tile is painted at 0% + /// greyscale, so we can save unnecessary painting steps. + /// + /// Therefore, it is likely more efficient to paint after running this method + /// than after a series of incoming tiles have been handled (as + /// [_incomingTileHandler] cannot prune the path cache, only the tile mapping). + /// + /// This method does not call [markNeedsPaint], the caller should perform that + /// if necessary. + void _recompileGreyscalePathCache() { + for (final path in _greyscalePathCache.values) { + path.reset(); } + for (int i = _tileMapping.length - 1; i >= 0; i--) { + final MapEntry(key: tile, value: tmv) = _tileMapping.entries.elementAt(i); - _tileMapping[parentistTile] = (_tileMapping[parentistTile] ?? -1) + 1;*/ - - //print(_tileMapping); - - markNeedsPaint(); + _greyscalePathCache[_calculateGreyscaleLevel( + tmv.subtilesCount, + _maxSubtilesCountPerZoomLevel[tile.z - minZoom], + )]! + .addRect(_calculateRectOfCoords(tmv.nwCoord, tmv.seCoord)); + } } - @override - void dispose() { - _tileCoordinatesSub.cancel(); - super.dispose(); - } + //! PAINTING + + /// Generate fresh layer handles lazily, as many as is needed + /// + /// Required to allow the child to be painted multiple times. + final _layerHandles = Iterable.generate( + double.maxFinite.toInt(), + (_) => LayerHandle(), + ); @override void paint(PaintingContext context, Offset offset) { - /*final rects = targetTiles.map((tile) { - final nw = mapCamera.latLngToScreenPoint( - mapCamera.crs.pointToLatLng(tile * tileSize, tile.z.toDouble()), - ); - final se = mapCamera.latLngToScreenPoint( - mapCamera.crs.pointToLatLng( - (tile + const Point(1, 1)) * tileSize, - tile.z.toDouble(), - ), - ); - return Rect.fromPoints(nw.toOffset(), se.toOffset()); - });*/ - + // Paint the map in greyscale context.pushColorFilter( offset, - _grayscale(1), + _generateGreyscaleFilter(1), (context, offset) => context.paintChild(child!, offset), ); + // Then paint, from colorest to greyscalist (high to low zoom level), each + // layer using the respective `Path` as a clip ('cut') int layerHandleIndex = 0; - for (int i = 0; i < _tileMapping.length; i++) { - final MapEntry(key: tile, value: subtilesCount) = - _tileMapping.entries.elementAt(i); - - final nw = mapCamera.latLngToScreenPoint( - mapCamera.crs.pointToLatLng(tile * tileSize, tile.z.toDouble()), - ); - final se = mapCamera.latLngToScreenPoint( - mapCamera.crs.pointToLatLng( - (tile + const Point(1, 1)) * tileSize, - tile.z.toDouble(), - ), - ); - final rect = Rect.fromPoints(nw.toOffset(), se.toOffset()); + for (int i = _greyscalePathCache.length - 1; i >= 0; i--) { + final MapEntry(key: greyscaleAmount, value: path) = + _greyscalePathCache.entries.elementAt(i); - /*context.canvas.drawRect( - rect, - Paint() - ..style = PaintingStyle.stroke - ..color = Colors.black - ..strokeWidth = 3, - );*/ - - final maxSubtilesCount = - _possibleSubtilesCountPerZoomLevel[tile.z - minZoom]; - - final greyscaleAmount = - maxSubtilesCount == 0 ? 1.0 : (subtilesCount / maxSubtilesCount); + final greyscalePercentage = greyscaleAmount * 1 / 25; _layerHandles.elementAt(layerHandleIndex).layer = context.pushColorFilter( offset, - _grayscale(1 - greyscaleAmount), - (context, offset) => context.pushClipRect( + _generateGreyscaleFilter(greyscalePercentage), + (context, offset) => context.pushClipPath( needsCompositing, offset, - rect, + Offset.zero & size, + path, (context, offset) { context.paintChild(child!, offset); - //context.canvas.clipRect(Offset.zero & size); - //context.canvas.drawColor(Colors.red, BlendMode.src); + /*context.canvas.clipRect(Offset.zero & size); + context.canvas.drawColor( + Colors.green, + BlendMode.modulate, + );*/ }, + clipBehavior: Clip.hardEdge, ), oldLayer: _layerHandles.elementAt(layerHandleIndex).layer, ); layerHandleIndex++; - - // TODO: Change to delete 100%ed tiles (recurse down towards maxzoom) - // TODO: Combine into paths - // TODO: Cache paths between paints unless mapcamera changed } - /*const double chessSize = 100; - final rows = size.height ~/ chessSize; - final cols = size.width ~/ chessSize; + context.canvas.drawCircle(offset, 100, Paint()..color = Colors.blue); + } +} - int i = 0; - for (int r = 0; r < rows; r++) { - for (int c = 0; c < cols; c++) { - /*_clipLayerHandles[childIndex].layer = context.pushClipRect( - needsCompositing, - offset, - Offset.zero & size, - (context, offset) => - context.paintChild(child!, offset + Offset(childIndex * 50, 0)), - oldLayer: _clipLayerHandles[childIndex].layer, - );*/ - layerHandles.elementAt(i).layer = context.pushColorFilter( - offset, - _grayscale(i % 2), - (context, offset) => context.pushClipRect( - true, - offset, - Rect.fromLTWH( - c * chessSize, - r * chessSize, - chessSize, - chessSize, - ), - (context, offset) => context.paintChild(child!, offset), - ), - oldLayer: layerHandles.elementAt(i).layer, - ); - i++; - } - }*/ +/// See [_GreyscaleMaskerRenderer._tileMapping] for documentation +/// +/// Is immutable to improve performance. +class _TileMappingValue { + _TileMappingValue.newTile({ + required this.nwCoord, + required this.seCoord, + }) : subtilesCount = 0; - context.canvas.drawCircle(offset, 100, Paint()..color = Colors.blue); + int subtilesCount; - /*int rectI = 0; - for (final rect in rects) { - context.canvas.drawRect( - rect, - Paint() - ..style = PaintingStyle.stroke - ..color = Colors.black - ..strokeWidth = 3, - ); - rectI++; - }*/ - } + final LatLng nwCoord; + final LatLng seCoord; } 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 8c5d0832..61d500d9 100644 --- a/example/lib/src/screens/main/map_view/map_view.dart +++ b/example/lib/src/screens/main/map_view/map_view.dart @@ -8,6 +8,7 @@ import 'package:flutter_map_tile_caching/flutter_map_tile_caching.dart'; import 'package:http/io_client.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; +import 'package:stream_transform/stream_transform.dart'; import '../../../shared/misc/shared_preferences.dart'; import '../../../shared/misc/store_metadata_keys.dart'; @@ -15,7 +16,6 @@ import '../../../shared/state/general_provider.dart'; import '../../../shared/state/region_selection_provider.dart'; import 'components/debugging_tile_builder/debugging_tile_builder.dart'; import 'components/download_progress/components/greyscale_masker.dart'; -import 'components/download_progress/download_progress_masker.dart'; import 'components/region_selection/crosshairs.dart'; import 'components/region_selection/custom_polygon_snapping_indicator.dart'; import 'components/region_selection/region_shape.dart'; @@ -64,6 +64,63 @@ class _MapViewState extends State with TickerProviderStateMixin { }, ); + final _testingCoordsList = [ + //TileCoordinates(2212, 1468, 12), + //TileCoordinates(2212 * 2, 1468 * 2, 13), + //TileCoordinates(2212 * 2 * 2, 1468 * 2 * 2, 14), + //TileCoordinates(2212 * 2 * 2 * 2, 1468 * 2 * 2 * 2, 15), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2, + 1468 * 2 * 2 * 2 * 2, + 16, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2, + 1468 * 2 * 2 * 2 * 2 * 2, + 17, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2, + 1468 * 2 * 2 * 2 * 2 * 2 * 2, + 18, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 18, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 18, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2 * 2, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 * 2, + 19, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 * 2, + 19, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2 * 2, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 19, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 * 2 + 1, + 19, + ), + const TileCoordinates( + 2212 * 2 * 2 * 2 * 2 * 2 * 2 * 2 + 2, + 1468 * 2 * 2 * 2 * 2 * 2 * 2 * 2 + 2, + 19, + ), + ]; + Stream? _testingDownloadTileCoordsStream; bool _isInRegionSelectMode() => @@ -91,27 +148,29 @@ class _MapViewState extends State with TickerProviderStateMixin { if (!_isInRegionSelectMode()) { setState( () => _testingDownloadTileCoordsStream = - const FMTCStore('Mapbox JaffaKetchup Outdoors') + const FMTCStore('Local Tile Server') .download .startForeground( - region: CircleRegion( + region: const CircleRegion( LatLng(45.3052535669648, 14.476223064038985), - 10, + 5, ).toDownloadable( minZoom: 11, - maxZoom: 18, + maxZoom: 16, options: TileLayer( - urlTemplate: 'http://127.0.0.1:7070/{z}/{x}/{y}', + //urlTemplate: 'http://127.0.0.1:7070/{z}/{x}/{y}', + urlTemplate: + 'https://tile.openstreetmap.org/{z}/{x}/{y}.png', ), ), - parallelThreads: 10, + parallelThreads: 3, skipSeaTiles: false, urlTransformer: (url) => FMTCTileProvider.urlTransformerOmitKeyValues( url: url, keys: ['access_token'], ), - rateLimit: 200, + rateLimit: 20, ) .map( (event) => event.latestTileEvent.coordinates, @@ -162,11 +221,11 @@ class _MapViewState extends State with TickerProviderStateMixin { } }, onSecondaryTap: (_, __) { + const FMTCStore('Local Tile Server').download.cancel(); if (!_isInRegionSelectMode()) return; context.read().removeLastCoordinate(); }, onLongPress: (_, __) { - const FMTCStore('Mapbox JaffaKetchup Outdoors').download.cancel(); if (!_isInRegionSelectMode()) return; context.read().removeLastCoordinate(); }, @@ -361,14 +420,53 @@ class _MapViewState extends State with TickerProviderStateMixin { builder: (context) => GreyscaleMasker( key: const ValueKey(11), mapCamera: MapCamera.of(context), - tileCoordinates: _testingDownloadTileCoordsStream!, + tileCoordinatesStream: + _testingDownloadTileCoordsStream!, + /*tileCoordinates: Stream.periodic( + const Duration(seconds: 5), + _testingCoordsList.elementAtOrNull, + ).whereNotNull(),*/ minZoom: 11, - maxZoom: 18, + maxZoom: 16, tileSize: 256, child: tileLayer, ), ), ), + PolygonLayer( + polygons: [ + Polygon( + points: [ + LatLng(-90, 180), + LatLng(90, 180), + LatLng(90, -180), + LatLng(-90, -180), + ], + holePointsList: [ + const CircleRegion( + LatLng(45.3052535669648, 14.476223064038985), + 6, + ).toOutline().toList(growable: false), + ], + color: Colors.black, + ), + Polygon( + points: [ + LatLng(-90, 180), + LatLng(90, 180), + LatLng(90, -180), + LatLng(-90, -180), + ], + holePointsList: [ + const CircleRegion( + LatLng(45.3052535669648, 14.476223064038985), + 5, + ).toOutline().toList(growable: false), + ], + color: Colors.black.withAlpha(255 ~/ 2), + ), + ], + ), if (widget.mode == MapViewMode.downloadRegion) ...[ const RegionShape(), const CustomPolygonSnappingIndicator(), diff --git a/lib/src/bulk_download/internal/manager.dart b/lib/src/bulk_download/internal/manager.dart index 66246d53..ac082e73 100644 --- a/lib/src/bulk_download/internal/manager.dart +++ b/lib/src/bulk_download/internal/manager.dart @@ -256,6 +256,7 @@ Future _downloadManager( .then((sp) => sp.send(null)), ); + // TODO: Debug whether multiple threads are downloading the same tile downloadThreadReceivePort.listen( (evt) async { // Thread is sending tile data