diff --git a/assets/changelog.json b/assets/changelog.json index 22bd7c3..39b5265 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,4 +1,9 @@ { + "2.6.3": { + "content": [ + "• Prevent edit submissions where nothing (location, tags, direction) has been changed" + ] + }, "2.6.2": { "content": [ "• Enhanced edit workflow - existing device properties are preserved by default, reducing accidental tag loss during edits", diff --git a/lib/localizations/de.json b/lib/localizations/de.json index 6d9d3dd..51b5740 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -124,7 +124,10 @@ "extractFromWay": "Knoten aus Weg/Relation extrahieren", "extractFromWaySubtitle": "Neuen Knoten mit gleichen Tags erstellen, Verschieben an neuen Ort ermöglichen", "refineTags": "Tags Verfeinern", - "existingTags": "" + "existingTags": "", + "noChangesDetected": "Keine Änderungen erkannt - nichts zu übertragen", + "noChangesTitle": "Keine Änderungen zu Übertragen", + "noChangesMessage": "Sie haben keine Änderungen an diesem Knoten vorgenommen. Um eine Bearbeitung zu übertragen, müssen Sie den Standort, das Profil, die Richtungen oder die Tags ändern." }, "download": { "title": "Kartenbereich Herunterladen", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 0aca05f..64b9cad 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -161,7 +161,10 @@ "extractFromWay": "Extract node from way/relation", "extractFromWaySubtitle": "Create new node with same tags, allow moving to new location", "refineTags": "Refine Tags", - "existingTags": "" + "existingTags": "", + "noChangesDetected": "No changes detected - nothing to submit", + "noChangesTitle": "No Changes to Submit", + "noChangesMessage": "You haven't made any changes to this node. To submit an edit, you need to change the location, profile, directions, or tags." }, "download": { "title": "Download Map Area", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index 3dc4f16..540cf5e 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -161,7 +161,10 @@ "extractFromWay": "Extraer nodo de way/relation", "extractFromWaySubtitle": "Crear nuevo nodo con las mismas etiquetas, permitir mover a nueva ubicación", "refineTags": "Refinar Etiquetas", - "existingTags": "" + "existingTags": "", + "noChangesDetected": "No se detectaron cambios - nada que enviar", + "noChangesTitle": "No Hay Cambios que Enviar", + "noChangesMessage": "No ha realizado ningún cambio en este nodo. Para enviar una edición, necesita cambiar la ubicación, el perfil, las direcciones o las etiquetas." }, "download": { "title": "Descargar Área del Mapa", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index 1fe71cf..4ad1eea 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -161,7 +161,10 @@ "extractFromWay": "Extraire le nœud du way/relation", "extractFromWaySubtitle": "Créer un nouveau nœud avec les mêmes balises, permettre le déplacement vers un nouvel emplacement", "refineTags": "Affiner Balises", - "existingTags": "" + "existingTags": "", + "noChangesDetected": "Aucun changement détecté - rien à soumettre", + "noChangesTitle": "Aucun Changement à Soumettre", + "noChangesMessage": "Vous n'avez apporté aucun changement à ce nœud. Pour soumettre une modification, vous devez changer l'emplacement, le profil, les directions ou les balises." }, "download": { "title": "Télécharger Zone de Carte", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 45857f2..fdb5e7d 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -161,7 +161,10 @@ "extractFromWay": "Estrai nodo da way/relation", "extractFromWaySubtitle": "Crea nuovo nodo con gli stessi tag, consenti spostamento in nuova posizione", "refineTags": "Affina Tag", - "existingTags": "" + "existingTags": "", + "noChangesDetected": "Nessuna modifica rilevata - niente da inviare", + "noChangesTitle": "Nessuna Modifica da Inviare", + "noChangesMessage": "Non hai apportato modifiche a questo nodo. Per inviare una modifica, devi cambiare la posizione, il profilo, le direzioni o i tag." }, "download": { "title": "Scarica Area Mappa", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index f2150cc..90f6f98 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -161,7 +161,10 @@ "extractFromWay": "Extrair nó do way/relation", "extractFromWaySubtitle": "Criar novo nó com as mesmas tags, permitir mover para nova localização", "refineTags": "Refinar Tags", - "existingTags": "" + "existingTags": "", + "noChangesDetected": "Nenhuma alteração detectada - nada para enviar", + "noChangesTitle": "Nenhuma Alteração para Enviar", + "noChangesMessage": "Você não fez nenhuma alteração neste nó. Para enviar uma edição, você precisa alterar a localização, o perfil, as direções ou as tags." }, "download": { "title": "Baixar Área do Mapa", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 1f3d0b3..dd8ca12 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -161,7 +161,10 @@ "extractFromWay": "从way/relation中提取节点", "extractFromWaySubtitle": "创建具有相同标签的新节点,允许移动到新位置", "refineTags": "细化标签", - "existingTags": "<现有标签>" + "existingTags": "<现有标签>", + "noChangesDetected": "未检测到更改 - 无需提交", + "noChangesTitle": "无更改可提交", + "noChangesMessage": "您尚未对此节点进行任何更改。要提交编辑,您需要更改位置、配置文件、方向或标签。" }, "download": { "title": "下载地图区域", diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 4630c95..c5f294f 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -8,11 +8,13 @@ import '../app_state.dart'; import '../dev_config.dart'; import '../models/node_profile.dart'; import '../models/operator_profile.dart'; +import '../models/pending_upload.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 '../state/session_state.dart'; import 'refine_tags_sheet.dart'; import 'advanced_edit_options_sheet.dart'; import 'proximity_warning_dialog.dart'; @@ -150,6 +152,116 @@ class _EditNodeSheetState extends State { ); } + /// Check if the edit session has any actual changes compared to the original node + bool _hasActualChanges(EditNodeSession session) { + // Extract operation is always a change + if (session.extractFromWay) return true; + + // Check location change + const double tolerance = 0.0000001; // ~1cm precision + if ((session.target.latitude - session.originalNode.coord.latitude).abs() > tolerance || + (session.target.longitude - session.originalNode.coord.longitude).abs() > tolerance) { + return true; + } + + // Check direction changes + if (!_directionsEqual(session.directions, session.originalNode.directionDeg)) { + return true; + } + + // Check tag changes (including operator profile) + final originalTags = session.originalNode.tags; + final newTags = _getSessionCombinedTags(session); + if (!_tagsEqual(originalTags, newTags)) { + return true; + } + + return false; + } + + /// Compare two direction lists, handling empty vs [0] cases + bool _directionsEqual(List sessionDirs, List originalDirs) { + // Sort both lists for comparison + final sorted1 = List.from(sessionDirs)..sort(); + final sorted2 = List.from(originalDirs)..sort(); + + // Handle empty list cases + if (sorted1.isEmpty && sorted2.isEmpty) return true; + if (sorted1.isEmpty || sorted2.isEmpty) { + // Special case: if one is empty and the other is [0], consider them different + // because the user either added or removed a direction + return false; + } + + if (sorted1.length != sorted2.length) return false; + + for (int i = 0; i < sorted1.length; i++) { + if ((sorted1[i] - sorted2[i]).abs() > 0.1) return false; // 0.1° tolerance + } + + return true; + } + + /// Compare two tag maps, ignoring direction tags (handled separately) + bool _tagsEqual(Map tags1, Map tags2) { + final filtered1 = Map.from(tags1); + final filtered2 = Map.from(tags2); + + // Remove direction tags - they're handled separately + filtered1.remove('direction'); + filtered1.remove('camera:direction'); + filtered2.remove('direction'); + filtered2.remove('camera:direction'); + + return _mapEquals(filtered1, filtered2); + } + + /// Deep equality check for maps + bool _mapEquals(Map map1, Map map2) { + if (map1.length != map2.length) return false; + + for (final entry in map1.entries) { + if (map2[entry.key] != entry.value) return false; + } + + return true; + } + + /// Get the combined tags that would be submitted for this session + Map _getSessionCombinedTags(EditNodeSession session) { + if (session.profile == null) return {}; + + // Create a temporary PendingUpload to use its getCombinedTags logic + final tempUpload = PendingUpload( + coord: session.target, + direction: session.directions.isNotEmpty ? session.directions.first : 0.0, + profile: session.profile, + operatorProfile: session.operatorProfile, + refinedTags: session.refinedTags, + uploadMode: UploadMode.production, // Mode doesn't matter for tag combination + operation: UploadOperation.modify, + ); + + return tempUpload.getCombinedTags(); + } + + /// Show dialog explaining why submission is disabled due to no changes + void _showNoChangesDialog(BuildContext context, LocalizationService locService) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(locService.t('editNode.noChangesTitle')), + content: Text(locService.t('editNode.noChangesMessage')), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(locService.ok), + ), + ], + ), + ); + } + Widget _buildDirectionControls(BuildContext context, AppState appState, EditNodeSession session, LocalizationService locService) { final requiresDirection = session.profile != null && session.profile!.requiresDirection; final is360Fov = session.profile?.fov == 360; @@ -321,6 +433,12 @@ class _EditNodeSheetState extends State { final appState = context.watch(); void _commit() { + // Check if there are any actual changes to submit + if (!_hasActualChanges(widget.session)) { + _showNoChangesDialog(context, locService); + return; + } + _checkProximityAndCommit(context, appState, locService); } diff --git a/pubspec.yaml b/pubspec.yaml index fe804e9..e53cc67 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: deflockapp description: Map public surveillance infrastructure with OpenStreetMap publish_to: "none" -version: 2.6.2+46 # The thing after the + is the version code, incremented with each release +version: 2.6.3+46 # The thing after the + is the version code, incremented with each release environment: sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+