modules/actions/connect.js (189 lines of code) (raw):

import { utilArrayUniq } from '@id-sdk/util'; import { actionDeleteNode } from './delete_node'; import { actionDeleteWay } from './delete_way'; // Connect the ways at the given nodes. // // First choose a node to be the survivor, with preference given // to an existing (not new) node. // // Tags and relation memberships of of non-surviving nodes are merged // to the survivor. // // This is the inverse of `iD.actionDisconnect`. // // Reference: // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeNodesAction.as // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/MergeNodesAction.java // export function actionConnect(nodeIDs) { var action = function(graph) { var survivor; var node; var parents; var i, j; // Choose a survivor node, prefer an existing (not new) node - #4974 for (i = 0; i < nodeIDs.length; i++) { survivor = graph.entity(nodeIDs[i]); if (survivor.version) break; // found one } // Replace all non-surviving nodes with the survivor and merge tags. for (i = 0; i < nodeIDs.length; i++) { node = graph.entity(nodeIDs[i]); if (node.id === survivor.id) continue; parents = graph.parentWays(node); for (j = 0; j < parents.length; j++) { graph = graph.replace(parents[j].replaceNode(node.id, survivor.id)); } parents = graph.parentRelations(node); for (j = 0; j < parents.length; j++) { graph = graph.replace(parents[j].replaceMember(node, survivor)); } survivor = survivor.mergeTags(node.tags); graph = actionDeleteNode(node.id)(graph); } graph = graph.replace(survivor); // find and delete any degenerate ways created by connecting adjacent vertices parents = graph.parentWays(survivor); for (i = 0; i < parents.length; i++) { if (parents[i].isDegenerate()) { graph = actionDeleteWay(parents[i].id)(graph); } } return graph; }; action.disabled = function(graph) { var seen = {}; var restrictionIDs = []; var survivor; var node, way; var relations, relation, role; var i, j, k; // Choose a survivor node, prefer an existing (not new) node - #4974 for (i = 0; i < nodeIDs.length; i++) { survivor = graph.entity(nodeIDs[i]); if (survivor.version) break; // found one } // 1. disable if the nodes being connected have conflicting relation roles for (i = 0; i < nodeIDs.length; i++) { node = graph.entity(nodeIDs[i]); relations = graph.parentRelations(node); for (j = 0; j < relations.length; j++) { relation = relations[j]; role = relation.memberById(node.id).role || ''; // if this node is a via node in a restriction, remember for later if (relation.hasFromViaTo()) { restrictionIDs.push(relation.id); } if (seen[relation.id] !== undefined && seen[relation.id] !== role) { return 'relation'; } else { seen[relation.id] = role; } } } // gather restrictions for parent ways for (i = 0; i < nodeIDs.length; i++) { node = graph.entity(nodeIDs[i]); var parents = graph.parentWays(node); for (j = 0; j < parents.length; j++) { var parent = parents[j]; relations = graph.parentRelations(parent); for (k = 0; k < relations.length; k++) { relation = relations[k]; if (relation.hasFromViaTo()) { restrictionIDs.push(relation.id); } } } } // test restrictions restrictionIDs = utilArrayUniq(restrictionIDs); for (i = 0; i < restrictionIDs.length; i++) { relation = graph.entity(restrictionIDs[i]); if (!relation.isComplete(graph)) continue; var memberWays = relation.members .filter(function(m) { return m.type === 'way'; }) .map(function(m) { return graph.entity(m.id); }); memberWays = utilArrayUniq(memberWays); var f = relation.memberByRole('from'); var t = relation.memberByRole('to'); var isUturn = (f.id === t.id); // 2a. disable if connection would damage a restriction // (a key node is a node at the junction of ways) var nodes = { from: [], via: [], to: [], keyfrom: [], keyto: [] }; for (j = 0; j < relation.members.length; j++) { collectNodes(relation.members[j], nodes); } nodes.keyfrom = utilArrayUniq(nodes.keyfrom.filter(hasDuplicates)); nodes.keyto = utilArrayUniq(nodes.keyto.filter(hasDuplicates)); var filter = keyNodeFilter(nodes.keyfrom, nodes.keyto); nodes.from = nodes.from.filter(filter); nodes.via = nodes.via.filter(filter); nodes.to = nodes.to.filter(filter); var connectFrom = false; var connectVia = false; var connectTo = false; var connectKeyFrom = false; var connectKeyTo = false; for (j = 0; j < nodeIDs.length; j++) { var n = nodeIDs[j]; if (nodes.from.indexOf(n) !== -1) { connectFrom = true; } if (nodes.via.indexOf(n) !== -1) { connectVia = true; } if (nodes.to.indexOf(n) !== -1) { connectTo = true; } if (nodes.keyfrom.indexOf(n) !== -1) { connectKeyFrom = true; } if (nodes.keyto.indexOf(n) !== -1) { connectKeyTo = true; } } if (connectFrom && connectTo && !isUturn) { return 'restriction'; } if (connectFrom && connectVia) { return 'restriction'; } if (connectTo && connectVia) { return 'restriction'; } // connecting to a key node - // if both nodes are on a member way (i.e. part of the turn restriction), // the connecting node must be adjacent to the key node. if (connectKeyFrom || connectKeyTo) { if (nodeIDs.length !== 2) { return 'restriction'; } var n0 = null; var n1 = null; for (j = 0; j < memberWays.length; j++) { way = memberWays[j]; if (way.contains(nodeIDs[0])) { n0 = nodeIDs[0]; } if (way.contains(nodeIDs[1])) { n1 = nodeIDs[1]; } } if (n0 && n1) { // both nodes are part of the restriction var ok = false; for (j = 0; j < memberWays.length; j++) { way = memberWays[j]; if (way.areAdjacent(n0, n1)) { ok = true; break; } } if (!ok) { return 'restriction'; } } } // 2b. disable if nodes being connected will destroy a member way in a restriction // (to test, make a copy and try actually connecting the nodes) for (j = 0; j < memberWays.length; j++) { way = memberWays[j].update({}); // make copy for (k = 0; k < nodeIDs.length; k++) { if (nodeIDs[k] === survivor.id) continue; if (way.areAdjacent(nodeIDs[k], survivor.id)) { way = way.removeNode(nodeIDs[k]); } else { way = way.replaceNode(nodeIDs[k], survivor.id); } } if (way.isDegenerate()) { return 'restriction'; } } } return false; // if a key node appears multiple times (indexOf !== lastIndexOf) it's a FROM-VIA or TO-VIA junction function hasDuplicates(n, i, arr) { return arr.indexOf(n) !== arr.lastIndexOf(n); } function keyNodeFilter(froms, tos) { return function(n) { return froms.indexOf(n) === -1 && tos.indexOf(n) === -1; }; } function collectNodes(member, collection) { var entity = graph.hasEntity(member.id); if (!entity) return; var role = member.role || ''; if (!collection[role]) { collection[role] = []; } if (member.type === 'node') { collection[role].push(member.id); if (role === 'via') { collection.keyfrom.push(member.id); collection.keyto.push(member.id); } } else if (member.type === 'way') { collection[role].push.apply(collection[role], entity.nodes); if (role === 'from' || role === 'via') { collection.keyfrom.push(entity.first()); collection.keyfrom.push(entity.last()); } if (role === 'to' || role === 'via') { collection.keyto.push(entity.first()); collection.keyto.push(entity.last()); } } } }; return action; }