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'}]);
+ });
+});