mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-08-14 07:00:28 +02:00
fixes tile re-rendering after long rate limit periods without having to zoom/pan
This commit is contained in:
@@ -6,6 +6,7 @@ import 'package:flutter_map/flutter_map.dart';
|
|||||||
import '../../models/camera_profile.dart';
|
import '../../models/camera_profile.dart';
|
||||||
import '../../models/osm_camera_node.dart';
|
import '../../models/osm_camera_node.dart';
|
||||||
import '../../app_state.dart';
|
import '../../app_state.dart';
|
||||||
|
import '../network_status.dart';
|
||||||
|
|
||||||
/// Fetches cameras from the Overpass OSM API for the given bounds and profiles.
|
/// Fetches cameras from the Overpass OSM API for the given bounds and profiles.
|
||||||
/// If fetchAllPages is true, returns all possible cameras using multiple API calls (paging with pageSize).
|
/// If fetchAllPages is true, returns all possible cameras using multiple API calls (paging with pageSize).
|
||||||
@@ -45,11 +46,13 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
|
|||||||
print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}');
|
print('[camerasFromOverpass] Status: ${resp.statusCode}, Length: ${resp.body.length}');
|
||||||
if (resp.statusCode != 200) {
|
if (resp.statusCode != 200) {
|
||||||
print('[camerasFromOverpass] Overpass failed: ${resp.body}');
|
print('[camerasFromOverpass] Overpass failed: ${resp.body}');
|
||||||
|
NetworkStatus.instance.reportOverpassIssue();
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
final data = jsonDecode(resp.body) as Map<String, dynamic>;
|
final data = jsonDecode(resp.body) as Map<String, dynamic>;
|
||||||
final elements = data['elements'] as List<dynamic>;
|
final elements = data['elements'] as List<dynamic>;
|
||||||
print('[camerasFromOverpass] Retrieved elements: ${elements.length}');
|
print('[camerasFromOverpass] Retrieved elements: ${elements.length}');
|
||||||
|
NetworkStatus.instance.reportOverpassSuccess();
|
||||||
return elements.whereType<Map<String, dynamic>>().map((e) {
|
return elements.whereType<Map<String, dynamic>>().map((e) {
|
||||||
return OsmCameraNode(
|
return OsmCameraNode(
|
||||||
id: e['id'],
|
id: e['id'],
|
||||||
@@ -59,6 +62,14 @@ Future<List<OsmCameraNode>> camerasFromOverpass({
|
|||||||
}).toList();
|
}).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[camerasFromOverpass] Overpass exception: $e');
|
print('[camerasFromOverpass] Overpass exception: $e');
|
||||||
|
|
||||||
|
// Report network issues on connection errors
|
||||||
|
if (e.toString().contains('Connection refused') ||
|
||||||
|
e.toString().contains('Connection timed out') ||
|
||||||
|
e.toString().contains('Connection reset')) {
|
||||||
|
NetworkStatus.instance.reportOverpassIssue();
|
||||||
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:async';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flock_map_app/dev_config.dart';
|
import 'package:flock_map_app/dev_config.dart';
|
||||||
|
import '../network_status.dart';
|
||||||
|
|
||||||
/// Global semaphore to limit simultaneous tile fetches
|
/// Global semaphore to limit simultaneous tile fetches
|
||||||
final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
|
final _tileFetchSemaphore = _SimpleSemaphore(4); // Max 4 concurrent
|
||||||
@@ -33,13 +34,23 @@ Future<List<int>> fetchOSMTile({
|
|||||||
print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}');
|
print('[fetchOSMTile] HTTP ${resp.statusCode} for $z/$x/$y, length=${resp.bodyBytes.length}');
|
||||||
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
if (resp.statusCode == 200 && resp.bodyBytes.isNotEmpty) {
|
||||||
print('[fetchOSMTile] SUCCESS $z/$x/$y');
|
print('[fetchOSMTile] SUCCESS $z/$x/$y');
|
||||||
|
NetworkStatus.instance.reportOsmTileSuccess();
|
||||||
return resp.bodyBytes;
|
return resp.bodyBytes;
|
||||||
} else {
|
} else {
|
||||||
print('[fetchOSMTile] FAIL $z/$x/$y: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
print('[fetchOSMTile] FAIL $z/$x/$y: code=${resp.statusCode}, bytes=${resp.bodyBytes.length}');
|
||||||
|
NetworkStatus.instance.reportOsmTileIssue();
|
||||||
throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}');
|
throw HttpException('Failed to fetch tile $z/$x/$y: status ${resp.statusCode}');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('[fetchOSMTile] Exception $z/$x/$y: $e');
|
print('[fetchOSMTile] Exception $z/$x/$y: $e');
|
||||||
|
|
||||||
|
// Report network issues on connection errors
|
||||||
|
if (e.toString().contains('Connection refused') ||
|
||||||
|
e.toString().contains('Connection timed out') ||
|
||||||
|
e.toString().contains('Connection reset')) {
|
||||||
|
NetworkStatus.instance.reportOsmTileIssue();
|
||||||
|
}
|
||||||
|
|
||||||
if (attempt >= maxAttempts) {
|
if (attempt >= maxAttempts) {
|
||||||
print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e");
|
print("[fetchOSMTile] Failed for $z/$x/$y after $attempt attempts: $e");
|
||||||
rethrow;
|
rethrow;
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
enum NetworkIssueType { osmTiles, overpassApi, both }
|
||||||
|
|
||||||
|
class NetworkStatus extends ChangeNotifier {
|
||||||
|
static final NetworkStatus instance = NetworkStatus._();
|
||||||
|
NetworkStatus._();
|
||||||
|
|
||||||
|
bool _osmTilesHaveIssues = false;
|
||||||
|
bool _overpassHasIssues = false;
|
||||||
|
Timer? _osmRecoveryTimer;
|
||||||
|
Timer? _overpassRecoveryTimer;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
bool get hasAnyIssues => _osmTilesHaveIssues || _overpassHasIssues;
|
||||||
|
bool get osmTilesHaveIssues => _osmTilesHaveIssues;
|
||||||
|
bool get overpassHasIssues => _overpassHasIssues;
|
||||||
|
|
||||||
|
NetworkIssueType? get currentIssueType {
|
||||||
|
if (_osmTilesHaveIssues && _overpassHasIssues) return NetworkIssueType.both;
|
||||||
|
if (_osmTilesHaveIssues) return NetworkIssueType.osmTiles;
|
||||||
|
if (_overpassHasIssues) return NetworkIssueType.overpassApi;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report OSM tile server issues
|
||||||
|
void reportOsmTileIssue() {
|
||||||
|
if (!_osmTilesHaveIssues) {
|
||||||
|
_osmTilesHaveIssues = true;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint('[NetworkStatus] OSM tile server issues detected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset recovery timer - if we keep getting errors, keep showing indicator
|
||||||
|
_osmRecoveryTimer?.cancel();
|
||||||
|
_osmRecoveryTimer = Timer(const Duration(minutes: 2), () {
|
||||||
|
_osmTilesHaveIssues = false;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint('[NetworkStatus] OSM tile server issues cleared');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report Overpass API issues
|
||||||
|
void reportOverpassIssue() {
|
||||||
|
if (!_overpassHasIssues) {
|
||||||
|
_overpassHasIssues = true;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint('[NetworkStatus] Overpass API issues detected');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset recovery timer
|
||||||
|
_overpassRecoveryTimer?.cancel();
|
||||||
|
_overpassRecoveryTimer = Timer(const Duration(minutes: 2), () {
|
||||||
|
_overpassHasIssues = false;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint('[NetworkStatus] Overpass API issues cleared');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Report successful operations to potentially clear issues faster
|
||||||
|
void reportOsmTileSuccess() {
|
||||||
|
// Don't immediately clear on single success, but reduce recovery time
|
||||||
|
if (_osmTilesHaveIssues) {
|
||||||
|
_osmRecoveryTimer?.cancel();
|
||||||
|
_osmRecoveryTimer = Timer(const Duration(seconds: 30), () {
|
||||||
|
_osmTilesHaveIssues = false;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint('[NetworkStatus] OSM tile server issues cleared after success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reportOverpassSuccess() {
|
||||||
|
if (_overpassHasIssues) {
|
||||||
|
_overpassRecoveryTimer?.cancel();
|
||||||
|
_overpassRecoveryTimer = Timer(const Duration(seconds: 30), () {
|
||||||
|
_overpassHasIssues = false;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint('[NetworkStatus] Overpass API issues cleared after success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_osmRecoveryTimer?.cancel();
|
||||||
|
_overpassRecoveryTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,6 +63,10 @@ class _MapViewState extends State<MapView> {
|
|||||||
|
|
||||||
// Ensure initial overlays are fetched
|
// Ensure initial overlays are fetched
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Set up tile refresh callback
|
||||||
|
final tileProvider = Provider.of<TileProviderWithCache>(context, listen: false);
|
||||||
|
tileProvider.setOnTilesCachedCallback(_onTilesCached);
|
||||||
|
|
||||||
_refreshCamerasFromProvider();
|
_refreshCamerasFromProvider();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -72,6 +76,15 @@ class _MapViewState extends State<MapView> {
|
|||||||
_positionSub?.cancel();
|
_positionSub?.cancel();
|
||||||
_debounce.dispose();
|
_debounce.dispose();
|
||||||
_cameraProvider.removeListener(_onCamerasUpdated);
|
_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
|
||||||
|
}
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +92,14 @@ class _MapViewState extends State<MapView> {
|
|||||||
if (mounted) setState(() {});
|
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
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _refreshCamerasFromProvider() {
|
void _refreshCamerasFromProvider() {
|
||||||
final appState = context.read<AppState>();
|
final appState = context.read<AppState>();
|
||||||
LatLngBounds? bounds;
|
LatLngBounds? bounds;
|
||||||
@@ -159,6 +180,8 @@ class _MapViewState extends State<MapView> {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appState = context.watch<AppState>();
|
final appState = context.watch<AppState>();
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import '../services/network_status.dart';
|
||||||
|
|
||||||
|
class NetworkStatusIndicator extends StatelessWidget {
|
||||||
|
const NetworkStatusIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ChangeNotifierProvider.value(
|
||||||
|
value: NetworkStatus.instance,
|
||||||
|
child: Consumer<NetworkStatus>(
|
||||||
|
builder: (context, networkStatus, child) {
|
||||||
|
if (!networkStatus.hasAnyIssues) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
String message;
|
||||||
|
IconData icon;
|
||||||
|
Color color;
|
||||||
|
|
||||||
|
switch (networkStatus.currentIssueType) {
|
||||||
|
case NetworkIssueType.osmTiles:
|
||||||
|
message = 'OSM tiles slow';
|
||||||
|
icon = Icons.map_outlined;
|
||||||
|
color = Colors.orange;
|
||||||
|
break;
|
||||||
|
case NetworkIssueType.overpassApi:
|
||||||
|
message = 'Camera data slow';
|
||||||
|
icon = Icons.camera_alt_outlined;
|
||||||
|
color = Colors.orange;
|
||||||
|
break;
|
||||||
|
case NetworkIssueType.both:
|
||||||
|
message = 'Network issues';
|
||||||
|
icon = Icons.cloud_off_outlined;
|
||||||
|
color = Colors.red;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top + 8,
|
||||||
|
left: 8,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black87,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: color, width: 1),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
size: 16,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(
|
||||||
|
color: color,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,15 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier {
|
|||||||
|
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
int _disposeCount = 0;
|
int _disposeCount = 0;
|
||||||
|
VoidCallback? _onTilesCachedCallback;
|
||||||
|
|
||||||
TileProviderWithCache();
|
TileProviderWithCache();
|
||||||
|
|
||||||
|
/// Set a callback to be called when tiles are cached (used by MapView for refresh)
|
||||||
|
void setOnTilesCachedCallback(VoidCallback? callback) {
|
||||||
|
_onTilesCachedCallback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_disposeCount++;
|
_disposeCount++;
|
||||||
@@ -67,6 +73,8 @@ class TileProviderWithCache extends TileProvider with ChangeNotifier {
|
|||||||
if (!_disposed && hasListeners) {
|
if (!_disposed && hasListeners) {
|
||||||
notifyListeners(); // This updates any listening widgets
|
notifyListeners(); // This updates any listening widgets
|
||||||
}
|
}
|
||||||
|
// Trigger map refresh callback to force tile re-rendering
|
||||||
|
_onTilesCachedCallback?.call();
|
||||||
}
|
}
|
||||||
// If bytes were empty, don't cache (will re-attempt next time)
|
// If bytes were empty, don't cache (will re-attempt next time)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user