Prevent no-change edit submissions

This commit is contained in:
stopflock
2026-02-01 16:51:35 -06:00
parent dd1838319d
commit ff5821b184
10 changed files with 152 additions and 8 deletions

View File

@@ -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",

View File

@@ -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": "<Vorhandene Tags>"
"existingTags": "<Vorhandene Tags>",
"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",

View File

@@ -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": "<Existing tags>"
"existingTags": "<Existing tags>",
"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",

View File

@@ -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": "<Etiquetas existentes>"
"existingTags": "<Etiquetas existentes>",
"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",

View File

@@ -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": "<Balises existantes>"
"existingTags": "<Balises existantes>",
"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",

View File

@@ -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": "<Tag esistenti>"
"existingTags": "<Tag esistenti>",
"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",

View File

@@ -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": "<Tags existentes>"
"existingTags": "<Tags existentes>",
"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",

View File

@@ -161,7 +161,10 @@
"extractFromWay": "从way/relation中提取节点",
"extractFromWaySubtitle": "创建具有相同标签的新节点,允许移动到新位置",
"refineTags": "细化标签",
"existingTags": "<现有标签>"
"existingTags": "<现有标签>",
"noChangesDetected": "未检测到更改 - 无需提交",
"noChangesTitle": "无更改可提交",
"noChangesMessage": "您尚未对此节点进行任何更改。要提交编辑,您需要更改位置、配置文件、方向或标签。"
},
"download": {
"title": "下载地图区域",

View File

@@ -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<EditNodeSheet> {
);
}
/// 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<double> sessionDirs, List<double> originalDirs) {
// Sort both lists for comparison
final sorted1 = List<double>.from(sessionDirs)..sort();
final sorted2 = List<double>.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<String, String> tags1, Map<String, String> tags2) {
final filtered1 = Map<String, String>.from(tags1);
final filtered2 = Map<String, String>.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<String, String> map1, Map<String, String> 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<String, String> _getSessionCombinedTags(EditNodeSession session) {
if (session.profile == null) return <String, String>{};
// 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<void>(
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<EditNodeSheet> {
final appState = context.watch<AppState>();
void _commit() {
// Check if there are any actual changes to submit
if (!_hasActualChanges(widget.session)) {
_showNoChangesDialog(context, locService);
return;
}
_checkProximityAndCommit(context, appState, locService);
}

View File

@@ -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+