diff --git a/data/core.yaml b/data/core.yaml index 621edc78d..6e3978c7e 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -109,9 +109,15 @@ en: annotation: Reversed a line. split: title: Split - description: Split this into two ways at this point. + description: + line: Split this line into two at this point. + area: Split the boundary of this area into two. + multiple: Split the lines/area boundaries at this point into two. key: X - annotation: Split a way. + annotation: + line: Split a line. + area: Split an area boundary. + multiple: "Split {n} lines/area boundaries." not_eligible: Lines can't be split at their beginning or end. multiple_ways: There are too many lines here to split. nothing_to_undo: Nothing to undo. diff --git a/data/locales.js b/data/locales.js index e12e0fc09..3e29d4856 100644 --- a/data/locales.js +++ b/data/locales.js @@ -139,9 +139,17 @@ locale.en = { }, "split": { "title": "Split", - "description": "Split this into two ways at this point.", + "description": { + "line": "Split this line into two at this point.", + "area": "Split the boundary of this area into two.", + "multiple": "Split the lines/area boundaries at this point into two." + }, "key": "X", - "annotation": "Split a way.", + "annotation": { + "line": "Split a line.", + "area": "Split an area boundary.", + "multiple": "Split {n} lines/area boundaries." + }, "not_eligible": "Lines can't be split at their beginning or end.", "multiple_ways": "There are too many lines here to split." } diff --git a/js/id/actions/split.js b/js/id/actions/split.js index 0f679fc77..b2df1abae 100644 --- a/js/id/actions/split.js +++ b/js/id/actions/split.js @@ -1,5 +1,8 @@ // 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. @@ -9,26 +12,7 @@ // Reference: // https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as // -iD.actions.Split = function(nodeId, newWayIds) { - function candidateWays(graph) { - var node = graph.entity(nodeId), - parents = graph.parentWays(node); - - return parents.filter(function(parent) { - if (parent.isClosed()) { - return true; - } - - for (var i = 1; i < parent.nodes.length - 1; i++) { - if (parent.nodes[i] === nodeId) { - return true; - } - } - - return false; - }); - } - +iD.actions.Split = function(nodeId, wayIds, newWayIds) { function split(graph, wayA, newWayId) { var wayB = iD.Way({id: newWayId, tags: wayA.tags}), nodesA, @@ -102,16 +86,38 @@ iD.actions.Split = function(nodeId, newWayIds) { } var action = function(graph) { - var candidates = candidateWays(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 = candidateWays(graph); - if (candidates.length === 0) + var candidates = action.ways(graph); + if (candidates.length === 0 || (wayIds && wayIds.length && wayIds.length !== candidates.length)) return 'not_eligible'; }; diff --git a/js/id/operations/split.js b/js/id/operations/split.js index d61ad5450..6695abccc 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -1,16 +1,27 @@ iD.operations.Split = function(selection, context) { - var entityId = selection[0], - action = iD.actions.Split(entityId); + var vertices = _.filter(selection, function vertex(entityId) { + return context.geometry(entityId) === 'vertex' + }); + + var entityId = vertices[0], + action = iD.actions.Split(entityId, _.without(selection, entityId)); var operation = function() { - var annotation = t('operations.split.annotation'), - difference = context.perform(action, annotation); + var annotation; + + var ways = action.ways(context.graph()); + if (ways.length === 1) { + annotation = t('operations.split.annotation.' + context.geometry(ways[0].id)); + } else { + annotation = t('operations.split.annotation.multiple', {n: ways.length}); + } + + var difference = context.perform(action, annotation); context.enter(iD.modes.Select(context, difference.extantIDs())); }; operation.available = function() { - return selection.length === 1 && - context.geometry(entityId) === 'vertex'; + return vertices.length === 1; }; operation.disabled = function() { @@ -19,9 +30,16 @@ iD.operations.Split = function(selection, context) { operation.tooltip = function() { var disable = operation.disabled(); - return disable ? - t('operations.split.' + disable) : - t('operations.split.description'); + if (disable) { + return t('operations.split.' + disable); + } + + var ways = action.ways(context.graph()); + if (ways.length === 1) { + return t('operations.split.description.' + context.geometry(ways[0].id)); + } else { + return t('operations.split.description.multiple'); + } }; operation.id = "split"; diff --git a/test/spec/actions/split.js b/test/spec/actions/split.js index ec8042708..bfdf4ab7c 100644 --- a/test/spec/actions/split.js +++ b/test/spec/actions/split.js @@ -25,6 +25,20 @@ describe("iD.actions.Split", function () { 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'}), @@ -56,6 +70,20 @@ describe("iD.actions.Split", function () { 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 () { @@ -74,7 +102,7 @@ describe("iD.actions.Split", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); @@ -89,7 +117,7 @@ describe("iD.actions.Split", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); // Immutable tags => should be shared by identity. expect(graph.entity('-').tags).to.equal(tags); @@ -118,7 +146,7 @@ describe("iD.actions.Split", function () { '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); @@ -152,7 +180,7 @@ describe("iD.actions.Split", function () { '|': iD.Way({id: '|', nodes: ['c', '*', 'd']}) }); - graph = iD.actions.Split('*', ['=', '¦'])(graph); + graph = iD.actions.Split('*', undefined, ['=', '¦'])(graph); expect(graph.entity('-').nodes).to.eql(['a', '*']); expect(graph.entity('=').nodes).to.eql(['*', 'b']); @@ -160,6 +188,34 @@ describe("iD.actions.Split", function () { 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 @@ -183,7 +239,7 @@ describe("iD.actions.Split", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) }); - graph = iD.actions.Split('a', ['='])(graph); + 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']); @@ -210,19 +266,19 @@ describe("iD.actions.Split", function () { '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) }); - var g1 = iD.actions.Split('a', ['='])(graph); + 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', ['='])(graph); + 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', ['='])(graph); + 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', ['='])(graph); + 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']); }); @@ -236,7 +292,7 @@ describe("iD.actions.Split", function () { '-': iD.Way({id: '-', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}) }); - graph = iD.actions.Split('a', ['='])(graph); + 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); @@ -268,7 +324,7 @@ describe("iD.actions.Split", function () { 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '-', type: 'way', role: 'forward'}, @@ -297,7 +353,7 @@ describe("iD.actions.Split", function () { 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=', '~']); }); @@ -323,7 +379,7 @@ describe("iD.actions.Split", function () { 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '=', '-']); }); @@ -337,7 +393,7 @@ describe("iD.actions.Split", function () { 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '-', '=']); }); @@ -367,7 +423,7 @@ describe("iD.actions.Split", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '=', role: 'from'}, @@ -399,7 +455,7 @@ describe("iD.actions.Split", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '~', role: 'from'}, @@ -431,7 +487,7 @@ describe("iD.actions.Split", function () { {id: 'c', role: 'via'}]}) }); - graph = iD.actions.Split('b', ['='])(graph); + graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '-', role: 'from'},