mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Repopulate node cache from pending
This commit is contained in:
@@ -227,6 +227,12 @@ The previous implementation conflated changeset creation + node operation as one
|
||||
**Why immediate visual feedback:**
|
||||
Users expect instant response to their actions. By immediately updating the cache with temporary markers (e.g., `_pending_deletion`), the UI stays responsive while the actual API calls happen in background.
|
||||
|
||||
**Queue persistence & cache synchronization (v1.5.4+):**
|
||||
- **Startup repopulation**: Queue initialization now repopulates cache with pending nodes, ensuring visual continuity after app restarts
|
||||
- **Specific node cleanup**: Each upload stores a `tempNodeId` for precise removal, preventing accidental cleanup of other pending nodes at the same location
|
||||
- **Proximity awareness**: Proximity warnings now consider pending nodes to prevent duplicate submissions at the same location
|
||||
- **Processing status UI**: Upload queue screen shows clear indicators when processing is paused due to offline mode or user settings
|
||||
|
||||
### 4. Cache & Visual States
|
||||
|
||||
**Node visual states:**
|
||||
|
||||
@@ -107,7 +107,6 @@ cp lib/keys.dart.example lib/keys.dart
|
||||
- Manual cleanup (cognitive load for users)
|
||||
- Delete the old one (also wrong answer unless user chooses intentionally)
|
||||
- Give multiple of these options??
|
||||
- Persistent cache for MY submissions: assume submissions worked, cache,clean up when we see that node appear in overpass/OSM results or when older than 24h
|
||||
- Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?)
|
||||
- Option to pull in profiles from NSI (man_made=surveillance only?)
|
||||
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
"1.5.4": {
|
||||
"content": [
|
||||
"• OSM message notifications - dot appears on Settings button and OSM Account section when you have unread messages on OpenStreetMap",
|
||||
"• Download area max zoom level is now limited to the currently selected tile provider's maximum zoom level",
|
||||
"• Download area max zoom level is now limited to the currently selected tile provider's maximum zoom level",
|
||||
"• Navigation route planning now prevents selecting start and end locations that are too close together",
|
||||
"• Cleaned up internal 'maxCameras' references to use 'maxNodes' terminology consistently"
|
||||
"• Cleaned up internal 'maxCameras' references to use 'maxNodes' terminology consistently",
|
||||
"• FIXED: Proximity warnings now consider pending nodes - prevents submitting multiple nodes at the same location without warning",
|
||||
"• FIXED: Deleting queue items now only removes that specific item, not all pending nodes at the same location",
|
||||
"• FIXED: Pending nodes now reappear on the map after app restart - queue items repopulate the visual cache on startup",
|
||||
"• NEW: Upload queue screen shows when processing is paused (offline mode or manually paused)"
|
||||
]
|
||||
},
|
||||
"1.5.3": {
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'services/node_cache.dart';
|
||||
import 'services/tile_preview_service.dart';
|
||||
import 'services/changelog_service.dart';
|
||||
import 'services/operator_profile_service.dart';
|
||||
import 'widgets/camera_provider_with_cache.dart';
|
||||
import 'services/profile_service.dart';
|
||||
import 'widgets/proximity_warning_dialog.dart';
|
||||
import 'widgets/reauth_messages_dialog.dart';
|
||||
@@ -212,6 +213,11 @@ class AppState extends ChangeNotifier {
|
||||
await _uploadQueueState.init();
|
||||
await _authState.init(_settingsState.uploadMode);
|
||||
|
||||
// Set up callback to repopulate pending nodes after cache clears
|
||||
CameraProviderWithCache.instance.setOnCacheClearedCallback(() {
|
||||
_uploadQueueState.repopulateCacheFromQueue();
|
||||
});
|
||||
|
||||
// Check for messages on app launch if user is already logged in
|
||||
if (isLoggedIn) {
|
||||
checkMessages();
|
||||
|
||||
@@ -152,7 +152,8 @@
|
||||
"simulate": "Simulieren",
|
||||
"productionDescription": "Hochladen in die Live-OSM-Datenbank (für alle Benutzer sichtbar)",
|
||||
"sandboxDescription": "Uploads gehen an die OSM Sandbox (sicher zum Testen, wird regelmäßig zurückgesetzt).",
|
||||
"simulateDescription": "Uploads simulieren (kontaktiert OSM-Server nicht)"
|
||||
"simulateDescription": "Uploads simulieren (kontaktiert OSM-Server nicht)",
|
||||
"cannotChangeWithQueue": "Upload-Ziel kann nicht geändert werden, während {} Elemente in der Warteschlange sind. Warteschlange zuerst leeren."
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "OpenStreetMap-Konto",
|
||||
@@ -220,7 +221,10 @@
|
||||
"errorDetails": "Fehlerdetails",
|
||||
"creatingChangeset": " (Changeset erstellen...)",
|
||||
"uploading": " (Uploading...)",
|
||||
"closingChangeset": " (Changeset schließen...)"
|
||||
"closingChangeset": " (Changeset schließen...)",
|
||||
"processingPaused": "Warteschlangenverarbeitung pausiert",
|
||||
"pausedDueToOffline": "Upload-Verarbeitung ist pausiert, da der Offline-Modus aktiviert ist.",
|
||||
"pausedByUser": "Upload-Verarbeitung ist manuell pausiert."
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "Kachel-Anbieter",
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"simulate": "Simulate",
|
||||
"productionDescription": "Upload to the live OSM database (visible to all users)",
|
||||
"sandboxDescription": "Uploads go to the OSM Sandbox (safe for testing, resets regularly).",
|
||||
"simulateDescription": "Simulate uploads (does not contact OSM servers)"
|
||||
"simulateDescription": "Simulate uploads (does not contact OSM servers)",
|
||||
"cannotChangeWithQueue": "Cannot change upload destination while {} items are in queue. Clear queue first."
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "OpenStreetMap Account",
|
||||
@@ -252,7 +253,10 @@
|
||||
"errorDetails": "Error Details",
|
||||
"creatingChangeset": " (Creating changeset...)",
|
||||
"uploading": " (Uploading...)",
|
||||
"closingChangeset": " (Closing changeset...)"
|
||||
"closingChangeset": " (Closing changeset...)",
|
||||
"processingPaused": "Queue Processing Paused",
|
||||
"pausedDueToOffline": "Upload processing is paused because offline mode is enabled.",
|
||||
"pausedByUser": "Upload processing is manually paused."
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "Tile Providers",
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"simulate": "Simular",
|
||||
"productionDescription": "Subir a la base de datos OSM en vivo (visible para todos los usuarios)",
|
||||
"sandboxDescription": "Las subidas van al Sandbox de OSM (seguro para pruebas, se reinicia regularmente).",
|
||||
"simulateDescription": "Simular subidas (no contacta servidores OSM)"
|
||||
"simulateDescription": "Simular subidas (no contacta servidores OSM)",
|
||||
"cannotChangeWithQueue": "No se puede cambiar el destino de subida mientras hay {} elementos en cola. Limpie la cola primero."
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "Cuenta de OpenStreetMap",
|
||||
@@ -252,7 +253,10 @@
|
||||
"errorDetails": "Detalles del Error",
|
||||
"creatingChangeset": " (Creando changeset...)",
|
||||
"uploading": " (Subiendo...)",
|
||||
"closingChangeset": " (Cerrando changeset...)"
|
||||
"closingChangeset": " (Cerrando changeset...)",
|
||||
"processingPaused": "Procesamiento de Cola Pausado",
|
||||
"pausedDueToOffline": "El procesamiento de subida está pausado porque el modo sin conexión está habilitado.",
|
||||
"pausedByUser": "El procesamiento de subida está pausado manualmente."
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "Proveedores de Tiles",
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"simulate": "Simuler",
|
||||
"productionDescription": "Télécharger vers la base de données OSM en direct (visible pour tous les utilisateurs)",
|
||||
"sandboxDescription": "Les téléchargements vont vers le Sandbox OSM (sûr pour les tests, réinitialisé régulièrement).",
|
||||
"simulateDescription": "Simuler les téléchargements (ne contacte pas les serveurs OSM)"
|
||||
"simulateDescription": "Simuler les téléchargements (ne contacte pas les serveurs OSM)",
|
||||
"cannotChangeWithQueue": "Impossible de changer la destination de téléversement tant que {} éléments sont en file d'attente. Videz d'abord la file d'attente."
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "Compte OpenStreetMap",
|
||||
@@ -252,7 +253,10 @@
|
||||
"errorDetails": "Détails de l'Erreur",
|
||||
"creatingChangeset": " (Création du changeset...)",
|
||||
"uploading": " (Téléchargement...)",
|
||||
"closingChangeset": " (Fermeture du changeset...)"
|
||||
"closingChangeset": " (Fermeture du changeset...)",
|
||||
"processingPaused": "Traitement de la File d'Attente Interrompu",
|
||||
"pausedDueToOffline": "Le traitement des téléversements est interrompu car le mode hors ligne est activé.",
|
||||
"pausedByUser": "Le traitement des téléversements est interrompu manuellement."
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "Fournisseurs de Tuiles",
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"simulate": "Simula",
|
||||
"productionDescription": "Carica nel database OSM dal vivo (visibile a tutti gli utenti)",
|
||||
"sandboxDescription": "Gli upload vanno alla Sandbox OSM (sicuro per i test, si resetta regolarmente).",
|
||||
"simulateDescription": "Simula upload (non contatta i server OSM)"
|
||||
"simulateDescription": "Simula upload (non contatta i server OSM)",
|
||||
"cannotChangeWithQueue": "Impossibile cambiare la destinazione di upload mentre ci sono {} elementi in coda. Svuota prima la coda."
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "Account OpenStreetMap",
|
||||
@@ -252,7 +253,10 @@
|
||||
"errorDetails": "Dettagli dell'Errore",
|
||||
"creatingChangeset": " (Creazione changeset...)",
|
||||
"uploading": " (Caricamento...)",
|
||||
"closingChangeset": " (Chiusura changeset...)"
|
||||
"closingChangeset": " (Chiusura changeset...)",
|
||||
"processingPaused": "Elaborazione Coda Sospesa",
|
||||
"pausedDueToOffline": "L'elaborazione dei caricamenti è sospesa perché la modalità offline è abilitata.",
|
||||
"pausedByUser": "L'elaborazione dei caricamenti è sospesa manualmente."
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "Fornitori di Tile",
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"simulate": "Simular",
|
||||
"productionDescription": "Enviar para o banco de dados OSM ao vivo (visível para todos os usuários)",
|
||||
"sandboxDescription": "Uploads vão para o Sandbox OSM (seguro para testes, redefine regularmente).",
|
||||
"simulateDescription": "Simular uploads (não contacta servidores OSM)"
|
||||
"simulateDescription": "Simular uploads (não contacta servidores OSM)",
|
||||
"cannotChangeWithQueue": "Não é possível alterar o destino de upload enquanto {} itens estão na fila. Limpe a fila primeiro."
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "Conta OpenStreetMap",
|
||||
@@ -252,7 +253,10 @@
|
||||
"errorDetails": "Detalhes do Erro",
|
||||
"creatingChangeset": " (Criando changeset...)",
|
||||
"uploading": " (Enviando...)",
|
||||
"closingChangeset": " (Fechando changeset...)"
|
||||
"closingChangeset": " (Fechando changeset...)",
|
||||
"processingPaused": "Processamento da Fila Pausado",
|
||||
"pausedDueToOffline": "O processamento de upload está pausado porque o modo offline está habilitado.",
|
||||
"pausedByUser": "O processamento de upload está pausado manualmente."
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "Provedores de Tiles",
|
||||
|
||||
@@ -184,7 +184,8 @@
|
||||
"simulate": "模拟",
|
||||
"productionDescription": "上传到实时 OSM 数据库(对所有用户可见)",
|
||||
"sandboxDescription": "上传到 OSM 沙盒(测试安全,定期重置)。",
|
||||
"simulateDescription": "模拟上传(不联系 OSM 服务器)"
|
||||
"simulateDescription": "模拟上传(不联系 OSM 服务器)",
|
||||
"cannotChangeWithQueue": "队列中有 {} 个项目时无法更改上传目标。请先清空队列。"
|
||||
},
|
||||
"auth": {
|
||||
"osmAccountTitle": "OpenStreetMap 账户",
|
||||
@@ -252,7 +253,10 @@
|
||||
"errorDetails": "错误详情",
|
||||
"creatingChangeset": " (创建变更集...)",
|
||||
"uploading": " (上传中...)",
|
||||
"closingChangeset": " (关闭变更集...)"
|
||||
"closingChangeset": " (关闭变更集...)",
|
||||
"processingPaused": "队列处理已暂停",
|
||||
"pausedDueToOffline": "因为离线模式已启用,上传处理已暂停。",
|
||||
"pausedByUser": "上传处理已手动暂停。"
|
||||
},
|
||||
"tileProviders": {
|
||||
"title": "瓦片提供商",
|
||||
|
||||
@@ -25,6 +25,7 @@ class PendingUpload {
|
||||
final UploadOperation operation; // Type of operation: create, modify, or delete
|
||||
final int? originalNodeId; // If this is modify/delete, the ID of the original OSM node
|
||||
int? submittedNodeId; // The actual node ID returned by OSM after successful submission
|
||||
int? tempNodeId; // ID of temporary node created in cache (for specific cleanup)
|
||||
int attempts;
|
||||
bool error; // DEPRECATED: Use uploadState instead
|
||||
String? errorMessage; // Detailed error message for debugging
|
||||
@@ -46,6 +47,7 @@ class PendingUpload {
|
||||
required this.operation,
|
||||
this.originalNodeId,
|
||||
this.submittedNodeId,
|
||||
this.tempNodeId,
|
||||
this.attempts = 0,
|
||||
this.error = false,
|
||||
this.errorMessage,
|
||||
@@ -255,6 +257,7 @@ class PendingUpload {
|
||||
'operation': operation.index,
|
||||
'originalNodeId': originalNodeId,
|
||||
'submittedNodeId': submittedNodeId,
|
||||
'tempNodeId': tempNodeId,
|
||||
'attempts': attempts,
|
||||
'error': error,
|
||||
'errorMessage': errorMessage,
|
||||
@@ -285,6 +288,7 @@ class PendingUpload {
|
||||
: (j['originalNodeId'] != null ? UploadOperation.modify : UploadOperation.create), // Legacy compatibility
|
||||
originalNodeId: j['originalNodeId'],
|
||||
submittedNodeId: j['submittedNodeId'],
|
||||
tempNodeId: j['tempNodeId'],
|
||||
attempts: j['attempts'] ?? 0,
|
||||
error: j['error'] ?? false,
|
||||
errorMessage: j['errorMessage'], // Can be null for legacy entries
|
||||
|
||||
@@ -37,7 +37,7 @@ class UploadModeSection extends StatelessWidget {
|
||||
child: Text(locService.t('uploadMode.simulate')),
|
||||
),
|
||||
],
|
||||
onChanged: (mode) {
|
||||
onChanged: appState.pendingCount > 0 ? null : (mode) {
|
||||
if (mode != null) {
|
||||
appState.setUploadMode(mode);
|
||||
// Check if re-authentication is needed after mode change
|
||||
@@ -50,27 +50,65 @@ class UploadModeSection extends StatelessWidget {
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 56, top: 2, right: 16, bottom: 12),
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
switch (appState.uploadMode) {
|
||||
case UploadMode.production:
|
||||
return Text(
|
||||
locService.t('uploadMode.productionDescription'),
|
||||
style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7))
|
||||
);
|
||||
case UploadMode.sandbox:
|
||||
return Text(
|
||||
locService.t('uploadMode.sandboxDescription'),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.orange),
|
||||
);
|
||||
case UploadMode.simulate:
|
||||
default:
|
||||
return Text(
|
||||
locService.t('uploadMode.simulateDescription'),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.deepPurple)
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Upload mode restriction message when queue has items
|
||||
if (appState.pendingCount > 0) ...[
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, color: Colors.orange, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
locService.t('uploadMode.cannotChangeWithQueue', params: [appState.pendingCount.toString()]),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.orange),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
],
|
||||
|
||||
// Normal upload mode description
|
||||
Builder(
|
||||
builder: (context) {
|
||||
switch (appState.uploadMode) {
|
||||
case UploadMode.production:
|
||||
return Text(
|
||||
locService.t('uploadMode.productionDescription'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appState.pendingCount > 0
|
||||
? Theme.of(context).disabledColor
|
||||
: Theme.of(context).colorScheme.onSurface.withOpacity(0.7)
|
||||
)
|
||||
);
|
||||
case UploadMode.sandbox:
|
||||
return Text(
|
||||
locService.t('uploadMode.sandboxDescription'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appState.pendingCount > 0
|
||||
? Theme.of(context).disabledColor
|
||||
: Colors.orange
|
||||
),
|
||||
);
|
||||
case UploadMode.simulate:
|
||||
default:
|
||||
return Text(
|
||||
locService.t('uploadMode.simulateDescription'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: appState.pendingCount > 0
|
||||
? Theme.of(context).disabledColor
|
||||
: Colors.deepPurple
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -93,6 +93,9 @@ class UploadQueueScreen extends StatelessWidget {
|
||||
final locService = LocalizationService.instance;
|
||||
final appState = context.watch<AppState>();
|
||||
|
||||
// Check if queue processing is paused
|
||||
final isQueuePaused = appState.offlineMode || appState.pauseQueueProcessing;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(locService.t('queue.title')),
|
||||
@@ -105,6 +108,46 @@ class UploadQueueScreen extends StatelessWidget {
|
||||
16 + MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
children: [
|
||||
// Queue processing status indicator
|
||||
if (isQueuePaused && appState.pendingCount > 0)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withOpacity(0.1),
|
||||
border: Border.all(color: Colors.orange.withOpacity(0.3)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.pause_circle_outline, color: Colors.orange),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
locService.t('queue.processingPaused'),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.orange,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
appState.offlineMode
|
||||
? locService.t('queue.pausedDueToOffline')
|
||||
: locService.t('queue.pausedByUser'),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Clear Upload Queue button - always visible
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -114,6 +114,23 @@ class NodeCache {
|
||||
print('[NodeCache] Removed ${nodesToRemove.length} temp nodes at coordinate ${coord.latitude}, ${coord.longitude}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a specific temporary node by its ID (for queue item-specific cleanup)
|
||||
void removeTempNodeById(int tempNodeId) {
|
||||
if (tempNodeId >= 0) {
|
||||
print('[NodeCache] Warning: Attempted to remove non-temp node ID $tempNodeId');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_nodes.remove(tempNodeId) != null) {
|
||||
print('[NodeCache] Removed specific temp node $tempNodeId from cache');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a specific node by ID (returns null if not found)
|
||||
OsmNode? getNodeById(int nodeId) {
|
||||
return _nodes[nodeId];
|
||||
}
|
||||
|
||||
/// Check if two coordinates match within tolerance
|
||||
bool _coordsMatch(LatLng coord1, LatLng coord2, double tolerance) {
|
||||
@@ -123,6 +140,7 @@ class NodeCache {
|
||||
|
||||
/// Find nodes within the specified distance (in meters) of the given coordinate
|
||||
/// Excludes nodes with the excludeNodeId (useful when checking proximity for edited nodes)
|
||||
/// Includes pending nodes to warn about potential duplicates
|
||||
List<OsmNode> findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) {
|
||||
final nearbyNodes = <OsmNode>[];
|
||||
|
||||
@@ -132,11 +150,9 @@ class NodeCache {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip temporary nodes (negative IDs) with pending upload/edit/deletion markers
|
||||
if (node.id < 0 && (
|
||||
node.tags.containsKey('_pending_upload') ||
|
||||
node.tags.containsKey('_pending_edit') ||
|
||||
node.tags.containsKey('_pending_deletion'))) {
|
||||
// Include all nodes (real and pending) to catch potential duplicates
|
||||
// Only skip nodes marked for deletion since they won't actually exist after processing
|
||||
if (node.tags.containsKey('_pending_deletion')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,9 +21,100 @@ class UploadQueueState extends ChangeNotifier {
|
||||
int get pendingCount => _queue.length;
|
||||
List<PendingUpload> get pendingUploads => List.unmodifiable(_queue);
|
||||
|
||||
// Initialize by loading queue from storage
|
||||
// Initialize by loading queue from storage and repopulate cache with pending nodes
|
||||
Future<void> init() async {
|
||||
await _loadQueue();
|
||||
print('[UploadQueue] Loaded ${_queue.length} items from storage');
|
||||
_repopulateCacheFromQueue();
|
||||
}
|
||||
|
||||
// Repopulate the cache with pending nodes from the queue on startup
|
||||
void _repopulateCacheFromQueue() {
|
||||
print('[UploadQueue] Repopulating cache from ${_queue.length} queue items');
|
||||
final nodesToAdd = <OsmNode>[];
|
||||
|
||||
for (final upload in _queue) {
|
||||
// Skip completed uploads - they should already be in OSM and will be fetched normally
|
||||
if (upload.isComplete) {
|
||||
print('[UploadQueue] Skipping completed upload at ${upload.coord}');
|
||||
continue;
|
||||
}
|
||||
|
||||
print('[UploadQueue] Processing ${upload.operation} upload at ${upload.coord}');
|
||||
|
||||
if (upload.isDeletion) {
|
||||
// For deletions: mark the original node as pending deletion if it exists in cache
|
||||
if (upload.originalNodeId != null) {
|
||||
final existingNode = NodeCache.instance.getNodeById(upload.originalNodeId!);
|
||||
if (existingNode != null) {
|
||||
final deletionTags = Map<String, String>.from(existingNode.tags);
|
||||
deletionTags['_pending_deletion'] = 'true';
|
||||
|
||||
final nodeWithDeletionTag = OsmNode(
|
||||
id: upload.originalNodeId!,
|
||||
coord: existingNode.coord,
|
||||
tags: deletionTags,
|
||||
);
|
||||
nodesToAdd.add(nodeWithDeletionTag);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For creates, edits, and extracts: recreate temp node if needed
|
||||
// Generate new temp ID if not already stored (for backward compatibility)
|
||||
final tempId = upload.tempNodeId ?? -DateTime.now().millisecondsSinceEpoch - _queue.indexOf(upload);
|
||||
|
||||
final tags = upload.getCombinedTags();
|
||||
tags['_pending_upload'] = 'true';
|
||||
tags['_temp_id'] = tempId.toString();
|
||||
|
||||
// Store temp ID for future cleanup if not already set
|
||||
if (upload.tempNodeId == null) {
|
||||
upload.tempNodeId = tempId;
|
||||
}
|
||||
|
||||
if (upload.isEdit) {
|
||||
// For edits: also mark original with _pending_edit if it exists
|
||||
if (upload.originalNodeId != null) {
|
||||
final existingOriginal = NodeCache.instance.getNodeById(upload.originalNodeId!);
|
||||
if (existingOriginal != null) {
|
||||
final originalTags = Map<String, String>.from(existingOriginal.tags);
|
||||
originalTags['_pending_edit'] = 'true';
|
||||
|
||||
final originalWithEdit = OsmNode(
|
||||
id: upload.originalNodeId!,
|
||||
coord: existingOriginal.coord,
|
||||
tags: originalTags,
|
||||
);
|
||||
nodesToAdd.add(originalWithEdit);
|
||||
}
|
||||
}
|
||||
|
||||
// Add connection line marker
|
||||
tags['_original_node_id'] = upload.originalNodeId.toString();
|
||||
} else if (upload.operation == UploadOperation.extract) {
|
||||
// For extracts: add connection line marker
|
||||
tags['_original_node_id'] = upload.originalNodeId.toString();
|
||||
}
|
||||
|
||||
final tempNode = OsmNode(
|
||||
id: tempId,
|
||||
coord: upload.coord,
|
||||
tags: tags,
|
||||
);
|
||||
nodesToAdd.add(tempNode);
|
||||
}
|
||||
}
|
||||
|
||||
if (nodesToAdd.isNotEmpty) {
|
||||
NodeCache.instance.addOrUpdate(nodesToAdd);
|
||||
print('[UploadQueue] Repopulated cache with ${nodesToAdd.length} pending nodes from queue');
|
||||
|
||||
// Save queue if we updated any temp IDs for backward compatibility
|
||||
_saveQueue();
|
||||
|
||||
// Notify node provider to update the map
|
||||
CameraProviderWithCache.instance.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// Add a completed session to the upload queue
|
||||
@@ -46,6 +137,10 @@ class UploadQueueState extends ChangeNotifier {
|
||||
final tempId = -DateTime.now().millisecondsSinceEpoch;
|
||||
final tags = upload.getCombinedTags();
|
||||
tags['_pending_upload'] = 'true'; // Mark as pending for potential UI distinction
|
||||
tags['_temp_id'] = tempId.toString(); // Store temp ID for specific removal
|
||||
|
||||
// Store the temp ID in the upload for cleanup purposes
|
||||
upload.tempNodeId = tempId;
|
||||
|
||||
final tempNode = OsmNode(
|
||||
id: tempId,
|
||||
@@ -100,6 +195,10 @@ class UploadQueueState extends ChangeNotifier {
|
||||
final extractedTags = upload.getCombinedTags();
|
||||
extractedTags['_pending_upload'] = 'true'; // Mark as pending upload
|
||||
extractedTags['_original_node_id'] = session.originalNode.id.toString(); // Track original for line drawing
|
||||
extractedTags['_temp_id'] = tempId.toString(); // Store temp ID for specific removal
|
||||
|
||||
// Store the temp ID in the upload for cleanup purposes
|
||||
upload.tempNodeId = tempId;
|
||||
|
||||
final extractedNode = OsmNode(
|
||||
id: tempId,
|
||||
@@ -125,6 +224,10 @@ class UploadQueueState extends ChangeNotifier {
|
||||
final editedTags = upload.getCombinedTags();
|
||||
editedTags['_pending_upload'] = 'true'; // Mark as pending upload
|
||||
editedTags['_original_node_id'] = session.originalNode.id.toString(); // Track original for line drawing
|
||||
editedTags['_temp_id'] = tempId.toString(); // Store temp ID for specific removal
|
||||
|
||||
// Store the temp ID in the upload for cleanup purposes
|
||||
upload.tempNodeId = tempId;
|
||||
|
||||
final editedNode = OsmNode(
|
||||
id: tempId,
|
||||
@@ -548,8 +651,10 @@ class UploadQueueState extends ChangeNotifier {
|
||||
// Add/update the cache with the real node
|
||||
NodeCache.instance.addOrUpdate([realNode]);
|
||||
|
||||
// Clean up any temp nodes at the same coordinate
|
||||
NodeCache.instance.removeTempNodesByCoordinate(item.coord);
|
||||
// Clean up the specific temp node for this upload
|
||||
if (item.tempNodeId != null) {
|
||||
NodeCache.instance.removeTempNodeById(item.tempNodeId!);
|
||||
}
|
||||
|
||||
// For modify operations, clean up the original node's _pending_edit marker
|
||||
// For extract operations, we don't modify the original node so leave it unchanged
|
||||
@@ -609,17 +714,23 @@ class UploadQueueState extends ChangeNotifier {
|
||||
NodeCache.instance.removePendingDeletionMarker(upload.originalNodeId!);
|
||||
}
|
||||
} else if (upload.isEdit) {
|
||||
// For edits: remove both the temp node and the _pending_edit marker from original
|
||||
NodeCache.instance.removeTempNodesByCoordinate(upload.coord);
|
||||
// For edits: remove the specific temp node and the _pending_edit marker from original
|
||||
if (upload.tempNodeId != null) {
|
||||
NodeCache.instance.removeTempNodeById(upload.tempNodeId!);
|
||||
}
|
||||
if (upload.originalNodeId != null) {
|
||||
NodeCache.instance.removePendingEditMarker(upload.originalNodeId!);
|
||||
}
|
||||
} else if (upload.operation == UploadOperation.extract) {
|
||||
// For extracts: remove the temp node (leave original unchanged)
|
||||
NodeCache.instance.removeTempNodesByCoordinate(upload.coord);
|
||||
// For extracts: remove the specific temp node (leave original unchanged)
|
||||
if (upload.tempNodeId != null) {
|
||||
NodeCache.instance.removeTempNodeById(upload.tempNodeId!);
|
||||
}
|
||||
} else {
|
||||
// For creates: remove the temp node
|
||||
NodeCache.instance.removeTempNodesByCoordinate(upload.coord);
|
||||
// For creates: remove the specific temp node
|
||||
if (upload.tempNodeId != null) {
|
||||
NodeCache.instance.removeTempNodeById(upload.tempNodeId!);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,6 +757,11 @@ class UploadQueueState extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// Public method to manually trigger cache repopulation (useful for debugging or after cache clears)
|
||||
void repopulateCacheFromQueue() {
|
||||
_repopulateCacheFromQueue();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_uploadTimer?.cancel();
|
||||
|
||||
@@ -67,12 +67,33 @@ class CameraProviderWithCache extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
/// Optionally: clear the cache (could be used for testing/dev)
|
||||
/// Clear the cache and repopulate with pending nodes from upload queue
|
||||
void clearCache() {
|
||||
NodeCache.instance.clear();
|
||||
// Repopulate with pending nodes from upload queue if available
|
||||
_repopulatePendingNodesAfterClear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Repopulate pending nodes after cache clear
|
||||
void _repopulatePendingNodesAfterClear() {
|
||||
// We need access to the upload queue state, but we don't have direct access here
|
||||
// Instead, we'll trigger a callback that the app state can handle
|
||||
// For now, let's use a more direct approach through a global service access
|
||||
// This could be refactored to use proper dependency injection later
|
||||
Future.microtask(() {
|
||||
// This will be called from app state when cache clears happen
|
||||
_onCacheCleared?.call();
|
||||
});
|
||||
}
|
||||
|
||||
VoidCallback? _onCacheCleared;
|
||||
|
||||
/// Set callback for when cache is cleared (used by app state to repopulate pending nodes)
|
||||
void setOnCacheClearedCallback(VoidCallback? callback) {
|
||||
_onCacheCleared = callback;
|
||||
}
|
||||
|
||||
/// Force refresh the display (useful when filters change but cache doesn't)
|
||||
void refreshDisplay() {
|
||||
notifyListeners();
|
||||
|
||||
Reference in New Issue
Block a user