Render turns of selected way

This commit is contained in:
John Firebaugh
2014-05-09 14:58:27 -07:00
parent 495fcc1b11
commit f39ae89177
9 changed files with 197 additions and 295 deletions
+12
View File
@@ -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 {
+1 -1
View File
@@ -64,9 +64,9 @@
<script src="js/id/svg/lines.js"></script>
<script src="js/id/svg/midpoints.js"></script>
<script src="js/id/svg/points.js"></script>
<script src="js/id/svg/restrictions.js"></script>
<script src="js/id/svg/surface.js"></script>
<script src="js/id/svg/tag_classes.js"></script>
<script src="js/id/svg/turns.js"></script>
<script src="js/id/svg/vertices.js"></script>
<script src="js/id/ui.js"></script>
+21 -1
View File
@@ -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 = [];
-75
View File
@@ -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;
};
+31
View File
@@ -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;
};
};
+33 -6
View File
@@ -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');
+1 -1
View File
@@ -258,7 +258,7 @@
<script src="spec/svg/points.js"></script>
<script src="spec/svg/vertices.js"></script>
<script src="spec/svg/tag_classes.js"></script>
<script src="spec/svg/restrictions.js"></script>
<script src="spec/svg/turns.js"></script>
<script src="spec/ui/inspector.js"></script>
<script src="spec/ui/raw_tag_editor.js"></script>
+98 -62
View File
@@ -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
-149
View File
@@ -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);
});
});
});