mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
lot of changes, got rid of custom cache stuff, now stepping in the way of http fetch instead of screwing with flutter map.
This commit is contained in:
@@ -5,7 +5,7 @@ import 'app_state.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'screens/settings_screen.dart';
|
||||
|
||||
import 'widgets/tile_provider_with_cache.dart';
|
||||
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:provider/provider.dart';
|
||||
import '../app_state.dart';
|
||||
import '../dev_config.dart';
|
||||
import '../widgets/map_view.dart';
|
||||
import '../widgets/tile_provider_with_cache.dart';
|
||||
|
||||
import '../widgets/add_camera_sheet.dart';
|
||||
import '../widgets/camera_provider_with_cache.dart';
|
||||
import '../widgets/download_area_dialog.dart';
|
||||
@@ -41,7 +41,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider<TileProviderWithCache>(create: (_) => TileProviderWithCache()),
|
||||
ChangeNotifierProvider<CameraProviderWithCache>(create: (_) => CameraProviderWithCache()),
|
||||
],
|
||||
child: Scaffold(
|
||||
|
||||
@@ -144,4 +144,9 @@ class MapDataProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear any queued tile requests (call when map view changes significantly)
|
||||
void clearTileQueue() {
|
||||
clearOSMTileQueue();
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,10 @@ import '../network_status.dart';
|
||||
/// Global semaphore to limit simultaneous tile fetches
|
||||
final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
|
||||
|
||||
/// Cancellation token to invalidate all pending requests
|
||||
int _globalCancelToken = 0;
|
||||
|
||||
/// Clear queued tile requests and cancel all retries
|
||||
/// Clear queued tile requests when map view changes significantly
|
||||
void clearOSMTileQueue() {
|
||||
final oldToken = _globalCancelToken;
|
||||
_globalCancelToken++; // Invalidate all pending requests and retries
|
||||
final clearedCount = _tileFetchSemaphore.clearQueue();
|
||||
debugPrint('[OSMTiles] Cancel token: $oldToken -> $_globalCancelToken, cleared $clearedCount queued');
|
||||
debugPrint('[OSMTiles] Cleared $clearedCount queued tile requests');
|
||||
}
|
||||
|
||||
/// Fetches a tile from OSM, with in-memory retries/backoff, and global concurrency limit.
|
||||
@@ -37,17 +32,7 @@ Future<List<int>> fetchOSMTile({
|
||||
kTileFetchThirdDelayMs + random.nextInt(kTileFetchJitter3Ms),
|
||||
];
|
||||
|
||||
// Remember the cancel token when we start this request
|
||||
final requestCancelToken = _globalCancelToken;
|
||||
print('[fetchOSMTile] START $z/$x/$y with token $requestCancelToken (global: $_globalCancelToken)');
|
||||
|
||||
while (true) {
|
||||
// Check if this request was cancelled
|
||||
if (requestCancelToken != _globalCancelToken) {
|
||||
print('[fetchOSMTile] CANCELLED $z/$x/$y (token: $requestCancelToken vs $_globalCancelToken)');
|
||||
throw Exception('Tile request cancelled');
|
||||
}
|
||||
|
||||
await _tileFetchSemaphore.acquire();
|
||||
try {
|
||||
print('[fetchOSMTile] FETCH $z/$x/$y');
|
||||
@@ -55,12 +40,6 @@ Future<List<int>> fetchOSMTile({
|
||||
final resp = await http.get(Uri.parse(url));
|
||||
print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}');
|
||||
|
||||
// Check cancellation after HTTP request completes - this is the key check!
|
||||
if (requestCancelToken != _globalCancelToken) {
|
||||
print('[fetchOSMTile] CANCELLED $z/$x/$y after HTTP (token: $requestCancelToken vs $_globalCancelToken)');
|
||||
throw Exception('Tile request cancelled');
|
||||
}
|
||||
|
||||
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
||||
print('[fetchOSMTile] SUCCESS $z/$x/$y');
|
||||
NetworkStatus.instance.reportOsmTileSuccess();
|
||||
@@ -71,11 +50,6 @@ Future<List<int>> fetchOSMTile({
|
||||
throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
// Don't retry cancelled requests
|
||||
if (e.toString().contains('cancelled')) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
print('[fetchOSMTile] Exception $z/$x/$y: $e');
|
||||
|
||||
// Report network issues on connection errors
|
||||
@@ -92,19 +66,7 @@ Future<List<int>> fetchOSMTile({
|
||||
|
||||
final delay = delays[attempt - 1].clamp(0, 60000);
|
||||
print("[fetchOSMTile] Attempt $attempt for $z/$x/$y failed: $e. Retrying in ${delay}ms.");
|
||||
|
||||
// Check cancellation before and after delay
|
||||
if (requestCancelToken != _globalCancelToken) {
|
||||
print('[fetchOSMTile] CANCELLED $z/$x/$y before retry');
|
||||
throw Exception('Tile request cancelled');
|
||||
}
|
||||
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
|
||||
if (requestCancelToken != _globalCancelToken) {
|
||||
print('[fetchOSMTile] CANCELLED $z/$x/$y after retry delay');
|
||||
throw Exception('Tile request cancelled');
|
||||
}
|
||||
} finally {
|
||||
_tileFetchSemaphore.release();
|
||||
}
|
||||
|
||||
99
lib/services/simple_tile_service.dart
Normal file
99
lib/services/simple_tile_service.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'map_data_provider.dart';
|
||||
|
||||
/// Simple HTTP client that routes tile requests through the centralized MapDataProvider.
|
||||
/// This ensures all tile fetching (offline/online routing, retries, etc.) is in one place.
|
||||
class SimpleTileHttpClient extends http.BaseClient {
|
||||
final http.Client _inner = http.Client();
|
||||
final MapDataProvider _mapDataProvider = MapDataProvider();
|
||||
|
||||
@override
|
||||
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
||||
// Only intercept tile requests to OSM
|
||||
if (request.url.host == 'tile.openstreetmap.org') {
|
||||
return _handleTileRequest(request);
|
||||
}
|
||||
|
||||
// Pass through all other requests
|
||||
return _inner.send(request);
|
||||
}
|
||||
|
||||
Future<http.StreamedResponse> _handleTileRequest(http.BaseRequest request) async {
|
||||
final pathSegments = request.url.pathSegments;
|
||||
|
||||
// Parse z/x/y from URL like: /15/5242/12666.png
|
||||
if (pathSegments.length == 3) {
|
||||
final z = int.tryParse(pathSegments[0]);
|
||||
final x = int.tryParse(pathSegments[1]);
|
||||
final yPng = pathSegments[2];
|
||||
final y = int.tryParse(yPng.replaceAll('.png', ''));
|
||||
|
||||
if (z != null && x != null && y != null) {
|
||||
return _getTile(z, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// Malformed tile URL - pass through to OSM
|
||||
return _inner.send(request);
|
||||
}
|
||||
|
||||
Future<http.StreamedResponse> _getTile(int z, int x, int y) async {
|
||||
try {
|
||||
// Use centralized tile fetching from MapDataProvider
|
||||
final tileBytes = await _mapDataProvider.getTile(z: z, x: x, y: y);
|
||||
debugPrint('[SimpleTileService] Serving tile $z/$x/$y via MapDataProvider');
|
||||
|
||||
return http.StreamedResponse(
|
||||
Stream.value(tileBytes),
|
||||
200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control': 'public, max-age=604800', // 1 week cache
|
||||
'Expires': _httpDateFormat(DateTime.now().add(Duration(days: 7))),
|
||||
'Last-Modified': _httpDateFormat(DateTime.now().subtract(Duration(hours: 1))),
|
||||
},
|
||||
);
|
||||
|
||||
} catch (e) {
|
||||
debugPrint('[SimpleTileService] Tile fetch failed for $z/$x/$y: $e');
|
||||
|
||||
// Return 404 for any failure - let flutter_map handle gracefully
|
||||
return http.StreamedResponse(
|
||||
Stream.value(<int>[]),
|
||||
404,
|
||||
reasonPhrase: 'Tile not available: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear any queued tile requests when map view changes
|
||||
void clearTileQueue() {
|
||||
_mapDataProvider.clearTileQueue();
|
||||
}
|
||||
|
||||
/// Format date for HTTP headers (RFC 7231)
|
||||
String _httpDateFormat(DateTime date) {
|
||||
final utc = date.toUtc();
|
||||
final weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
|
||||
final weekday = weekdays[utc.weekday - 1];
|
||||
final day = utc.day.toString().padLeft(2, '0');
|
||||
final month = months[utc.month - 1];
|
||||
final year = utc.year;
|
||||
final hour = utc.hour.toString().padLeft(2, '0');
|
||||
final minute = utc.minute.toString().padLeft(2, '0');
|
||||
final second = utc.second.toString().padLeft(2, '0');
|
||||
|
||||
return '$weekday, $day $month $year $hour:$minute:$second GMT';
|
||||
}
|
||||
|
||||
@override
|
||||
void close() {
|
||||
_inner.close();
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../widgets/tile_provider_with_cache.dart';
|
||||
|
||||
|
||||
// Enum for upload mode (Production, OSM Sandbox, Simulate)
|
||||
enum UploadMode { production, sandbox, simulate }
|
||||
@@ -49,16 +49,9 @@ class SettingsState extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> setOfflineMode(bool enabled) async {
|
||||
final wasOffline = _offlineMode;
|
||||
_offlineMode = enabled;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_offlineModePrefsKey, enabled);
|
||||
|
||||
if (wasOffline && !enabled) {
|
||||
// Transitioning from offline to online: clear tile cache!
|
||||
TileProviderWithCache.clearCache();
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import '../app_state.dart';
|
||||
import '../services/offline_area_service.dart';
|
||||
import '../services/simple_tile_service.dart';
|
||||
import '../models/osm_camera_node.dart';
|
||||
import '../models/camera_profile.dart';
|
||||
import 'debouncer.dart';
|
||||
import 'tile_provider_with_cache.dart';
|
||||
import 'camera_provider_with_cache.dart';
|
||||
import 'map/camera_markers.dart';
|
||||
import 'map/direction_cones.dart';
|
||||
@@ -36,38 +38,36 @@ class MapView extends StatefulWidget {
|
||||
|
||||
class _MapViewState extends State<MapView> {
|
||||
late final MapController _controller;
|
||||
final Debouncer _debounce = Debouncer(kDebounceCameraRefresh);
|
||||
final Debouncer _cameraDebounce = Debouncer(kDebounceCameraRefresh);
|
||||
final Debouncer _tileDebounce = Debouncer(const Duration(milliseconds: 150));
|
||||
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
LatLng? _currentLatLng;
|
||||
|
||||
late final CameraProviderWithCache _cameraProvider;
|
||||
late final SimpleTileHttpClient _tileHttpClient;
|
||||
|
||||
// Track profile changes to trigger camera refresh
|
||||
List<CameraProfile>? _lastEnabledProfiles;
|
||||
|
||||
// Track offline mode changes to trigger tile refresh
|
||||
bool? _lastOfflineMode;
|
||||
int _mapRebuildCounter = 0;
|
||||
// Track map position to only clear queue on significant changes
|
||||
double? _lastZoom;
|
||||
LatLng? _lastCenter;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// _debounceTileLayerUpdate removed
|
||||
OfflineAreaService();
|
||||
_controller = widget.controller;
|
||||
_tileHttpClient = SimpleTileHttpClient();
|
||||
_initLocation();
|
||||
|
||||
// Set up camera overlay caching
|
||||
_cameraProvider = CameraProviderWithCache.instance;
|
||||
_cameraProvider.addListener(_onCamerasUpdated);
|
||||
|
||||
// Ensure initial overlays are fetched
|
||||
// Fetch initial cameras
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Set up tile refresh callback
|
||||
final tileProvider = Provider.of<TileProviderWithCache>(context, listen: false);
|
||||
tileProvider.setOnTilesCachedCallback(_onTilesCached);
|
||||
|
||||
_refreshCamerasFromProvider();
|
||||
});
|
||||
}
|
||||
@@ -75,17 +75,10 @@ class _MapViewState extends State<MapView> {
|
||||
@override
|
||||
void dispose() {
|
||||
_positionSub?.cancel();
|
||||
_debounce.dispose();
|
||||
_cameraDebounce.dispose();
|
||||
_tileDebounce.dispose();
|
||||
_cameraProvider.removeListener(_onCamerasUpdated);
|
||||
|
||||
// Clean up tile refresh callback
|
||||
try {
|
||||
final tileProvider = Provider.of<TileProviderWithCache>(context, listen: false);
|
||||
tileProvider.setOnTilesCachedCallback(null);
|
||||
} catch (e) {
|
||||
// Context might be disposed already - that's okay
|
||||
}
|
||||
|
||||
_tileHttpClient.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -93,14 +86,7 @@ class _MapViewState extends State<MapView> {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
void _onTilesCached() {
|
||||
// When new tiles are cached, just trigger a widget rebuild
|
||||
// This should cause the TileLayer to re-render with cached tiles
|
||||
debugPrint('[MapView] Tile cached callback triggered, calling setState');
|
||||
if (mounted) {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void _refreshCamerasFromProvider() {
|
||||
final appState = context.read<AppState>();
|
||||
@@ -178,6 +164,19 @@ class _MapViewState extends State<MapView> {
|
||||
return ids1.length == ids2.length && ids1.containsAll(ids2);
|
||||
}
|
||||
|
||||
/// Calculate approximate distance between two LatLng points in meters
|
||||
double _distanceInMeters(LatLng point1, LatLng point2) {
|
||||
const double earthRadius = 6371000; // Earth radius in meters
|
||||
final dLat = (point2.latitude - point1.latitude) * (3.14159 / 180);
|
||||
final dLon = (point2.longitude - point1.longitude) * (3.14159 / 180);
|
||||
final a = math.sin(dLat / 2) * math.sin(dLat / 2) +
|
||||
math.cos(point1.latitude * (3.14159 / 180)) *
|
||||
math.cos(point2.latitude * (3.14159 / 180)) *
|
||||
math.sin(dLon / 2) * math.sin(dLon / 2);
|
||||
final c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a));
|
||||
return earthRadius * c;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appState = context.watch<AppState>();
|
||||
@@ -197,15 +196,6 @@ class _MapViewState extends State<MapView> {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if offline mode changed and force complete map rebuild
|
||||
final currentOfflineMode = appState.offlineMode;
|
||||
if (_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode) {
|
||||
// Offline mode changed - increment counter to force FlutterMap rebuild
|
||||
_mapRebuildCounter++;
|
||||
debugPrint('[MapView] Offline mode changed, forcing map rebuild #$_mapRebuildCounter');
|
||||
}
|
||||
_lastOfflineMode = currentOfflineMode;
|
||||
|
||||
// Seed add‑mode target once, after first controller center is available.
|
||||
if (session != null && session.target == null) {
|
||||
try {
|
||||
@@ -254,7 +244,7 @@ class _MapViewState extends State<MapView> {
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
key: ValueKey('map_rebuild_$_mapRebuildCounter'),
|
||||
key: ValueKey('map_offline_${appState.offlineMode}'),
|
||||
mapController: _controller,
|
||||
options: MapOptions(
|
||||
initialCenter: _currentLatLng ?? LatLng(37.7749, -122.4194),
|
||||
@@ -267,43 +257,39 @@ class _MapViewState extends State<MapView> {
|
||||
appState.updateSession(target: pos.center);
|
||||
}
|
||||
|
||||
// Simple approach: cancel tiles on ANY significant view change
|
||||
final tileProvider = Provider.of<TileProviderWithCache>(context, listen: false);
|
||||
tileProvider.cancelAllTileRequests();
|
||||
// TODO: Re-enable smart queue clearing once we debug tile loading issues
|
||||
// For now, let flutter_map handle its own request management
|
||||
/*
|
||||
// Only clear tile queue on significant movement changes
|
||||
final currentZoom = pos.zoom;
|
||||
final currentCenter = pos.center;
|
||||
final zoomChanged = _lastZoom == null || (currentZoom - _lastZoom!).abs() > 0.1;
|
||||
final centerChanged = _lastCenter == null ||
|
||||
_distanceInMeters(_lastCenter!, currentCenter) > 100; // 100m threshold
|
||||
|
||||
// Request more cameras on any map movement/zoom at valid zoom level
|
||||
// This ensures cameras load even when zooming without panning (like with zoom buttons)
|
||||
if (zoomChanged || centerChanged) {
|
||||
_tileDebounce(() {
|
||||
debugPrint('[MapView] Significant map change - clearing stale tile requests');
|
||||
_tileHttpClient.clearTileQueue();
|
||||
});
|
||||
_lastZoom = currentZoom;
|
||||
_lastCenter = currentCenter;
|
||||
}
|
||||
*/
|
||||
|
||||
// Request more cameras on any map movement/zoom at valid zoom level (slower debounce)
|
||||
if (pos.zoom >= 10) {
|
||||
_debounce(_refreshCamerasFromProvider);
|
||||
_cameraDebounce(_refreshCamerasFromProvider);
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
tileProvider: Provider.of<TileProviderWithCache>(context),
|
||||
urlTemplate: 'unused-{z}-{x}-{y}',
|
||||
tileSize: 256,
|
||||
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 tileWidget;
|
||||
} catch (e) {
|
||||
print('tileBuilder error: $e for tileImage: ${tileImage.toString()}');
|
||||
return tileWidget;
|
||||
}
|
||||
},
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
),
|
||||
),
|
||||
cameraLayers,
|
||||
// Built-in scale bar from flutter_map
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import '../services/map_data_provider.dart';
|
||||
import '../services/map_data_submodules/tiles_from_osm.dart';
|
||||
|
||||
/// In-memory tile cache and async provider for custom tiles.
|
||||
class TileProviderWithCache extends TileProvider with ChangeNotifier {
|
||||
static final Map<String, Uint8List> _tileCache = {};
|
||||
static Map<String, Uint8List> get tileCache => _tileCache;
|
||||
|
||||
bool _disposed = false;
|
||||
int _disposeCount = 0;
|
||||
VoidCallback? _onTilesCachedCallback;
|
||||
|
||||
TileProviderWithCache();
|
||||
|
||||
/// Set a callback to be called when tiles are cached (used by MapView for refresh)
|
||||
void setOnTilesCachedCallback(VoidCallback? callback) {
|
||||
_onTilesCachedCallback = callback;
|
||||
}
|
||||
|
||||
/// Cancel ALL pending tile requests - delegates to OSM tile fetcher
|
||||
void cancelAllTileRequests() {
|
||||
clearOSMTileQueue(); // This handles all the cancellation logic
|
||||
debugPrint('[TileProviderWithCache] Cancelled all tile requests');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposeCount++;
|
||||
|
||||
// If already disposed, just silently return (common during FlutterMap rebuilds)
|
||||
if (_disposed) {
|
||||
debugPrint('[TileProviderWithCache] Already disposed (call #$_disposeCount) - ignoring');
|
||||
return;
|
||||
}
|
||||
|
||||
debugPrint('[TileProviderWithCache] Disposing (call #$_disposeCount)');
|
||||
_disposed = true;
|
||||
|
||||
// Safely call super.dispose() with error handling
|
||||
try {
|
||||
super.dispose();
|
||||
} catch (e) {
|
||||
debugPrint('[TileProviderWithCache] Error during disposal: $e');
|
||||
// Continue execution - disposal errors shouldn't crash the app
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
ImageProvider getImage(TileCoordinates coords, TileLayer options, {MapSource source = MapSource.auto}) {
|
||||
final key = '${coords.z}/${coords.x}/${coords.y}';
|
||||
|
||||
if (_tileCache.containsKey(key)) {
|
||||
final bytes = _tileCache[key]!;
|
||||
return MemoryImage(bytes);
|
||||
} else {
|
||||
_fetchAndCacheTile(coords, key, source: source);
|
||||
// Always return a placeholder until the real tile is cached
|
||||
return const AssetImage('assets/transparent_1x1.png');
|
||||
}
|
||||
}
|
||||
|
||||
static void clearCache() {
|
||||
_tileCache.clear();
|
||||
}
|
||||
|
||||
void _fetchAndCacheTile(TileCoordinates coords, String key, {MapSource source = MapSource.auto}) 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, source: source,
|
||||
);
|
||||
if (bytes.isNotEmpty) {
|
||||
_tileCache[key] = Uint8List.fromList(bytes);
|
||||
// Only notify listeners if not disposed and still mounted
|
||||
if (!_disposed && hasListeners) {
|
||||
notifyListeners(); // This updates any listening widgets
|
||||
}
|
||||
// Trigger map refresh callback to force tile re-rendering
|
||||
debugPrint('[TileProviderWithCache] Tile cached: $key, calling refresh callback');
|
||||
_onTilesCachedCallback?.call();
|
||||
}
|
||||
// If bytes were empty, don't cache (will re-attempt next time)
|
||||
} catch (e) {
|
||||
// Cancelled requests will throw exceptions from fetchOSMTile(), just ignore them
|
||||
if (e.toString().contains('cancelled')) {
|
||||
debugPrint('[TileProviderWithCache] Tile request was cancelled: $key');
|
||||
}
|
||||
// Don't cache failed tiles regardless of reason
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user