diff --git a/lib/services/network_status.dart b/lib/services/network_status.dart index a36e73e..eedc29e 100644 --- a/lib/services/network_status.dart +++ b/lib/services/network_status.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'dart:async'; enum NetworkIssueType { osmTiles, overpassApi, both } +enum NetworkStatusType { waiting, issues, timedOut, ready } class NetworkStatus extends ChangeNotifier { static final NetworkStatus instance = NetworkStatus._(); @@ -9,13 +10,25 @@ class NetworkStatus extends ChangeNotifier { bool _osmTilesHaveIssues = false; bool _overpassHasIssues = false; + bool _isWaitingForData = false; + bool _isTimedOut = false; Timer? _osmRecoveryTimer; Timer? _overpassRecoveryTimer; + Timer? _waitingTimer; // Getters bool get hasAnyIssues => _osmTilesHaveIssues || _overpassHasIssues; bool get osmTilesHaveIssues => _osmTilesHaveIssues; bool get overpassHasIssues => _overpassHasIssues; + bool get isWaitingForData => _isWaitingForData; + bool get isTimedOut => _isTimedOut; + + NetworkStatusType get currentStatus { + if (hasAnyIssues) return NetworkStatusType.issues; + if (_isWaitingForData) return NetworkStatusType.waiting; + if (_isTimedOut) return NetworkStatusType.timedOut; + return NetworkStatusType.ready; + } NetworkIssueType? get currentIssueType { if (_osmTilesHaveIssues && _overpassHasIssues) return NetworkIssueType.both; @@ -78,10 +91,43 @@ class NetworkStatus extends ChangeNotifier { } } + /// Set waiting status (show when loading tiles/cameras) + void setWaiting() { + // Clear any previous timeout state when starting new wait + _isTimedOut = false; + + if (!_isWaitingForData) { + _isWaitingForData = true; + notifyListeners(); + debugPrint('[NetworkStatus] Waiting for data...'); + } + + // Set timeout to show "timed out" status after reasonable time + _waitingTimer?.cancel(); + _waitingTimer = Timer(const Duration(seconds: 10), () { + _isWaitingForData = false; + _isTimedOut = true; + notifyListeners(); + debugPrint('[NetworkStatus] Data request timed out'); + }); + } + + /// Clear waiting/timeout status when data arrives + void clearWaiting() { + if (_isWaitingForData || _isTimedOut) { + _isWaitingForData = false; + _isTimedOut = false; + _waitingTimer?.cancel(); + notifyListeners(); + debugPrint('[NetworkStatus] Waiting/timeout status cleared - data arrived'); + } + } + @override void dispose() { _osmRecoveryTimer?.cancel(); _overpassRecoveryTimer?.cancel(); + _waitingTimer?.cancel(); super.dispose(); } } \ No newline at end of file diff --git a/lib/services/simple_tile_service.dart b/lib/services/simple_tile_service.dart index 0dfcd75..14bfaaa 100644 --- a/lib/services/simple_tile_service.dart +++ b/lib/services/simple_tile_service.dart @@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart'; import '../app_state.dart'; import 'map_data_provider.dart'; +import 'network_status.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. @@ -47,6 +48,9 @@ class SimpleTileHttpClient extends http.BaseClient { debugPrint('[SimpleTileService] Serving tile $z/$x/$y from offline storage'); + // Clear waiting status - we got data + NetworkStatus.instance.clearWaiting(); + // Serve offline tile with proper cache headers return http.StreamedResponse( Stream.value(localTileBytes), @@ -76,7 +80,12 @@ class SimpleTileHttpClient extends http.BaseClient { // We're online - try OSM with proper error handling debugPrint('[SimpleTileService] Online mode - trying OSM for $z/$x/$y'); try { - return await _inner.send(http.Request('GET', Uri.parse('https://tile.openstreetmap.org/$z/$x/$y.png'))); + final response = await _inner.send(http.Request('GET', Uri.parse('https://tile.openstreetmap.org/$z/$x/$y.png'))); + // Clear waiting status on successful network tile + if (response.statusCode == 200) { + NetworkStatus.instance.clearWaiting(); + } + return response; } catch (networkError) { debugPrint('[SimpleTileService] OSM request failed for $z/$x/$y: $networkError'); // Return 404 instead of throwing - let flutter_map handle gracefully diff --git a/lib/widgets/camera_provider_with_cache.dart b/lib/widgets/camera_provider_with_cache.dart index e5139ab..3e0c9f0 100644 --- a/lib/widgets/camera_provider_with_cache.dart +++ b/lib/widgets/camera_provider_with_cache.dart @@ -5,6 +5,7 @@ import 'package:flutter_map/flutter_map.dart' show LatLngBounds; import '../services/map_data_provider.dart'; import '../services/camera_cache.dart'; +import '../services/network_status.dart'; import '../models/camera_profile.dart'; import '../models/osm_camera_node.dart'; import '../app_state.dart'; @@ -55,6 +56,8 @@ class CameraProviderWithCache extends ChangeNotifier { ); if (fresh.isNotEmpty) { CameraCache.instance.addOrUpdate(fresh); + // Clear waiting status when camera data arrives + NetworkStatus.instance.clearWaiting(); notifyListeners(); } } catch (e) { diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index c04baa5..d8fa528 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -9,6 +9,7 @@ import 'package:http/http.dart' as http; import '../app_state.dart'; import '../services/offline_area_service.dart'; import '../services/simple_tile_service.dart'; +import '../services/network_status.dart'; import '../models/osm_camera_node.dart'; import '../models/camera_profile.dart'; import 'debouncer.dart'; @@ -250,6 +251,9 @@ class MapViewState extends State { appState.updateSession(target: pos.center); } + // Show waiting indicator when map moves (user is expecting new content) + NetworkStatus.instance.setWaiting(); + // Only clear tile queue on significant ZOOM changes (not panning) final currentZoom = pos.zoom; final zoomChanged = _lastZoom != null && (currentZoom - _lastZoom!).abs() > 0.5; diff --git a/lib/widgets/network_status_indicator.dart b/lib/widgets/network_status_indicator.dart index 55b4675..829c308 100644 --- a/lib/widgets/network_status_indicator.dart +++ b/lib/widgets/network_status_indicator.dart @@ -11,31 +11,46 @@ class NetworkStatusIndicator extends StatelessWidget { value: NetworkStatus.instance, child: Consumer( 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; + switch (networkStatus.currentStatus) { + case NetworkStatusType.waiting: + message = 'Loading...'; + icon = Icons.hourglass_empty; + color = Colors.blue; + break; + + case NetworkStatusType.timedOut: + message = 'Timed out'; + icon = Icons.hourglass_disabled; color = Colors.orange; break; - case NetworkIssueType.overpassApi: - message = 'Camera data slow'; - icon = Icons.camera_alt_outlined; - color = Colors.orange; + + case NetworkStatusType.issues: + 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(); + } break; - case NetworkIssueType.both: - message = 'Network issues'; - icon = Icons.cloud_off_outlined; - color = Colors.red; - break; - default: + + case NetworkStatusType.ready: return const SizedBox.shrink(); }