Cleanup docs and tests for actionRestrictTurn / actionUnrestrictTurn

- actionRestrictTurn will no longer "infer" the turn type
- restrictionType *must* be passed in - this is ok because the only code
  we use this action (restrictions.js) already has inferred the type
- this simplifies what the action actually does
- moved the tests from restrict_turn.js that were really just testing
  the restriction type inferrence over to intersection.js
  (and added a few more tests for iD.osmInferRestriction)
This commit is contained in:
Bryan Housel
2018-02-28 23:55:59 -05:00
parent e1cf49eaff
commit 241159b547
6 changed files with 239 additions and 317 deletions
+10 -20
View File
@@ -1,33 +1,23 @@
import {
osmInferRestriction,
osmRelation
} from '../osm';
import { osmRelation } from '../osm';
// Create a restriction relation for `turn`, which must have the following structure:
// `actionRestrictTurn` creates a turn restriction relation.
//
// {
// from: { node: <node ID>, way: <way ID> },
// via: { node: <node ID>, ways: [<way ID>,<way ID>,...] },
// to: { node: <node ID>, way: <way ID> },
// restriction: <'no_right_turn', 'no_left_turn', etc.>
// }
// `turn` must be an `osmTurn` object
// see osm/intersection.js, pathToTurn()
//
// This specifies a restriction of type `restriction` when traveling from
// `from.node` in `from.way` toward `to.node` in `to.way` via `via.node` OR `via.ways`.
// `turn.from.way` toward `turn.to.way` via `turn.via.node` OR `turn.via.ways`.
// (The action does not check that these entities form a valid intersection.)
//
// If `restriction` is not provided, it is automatically determined by
// osmInferRestriction.
//
// From, to, and via ways should be split before calling this action.
// (old versions of the code would split the ways here, but we no longer do it)
//
// 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.
// For testing convenience, accepts a restrictionID to assign to the new
// relation. Normally, this will be undefined and the relation will
// automatically be assigned a new ID.
//
export function actionRestrictTurn(turn, projection, restrictionID) {
export function actionRestrictTurn(turn, restrictionType, restrictionID) {
return function(graph) {
var fromWay = graph.entity(turn.from.way);
@@ -52,7 +42,7 @@ export function actionRestrictTurn(turn, projection, restrictionID) {
id: restrictionID,
tags: {
type: 'restriction',
restriction: turn.restriction || osmInferRestriction(graph, turn, projection)
restriction: restrictionType
},
members: members
}));
+3 -16
View File
@@ -1,23 +1,10 @@
import { actionDeleteRelation } from './delete_relation';
// Remove the effects of `turn.restriction` on `turn`, which must have the
// following structure:
// `actionUnrestrictTurn` deletes a turn restriction relation.
//
// {
// from: { node: <node ID>, way: <way ID> },
// via: { node: <node ID> },
// to: { node: <node ID>, way: <way ID> },
// restrictionID: <relation ID>
// }
//
// In the simple case, `restrictionID` is a reference to a `no_*` restriction
// on the turn itself. In this case, it is simply deleted.
//
// The more complex case is where `restrictionID` 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.
// `turn` must be an `osmTurn` object with a `restrictionID` property.
// see osm/intersection.js, pathToTurn()
//
export function actionUnrestrictTurn(turn) {
return function(graph) {
+11 -10
View File
@@ -339,15 +339,15 @@ export function uiFieldRestrictions(field, context) {
} else if (datum instanceof osmTurn) {
var actions, extraActions, turns, i;
datum.restriction = osmInferRestriction(vgraph, datum, projection);
var restrictionType = osmInferRestriction(vgraph, datum, projection);
if (datum.restrictionID && !datum.direct) {
return;
} else if (datum.restrictionID && !datum.only) { // cycle NO -> ONLY
} else if (datum.restrictionID && !datum.only) { // NO -> ONLY
var datumOnly = _cloneDeep(datum);
datumOnly.only = true;
datumOnly.restriction = datumOnly.restriction.replace(/^no/, 'only');
restrictionType = restrictionType.replace(/^no/, 'only');
// Adding an ONLY restriction should destroy all other direct restrictions from the FROM.
// We will remember them in _oldTurns, and restore them if the user clicks again.
@@ -356,17 +356,18 @@ export function uiFieldRestrictions(field, context) {
_oldTurns = [];
for (i = 0; i < turns.length; i++) {
if (turns[i].direct) {
turns[i].restrictionType = osmInferRestriction(vgraph, turns[i], projection);
_oldTurns.push(turns[i]);
extraActions.push(actionUnrestrictTurn(turns[i], projection));
extraActions.push(actionUnrestrictTurn(turns[i]));
}
}
actions = _intersection.actions.concat(extraActions, [
actionRestrictTurn(datumOnly, projection),
actionRestrictTurn(datumOnly, restrictionType),
t('operations.restriction.annotation.create')
]);
} else if (datum.restrictionID) { // cycle ONLY -> Allowed
} else if (datum.restrictionID) { // ONLY -> Allowed
// Restore whatever restrictions we might have destroyed by cycling thru the ONLY state.
// This relies on the assumption that the intersection was already split up when we
// performed the previous action (NO -> ONLY), so the IDs in _oldTurns shouldn't have changed.
@@ -374,19 +375,19 @@ export function uiFieldRestrictions(field, context) {
extraActions = [];
for (i = 0; i < turns.length; i++) {
if (turns[i].key !== datum.key) {
extraActions.push(actionRestrictTurn(turns[i], projection));
extraActions.push(actionRestrictTurn(turns[i], turns[i].restrictionType));
}
}
_oldTurns = null;
actions = _intersection.actions.concat(extraActions, [
actionUnrestrictTurn(datum, projection),
actionUnrestrictTurn(datum),
t('operations.restriction.annotation.delete')
]);
} else { // cycle Allowed -> NO
} else { // Allowed -> NO
actions = _intersection.actions.concat([
actionRestrictTurn(datum, projection),
actionRestrictTurn(datum, restrictionType),
t('operations.restriction.annotation.create')
]);
}
+62 -270
View File
@@ -1,288 +1,80 @@
describe('iD.actionRestrictTurn', function() {
var projection = d3.geoMercator().scale(250 / Math.PI);
it('adds a via node restriction to an unrestricted turn', function() {
//
// u === * --- w
//
var graph = iD.coreGraph([
iD.osmNode({id: 'u'}),
iD.osmNode({id: '*'}),
iD.osmNode({id: 'w'}),
iD.osmWay({id: '=', nodes: ['u', '*']}),
iD.osmWay({id: '-', nodes: ['*', 'w']})
]);
describe('via node', function() {
var turn = {
from: { node: 'u', way: '=' },
via: { node: '*'},
to: { node: 'w', way: '-' }
};
it('adds a via node restriction to an unrestricted turn', function() {
// u====*--->w
var graph = iD.coreGraph([
iD.osmNode({id: 'u'}),
iD.osmNode({id: '*'}),
iD.osmNode({id: 'w'}),
iD.osmWay({id: '=', nodes: ['u', '*']}),
iD.osmWay({id: '-', nodes: ['*', 'w']})
]);
var action = iD.actionRestrictTurn({
from: {node: 'u', way: '='},
via: {node: '*'},
to: {node: 'w', way: '-'},
restriction: 'no_right_turn'
}, projection, 'r');
var action = iD.actionRestrictTurn(turn, 'no_straight_on', 'r');
graph = action(graph);
graph = action(graph);
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_straight_on'});
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'});
var f = r.memberByRole('from');
expect(f.id).to.eql('=');
expect(f.type).to.eql('way');
var f = r.memberByRole('from');
expect(f.id).to.eql('=');
expect(f.type).to.eql('way');
var v = r.memberByRole('via');
expect(v.id).to.eql('*');
expect(v.type).to.eql('node');
var v = r.memberByRole('via');
expect(v.id).to.eql('*');
expect(v.type).to.eql('node');
var t = r.memberByRole('to');
expect(t.id).to.eql('-');
expect(t.type).to.eql('way');
});
//TODO?
it.skip('infers the restriction type based on the turn angle', function() {
// u====*~~~~w
// |
// x
var graph = iD.coreGraph([
iD.osmNode({id: 'u', loc: [-1, 0]}),
iD.osmNode({id: '*', loc: [ 0, 0]}),
iD.osmNode({id: 'w', loc: [ 1, 0]}),
iD.osmNode({id: 'x', loc: [ 0, -1]}),
iD.osmWay({id: '=', nodes: ['u', '*']}),
iD.osmWay({id: '-', nodes: ['*', 'x']}),
iD.osmWay({id: '~', nodes: ['*', 'w']})
]);
var r1 = iD.actionRestrictTurn({
from: {node: 'u', way: '='},
via: {node: '*'},
to: {node: 'x', way: '-'}
}, projection, 'r')(graph);
expect(r1.entity('r').tags.restriction).to.equal('no_right_turn');
var r2 = iD.actionRestrictTurn({
from: {node: 'x', way: '-'},
via: {node: '*'},
to: {node: 'w', way: '~'}
}, projection, 'r')(graph);
expect(r2.entity('r').tags.restriction).to.equal('no_right_turn');
var l1 = iD.actionRestrictTurn({
from: {node: 'x', way: '-'},
via: {node: '*'},
to: {node: 'u', way: '='}
}, projection, 'r')(graph);
expect(l1.entity('r').tags.restriction).to.equal('no_left_turn');
var l2 = iD.actionRestrictTurn({
from: {node: 'w', way: '~'},
via: {node: '*'},
to: {node: 'x', way: '-'}
}, projection, 'r')(graph);
expect(l2.entity('r').tags.restriction).to.equal('no_left_turn');
var s = iD.actionRestrictTurn({
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.actionRestrictTurn({
from: {node: 'u', way: '='},
via: {node: '*'},
to: {node: 'u', way: '='}
}, projection, 'r')(graph);
expect(u.entity('r').tags.restriction).to.equal('no_u_turn');
});
//TODO?
it.skip('infers no_u_turn from acute angle made by forward oneways', function() {
// *
// / \
// w2/ \w1
// / \
// u x
var graph = iD.coreGraph([
iD.osmNode({id: 'u', loc: [-1, -20]}),
iD.osmNode({id: '*', loc: [ 0, 0]}),
iD.osmNode({id: 'x', loc: [ 1, -20]}),
iD.osmWay({id: 'w1', nodes: ['x', '*'], tags: {oneway: 'yes'}}),
iD.osmWay({id: 'w2', nodes: ['*', 'u'], tags: {oneway: 'yes'}})
]);
var r = iD.actionRestrictTurn({
from: {node: 'x', way: 'w1'},
via: {node: '*'},
to: {node: 'u', way: 'w2'}
}, projection, 'r')(graph);
expect(r.entity('r').tags.restriction).to.equal('no_u_turn');
});
//TODO?
it.skip('infers no_u_turn from acute angle made by reverse oneways', function() {
// *
// / \
// w2/ \w1
// / \
// u x
var graph = iD.coreGraph([
iD.osmNode({id: 'u', loc: [-1, -20]}),
iD.osmNode({id: '*', loc: [ 0, 0]}),
iD.osmNode({id: 'x', loc: [ 1, -20]}),
iD.osmWay({id: 'w1', nodes: ['*', 'x'], tags: {oneway: '-1'}}),
iD.osmWay({id: 'w2', nodes: ['u', '*'], tags: {oneway: '-1'}})
]);
var r = iD.actionRestrictTurn({
from: {node: 'x', way: 'w1'},
via: {node: '*'},
to: {node: 'u', way: 'w2'}
}, projection, 'r')(graph);
expect(r.entity('r').tags.restriction).to.equal('no_u_turn');
});
var t = r.memberByRole('to');
expect(t.id).to.eql('-');
expect(t.type).to.eql('way');
});
describe('via way', function() {
it('adds a via way restriction to an unrestricted turn', function() {
//
// u === v1
// |
// w --- v2
//
var graph = iD.coreGraph([
iD.osmNode({id: 'u'}),
iD.osmNode({id: 'v1'}),
iD.osmNode({id: 'v2'}),
iD.osmNode({id: 'w'}),
iD.osmWay({id: '=', nodes: ['u', 'v1']}),
iD.osmWay({id: '|', nodes: ['v1', 'v2']}),
iD.osmWay({id: '-', nodes: ['v2', 'w']})
]);
it('adds a via way restriction to an unrestricted turn', function() {
// u ==== VIA ---> w
var graph = iD.coreGraph([
iD.osmNode({id: 'u'}),
iD.osmNode({id: 'V1'}),
iD.osmNode({id: 'V2'}),
iD.osmNode({id: 'w'}),
iD.osmWay({id: '=', nodes: ['u', 'V1']}),
iD.osmWay({id: 'VIA', nodes: ['V1', 'V2']}),
iD.osmWay({id: '-', nodes: ['V2', 'w']})
]);
var action = iD.actionRestrictTurn({
from: {node: 'u', way: '='},
via: {ways: ['VIA']},
to: {node: 'w', way: '-'},
restriction: 'no_u_turn'
}, projection, 'r');
var turn = {
from: { node: 'u', way: '=' },
via: { ways: ['|'] },
to: { node: 'w', way: '-' }
};
graph = action(graph);
var action = iD.actionRestrictTurn(turn, 'no_u_turn', 'r');
graph = action(graph);
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_u_turn'});
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_u_turn'});
var f = r.memberByRole('from');
expect(f.id).to.eql('=');
expect(f.type).to.eql('way');
var f = r.memberByRole('from');
expect(f.id).to.eql('=');
expect(f.type).to.eql('way');
var v = r.memberByRole('via');
expect(v.id).to.eql('VIA');
expect(v.type).to.eql('way');
var v = r.memberByRole('via');
expect(v.id).to.eql('|');
expect(v.type).to.eql('way');
var t = r.memberByRole('to');
expect(t.id).to.eql('-');
expect(t.type).to.eql('way');
});
// TODO?
// it('infers the restriction type based on the turn angle', function() {
// // u====*~~~~w
// // |
// // x
// var graph = iD.coreGraph([
// iD.osmNode({id: 'u', loc: [-1, 0]}),
// iD.osmNode({id: '*', loc: [ 0, 0]}),
// iD.osmNode({id: 'w', loc: [ 1, 0]}),
// iD.osmNode({id: 'x', loc: [ 0, -1]}),
// iD.osmWay({id: '=', nodes: ['u', '*']}),
// iD.osmWay({id: '-', nodes: ['*', 'x']}),
// iD.osmWay({id: '~', nodes: ['*', 'w']})
// ]);
// var r1 = iD.actionRestrictTurn({
// from: {node: 'u', way: '='},
// via: {node: '*'},
// to: {node: 'x', way: '-'}
// }, projection, 'r')(graph);
// expect(r1.entity('r').tags.restriction).to.equal('no_right_turn');
// var r2 = iD.actionRestrictTurn({
// from: {node: 'x', way: '-'},
// via: {node: '*'},
// to: {node: 'w', way: '~'}
// }, projection, 'r')(graph);
// expect(r2.entity('r').tags.restriction).to.equal('no_right_turn');
// var l1 = iD.actionRestrictTurn({
// from: {node: 'x', way: '-'},
// via: {node: '*'},
// to: {node: 'u', way: '='}
// }, projection, 'r')(graph);
// expect(l1.entity('r').tags.restriction).to.equal('no_left_turn');
// var l2 = iD.actionRestrictTurn({
// from: {node: 'w', way: '~'},
// via: {node: '*'},
// to: {node: 'x', way: '-'}
// }, projection, 'r')(graph);
// expect(l2.entity('r').tags.restriction).to.equal('no_left_turn');
// var s = iD.actionRestrictTurn({
// 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.actionRestrictTurn({
// from: {node: 'u', way: '='},
// via: {node: '*'},
// to: {node: 'u', way: '='}
// }, projection, 'r')(graph);
// expect(u.entity('r').tags.restriction).to.equal('no_u_turn');
// });
// it('infers no_u_turn from acute angle made by forward oneways', function() {
// // *
// // / \
// // w2/ \w1
// // / \
// // u x
// var graph = iD.coreGraph([
// iD.osmNode({id: 'u', loc: [-1, -20]}),
// iD.osmNode({id: '*', loc: [ 0, 0]}),
// iD.osmNode({id: 'x', loc: [ 1, -20]}),
// iD.osmWay({id: 'w1', nodes: ['x', '*'], tags: {oneway: 'yes'}}),
// iD.osmWay({id: 'w2', nodes: ['*', 'u'], tags: {oneway: 'yes'}})
// ]);
// var r = iD.actionRestrictTurn({
// from: {node: 'x', way: 'w1'},
// via: {node: '*'},
// to: {node: 'u', way: 'w2'}
// }, projection, 'r')(graph);
// expect(r.entity('r').tags.restriction).to.equal('no_u_turn');
// });
// it('infers no_u_turn from acute angle made by reverse oneways', function() {
// // *
// // / \
// // w2/ \w1
// // / \
// // u x
// var graph = iD.coreGraph([
// iD.osmNode({id: 'u', loc: [-1, -20]}),
// iD.osmNode({id: '*', loc: [ 0, 0]}),
// iD.osmNode({id: 'x', loc: [ 1, -20]}),
// iD.osmWay({id: 'w1', nodes: ['*', 'x'], tags: {oneway: '-1'}}),
// iD.osmWay({id: 'w2', nodes: ['u', '*'], tags: {oneway: '-1'}})
// ]);
// var r = iD.actionRestrictTurn({
// from: {node: 'x', way: 'w1'},
// via: {node: '*'},
// to: {node: 'u', way: 'w2'}
// }, projection, 'r')(graph);
// expect(r.entity('r').tags.restriction).to.equal('no_u_turn');
// });
var t = r.memberByRole('to');
expect(t.id).to.eql('-');
expect(t.type).to.eql('way');
});
});
+3 -1
View File
@@ -1,6 +1,8 @@
describe('iD.actionUnrestrictTurn', function() {
it('removes a restriction from a restricted turn', function() {
// u====*--->w
//
// u === * --- w
//
var graph = iD.coreGraph([
iD.osmNode({ id: 'u' }),
iD.osmNode({ id: '*' }),
+150
View File
@@ -557,3 +557,153 @@ describe('iD.osmIntersection', function() {
});
});
describe('iD.osmInferRestriction', function() {
var projection = d3.geoMercator().scale(250 / Math.PI);
it('infers the restriction type based on the turn angle', function() {
//
// u === * ~~~ w
// |
// x
//
var graph = iD.coreGraph([
iD.osmNode({id: 'u', loc: [-1, 0]}),
iD.osmNode({id: '*', loc: [ 0, 0]}),
iD.osmNode({id: 'w', loc: [ 1, 0]}),
iD.osmNode({id: 'x', loc: [ 0, -1]}),
iD.osmWay({id: '=', nodes: ['u', '*']}),
iD.osmWay({id: '-', nodes: ['*', 'x']}),
iD.osmWay({id: '~', nodes: ['*', 'w']})
]);
var r1 = iD.osmInferRestriction(graph, {
from: { node: 'u', way: '=', vertex: '*' },
to: { node: 'x', way: '-', vertex: '*' }
}, projection);
expect(r1).to.equal('no_right_turn');
var r2 = iD.osmInferRestriction(graph, {
from: { node: 'x', way: '-', vertex: '*' },
to: { node: 'w', way: '~', vertex: '*' }
}, projection);
expect(r2).to.equal('no_right_turn');
var l1 = iD.osmInferRestriction(graph, {
from: { node: 'x', way: '-', vertex: '*' },
to: { node: 'u', way: '=', vertex: '*' }
}, projection);
expect(l1).to.equal('no_left_turn');
var l2 = iD.osmInferRestriction(graph, {
from: { node: 'w', way: '~', vertex: '*' },
to: { node: 'x', way: '-', vertex: '*' }
}, projection);
expect(l2).to.equal('no_left_turn');
var s = iD.osmInferRestriction(graph, {
from: { node: 'u', way: '=', vertex: '*' },
to: { node: 'w', way: '~', vertex: '*' }
}, projection);
expect(s).to.equal('no_straight_on');
var u = iD.osmInferRestriction(graph, {
from: { node: 'u', way: '=', vertex: '*' },
to: { node: 'u', way: '=', vertex: '*' }
}, projection);
expect(u).to.equal('no_u_turn');
});
it('infers no_u_turn from sharply acute angle made by forward oneways', function() {
// *
// / \
// w2/ \w1 angle ≈22.6°
// / \
// u x
var graph = iD.coreGraph([
iD.osmNode({ id: 'u', loc: [0, -5] }),
iD.osmNode({ id: '*', loc: [1, 0] }),
iD.osmNode({ id: 'x', loc: [2, -5] }),
iD.osmWay({ id: 'w1', nodes: ['x', '*'], tags: { oneway: 'yes' } }),
iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } })
]);
var r = iD.osmInferRestriction(graph, {
from: { node: 'x', way: 'w1', vertex: '*' },
to: { node: 'u', way: 'w2', vertex: '*' }
}, projection);
expect(r).to.equal('no_u_turn');
});
it('does not infer no_u_turn from widely acute angle made by forward oneways', function() {
// *
// / \
// w2/ \w1 angle ≈36.9°
// / \
// u x
var graph = iD.coreGraph([
iD.osmNode({ id: 'u', loc: [0, -3] }),
iD.osmNode({ id: '*', loc: [1, 0] }),
iD.osmNode({ id: 'x', loc: [2, -3] }),
iD.osmWay({ id: 'w1', nodes: ['x', '*'], tags: { oneway: 'yes' } }),
iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } })
]);
var r = iD.osmInferRestriction(graph, {
from: { node: 'x', way: 'w1', vertex: '*' },
to: { node: 'u', way: 'w2', vertex: '*' }
}, projection);
expect(r).to.equal('no_left_turn');
});
it('infers no_u_turn from sharply acute angle made by forward oneways with a via way', function() {
// * -- +
// / \
// w2/ \w1 angle ≈22.6°
// / \
// u x
var graph = iD.coreGraph([
iD.osmNode({ id: 'u', loc: [0, -5] }),
iD.osmNode({ id: '*', loc: [1, 0] }),
iD.osmNode({ id: '+', loc: [2, 0] }),
iD.osmNode({ id: 'x', loc: [3, -5] }),
iD.osmWay({ id: 'w1', nodes: ['x', '+'], tags: { oneway: 'yes' } }),
iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } }),
iD.osmWay({ id: '-', nodes: ['*', '+'] })
]);
var r = iD.osmInferRestriction(graph, {
from: { node: 'x', way: 'w1', vertex: '+' },
to: { node: 'u', way: 'w2', vertex: '*' }
}, projection);
expect(r).to.equal('no_u_turn');
});
it('infers no_u_turn from widely acute angle made by forward oneways with a via way', function() {
// * -- +
// / \
// w2/ \w1 angle ≈36.9°
// / \
// u x
var graph = iD.coreGraph([
iD.osmNode({ id: 'u', loc: [0, -3] }),
iD.osmNode({ id: '*', loc: [1, 0] }),
iD.osmNode({ id: '+', loc: [2, 0] }),
iD.osmNode({ id: 'x', loc: [3, -3] }),
iD.osmWay({ id: 'w1', nodes: ['x', '+'], tags: { oneway: 'yes' } }),
iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } }),
iD.osmWay({ id: '-', nodes: ['*', '+'] })
]);
var r = iD.osmInferRestriction(graph, {
from: { node: 'x', way: 'w1', vertex: '+' },
to: { node: 'u', way: 'w2', vertex: '*' }
}, projection);
expect(r).to.equal('no_u_turn');
});
});