fix splitting of (route) relation member ways

instead of fully re-sorting the whole relation every time a member is split, perform a local operation: This works under the assumption that the relation is already sorted properly. The new way is inserted into the relation before or after the existing member, depending on how the old/new way connect to their neighboring members.

for cases where two ways form a loop, a additional look-ahead is implemented to disambiguate the order
This commit is contained in:
Martin Raifer
2024-03-02 12:16:57 +01:00
parent cc920d3fba
commit 11dfbe804c
5 changed files with 261 additions and 224 deletions

View File

@@ -499,6 +499,7 @@ en:
one: "Split a feature."
other: "Split {n} features."
not_eligible: Lines can't be split at their beginning or end.
parent_incomplete: This line cannot be split because it is part of a larger relation which has only been partially loaded. Make sure that all ways connected to this way are present on the map.
connected_to_hidden: This can't be split because it is connected to a hidden feature.
restriction:
annotation:

View File

@@ -1,9 +1,8 @@
import { osmJoinWays } from '../osm/multipolygon';
import { osmWay } from '../osm/way';
import { utilArrayGroupBy, utilObjectOmit } from '../util';
export function actionAddMember(relationId, member, memberIndex, insertPair) {
export function actionAddMember(relationId, member, memberIndex) {
return function action(graph) {
var relation = graph.entity(relationId);
@@ -11,7 +10,7 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) {
// There are some special rules for Public Transport v2 routes.
var isPTv2 = /stop|platform/.test(member.role);
if ((isNaN(memberIndex) || insertPair) && member.type === 'way' && !isPTv2) {
if (member.type === 'way' && !isPTv2) {
// Try to perform sensible inserts based on how the ways join together
graph = addWayMember(relation, graph);
} else {
@@ -30,9 +29,8 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) {
// Add a way member into the relation "wherever it makes sense".
// In this situation we were not supplied a memberIndex.
function addWayMember(relation, graph) {
var groups, tempWay, insertPairIsReversed, item, i, j, k;
var groups, item, i, j, k;
// remove PTv2 stops and platforms before doing anything.
var PTv2members = [];
@@ -47,38 +45,10 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) {
}
relation = relation.update({ members: members });
if (insertPair) {
// We're adding a member that must stay paired with an existing member.
// (This feature is used by `actionSplit`)
//
// This is tricky because the members may exist multiple times in the
// member list, and with different A-B/B-A ordering and different roles.
// (e.g. a bus route that loops out and back - #4589).
//
// Replace the existing member with a temporary way,
// so that `osmJoinWays` can treat the pair like a single way.
tempWay = osmWay({ id: 'wTemp', nodes: insertPair.nodes });
graph = graph.replace(tempWay);
var tempMember = { id: tempWay.id, type: 'way', role: member.role };
var tempRelation = relation.replaceMember({id: insertPair.originalID}, tempMember, true);
groups = utilArrayGroupBy(tempRelation.members, 'type');
groups.way = groups.way || [];
// Insert pair is reversed if the inserted way comes before the original one.
// (Except when they form a loop.)
var originalWay = graph.entity(insertPair.originalID);
var insertedWay = graph.entity(insertPair.insertedID);
insertPairIsReversed = originalWay.nodes.length > 0 && insertedWay.nodes.length > 0 &&
insertedWay.nodes[insertedWay.nodes.length - 1] === originalWay.nodes[0] &&
originalWay.nodes[originalWay.nodes.length - 1] !== insertedWay.nodes[0];
} else {
// Add the member anywhere, one time. Just push and let `osmJoinWays` decide where to put it.
groups = utilArrayGroupBy(relation.members, 'type');
groups.way = groups.way || [];
groups.way.push(member);
}
// Add the member anywhere, one time. Just push and let `osmJoinWays` decide where to put it.
groups = utilArrayGroupBy(relation.members, 'type');
groups.way = groups.way || [];
groups.way.push(member);
members = withIndex(groups.way);
var joined = osmJoinWays(members, graph);
@@ -102,22 +72,6 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) {
item = segment[k];
var way = graph.entity(item.id);
// If this is a paired item, generate members in correct order and role
if (tempWay && item.id === tempWay.id) {
var reverse = nodes[0].id !== insertPair.nodes[0] ^ insertPairIsReversed;
if (reverse) {
item.pair = [
{ id: insertPair.insertedID, type: 'way', role: item.role },
{ id: insertPair.originalID, type: 'way', role: item.role }
];
} else {
item.pair = [
{ id: insertPair.originalID, type: 'way', role: item.role },
{ id: insertPair.insertedID, type: 'way', role: item.role }
];
}
}
// reorder `members` if necessary
if (k > 0) {
if (j+k >= members.length || item.index !== members[j+k].index) {
@@ -129,22 +83,13 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) {
}
}
if (tempWay) {
graph = graph.remove(tempWay);
}
// Final pass: skip dead items, split pairs, remove index properties
// Final pass: skip dead items, remove index properties
var wayMembers = [];
for (i = 0; i < members.length; i++) {
item = members[i];
if (item.index === -1) continue;
if (item.pair) {
wayMembers.push(item.pair[0]);
wayMembers.push(item.pair[1]);
} else {
wayMembers.push(utilObjectOmit(item, ['index']));
}
wayMembers.push(utilObjectOmit(item, ['index']));
}
// Put stops and platforms first, then nodes, ways, relations
@@ -186,8 +131,8 @@ export function actionAddMember(relationId, member, memberIndex, insertPair) {
}
var item = Object.assign({}, arr[i]); // shallow copy
arr[i].index = -1; // mark as dead
item.index = toIndex;
arr[i].index = -1; // mark previous entry as dead
delete item.index; // inserted items must never be moved again
arr.splice(toIndex, 0, item);
}

View File

@@ -1,4 +1,3 @@
import { actionAddMember } from './add_member';
import { geoSphericalDistance } from '../geo/geo';
import { osmRelation } from '../osm/relation';
import { osmWay } from '../osm/way';
@@ -95,7 +94,6 @@ export function actionSplit(nodeIds, newWayIds) {
function split(graph, nodeId, wayA, newWayId) {
var wayB = osmWay({ id: newWayId, tags: wayA.tags }); // `wayB` is the NEW way
var origNodes = wayA.nodes.slice();
var nodesA;
var nodesB;
var isArea = wayA.isArea();
@@ -165,8 +163,6 @@ export function actionSplit(nodeIds, newWayIds) {
graph = graph.replace(wayB);
graph.parentRelations(wayA).forEach(function(relation) {
var member;
// Turn restrictions - make sure:
// 1. Splitting a FROM/TO way - only `wayA` OR `wayB` remains in relation
// (whichever one is connected to the VIA node/ways)
@@ -203,34 +199,16 @@ export function actionSplit(nodeIds, newWayIds) {
} else {
for (i = 0; i < v.length; i++) {
if (v[i].type === 'way' && v[i].id === wayA.id) {
member = {
id: wayB.id,
type: 'way',
role: 'via'
};
graph = actionAddMember(relation.id, member, v[i].index + 1)(graph);
break;
graph = splitWayMember(graph, relation.id, wayA, wayB);
}
}
}
// All other relations (Routes, Multipolygons, etc):
// 1. Both `wayA` and `wayB` remain in the relation
// 2. But must be inserted as a pair (see `actionAddMember` for details)
// 2. But must be inserted in the correct order
} else {
member = {
id: wayB.id,
type: 'way',
role: relation.memberById(wayA.id).role
};
var insertPair = {
originalID: wayA.id,
insertedID: wayB.id,
nodes: origNodes
};
graph = actionAddMember(relation.id, member, undefined, insertPair)(graph);
graph = splitWayMember(graph, relation.id, wayA, wayB);
}
});
@@ -253,6 +231,154 @@ export function actionSplit(nodeIds, newWayIds) {
return graph;
}
function splitWayMember(graph, relationId, wayA, wayB) {
let relation = graph.entity(relationId);
const insertMembers = [];
for (let i = 0; i < relation.members.length; i++) {
const member = relation.members[i];
if (member.id === wayA.id) {
let wayAconnectsPrev = false;
let wayAconnectsNext = false;
let wayBconnectsPrev = false;
let wayBconnectsNext = false;
function connects(way1, way2) {
if (way1.nodes.length < 2 || way2.nodes.length < 2) return false;
if (way1.nodes[0] === way2.nodes[0]) return true;
if (way1.nodes[0] === way2.nodes[way2.nodes.length - 1]) return true;
if (way1.nodes[way1.nodes.length - 1] === way2.nodes[way2.nodes.length - 1]) return true;
if (way1.nodes[way1.nodes.length - 1] === way2.nodes[0]) return true;
return false;
}
if (i > 0 && graph.hasEntity(relation.members[i - 1].id)) {
const prevMember = relation.members[i - 1];
const prevEntity = graph.entity(prevMember.id);
if (prevEntity.type === 'way' && prevEntity.id !== wayA.id && prevEntity.nodes.length > 0) {
wayAconnectsPrev = connects(prevEntity, wayA);
wayBconnectsPrev = connects(prevEntity, wayB);
}
}
if (i < relation.members.length - 1 && graph.hasEntity(relation.members[i + 1].id)) {
const nextMember = relation.members[i + 1];
const nextEntity = graph.entity(nextMember.id);
if (nextEntity.type === 'way' && nextEntity.nodes.length > 0) {
wayAconnectsNext = connects(nextEntity, wayA);
wayBconnectsNext = connects(nextEntity, wayB);
}
}
if (wayAconnectsPrev && !wayBconnectsPrev && !wayAconnectsNext && !wayBconnectsNext) {
// wayA connects to prev member -> insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
if (wayAconnectsPrev && !wayBconnectsPrev && wayAconnectsNext && wayBconnectsNext) {
// wayB only connects to next -> insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
if (!wayAconnectsPrev && !wayBconnectsPrev && !wayAconnectsNext && wayBconnectsNext) {
// wayB connects to next member -> insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
if (wayAconnectsPrev && wayBconnectsPrev && !wayAconnectsNext && wayBconnectsNext) {
// wayA only connects to prev -> insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
if (wayAconnectsPrev && !wayBconnectsPrev && !wayAconnectsNext && wayBconnectsNext) {
// wayA connects to prev, wayB connects to next -> insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
if (!wayAconnectsPrev && wayBconnectsPrev && !wayAconnectsNext && !wayBconnectsNext) {
// wayB connects to prev member -> insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
if (!wayAconnectsPrev && wayBconnectsPrev && wayAconnectsNext && wayBconnectsNext) {
// wayA only connects to next -> insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
if (!wayAconnectsPrev && !wayBconnectsPrev && wayAconnectsNext && !wayBconnectsNext) {
// wayA connects to next member -> insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
if (wayAconnectsPrev && wayBconnectsPrev && wayAconnectsNext && !wayBconnectsNext) {
// wayB only connects to prev -> insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
if (!wayAconnectsPrev && wayBconnectsPrev && wayAconnectsNext && !wayBconnectsNext) {
// wayB connects to prev, wayA connects to next -> insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
// check for loops
if (wayAconnectsPrev && wayBconnectsPrev && wayAconnectsNext && wayBconnectsNext) {
// complete loop
// look one more member ahead
if (i > 2 && graph.hasEntity(relation.members[i - 2].id)) {
const prev2Entity = graph.entity(relation.members[i - 2].id);
if (connects(prev2Entity, wayA) && !connects(prev2Entity, wayB)) {
// prev-2 member connects only to A: insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
if (connects(prev2Entity, wayB) && !connects(prev2Entity, wayA)) {
// prev-2 member connects only to B: insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
}
if (i < relation.members.length - 2 && graph.hasEntity(relation.members[i + 2].id)) {
const next2Entity = graph.entity(relation.members[i + 2].id);
if (connects(next2Entity, wayA) && !connects(next2Entity, wayB)) {
// next+2 member connects only to A: insert B after A
insertMembers.push({at: i + 1, role: member.role});
continue;
}
if (connects(next2Entity, wayB) && !connects(next2Entity, wayA)) {
// next+2 member connects only to B: insert B before A
insertMembers.push({at: i, role: member.role});
continue;
}
}
}
// could not determine how new member should connect (i.e. existing way was not connected to other member ways)
// just make sure before/after still connect
if (wayA.nodes[wayA.nodes.length - 1] === wayB.nodes[0]) {
insertMembers.push({at: i + 1, role: member.role});
continue;
} else {
insertMembers.push({at: i, role: member.role});
continue;
}
/*
// could not determine how new member should connect (i.e. existing way was not connected to other member ways)
// -> insert new way after existing way
insertMembers.push({at: i + 1, role: member.role});*/
}
}
// insert new member(s)
insertMembers.reverse().forEach(item => {
graph = graph.replace(relation.addMember({
id: wayB.id,
type: 'way',
role: item.role
}, item.at));
relation = graph.entity(relation.id);
});
return graph;
}
var action = function(graph) {
_createdWayIDs = [];
var newWayIndex = 0;
@@ -313,12 +439,34 @@ export function actionSplit(nodeIds, newWayIds) {
action.disabled = function(graph) {
for (var i = 0; i < nodeIds.length; i++) {
var nodeId = nodeIds[i];
var candidates = action.waysForNode(nodeId, graph);
for (const nodeId of nodeIds) {
const candidates = action.waysForNode(nodeId, graph);
if (candidates.length === 0 || (_wayIDs && _wayIDs.length !== candidates.length)) {
return 'not_eligible';
}
for (const way of candidates) {
const parentRelations = graph.parentRelations(way);
for (const parentRelation of parentRelations) {
if (parentRelation.hasFromViaTo()) {
// turn restrictions: via memebers must be loaded
const vias = parentRelation.membersByRole('via');
if (!vias.every(via => graph.hasEntity(via.id))) {
return 'parent_incomplete';
}
} else {
// other relations (e.g. route relations): at least one members before or after way must be present
for (let i = 0; i < parentRelation.members.length; i++) {
if (parentRelation.members[i].id === way.id) {
const memberBeforePresent = i > 0 && graph.hasEntity(parentRelation.members[i - 1].id);
const memberAfterPresent = i < parentRelation.members.length - 1 && graph.hasEntity(parentRelation.members[i + 1].id);
if (!memberBeforePresent && !memberAfterPresent) {
return 'parent_incomplete';
}
}
}
}
}
}
}
};

View File

@@ -119,122 +119,6 @@ describe('iD.actionAddMember', function() {
expect(members(graph)).to.eql(['-', '=', '~']);
});
it('inserts the member multiple times if insertPair provided (middle)', function() {
// Before: a ---> b .. c ~~~> d <~~~ c .. b <--- a
// After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a
var graph = iD.coreGraph([
iD.osmNode({id: 'a', loc: [0, 0]}),
iD.osmNode({id: 'b', loc: [0, 0]}),
iD.osmNode({id: 'c', loc: [0, 0]}),
iD.osmNode({id: 'd', loc: [0, 0]}),
iD.osmWay({id: '-', nodes: ['a', 'b']}),
iD.osmWay({id: '=', nodes: ['b', 'c']}),
iD.osmWay({id: '~', nodes: ['c', 'd']}),
iD.osmRelation({id: 'r', members: [
{id: '-', type: 'way'},
{id: '~', type: 'way'},
{id: '~', type: 'way'},
{id: '-', type: 'way'}
]})
]);
var member = { id: '=', type: 'way' };
var insertPair = {
originalID: '-',
insertedID: '=',
nodes: ['a','b','c']
};
graph = iD.actionAddMember('r', member, undefined, insertPair)(graph);
expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']);
});
it('inserts the member multiple times if insertPair provided (middle) (reversed pair)', function() {
// Before: a .. b ===> c ~~~> d <~~~ c <=== b .. a
// After: a ---> b ===> c ~~~> d <~~~ c <=== b <--- a
var graph = iD.coreGraph([
iD.osmNode({id: 'a', loc: [0, 0]}),
iD.osmNode({id: 'b', loc: [0, 0]}),
iD.osmNode({id: 'c', loc: [0, 0]}),
iD.osmNode({id: 'd', loc: [0, 0]}),
iD.osmWay({id: '-', nodes: ['a', 'b']}),
iD.osmWay({id: '=', nodes: ['b', 'c']}),
iD.osmWay({id: '~', nodes: ['c', 'd']}),
iD.osmRelation({id: 'r', members: [
{id: '=', type: 'way'},
{id: '~', type: 'way'},
{id: '~', type: 'way'},
{id: '=', type: 'way'}
]})
]);
var member = { id: '=', type: 'way' };
var insertPair = {
originalID: '=',
insertedID: '-',
nodes: ['a','b','c']
};
graph = iD.actionAddMember('r', member, undefined, insertPair)(graph);
expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']);
});
it('inserts the member multiple times if insertPair provided (beginning/end)', function() {
// Before: b <=== c ~~~> d <~~~ c ===> b
// After: a <--- b <=== c ~~~> d <~~~ c ===> b ---> a
var graph = iD.coreGraph([
iD.osmNode({id: 'a', loc: [0, 0]}),
iD.osmNode({id: 'b', loc: [0, 0]}),
iD.osmNode({id: 'c', loc: [0, 0]}),
iD.osmNode({id: 'd', loc: [0, 0]}),
iD.osmWay({id: '-', nodes: ['b', 'a']}),
iD.osmWay({id: '=', nodes: ['c', 'b']}),
iD.osmWay({id: '~', nodes: ['c', 'd']}),
iD.osmRelation({id: 'r', members: [
{id: '=', type: 'way'},
{id: '~', type: 'way'},
{id: '~', type: 'way'},
{id: '=', type: 'way'}
]})
]);
var member = { id: '-', type: 'way' };
var insertPair = {
originalID: '=',
insertedID: '-',
nodes: ['c','b','a']
};
graph = iD.actionAddMember('r', member, undefined, insertPair)(graph);
expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']);
});
it('inserts the member multiple times if insertPair provided (beginning/end) (reversed pair)', function() {
// Before: a <--- b .. c ~~~> d <~~~ c .. b ---> a
// After: a <--- b <=== c ~~~> d <~~~ c ===> b ---> a
var graph = iD.coreGraph([
iD.osmNode({id: 'a', loc: [0, 0]}),
iD.osmNode({id: 'b', loc: [0, 0]}),
iD.osmNode({id: 'c', loc: [0, 0]}),
iD.osmNode({id: 'd', loc: [0, 0]}),
iD.osmWay({id: '-', nodes: ['b', 'a']}),
iD.osmWay({id: '=', nodes: ['c', 'b']}),
iD.osmWay({id: '~', nodes: ['c', 'd']}),
iD.osmRelation({id: 'r', members: [
{id: '-', type: 'way'},
{id: '~', type: 'way'},
{id: '~', type: 'way'},
{id: '-', type: 'way'}
]})
]);
var member = { id: '-', type: 'way' };
var insertPair = {
originalID: '-',
insertedID: '=',
nodes: ['c','b','a']
};
graph = iD.actionAddMember('r', member, undefined, insertPair)(graph);
expect(members(graph)).to.eql(['-', '=', '~', '~', '=', '-']);
});
it('keeps stops and platforms ordered before node, way, relation (for PTv2 routes)', function() {
var graph = iD.coreGraph([
iD.osmNode({id: 'a', loc: [0, 0]}),

View File

@@ -425,15 +425,14 @@ describe('iD.actionSplit', function () {
}
it('handles incomplete relations', function () {
it('disables split action on too incomplete relations', function () {
//
// Situation:
// a ---> b ---> c split at 'b'
// Relation: ['~', '-']
// Relation: ['?', '-'] member '?' missing
//
// Expected result:
// a ---> b ===> c
// Relation: ['~', '-', '=']
// forbidden, because correct order of -/= cannot be determined
//
var graph = iD.coreGraph([
iD.osmNode({ id: 'a', loc: [0, 0] }),
@@ -441,13 +440,73 @@ describe('iD.actionSplit', function () {
iD.osmNode({ id: 'c', loc: [2, 0] }),
iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }),
iD.osmRelation({id: 'r', members: [
{ id: '~', type: 'way' },
{ id: '?', type: 'way' },
{ id: '-', type: 'way' }
]})
]);
graph = iD.actionSplit('b', ['='])(graph);
expect(members(graph)).to.eql(['~', '-', '=']);
var action = iD.actionSplit('b', ['=']);
expect(action.disabled(graph)).to.be.ok;
});
it('enables split action on partially incomplete, but still sufficiently complete relations (before split)', function () {
//
// Situation:
// a ~~~> b ---> c ---> d split at 'c'
// Relation: ['~', '-', '?'] member '?' missing
//
// Expected result:
// a ~~~> b ---> c ===> d
// Relation: ['~', '-', '=', '?']
//
var graph = iD.coreGraph([
iD.osmNode({ id: 'a', loc: [0, 0] }),
iD.osmNode({ id: 'b', loc: [1, 0] }),
iD.osmNode({ id: 'c', loc: [2, 0] }),
iD.osmNode({ id: 'd', loc: [3, 0] }),
iD.osmWay({ id: '~', nodes: ['a', 'b'] }),
iD.osmWay({ id: '-', nodes: ['b', 'c', 'd'] }),
iD.osmRelation({id: 'r', members: [
{ id: '~', type: 'way' },
{ id: '-', type: 'way' },
{ id: '?', type: 'way' }
]})
]);
var action = iD.actionSplit('c', ['=']);
expect(action.disabled(graph)).to.be.not.ok;
graph = action(graph);
expect(members(graph)).to.eql(['~', '-', '=', '?']);
});
it('enables split action on partially incomplete, but still sufficiently complete relations (after split)', function () {
//
// Situation:
// a ---> b ---> c ~~~> d split at 'b'
// Relation: ['?', '-', '~'] member '?' missing
//
// Expected result:
// a ---> b ===> c ~~~> d
// Relation: ['?', '-', '=', '~']
//
var graph = iD.coreGraph([
iD.osmNode({ id: 'a', loc: [0, 0] }),
iD.osmNode({ id: 'b', loc: [1, 0] }),
iD.osmNode({ id: 'c', loc: [2, 0] }),
iD.osmNode({ id: 'd', loc: [3, 0] }),
iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }),
iD.osmWay({ id: '~', nodes: ['c', 'd'] }),
iD.osmRelation({id: 'r', members: [
{ id: '?', type: 'way' },
{ id: '-', type: 'way' },
{ id: '~', type: 'way' }
]})
]);
var action = iD.actionSplit('b', ['=']);
expect(action.disabled(graph)).to.be.not.ok;
graph = action(graph);
expect(members(graph)).to.eql(['?', '-', '=', '~']);
});
@@ -593,28 +652,28 @@ describe('iD.actionSplit', function () {
]);
});
it('reorders members as node, way, relation (for Public Transport routing)', function () {
it('preserves other members (example: Public Transport routing)', function () {
var graph = iD.coreGraph([
iD.osmNode({ id: 'a', loc: [0, 0] }),
iD.osmNode({ id: 'b', loc: [1, 0] }),
iD.osmNode({ id: 'c', loc: [2, 0] }),
iD.osmWay({ id: '-', nodes: ['a', 'b', 'c'] }),
iD.osmRelation({id: 'r', members: [
{ id: 'n1', type: 'node', role: 'forward' },
{ id: 'n1', type: 'node', role: 'stop' },
{ id: '-', type: 'way', role: 'forward' },
{ id: 'r1', type: 'relation', role: 'forward' },
{ id: 'n2', type: 'node', role: 'forward' }
{ id: 'r1', type: 'relation', role: '' },
{ id: 'n2', type: 'node', role: 'stop' }
]})
]);
graph = iD.actionSplit('b', ['='])(graph);
expect(graph.entity('r').members).to.eql([
{ id: 'n1', type: 'node', role: 'forward' },
{ id: 'n2', type: 'node', role: 'forward' },
{ id: 'n1', type: 'node', role: 'stop' },
{ id: '-', type: 'way', role: 'forward' },
{ id: '=', type: 'way', role: 'forward' },
{ id: 'r1', type: 'relation', role: 'forward'}
{ id: 'r1', type: 'relation', role: ''},
{ id: 'n2', type: 'node', role: 'stop' }
]);
});
});