diff --git a/index.html b/index.html index 585b32281..782d4b32f 100644 --- a/index.html +++ b/index.html @@ -62,6 +62,7 @@ + diff --git a/js/id/actions/reverse_way.js b/js/id/actions/reverse_way.js index facc75edf..a6ba24518 100644 --- a/js/id/actions/reverse_way.js +++ b/js/id/actions/reverse_way.js @@ -1,8 +1,75 @@ -// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as +/* + Order the nodes of a way in reverse order and reverse any direction dependent tags + other than `oneway`. (We assume that correcting a backwards oneway is the primary + reason for reversing a way.) + + The following transforms are performed: + + Keys: + *:right=* ⟺ *:left=* + *:forward=* ⟺ *:backward=* + direction=up ⟺ direction=down + incline=up ⟺ incline=down + *=right ⟺ *=left + + Relation members: + role=forward ⟺ role=backward + + In addition, numeric-valued `incline` tags are negated. + + The JOSM implementation was used as a guide, but transformations that were of unclear benefit + or adjusted tags that don't seem to be used in practice were omitted. + + References: + http://wiki.openstreetmap.org/wiki/Forward_%26_backward,_left_%26_right + http://wiki.openstreetmap.org/wiki/Key:direction#Steps + http://wiki.openstreetmap.org/wiki/Key:incline + http://wiki.openstreetmap.org/wiki/Route#Members + http://josm.openstreetmap.de/browser/josm/trunk/src/org/openstreetmap/josm/corrector/ReverseWayTagCorrector.java + */ iD.actions.ReverseWay = function(wayId) { + var replacements = [ + [/:right$/, ':left'], [/:left$/, ':right'], + [/:forward$/, ':backward'], [/:backward$/, ':forward'] + ], numeric = /^([+-]?)(?=[\d.])/; + + function reverseKey(key) { + for (var i = 0; i < replacements.length; ++i) { + var replacement = replacements[i]; + if (replacement[0].test(key)) { + return key.replace(replacement[0], replacement[1]); + } + } + return key; + } + + function reverseValue(key, value) { + if (key === "incline" && numeric.test(value)) { + return value.replace(numeric, function(_, sign) { return sign === '-' ? '' : '-'; }); + } else if (key === "incline" || key === "direction") { + return {up: 'down', down: 'up'}[value] || value; + } else { + return {left: 'right', right: 'left'}[value] || value; + } + } + return function(graph) { var way = graph.entity(wayId), - nodes = way.nodes.slice().reverse(); - return graph.replace(way.update({nodes: nodes})); + nodes = way.nodes.slice().reverse(), + tags = {}, key, role; + + for (key in way.tags) { + tags[reverseKey(key)] = reverseValue(key, way.tags[key]); + } + + graph.parentRelations(way.id).forEach(function (relation) { + relation.members.forEach(function (member, index) { + if (member.id === way.id && (role = {forward: 'backward', backward: 'forward'}[member.role])) { + graph = iD.actions.UpdateRelationMember(relation.id, index, {role: role})(graph); + } + }); + }); + + return graph.replace(way.update({nodes: nodes, tags: tags})); }; }; diff --git a/js/id/actions/update_relation_member.js b/js/id/actions/update_relation_member.js new file mode 100644 index 000000000..67a45b6ab --- /dev/null +++ b/js/id/actions/update_relation_member.js @@ -0,0 +1,9 @@ +iD.actions.UpdateRelationMember = function(relationId, index, properties) { + return function(graph) { + var relation = graph.entity(relationId), + members = relation.members.slice(); + + members.splice(index, 1, _.extend({}, members[index], properties)); + return graph.replace(relation.update({members: members})); + }; +}; diff --git a/test/index.html b/test/index.html index e58eb7f39..77f8fd590 100644 --- a/test/index.html +++ b/test/index.html @@ -60,6 +60,7 @@ + @@ -111,6 +112,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index eb211a0b7..22d613ec3 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -37,6 +37,7 @@ + diff --git a/test/spec/actions/reverse_way.js b/test/spec/actions/reverse_way.js index 0df898ae7..ca61cb778 100644 --- a/test/spec/actions/reverse_way.js +++ b/test/spec/actions/reverse_way.js @@ -6,4 +6,112 @@ describe("iD.actions.ReverseWay", function () { graph = iD.actions.ReverseWay(way.id)(iD.Graph([node1, node2, way])); expect(graph.entity(way.id).nodes).to.eql([node2.id, node1.id]); }); + + it("preserves non-directional tags", function () { + var way = iD.Way({tags: {'highway': 'residential'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'highway': 'residential'}); + }); + + it("preserves oneway tags", function () { + var way = iD.Way({tags: {'oneway': 'yes'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'oneway': 'yes'}); + }); + + it("transforms *:right=* ⟺ *:left=*", function () { + var way = iD.Way({tags: {'cycleway:right': 'lane'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'cycleway:left': 'lane'}); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'cycleway:right': 'lane'}); + }); + + it("transforms *:forward=* ⟺ *:backward=*", function () { + var way = iD.Way({tags: {'maxspeed:forward': '25'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'maxspeed:backward': '25'}); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'maxspeed:forward': '25'}); + }); + + it("transforms direction=up ⟺ direction=down", function () { + var way = iD.Way({tags: {'incline': 'up'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': 'down'}); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': 'up'}); + }); + + it("transforms incline=up ⟺ incline=down", function () { + var way = iD.Way({tags: {'incline': 'up'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': 'down'}); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': 'up'}); + }); + + it("negates numeric-valued incline tags", function () { + var way = iD.Way({tags: {'incline': '5%'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': '-5%'}); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': '5%'}); + + way = iD.Way({tags: {'incline': '.8°'}}); + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'incline': '-.8°'}); + }); + + it("transforms *=right ⟺ *=left", function () { + var way = iD.Way({tags: {'sidewalk': 'right'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'sidewalk': 'left'}); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'sidewalk': 'right'}); + }); + + it("transforms multiple directional tags", function () { + var way = iD.Way({tags: {'maxspeed:forward': '25', 'maxspeed:backward': '30'}}), + graph = iD.Graph([way]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(way.id).tags).to.eql({'maxspeed:backward': '25', 'maxspeed:forward': '30'}); + }); + + it("transforms role=forward ⟺ role=backward in member relations", function () { + var way = iD.Way({tags: {highway: 'residential'}}), + relation = iD.Relation({members: [{type: 'way', id: way.id, role: 'forward'}]}), + graph = iD.Graph([way, relation]); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(relation.id).members[0].role).to.eql('backward'); + + graph = iD.actions.ReverseWay(way.id)(graph); + expect(graph.entity(relation.id).members[0].role).to.eql('forward'); + }); }); diff --git a/test/spec/actions/update_relation_member.js b/test/spec/actions/update_relation_member.js new file mode 100644 index 000000000..1878ff923 --- /dev/null +++ b/test/spec/actions/update_relation_member.js @@ -0,0 +1,8 @@ +describe("iD.actions.UpdateRelationMember", function () { + it("updates the properties of the relation member at the specified index", function () { + var node = iD.Node(), + relation = iD.Relation({members: [{id: node.id, role: 'forward'}]}), + graph = iD.actions.UpdateRelationMember(relation.id, 0, {role: 'backward'})(iD.Graph([node, relation])); + expect(graph.entity(relation.id).members).to.eql([{id: node.id, role: 'backward'}]); + }); +});