mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
189 lines
6.0 KiB
JavaScript
189 lines
6.0 KiB
JavaScript
import { AddMember } from './add_member';
|
|
// 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 iD.util.wrap(index, nodes.length);
|
|
}
|
|
|
|
function dist(nA, nB) {
|
|
return iD.geo.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 = iD.Way({id: newWayId, tags: wayA.tags}),
|
|
nodesA,
|
|
nodesB,
|
|
isArea = wayA.isArea(),
|
|
isOuter = iD.geo.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 = iD.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;
|
|
}
|