Disallow editing location of nodes attached to ways/relations

This commit is contained in:
stopflock
2025-11-16 00:17:53 -06:00
parent ac53f7f74e
commit 192c6e5158
17 changed files with 366 additions and 159 deletions

View File

@@ -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) {

View File

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

View File

@@ -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
);
}
}