diff --git a/lib/app_state.dart b/lib/app_state.dart index 6013419..2231b46 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -16,6 +16,7 @@ import 'models/tile_provider.dart'; import 'models/search_result.dart'; import 'services/offline_area_service.dart'; import 'services/map_data_provider.dart'; +import 'services/node_data_manager.dart'; import 'services/tile_preview_service.dart'; import 'services/changelog_service.dart'; import 'services/operator_profile_service.dart'; @@ -241,6 +242,9 @@ class AppState extends ChangeNotifier { // Initialize OfflineAreaService to ensure offline areas are loaded await OfflineAreaService().ensureInitialized(); + // Preload offline nodes into cache for immediate display + await NodeDataManager().preloadOfflineNodes(); + // Start uploader if conditions are met _startUploader(); diff --git a/lib/localizations/de.json b/lib/localizations/de.json index c4b1278..88a7ce8 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -103,6 +103,7 @@ "mustBeLoggedIn": "Sie müssen angemeldet sein, um neue Knoten zu übertragen. Bitte melden Sie sich über die Einstellungen an.", "enableSubmittableProfile": "Aktivieren Sie ein übertragbares Profil in den Einstellungen, um neue Knoten zu übertragen.", "profileViewOnlyWarning": "Dieses Profil ist nur zum Anzeigen der Karte gedacht. Bitte wählen Sie ein übertragbares Profil aus, um neue Knoten zu übertragen.", + "loadingAreaData": "Lade Bereichsdaten... Bitte warten Sie vor dem Übertragen.", "refineTags": "Tags Verfeinern", "refineTagsWithProfile": "Tags Verfeinern ({})" }, @@ -118,6 +119,7 @@ "sandboxModeWarning": "Bearbeitungen von Produktionsknoten können nicht an die Sandbox übertragen werden. Wechseln Sie in den Produktionsmodus in den Einstellungen, um Knoten zu bearbeiten.", "enableSubmittableProfile": "Aktivieren Sie ein übertragbares Profil in den Einstellungen, um Knoten zu bearbeiten.", "profileViewOnlyWarning": "Dieses Profil ist nur zum Anzeigen der Karte gedacht. Bitte wählen Sie ein übertragbares Profil aus, um Knoten zu bearbeiten.", + "loadingAreaData": "Lade Bereichsdaten... Bitte warten Sie vor dem Übertragen.", "cannotMoveConstrainedNode": "Kann diese Kamera nicht verschieben - sie ist mit einem anderen Kartenelement verbunden (OSM-Weg/Relation). Sie können trotzdem ihre Tags und Richtung bearbeiten.", "zoomInRequiredMessage": "Zoomen Sie auf mindestens Stufe {} heran, um Überwachungsknoten hinzuzufügen oder zu bearbeiten. Dies gewährleistet eine präzise Positionierung für genaues Kartieren.", "extractFromWay": "Knoten aus Weg/Relation extrahieren", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index e037d73..120f29c 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -140,6 +140,7 @@ "mustBeLoggedIn": "You must be logged in to submit new nodes. Please log in via Settings.", "enableSubmittableProfile": "Enable a submittable profile in Settings to submit new nodes.", "profileViewOnlyWarning": "This profile is for map viewing only. Please select a submittable profile to submit new nodes.", + "loadingAreaData": "Loading area data... Please wait before submitting.", "refineTags": "Refine Tags", "refineTagsWithProfile": "Refine Tags ({})" }, @@ -155,6 +156,7 @@ "sandboxModeWarning": "Cannot submit edits on production nodes to sandbox. Switch to Production mode in Settings to edit nodes.", "enableSubmittableProfile": "Enable a submittable profile in Settings to edit nodes.", "profileViewOnlyWarning": "This profile is for map viewing only. Please select a submittable profile to edit nodes.", + "loadingAreaData": "Loading area data... Please wait before submitting.", "cannotMoveConstrainedNode": "Cannot move this camera - it's connected to another map element (OSM way/relation). You can still edit its tags and direction.", "zoomInRequiredMessage": "Zoom in to at least level {} to add or edit surveillance nodes. This ensures precise positioning for accurate mapping.", "extractFromWay": "Extract node from way/relation", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index 5aa01d1..7c64fe7 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -140,6 +140,7 @@ "mustBeLoggedIn": "Debe estar conectado para enviar nuevos nodos. Por favor, inicie sesión a través de Configuración.", "enableSubmittableProfile": "Habilite un perfil envíable en Configuración para enviar nuevos nodos.", "profileViewOnlyWarning": "Este perfil es solo para visualización del mapa. Por favor, seleccione un perfil envíable para enviar nuevos nodos.", + "loadingAreaData": "Cargando datos del área... Por favor espere antes de enviar.", "refineTags": "Refinar Etiquetas", "refineTagsWithProfile": "Refinar Etiquetas ({})" }, @@ -155,6 +156,7 @@ "sandboxModeWarning": "No se pueden enviar ediciones de nodos de producción al sandbox. Cambie al modo Producción en Configuración para editar nodos.", "enableSubmittableProfile": "Habilite un perfil envíable en Configuración para editar nodos.", "profileViewOnlyWarning": "Este perfil es solo para visualización del mapa. Por favor, seleccione un perfil envíable para editar nodos.", + "loadingAreaData": "Cargando datos del área... Por favor espere antes de enviar.", "cannotMoveConstrainedNode": "No se puede mover esta cámara - está conectada a otro elemento del mapa (OSM way/relation). Aún puede editar sus etiquetas y dirección.", "zoomInRequiredMessage": "Amplíe al menos al nivel {} para agregar o editar nodos de vigilancia. Esto garantiza un posicionamiento preciso para un mapeo exacto.", "extractFromWay": "Extraer nodo de way/relation", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index ef238dd..4fab811 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -140,6 +140,7 @@ "mustBeLoggedIn": "Vous devez être connecté pour soumettre de nouveaux nœuds. Veuillez vous connecter via les Paramètres.", "enableSubmittableProfile": "Activez un profil soumissible dans les Paramètres pour soumettre de nouveaux nœuds.", "profileViewOnlyWarning": "Ce profil est uniquement pour la visualisation de la carte. Veuillez sélectionner un profil soumissible pour soumettre de nouveaux nœuds.", + "loadingAreaData": "Chargement des données de zone... Veuillez patienter avant de soumettre.", "refineTags": "Affiner Balises", "refineTagsWithProfile": "Affiner Balises ({})" }, @@ -155,6 +156,7 @@ "sandboxModeWarning": "Impossible de soumettre des modifications de nœuds de production au sandbox. Passez au mode Production dans les Paramètres pour modifier les nœuds.", "enableSubmittableProfile": "Activez un profil soumissible dans les Paramètres pour modifier les nœuds.", "profileViewOnlyWarning": "Ce profil est uniquement pour la visualisation de la carte. Veuillez sélectionner un profil soumissible pour modifier les nœuds.", + "loadingAreaData": "Chargement des données de zone... Veuillez patienter avant de soumettre.", "cannotMoveConstrainedNode": "Impossible de déplacer cette caméra - elle est connectée à un autre élément de carte (OSM way/relation). Vous pouvez toujours modifier ses balises et sa direction.", "zoomInRequiredMessage": "Zoomez au moins au niveau {} pour ajouter ou modifier des nœuds de surveillance. Cela garantit un positionnement précis pour une cartographie exacte.", "extractFromWay": "Extraire le nœud du way/relation", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 52c9ff3..b98c9d7 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -140,6 +140,7 @@ "mustBeLoggedIn": "Devi essere loggato per inviare nuovi nodi. Per favore accedi tramite Impostazioni.", "enableSubmittableProfile": "Abilita un profilo inviabile nelle Impostazioni per inviare nuovi nodi.", "profileViewOnlyWarning": "Questo profilo è solo per la visualizzazione della mappa. Per favore seleziona un profilo inviabile per inviare nuovi nodi.", + "loadingAreaData": "Caricamento dati area... Per favore attendi prima di inviare.", "refineTags": "Affina Tag", "refineTagsWithProfile": "Affina Tag ({})" }, @@ -155,6 +156,7 @@ "sandboxModeWarning": "Impossibile inviare modifiche di nodi di produzione alla sandbox. Passa alla modalità Produzione nelle Impostazioni per modificare i nodi.", "enableSubmittableProfile": "Abilita un profilo inviabile nelle Impostazioni per modificare i nodi.", "profileViewOnlyWarning": "Questo profilo è solo per la visualizzazione della mappa. Per favore seleziona un profilo inviabile per modificare i nodi.", + "loadingAreaData": "Caricamento dati area... Per favore attendi prima di inviare.", "cannotMoveConstrainedNode": "Impossibile spostare questa telecamera - è collegata a un altro elemento della mappa (OSM way/relation). Puoi ancora modificare i suoi tag e direzione.", "zoomInRequiredMessage": "Ingrandisci almeno al livello {} per aggiungere o modificare nodi di sorveglianza. Questo garantisce un posizionamento preciso per una mappatura accurata.", "extractFromWay": "Estrai nodo da way/relation", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 374a8f9..f521c6f 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -140,6 +140,7 @@ "mustBeLoggedIn": "Você deve estar logado para enviar novos nós. Por favor, faça login via Configurações.", "enableSubmittableProfile": "Ative um perfil enviável nas Configurações para enviar novos nós.", "profileViewOnlyWarning": "Este perfil é apenas para visualização do mapa. Por favor, selecione um perfil enviável para enviar novos nós.", + "loadingAreaData": "Carregando dados da área... Por favor aguarde antes de enviar.", "refineTags": "Refinar Tags", "refineTagsWithProfile": "Refinar Tags ({})" }, @@ -155,6 +156,7 @@ "sandboxModeWarning": "Não é possível enviar edições de nós de produção para o sandbox. Mude para o modo Produção nas Configurações para editar nós.", "enableSubmittableProfile": "Ative um perfil enviável nas Configurações para editar nós.", "profileViewOnlyWarning": "Este perfil é apenas para visualização do mapa. Por favor, selecione um perfil enviável para editar nós.", + "loadingAreaData": "Carregando dados da área... Por favor aguarde antes de enviar.", "cannotMoveConstrainedNode": "Não é possível mover esta câmera - ela está conectada a outro elemento do mapa (OSM way/relation). Você ainda pode editar suas tags e direção.", "zoomInRequiredMessage": "Amplie para pelo menos o nível {} para adicionar ou editar nós de vigilância. Isto garante um posicionamento preciso para mapeamento exato.", "extractFromWay": "Extrair nó do way/relation", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 3a4fb8b..04822bc 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -140,6 +140,7 @@ "mustBeLoggedIn": "您必须登录才能提交新节点。请通过设置登录。", "enableSubmittableProfile": "在设置中启用可提交的配置文件以提交新节点。", "profileViewOnlyWarning": "此配置文件仅用于地图查看。请选择可提交的配置文件来提交新节点。", + "loadingAreaData": "正在加载区域数据...提交前请稍候。", "refineTags": "细化标签", "refineTagsWithProfile": "细化标签({})" }, @@ -155,6 +156,7 @@ "sandboxModeWarning": "无法将生产节点的编辑提交到沙盒。在设置中切换到生产模式以编辑节点。", "enableSubmittableProfile": "在设置中启用可提交的配置文件以编辑节点。", "profileViewOnlyWarning": "此配置文件仅用于地图查看。请选择可提交的配置文件来编辑节点。", + "loadingAreaData": "正在加载区域数据...提交前请稍候。", "cannotMoveConstrainedNode": "无法移动此相机 - 它连接到另一个地图元素(OSM way/relation)。您仍可以编辑其标签和方向。", "zoomInRequiredMessage": "请放大至至少第{}级来添加或编辑监控节点。这确保精确定位以便准确制图。", "extractFromWay": "从way/relation中提取节点", diff --git a/lib/services/map_data_provider.dart b/lib/services/map_data_provider.dart index 5d6dd52..d989589 100644 --- a/lib/services/map_data_provider.dart +++ b/lib/services/map_data_provider.dart @@ -8,6 +8,7 @@ import '../app_state.dart'; import 'map_data_submodules/tiles_from_remote.dart'; import 'map_data_submodules/tiles_from_local.dart'; import 'node_data_manager.dart'; +import 'node_spatial_cache.dart'; enum MapSource { local, remote, auto } // For future use @@ -156,10 +157,14 @@ class MapDataProvider { } /// NodeCache compatibility methods for upload queue - OsmNode? getNodeById(int nodeId) => _nodeDataManager.getNodeById(nodeId); - void removePendingEditMarker(int nodeId) => _nodeDataManager.removePendingEditMarker(nodeId); - void removePendingDeletionMarker(int nodeId) => _nodeDataManager.removePendingDeletionMarker(nodeId); - void removeTempNodeById(int tempNodeId) => _nodeDataManager.removeTempNodeById(tempNodeId); + /// These all delegate to the singleton cache to ensure consistency + OsmNode? getNodeById(int nodeId) => NodeSpatialCache().getNodeById(nodeId); + void removePendingEditMarker(int nodeId) => NodeSpatialCache().removePendingEditMarker(nodeId); + void removePendingDeletionMarker(int nodeId) => NodeSpatialCache().removePendingDeletionMarker(nodeId); + void removeTempNodeById(int tempNodeId) => NodeSpatialCache().removeTempNodeById(tempNodeId); List findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) => - _nodeDataManager.findNodesWithinDistance(coord, distanceMeters, excludeNodeId: excludeNodeId); + NodeSpatialCache().findNodesWithinDistance(coord, distanceMeters, excludeNodeId: excludeNodeId); + + /// Check if we have good cache coverage for the given area (prevents submission in uncovered areas) + bool hasGoodCoverageFor(LatLngBounds bounds) => NodeSpatialCache().hasDataFor(bounds); } \ No newline at end of file diff --git a/lib/services/node_data_manager.dart b/lib/services/node_data_manager.dart index 5e357ee..b1c2bf5 100644 --- a/lib/services/node_data_manager.dart +++ b/lib/services/node_data_manager.dart @@ -1,5 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:flutter/widgets.dart'; import 'package:latlong2/latlong.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -11,6 +13,8 @@ import 'node_spatial_cache.dart'; import 'network_status.dart'; import 'map_data_submodules/nodes_from_osm_api.dart'; import 'map_data_submodules/nodes_from_local.dart'; +import 'offline_area_service.dart'; +import 'offline_areas/offline_area_models.dart'; /// Coordinates node data fetching between cache, Overpass, and OSM API. /// Simple interface: give me nodes for this view with proper caching and error handling. @@ -32,15 +36,37 @@ class NodeDataManager extends ChangeNotifier { }) async { if (profiles.isEmpty) return []; - // Handle offline mode + // Handle offline mode - no loading states needed, data is instant if (AppState.instance.offlineMode) { + // Clear any existing loading states since offline data is instant + if (isUserInitiated) { + NetworkStatus.instance.clearWaiting(); + } + if (uploadMode == UploadMode.sandbox) { // Offline + Sandbox = no nodes (local cache is production data) debugPrint('[NodeDataManager] Offline + Sandbox mode: returning no nodes'); return []; } else { - // Offline + Production = use local cache only - return fetchLocalNodes(bounds: bounds, profiles: profiles); + // Offline + Production = use local offline areas (instant) + final offlineNodes = await fetchLocalNodes(bounds: bounds, profiles: profiles); + + // Add offline nodes to cache so they integrate with the rest of the system + if (offlineNodes.isNotEmpty) { + _cache.addOrUpdateNodes(offlineNodes); + // Mark this area as having coverage for submit button logic + _cache.markAreaAsFetched(bounds, offlineNodes); + notifyListeners(); + } + + // Show brief success for user-initiated offline loads + if (isUserInitiated && offlineNodes.isNotEmpty) { + WidgetsBinding.instance.addPostFrameCallback((_) { + NetworkStatus.instance.setSuccess(); + }); + } + + return offlineNodes; } } @@ -69,11 +95,16 @@ class NodeDataManager extends ChangeNotifier { try { final nodes = await fetchWithSplitting(bounds, profiles); + // Don't set success immediately - wait for UI to render the nodes + notifyListeners(); + + // Set success after the next frame renders (when nodes are actually visible) if (isUserInitiated) { - NetworkStatus.instance.setSuccess(); + WidgetsBinding.instance.addPostFrameCallback((_) { + NetworkStatus.instance.setSuccess(); + }); } - notifyListeners(); return nodes; } catch (e) { @@ -231,6 +262,39 @@ class NodeDataManager extends ChangeNotifier { List findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) => _cache.findNodesWithinDistance(coord, distanceMeters, excludeNodeId: excludeNodeId); + /// Check if we have good cache coverage for the given area + bool hasGoodCoverageFor(LatLngBounds bounds) { + return _cache.hasDataFor(bounds); + } + + /// Load all offline nodes into cache (call at app startup) + Future preloadOfflineNodes() async { + try { + final offlineAreaService = OfflineAreaService(); + + for (final area in offlineAreaService.offlineAreas) { + if (area.status != OfflineAreaStatus.complete) continue; + + // Load nodes from this offline area + final nodes = await fetchLocalNodes( + bounds: area.bounds, + profiles: [], // Empty profiles = load all nodes + ); + + if (nodes.isNotEmpty) { + _cache.addOrUpdateNodes(nodes); + // Mark the offline area as having coverage so submit buttons work + _cache.markAreaAsFetched(area.bounds, nodes); + debugPrint('[NodeDataManager] Preloaded ${nodes.length} offline nodes from area ${area.name}'); + } + } + + notifyListeners(); + } catch (e) { + debugPrint('[NodeDataManager] Error preloading offline nodes: $e'); + } + } + /// Get cache statistics String get cacheStats => _cache.stats.toString(); } \ No newline at end of file diff --git a/lib/widgets/add_node_sheet.dart b/lib/widgets/add_node_sheet.dart index e8453b5..8381f8f 100644 --- a/lib/widgets/add_node_sheet.dart +++ b/lib/widgets/add_node_sheet.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/flutter_map.dart'; import '../app_state.dart'; import '../dev_config.dart'; @@ -8,6 +10,7 @@ import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../services/localization_service.dart'; import '../services/map_data_provider.dart'; +import '../services/node_data_manager.dart'; import '../services/changelog_service.dart'; import 'refine_tags_sheet.dart'; import 'proximity_warning_dialog.dart'; @@ -31,6 +34,13 @@ class _AddNodeSheetState extends State { void initState() { super.initState(); _checkTutorialStatus(); + // Listen to node data manager for cache updates + NodeDataManager().addListener(_onCacheUpdated); + } + + void _onCacheUpdated() { + // Rebuild when cache updates (e.g., when new data loads) + if (mounted) setState(() {}); } Future _checkTutorialStatus() async { @@ -76,6 +86,9 @@ class _AddNodeSheetState extends State { @override void dispose() { + // Remove listener + NodeDataManager().removeListener(_onCacheUpdated); + // Clear tutorial callback when widget is disposed if (_showTutorial) { try { @@ -401,10 +414,34 @@ class _AddNodeSheetState extends State { final session = widget.session; final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList(); + + // Check if we have good cache coverage around the node position + bool hasGoodCoverage = true; + if (session.target != null) { + // Create a small bounds around the target position to check coverage + const double bufferDegrees = 0.001; // ~100m buffer + final targetBounds = LatLngBounds( + LatLng(session.target!.latitude - bufferDegrees, session.target!.longitude - bufferDegrees), + LatLng(session.target!.latitude + bufferDegrees, session.target!.longitude + bufferDegrees), + ); + hasGoodCoverage = MapDataProvider().hasGoodCoverageFor(targetBounds); + + // If strict coverage check fails, fall back to checking if we have any nodes nearby + // This handles the timing issue where cache might not be marked as "covered" yet + if (!hasGoodCoverage) { + final nearbyNodes = MapDataProvider().findNodesWithinDistance( + session.target!, + 200.0, // 200m radius - if we have nodes nearby, we likely have good data + ); + hasGoodCoverage = nearbyNodes.isNotEmpty; + } + } + final allowSubmit = appState.isLoggedIn && submittableProfiles.isNotEmpty && session.profile != null && - session.profile!.isSubmittable; + session.profile!.isSubmittable && + hasGoodCoverage; void _navigateToLogin() { Navigator.pushNamed(context, '/settings/osm-account'); @@ -517,6 +554,22 @@ class _AddNodeSheetState extends State { ), ], ), + ) + else if (!hasGoodCoverage) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + const Icon(Icons.cloud_download, color: Colors.blue, size: 20), + const SizedBox(width: 6), + Expanded( + child: Text( + locService.t('addNode.loadingAreaData'), + style: const TextStyle(color: Colors.blue, fontSize: 13), + ), + ), + ], + ), ), const SizedBox(height: 8), Padding( diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 5d3d418..806825a 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:flutter_map/flutter_map.dart'; import '../app_state.dart'; import '../dev_config.dart'; @@ -8,6 +10,7 @@ import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../services/localization_service.dart'; import '../services/map_data_provider.dart'; +import '../services/node_data_manager.dart'; import '../services/changelog_service.dart'; import '../state/settings_state.dart'; import 'refine_tags_sheet.dart'; @@ -33,6 +36,13 @@ class _EditNodeSheetState extends State { void initState() { super.initState(); _checkTutorialStatus(); + // Listen to node data manager for cache updates + NodeDataManager().addListener(_onCacheUpdated); + } + + void _onCacheUpdated() { + // Rebuild when cache updates (e.g., when new data loads) + if (mounted) setState(() {}); } Future _checkTutorialStatus() async { @@ -59,8 +69,12 @@ class _EditNodeSheetState extends State { } } + @override @override void dispose() { + // Remove listener + NodeDataManager().removeListener(_onCacheUpdated); + // Clear tutorial callback when widget is disposed if (_showTutorial) { try { @@ -287,11 +301,33 @@ class _EditNodeSheetState extends State { final session = widget.session; final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList(); final isSandboxMode = appState.uploadMode == UploadMode.sandbox; + + // Check if we have good cache coverage around the node position + bool hasGoodCoverage = true; + final nodeCoord = session.originalNode.coord; + const double bufferDegrees = 0.001; // ~100m buffer + final targetBounds = LatLngBounds( + LatLng(nodeCoord.latitude - bufferDegrees, nodeCoord.longitude - bufferDegrees), + LatLng(nodeCoord.latitude + bufferDegrees, nodeCoord.longitude + bufferDegrees), + ); + hasGoodCoverage = MapDataProvider().hasGoodCoverageFor(targetBounds); + + // If strict coverage check fails, fall back to checking if we have any nodes nearby + // This handles the timing issue where cache might not be marked as "covered" yet + if (!hasGoodCoverage) { + final nearbyNodes = MapDataProvider().findNodesWithinDistance( + nodeCoord, + 200.0, // 200m radius - if we have nodes nearby, we likely have good data + ); + hasGoodCoverage = nearbyNodes.isNotEmpty; + } + final allowSubmit = kEnableNodeEdits && appState.isLoggedIn && submittableProfiles.isNotEmpty && session.profile != null && - session.profile!.isSubmittable; + session.profile!.isSubmittable && + hasGoodCoverage; void _navigateToLogin() { Navigator.pushNamed(context, '/settings/osm-account'); @@ -478,6 +514,22 @@ class _EditNodeSheetState extends State { ), ], ), + ) + else if (!hasGoodCoverage) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + const Icon(Icons.cloud_download, color: Colors.blue, size: 20), + const SizedBox(width: 6), + Expanded( + child: Text( + locService.t('editNode.loadingAreaData'), + style: const TextStyle(color: Colors.blue, fontSize: 13), + ), + ), + ], + ), ), const SizedBox(height: 8), Padding( diff --git a/lib/widgets/node_provider_with_cache.dart b/lib/widgets/node_provider_with_cache.dart index c28290f..e54eade 100644 --- a/lib/widgets/node_provider_with_cache.dart +++ b/lib/widgets/node_provider_with_cache.dart @@ -19,12 +19,12 @@ class NodeProviderWithCache extends ChangeNotifier { NodeProviderWithCache._internal(); final NodeDataManager _nodeDataManager = NodeDataManager(); - final NodeSpatialCache _cache = NodeSpatialCache(); Timer? _debounceTimer; /// Get cached nodes for the given bounds, filtered by enabled profiles List getCachedNodesForBounds(LatLngBounds bounds) { - final allNodes = _cache.getNodesFor(bounds); + // Use the same cache instance as NodeDataManager + final allNodes = NodeSpatialCache().getNodesFor(bounds); final enabledProfiles = AppState.instance.enabledProfiles; // If no profiles are enabled, show no nodes @@ -68,7 +68,6 @@ class NodeProviderWithCache extends ChangeNotifier { /// Clear the cache and repopulate with pending nodes from upload queue void clearCache() { - _cache.clear(); _nodeDataManager.clearCache(); // Repopulate with pending nodes from upload queue if available _repopulatePendingNodesAfterClear();