diff --git a/data/core.yaml b/data/core.yaml index 20f91e406..68bd982f7 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -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: diff --git a/modules/actions/add_member.js b/modules/actions/add_member.js index 9be9dc937..f33145a3f 100644 --- a/modules/actions/add_member.js +++ b/modules/actions/add_member.js @@ -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); } diff --git a/modules/actions/split.js b/modules/actions/split.js index 042b25aa0..23905622f 100644 --- a/modules/actions/split.js +++ b/modules/actions/split.js @@ -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'; + } + } + } + } + } + } } }; diff --git a/test/spec/actions/add_member.js b/test/spec/actions/add_member.js index 62f6c69bc..6bc8669bc 100644 --- a/test/spec/actions/add_member.js +++ b/test/spec/actions/add_member.js @@ -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]}), diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index 7a4dfd635..7202f3d8f 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -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' } ]); }); });