import _ from 'lodash'; import { Relation, Way } from '../core/index'; import { isSimpleMultipolygonOuterMember, sphericalDistance } from '../geo/index'; import { AddMember } from './add_member'; import { wrap as Wrap } from '../util/index'; // Split a way at the given node. // // Optionally, split only the given ways, if multiple ways share // the given node. // // This is the inverse of `iD.actions.Join`. // // For testing convenience, accepts an ID to assign to the new way. // Normally, this will be undefined and the way will automatically // be assigned a new ID. // // Reference: // https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as // export function Split(nodeId, newWayIds) { var wayIds; // if the way is closed, we need to search for a partner node // to split the way at. // // The following looks for a node that is both far away from // the initial node in terms of way segment length and nearby // in terms of beeline-distance. This assures that areas get // split on the most "natural" points (independent of the number // of nodes). // For example: bone-shaped areas get split across their waist // line, circles across the diameter. function splitArea(nodes, idxA, graph) { var lengths = new Array(nodes.length), length, i, best = 0, idxB; function wrap(index) { return Wrap(index, nodes.length); } function dist(nA, nB) { return sphericalDistance(graph.entity(nA).loc, graph.entity(nB).loc); } // calculate lengths length = 0; for (i = wrap(idxA+1); i !== idxA; i = wrap(i+1)) { length += dist(nodes[i], nodes[wrap(i-1)]); lengths[i] = length; } length = 0; for (i = wrap(idxA-1); i !== idxA; i = wrap(i-1)) { length += dist(nodes[i], nodes[wrap(i+1)]); if (length < lengths[i]) lengths[i] = length; } // determine best opposite node to split for (i = 0; i < nodes.length; i++) { var cost = lengths[i] / dist(nodes[idxA], nodes[i]); if (cost > best) { idxB = i; best = cost; } } return idxB; } function split(graph, wayA, newWayId) { var wayB = Way({id: newWayId, tags: wayA.tags}), nodesA, nodesB, isArea = wayA.isArea(), isOuter = isSimpleMultipolygonOuterMember(wayA, graph); if (wayA.isClosed()) { var nodes = wayA.nodes.slice(0, -1), idxA = _.indexOf(nodes, nodeId), idxB = splitArea(nodes, idxA, graph); if (idxB < idxA) { nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1)); nodesB = nodes.slice(idxB, idxA + 1); } else { nodesA = nodes.slice(idxA, idxB + 1); nodesB = nodes.slice(idxB).concat(nodes.slice(0, idxA + 1)); } } else { var idx = _.indexOf(wayA.nodes, nodeId, 1); nodesA = wayA.nodes.slice(0, idx + 1); nodesB = wayA.nodes.slice(idx); } wayA = wayA.update({nodes: nodesA}); wayB = wayB.update({nodes: nodesB}); graph = graph.replace(wayA); graph = graph.replace(wayB); graph.parentRelations(wayA).forEach(function(relation) { if (relation.isRestriction()) { var via = relation.memberByRole('via'); if (via && wayB.contains(via.id)) { relation = relation.updateMember({id: wayB.id}, relation.memberById(wayA.id).index); graph = graph.replace(relation); } } else { if (relation === isOuter) { graph = graph.replace(relation.mergeTags(wayA.tags)); graph = graph.replace(wayA.update({tags: {}})); graph = graph.replace(wayB.update({tags: {}})); } var member = { id: wayB.id, type: 'way', role: relation.memberById(wayA.id).role }; graph = AddMember(relation.id, member)(graph); } }); if (!isOuter && isArea) { var multipolygon = Relation({ tags: _.extend({}, wayA.tags, {type: 'multipolygon'}), members: [ {id: wayA.id, role: 'outer', type: 'way'}, {id: wayB.id, role: 'outer', type: 'way'} ]}); graph = graph.replace(multipolygon); graph = graph.replace(wayA.update({tags: {}})); graph = graph.replace(wayB.update({tags: {}})); } return graph; } var action = function(graph) { var candidates = action.ways(graph); for (var i = 0; i < candidates.length; i++) { graph = split(graph, candidates[i], newWayIds && newWayIds[i]); } return graph; }; action.ways = function(graph) { var node = graph.entity(nodeId), parents = graph.parentWays(node), hasLines = _.some(parents, function(parent) { return parent.geometry(graph) === 'line'; }); return parents.filter(function(parent) { if (wayIds && wayIds.indexOf(parent.id) === -1) return false; if (!wayIds && hasLines && parent.geometry(graph) !== 'line') return false; if (parent.isClosed()) { return true; } for (var i = 1; i < parent.nodes.length - 1; i++) { if (parent.nodes[i] === nodeId) { return true; } } return false; }); }; action.disabled = function(graph) { var candidates = action.ways(graph); if (candidates.length === 0 || (wayIds && wayIds.length !== candidates.length)) return 'not_eligible'; }; action.limitWays = function(_) { if (!arguments.length) return wayIds; wayIds = _; return action; }; return action; }