mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-05-23 16:49:55 +02:00
idk but it's better - cache busting works.
This commit is contained in:
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import '../app_state.dart';
|
||||
import '../models/tile_provider.dart';
|
||||
@@ -158,9 +159,33 @@ class _TileProviderEditorScreenState extends State<TileProviderEditorScreen> {
|
||||
void _deleteTileType(int index) {
|
||||
if (_tileTypes.length <= 1) return;
|
||||
|
||||
final tileTypeToDelete = _tileTypes[index];
|
||||
final appState = context.read<AppState>();
|
||||
|
||||
setState(() {
|
||||
_tileTypes.removeAt(index);
|
||||
});
|
||||
|
||||
// If we're deleting the currently selected tile type, switch to another one
|
||||
if (appState.selectedTileType?.id == tileTypeToDelete.id) {
|
||||
// Find first remaining tile type in this provider or any other provider
|
||||
TileType? replacement;
|
||||
if (_tileTypes.isNotEmpty) {
|
||||
replacement = _tileTypes.first;
|
||||
} else {
|
||||
// Look in other providers
|
||||
for (final provider in appState.tileProviders) {
|
||||
if (provider.availableTileTypes.isNotEmpty) {
|
||||
replacement = provider.availableTileTypes.first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (replacement != null) {
|
||||
appState.setSelectedTileType(replacement.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showTileTypeDialog({TileType? tileType, int? index}) {
|
||||
|
||||
@@ -79,7 +79,7 @@ class MapDataProvider {
|
||||
pageSize: AppState.instance.maxCameras,
|
||||
);
|
||||
} catch (e) {
|
||||
print('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.');
|
||||
debugPrint('[MapDataProvider] Remote camera fetch failed, error: $e. Falling back to local.');
|
||||
return fetchLocalCameras(
|
||||
bounds: bounds,
|
||||
profiles: profiles,
|
||||
@@ -152,14 +152,13 @@ class MapDataProvider {
|
||||
final selectedTileType = appState.selectedTileType;
|
||||
final selectedProvider = appState.selectedTileProvider;
|
||||
|
||||
if (selectedTileType != null && selectedProvider != null) {
|
||||
// Use current provider
|
||||
final tileUrl = selectedTileType.getTileUrl(z, x, y, apiKey: selectedProvider.apiKey);
|
||||
return fetchRemoteTile(z: z, x: x, y: y, url: tileUrl);
|
||||
} else {
|
||||
// Fallback to OSM if no provider selected
|
||||
return fetchOSMTile(z: z, x: x, y: y);
|
||||
// We guarantee that a provider and tile type are always selected
|
||||
if (selectedTileType == null || selectedProvider == null) {
|
||||
throw Exception('No tile provider selected - this should never happen');
|
||||
}
|
||||
|
||||
final tileUrl = selectedTileType.getTileUrl(z, x, y, apiKey: selectedProvider.apiKey);
|
||||
return fetchRemoteTile(z: z, x: x, y: y, url: tileUrl);
|
||||
}
|
||||
|
||||
/// Clear any queued tile requests (call when map view changes significantly)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
|
||||
@@ -43,15 +44,19 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
|
||||
print('[camerasFromOverpass] Querying Overpass...');
|
||||
print('[camerasFromOverpass] Query:\n$query');
|
||||
final resp = await http.post(Uri.parse(prodEndpoint), body: {'data': query.trim()});
|
||||
print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}');
|
||||
// Only log errors
|
||||
if (resp.statusCode != 200) {
|
||||
print('[camerasFromOverpass] Overpass failed: ${resp.body}');
|
||||
debugPrint('[camerasFromOverpass] Overpass failed: ${resp.body}');
|
||||
NetworkStatus.instance.reportOverpassIssue();
|
||||
return [];
|
||||
}
|
||||
final data = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||
final elements = data['elements'] as List<dynamic>;
|
||||
print('[camerasFromOverpass] Retrieved elements: ${elements.length}');
|
||||
|
||||
// Only log if many cameras found or if it's a bulk download
|
||||
if (elements.length > 20 || fetchAllPages) {
|
||||
debugPrint('[camerasFromOverpass] Retrieved ${elements.length} cameras');
|
||||
}
|
||||
NetworkStatus.instance.reportOverpassSuccess();
|
||||
return elements.whereType<Map<String, dynamic>>().map((e) {
|
||||
return OsmCameraNode(
|
||||
|
||||
@@ -12,12 +12,13 @@ final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
|
||||
/// Clear queued tile requests when map view changes significantly
|
||||
void clearRemoteTileQueue() {
|
||||
final clearedCount = _tileFetchSemaphore.clearQueue();
|
||||
debugPrint('[RemoteTiles] Cleared $clearedCount queued tile requests');
|
||||
// Only log if we actually cleared something significant
|
||||
if (clearedCount > 5) {
|
||||
debugPrint('[RemoteTiles] Cleared $clearedCount queued tile requests');
|
||||
}
|
||||
}
|
||||
|
||||
/// Legacy alias for backward compatibility
|
||||
@Deprecated('Use clearRemoteTileQueue instead')
|
||||
void clearOSMTileQueue() => clearRemoteTileQueue();
|
||||
|
||||
|
||||
/// Fetches a tile from any remote provider, with in-memory retries/backoff, and global concurrency limit.
|
||||
/// Returns tile image bytes, or throws on persistent failure.
|
||||
@@ -50,11 +51,11 @@ Future<List<int>> fetchRemoteTile({
|
||||
|
||||
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
||||
// Success - no logging for normal operation
|
||||
NetworkStatus.instance.reportOsmTileSuccess(); // Still use OSM reporting for now
|
||||
NetworkStatus.instance.reportOsmTileSuccess(); // Generic tile server reporting
|
||||
return resp.bodyBytes;
|
||||
} else {
|
||||
debugPrint('[fetchRemoteTile] FAIL $z/$x/$y from $hostInfo: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
||||
NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now
|
||||
NetworkStatus.instance.reportOsmTileIssue(); // Generic tile server reporting
|
||||
throw HttpException('Failed to fetch tile $z/$x/$y from $hostInfo: status ${resp.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -62,7 +63,7 @@ Future<List<int>> fetchRemoteTile({
|
||||
if (e.toString().contains('Connection refused') ||
|
||||
e.toString().contains('Connection timed out') ||
|
||||
e.toString().contains('Connection reset')) {
|
||||
NetworkStatus.instance.reportOsmTileIssue(); // Still use OSM reporting for now
|
||||
NetworkStatus.instance.reportOsmTileIssue(); // Generic tile server reporting
|
||||
}
|
||||
|
||||
if (attempt >= maxAttempts) {
|
||||
|
||||
@@ -44,12 +44,12 @@ class NetworkStatus extends ChangeNotifier {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Report OSM tile server issues
|
||||
/// Report tile server issues (for any provider)
|
||||
void reportOsmTileIssue() {
|
||||
if (!_osmTilesHaveIssues) {
|
||||
_osmTilesHaveIssues = true;
|
||||
notifyListeners();
|
||||
debugPrint('[NetworkStatus] OSM tile server issues detected');
|
||||
debugPrint('[NetworkStatus] Tile server issues detected');
|
||||
}
|
||||
|
||||
// Reset recovery timer - if we keep getting errors, keep showing indicator
|
||||
@@ -57,7 +57,7 @@ class NetworkStatus extends ChangeNotifier {
|
||||
_osmRecoveryTimer = Timer(const Duration(minutes: 2), () {
|
||||
_osmTilesHaveIssues = false;
|
||||
notifyListeners();
|
||||
debugPrint('[NetworkStatus] OSM tile server issues cleared');
|
||||
debugPrint('[NetworkStatus] Tile server issues cleared');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class NetworkStatus extends ChangeNotifier {
|
||||
void reportOsmTileSuccess() {
|
||||
// Clear issues immediately on success (they were likely temporary)
|
||||
if (_osmTilesHaveIssues) {
|
||||
debugPrint('[NetworkStatus] OSM tile server issues cleared after success');
|
||||
// Quietly clear - don't log routine success
|
||||
_osmTilesHaveIssues = false;
|
||||
_osmRecoveryTimer?.cancel();
|
||||
notifyListeners();
|
||||
@@ -91,7 +91,7 @@ class NetworkStatus extends ChangeNotifier {
|
||||
|
||||
void reportOverpassSuccess() {
|
||||
if (_overpassHasIssues) {
|
||||
debugPrint('[NetworkStatus] Overpass API issues cleared after success');
|
||||
// Quietly clear - don't log routine success
|
||||
_overpassHasIssues = false;
|
||||
_overpassRecoveryTimer?.cancel();
|
||||
notifyListeners();
|
||||
@@ -109,7 +109,7 @@ class NetworkStatus extends ChangeNotifier {
|
||||
if (!_isWaitingForData) {
|
||||
_isWaitingForData = true;
|
||||
notifyListeners();
|
||||
debugPrint('[NetworkStatus] Waiting for data...');
|
||||
// Don't log routine waiting - only log if we stay waiting too long
|
||||
}
|
||||
|
||||
// Set timeout to show appropriate status after reasonable time
|
||||
@@ -140,7 +140,7 @@ class NetworkStatus extends ChangeNotifier {
|
||||
_waitingTimer?.cancel();
|
||||
_noDataResetTimer?.cancel();
|
||||
notifyListeners();
|
||||
debugPrint('[NetworkStatus] Waiting/timeout/no-data status cleared - data arrived');
|
||||
// Quietly clear waiting status - don't log routine data arrival
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,11 @@ class CameraTagSheet extends StatelessWidget {
|
||||
return SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Camera #${node.id}',
|
||||
style: Theme.of(context).textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
@@ -55,6 +56,7 @@ class CameraTagSheet extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,11 @@ class MapViewState extends State<MapView> {
|
||||
// Track zoom to clear queue on zoom changes
|
||||
double? _lastZoom;
|
||||
|
||||
// Track tile type changes to clear cache
|
||||
// Track changes that require cache clearing
|
||||
String? _lastTileTypeId;
|
||||
bool? _lastOfflineMode;
|
||||
int _mapRebuildKey = 0;
|
||||
bool _shouldClearCache = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -126,6 +129,10 @@ class MapViewState extends State<MapView> {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MapView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@@ -176,13 +183,38 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
/// Build tile layer - uses standard URL that SimpleTileHttpClient can parse
|
||||
Widget _buildTileLayer(AppState appState) {
|
||||
final selectedTileType = appState.selectedTileType;
|
||||
final selectedProvider = appState.selectedTileProvider;
|
||||
final offlineMode = appState.offlineMode;
|
||||
|
||||
// Create a unique cache key that includes provider, tile type, and offline mode
|
||||
// This ensures different providers/modes have separate cache entries
|
||||
String generateTileKey(String url) {
|
||||
final providerKey = selectedProvider?.id ?? 'unknown';
|
||||
final typeKey = selectedTileType?.id ?? 'unknown';
|
||||
final modeKey = offlineMode ? 'offline' : 'online';
|
||||
return '$providerKey-$typeKey-$modeKey-$url';
|
||||
}
|
||||
|
||||
// Use a generic URL template that SimpleTileHttpClient recognizes
|
||||
// The actual provider URL will be built by MapDataProvider using current AppState
|
||||
// Create a completely fresh HTTP client when providers change
|
||||
// This should bypass any caching at the HTTP client level
|
||||
final httpClient = _shouldClearCache
|
||||
? SimpleTileHttpClient() // Fresh instance
|
||||
: _tileHttpClient; // Reuse existing
|
||||
|
||||
if (_shouldClearCache) {
|
||||
debugPrint('[MapView] Creating fresh HTTP client to bypass cache');
|
||||
}
|
||||
|
||||
return TileLayer(
|
||||
urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png',
|
||||
urlTemplate: 'https://tiles.local/{z}/{x}/{y}.png?provider=${selectedProvider?.id}&type=${selectedTileType?.id}&mode=${offlineMode ? 'offline' : 'online'}',
|
||||
userAgentPackageName: 'com.stopflock.flock_map_app',
|
||||
tileProvider: NetworkTileProvider(
|
||||
httpClient: _tileHttpClient,
|
||||
httpClient: httpClient,
|
||||
// Also disable flutter_map caching
|
||||
cachingProvider: const DisabledMapCachingProvider(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -208,16 +240,28 @@ class MapViewState extends State<MapView> {
|
||||
});
|
||||
}
|
||||
|
||||
// Check if tile type changed and clear cache if needed
|
||||
// Check if tile type OR offline mode changed and clear cache if needed
|
||||
final currentTileTypeId = appState.selectedTileType?.id;
|
||||
if (_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) {
|
||||
final currentOfflineMode = appState.offlineMode;
|
||||
|
||||
if ((_lastTileTypeId != null && _lastTileTypeId != currentTileTypeId) ||
|
||||
(_lastOfflineMode != null && _lastOfflineMode != currentOfflineMode)) {
|
||||
// Force map rebuild with new key and destroy cache
|
||||
_mapRebuildKey++;
|
||||
_shouldClearCache = true;
|
||||
final reason = _lastTileTypeId != currentTileTypeId
|
||||
? 'tile type ($currentTileTypeId)'
|
||||
: 'offline mode ($currentOfflineMode)';
|
||||
debugPrint('[MapView] *** CACHE CLEAR *** $reason changed - destroying cache $_mapRebuildKey');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Clear our tile request queue
|
||||
debugPrint('[MapView] Post-frame: Clearing tile request queue');
|
||||
_tileHttpClient.clearTileQueue();
|
||||
// Note: The ValueKey on FlutterMap will cause flutter_map to rebuild and clear its cache
|
||||
_shouldClearCache = false;
|
||||
});
|
||||
}
|
||||
|
||||
_lastTileTypeId = currentTileTypeId;
|
||||
_lastOfflineMode = currentOfflineMode;
|
||||
|
||||
// Seed add‑mode target once, after first controller center is available.
|
||||
if (session != null && session.target == null) {
|
||||
@@ -267,7 +311,7 @@ class MapViewState extends State<MapView> {
|
||||
return Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
key: ValueKey('map_offline_${appState.offlineMode}_tiletype_${appState.selectedTileType?.id ?? 'none'}'),
|
||||
key: ValueKey('map_${appState.offlineMode}_${appState.selectedTileType?.id ?? 'none'}_$_mapRebuildKey'),
|
||||
mapController: _controller,
|
||||
options: MapOptions(
|
||||
initialCenter: _currentLatLng ?? LatLng(37.7749, -122.4194),
|
||||
@@ -289,7 +333,7 @@ class MapViewState extends State<MapView> {
|
||||
|
||||
if (zoomChanged) {
|
||||
_tileDebounce(() {
|
||||
debugPrint('[MapView] Zoom change detected - clearing stale tile requests');
|
||||
// Clear stale tile requests on zoom change (quietly)
|
||||
_tileHttpClient.clearTileQueue();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user