diff --git a/lib/services/map_data_submodules/tiles_from_osm.dart b/lib/services/map_data_submodules/tiles_from_osm.dart index f461df4..feefc97 100644 --- a/lib/services/map_data_submodules/tiles_from_osm.dart +++ b/lib/services/map_data_submodules/tiles_from_osm.dart @@ -1,8 +1,13 @@ import 'dart:math'; import 'dart:io'; +import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:flutter/foundation.dart'; -/// Fetches a tile from OSM, with in-memory retries/backoff. +/// Global semaphore to limit simultaneous tile fetches +final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent + +/// Fetches a tile from OSM, with in-memory retries/backoff, and global concurrency limit. /// Returns tile image bytes, or throws on persistent failure. Future> fetchOSMTile({ required int z, @@ -19,6 +24,7 @@ Future> fetchOSMTile({ 10000 + random.nextInt(4000) - 2000 ]; while (true) { + await _tileFetchSemaphore.acquire(); try { print('[fetchOSMTile] FETCH $z/$x/$y'); attempt++; @@ -40,6 +46,36 @@ Future> fetchOSMTile({ final delay = delays[attempt - 1].clamp(0, 60000); print("[fetchOSMTile] Attempt $attempt for $z/$x/$y failed: $e. Retrying in ${delay}ms."); await Future.delayed(Duration(milliseconds: delay)); + } finally { + _tileFetchSemaphore.release(); } } } + +/// Simple counting semaphore, suitable for single-thread Flutter concurrency +class _SimpleSemaphore { + final int _max; + int _current = 0; + final List _queue = []; + _SimpleSemaphore(this._max); + + Future acquire() async { + if (_current < _max) { + _current++; + return; + } else { + final c = Completer(); + _queue.add(() => c.complete()); + await c.future; + } + } + + void release() { + if (_queue.isNotEmpty) { + final callback = _queue.removeAt(0); + callback(); + } else { + _current--; + } + } +} \ No newline at end of file diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index b646ce3..0e5a0c6 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -86,6 +86,7 @@ class _MapViewState extends State { late final MapController _controller; final MapDataProvider _mapDataProvider = MapDataProvider(); final Debouncer _debounce = Debouncer(const Duration(milliseconds: 500)); + Debouncer? _debounceTileLayerUpdate; StreamSubscription? _positionSub; LatLng? _currentLatLng; @@ -111,6 +112,7 @@ class _MapViewState extends State { @override void initState() { super.initState(); + _debounceTileLayerUpdate = Debouncer(const Duration(milliseconds: 50),); // Kick off offline area loading as soon as map loads OfflineAreaService(); _controller = widget.controller; @@ -239,7 +241,9 @@ class _MapViewState extends State { children: [ TileLayer( tileProvider: TileProviderWithCache( - onTileCacheUpdated: () { if (mounted) setState(() {}); }, + onTileCacheUpdated: () { + if (_debounceTileLayerUpdate != null) _debounceTileLayerUpdate!(() { if (mounted) setState(() {}); }); + }, ), urlTemplate: 'unused-{z}-{x}-{y}', tileSize: 256,