Reverse directional tags and roles when reversing a way

Reverse known 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

Fixes #299.
This commit is contained in:
John Firebaugh
2013-01-07 16:21:54 -08:00
parent d4af1032a3
commit bcb4de4305
7 changed files with 199 additions and 3 deletions

View File

@@ -62,6 +62,7 @@
<script src='js/id/actions/remove_way_node.js'></script>
<script src='js/id/actions/reverse_way.js'></script>
<script src='js/id/actions/split_way.js'></script>
<script src='js/id/actions/update_relation_member.js'></script>
<script src='js/id/behavior.js'></script>
<script src='js/id/behavior/drag.js'></script>

View File

@@ -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}));
};
};

View File

@@ -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}));
};
};

View File

@@ -60,6 +60,7 @@
<script src='../js/id/actions/remove_relation_member.js'></script>
<script src='../js/id/actions/remove_way_node.js'></script>
<script src='../js/id/actions/reverse_way.js'></script>
<script src='../js/id/actions/update_relation_member.js'></script>
<script src='../js/id/behavior.js'></script>
<script src='../js/id/behavior/drag.js'></script>
@@ -111,6 +112,7 @@
<script src="spec/actions/remove_way_node.js"></script>
<script src="spec/actions/remove_relation_member.js"></script>
<script src="spec/actions/reverse_way.js"></script>
<script src="spec/actions/update_relation_member.js"></script>
<script src="spec/behavior/hover.js"></script>

View File

@@ -37,6 +37,7 @@
<script src="spec/actions/remove_way_node.js"></script>
<script src="spec/actions/remove_relation_member.js"></script>
<script src="spec/actions/reverse_way.js"></script>
<script src="spec/actions/update_relation_member.js"></script>
<script src="spec/behavior/hover.js"></script>

View File

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

View File

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