diff --git a/index.html b/index.html index 9cdea5a66..490ee3843 100644 --- a/index.html +++ b/index.html @@ -43,6 +43,7 @@ + diff --git a/js/id/geo/turn.js b/js/id/geo/turn.js new file mode 100644 index 000000000..296a43e31 --- /dev/null +++ b/js/id/geo/turn.js @@ -0,0 +1,62 @@ +iD.geo.turns = function(graph, entityID) { + var way = graph.entity(entityID); + if (way.type !== 'way' || !way.tags.highway || way.isArea()) + return []; + + function withRestriction(turn) { + graph.parentRelations(turn.from).forEach(function(relation) { + if (relation.tags.type !== 'restriction') + return; + + var f = relation.memberByRole('from'), + t = relation.memberByRole('to'), + v = relation.memberByRole('via'); + + if (f && f.id === turn.from.id && + t && t.id === turn.to.id && + v && v.id === turn.via.id) { + turn.restriction = relation; + } + }); + + return turn; + } + + var turns = []; + + [way.first(), way.last()].forEach(function(nodeID) { + var node = graph.entity(nodeID); + graph.parentWays(node).forEach(function(parent) { + if (parent === way || parent.isDegenerate() || !parent.tags.highway) + return; + if (way.first() === node.id && way.tags.oneway === 'yes') + return; + if (way.last() === node.id && way.tags.oneway === '-1') + return; + + var index = parent.nodes.indexOf(node.id); + + // backward + if (parent.first() !== node.id && parent.tags.oneway !== 'yes') { + turns.push(withRestriction({ + from: way, + to: parent, + via: node, + toward: graph.entity(parent.nodes[index - 1]) + })); + } + + // forward + if (parent.last() !== node.id && parent.tags.oneway !== '-1') { + turns.push(withRestriction({ + from: way, + to: parent, + via: node, + toward: graph.entity(parent.nodes[index + 1]) + })); + } + }); + }); + + return turns; +}; diff --git a/test/index.html b/test/index.html index 185528971..1ecab3f75 100644 --- a/test/index.html +++ b/test/index.html @@ -44,6 +44,7 @@ + @@ -221,6 +222,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index f187c3925..dbdecfc1c 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -50,6 +50,7 @@ + diff --git a/test/spec/geo/turn.js b/test/spec/geo/turn.js new file mode 100644 index 000000000..e4d378667 --- /dev/null +++ b/test/spec/geo/turn.js @@ -0,0 +1,276 @@ +describe("iD.geo.turns", function() { + it("returns an empty array for non-ways", function() { + var graph = iD.Graph({ + 'n': iD.Node({id: 'n'}) + }); + expect(iD.geo.turns(graph, 'n')).to.eql([]); + }); + + it("returns an empty array for non-lines", function() { + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', area: 'yes'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("returns an empty array for an unconnected way", function() { + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + '=': iD.Way({id: '=', nodes: ['u', 'v']}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("omits turns onto degenerate ways", function() { + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("omits turns from non-highways", function() { + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v']}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("omits turns onto non-highways", function() { + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w']}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("omits turns onto non-lines", function() { + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + 'x': iD.Node({id: 'x'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w', 'x', 'v'], tags: {highway: 'residential', area: 'yes'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("permits turns onto a way forward", function() { + // u====v--->w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }]); + }); + + it("permits turns onto a way backward", function() { + // u====v<---w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }]); + }); + + it("permits turns onto a way in both directions", function() { + // w + // | + // u===v + // | + // x + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + 'x': iD.Node({id: 'x'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['w', 'v', 'x'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }, { + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('x') + }]); + }); + + it("permits turns from a oneway forward", function() { + // u===>v----w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', oneway: 'yes'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }]); + }); + + it("permits turns from a reverse oneway backward", function() { + // u<===v----w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['v', 'u'], tags: {highway: 'residential', oneway: '-1'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }]); + }); + + it("omits turns from a oneway backward", function() { + // u<===v----w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['v', 'u'], tags: {highway: 'residential', oneway: 'yes'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("omits turns from a reverse oneway forward", function() { + // u===>v----w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', oneway: '-1'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("permits turns onto a oneway forward", function() { + // u====v--->w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential', oneway: 'yes'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }]); + }); + + it("permits turns onto a reverse oneway backward", function() { + // u====v<---w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential', oneway: '-1'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w') + }]); + }); + + it("omits turns onto a oneway backward", function() { + // u====v<---w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential', oneway: 'yes'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("omits turns onto a reverse oneway forward", function() { + // u====v--->w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential', oneway: '-1'}}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([]); + }); + + it("restricts turns with a restriction relation", function() { + // u====v--->w + var graph = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + '=': iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + '-': iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}), + 'r': iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + {id: '=', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: 'v', role: 'via', type: 'node'} + ]}) + }); + expect(iD.geo.turns(graph, '=')).to.eql([{ + from: graph.entity('='), + to: graph.entity('-'), + via: graph.entity('v'), + toward: graph.entity('w'), + restriction: graph.entity('r') + }]); + }); + + // U-turns + // Self-intersections + // Split point +});