diff --git a/assets/black_1x1.png b/assets/black_1x1.png new file mode 100644 index 0000000..92f7ea3 Binary files /dev/null and b/assets/black_1x1.png differ diff --git a/assets/transparent_1x1.png b/assets/transparent_1x1.png new file mode 100644 index 0000000..1914264 Binary files /dev/null and b/assets/transparent_1x1.png differ diff --git a/lib/services/map_data_submodules/tiles_from_osm.dart b/lib/services/map_data_submodules/tiles_from_osm.dart index 1436b2f..f461df4 100644 --- a/lib/services/map_data_submodules/tiles_from_osm.dart +++ b/lib/services/map_data_submodules/tiles_from_osm.dart @@ -23,12 +23,16 @@ Future> fetchOSMTile({ print('[fetchOSMTile] FETCH $z/$x/$y'); attempt++; final resp = await http.get(Uri.parse(url)); - if (resp.statusCode == 200) { + print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}'); + if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) { + print('[fetchOSMTile] SUCCESS $z/$x/$y'); return resp.bodyBytes; } else { + print('[fetchOSMTile] FAIL $z/$x/$y: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}'); throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}'); } } catch (e) { + print('[fetchOSMTile] Exception $z/$x/$y: $e'); if (attempt >= maxAttempts) { print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e"); rethrow; diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 46a4e3a..b646ce3 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -19,61 +19,7 @@ import '../services/offline_area_service.dart'; import '../models/osm_camera_node.dart'; import 'debouncer.dart'; import 'camera_tag_sheet.dart'; - -class DataProviderTileProvider extends TileProvider { - @override - ImageProvider getImage(TileCoordinates coords, TileLayer options) { - print('[DataProviderTileProvider] getImage called for \\${coords.z}/\\${coords.x}/\\${coords.y}'); - return DataProviderImage(coords, options); - } -} - -class DataProviderImage extends ImageProvider { - final TileCoordinates coords; - final TileLayer options; - DataProviderImage(this.coords, this.options); - - @override - Future obtainKey(ImageConfiguration configuration) { - return SynchronousFuture(this); - } - - @override - ImageStreamCompleter load( - DataProviderImage key, - Future Function(Uint8List, {int? cacheWidth, int? cacheHeight}) decode) { - return MultiFrameImageStreamCompleter( - codec: _loadAsync(key, decode), - scale: 1.0, - ); - } - - Future _loadAsync(DataProviderImage key, Future Function(Uint8List, {int? cacheWidth, int? cacheHeight}) decode) async { - final z = key.coords.z; - final x = key.coords.x; - final y = key.coords.y; - print('[_loadAsync] Called for $z/$x/$y'); - try { - final bytes = await MapDataProvider().getTile(z: z, x: x, y: y); - print('[_loadAsync] Got bytes for $z/$x/$y: length=\\${bytes.length}'); - if (bytes.isEmpty) throw Exception("Empty image bytes for $z/$x/$y"); - return await decode(Uint8List.fromList(bytes)); - } catch (e) { - // Optionally: provide an error tile - print('[MapView] Failed to load OSM tile for $z/$x/$y: $e'); - // Return a blank pixel or a fallback error tile of your design - return await decode(Uint8List.fromList(_transparentPng)); - } - } - - // A tiny 1x1 transparent PNG - static const List _transparentPng = [ - 137,80,78,71,13,10,26,10,0,0,0,13,73,72,68,82, - 0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137, - 0,0,0,10,73,68,65,84,8,153,99,0,1,0,0,5, - 0,1,13,10,42,100,0,0,0,0,73,69,78,68,174,66, - 96,130]; -} +import 'tile_provider_with_cache.dart'; // --- Smart marker widget for camera with single/double tap distinction class _CameraMapMarker extends StatefulWidget { @@ -292,10 +238,32 @@ class _MapViewState extends State { ), children: [ TileLayer( - tileProvider: DataProviderTileProvider(), - urlTemplate: 'unused-{z}-{x}-{y}', // Required by flutter_map for tile addressing + tileProvider: TileProviderWithCache( + onTileCacheUpdated: () { if (mounted) setState(() {}); }, + ), + urlTemplate: 'unused-{z}-{x}-{y}', tileSize: 256, - // Any other TileLayer customization as needed + tileBuilder: (ctx, tileWidget, tileImage) { + try { + final str = tileImage.toString(); + final regex = RegExp(r'TileCoordinate\((\d+), (\d+), (\d+)\)'); + final match = regex.firstMatch(str); + if (match != null) { + final x = match.group(1); + final y = match.group(2); + final z = match.group(3); + final key = '$z/$x/$y'; + final bytes = TileProviderWithCache.tileCache[key]; + if (bytes != null && bytes.isNotEmpty) { + return Image.memory(bytes, gaplessPlayback: true, fit: BoxFit.cover); + } + } + return Image.asset('assets/transparent_1x1.png', gaplessPlayback: true, fit: BoxFit.cover); + } catch (e) { + print('tileBuilder error: $e for tileImage: ${tileImage.toString()}'); + return tileWidget; + } + } ), PolygonLayer(polygons: overlays), MarkerLayer(markers: markers), diff --git a/lib/widgets/tile_provider_with_cache.dart b/lib/widgets/tile_provider_with_cache.dart new file mode 100644 index 0000000..7100656 --- /dev/null +++ b/lib/widgets/tile_provider_with_cache.dart @@ -0,0 +1,45 @@ +import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter/scheduler.dart'; +import '../services/map_data_provider.dart'; + +/// Singleton in-memory tile cache and async provider for custom tiles. +class TileProviderWithCache extends TileProvider { + static final Map _tileCache = {}; + static Map get tileCache => _tileCache; + final VoidCallback? onTileCacheUpdated; + + TileProviderWithCache({this.onTileCacheUpdated}); + + @override + ImageProvider getImage(TileCoordinates coords, TileLayer options) { + final key = '${coords.z}/${coords.x}/${coords.y}'; + if (_tileCache.containsKey(key)) { + return MemoryImage(_tileCache[key]!); + } else { + _fetchAndCacheTile(coords, key); + // Return a transparent PNG until the tile is available. + return const AssetImage('assets/transparent_1x1.png'); + } + } + + void _fetchAndCacheTile(TileCoordinates coords, String key) async { + // Don't fire multiple fetches for the same tile simultaneously + if (_tileCache.containsKey(key)) return; + try { + final bytes = await MapDataProvider().getTile(z: coords.z, x: coords.x, y: coords.y); + if (bytes.isNotEmpty) { + _tileCache[key] = Uint8List.fromList(bytes); + print('[TileProviderWithCache] Cached tile $key, bytes=${bytes.length}'); + if (onTileCacheUpdated != null) { + SchedulerBinding.instance.addPostFrameCallback((_) => onTileCacheUpdated!()); + } + } + } catch (e) { + print('[TileProviderWithCache] Error fetching tile $key: $e'); + // Optionally: fall back to a different asset, or record failures + } + } +} diff --git a/pubspec.yaml b/pubspec.yaml index dd10035..06e6a4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,3 +32,5 @@ flutter: assets: - assets/info.txt + - assets/transparent_1x1.png + - assets/black_1x1.png