diff --git a/data/core.yaml b/data/core.yaml
index 2c4edca74..048a433dc 100644
--- a/data/core.yaml
+++ b/data/core.yaml
@@ -145,6 +145,10 @@ en:
multiple: "Split {n} lines/area boundaries."
not_eligible: Lines can't be split at their beginning or end.
multiple_ways: There are too many lines here to split.
+ restriction:
+ annotation:
+ create: Added a turn restriction
+ delete: Deleted a turn restriction
undo:
tooltip: "Undo: {action}"
nothing: Nothing to undo.
diff --git a/dist/locales/en.json b/dist/locales/en.json
index 2c5a525fe..cd29e7272 100644
--- a/dist/locales/en.json
+++ b/dist/locales/en.json
@@ -182,6 +182,12 @@
},
"not_eligible": "Lines can't be split at their beginning or end.",
"multiple_ways": "There are too many lines here to split."
+ },
+ "restriction": {
+ "annotation": {
+ "create": "Added a turn restriction",
+ "delete": "Deleted a turn restriction"
+ }
}
},
"undo": {
diff --git a/index.html b/index.html
index aa156ee5e..48cd10b82 100644
--- a/index.html
+++ b/index.html
@@ -138,6 +138,7 @@
+
@@ -151,13 +152,14 @@
-
-
-
-
+
+
+
+
+
diff --git a/js/id/actions/restrict_turn.js b/js/id/actions/restrict_turn.js
new file mode 100644
index 000000000..e6786e6e6
--- /dev/null
+++ b/js/id/actions/restrict_turn.js
@@ -0,0 +1,88 @@
+// Create a restriction relation for `turn`, which must have the following structure:
+//
+// {
+// from: { node: , way: },
+// via: { node: },
+// to: { node: , way: },
+// restriction: <'no_right_turn', 'no_left_turn', etc.>
+// }
+//
+// This specifies a restriction of type `restriction` when traveling from
+// `from.node` in `from.way` toward `to.node` in `to.way` via `via.node`.
+// (The action does not check that these entities form a valid intersection.)
+//
+// If `restriction` is not provided, it is automatically determined by the
+// angle of the turn:
+//
+// 0-23 degrees: no_u_turn
+// 23-158 degrees: no_right_turn
+// 158-202 degrees: no_straight_on
+// 202-326 degrees: no_left_turn
+// 336-360 degrees: no_u_turn
+//
+// If necessary, the `from` and `to` ways are split. In these cases, `from.node`
+// and `to.node` are used to determine which portion of the split ways become
+// members of the restriction.
+//
+// For testing convenience, accepts an ID to assign to the new relation.
+// Normally, this will be undefined and the relation will automatically
+// be assigned a new ID.
+//
+iD.actions.RestrictTurn = function(turn, projection, restrictionId) {
+ return function(graph) {
+ var from = graph.entity(turn.from.way),
+ via = graph.entity(turn.via.node),
+ to = graph.entity(turn.to.way);
+
+ if (!from.affix(via.id)) {
+ var newFromId = turn.from.newID || iD.Way().id;
+
+ graph = iD.actions.Split(via.id, [newFromId])
+ .limitWays([from.id])(graph);
+
+ var newFrom = graph.entity(newFromId);
+ if (newFrom.nodes.indexOf(turn.from.node) !== -1)
+ from = newFrom;
+ }
+
+ if (turn.from.way === turn.to.way) {
+ to = from;
+ } else if (!to.affix(via.id)) {
+ var newToId = turn.to.newID || iD.Way().id;
+
+ graph = iD.actions.Split(via.id, [newToId])
+ .limitWays([to.id])(graph);
+
+ var newTo = graph.entity(newToId);
+ if (newTo.nodes.indexOf(turn.to.node) !== -1)
+ to = newTo;
+ }
+
+ return graph.replace(iD.Relation({
+ id: restrictionId,
+ tags: {
+ type: 'restriction',
+ restriction: turn.restriction || guessRestriction()
+ },
+ members: [
+ {id: from.id, type: 'way', role: 'from'},
+ {id: via.id, type: 'node', role: 'via'},
+ {id: to.id, type: 'way', role: 'to'}
+ ]
+ }));
+
+ function guessRestriction() {
+ var angle = iD.geo.angle(via, graph.entity(turn.from.node), projection) -
+ iD.geo.angle(via, graph.entity(turn.to.node), projection);
+
+ if (angle > 158 || angle < -158)
+ return 'no_straight_on';
+ if (angle > 23)
+ return 'no_right_turn';
+ if (angle < -22)
+ return 'no_left_turn';
+
+ return 'no_u_turn';
+ }
+ };
+};
diff --git a/js/id/actions/unrestrict_turn.js b/js/id/actions/unrestrict_turn.js
new file mode 100644
index 000000000..f57186d7a
--- /dev/null
+++ b/js/id/actions/unrestrict_turn.js
@@ -0,0 +1,23 @@
+// Remove the effects of `turn.restriction` on `turn`, which must have the
+// following structure:
+//
+// {
+// from: { node: , way: },
+// via: { node: },
+// to: { node: , way: },
+// restriction:
+// }
+//
+// In the simple case, `restriction` is a reference to a `no_*` restriction
+// on the turn itself. In this case, it is simply deleted.
+//
+// The more complex case is where `restriction` references an `only_*`
+// restriction on a different turn in the same intersection. In that case,
+// that restriction is also deleted, but at the same time restrictions on
+// the turns other than the first two are created.
+//
+iD.actions.UnrestrictTurn = function(turn) {
+ return function(graph) {
+ return iD.actions.DeleteRelation(turn.restriction)(graph);
+ };
+};
diff --git a/js/id/geo.js b/js/id/geo.js
index 2fa2e2ace..f667ca100 100644
--- a/js/id/geo.js
+++ b/js/id/geo.js
@@ -33,6 +33,14 @@ iD.geo.edgeEqual = function(a, b) {
(a[0] === b[1] && a[1] === b[0]);
};
+// Return the counterclockwise angle in the range (-180, 180) degrees
+// between the positive X axis and the line intersecting a and b.
+iD.geo.angle = function(a, b, projection) {
+ a = projection(a.loc);
+ b = projection(b.loc);
+ return Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
+};
+
// Choose the edge with the minimal distance from `point` to its orthogonal
// projection onto that edge, if such a projection exists, or the distance to
// the closest vertex on that edge. Returns an object with the `index` of the
diff --git a/js/id/geo/intersection.js b/js/id/geo/intersection.js
index c53e6db2a..c76405e08 100644
--- a/js/id/geo/intersection.js
+++ b/js/id/geo/intersection.js
@@ -1,21 +1,7 @@
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;
+ if (!(this instanceof iD.geo.Turn))
+ return new iD.geo.Turn(turn);
+ _.extend(this, turn);
};
iD.geo.Intersection = function(graph, vertexId) {
@@ -33,8 +19,8 @@ iD.geo.Intersection = function(graph, vertexId) {
highways.push(way);
} else {
var idx = _.indexOf(way.nodes, vertex.id, 1),
- wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, idx + 1)}),
- wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(idx)});
+ wayA = iD.Way({id: way.id + '.a', tags: way.tags, nodes: way.nodes.slice(0, idx + 1)}),
+ wayB = iD.Way({id: way.id + '.b', tags: way.tags, nodes: way.nodes.slice(idx)});
graph = graph.replace(wayA);
graph = graph.replace(wayB);
@@ -60,7 +46,7 @@ iD.geo.Intersection = function(graph, vertexId) {
return [];
function withRestriction(turn) {
- graph.parentRelations(turn.from).forEach(function(relation) {
+ graph.parentRelations(graph.entity(turn.from.way)).forEach(function(relation) {
if (relation.tags.type !== 'restriction')
return;
@@ -68,17 +54,19 @@ iD.geo.Intersection = function(graph, vertexId) {
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;
+ if (f && f.id === turn.from.way &&
+ v && v.id === turn.via.node &&
+ t && t.id === turn.to.way) {
+ turn.restriction = relation.id;
}
});
return iD.geo.Turn(turn);
}
- var turns = [];
+ var from = {node: way.nodes[way.first() === vertex.id ? 1 : way.nodes.length - 2], way: way.id},
+ via = {node: vertex.id},
+ turns = [];
highways.forEach(function(parent) {
if (parent === way)
@@ -89,20 +77,18 @@ iD.geo.Intersection = function(graph, vertexId) {
// backward
if (parent.first() !== vertex.id && parent.tags.oneway !== 'yes') {
turns.push(withRestriction({
- from: way,
- to: parent,
- via: vertex,
- toward: graph.entity(parent.nodes[index - 1])
+ from: from,
+ via: via,
+ to: {node: parent.nodes[index - 1], way: parent.id.split('.')[0]}
}));
}
// forward
if (parent.last() !== vertex.id && parent.tags.oneway !== '-1') {
turns.push(withRestriction({
- from: way,
- to: parent,
- via: vertex,
- toward: graph.entity(parent.nodes[index + 1])
+ from: from,
+ via: via,
+ to: {node: parent.nodes[index + 1], way: parent.id.split('.')[0]}
}));
}
});
diff --git a/js/id/svg/turns.js b/js/id/svg/turns.js
index dce3ca924..f20f20a3d 100644
--- a/js/id/svg/turns.js
+++ b/js/id/svg/turns.js
@@ -1,7 +1,7 @@
iD.svg.Turns = function(projection) {
return function(surface, graph, turns) {
var groups = surface.select('.layer-hit').selectAll('g.turn')
- .data(turns, function(turn) { return turn.key(); });
+ .data(turns);
var enter = groups.enter().append('g')
.attr('class', 'turn');
@@ -17,10 +17,14 @@ iD.svg.Turns = function(projection) {
return turn.restriction;
})
.attr('transform', function(turn) {
- return iD.svg.PointTransform(projection)(turn.via) +
- 'rotate(' + turn.angle(projection) * 180 / Math.PI + ')';
+ var v = graph.entity(turn.via.node),
+ t = graph.entity(turn.to.node);
+ return iD.svg.PointTransform(projection)(v) +
+ 'rotate(' + iD.geo.angle(v, t, projection) + ')';
});
+ groups.select('path'); // Propagate updated data.
+
groups.exit()
.remove();
diff --git a/js/id/ui/preset/restrictions.js b/js/id/ui/preset/restrictions.js
index c604d04bd..61baebc88 100644
--- a/js/id/ui/preset/restrictions.js
+++ b/js/id/ui/preset/restrictions.js
@@ -7,8 +7,6 @@ iD.ui.preset.restrictions = function(field, context) {
var wrap = selection.selectAll('.preset-input-wrap')
.data([0]);
- // Enter
-
var enter = wrap.enter().append('div')
.attr('class', 'preset-input-wrap');
@@ -48,6 +46,16 @@ iD.ui.preset.restrictions = function(field, context) {
if (datum instanceof iD.Entity) {
selectedID = datum.id;
render();
+ } else if (datum instanceof iD.geo.Turn) {
+ if (datum.restriction) {
+ context.perform(
+ iD.actions.UnrestrictTurn(datum, projection),
+ t('operations.restriction.annotation.delete'));
+ } else {
+ context.perform(
+ iD.actions.RestrictTurn(datum, projection),
+ t('operations.restriction.annotation.create'));
+ }
}
});
diff --git a/test/index.html b/test/index.html
index 5b6469948..dec049083 100644
--- a/test/index.html
+++ b/test/index.html
@@ -118,6 +118,7 @@
+
@@ -131,13 +132,14 @@
-
-
-
-
+
+
+
+
+
@@ -230,8 +232,10 @@
+
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 0f83951a5..b22e812ee 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -47,6 +47,7 @@
+
diff --git a/test/spec/actions/restrict_turn.js b/test/spec/actions/restrict_turn.js
new file mode 100644
index 000000000..302f14cad
--- /dev/null
+++ b/test/spec/actions/restrict_turn.js
@@ -0,0 +1,239 @@
+describe("iD.actions.RestrictTurn", function() {
+ var projection = d3.geo.mercator().scale(250 / Math.PI);
+
+ it('adds a restriction to an unrestricted turn', function() {
+ // u====*--->w
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Way({id: '=', nodes: ['u', '*']}),
+ iD.Way({id: '-', nodes: ['*', 'w']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'},
+ restriction: 'no_right_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '-', type: 'way'});
+ });
+
+ it('splits the from way when necessary (forward)', function() {
+ // u====*===>w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Node({id: 'x'}),
+ iD.Way({id: '=', nodes: ['u', '*', 'w']}),
+ iD.Way({id: '-', nodes: ['*', 'x']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'x', way: '-'},
+ restriction: 'no_right_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '-', type: 'way'});
+ });
+
+ it('splits the from way when necessary (backward)', function() {
+ // u====*===>w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Node({id: 'x'}),
+ iD.Way({id: '=', nodes: ['u', '*', 'w']}),
+ iD.Way({id: '-', nodes: ['*', 'x']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'w', way: '=', newID: '=='},
+ via: {node: '*'},
+ to: {node: 'x', way: '-'},
+ restriction: 'no_left_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_left_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '==', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '-', type: 'way'});
+ });
+
+ it('splits the to way when necessary (forward)', function() {
+ // u====*===>w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Node({id: 'x'}),
+ iD.Way({id: '=', nodes: ['u', '*', 'w']}),
+ iD.Way({id: '-', nodes: ['*', 'x']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'x', way: '-'},
+ via: {node: '*'},
+ to: {node: 'w', way: '=', newID: '=='},
+ restriction: 'no_right_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '-', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '==', type: 'way'});
+ });
+
+ it('splits the to way when necessary (backward)', function() {
+ // u====*===>w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Node({id: 'x'}),
+ iD.Way({id: '=', nodes: ['u', '*', 'w']}),
+ iD.Way({id: '-', nodes: ['*', 'x']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'x', way: '-'},
+ via: {node: '*'},
+ to: {node: 'u', way: '='},
+ restriction: 'no_left_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_left_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '-', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '=', type: 'way'});
+ });
+
+ it('splits the from/to way of a U-turn (forward)', function() {
+ // u====*===>w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Node({id: 'x'}),
+ iD.Way({id: '=', nodes: ['u', '*', 'w']}),
+ iD.Way({id: '-', nodes: ['*', 'x']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'u', way: '='},
+ restriction: 'no_u_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_u_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '=', type: 'way'});
+ });
+
+ it('splits the from/to way of a U-turn (backward)', function() {
+ // u====*===>w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Node({id: 'x'}),
+ iD.Way({id: '=', nodes: ['u', '*', 'w']}),
+ iD.Way({id: '-', nodes: ['*', 'x']})
+ ]),
+ action = iD.actions.RestrictTurn({
+ from: {node: 'w', way: '=', newID: '=='},
+ via: {node: '*'},
+ to: {node: 'w', way: '=', newID: '~~'},
+ restriction: 'no_u_turn'
+ }, projection, 'r');
+
+ graph = action(graph);
+
+ var r = graph.entity('r');
+ expect(r.tags).to.eql({type: 'restriction', restriction: 'no_u_turn'});
+ expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '==', type: 'way'});
+ expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'});
+ expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '==', type: 'way'});
+ });
+
+ it('guesses the restriction type based on the turn angle', function() {
+ // u====*~~~~w
+ // |
+ // x
+ var graph = iD.Graph([
+ iD.Node({id: 'u', loc: [-1, 0]}),
+ iD.Node({id: '*', loc: [ 0, 0]}),
+ iD.Node({id: 'w', loc: [ 0, 1]}),
+ iD.Node({id: 'x', loc: [ 0, -1]}),
+ iD.Way({id: '=', nodes: ['u', '*']}),
+ iD.Way({id: '-', nodes: ['*', 'x']}),
+ iD.Way({id: '~', nodes: ['*', 'w']})
+ ]);
+
+ var r = iD.actions.RestrictTurn({
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'x', way: '-'}
+ }, projection, 'r')(graph);
+ expect(r.entity('r').tags.restriction).to.equal('no_right_turn');
+
+ var l = iD.actions.RestrictTurn({
+ from: {node: 'x', way: '-'},
+ via: {node: '*'},
+ to: {node: 'u', way: '='}
+ }, projection, 'r')(graph);
+ expect(l.entity('r').tags.restriction).to.equal('no_left_turn');
+
+ var s = iD.actions.RestrictTurn({
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '~'}
+ }, projection, 'r')(graph);
+ expect(s.entity('r').tags.restriction).to.equal('no_straight_on');
+
+ var u = iD.actions.RestrictTurn({
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'u', way: '='}
+ }, projection, 'r')(graph);
+ expect(u.entity('r').tags.restriction).to.equal('no_u_turn');
+ });
+});
diff --git a/test/spec/actions/unrestrict_turn.js b/test/spec/actions/unrestrict_turn.js
new file mode 100644
index 000000000..f7c5a6830
--- /dev/null
+++ b/test/spec/actions/unrestrict_turn.js
@@ -0,0 +1,24 @@
+describe("iD.actions.UnrestrictTurn", function() {
+ it('removes a restriction from a restricted turn', function() {
+ // u====*--->w
+ var graph = iD.Graph([
+ iD.Node({id: 'u'}),
+ iD.Node({id: '*'}),
+ iD.Node({id: 'w'}),
+ iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}),
+ iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}),
+ iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [
+ {id: '=', role: 'from', type: 'way'},
+ {id: '-', role: 'to', type: 'way'},
+ {id: '*', role: 'via', type: 'node'}
+ ]})
+ ]),
+ action = iD.actions.UnrestrictTurn({
+ restriction: 'r'
+ });
+
+ graph = action(graph);
+
+ expect(graph.hasEntity('r')).to.be.undefined;
+ });
+});
diff --git a/test/spec/geo/intersection.js b/test/spec/geo/intersection.js
index c77b50c78..68def534d 100644
--- a/test/spec/geo/intersection.js
+++ b/test/spec/geo/intersection.js
@@ -1,18 +1,3 @@
-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.Intersection", function() {
describe('highways', function() {
it('excludes non-highways', function() {
@@ -64,27 +49,11 @@ describe("iD.geo.Intersection", function() {
iD.Node({id: 'w'}),
iD.Way({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'residential'}})
]);
- expect(_.pluck(iD.geo.Intersection(graph, '*').highways, 'id')).to.eql(['=-a', '=-b']);
+ expect(_.pluck(iD.geo.Intersection(graph, '*').highways, 'id')).to.eql(['=.a', '=.b']);
});
});
describe('#turns', function() {
- function ids(turns) {
- return turns.map(function (turn) {
- var result = {
- from: turn.from.id,
- to: turn.to.id,
- via: turn.via.id,
- toward: turn.toward.id
- };
-
- if (turn.restriction)
- result.restriction = turn.restriction.id;
-
- return result;
- });
- }
-
it("permits turns onto a way forward", function() {
// u====*--->w
var graph = iD.Graph([
@@ -96,11 +65,10 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{
- from: '=',
- to: '-',
- via: '*',
- toward: 'w'
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
}]);
});
@@ -115,7 +83,11 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{from: '=', to: '-', via: '*', toward: 'w'}]);
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
+ }]);
});
it("permits turns onto a way in both directions", function() {
@@ -134,10 +106,15 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([
- {from: '=', to: '--a', via: '*', toward: 'w'},
- {from: '=', to: '--b', via: '*', toward: 'x'}
- ]);
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
+ }, {
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'x', way: '-'}
+ }]);
});
it("permits turns from a oneway forward", function() {
@@ -151,7 +128,11 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{from: '=', to: '-', via: '*', toward: 'w'}]);
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
+ }]);
});
it("permits turns from a reverse oneway backward", function() {
@@ -165,7 +146,11 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{from: '=', to: '-', via: '*', toward: 'w'}]);
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
+ }]);
});
it("omits turns from a oneway backward", function() {
@@ -203,7 +188,11 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{from: '=', to: '-', via: '*', toward: 'w'}]);
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
+ }]);
});
it("permits turns onto a reverse oneway backward", function() {
@@ -217,7 +206,11 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{from: '=', to: '-', via: '*', toward: 'w'}]);
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'}
+ }]);
});
it("omits turns onto a oneway backward", function() {
@@ -260,11 +253,10 @@ describe("iD.geo.Intersection", function() {
]),
turns = iD.geo.Intersection(graph, '*').turns('=');
- expect(ids(turns)).to.eql([{
- from: '=',
- to: '-',
- via: '*',
- toward: 'w',
+ expect(turns).to.eql([{
+ from: {node: 'u', way: '='},
+ via: {node: '*'},
+ to: {node: 'w', way: '-'},
restriction: 'r'
}]);
});
@@ -273,4 +265,5 @@ describe("iD.geo.Intersection", function() {
// 'no' vs 'only'
// U-turns
// Self-intersections
+ // Incomplete relations
});