Slide 50
Slide 50 text
// 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
//
iD.actions.Split = function(nodeId, wayIds, newWayIds) {
function split(graph, wayA, newWayId) {
var wayB = iD.Way({id: newWayId, tags: wayA.tags}),
nodesA,
nodesB,
isArea = wayA.isArea();
if (wayA.isClosed()) {
var nodes = wayA.nodes.slice(0, -1),
idxA = _.indexOf(nodes, nodeId),
idxB = idxA + Math.floor(nodes.length / 2);
if (idxB >= nodes.length) {
idxB %= nodes.length;
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 {
var role = relation.memberById(wayA.id).role,
last = wayB.last(),
i = relation.memberById(wayA.id).index,
j;
for (j = 0; j < relation.members.length; j++) {
var entity = graph.entity(relation.members[j].id);
if (entity && entity.type === 'way' && entity.contains(last)) {
break;
}
}
relation = relation.addMember({id: wayB.id, type: 'way', role: role}, i <= j ? i + 1 : i);
graph = graph.replace(relation);
}
});
if (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);
return parents.filter(function(parent) {
if (wayIds && wayIds.length && wayIds.indexOf(parent.id) === -1)
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 && wayIds.length !== candidates.length))
return 'not_eligible';
};
return action;
};
describe("iD.actions.Split", function () {
describe("#disabled", function () {
it("returns falsy for a non-end node of a single way", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']})
});
expect(iD.actions.Split('b').disabled(graph)).not.to.be.ok;
});
it("returns falsy for an intersection of two ways", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'*': iD.Node({id: '*'}),
'-': iD.Way({id: '-', nodes: ['a', '*', 'b']}),
'|': iD.Way({id: '|', nodes: ['c', '*', 'd']})
});
expect(iD.actions.Split('*').disabled(graph)).not.to.be.ok;
});
it("returns falsy for an intersection of two ways with parent way specified", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'*': iD.Node({id: '*'}),
'-': iD.Way({id: '-', nodes: ['a', '*', 'b']}),
'|': iD.Way({id: '|', nodes: ['c', '*', 'd']})
});
expect(iD.actions.Split('*', ['-']).disabled(graph)).not.to.be.ok;
});
it("returns falsy for a self-intersection", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']})
});
expect(iD.actions.Split('a').disabled(graph)).not.to.be.ok;
});
it("returns 'not_eligible' for the first node of a single way", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'-': iD.Way({id: '-', nodes: ['a', 'b']})
});
expect(iD.actions.Split('a').disabled(graph)).to.equal('not_eligible');
});
it("returns 'not_eligible' for the last node of a single way", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'-': iD.Way({id: '-', nodes: ['a', 'b']})
});
expect(iD.actions.Split('b').disabled(graph)).to.equal('not_eligible');
});
it("returns 'not_eligible' for an intersection of two ways with non-parent way specified", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'*': iD.Node({id: '*'}),
'-': iD.Way({id: '-', nodes: ['a', '*', 'b']}),
'|': iD.Way({id: '|', nodes: ['c', '*', 'd']})
});
expect(iD.actions.Split('*', ['-', '=']).disabled(graph)).to.equal('not_eligible');
});
});
it("creates a new way with the appropriate nodes", function () {
// Situation:
// a ---- b ---- c
//
// Split at b.
//
// Expected result:
// a ---- b ==== c
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(graph.entity('-').nodes).to.eql(['a', 'b']);
expect(graph.entity('=').nodes).to.eql(['b', 'c']);
});
it("copies tags to the new way", function () {
var tags = {highway: 'residential'},
graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c'], tags: tags})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
// Immutable tags => should be shared by identity.
expect(graph.entity('-').tags).to.equal(tags);
expect(graph.entity('=').tags).to.equal(tags);
});
it("splits a way at a T-junction", function () {
// Situation:
// a ---- b ---- c
// |
// d
//
// Split at b.
//
// Expected result:
// a ---- b ==== c
// |
// d
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'|': iD.Way({id: '|', nodes: ['d', 'b']})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(graph.entity('-').nodes).to.eql(['a', 'b']);
expect(graph.entity('=').nodes).to.eql(['b', 'c']);
expect(graph.entity('|').nodes).to.eql(['d', 'b']);
});
it("splits multiple ways at an intersection", function () {
// Situation:
// c
// |
// a ---- * ---- b
// ¦
// d
//
// Split at b.
//
// Expected result:
// c
// |
// a ---- * ==== b
// ¦
// d
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'*': iD.Node({id: '*'}),
'-': iD.Way({id: '-', nodes: ['a', '*', 'b']}),
'|': iD.Way({id: '|', nodes: ['c', '*', 'd']})
});
graph = iD.actions.Split('*', undefined, ['=', '¦'])(graph);
expect(graph.entity('-').nodes).to.eql(['a', '*']);
expect(graph.entity('=').nodes).to.eql(['*', 'b']);
expect(graph.entity('|').nodes).to.eql(['c', '*']);
expect(graph.entity('¦').nodes).to.eql(['*', 'd']);
});
it("splits the specified ways at an intersection", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'*': iD.Node({id: '*'}),
'-': iD.Way({id: '-', nodes: ['a', '*', 'b']}),
'|': iD.Way({id: '|', nodes: ['c', '*', 'd']})
});
var g1 = iD.actions.Split('*', ['-'], ['='])(graph);
expect(g1.entity('-').nodes).to.eql(['a', '*']);
expect(g1.entity('=').nodes).to.eql(['*', 'b']);
expect(g1.entity('|').nodes).to.eql(['c', '*', 'd']);
var g2 = iD.actions.Split('*', ['|'], ['¦'])(graph);
expect(g2.entity('-').nodes).to.eql(['a', '*', 'b']);
expect(g2.entity('|').nodes).to.eql(['c', '*']);
expect(g2.entity('¦').nodes).to.eql(['*', 'd']);
var g3 = iD.actions.Split('*', ['-', '|'], ['=', '¦'])(graph);
expect(g3.entity('-').nodes).to.eql(['a', '*']);
expect(g3.entity('=').nodes).to.eql(['*', 'b']);
expect(g3.entity('|').nodes).to.eql(['c', '*']);
expect(g3.entity('¦').nodes).to.eql(['*', 'd']);
});
it("splits self-intersecting ways", function () {
// Situation:
// b
// / |
// / |
// c - a -- d
//
// Split at a.
//
// Expected result:
// b
// / |
// / |
// c - a == d
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']})
});
graph = iD.actions.Split('a', undefined, ['='])(graph);
expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'a']);
expect(graph.entity('=').nodes).to.eql(['a', 'd']);
});
it("splits a closed way at the given point and its antipode", function () {
// Situation:
// a ---- b
// | |
// d ---- c
//
// Split at a.
//
// Expected result:
// a ---- b
// || |
// d ==== c
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']})
});
var g1 = iD.actions.Split('a', undefined, ['='])(graph);
expect(g1.entity('-').nodes).to.eql(['a', 'b', 'c']);
expect(g1.entity('=').nodes).to.eql(['c', 'd', 'a']);
var g2 = iD.actions.Split('b', undefined, ['='])(graph);
expect(g2.entity('-').nodes).to.eql(['b', 'c', 'd']);
expect(g2.entity('=').nodes).to.eql(['d', 'a', 'b']);
var g3 = iD.actions.Split('c', undefined, ['='])(graph);
expect(g3.entity('-').nodes).to.eql(['c', 'd', 'a']);
expect(g3.entity('=').nodes).to.eql(['a', 'b', 'c']);
var g4 = iD.actions.Split('d', undefined, ['='])(graph);
expect(g4.entity('-').nodes).to.eql(['d', 'a', 'b']);
expect(g4.entity('=').nodes).to.eql(['b', 'c', 'd']);
});
it("splits an area by converting it to a multipolygon", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']})
});
graph = iD.actions.Split('a', undefined, ['='])(graph);
expect(graph.entity('-').tags).to.eql({});
expect(graph.entity('=').tags).to.eql({});
expect(graph.parentRelations(graph.entity('-'))).to.have.length(1);
var relation = graph.parentRelations(graph.entity('-'))[0];
expect(relation.tags).to.eql({type: 'multipolygon', building: 'yes'});
expect(relation.members).to.eql([
{id: '-', role: 'outer', type: 'way'},
{id: '=', role: 'outer', type: 'way'}
]);
});
it("adds the new way to parent relations (no connections)", function () {
// Situation:
// a ---- b ---- c
// Relation: [----]
//
// Split at b.
//
// Expected result:
// a ---- b ==== c
// Relation: [----, ====]
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(graph.entity('r').members).to.eql([
{id: '-', type: 'way', role: 'forward'},
{id: '=', type: 'way', role: 'forward'}
]);
});
it("adds the new way to parent relations (forward order)", function () {
// Situation:
// a ---- b ---- c ~~~~ d
// Relation: [----, ~~~~]
//
// Split at b.
//
// Expected result:
// a ---- b ==== c ~~~~ d
// Relation: [----, ====, ~~~~]
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'~': iD.Way({id: '~', nodes: ['c', 'd']}),
'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=', '~']);
});
it("adds the new way to parent relations (reverse order)", function () {
// Situation:
// a ---- b ---- c ~~~~ d
// Relation: [~~~~, ----]
//
// Split at b.
//
// Expected result:
// a ---- b ==== c ~~~~ d
// Relation: [~~~~, ====, ----]
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'~': iD.Way({id: '~', nodes: ['c', 'd']}),
'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '=', '-']);
});
it("handles incomplete relations", function () {
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '-', '=']);
});
['restriction', 'restriction:bus'].forEach(function (type) {
it("updates a restriction's 'from' role", function () {
// Situation:
// a ----> b ----> c ~~~~ d
// A restriction from ---- to ~~~~ via c.
//
// Split at b.
//
// Expected result:
// a ----> b ====> c ~~~~ d
// A restriction from ==== to ~~~~ via c.
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'~': iD.Way({id: '~', nodes: ['c', 'd']}),
'r': iD.Relation({id: 'r', tags: {type: type}, members: [
{id: '-', role: 'from'},
{id: '~', role: 'to'},
{id: 'c', role: 'via'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(graph.entity('r').members).to.eql([
{id: '=', role: 'from'},
{id: '~', role: 'to'},
{id: 'c', role: 'via'}]);
});
it("updates a restriction's 'to' role", function () {
// Situation:
// a ----> b ----> c ~~~~ d
// A restriction from ~~~~ to ---- via c.
//
// Split at b.
//
// Expected result:
// a ----> b ====> c ~~~~ d
// A restriction from ~~~~ to ==== via c.
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}),
'~': iD.Way({id: '~', nodes: ['c', 'd']}),
'r': iD.Relation({id: 'r', tags: {type: type}, members: [
{id: '~', role: 'from'},
{id: '-', role: 'to'},
{id: 'c', role: 'via'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(graph.entity('r').members).to.eql([
{id: '~', role: 'from'},
{id: '=', role: 'to'},
{id: 'c', role: 'via'}]);
});
it("leaves unaffected restrictions unchanged", function () {
// Situation:
// a <---- b <---- c ~~~~ d
// A restriction from ---- to ~~~~ via c.
//
// Split at b.
//
// Expected result:
// a <==== b <---- c ~~~~ d
// A restriction from ---- to ~~~~ via c.
//
var graph = iD.Graph({
'a': iD.Node({id: 'a'}),
'b': iD.Node({id: 'b'}),
'c': iD.Node({id: 'c'}),
'd': iD.Node({id: 'd'}),
'-': iD.Way({id: '-', nodes: ['c', 'b', 'a']}),
'~': iD.Way({id: '~', nodes: ['c', 'd']}),
'r': iD.Relation({id: 'r', tags: {type: type}, members: [
{id: '-', role: 'from'},
{id: '~', role: 'to'},
{id: 'c', role: 'via'}]})
});
graph = iD.actions.Split('b', undefined, ['='])(graph);
expect(graph.entity('r').members).to.eql([
{id: '-', role: 'from'},
{id: '~', role: 'to'},
{id: 'c', role: 'via'}]);
});
});
});
Code Test