From f39ae89177a5b077ed13e36c34b07b87716034b3 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Fri, 9 May 2014 14:58:27 -0700 Subject: [PATCH] Render turns of selected way --- css/map.css | 12 +++ index.html | 2 +- js/id/geo/turn.js | 22 ++++- js/id/svg/restrictions.js | 75 --------------- js/id/svg/turns.js | 31 +++++++ js/id/ui/preset/restrictions.js | 39 ++++++-- test/index.html | 2 +- test/spec/geo/turn.js | 160 +++++++++++++++++++------------- test/spec/svg/restrictions.js | 149 ----------------------------- 9 files changed, 197 insertions(+), 295 deletions(-) delete mode 100644 js/id/svg/restrictions.js create mode 100644 js/id/svg/turns.js diff --git a/css/map.css b/css/map.css index fd4509065..f3c540cc0 100644 --- a/css/map.css +++ b/css/map.css @@ -945,6 +945,18 @@ text.point { font-size: 10px; } +/* Turns */ + +g.turn path { + stroke: green; + stroke-width: 10px; + stroke-linecap: round; +} + +g.turn.restricted path { + stroke: red; +} + /* Cursors */ #map { diff --git a/index.html b/index.html index aee983d3b..7f192a48d 100644 --- a/index.html +++ b/index.html @@ -64,9 +64,9 @@ - + diff --git a/js/id/geo/turn.js b/js/id/geo/turn.js index 296a43e31..5776000e9 100644 --- a/js/id/geo/turn.js +++ b/js/id/geo/turn.js @@ -1,3 +1,23 @@ +iD.geo.Turn = function(turn) { + turn = _.clone(turn); + + turn.key = function() { + var components = [turn.from, turn.to, turn.via, turn.toward]; + if (turn.restriction) + components.push(turn.restriction); + return components.map(iD.Entity.key).join('-'); + }; + + turn.angle = function(projection) { + var v = projection(turn.via.loc), + t = projection(turn.toward.loc); + + return Math.atan2(t[1] - v[1], t[0] - v[0]); + }; + + return turn; +}; + iD.geo.turns = function(graph, entityID) { var way = graph.entity(entityID); if (way.type !== 'way' || !way.tags.highway || way.isArea()) @@ -19,7 +39,7 @@ iD.geo.turns = function(graph, entityID) { } }); - return turn; + return iD.geo.Turn(turn); } var turns = []; diff --git a/js/id/svg/restrictions.js b/js/id/svg/restrictions.js deleted file mode 100644 index 66c8289c5..000000000 --- a/js/id/svg/restrictions.js +++ /dev/null @@ -1,75 +0,0 @@ -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/js/id/svg/turns.js b/js/id/svg/turns.js new file mode 100644 index 000000000..f86518ce3 --- /dev/null +++ b/js/id/svg/turns.js @@ -0,0 +1,31 @@ +iD.svg.Turns = function(projection) { + return function(surface, graph, wayID) { + var turns = wayID ? iD.geo.turns(graph, wayID) : []; + + var groups = surface.select('.layer-hit').selectAll('g.turn') + .data(turns, function(turn) { return turn.key(); }); + + var enter = groups.enter().append('g') + .attr('class', 'turn'); + + enter.append('path') + .attr('class', 'turn') + .attr('d', function() { + return 'M20 0 L50 0 M40 10 L50 0 M40 -10 L50 0'; + }); + + groups + .classed('restricted', function(turn) { + return turn.restriction; + }) + .attr('transform', function(turn) { + return iD.svg.PointTransform(projection)(turn.via) + + 'rotate(' + turn.angle(projection) * 180 / Math.PI + ')'; + }); + + groups.exit() + .remove(); + + return this; + }; +}; diff --git a/js/id/ui/preset/restrictions.js b/js/id/ui/preset/restrictions.js index a6d12953a..9d72bd406 100644 --- a/js/id/ui/preset/restrictions.js +++ b/js/id/ui/preset/restrictions.js @@ -1,6 +1,7 @@ iD.ui.preset.restrictions = function(field, context) { var event = d3.dispatch('change'), - entity; + entity, + selectedID; function restrictions(selection) { var wrap = selection.selectAll('.preset-input-wrap') @@ -34,7 +35,8 @@ iD.ui.preset.restrictions = function(field, context) { entities = [], graph = context.graph(), lines = iD.svg.Lines(projection, context), - vertices = iD.svg.Vertices(projection, context); + vertices = iD.svg.Vertices(projection, context), + turns = iD.svg.Turns(projection, context); if (entity) { entities = graph.parentWays(entity).filter(function (parent) { @@ -44,9 +46,32 @@ iD.ui.preset.restrictions = function(field, context) { entities.push(entity); } + if (!selectedID && entities.length) { + selectedID = entities[0].id; + } + surface .call(vertices, graph, entities, filter, extent, z) - .call(lines, graph, entities, filter); + .call(lines, graph, entities, filter) + .call(turns, graph, selectedID); + + surface.on('click.select', function() { + var datum = d3.event.target.__data__; + if (datum instanceof iD.Entity) { + selectedID = datum.id; + render(); + } + }); + + surface + .selectAll('.selected') + .classed('selected', false); + + if (selectedID) { + surface + .selectAll('.' + selectedID) + .classed('selected', true); + } context.history() .on('change.restrictions', render); @@ -59,12 +84,14 @@ iD.ui.preset.restrictions = function(field, context) { } } - restrictions.tags = function() {}; - restrictions.entity = function(_) { - entity = _; + if (!entity || entity.id !== _.id) { + selectedID = null; + entity = _; + } }; + restrictions.tags = function() {}; restrictions.focus = function() {}; return d3.rebind(restrictions, event, 'on'); diff --git a/test/index.html b/test/index.html index 85dfeba68..708e20622 100644 --- a/test/index.html +++ b/test/index.html @@ -258,7 +258,7 @@ - + diff --git a/test/spec/geo/turn.js b/test/spec/geo/turn.js index 4d0c87cac..44f95dc92 100644 --- a/test/spec/geo/turn.js +++ b/test/spec/geo/turn.js @@ -1,4 +1,23 @@ +describe('iD.geo.Turn', function() { + describe('#angle', function() { + it("calculates the angle of via to toward", function() { + function projection(x) { return x; } + + var turn = iD.geo.Turn({ + via: iD.Node({id: 'v', loc: [1, 0]}), + toward: iD.Node({id: 'w', loc: [1, 1]}) + }); + + expect(turn.angle(projection)).to.eql(Math.PI / 2); + }); + }); +}); + describe("iD.geo.turns", function() { + function properties(turns) { + return turns.map(function (turn) { return _.pick(turn, 'from', 'to', 'via', 'toward', 'restriction') }); + } + it("returns an empty array for non-ways", function() { var graph = iD.Graph([ iD.Node({id: 'n'}) @@ -73,13 +92,15 @@ describe("iD.geo.turns", function() { it("permits turns onto a way forward", function() { // u====v--->w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -90,13 +111,15 @@ describe("iD.geo.turns", function() { it("permits turns onto a way backward", function() { // u====v<---w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -111,14 +134,16 @@ describe("iD.geo.turns", function() { // | // x var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', 'v', 'x'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -134,13 +159,15 @@ describe("iD.geo.turns", function() { it("permits turns from a oneway forward", function() { // u===>v----w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -151,13 +178,15 @@ describe("iD.geo.turns", function() { it("permits turns from a reverse oneway backward", function() { // u<===v----w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['v', 'u'], tags: {highway: 'residential', oneway: '-1'}}), + iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -192,13 +221,15 @@ describe("iD.geo.turns", function() { it("permits turns onto a oneway forward", function() { // u====v--->w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential', oneway: 'yes'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -209,13 +240,15 @@ describe("iD.geo.turns", function() { it("permits turns onto a reverse oneway backward", function() { // u====v<---w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential', oneway: '-1'}}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -250,18 +283,20 @@ describe("iD.geo.turns", function() { it("restricts turns with a restriction relation", function() { // u====v--->w var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}), - 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([{ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}), + iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + {id: '=', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: 'v', role: 'via', type: 'node'} + ]}) + ]), + turns = iD.geo.turns(graph, '='); + + expect(properties(turns)).to.eql([{ from: graph.entity('='), to: graph.entity('-'), via: graph.entity('v'), @@ -270,6 +305,7 @@ describe("iD.geo.turns", function() { }]); }); + // 'no' vs 'only' // U-turns // Self-intersections // Split point diff --git a/test/spec/svg/restrictions.js b/test/spec/svg/restrictions.js index 38f3bf1b9..5d4f5e5e3 100644 --- a/test/spec/svg/restrictions.js +++ b/test/spec/svg/restrictions.js @@ -1,151 +1,2 @@ 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([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: 'f', nodes: ['u', 'v']}), - iD.Way({id: 't', nodes: ['v', 'w']}), - 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([ - iD.Node({id: 'u', loc: [0, 0]}), - iD.Node({id: 'v', loc: [1, 0]}), - iD.Node({id: 'w', loc: [1, 1]}), - iD.Node({id: 'x', loc: [2, 1]}), - iD.Node({id: 'y', loc: [3, 1]}), - iD.Way({id: '=', nodes: ['u', 'v']}), - iD.Way({id: '-', nodes: ['v', 'w', 'x', 'y']}), - 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([ - iD.Node({id: 'u', loc: [0, 0]}), - iD.Node({id: 'v', loc: [1, 0]}), - iD.Node({id: 'w', loc: [1, 1]}), - iD.Node({id: 'x', loc: [2, 1]}), - iD.Node({id: 'y', loc: [3, 1]}), - iD.Way({id: '=', nodes: ['u', 'v']}), - iD.Way({id: '-', nodes: ['y', 'x', 'w', 'v']}), - 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); - }); - }); });