diff --git a/index.html b/index.html index 87e93c643..07063cf81 100644 --- a/index.html +++ b/index.html @@ -81,6 +81,7 @@ + diff --git a/js/id/actions/join.js b/js/id/actions/join.js new file mode 100644 index 000000000..fd6862a9b --- /dev/null +++ b/js/id/actions/join.js @@ -0,0 +1,65 @@ +// Join ways at the end node they share. +// +// This is the inverse of `iD.actions.Split`. +// +// Reference: +// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MergeWaysAction.as +// https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/actions/CombineWayAction.java +// +iD.actions.Join = function(idA, idB) { + var action = function(graph) { + var a = graph.entity(idA), + b = graph.entity(idB), + nodes, tags; + + if (a.first() === b.first()) { + // a <-- b ==> c + // Expected result: + // a <-- b <-- c + nodes = b.nodes.slice().reverse().concat(a.nodes.slice(1)); + + } else if (a.first() === b.last()) { + // a <-- b <== c + // Expected result: + // a <-- b <-- c + nodes = b.nodes.concat(a.nodes.slice(1)); + + } else if (a.last() === b.first()) { + // a --> b ==> c + // Expected result: + // a --> b --> c + nodes = a.nodes.concat(b.nodes.slice(1)); + + } else if (a.last() === b.last()) { + // a --> b <== c + // Expected result: + // a --> b --> c + nodes = a.nodes.concat(b.nodes.slice().reverse().slice(1)); + } + + graph.parentRelations(b) + .forEach(function (parent) { + var memberA = parent.memberById(idA), + memberB = parent.memberById(idB); + if (!memberA) { + graph = graph.replace(parent.addMember({id: idA, role: memberB.role})); + } + }); + + graph = graph.replace(a.mergeTags(b.tags).update({nodes: nodes})); + graph = iD.actions.DeleteWay(idB)(graph); + + return graph; + }; + + action.enabled = function(graph) { + var a = graph.entity(idA), + b = graph.entity(idB); + return a.first() === b.first() || + a.first() === b.last() || + a.last() === b.first() || + a.last() === b.last(); + }; + + return action; +}; diff --git a/js/id/actions/split.js b/js/id/actions/split.js index 0c648bd26..19a71c619 100644 --- a/js/id/actions/split.js +++ b/js/id/actions/split.js @@ -1,5 +1,7 @@ // Split a way at 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. diff --git a/test/index.html b/test/index.html index 512a1ee1e..524894c36 100644 --- a/test/index.html +++ b/test/index.html @@ -78,6 +78,7 @@ + @@ -147,6 +148,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index 5adaf06f6..948ed0ae1 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -40,6 +40,7 @@ + diff --git a/test/spec/actions/join.js b/test/spec/actions/join.js new file mode 100644 index 000000000..1cddb94bf --- /dev/null +++ b/test/spec/actions/join.js @@ -0,0 +1,172 @@ +describe("iD.actions.Join", function () { + describe("#enabled", function () { + it("returns true for ways that share an end/start node", function () { + // 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']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns true for ways that share a start/end node", function () { + // 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: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns true for ways that share a start/start node", function () { + // 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: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns true for ways that share an end/end node", function () { + // 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']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.true; + }); + + it("returns false for ways that don't share the necessary nodes", function () { + // 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: ['b', 'd']}) + }); + + expect(iD.actions.Join('-', '=').enabled(graph)).to.be.false; + }); + }); + + it("joins a --> b ==> c", function () { + // 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']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a <-- b <== c", function () { + // 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: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a <-- b ==> c", function () { + // Expected result: + // a <-- b <-- c + // tags on === reversed + var graph = iD.Graph({ + 'a': iD.Node({id: 'a'}), + 'b': iD.Node({id: 'b'}), + 'c': iD.Node({id: 'c'}), + '-': iD.Way({id: '-', nodes: ['b', 'a']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['c', 'b', 'a']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("joins a --> b <== c", function () { + // Expected result: + // a --> b --> c + // tags on === reversed + 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']}), + '=': iD.Way({id: '=', nodes: ['c', 'b']}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c']); + expect(graph.entity('=')).to.be.undefined; + }); + + it("merges tags", 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'], tags: {a: 'a', b: '-', c: 'c'}}), + '=': iD.Way({id: '=', nodes: ['b', 'c'], tags: {a: 'a', b: '=', d: 'd'}}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('-').tags).to.eql({a: 'a', b: '-; =', c: 'c', d: 'd'}); + }); + + it("merges 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']}), + '=': iD.Way({id: '=', nodes: ['b', 'c']}), + 'r1': iD.Relation({id: 'r1', members: [{id: '=', role: 'r1'}]}), + 'r2': iD.Relation({id: 'r2', members: [{id: '=', role: 'r1'}, {id: '-', role: 'r2'}]}) + }); + + graph = iD.actions.Join('-', '=')(graph); + + expect(graph.entity('r1').members).to.eql([{id: '-', role: 'r1'}]); + expect(graph.entity('r2').members).to.eql([{id: '-', role: 'r2'}]); + }); +});