mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Disallow editing location of nodes attached to ways/relations
This commit is contained in:
@@ -50,7 +50,7 @@ const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simu
|
||||
const bool kEnableNavigationFeatures = kEnableDevelopmentModes; // Hide navigation until fully implemented
|
||||
|
||||
// Node editing features - set to false to temporarily disable editing
|
||||
const bool kEnableNodeEdits = false; // Set to false to temporarily disable node editing
|
||||
const bool kEnableNodeEdits = true; // Set to false to temporarily disable node editing
|
||||
|
||||
/// Navigation availability: only dev builds, and only when online
|
||||
bool enableNavigationFeatures({required bool offlineMode}) {
|
||||
|
||||
@@ -94,6 +94,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.",
|
||||
"cannotMoveConstrainedNode": "Kann diese Kamera nicht verschieben - sie ist mit einer Straße oder einem Bereich verbunden (OSM-Weg/Relation). Sie können trotzdem ihre Tags und Richtung bearbeiten.",
|
||||
"refineTags": "Tags Verfeinern",
|
||||
"refineTagsWithProfile": "Tags Verfeinern ({})"
|
||||
},
|
||||
@@ -307,7 +308,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "Netzwerkstatus-Anzeige anzeigen",
|
||||
"showIndicatorSubtitle": "Netzwerk-Ladestatus und Fehlerstatus auf der Karte anzeigen"
|
||||
"showIndicatorSubtitle": "Netzwerk-Ladestatus und Fehlerstatus auf der Karte anzeigen",
|
||||
"loading": "Lädt...",
|
||||
"timedOut": "Zeitüberschreitung",
|
||||
"noData": "Keine Kacheln hier",
|
||||
"success": "Fertig",
|
||||
"nodeLimitReached": "Limit erreicht - in Einstellungen erhöhen",
|
||||
"tileProviderSlow": "Kartenanbieter langsam",
|
||||
"nodeDataSlow": "Knotendaten langsam",
|
||||
"networkIssues": "Netzwerkprobleme"
|
||||
},
|
||||
"about": {
|
||||
"title": "DeFlock - Überwachungs-Transparenz",
|
||||
|
||||
@@ -112,6 +112,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.",
|
||||
"cannotMoveConstrainedNode": "Cannot move this camera - it's connected to a street or area (OSM way/relation). You can still edit its tags and direction.",
|
||||
"refineTags": "Refine Tags",
|
||||
"refineTagsWithProfile": "Refine Tags ({})"
|
||||
},
|
||||
@@ -325,7 +326,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "Show network status indicator",
|
||||
"showIndicatorSubtitle": "Display network loading and error status on the map"
|
||||
"showIndicatorSubtitle": "Display network loading and error status on the map",
|
||||
"loading": "Loading...",
|
||||
"timedOut": "Timed out",
|
||||
"noData": "No tiles here",
|
||||
"success": "Done",
|
||||
"nodeLimitReached": "Showing limit - increase in settings",
|
||||
"tileProviderSlow": "Tile provider slow",
|
||||
"nodeDataSlow": "Node data slow",
|
||||
"networkIssues": "Network issues"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "Search Location",
|
||||
|
||||
@@ -112,6 +112,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.",
|
||||
"cannotMoveConstrainedNode": "No se puede mover esta cámara - está conectada a una calle o área (OSM way/relation). Aún puede editar sus etiquetas y dirección.",
|
||||
"refineTags": "Refinar Etiquetas",
|
||||
"refineTagsWithProfile": "Refinar Etiquetas ({})"
|
||||
},
|
||||
@@ -325,7 +326,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "Mostrar indicador de estado de red",
|
||||
"showIndicatorSubtitle": "Mostrar estado de carga y errores de red en el mapa"
|
||||
"showIndicatorSubtitle": "Mostrar estado de carga y errores de red en el mapa",
|
||||
"loading": "Cargando...",
|
||||
"timedOut": "Tiempo agotado",
|
||||
"noData": "Sin mosaicos aquí",
|
||||
"success": "Hecho",
|
||||
"nodeLimitReached": "Mostrando límite - aumentar en ajustes",
|
||||
"tileProviderSlow": "Proveedor de mosaicos lento",
|
||||
"nodeDataSlow": "Datos de nodo lentos",
|
||||
"networkIssues": "Problemas de red"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "Buscar ubicación",
|
||||
|
||||
@@ -112,6 +112,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.",
|
||||
"cannotMoveConstrainedNode": "Impossible de déplacer cette caméra - elle est connectée à une rue ou une zone (OSM way/relation). Vous pouvez toujours modifier ses balises et sa direction.",
|
||||
"refineTags": "Affiner Balises",
|
||||
"refineTagsWithProfile": "Affiner Balises ({})"
|
||||
},
|
||||
@@ -325,7 +326,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "Afficher l'indicateur de statut réseau",
|
||||
"showIndicatorSubtitle": "Afficher l'état de chargement et d'erreur réseau sur la carte"
|
||||
"showIndicatorSubtitle": "Afficher l'état de chargement et d'erreur réseau sur la carte",
|
||||
"loading": "Chargement...",
|
||||
"timedOut": "Temps dépassé",
|
||||
"noData": "Aucune tuile ici",
|
||||
"success": "Terminé",
|
||||
"nodeLimitReached": "Limite affichée - augmenter dans les paramètres",
|
||||
"tileProviderSlow": "Fournisseur de tuiles lent",
|
||||
"nodeDataSlow": "Données de nœud lentes",
|
||||
"networkIssues": "Problèmes réseau"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "Rechercher lieu",
|
||||
|
||||
@@ -112,6 +112,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.",
|
||||
"cannotMoveConstrainedNode": "Impossibile spostare questa telecamera - è collegata a una strada o area (OSM way/relation). Puoi ancora modificare i suoi tag e direzione.",
|
||||
"refineTags": "Affina Tag",
|
||||
"refineTagsWithProfile": "Affina Tag ({})"
|
||||
},
|
||||
@@ -325,7 +326,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "Mostra indicatore di stato di rete",
|
||||
"showIndicatorSubtitle": "Visualizza lo stato di caricamento e errori di rete sulla mappa"
|
||||
"showIndicatorSubtitle": "Visualizza lo stato di caricamento e errori di rete sulla mappa",
|
||||
"loading": "Caricamento...",
|
||||
"timedOut": "Tempo scaduto",
|
||||
"noData": "Nessuna tessera qui",
|
||||
"success": "Fatto",
|
||||
"nodeLimitReached": "Limite visualizzato - aumentare nelle impostazioni",
|
||||
"tileProviderSlow": "Provider di tessere lento",
|
||||
"nodeDataSlow": "Dati del nodo lenti",
|
||||
"networkIssues": "Problemi di rete"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "Cerca posizione",
|
||||
|
||||
@@ -112,6 +112,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.",
|
||||
"cannotMoveConstrainedNode": "Não é possível mover esta câmera - ela está conectada a uma rua ou área (OSM way/relation). Você ainda pode editar suas tags e direção.",
|
||||
"refineTags": "Refinar Tags",
|
||||
"refineTagsWithProfile": "Refinar Tags ({})"
|
||||
},
|
||||
@@ -325,7 +326,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "Exibir indicador de status de rede",
|
||||
"showIndicatorSubtitle": "Mostrar status de carregamento e erro de rede no mapa"
|
||||
"showIndicatorSubtitle": "Mostrar status de carregamento e erro de rede no mapa",
|
||||
"loading": "Carregando...",
|
||||
"timedOut": "Tempo esgotado",
|
||||
"noData": "Nenhum tile aqui",
|
||||
"success": "Concluído",
|
||||
"nodeLimitReached": "Limite exibido - aumentar nas configurações",
|
||||
"tileProviderSlow": "Provedor de tiles lento",
|
||||
"nodeDataSlow": "Dados do nó lentos",
|
||||
"networkIssues": "Problemas de rede"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "Buscar localização",
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"sandboxModeWarning": "无法将生产节点的编辑提交到沙盒。在设置中切换到生产模式以编辑节点。",
|
||||
"enableSubmittableProfile": "在设置中启用可提交的配置文件以编辑节点。",
|
||||
"profileViewOnlyWarning": "此配置文件仅用于地图查看。请选择可提交的配置文件来编辑节点。",
|
||||
"cannotMoveConstrainedNode": "无法移动此相机 - 它连接到街道或区域(OSM way/relation)。您仍可以编辑其标签和方向。",
|
||||
"refineTags": "细化标签",
|
||||
"refineTagsWithProfile": "细化标签({})"
|
||||
},
|
||||
@@ -325,7 +326,15 @@
|
||||
},
|
||||
"networkStatus": {
|
||||
"showIndicator": "显示网络状态指示器",
|
||||
"showIndicatorSubtitle": "在地图上显示网络加载和错误状态"
|
||||
"showIndicatorSubtitle": "在地图上显示网络加载和错误状态",
|
||||
"loading": "加载中...",
|
||||
"timedOut": "超时",
|
||||
"noData": "这里没有瓦片",
|
||||
"success": "完成",
|
||||
"nodeLimitReached": "显示限制 - 在设置中增加",
|
||||
"tileProviderSlow": "瓦片提供商缓慢",
|
||||
"nodeDataSlow": "节点数据缓慢",
|
||||
"networkIssues": "网络问题"
|
||||
},
|
||||
"navigation": {
|
||||
"searchLocation": "搜索位置",
|
||||
|
||||
@@ -4,11 +4,13 @@ class OsmNode {
|
||||
final int id;
|
||||
final LatLng coord;
|
||||
final Map<String, String> tags;
|
||||
final bool isConstrained; // true if part of any way/relation
|
||||
|
||||
OsmNode({
|
||||
required this.id,
|
||||
required this.coord,
|
||||
required this.tags,
|
||||
this.isConstrained = false, // Default to unconstrained for backward compatibility
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
@@ -16,6 +18,7 @@ class OsmNode {
|
||||
'lat': coord.latitude,
|
||||
'lon': coord.longitude,
|
||||
'tags': tags,
|
||||
'isConstrained': isConstrained,
|
||||
};
|
||||
|
||||
factory OsmNode.fromJson(Map<String, dynamic> json) {
|
||||
@@ -29,6 +32,7 @@ class OsmNode {
|
||||
id: json['id'] is int ? json['id'] as int : int.tryParse(json['id'].toString()) ?? 0,
|
||||
coord: LatLng((json['lat'] as num).toDouble(), (json['lon'] as num).toDouble()),
|
||||
tags: tags,
|
||||
isConstrained: json['isConstrained'] as bool? ?? false, // Default to false for backward compatibility
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,44 +47,7 @@ Future<List<OsmNode>> fetchOsmApiNodes({
|
||||
|
||||
// Parse XML response
|
||||
final document = XmlDocument.parse(response.body);
|
||||
final nodes = <OsmNode>[];
|
||||
|
||||
// Find all node elements
|
||||
for (final nodeElement in document.findAllElements('node')) {
|
||||
final id = int.tryParse(nodeElement.getAttribute('id') ?? '');
|
||||
final latStr = nodeElement.getAttribute('lat');
|
||||
final lonStr = nodeElement.getAttribute('lon');
|
||||
|
||||
if (id == null || latStr == null || lonStr == null) continue;
|
||||
|
||||
final lat = double.tryParse(latStr);
|
||||
final lon = double.tryParse(lonStr);
|
||||
if (lat == null || lon == null) continue;
|
||||
|
||||
// Parse tags
|
||||
final tags = <String, String>{};
|
||||
for (final tagElement in nodeElement.findElements('tag')) {
|
||||
final key = tagElement.getAttribute('k');
|
||||
final value = tagElement.getAttribute('v');
|
||||
if (key != null && value != null) {
|
||||
tags[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this node matches any of our profiles
|
||||
if (_nodeMatchesProfiles(tags, profiles)) {
|
||||
nodes.add(OsmNode(
|
||||
id: id,
|
||||
coord: LatLng(lat, lon),
|
||||
tags: tags,
|
||||
));
|
||||
}
|
||||
|
||||
// Respect maxResults limit if set
|
||||
if (maxResults > 0 && nodes.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
final nodes = _parseOsmApiResponseWithConstraints(document, profiles, maxResults);
|
||||
|
||||
if (nodes.isNotEmpty) {
|
||||
debugPrint('[fetchOsmApiNodes] Retrieved ${nodes.length} matching surveillance nodes');
|
||||
@@ -107,6 +70,93 @@ Future<List<OsmNode>> fetchOsmApiNodes({
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse OSM API XML response to create OsmNode objects with constraint information.
|
||||
List<OsmNode> _parseOsmApiResponseWithConstraints(XmlDocument document, List<NodeProfile> profiles, int maxResults) {
|
||||
final surveillanceNodes = <int, Map<String, dynamic>>{}; // nodeId -> node data
|
||||
final constrainedNodeIds = <int>{};
|
||||
|
||||
// First pass: collect surveillance nodes
|
||||
for (final nodeElement in document.findAllElements('node')) {
|
||||
final id = int.tryParse(nodeElement.getAttribute('id') ?? '');
|
||||
final latStr = nodeElement.getAttribute('lat');
|
||||
final lonStr = nodeElement.getAttribute('lon');
|
||||
|
||||
if (id == null || latStr == null || lonStr == null) continue;
|
||||
|
||||
final lat = double.tryParse(latStr);
|
||||
final lon = double.tryParse(lonStr);
|
||||
if (lat == null || lon == null) continue;
|
||||
|
||||
// Parse tags
|
||||
final tags = <String, String>{};
|
||||
for (final tagElement in nodeElement.findElements('tag')) {
|
||||
final key = tagElement.getAttribute('k');
|
||||
final value = tagElement.getAttribute('v');
|
||||
if (key != null && value != null) {
|
||||
tags[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this node matches any of our profiles
|
||||
if (_nodeMatchesProfiles(tags, profiles)) {
|
||||
surveillanceNodes[id] = {
|
||||
'id': id,
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'tags': tags,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: identify constrained nodes from ways
|
||||
for (final wayElement in document.findAllElements('way')) {
|
||||
for (final ndElement in wayElement.findElements('nd')) {
|
||||
final ref = int.tryParse(ndElement.getAttribute('ref') ?? '');
|
||||
if (ref != null && surveillanceNodes.containsKey(ref)) {
|
||||
constrainedNodeIds.add(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Third pass: identify constrained nodes from relations
|
||||
for (final relationElement in document.findAllElements('relation')) {
|
||||
for (final memberElement in relationElement.findElements('member')) {
|
||||
if (memberElement.getAttribute('type') == 'node') {
|
||||
final ref = int.tryParse(memberElement.getAttribute('ref') ?? '');
|
||||
if (ref != null && surveillanceNodes.containsKey(ref)) {
|
||||
constrainedNodeIds.add(ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create OsmNode objects with constraint information
|
||||
final nodes = <OsmNode>[];
|
||||
for (final nodeData in surveillanceNodes.values) {
|
||||
final nodeId = nodeData['id'] as int;
|
||||
final isConstrained = constrainedNodeIds.contains(nodeId);
|
||||
|
||||
nodes.add(OsmNode(
|
||||
id: nodeId,
|
||||
coord: LatLng(nodeData['lat'], nodeData['lon']),
|
||||
tags: nodeData['tags'] as Map<String, String>,
|
||||
isConstrained: isConstrained,
|
||||
));
|
||||
|
||||
// Respect maxResults limit if set
|
||||
if (maxResults > 0 && nodes.length >= maxResults) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final constrainedCount = nodes.where((n) => n.isConstrained).length;
|
||||
if (constrainedCount > 0) {
|
||||
debugPrint('[fetchOsmApiNodes] Found $constrainedCount constrained nodes out of ${nodes.length} total');
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/// Check if a node's tags match any of the given profiles
|
||||
bool _nodeMatchesProfiles(Map<String, String> nodeTags, List<NodeProfile> profiles) {
|
||||
for (final profile in profiles) {
|
||||
|
||||
@@ -154,18 +154,13 @@ Future<List<OsmNode>> _fetchSingleOverpassQuery({
|
||||
final elements = data['elements'] as List<dynamic>;
|
||||
|
||||
if (elements.length > 20) {
|
||||
debugPrint('[fetchOverpassNodes] Retrieved ${elements.length} surveillance nodes');
|
||||
debugPrint('[fetchOverpassNodes] Retrieved ${elements.length} elements (nodes + ways/relations)');
|
||||
}
|
||||
|
||||
NetworkStatus.instance.reportOverpassSuccess();
|
||||
|
||||
final nodes = elements.whereType<Map<String, dynamic>>().map((element) {
|
||||
return OsmNode(
|
||||
id: element['id'],
|
||||
coord: LatLng(element['lat'], element['lon']),
|
||||
tags: Map<String, String>.from(element['tags'] ?? {}),
|
||||
);
|
||||
}).toList();
|
||||
// Parse response to determine which nodes are constrained
|
||||
final nodes = _parseOverpassResponseWithConstraints(elements);
|
||||
|
||||
// Clean up any pending uploads that now appear in Overpass results
|
||||
_cleanupCompletedUploads(nodes);
|
||||
@@ -190,6 +185,7 @@ Future<List<OsmNode>> _fetchSingleOverpassQuery({
|
||||
}
|
||||
|
||||
/// Builds an Overpass API query for surveillance nodes matching the given profiles within bounds.
|
||||
/// Also fetches ways and relations that reference these nodes to determine constraint status.
|
||||
String _buildOverpassQuery(LatLngBounds bounds, List<NodeProfile> profiles, int maxResults) {
|
||||
// Build node clauses for each profile
|
||||
final nodeClauses = profiles.map((profile) {
|
||||
@@ -200,17 +196,19 @@ String _buildOverpassQuery(LatLngBounds bounds, List<NodeProfile> profiles, int
|
||||
|
||||
// Build the node query with tag filters and bounding box
|
||||
return 'node$tagFilters(${bounds.southWest.latitude},${bounds.southWest.longitude},${bounds.northEast.latitude},${bounds.northEast.longitude});';
|
||||
}).join('\n ');
|
||||
}).join('\n ');
|
||||
|
||||
// Use unlimited output if maxResults is 0
|
||||
final outputClause = maxResults > 0 ? 'out body $maxResults;' : 'out body;';
|
||||
|
||||
return '''
|
||||
[out:json][timeout:25];
|
||||
(
|
||||
$nodeClauses
|
||||
);
|
||||
$outputClause
|
||||
out body ${maxResults > 0 ? maxResults : ''};
|
||||
(
|
||||
way(bn);
|
||||
rel(bn);
|
||||
);
|
||||
out meta;
|
||||
''';
|
||||
}
|
||||
|
||||
@@ -243,6 +241,56 @@ List<LatLngBounds> _splitBounds(LatLngBounds bounds) {
|
||||
];
|
||||
}
|
||||
|
||||
/// Parse Overpass response elements to create OsmNode objects with constraint information.
|
||||
List<OsmNode> _parseOverpassResponseWithConstraints(List<dynamic> elements) {
|
||||
final nodeElements = <Map<String, dynamic>>[];
|
||||
final constrainedNodeIds = <int>{};
|
||||
|
||||
// First pass: collect surveillance nodes and identify constrained nodes
|
||||
for (final element in elements.whereType<Map<String, dynamic>>()) {
|
||||
final type = element['type'] as String?;
|
||||
|
||||
if (type == 'node') {
|
||||
// This is a surveillance node - collect it
|
||||
nodeElements.add(element);
|
||||
} else if (type == 'way' || type == 'relation') {
|
||||
// This is a way/relation that references some of our nodes
|
||||
final refs = element['nodes'] as List<dynamic>? ??
|
||||
element['members']?.where((m) => m['type'] == 'node').map((m) => m['ref']) ?? [];
|
||||
|
||||
// Mark all referenced nodes as constrained
|
||||
for (final ref in refs) {
|
||||
if (ref is int) {
|
||||
constrainedNodeIds.add(ref);
|
||||
} else if (ref is String) {
|
||||
final nodeId = int.tryParse(ref);
|
||||
if (nodeId != null) constrainedNodeIds.add(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: create OsmNode objects with constraint info
|
||||
final nodes = nodeElements.map((element) {
|
||||
final nodeId = element['id'] as int;
|
||||
final isConstrained = constrainedNodeIds.contains(nodeId);
|
||||
|
||||
return OsmNode(
|
||||
id: nodeId,
|
||||
coord: LatLng(element['lat'], element['lon']),
|
||||
tags: Map<String, String>.from(element['tags'] ?? {}),
|
||||
isConstrained: isConstrained,
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final constrainedCount = nodes.where((n) => n.isConstrained).length;
|
||||
if (constrainedCount > 0) {
|
||||
debugPrint('[fetchOverpassNodes] Found $constrainedCount constrained nodes out of ${nodes.length} total');
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/// Clean up pending uploads that now appear in Overpass results
|
||||
void _cleanupCompletedUploads(List<OsmNode> overpassNodes) {
|
||||
try {
|
||||
|
||||
@@ -27,6 +27,7 @@ class NodeCache {
|
||||
id: node.id,
|
||||
coord: node.coord,
|
||||
tags: mergedTags,
|
||||
isConstrained: node.isConstrained, // Preserve constraint information
|
||||
);
|
||||
} else {
|
||||
_nodes[node.id] = node;
|
||||
@@ -58,6 +59,7 @@ class NodeCache {
|
||||
id: node.id,
|
||||
coord: node.coord,
|
||||
tags: cleanTags,
|
||||
isConstrained: node.isConstrained, // Preserve constraint information
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,7 @@ class ChangelogDialog extends StatelessWidget {
|
||||
});
|
||||
|
||||
void _onClose(BuildContext context) async {
|
||||
// Update version tracking when closing changelog dialog
|
||||
await ChangelogService().updateLastSeenVersion();
|
||||
// Note: Version tracking is updated by completeVersionChange() after all dialogs
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
@@ -210,6 +210,24 @@ class EditNodeSheet extends StatelessWidget {
|
||||
// Direction controls
|
||||
_buildDirectionControls(context, appState, session, locService),
|
||||
|
||||
// Constraint message for nodes that cannot be moved
|
||||
if (session.originalNode.isConstrained)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.info_outline, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
locService.t('editNode.cannotMoveConstrainedNode'),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (!kEnableNodeEdits)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../models/osm_node.dart';
|
||||
import '../models/node_profile.dart';
|
||||
import '../models/suspected_location.dart';
|
||||
import '../models/tile_provider.dart';
|
||||
import '../state/session_state.dart';
|
||||
import 'debouncer.dart';
|
||||
import 'camera_provider_with_cache.dart';
|
||||
import 'camera_icon.dart';
|
||||
@@ -260,6 +261,28 @@ class MapViewState extends State<MapView> {
|
||||
return latDiff > significantMovementThreshold || lngDiff > significantMovementThreshold;
|
||||
}
|
||||
|
||||
/// Get interaction options for the map based on whether we're editing a constrained node.
|
||||
/// Allows zoom and rotation but disables panning/dragging for constrained nodes.
|
||||
InteractionOptions _getInteractionOptions(EditNodeSession? editSession) {
|
||||
// Check if we're editing a constrained node
|
||||
if (editSession?.originalNode.isConstrained == true) {
|
||||
// Constrained node: disable dragging/panning but keep zoom, rotate, etc.
|
||||
return const InteractionOptions(
|
||||
flags: InteractiveFlag.all & ~InteractiveFlag.drag,
|
||||
scrollWheelVelocity: kScrollWheelVelocity,
|
||||
pinchZoomThreshold: kPinchZoomThreshold,
|
||||
pinchMoveThreshold: kPinchMoveThreshold,
|
||||
);
|
||||
}
|
||||
|
||||
// Normal case: all interactions allowed
|
||||
return const InteractionOptions(
|
||||
scrollWheelVelocity: kScrollWheelVelocity,
|
||||
pinchZoomThreshold: kPinchZoomThreshold,
|
||||
pinchMoveThreshold: kPinchMoveThreshold,
|
||||
);
|
||||
}
|
||||
|
||||
/// Show zoom warning if user is below minimum zoom level
|
||||
void _showZoomWarningIfNeeded(BuildContext context, double currentZoom, int minZoom) {
|
||||
// Only show warning once per zoom level to avoid spam
|
||||
@@ -543,11 +566,7 @@ class MapViewState extends State<MapView> {
|
||||
initialCenter: _gpsController.currentLocation ?? _positionManager.initialLocation ?? LatLng(37.7749, -122.4194),
|
||||
initialZoom: _positionManager.initialZoom ?? 15,
|
||||
maxZoom: (appState.selectedTileType?.maxZoom ?? 18).toDouble(),
|
||||
interactionOptions: const InteractionOptions(
|
||||
scrollWheelVelocity: kScrollWheelVelocity,
|
||||
pinchZoomThreshold: kPinchZoomThreshold,
|
||||
pinchMoveThreshold: kPinchMoveThreshold,
|
||||
),
|
||||
interactionOptions: _getInteractionOptions(editSession),
|
||||
onPositionChanged: (pos, gesture) {
|
||||
setState(() {}); // Instant UI update for zoom, etc.
|
||||
if (gesture) {
|
||||
|
||||
@@ -1,109 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../services/network_status.dart';
|
||||
import '../services/localization_service.dart';
|
||||
|
||||
class NetworkStatusIndicator extends StatelessWidget {
|
||||
const NetworkStatusIndicator({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: NetworkStatus.instance,
|
||||
child: Consumer<NetworkStatus>(
|
||||
builder: (context, networkStatus, child) {
|
||||
String message;
|
||||
IconData icon;
|
||||
Color color;
|
||||
return AnimatedBuilder(
|
||||
animation: LocalizationService.instance,
|
||||
builder: (context, child) => ChangeNotifierProvider.value(
|
||||
value: NetworkStatus.instance,
|
||||
child: Consumer<NetworkStatus>(
|
||||
builder: (context, networkStatus, child) {
|
||||
final locService = LocalizationService.instance;
|
||||
String message;
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
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 NetworkStatusType.noData:
|
||||
message = 'No tiles here';
|
||||
icon = Icons.cloud_off;
|
||||
color = Colors.grey;
|
||||
break;
|
||||
switch (networkStatus.currentStatus) {
|
||||
case NetworkStatusType.waiting:
|
||||
message = locService.t('networkStatus.loading');
|
||||
icon = Icons.hourglass_empty;
|
||||
color = Colors.blue;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.timedOut:
|
||||
message = locService.t('networkStatus.timedOut');
|
||||
icon = Icons.hourglass_disabled;
|
||||
color = Colors.orange;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.noData:
|
||||
message = locService.t('networkStatus.noData');
|
||||
icon = Icons.cloud_off;
|
||||
color = Colors.grey;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.success:
|
||||
message = 'Done';
|
||||
icon = Icons.check_circle;
|
||||
color = Colors.green;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.nodeLimitReached:
|
||||
message = 'Showing limit - increase in settings';
|
||||
icon = Icons.visibility_off;
|
||||
color = Colors.amber;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.issues:
|
||||
switch (networkStatus.currentIssueType) {
|
||||
case NetworkIssueType.osmTiles:
|
||||
message = 'Tile provider 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 NetworkStatusType.ready:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
case NetworkStatusType.success:
|
||||
message = locService.t('networkStatus.success');
|
||||
icon = Icons.check_circle;
|
||||
color = Colors.green;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.nodeLimitReached:
|
||||
message = locService.t('networkStatus.nodeLimitReached');
|
||||
icon = Icons.visibility_off;
|
||||
color = Colors.amber;
|
||||
break;
|
||||
|
||||
case NetworkStatusType.issues:
|
||||
switch (networkStatus.currentIssueType) {
|
||||
case NetworkIssueType.osmTiles:
|
||||
message = locService.t('networkStatus.tileProviderSlow');
|
||||
icon = Icons.map_outlined;
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case NetworkIssueType.overpassApi:
|
||||
message = locService.t('networkStatus.nodeDataSlow');
|
||||
icon = Icons.camera_alt_outlined;
|
||||
color = Colors.orange;
|
||||
break;
|
||||
case NetworkIssueType.both:
|
||||
message = locService.t('networkStatus.networkIssues');
|
||||
icon = Icons.cloud_off_outlined;
|
||||
color = Colors.red;
|
||||
break;
|
||||
default:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
break;
|
||||
|
||||
case NetworkStatusType.ready:
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Positioned(
|
||||
top: 8, // Position relative to the map area (not the screen)
|
||||
left: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
return Positioned(
|
||||
top: 8, // Position relative to the map area (not the screen)
|
||||
left: 8,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black87,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color, width: 1),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
message,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ class _WelcomeDialogState extends State<WelcomeDialog> {
|
||||
await ChangelogService().markWelcomeSeen();
|
||||
}
|
||||
|
||||
// Always update version tracking when closing welcome dialog
|
||||
await ChangelogService().updateLastSeenVersion();
|
||||
// Note: Version tracking is updated by completeVersionChange() after all dialogs
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
|
||||
Reference in New Issue
Block a user