From 405a49506b7f55d279a877c2394837d8ad348925 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 30 Aug 2013 14:23:05 -0700 Subject: [PATCH] Building logic for turn restrictions --- index.html | 1 + js/id/svg/restrictions.js | 75 +++++++++++++++++ test/index.html | 2 + test/index_packaged.html | 1 + test/spec/svg/restrictions.js | 151 ++++++++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+) create mode 100644 js/id/svg/restrictions.js create mode 100644 test/spec/svg/restrictions.js diff --git a/index.html b/index.html index ed2335394..9cdea5a66 100644 --- a/index.html +++ b/index.html @@ -59,6 +59,7 @@ + diff --git a/js/id/svg/restrictions.js b/js/id/svg/restrictions.js new file mode 100644 index 000000000..9d9e41b53 --- /dev/null +++ b/js/id/svg/restrictions.js @@ -0,0 +1,75 @@ +iD.svg.Restrictions = function(context) { + var projection = context.projection; + + function drawRestrictions(surface) { + var turns = drawRestrictions.turns(context.graph(), context.selectedIDs()); + + var groups = surface.select('.layer-hit').selectAll('g.restriction') + .data(turns, iD.Entity.key); + + var enter = groups.enter().append('g') + .attr('class', 'restriction'); + + enter.append('circle') + .attr('class', 'restriction') + .attr('r', 4); + + groups + .attr('transform', function(restriction) { + var via = context.entity(restriction.memberByRole('via').id); + return iD.svg.PointTransform(projection)(via); + }); + + groups.exit() + .remove(); + + return this; + } + + drawRestrictions.turns = function (graph, selectedIDs) { + if (selectedIDs.length != 1) + return []; + + var from = graph.entity(selectedIDs[0]); + if (from.type !== 'way') + return []; + + return graph.parentRelations(from).filter(function(relation) { + var f = relation.memberById(from.id), + t = relation.memberByRole('to'), + v = relation.memberByRole('via'); + + return relation.tags.type === 'restriction' && f.role === 'from' && + t && t.type === 'way' && graph.hasEntity(t.id) && + v && v.type === 'node' && graph.hasEntity(v.id) && + !graph.entity(t.id).isDegenerate() && + !graph.entity(f.id).isDegenerate() && + graph.entity(t.id).affix(v.id) && + graph.entity(f.id).affix(v.id); + }); + }; + + drawRestrictions.datum = function(graph, from, restriction, projection) { + var to = graph.entity(restriction.memberByRole('to').id), + a = graph.entity(restriction.memberByRole('via').id), + b; + + if (to.first() === a.id) { + b = graph.entity(to.nodes[1]); + } else { + b = graph.entity(to.nodes[to.nodes.length - 2]); + } + + a = projection(a.loc); + b = projection(b.loc); + + return { + from: from, + to: to, + restriction: restriction, + angle: Math.atan2(b[1] - a[1], b[0] - a[0]) + } + }; + + return drawRestrictions; +}; diff --git a/test/index.html b/test/index.html index ff9c9d143..185528971 100644 --- a/test/index.html +++ b/test/index.html @@ -60,6 +60,7 @@ + @@ -242,6 +243,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index 793cad825..f187c3925 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -72,6 +72,7 @@ + diff --git a/test/spec/svg/restrictions.js b/test/spec/svg/restrictions.js new file mode 100644 index 000000000..dc6169eb5 --- /dev/null +++ b/test/spec/svg/restrictions.js @@ -0,0 +1,151 @@ +describe("iD.svg.Restrictions", function() { + var restrictions = iD.svg.Restrictions({}); + + describe("#turns", function() { + it("returns an empty array with no selection", function() { + var graph = iD.Graph(); + expect(restrictions.turns(graph, [])).to.eql([]); + }); + + it("returns an empty array with a multiselection", function() { + var graph = iD.Graph(); + expect(restrictions.turns(graph, ['a', 'b'])).to.eql([]); + }); + + var valid = iD.Graph({ + 'u': iD.Node({id: 'u'}), + 'v': iD.Node({id: 'v'}), + 'w': iD.Node({id: 'w'}), + 'f': iD.Way({id: 'f', nodes: ['u', 'v']}), + 't': iD.Way({id: 't', nodes: ['v', 'w']}), + 'r': iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + { role: 'via', id: 'v', type: 'node' }, + { role: 'from', id: 'f', type: 'way' }, + { role: 'to', id: 't', type: 'way' } + ]}) + }); + + it("returns a valid restriction when the selected way has role 'from'", function() { + expect(restrictions.turns(valid, ['f'])).to.eql([valid.entity('r')]); + }); + + it("returns an empty array when the selected way has role 'to'", function() { + expect(restrictions.turns(valid, ['t'])).to.eql([]); + }); + + it("ignores restrictions missing a 'to' role", function() { + var graph = valid.replace(valid.entity('r').removeMembersWithID('t')); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions with an incomplete 'to' role", function() { + var graph = valid.remove(valid.entity('t')); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions missing a 'via' role", function() { + var graph = valid.replace(valid.entity('r').removeMembersWithID('v')); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions with an incomplete 'via' role", function() { + var graph = valid.remove(valid.entity('v')); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions whose 'from' role is not a way", function() { + var graph = valid.replace(iD.Node({id: 'f2'})) + .replace(valid.entity('r').replaceMember({id: 'f'}, {id: 'f2', type: 'node'})); + expect(restrictions.turns(graph, ['f2'])).to.eql([]); + }); + + it("ignores restrictions whose 'to' role is not a way", function() { + var graph = valid.replace(iD.Node({id: 't2'})) + .replace(valid.entity('r').replaceMember({id: 't'}, {id: 't2', type: 'node'})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions whose 'via' role is not a node", function() { + var graph = valid.replace(iD.Way({id: 'v2'})) + .replace(valid.entity('r').replaceMember({id: 'v'}, {id: 'v2', type: 'way'})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions whose 'from' role does not start or end with the via node", function() { + var graph = valid.replace(valid.entity('f').update({nodes: ['o']})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions whose 'to' role does not start or end with the via node", function() { + var graph = valid.replace(valid.entity('t').update({nodes: ['o']})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions whose 'from' role has less than two nodes", function() { + var graph = valid.replace(valid.entity('f').update({nodes: ['v']})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restrictions whose 'to' role has less than two nodes", function() { + var graph = valid.replace(valid.entity('t').update({nodes: ['v']})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + + it("ignores restriction subtypes", function() { + var graph = valid.replace(valid.entity('r').update({tags: {type: 'restriction:hgv'}})); + expect(restrictions.turns(graph, ['f'])).to.eql([]); + }); + }); + + describe("#datum", function() { + function projection(x) { return x; } + + it("calculates the angle of a forward 'to' role", function() { + // w---x--->y + // | + // u====>v + // From = to - via v + + var graph = iD.Graph({ + 'u': iD.Node({id: 'u', loc: [0, 0]}), + 'v': iD.Node({id: 'v', loc: [1, 0]}), + 'w': iD.Node({id: 'w', loc: [1, 1]}), + 'x': iD.Node({id: 'w', loc: [2, 1]}), + 'y': iD.Node({id: 'w', loc: [3, 1]}), + '=': iD.Way({id: '=', nodes: ['u', 'v']}), + '-': iD.Way({id: '-', nodes: ['v', 'w', 'x', 'y']}), + 'r': iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + { role: 'via', id: 'v', type: 'node' }, + { role: 'from', id: '=', type: 'way' }, + { role: 'to', id: '-', type: 'way' } + ]}) + }); + + expect(restrictions.datum(graph, graph.entity('='), graph.entity('r'), projection).angle).to.eql(Math.PI / 2); + }); + + it("calculates the angle of a reverse 'to' role", function() { + // w<---x---y + // | + // u====>v + // From = to - via v + + var graph = iD.Graph({ + 'u': iD.Node({id: 'u', loc: [0, 0]}), + 'v': iD.Node({id: 'v', loc: [1, 0]}), + 'w': iD.Node({id: 'w', loc: [1, 1]}), + 'x': iD.Node({id: 'w', loc: [2, 1]}), + 'y': iD.Node({id: 'w', loc: [3, 1]}), + '=': iD.Way({id: '=', nodes: ['u', 'v']}), + '-': iD.Way({id: '-', nodes: ['y', 'x', 'w', 'v']}), + 'r': iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + { role: 'via', id: 'v', type: 'node' }, + { role: 'from', id: '=', type: 'way' }, + { role: 'to', id: '-', type: 'way' } + ]}) + }); + + expect(restrictions.datum(graph, graph.entity('='), graph.entity('r'), projection).angle).to.eql(Math.PI / 2); + }); + }); +});