Merge pull request #4768 from openstreetmap/advanced_intersection

Add support for complex intersection and via way restrictions
This commit is contained in:
Bryan Housel
2018-03-01 01:46:30 -05:00
committed by GitHub
25 changed files with 2912 additions and 1638 deletions
+32 -3
View File
@@ -26,6 +26,11 @@
pointer-events: none;
}
.lasso #map {
pointer-events: visibleStroke;
}
/* `.target` objects are interactive */
/* They can be picked up, clicked, hovered, or things can connect to them */
.node.target {
@@ -242,7 +247,7 @@ text.point {
}
/* Turns */
/* Turn Restrictions */
g.turn rect,
g.turn circle {
@@ -255,10 +260,34 @@ g.turn circle {
pointer-events: none;
}
.lasso #map {
pointer-events: visibleStroke;
/* Turn restriction paths and vertices */
.surface.tr .way.target,
.surface.tr path.shadow.selected,
.surface.tr path.shadow.related {
stroke-width: 25px;
}
.surface.tr path.shadow.selected,
.surface.tr path.shadow.related,
.surface.tr g.vertex.selected .shadow,
.surface.tr g.vertex.related .shadow {
stroke-opacity: 0.7;
stroke: #777;
}
.surface.tr path.shadow.related.allow,
.surface.tr g.vertex.related.allow .shadow {
stroke: #5b3;
}
.surface.tr path.shadow.related.restrict,
.surface.tr g.vertex.related.restrict .shadow {
stroke: #d53;
}
.surface.tr path.shadow.related.only,
.surface.tr g.vertex.related.only .shadow {
stroke: #68f;
}
/* GPX Paths */
.layer-gpx {
+173 -6
View File
@@ -681,12 +681,14 @@ button.save.has-count .count::before {
height: 100%;
}
.field-help-title button.close,
.entity-editor-pane .header button.preset-close,
.preset-list-pane .header button.preset-choose {
position: absolute;
right: 0;
top: 0;
}
[dir='rtl'] .field-help-title button.close,
[dir='rtl'] .entity-editor-pane .header button.preset-close,
[dir='rtl'] .preset-list-pane .header button.preset-choose {
left: 0;
@@ -1266,6 +1268,7 @@ a.hide-toggle {
border-top: 0;
border-radius: 0 0 4px 4px;
overflow: hidden;
position: relative;
}
.form-field textarea {
@@ -1834,9 +1837,48 @@ input[type=number] {
/* Restrictions editor */
.form-field-restrictions .preset-input-wrap {
.form-field-restrictions .restriction-controls-container {
background-color: #fff;
border-top: 1px solid #ccc;
width: 100%;
padding: 5px;
}
.restriction-controls-container .restriction-controls {
display: table;
}
.restriction-controls .restriction-control {
display: table-row;
padding: 5px 10px;
height: 25px;
}
.restriction-control input,
.restriction-control span {
display: table-cell;
text-align: start;
padding: 0px 5px;
}
.restriction-control span.restriction-control-label {
text-align: end;
}
.restriction-control input {
width: 60px;
padding: 0;
margin: 0px 5px;
vertical-align: middle;
}
.form-field-restrictions .restriction-container {
position: relative;
height: 300px;
height: 370px;
}
/* zero width space, so container takes up space */
.form-field-restrictions .restriction-container:after {
content: '\200b';
}
.form-field-restrictions svg.surface {
@@ -1844,7 +1886,7 @@ input[type=number] {
height: 100%;
}
.form-field-restrictions .restriction-help {
.restriction-container .restriction-help {
z-index: 1;
position: absolute;
top: 0;
@@ -1852,8 +1894,32 @@ input[type=number] {
right: 0;
padding: 2px 6px;
background-color: rgba(255, 255, 255, .8);
color: #999;
color: #888;
text-align: center;
pointer-events: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.restriction-help span {
margin: 2px;
}
.restriction-help .qualifier {
color: #666;
font-weight: bold;
}
.restriction-help .qualifier.allow {
color: #8b5;
}
.restriction-help .qualifier.restrict {
color: #d53;
}
.restriction-help .qualifier.only {
color: #78f;
}
/* Changeset editor while comment text is empty */
@@ -1926,7 +1992,7 @@ div.combobox {
}
.combobox-caret::after {
content:"";
content: "";
height: 0; width: 0;
position: absolute;
left: 0; right: 0; bottom: 0; top: 0;
@@ -1936,6 +2002,107 @@ div.combobox {
border-right: 5px solid transparent;
}
/* Field Help */
.field-help-body {
display: block;
position: absolute;
top: 0;
left: 20px;
right: 20px;
margin: 5px;
padding: 8px;
border: 1px solid #ccc;
border-top: 0;
border-radius: 0 0 4px 4px;
z-index: 20;
background: rgba(255,255,255,0.95);
box-shadow: 0 0 30px 5px rgba(0,0,0,.4);
}
.field-help-title h2 {
padding: 10px;
margin-bottom: 0px;
font-size: 17px;
}
.field-help-title button {
width: 45px;
height: 55px;
border-radius: 0;
}
.field-help-nav {
font-size: 13px;
font-weight: bold;
margin-bottom: 10px;
}
.field-help-nav-item {
display: inline-block;
padding: 5px 10px;
cursor: pointer;
color: #666;
}
.field-help-nav-item.active {
color: #7092ff;
}
.field-help-nav-item:hover {
color: #597be7;
background-color: #efefef;
}
.field-help-content {
padding: 10px;
overflow-y: auto;
overflow-x: hidden;
}
.field-help-content h3 {
font-size: 12px;
margin-bottom: 5px;
}
.field-help-content p {
margin-bottom: 15px;
}
.field-help-content ul li {
list-style: inside;
margin-bottom: 5px;
}
.field-help-content .field-help-image {
width: 100%;
margin-bottom: 15px;
}
.field-help-content svg.turn {
width: 40px;
height: 20px;
}
.field-help-content svg.shadow {
opacity: 0.7;
width: 60px;
height: 20px;
}
.field-help-content svg.from {
color: #777;
}
.field-help-content svg.allow {
color: #5b3;
}
.field-help-content svg.restrict {
color: #d53;
}
.field-help-content svg.only {
color: #68f;
}
.field-help-content p.from_shadow,
.field-help-content p.allow_shadow,
.field-help-content p.restrict_shadow,
.field-help-content p.allow_turn,
.field-help-content p.restrict_turn {
margin-bottom: 5px;
}
/* Raw Tag Editor */
.tag-list {
@@ -2626,7 +2793,7 @@ div.full-screen > button:hover {
}
.help-wrap .toc li a:hover,
.help-wrap .nav a:hover {
.help-wrap .nav a:hover {
background: #ececec;
}
+64 -5
View File
@@ -219,14 +219,41 @@ en:
multiple_ways: There are too many lines here to split.
connected_to_hidden: This can't be split because it is connected to a hidden feature.
restriction:
help:
select: Click to select a road segment.
toggle: Click to toggle turn restrictions.
toggle_on: 'Click to add a "{restriction}" restriction.'
toggle_off: 'Click to remove the "{restriction}" restriction.'
annotation:
create: Added a turn restriction
delete: Deleted a turn restriction
restriction:
controls:
distance: Distance
distance_up_to: "Up to {distance}"
via: Via
via_node_only: "Node only"
via_up_to_one: "Up to 1 way"
via_up_to_two: "Up to 2 ways"
help:
indirect: "(indirect)"
turn:
no_left_turn: "NO Left Turn {indirect}"
no_right_turn: "NO Right Turn {indirect}"
no_u_turn: "NO U-Turn {indirect}"
no_straight_on: "NO Straight On {indirect}"
only_left_turn: "ONLY Left Turn {indirect}"
only_right_turn: "ONLY Right Turn {indirect}"
only_u_turn: "ONLY U-Turn {indirect}"
only_straight_on: "ONLY Straight On {indirect}"
allowed_left_turn: "Left Turn Allowed {indirect}"
allowed_right_turn: "Right Turn Allowed {indirect}"
allowed_u_turn: "U-Turn Allowed {indirect}"
allowed_straight_on: "Straight On Allowed {indirect}"
from: FROM
via: VIA
to: TO
from_name: "{from} {fromName}"
from_name_to_name: "{from} {fromName} {to} {toName}"
via_names: "{via} {viaNames}"
select_from: "Click to select a {from} segment"
select_from_name: "Click to select {from} {fromName}"
toggle: "Click for \"{turn}\""
undo:
tooltip: "Undo: {action}"
nothing: Nothing to undo.
@@ -715,6 +742,38 @@ en:
using: "To use a GPS trace for mapping, drag and drop the data file onto the map editor. If it's recognized, it will be drawn on the map as a bright purple line. Click the {data} **Map data** panel on the side of the map to enable, disable, or zoom to your GPS data."
tracing: "The GPS track isn't sent to OpenStreetMap - the best way to use it is to draw on the map, using it as a guide for the new features that you add."
upload: "You can also [upload your GPS data to OpenStreetMap](https://www.openstreetmap.org/trace/create) for other users to use."
field:
restrictions:
title: Turn Restrictions Help
about:
title: About
about: "This field allows you to inspect and modify turn restrictions. It displays a model of the selected intersection including other nearby connected roads."
from_via_to: "A turn restriction always contains: one **FROM way**, one **TO way**, and either one **VIA node** or one or more **VIA ways**."
maxdist: "The \"{distField}\" slider controls how far to search for additional connected roads."
maxvia: "The \"{viaField}\" slider adjusts how many via ways may be included in the search. (Tip: simple is better)"
inspecting:
title: Inspecting
about: "Hover over any **FROM** segment to see whether it has any turn restrictions. Each possible **TO** destination will be drawn with a colored shadow showing whether a restriction exists."
from_shadow: "{fromShadow} **FROM segment**"
allow_shadow: "{allowShadow} **TO Allowed**"
restrict_shadow: "{restrictShadow} **TO Restricted**"
only_shadow: "{onlyShadow} **TO Only**"
restricted: "\"Restricted\" means that there is a turn restriction, for example \"No Left Turn\"."
only: "\"Only\" means that a vehicle taking that path may only make that choice, for example \"Only Straight On\"."
modifying:
title: Modifying
about: "To modify turn restrictions, first click on any starting **FROM** segment to select it. The selected segment will pulse, and all possible **TO** destinations will appear as turn symbols."
indicators: "Then, click on a turn symbol to toggle it between \"Allowed\", \"Restricted\", and \"Only\"."
allow_turn: "{allowTurn} **TO Allowed**"
restrict_turn: "{restrictTurn} **TO Restricted**"
only_turn: "{onlyTurn} **TO Only**"
tips:
title: Tips
simple: "**Prefer simple restrictions over complex ones.**"
simple_example: "For example, avoid creating a via-way restriction if a simpler via-node turn restriction will do."
indirect: "**Some restrictions display the text \"(indirect)\" and are drawn lighter.**"
indirect_example: "These restrictions exist because of another nearby restriction. For example, an \"Only Straight On\" restriction will indirectly create \"No Turn\" restrictions for all other paths through the intersection."
indirect_noedit: "You may not edit indirect restrictions. Instead, edit the nearby direct restriction."
intro:
done: done
ok: OK
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

+74 -6
View File
@@ -285,18 +285,48 @@
"connected_to_hidden": "This can't be split because it is connected to a hidden feature."
},
"restriction": {
"help": {
"select": "Click to select a road segment.",
"toggle": "Click to toggle turn restrictions.",
"toggle_on": "Click to add a \"{restriction}\" restriction.",
"toggle_off": "Click to remove the \"{restriction}\" restriction."
},
"annotation": {
"create": "Added a turn restriction",
"delete": "Deleted a turn restriction"
}
}
},
"restriction": {
"controls": {
"distance": "Distance",
"distance_up_to": "Up to {distance}",
"via": "Via",
"via_node_only": "Node only",
"via_up_to_one": "Up to 1 way",
"via_up_to_two": "Up to 2 ways"
},
"help": {
"indirect": "(indirect)",
"turn": {
"no_left_turn": "NO Left Turn {indirect}",
"no_right_turn": "NO Right Turn {indirect}",
"no_u_turn": "NO U-Turn {indirect}",
"no_straight_on": "NO Straight On {indirect}",
"only_left_turn": "ONLY Left Turn {indirect}",
"only_right_turn": "ONLY Right Turn {indirect}",
"only_u_turn": "ONLY U-Turn {indirect}",
"only_straight_on": "ONLY Straight On {indirect}",
"allowed_left_turn": "Left Turn Allowed {indirect}",
"allowed_right_turn": "Right Turn Allowed {indirect}",
"allowed_u_turn": "U-Turn Allowed {indirect}",
"allowed_straight_on": "Straight On Allowed {indirect}"
},
"from": "FROM",
"via": "VIA",
"to": "TO",
"from_name": "{from} {fromName}",
"from_name_to_name": "{from} {fromName} {to} {toName}",
"via_names": "{via} {viaNames}",
"select_from": "Click to select a {from} segment",
"select_from_name": "Click to select {from} {fromName}",
"toggle": "Click for \"{turn}\""
}
},
"undo": {
"tooltip": "Undo: {action}",
"nothing": "Nothing to undo."
@@ -852,6 +882,44 @@
"using": "To use a GPS trace for mapping, drag and drop the data file onto the map editor. If it's recognized, it will be drawn on the map as a bright purple line. Click the {data} **Map data** panel on the side of the map to enable, disable, or zoom to your GPS data.",
"tracing": "The GPS track isn't sent to OpenStreetMap - the best way to use it is to draw on the map, using it as a guide for the new features that you add.",
"upload": "You can also [upload your GPS data to OpenStreetMap](https://www.openstreetmap.org/trace/create) for other users to use."
},
"field": {
"restrictions": {
"title": "Turn Restrictions Help",
"about": {
"title": "About",
"about": "This field allows you to inspect and modify turn restrictions. It displays a model of the selected intersection including other nearby connected roads.",
"from_via_to": "A turn restriction always contains: one **FROM way**, one **TO way**, and either one **VIA node** or one or more **VIA ways**.",
"maxdist": "The \"{distField}\" slider controls how far to search for additional connected roads.",
"maxvia": "The \"{viaField}\" slider adjusts how many via ways may be included in the search. (Tip: simple is better)"
},
"inspecting": {
"title": "Inspecting",
"about": "Hover over any **FROM** segment to see whether it has any turn restrictions. Each possible **TO** destination will be drawn with a colored shadow showing whether a restriction exists.",
"from_shadow": "{fromShadow} **FROM segment**",
"allow_shadow": "{allowShadow} **TO Allowed**",
"restrict_shadow": "{restrictShadow} **TO Restricted**",
"only_shadow": "{onlyShadow} **TO Only**",
"restricted": "\"Restricted\" means that there is a turn restriction, for example \"No Left Turn\".",
"only": "\"Only\" means that a vehicle taking that path may only make that choice, for example \"Only Straight On\"."
},
"modifying": {
"title": "Modifying",
"about": "To modify turn restrictions, first click on any starting **FROM** segment to select it. The selected segment will pulse, and all possible **TO** destinations will appear as turn symbols.",
"indicators": "Then, click on a turn symbol to toggle it between \"Allowed\", \"Restricted\", and \"Only\".",
"allow_turn": "{allowTurn} **TO Allowed**",
"restrict_turn": "{restrictTurn} **TO Restricted**",
"only_turn": "{onlyTurn} **TO Only**"
},
"tips": {
"title": "Tips",
"simple": "**Prefer simple restrictions over complex ones.**",
"simple_example": "For example, avoid creating a via-way restriction if a simpler via-node turn restriction will do.",
"indirect": "**Some restrictions display the text \"(indirect)\" and are drawn lighter.**",
"indirect_example": "These restrictions exist because of another nearby restriction. For example, an \"Only Straight On\" restriction will indirectly create \"No Turn\" restrictions for all other paths through the intersection.",
"indirect_noedit": "You may not edit indirect restrictions. Instead, edit the nearby direct restriction."
}
}
}
},
"intro": {
+28 -76
View File
@@ -1,98 +1,50 @@
import { actionSplit } from './split';
import {
osmInferRestriction,
osmRelation,
osmWay
} 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> },
// 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`.
// `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)
//
// 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 a restrictionID 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 an ID 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 from = graph.entity(turn.from.way),
via = graph.entity(turn.via.node),
to = graph.entity(turn.to.way);
var fromWay = graph.entity(turn.from.way);
var toWay = graph.entity(turn.to.way);
var viaNode = turn.via.node && graph.entity(turn.via.node);
var viaWays = turn.via.ways && turn.via.ways.map(function(id) { return graph.entity(id); });
var members = [];
function isClosingNode(way, nodeId) {
return nodeId === way.first() && nodeId === way.last();
members.push({ id: fromWay.id, type: 'way', role: 'from' });
if (viaNode) {
members.push({ id: viaNode.id, type: 'node', role: 'via' });
} else if (viaWays) {
viaWays.forEach(function(viaWay) {
members.push({ id: viaWay.id, type: 'way', role: 'via' });
});
}
function split(toOrFrom) {
var newID = toOrFrom.newID || osmWay().id;
graph = actionSplit(via.id, [newID])
.limitWays([toOrFrom.way])(graph);
var a = graph.entity(newID),
b = graph.entity(toOrFrom.way);
if (a.nodes.indexOf(toOrFrom.node) !== -1) {
return [a, b];
} else {
return [b, a];
}
}
if (!from.affix(via.id) || isClosingNode(from, via.id)) {
if (turn.from.node === turn.to.node) {
// U-turn
from = to = split(turn.from)[0];
} else if (turn.from.way === turn.to.way) {
// Straight-on or circular
var s = split(turn.from);
from = s[0];
to = s[1];
} else {
// Other
from = split(turn.from)[0];
}
}
if (!to.affix(via.id) || isClosingNode(to, via.id)) {
to = split(turn.to)[0];
}
members.push({ id: toWay.id, type: 'way', role: 'to' });
return graph.replace(osmRelation({
id: restrictionId,
id: restrictionID,
tags: {
type: 'restriction',
restriction: turn.restriction ||
osmInferRestriction(
graph,
turn.from,
turn.via,
turn.to,
projection)
restriction: restrictionType
},
members: [
{id: from.id, type: 'way', role: 'from'},
{id: via.id, type: 'node', role: 'via'},
{id: to.id, type: 'way', role: 'to'}
]
members: members
}));
};
}
+4 -1
View File
@@ -53,7 +53,10 @@ export function actionSplit(nodeId, newWayIds) {
}
function dist(nA, nB) {
return geoSphericalDistance(graph.entity(nA).loc, graph.entity(nB).loc);
var locA = graph.entity(nA).loc;
var locB = graph.entity(nB).loc;
var epsilon = 1e-6;
return (locA && locB) ? geoSphericalDistance(locA, locB) : epsilon;
}
// calculate lengths
+4 -17
View File
@@ -1,26 +1,13 @@
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> },
// restriction: <relation ID>
// }
//
// 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.
// `turn` must be an `osmTurn` object with a `restrictionID` property.
// see osm/intersection.js, pathToTurn()
//
export function actionUnrestrictTurn(turn) {
return function(graph) {
return actionDeleteRelation(turn.restriction)(graph);
return actionDeleteRelation(turn.restrictionID)(graph);
};
}
+587 -162
View File
@@ -1,197 +1,620 @@
import _each from 'lodash-es/each';
import _clone from 'lodash-es/clone';
import _every from 'lodash-es/every';
import _extend from 'lodash-es/extend';
import _find from 'lodash-es/find';
import _indexOf from 'lodash-es/indexOf';
import _keys from 'lodash-es/keys';
import _values from 'lodash-es/values';
import _uniq from 'lodash-es/uniq';
import {
actionDeleteRelation,
actionReverse,
actionSplit
} from '../actions';
import { coreGraph } from '../core';
import {
geoAngle,
geoSphericalDistance,
geoVecInterp
} from '../geo';
import { osmEntity } from './entity';
import { geoAngle } from '../geo/index';
import { osmWay } from './way';
export function osmTurn(turn) {
if (!(this instanceof osmTurn))
if (!(this instanceof osmTurn)) {
return new osmTurn(turn);
}
_extend(this, turn);
}
export function osmIntersection(graph, vertexId) {
var vertex = graph.entity(vertexId),
parentWays = graph.parentWays(vertex),
coincident = [],
highways = {};
export function osmIntersection(graph, startVertexId, maxDistance) {
maxDistance = maxDistance || 30; // in meters
var vgraph = coreGraph(); // virtual graph
var i, j, k;
function addHighway(way, adjacentNodeId) {
if (highways[adjacentNodeId]) {
coincident.push(adjacentNodeId);
} else {
highways[adjacentNodeId] = way;
function memberOfRestriction(entity) {
return graph.parentRelations(entity)
.some(function(r) { return r.isRestriction(); });
}
function isRoad(way) {
if (way.isArea() || way.isDegenerate()) return false;
var roads = {
'motorway': true,
'motorway_link': true,
'trunk': true,
'trunk_link': true,
'primary': true,
'primary_link': true,
'secondary': true,
'secondary_link': true,
'tertiary': true,
'tertiary_link': true,
'residential': true,
'unclassified': true,
'living_street': true,
'service': true,
'road': true,
'track': true
};
return roads[way.tags.highway];
}
var startNode = graph.entity(startVertexId);
var checkVertices = [startNode];
var checkWays;
var vertices = [];
var vertexIds = [];
var vertex;
var ways = [];
var wayIds = [];
var way;
var nodes = [];
var node;
var parents = [];
var parent;
// `actions` will store whatever actions must be performed to satisfy
// preconditions for adding a turn restriction to this intersection.
// - Remove any existing degenerate turn restrictions (missing from/to, etc)
// - Reverse oneways so that they are drawn in the forward direction
// - Split ways on key vertices
var actions = [];
// STEP 1: walk the graph outwards from starting vertex to search
// for more key vertices and ways to include in the intersection..
while (checkVertices.length) {
vertex = checkVertices.pop();
// check this vertex for parent ways that are roads
checkWays = graph.parentWays(vertex);
var hasWays = false;
for (i = 0; i < checkWays.length; i++) {
way = checkWays[i];
if (!isRoad(way) && !memberOfRestriction(way)) continue;
ways.push(way); // it's a road, or it's already in a turn restriction
hasWays = true;
// check the way's children for more key vertices
nodes = _uniq(graph.childNodes(way));
for (j = 0; j < nodes.length; j++) {
node = nodes[j];
if (node === vertex) continue; // same thing
if (vertices.indexOf(node) !== -1) continue; // seen it already
if (node.loc && startNode.loc &&
geoSphericalDistance(node.loc, startNode.loc) > maxDistance) continue; // too far from start
// a key vertex will have parents that are also roads
var hasParents = false;
parents = graph.parentWays(node);
for (k = 0; k < parents.length; k++) {
parent = parents[k];
if (parent === way) continue; // same thing
if (ways.indexOf(parent) !== -1) continue; // seen it already
if (!isRoad(parent)) continue; // not a road
hasParents = true;
break;
}
if (hasParents) {
checkVertices.push(node);
}
}
}
if (hasWays) {
vertices.push(vertex);
}
}
// Pre-split ways that would need to be split in
// order to add a restriction. The real split will
// happen when the restriction is added.
parentWays.forEach(function(way) {
if (!way.tags.highway || way.isArea() || way.isDegenerate())
return;
var isFirst = (vertexId === way.first()),
isLast = (vertexId === way.last()),
isAffix = (isFirst || isLast),
isClosingNode = (isFirst && isLast);
if (isAffix && !isClosingNode) {
var index = (isFirst ? 1 : way.nodes.length - 2);
addHighway(way, way.nodes[index]);
} else {
var splitIndex, wayA, wayB, indexA, indexB;
if (isClosingNode) {
splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint
wayA = osmWay({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
wayB = osmWay({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = 1;
indexB = way.nodes.length - 2;
} else {
splitIndex = _indexOf(way.nodes, vertex.id, 1); // split at vertexid
wayA = osmWay({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
wayB = osmWay({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
indexA = splitIndex - 1;
indexB = splitIndex + 1;
}
graph = graph.replace(wayA).replace(wayB);
addHighway(wayA, way.nodes[indexA]);
addHighway(wayB, way.nodes[indexB]);
}
});
// remove any ways from this intersection that are coincident
// (i.e. any adjacent node used by more than one intersecting way)
coincident.forEach(function (n) {
delete highways[n];
});
vertices = _uniq(vertices);
ways = _uniq(ways);
var intersection = {
highways: highways,
ways: _values(highways),
graph: graph
};
intersection.adjacentNodeId = function(fromWayId) {
return _find(_keys(highways), function(k) {
return highways[k].id === fromWayId;
// STEP 2: Build a virtual graph containing only the entities in the intersection..
// Everything done after this step should act on the virtual graph
// Any actions that must be performed later to the main graph go in `actions` array
ways.forEach(function(way) {
graph.childNodes(way).forEach(function(node) {
vgraph = vgraph.replace(node);
});
};
vgraph = vgraph.replace(way);
intersection.turns = function(fromNodeId) {
var start = highways[fromNodeId];
if (!start)
return [];
if (start.first() === vertex.id && start.tags.oneway === 'yes')
return [];
if (start.last() === vertex.id && start.tags.oneway === '-1')
return [];
function withRestriction(turn) {
graph.parentRelations(graph.entity(turn.from.way)).forEach(function(relation) {
if (relation.tags.type !== 'restriction')
return;
var f = relation.memberByRole('from'),
t = relation.memberByRole('to'),
v = relation.memberByRole('via');
if (f && f.id === turn.from.way &&
v && v.id === turn.via.node &&
t && t.id === turn.to.way) {
turn.restriction = relation.id;
} else if (/^only_/.test(relation.tags.restriction) &&
f && f.id === turn.from.way &&
v && v.id === turn.via.node &&
t && t.id !== turn.to.way) {
turn.restriction = relation.id;
turn.indirect_restriction = true;
graph.parentRelations(way).forEach(function(relation) {
if (relation.isRestriction()) {
if (relation.isValidRestriction(graph)) {
vgraph = vgraph.replace(relation);
} else if (relation.isComplete(graph)) {
actions.push(actionDeleteRelation(relation.id));
}
});
return osmTurn(turn);
}
var from = {
node: fromNodeId,
way: start.id.split(/-(a|b)/)[0]
},
via = { node: vertex.id },
turns = [];
_each(highways, function(end, adjacentNodeId) {
if (end === start)
return;
// backward
if (end.first() !== vertex.id && end.tags.oneway !== 'yes') {
turns.push(withRestriction({
from: from,
via: via,
to: {
node: adjacentNodeId,
way: end.id.split(/-(a|b)/)[0]
}
}));
}
// forward
if (end.last() !== vertex.id && end.tags.oneway !== '-1') {
turns.push(withRestriction({
from: from,
via: via,
to: {
node: adjacentNodeId,
way: end.id.split(/-(a|b)/)[0]
}
}));
}
});
});
// U-turn
if (start.tags.oneway !== 'yes' && start.tags.oneway !== '-1') {
turns.push(withRestriction({
from: from,
via: via,
to: from,
u: true
}));
// STEP 3: Force all oneways to be drawn in the forward direction
ways.forEach(function(w) {
var way = vgraph.entity(w.id);
if (way.tags.oneway === '-1') {
var action = actionReverse(way.id, { reverseOneway: true });
actions.push(action);
vgraph = action(vgraph);
}
});
// STEP 4: Split ways on key vertices
var origCount = osmEntity.id.next.way;
vertices.forEach(function(v) {
// This is an odd way to do it, but we need to find all the ways that
// will be split here, then split them one at a time to ensure that these
// actions can be replayed on the main graph exactly in the same order.
// (It is unintuitive, but the order of ways returned from graph.parentWays()
// is arbitrary, depending on how the main graph and vgraph were built)
var splitAll = actionSplit(v.id);
if (!splitAll.disabled(vgraph)) {
splitAll.ways(vgraph).forEach(function(way) {
var splitOne = actionSplit(v.id).limitWays([way.id]);
actions.push(splitOne);
vgraph = splitOne(vgraph);
});
}
});
// In here is where we should also split the intersection at nearby junction.
// for https://github.com/mapbox/iD-internal/issues/31
// nearbyVertices.forEach(function(v) {
// });
// Reasons why we reset the way id count here:
// 1. Continuity with way ids created by the splits so that we can replay
// these actions later if the user decides to create a turn restriction
// 2. Avoids churning way ids just by hovering over a vertex
// and displaying the turn restriction editor
osmEntity.id.next.way = origCount;
// STEP 5: Update arrays to point to vgraph entities
vertexIds = vertices.map(function(v) { return v.id; });
vertices = [];
ways = [];
vertexIds.forEach(function(id) {
var vertex = vgraph.entity(id);
var parents = vgraph.parentWays(vertex);
vertices.push(vertex);
ways = ways.concat(parents);
});
vertices = _uniq(vertices);
ways = _uniq(ways);
vertexIds = vertices.map(function(v) { return v.id; });
wayIds = ways.map(function(w) { return w.id; });
// STEP 6: Update the ways with some metadata that will be useful for
// walking the intersection graph later and rendering turn arrows.
function withMetadata(way, vertexIds) {
var __oneWay = way.isOneWay();
// which affixes are key vertices?
var __first = (vertexIds.indexOf(way.first()) !== -1);
var __last = (vertexIds.indexOf(way.last()) !== -1);
// what roles is this way eligible for?
var __via = (__first && __last);
var __from = ((__first && !__oneWay) || __last);
var __to = (__first || (__last && !__oneWay));
return way.update({
__first: __first,
__last: __last,
__from: __from,
__via: __via,
__to: __to,
__oneWay: __oneWay
});
}
ways = [];
wayIds.forEach(function(id) {
var way = withMetadata(vgraph.entity(id), vertexIds);
vgraph = vgraph.replace(way);
ways.push(way);
});
// STEP 7: Simplify - This is an iterative process where we:
// 1. Find trivial vertices with only 2 parents
// 2. trim off the leaf way from those vertices and remove from vgraph
var keepGoing;
var removeWayIds = [];
var removeVertexIds = [];
do {
keepGoing = false;
checkVertices = vertexIds.slice();
for (i = 0; i < checkVertices.length; i++) {
var vertexId = checkVertices[i];
vertex = vgraph.hasEntity(vertexId);
if (!vertex) {
if (vertexIds.indexOf(vertexId) !== -1) {
vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one
}
removeVertexIds.push(vertexId);
continue;
}
parents = vgraph.parentWays(vertex);
if (parents.length < 3) {
if (vertexIds.indexOf(vertexId) !== -1) {
vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one
}
}
if (parents.length === 2) { // vertex with 2 parents is trivial
var a = parents[0];
var b = parents[1];
var aIsLeaf = a && !a.__via;
var bIsLeaf = b && !b.__via;
var leaf, survivor;
if (aIsLeaf && !bIsLeaf) {
leaf = a;
survivor = b;
} else if (!aIsLeaf && bIsLeaf) {
leaf = b;
survivor = a;
}
if (leaf && survivor) {
survivor = withMetadata(survivor, vertexIds); // update survivor way
vgraph = vgraph.replace(survivor).remove(leaf); // update graph
removeWayIds.push(leaf.id);
keepGoing = true;
}
}
parents = vgraph.parentWays(vertex);
if (parents.length < 2) { // vertex is no longer a key vertex
if (vertexIds.indexOf(vertexId) !== -1) {
vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one
}
removeVertexIds.push(vertexId);
keepGoing = true;
}
if (parents.length < 1) { // vertex is no longer attached to anything
vgraph = vgraph.remove(vertex);
}
}
} while (keepGoing);
vertices = vertices
.filter(function(vertex) { return removeVertexIds.indexOf(vertex.id) === -1; })
.map(function(vertex) { return vgraph.entity(vertex.id); });
ways = ways
.filter(function(way) { return removeWayIds.indexOf(way.id) === -1; })
.map(function(way) { return vgraph.entity(way.id); });
// STEP 8: Extend leaf ways, so they don't end within the viewer
ways.forEach(function(way) {
var n1, n2;
if (way.__via) return; // not a leaf
if (way.__first) {
n1 = vgraph.entity(way.nodes[way.nodes.length - 2]);
n2 = vgraph.entity(way.nodes[way.nodes.length - 1]);
} else {
n1 = vgraph.entity(way.nodes[1]);
n2 = vgraph.entity(way.nodes[0]);
}
if (n1.loc && n2.loc && vgraph.parentWays(n2).length === 1) {
var toLoc = geoVecInterp(n1.loc, n2.loc, 10); // extend 1000%
n2 = n2.move(toLoc);
vgraph = vgraph.replace(n2);
}
});
// OK! Here is our intersection..
var intersection = {
graph: vgraph,
actions: actions,
vertices: vertices,
ways: ways,
};
// Get all the valid turns through this intersection given a starting way id.
// This operates on the virtual graph for everything.
//
// Basically, walk through all possible paths from starting way,
// honoring the existing turn restrictions as we go (watch out for loops!)
//
// For each path found, generate and return a `osmTurn` datastructure.
//
intersection.turns = function(fromWayId, maxViaWay) {
if (!fromWayId) return [];
if (!maxViaWay) maxViaWay = 0;
var vgraph = intersection.graph;
var keyVertexIds = intersection.vertices.map(function(v) { return v.id; });
var keyWayIds = intersection.ways.map(function(w) { return w.id; });
var start = vgraph.entity(fromWayId);
if (!start || !(start.__from || start.__via)) return [];
// maxViaWay=0 from-*-to (0 vias)
// maxViaWay=1 from-*-via-*-to (1 via max)
// maxViaWay=2 from-*-via-*-via-*-to (2 vias max)
var maxPathLength = (maxViaWay * 2) + 3;
var maxDistance = 30; // meters
var turns = [];
step(start);
return turns;
// traverse the intersection graph and find all the valid paths
function step(entity, currPath, currRestrictions, matchedRestriction) {
currPath = _clone(currPath || []);
if (currPath.length >= maxPathLength) return;
currPath.push(entity.id);
currRestrictions = _clone(currRestrictions || []);
var i, j;
if (entity.type === 'node') {
var parents = vgraph.parentWays(entity);
var nextWays = [];
// which ways can we step into?
for (i = 0; i < parents.length; i++) {
var way = parents[i];
// if next way is a oneway incoming to this vertex, skip
if (way.__oneWay && way.nodes[0] !== entity.id) continue;
// if we have seen it before (allowing for an initial u-turn), skip
if (currPath.indexOf(way.id) !== -1 && currPath.length >= 3) continue;
// Check all "current" restrictions (where we've already walked the `from`)
var restrict = undefined;
for (j = 0; j < currRestrictions.length; j++) {
var restriction = currRestrictions[j];
var f = restriction.memberByRole('from');
var v = restriction.membersByRole('via');
var t = restriction.memberByRole('to');
var isOnly = /^only_/.test(restriction.tags.restriction);
// Are all the vias part of this local intersection?
// This matters for flagging "indirect" restrictions
var isLocalVia;
if (v.length === 1 && v[0].type === 'node') {
isLocalVia = (keyVertexIds.indexOf(v[0].id) !== -1);
} else {
isLocalVia = _every(v, function(via) { return keyWayIds.indexOf(via.id) !== -1; });
}
// Does the current path match this turn restriction?
var matchesFrom = (f.id === fromWayId);
var matchesViaTo = false;
var isAlongOnlyPath = false;
if (t.id === way.id) { // match VIA, TO
if (v.length === 1 && v[0].type === 'node' && v[0].id === entity.id) {
matchesViaTo = true; // match VIA node
} else if (_every(v, function(via) { return currPath.indexOf(via.id) !== -1; })) {
matchesViaTo = true; // match all VIA ways
}
} else if (isOnly) {
for (k = 0; k < v.length; k++) {
// way doesn't match TO, but is one of the via ways along the path of an "only"
if (v[k].type === 'way' && v[k].id === way.id) {
isAlongOnlyPath = true;
break;
}
}
}
if (matchesViaTo) {
if (isOnly) {
restrict = { id: restriction.id, direct: matchesFrom, from: f.id, only: true, end: true };
} else {
restrict = { id: restriction.id, direct: matchesFrom, from: f.id, no: true, end: true };
}
} else { // indirect - caused by a different nearby restriction
if (isAlongOnlyPath) {
restrict = { id: restriction.id, direct: false, from: f.id, only: true, end: false };
} else if (isOnly && isLocalVia) {
restrict = { id: restriction.id, direct: false, from: f.id, no: true, end: true };
}
}
// stop looking if we find a "direct" restriction (matching FROM, VIA, TO)
if (restrict && restrict.direct)
break;
}
nextWays.push({ way: way, restrict: restrict });
}
nextWays.forEach(function(nextWay) {
step(nextWay.way, currPath, currRestrictions, nextWay.restrict);
});
} else { // entity.type === 'way'
if (currPath.length >= 3) { // this is a "complete" path..
var turnPath = _clone(currPath);
// an indirect restriction - only include the partial path (starting at FROM)
if (matchedRestriction && matchedRestriction.direct === false) {
for (i = 0; i < turnPath.length; i++) {
if (turnPath[i] === matchedRestriction.from) {
turnPath = turnPath.slice(i);
break;
}
}
}
var turn = pathToTurn(turnPath);
if (turn) {
if (matchedRestriction) {
turn.restrictionID = matchedRestriction.id;
turn.no = matchedRestriction.no;
turn.only = matchedRestriction.only;
turn.direct = matchedRestriction.direct;
}
turns.push(osmTurn(turn));
}
if (currPath[0] === currPath[2]) return; // if we made a u-turn - stop here
}
if (matchedRestriction && matchedRestriction.end) return; // don't advance any further
// which nodes can we step into?
var n1 = vgraph.entity(entity.first());
var n2 = vgraph.entity(entity.last());
var dist = n1.loc && n2.loc && geoSphericalDistance(n1.loc, n2.loc);
var nextNodes = [];
if (currPath.length > 1) {
if (dist > maxDistance) return; // the next node is too far
if (!entity.__via) return; // this way is a leaf / can't be a via
}
if (!entity.__oneWay && // bidirectional..
keyVertexIds.indexOf(n1.id) !== -1 && // key vertex..
currPath.indexOf(n1.id) === -1) { // haven't seen it yet..
nextNodes.push(n1); // can advance to first node
}
if (keyVertexIds.indexOf(n2.id) !== -1 && // key vertex..
currPath.indexOf(n2.id) === -1) { // haven't seen it yet..
nextNodes.push(n2); // can advance to last node
}
// gather restrictions FROM this way
var fromRestrictions = vgraph.parentRelations(entity).filter(function(r) {
if (!r.isRestriction()) return false;
var f = r.memberByRole('from');
return f && f.id === entity.id;
});
nextNodes.forEach(function(node) {
step(node, currPath, currRestrictions.concat(fromRestrictions), false);
});
}
}
// assumes path is alternating way-node-way of odd length
function pathToTurn(path) {
if (path.length < 3) return;
var fromWayId, fromNodeId, fromVertexId;
var toWayId, toNodeId, toVertexId;
var viaWayIds, viaNodeId, isUturn;
fromWayId = path[0];
toWayId = path[path.length - 1];
if (path.length === 3 && fromWayId === toWayId) { // u turn
var way = vgraph.entity(fromWayId);
if (way.__oneWay) return null;
isUturn = true;
viaNodeId = fromVertexId = toVertexId = path[1];
fromNodeId = toNodeId = adjacentNode(fromWayId, viaNodeId);
} else {
isUturn = false;
fromVertexId = path[1];
fromNodeId = adjacentNode(fromWayId, fromVertexId);
toVertexId = path[path.length - 2];
toNodeId = adjacentNode(toWayId, toVertexId);
if (path.length === 3) {
viaNodeId = path[1];
} else {
viaWayIds = path.filter(function(entityId) { return entityId[0] === 'w'; });
viaWayIds = viaWayIds.slice(1, viaWayIds.length - 1); // remove first, last
}
}
return {
key: path.join('_'),
path: path,
from: { node: fromNodeId, way: fromWayId, vertex: fromVertexId },
via: { node: viaNodeId, ways: viaWayIds },
to: { node: toNodeId, way: toWayId, vertex: toVertexId },
u: isUturn
};
function adjacentNode(wayId, affixId) {
var nodes = vgraph.entity(wayId).nodes;
return affixId === nodes[0] ? nodes[1] : nodes[nodes.length - 2];
}
}
};
return intersection;
}
export function osmInferRestriction(graph, from, via, to, projection) {
var fromWay = graph.entity(from.way),
fromNode = graph.entity(from.node),
toWay = graph.entity(to.way),
toNode = graph.entity(to.node),
viaNode = graph.entity(via.node),
fromOneWay = (fromWay.tags.oneway === 'yes' && fromWay.last() === via.node) ||
(fromWay.tags.oneway === '-1' && fromWay.first() === via.node),
toOneWay = (toWay.tags.oneway === 'yes' && toWay.first() === via.node) ||
(toWay.tags.oneway === '-1' && toWay.last() === via.node),
angle = geoAngle(viaNode, fromNode, projection) -
geoAngle(viaNode, toNode, projection);
export function osmInferRestriction(graph, turn, projection) {
var fromWay = graph.entity(turn.from.way);
var fromNode = graph.entity(turn.from.node);
var fromVertex = graph.entity(turn.from.vertex);
var toWay = graph.entity(turn.to.way);
var toNode = graph.entity(turn.to.node);
var toVertex = graph.entity(turn.to.vertex);
angle = angle * 180 / Math.PI;
var fromOneWay = (fromWay.tags.oneway === 'yes');
var toOneWay = (toWay.tags.oneway === 'yes');
var angle = (geoAngle(fromVertex, fromNode, projection) -
geoAngle(toVertex, toNode, projection)) * 180 / Math.PI;
while (angle < 0)
angle += 360;
@@ -199,7 +622,9 @@ export function osmInferRestriction(graph, from, via, to, projection) {
if (fromNode === toNode)
return 'no_u_turn';
if ((angle < 23 || angle > 336) && fromOneWay && toOneWay)
return 'no_u_turn';
return 'no_u_turn'; // wider tolerance for u-turn if both ways are oneway
if ((angle < 40 || angle > 319) && fromOneWay && toOneWay && turn.from.vertex !== turn.to.vertex)
return 'no_u_turn'; // even wider tolerance for u-turn if there is a via way (from !== to)
if (angle < 158)
return 'no_right_turn';
if (angle > 202)
+32 -2
View File
@@ -109,6 +109,16 @@ _extend(osmRelation.prototype, {
}
},
// Same as memberByRole, but returns all members with the given role
membersByRole: function(role) {
var result = [];
for (var i = 0; i < this.members.length; i++) {
if (this.members[i].role === role) {
result.push(_extend({}, this.members[i], {index: i}));
}
}
return result;
},
// Return the first member with the given id. A copy of the member object
// is returned, extended with an 'index' property whose value is the member index.
@@ -253,6 +263,26 @@ _extend(osmRelation.prototype, {
},
isValidRestriction: function() {
if (!this.isRestriction()) return false;
var froms = this.members.filter(function(m) { return m.role === 'from'; });
var vias = this.members.filter(function(m) { return m.role === 'via'; });
var tos = this.members.filter(function(m) { return m.role === 'to'; });
if (froms.length !== 1 && this.tags.restriction !== 'no_entry') return false;
if (froms.some(function(m) { return m.type !== 'way'; })) return false;
if (tos.length !== 1 && this.tags.restriction !== 'no_exit') return false;
if (tos.some(function(m) { return m.type !== 'way'; })) return false;
if (vias.length === 0) return false;
if (vias.length > 1 && vias.some(function(m) { return m.type !== 'way'; })) return false;
return true;
},
// Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
// where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
//
@@ -264,8 +294,8 @@ _extend(osmRelation.prototype, {
// rings not matched with the intended outer ring.
//
multipolygon: function(resolver) {
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); }),
inners = this.members.filter(function(m) { return 'inner' === m.role; });
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); });
var inners = this.members.filter(function(m) { return 'inner' === m.role; });
outers = osmJoinWays(outers, resolver);
inners = osmJoinWays(inners, resolver);
+37 -22
View File
@@ -1,26 +1,28 @@
import { geoAngle } from '../geo';
import { geoAngle, geoPathLength } from '../geo';
export function svgTurns(projection) {
return function drawTurns(selection, graph, turns) {
function key(turn) {
return [turn.from.node + turn.via.node + turn.to.node].join('-');
}
function icon(turn) {
var u = turn.u ? '-u' : '';
if (!turn.restriction)
return '#turn-yes' + u;
var restriction = graph.entity(turn.restriction).tags.restriction;
return '#turn-' +
(!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u;
if (turn.no) return '#turn-no' + u;
if (turn.only) return '#turn-only' + u;
return '#turn-yes' + u;
}
var layer = selection.selectAll('.layer-points .layer-points-turns');
var layer = selection.selectAll('.data-layer-osm').selectAll('.layer-turns')
.data([0]);
layer = layer.enter()
.append('g')
.attr('class', 'layer-osm layer-turns')
.merge(layer);
var groups = layer.selectAll('g.turn')
.data(turns, key);
.data(turns, function(d) { return d.key; });
groups.exit()
.remove();
@@ -28,10 +30,10 @@ export function svgTurns(projection) {
var enter = groups.enter()
.append('g')
.attr('class', 'turn');
.attr('class', function(d) { return 'turn ' + d.key; });
var nEnter = enter
.filter(function (turn) { return !turn.u; });
.filter(function(d) { return !d.u; });
nEnter.append('rect')
.attr('transform', 'translate(-22, -12)')
@@ -45,7 +47,7 @@ export function svgTurns(projection) {
var uEnter = enter
.filter(function (turn) { return turn.u; });
.filter(function(d) { return d.u; });
uEnter.append('circle')
.attr('r', '16');
@@ -60,14 +62,27 @@ export function svgTurns(projection) {
.merge(enter);
groups
.attr('transform', function (turn) {
var v = graph.entity(turn.via.node),
t = graph.entity(turn.to.node),
a = geoAngle(v, t, projection),
p = projection(v.loc),
r = turn.u ? 0 : 60;
.attr('opacity', function(d) {
return d.direct === false ? '0.7' : null;
})
.attr('transform', function(d) {
var pxRadius = 50;
var toWay = graph.entity(d.to.way);
var toPoints = graph.childNodes(toWay)
.map(function (n) { return n.loc; })
.map(projection);
var toLength = geoPathLength(toPoints);
var mid = toLength / 2; // midpoint of destination way
return 'translate(' + (r * Math.cos(a) + p[0]) + ',' + (r * Math.sin(a) + p[1]) + ') ' +
var toNode = graph.entity(d.to.node);
var toVertex = graph.entity(d.to.vertex);
var a = geoAngle(toVertex, toNode, projection);
var o = projection(toVertex.loc);
var r = d.u ? 0 // u-turn: no radius
: !toWay.__via ? pxRadius // leaf way: put marker at pxRadius
: Math.min(mid, pxRadius); // via way: prefer pxRadius, fallback to mid for very short ways
return 'translate(' + (r * Math.cos(a) + o[0]) + ',' + (r * Math.sin(a) + o[1]) + ') ' +
'rotate(' + a * 180 / Math.PI + ')';
});
+38 -20
View File
@@ -11,6 +11,7 @@ import {
import { textDirection } from '../util/locale';
import { svgIcon } from '../svg';
import { uiFieldHelp } from './field_help';
import { uiFields } from './fields';
import { uiTagReference } from './tag_reference';
import { utilRebind } from '../util';
@@ -25,11 +26,11 @@ export function uiField(context, presetField, entity, options) {
info: true
}, options);
var dispatch = d3_dispatch('change'),
field = _clone(presetField),
show = options.show,
state = '',
tags = {};
var dispatch = d3_dispatch('change');
var field = _clone(presetField);
var _show = options.show;
var _state = '';
var _tags = {};
field.impl = uiFields[field.type](field, context)
@@ -48,14 +49,14 @@ export function uiField(context, presetField, entity, options) {
if (!entity) return false;
var original = context.graph().base().entities[entity.id];
return _some(field.keys, function(key) {
return original ? tags[key] !== original.tags[key] : tags[key];
return original ? _tags[key] !== original.tags[key] : _tags[key];
});
}
function isPresent() {
return _some(field.keys, function(key) {
return tags[key];
return _tags[key];
});
}
@@ -65,8 +66,8 @@ export function uiField(context, presetField, entity, options) {
d3_event.preventDefault();
if (!entity) return false;
var original = context.graph().base().entities[entity.id],
t = {};
var original = context.graph().base().entities[entity.id];
var t = {};
d.keys.forEach(function(key) {
t[key] = original ? original.tags[key] : undefined;
});
@@ -143,14 +144,22 @@ export function uiField(context, presetField, entity, options) {
.classed('modified', isModified())
.classed('present', isPresent())
.each(function(d) {
var reference, help;
// instantiate field help
if (options.wrap && field.type === 'restrictions') {
help = uiFieldHelp(context, 'restrictions');
}
// instantiate tag reference
if (options.wrap && options.info) {
var referenceKey = d.key;
if (d.type === 'multiCombo') { // lookup key without the trailing ':'
referenceKey = referenceKey.replace(/:$/, '');
}
var reference = uiTagReference(d.reference || { key: referenceKey }, context);
if (state === 'hover') {
reference = uiTagReference(d.reference || { key: referenceKey }, context);
if (_state === 'hover') {
reference.showing(false);
}
}
@@ -158,35 +167,44 @@ export function uiField(context, presetField, entity, options) {
d3_select(this)
.call(d.impl);
if (options.wrap && options.info) {
// add field help components
if (help) {
d3_select(this)
.call(help.body)
.select('.form-label-button-wrap')
.call(help.button);
}
// add tag reference components
if (reference) {
d3_select(this)
.call(reference.body)
.select('.form-label-button-wrap')
.call(reference.button);
}
d.impl.tags(tags);
d.impl.tags(_tags);
});
};
field.state = function(_) {
if (!arguments.length) return state;
state = _;
if (!arguments.length) return _state;
_state = _;
return field;
};
field.tags = function(_) {
if (!arguments.length) return tags;
tags = _;
if (!arguments.length) return _tags;
_tags = _;
return field;
};
field.show = function() {
show = true;
if (field.default && field.key && tags[field.key] !== field.default) {
_show = true;
if (field.default && field.key && _tags[field.key] !== field.default) {
var t = {};
t[field.key] = field.default;
dispatch.call('change', this, t);
@@ -195,7 +213,7 @@ export function uiField(context, presetField, entity, options) {
field.isShown = function() {
return show || _some(field.keys, function(key) { return !!tags[key]; });
return _show || _some(field.keys, function(key) { return !!_tags[key]; });
};
+242
View File
@@ -0,0 +1,242 @@
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import marked from 'marked';
import { t } from '../util/locale';
import { svgIcon } from '../svg';
import { icon } from 'intro/helper';
// This currently only works with the 'restrictions' field
// It borrows some code from uiHelp
export function uiFieldHelp(context, fieldName) {
var fieldHelp = {};
var _inspector = d3_select(null);
var _wrap = d3_select(null);
var _body = d3_select(null);
var fieldHelpKeys = {
restrictions: [
['about',[
'about',
'from_via_to',
'maxdist',
'maxvia'
]],
['inspecting',[
'about',
'from_shadow',
'allow_shadow',
'restrict_shadow',
'only_shadow',
'restricted',
'only'
]],
['modifying',[
'about',
'indicators',
'allow_turn',
'restrict_turn',
'only_turn'
]],
['tips',[
'simple',
'simple_example',
'indirect',
'indirect_example',
'indirect_noedit'
]]
]
};
var fieldHelpHeadings = {};
var replacements = {
distField: t('restriction.controls.distance'),
viaField: t('restriction.controls.via'),
fromShadow: icon('#turn-shadow', 'pre-text shadow from'),
allowShadow: icon('#turn-shadow', 'pre-text shadow allow'),
restrictShadow: icon('#turn-shadow', 'pre-text shadow restrict'),
onlyShadow: icon('#turn-shadow', 'pre-text shadow only'),
allowTurn: icon('#turn-yes', 'pre-text turn'),
restrictTurn: icon('#turn-no', 'pre-text turn'),
onlyTurn: icon('#turn-only', 'pre-text turn')
};
// For each section, squash all the texts into a single markdown document
var docs = fieldHelpKeys[fieldName].map(function(key) {
var helpkey = 'help.field.' + fieldName + '.' + key[0];
var text = key[1].reduce(function(all, part) {
var subkey = helpkey + '.' + part;
var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?
var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s
return all + hhh + t(subkey, replacements) + '\n\n';
}, '');
return {
key: helpkey,
title: t(helpkey + '.title'),
html: marked(text.trim())
};
});
function show() {
updatePosition();
_body
.classed('hide', false)
.style('opacity', '0')
.transition()
.duration(200)
.style('opacity', '1');
}
function hide() {
_body
.classed('hide', true)
.transition()
.duration(200)
.style('opacity', '0')
.on('end', function () {
_body.classed('hide', true);
});
}
function clickHelp(index) {
var d = docs[index];
var tkeys = fieldHelpKeys[fieldName][index][1];
_body.selectAll('.field-help-nav-item')
.classed('active', function(d, i) { return i === index; });
var content = _body.selectAll('.field-help-content')
.html(d.html);
// class the paragraphs so we can find and style them
content.selectAll('p')
.attr('class', function(d, i) { return tkeys[i]; });
// insert special content for certain help sections
if (d.key === 'help.field.restrictions.inspecting') {
content
.insert('img', 'p.from_shadow')
.attr('class', 'field-help-image cf')
.attr('src', context.imagePath('tr_inspect.gif'));
} else if (d.key === 'help.field.restrictions.modifying') {
content
.insert('img', 'p.allow_turn')
.attr('class', 'field-help-image cf')
.attr('src', context.imagePath('tr_modify.gif'));
}
}
fieldHelp.button = function(selection) {
if (_body.empty()) return;
var button = selection.selectAll('.field-help-button')
.data([0]);
// enter/update
button.enter()
.append('button')
.attr('class', 'field-help-button')
.attr('tabindex', -1)
.call(svgIcon('#icon-help'))
.merge(button)
.on('click', function () {
d3_event.stopPropagation();
d3_event.preventDefault();
if (_body.classed('hide')) {
show();
} else {
hide();
}
});
};
function updatePosition() {
var wrap = _wrap.node();
var inspector = _inspector.node();
var wRect = wrap.getBoundingClientRect();
var iRect = inspector.getBoundingClientRect();
_body
.style('top', wRect.top + inspector.scrollTop - iRect.top + 'px');
}
fieldHelp.body = function(selection) {
// This control expects the field to have a preset-input-wrap div
_wrap = selection.selectAll('.preset-input-wrap');
if (_wrap.empty()) return;
// absolute position relative to the inspector, so it "floats" above the fields
_inspector = d3_select('#sidebar .entity-editor-pane .inspector-body');
if (_inspector.empty()) return;
_body = _inspector.selectAll('.field-help-body')
.data([0]);
var enter = _body.enter()
.append('div')
.attr('class', 'field-help-body hide'); // initially hidden
var titleEnter = enter
.append('div')
.attr('class', 'field-help-title cf');
titleEnter
.append('h2')
.attr('class', 'fl')
.text(t('help.field.' + fieldName + '.title'));
titleEnter
.append('button')
.attr('class', 'fr close')
.on('click', function() {
d3_event.stopPropagation();
d3_event.preventDefault();
hide();
})
.call(svgIcon('#icon-close'));
var navEnter = enter
.append('div')
.attr('class', 'field-help-nav cf');
var titles = docs.map(function(d) { return d.title; });
navEnter.selectAll('.field-help-nav-item')
.data(titles)
.enter()
.append('div')
.attr('class', 'field-help-nav-item')
.text(function(d) { return d; })
.on('click', function(d, i) {
d3_event.stopPropagation();
d3_event.preventDefault();
clickHelp(i);
});
enter
.append('div')
.attr('class', 'field-help-content');
_body = _body
.merge(enter);
clickHelp(0);
};
return fieldHelp;
}
+510 -121
View File
@@ -1,3 +1,5 @@
import _cloneDeep from 'lodash-es/cloneDeep';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import {
@@ -6,30 +8,24 @@ import {
} from 'd3-selection';
import { t } from '../../util/locale';
import {
behaviorBreathe,
behaviorHover
} from '../../behavior';
import {
osmEntity,
osmIntersection,
osmInferRestriction,
osmTurn
} from '../../osm';
import {
actionRestrictTurn,
actionUnrestrictTurn
} from '../../actions';
import { actionRestrictTurn, actionUnrestrictTurn } from '../../actions';
import { behaviorBreathe } from '../../behavior';
import {
geoExtent,
geoRawMercator,
geoVecScale,
geoVecSubtract,
geoZoomToScale
} from '../../geo';
import {
osmIntersection,
osmInferRestriction,
osmTurn,
osmWay
} from '../../osm';
import {
svgLayers,
svgLines,
@@ -37,8 +33,15 @@ import {
svgVertices
} from '../../svg';
import { utilRebind } from '../../util/rebind';
import { utilFunctor } from '../../util';
import {
utilDisplayName,
utilDisplayType,
utilEntitySelector,
utilFunctor,
utilRebind
} from '../../util';
import { utilDetect } from '../../util/detect';
import {
utilGetDimensions,
@@ -49,103 +52,273 @@ import {
export function uiFieldRestrictions(field, context) {
var dispatch = d3_dispatch('change');
var breathe = behaviorBreathe(context);
var hover = behaviorHover(context);
var initialized = false;
var vertexID;
var fromNodeID;
var storedViaWay = context.storage('turn-restriction-via-way');
var storedDistance = context.storage('turn-restriction-distance');
var _maxViaWay = storedViaWay !== null ? (+storedViaWay) : 1;
var _maxDistance = storedDistance ? (+storedDistance) : 30;
var _initialized = false;
var _parent = d3_select(null); // the entire field
var _container = d3_select(null); // just the map
var _oldTurns;
var _graph;
var _vertexID;
var _intersection;
var _fromWayID;
function restrictions(selection) {
_parent = selection;
// try to reuse the intersection, but always rebuild it if the graph has changed
if (_vertexID && (context.graph() !== _graph || !_intersection)) {
_graph = context.graph();
_intersection = osmIntersection(_graph, _vertexID, _maxDistance);
}
// It's possible for there to be no actual intersection here.
// for example, a vertex of two `highway=path`
// In this case, hide the field.
var isOK = (_intersection && _intersection.vertices.length && _intersection.ways.length);
d3_select(selection.node().parentNode).classed('hide', !isOK);
// if form field is hidden or has detached from dom, clean up.
if (!d3_select('.inspector-wrap.inspector-hidden').empty() || !selection.node().parentNode) {
if (!isOK ||
!d3_select('.inspector-wrap.inspector-hidden').empty() ||
!selection.node().parentNode ||
!selection.node().parentNode.parentNode) {
selection.call(restrictions.off);
return;
}
var wrap = selection.selectAll('.preset-input-wrap')
.data([0]);
var enter = wrap.enter()
wrap = wrap.enter()
.append('div')
.attr('class', 'preset-input-wrap');
.attr('class', 'preset-input-wrap')
.merge(wrap);
enter
var container = wrap.selectAll('.restriction-container')
.data([0]);
// enter
var containerEnter = container.enter()
.append('div')
.attr('class', 'restriction-container');
containerEnter
.append('div')
.attr('class', 'restriction-help');
// update
_container = containerEnter
.merge(container)
.call(renderViewer);
var intersection = osmIntersection(context.graph(), vertexID);
var graph = intersection.graph;
var vertex = graph.entity(vertexID);
var controls = wrap.selectAll('.restriction-controls')
.data([0]);
// enter/update
controls.enter()
.append('div')
.attr('class', 'restriction-controls-container')
.append('div')
.attr('class', 'restriction-controls')
.merge(controls)
.call(renderControls);
}
function renderControls(selection) {
var distControl = selection.selectAll('.restriction-distance')
.data([0]);
distControl.exit()
.remove();
var distControlEnter = distControl.enter()
.append('div')
.attr('class', 'restriction-control restriction-distance');
distControlEnter
.append('span')
.attr('class', 'restriction-control-label restriction-distance-label')
.text(t('restriction.controls.distance') + ':');
distControlEnter
.append('input')
.attr('class', 'restriction-distance-input')
.attr('type', 'range')
.attr('min', '20')
.attr('max', '50')
.attr('step', '5');
distControlEnter
.append('span')
.attr('class', 'restriction-distance-text');
// update
selection.selectAll('.restriction-distance-input')
.property('value', _maxDistance)
.on('input', function() {
var val = d3_select(this).property('value');
_maxDistance = +val;
_intersection = null;
_container.selectAll('.layer-osm .layer-turns *').remove();
context.storage('turn-restriction-distance', _maxDistance);
_parent.call(restrictions);
});
selection.selectAll('.restriction-distance-text')
.text(displayMaxDistance(_maxDistance));
var viaControl = selection.selectAll('.restriction-via-way')
.data([0]);
viaControl.exit()
.remove();
var viaControlEnter = viaControl.enter()
.append('div')
.attr('class', 'restriction-control restriction-via-way');
viaControlEnter
.append('span')
.attr('class', 'restriction-control-label restriction-via-way-label')
.text(t('restriction.controls.via') + ':');
viaControlEnter
.append('input')
.attr('class', 'restriction-via-way-input')
.attr('type', 'range')
.attr('min', '0')
.attr('max', '2')
.attr('step', '1');
viaControlEnter
.append('span')
.attr('class', 'restriction-via-way-text');
// update
selection.selectAll('.restriction-via-way-input')
.property('value', _maxViaWay)
.on('input', function() {
var val = d3_select(this).property('value');
_maxViaWay = +val;
_container.selectAll('.layer-osm .layer-turns *').remove();
context.storage('turn-restriction-via-way', _maxViaWay);
_parent.call(restrictions);
});
selection.selectAll('.restriction-via-way-text')
.text(displayMaxVia(_maxViaWay));
}
function renderViewer(selection) {
if (!_intersection) return;
var vgraph = _intersection.graph;
var filter = utilFunctor(true);
var projection = geoRawMercator();
var d = utilGetDimensions(wrap.merge(enter));
var c = [d[0] / 2, d[1] / 2];
var z = 24;
var d = utilGetDimensions(selection);
var c = geoVecScale(d, 0.5);
var z = 22;
projection.scale(geoZoomToScale(z));
// Calculate extent of all key vertices
var extent = geoExtent();
for (var i = 0; i < _intersection.vertices.length; i++) {
extent._extend(_intersection.vertices[i].extent());
}
// If this is a large intersection, adjust zoom to fit extent
if (_intersection.vertices.length > 1) {
var padding = 180; // in z22 pixels
var tl = projection([extent[0][0], extent[1][1]]);
var br = projection([extent[1][0], extent[0][1]]);
var hFactor = (br[0] - tl[0]) / (d[0] - padding);
var vFactor = (br[1] - tl[1]) / (d[1] - padding);
var hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2;
var vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2;
z = z - Math.max(hZoomDiff, vZoomDiff);
projection.scale(geoZoomToScale(z));
}
var padTop = 35; // reserve top space for hint text
var extentCenter = projection(extent.center());
extentCenter[1] = extentCenter[1] - padTop;
projection
.scale(geoZoomToScale(z));
var s = projection(vertex.loc);
projection
.translate([c[0] - s[0], c[1] - s[1]])
.translate(geoVecSubtract(c, extentCenter))
.clipExtent([[0, 0], d]);
var extent = geoExtent(projection.invert([0, d[1]]), projection.invert([d[0], 0]));
var drawLayers = svgLayers(projection, context).only('osm').dimensions(d);
var drawVertices = svgVertices(projection, context);
var drawLines = svgLines(projection, context);
var drawTurns = svgTurns(projection, context);
enter
var firstTime = selection.selectAll('.surface').empty();
selection
.call(drawLayers);
wrap = wrap
.merge(enter);
var surface = selection.selectAll('.surface')
.classed('tr', true);
var surface = wrap.selectAll('.surface');
if (firstTime) {
_initialized = true;
if (!enter.empty()) {
initialized = true;
surface
.call(breathe)
.call(hover);
.call(breathe);
d3_select(window)
.on('resize.restrictions', function() {
utilSetDimensions(_container, null);
redraw();
});
}
// This can happen if we've lowered the detail while a FROM way
// is selected, and that way is no longer part of the intersection.
if (_fromWayID && !vgraph.hasEntity(_fromWayID)) {
_fromWayID = null;
_oldTurns = null;
}
surface
.call(utilSetDimensions, d)
.call(drawVertices, graph, [vertex], filter, extent, true)
.call(drawLines, graph, intersection.ways, filter)
.call(drawTurns, graph, intersection.turns(fromNodeID));
.call(drawVertices, vgraph, _intersection.vertices, filter, extent, z)
.call(drawLines, vgraph, _intersection.ways, filter)
.call(drawTurns, vgraph, _intersection.turns(_fromWayID, _maxViaWay));
surface
.on('click.restrictions', click)
.on('mouseover.restrictions', mouseover)
.on('mouseout.restrictions', mouseout);
.on('mouseover.restrictions', mouseover);
surface
.selectAll('.selected')
.classed('selected', false);
if (fromNodeID) {
surface
.selectAll('.related')
.classed('related', false);
if (_fromWayID) {
var way = vgraph.entity(_fromWayID);
surface
.selectAll('.' + intersection.highways[fromNodeID].id)
.classed('selected', true);
.selectAll('.' + _fromWayID)
.classed('selected', true)
.classed('related', true);
}
mouseout();
context.history()
.on('change.restrictions', render);
d3_select(window)
.on('resize.restrictions', function() {
utilSetDimensions(wrap, null);
render();
});
updateHints(null);
function click() {
@@ -155,79 +328,300 @@ export function uiFieldRestrictions(field, context) {
var datum = d3_event.target.__data__;
var entity = datum && datum.properties && datum.properties.entity;
if (entity) datum = entity;
if (entity) {
datum = entity;
}
if (datum instanceof osmEntity) {
fromNodeID = intersection.adjacentNodeId(datum.id);
render();
if (datum instanceof osmWay && (datum.__from || datum.__via)) {
_fromWayID = datum.id;
_oldTurns = null;
redraw();
} else if (datum instanceof osmTurn) {
if (datum.restriction) {
context.perform(
actionUnrestrictTurn(datum, projection),
t('operations.restriction.annotation.delete')
);
} else {
context.perform(
actionRestrictTurn(datum, projection),
var actions, extraActions, turns, i;
var restrictionType = osmInferRestriction(vgraph, datum, projection);
if (datum.restrictionID && !datum.direct) {
return;
} else if (datum.restrictionID && !datum.only) { // NO -> ONLY
var datumOnly = _cloneDeep(datum);
datumOnly.only = true;
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.
turns = _intersection.turns(_fromWayID, 2);
extraActions = [];
_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]));
}
}
actions = _intersection.actions.concat(extraActions, [
actionRestrictTurn(datumOnly, restrictionType),
t('operations.restriction.annotation.create')
);
]);
} 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.
turns = _oldTurns || [];
extraActions = [];
for (i = 0; i < turns.length; i++) {
if (turns[i].key !== datum.key) {
extraActions.push(actionRestrictTurn(turns[i], turns[i].restrictionType));
}
}
_oldTurns = null;
actions = _intersection.actions.concat(extraActions, [
actionUnrestrictTurn(datum),
t('operations.restriction.annotation.delete')
]);
} else { // Allowed -> NO
actions = _intersection.actions.concat([
actionRestrictTurn(datum, restrictionType),
t('operations.restriction.annotation.create')
]);
}
context.perform.apply(context, actions);
// At this point the datum will be changed, but will have same key..
// Refresh it and update the help..
var s = surface.selectAll('.' + datum.key);
datum = s.empty() ? null : s.datum();
updateHints(datum);
} else {
_fromWayID = null;
_oldTurns = null;
redraw();
}
}
function mouseover() {
var datum = d3_event.target.__data__;
if (datum instanceof osmTurn) {
var graph = context.graph();
var presets = context.presets();
var preset;
updateHints(datum);
}
if (datum.restriction) {
preset = presets.match(graph.entity(datum.restriction), graph);
} else {
preset = presets.item('type/restriction/' +
osmInferRestriction(
graph,
datum.from,
datum.via,
datum.to,
projection
)
);
}
wrap.selectAll('.restriction-help')
.text(t('operations.restriction.help.' +
(datum.restriction ? 'toggle_off' : 'toggle_on'),
{ restriction: preset.name() })
);
function redraw() {
if (context.hasEntity(_vertexID)) {
_container.call(renderViewer);
}
}
function mouseout() {
wrap.selectAll('.restriction-help')
.text(t('operations.restriction.help.' +
(fromNodeID ? 'toggle' : 'select'))
);
function highlightPathsFrom(wayID) {
surface.selectAll('.related')
.classed('related', false)
.classed('allow', false)
.classed('restrict', false)
.classed('only', false);
surface.selectAll('.' + wayID)
.classed('related', true);
if (wayID) {
var turns = _intersection.turns(wayID, _maxViaWay);
for (var i = 0; i < turns.length; i++) {
var turn = turns[i];
var ids = [turn.to.way];
var klass = (turn.no ? 'restrict' : (turn.only ? 'only' : 'allow'));
if (turn.only || turns.length === 1) {
if (turn.via.ways) {
ids = ids.concat(turn.via.ways);
}
} else if (turn.to.way === wayID) {
continue;
}
surface.selectAll(utilEntitySelector(ids))
.classed('related', true)
.classed('allow', (klass === 'allow'))
.classed('restrict', (klass === 'restrict'))
.classed('only', (klass === 'only'));
}
}
}
function render() {
if (context.hasEntity(vertexID)) {
restrictions(selection);
function updateHints(datum) {
var help = _container.selectAll('.restriction-help').html('');
var placeholders = {};
['from', 'via', 'to'].forEach(function(k) {
placeholders[k] = '<span class="qualifier">' + t('restriction.help.' + k) + '</span>';
});
var entity = datum && datum.properties && datum.properties.entity;
if (entity) {
datum = entity;
}
if (_fromWayID) {
way = vgraph.entity(_fromWayID);
surface
.selectAll('.' + _fromWayID)
.classed('selected', true)
.classed('related', true);
}
// Hovering a way
if (datum instanceof osmWay && datum.__from) {
way = datum;
highlightPathsFrom(_fromWayID ? null : way.id);
surface.selectAll('.' + way.id)
.classed('related', true);
var clickSelect = (!_fromWayID || _fromWayID !== way.id);
help
.append('div') // "Click to select FROM {fromName}." / "FROM {fromName}"
.html(t('restriction.help.' + (clickSelect ? 'select_from_name' : 'from_name'), {
from: placeholders.from,
fromName: displayName(way.id, vgraph)
}));
// Hovering a turn arrow
} else if (datum instanceof osmTurn) {
var restrictionType = osmInferRestriction(vgraph, datum, projection);
var turnType = restrictionType.replace(/^(only|no)\_/, '');
var indirect = (datum.direct === false ? t('restriction.help.indirect') : '');
var klass, turnText, nextText;
if (datum.no) {
klass = 'restrict';
turnText = t('restriction.help.turn.no_' + turnType, { indirect: indirect });
nextText = t('restriction.help.turn.only_' + turnType, { indirect: '' });
} else if (datum.only) {
klass = 'only';
turnText = t('restriction.help.turn.only_' + turnType, { indirect: indirect });
nextText = t('restriction.help.turn.allowed_' + turnType, { indirect: '' });
} else {
klass = 'allow';
turnText = t('restriction.help.turn.allowed_' + turnType, { indirect: indirect });
nextText = t('restriction.help.turn.no_' + turnType, { indirect: '' });
}
help
.append('div') // "NO Right Turn (indirect)"
.attr('class', 'qualifier ' + klass)
.text(turnText);
help
.append('div') // "FROM {fromName} TO {toName}"
.html(t('restriction.help.from_name_to_name', {
from: placeholders.from,
fromName: displayName(datum.from.way, vgraph),
to: placeholders.to,
toName: displayName(datum.to.way, vgraph)
}));
if (datum.via.ways && datum.via.ways.length) {
var names = [];
for (var i = 0; i < datum.via.ways.length; i++) {
var prev = names[names.length - 1];
var curr = displayName(datum.via.ways[i], vgraph);
if (!prev || curr !== prev) // collapse identical names
names.push(curr);
}
help
.append('div') // "VIA {viaNames}"
.html(t('restriction.help.via_names', {
via: placeholders.via,
viaNames: names.join(', ')
}));
}
if (!indirect) {
help
.append('div') // Click for "No Right Turn"
.text(t('restriction.help.toggle', { turn: nextText.trim() }));
}
highlightPathsFrom(null);
var alongIDs = datum.path.slice();
surface.selectAll(utilEntitySelector(alongIDs))
.classed('related', true)
.classed('allow', (klass === 'allow'))
.classed('restrict', (klass === 'restrict'))
.classed('only', (klass === 'only'));
// Hovering empty surface
} else {
highlightPathsFrom(null);
if (_fromWayID) {
help
.append('div') // "FROM {fromName}"
.html(t('restriction.help.from_name', {
from: placeholders.from,
fromName: displayName(_fromWayID, vgraph)
}));
} else {
help
.append('div') // "Click to select a FROM segment."
.html(t('restriction.help.select_from', {
from: placeholders.from
}));
}
}
}
}
restrictions.entity = function(_) {
if (!vertexID || vertexID !== _.id) {
fromNodeID = null;
vertexID = _.id;
function displayMaxDistance(maxDist) {
var isImperial = (utilDetect().locale.toLowerCase() === 'en-us');
var opts;
if (isImperial) {
var distToFeet = { // imprecise conversion for prettier display
20: 70, 25: 85, 30: 100, 35: 115, 40: 130, 45: 145, 50: 160
}[maxDist];
opts = { distance: t('units.feet', { quantity: distToFeet }) };
} else {
opts = { distance: t('units.meters', { quantity: maxDist }) };
}
return t('restriction.controls.distance_up_to', opts);
}
function displayMaxVia(maxVia) {
return maxVia === 0 ? t('restriction.controls.via_node_only')
: maxVia === 1 ? t('restriction.controls.via_up_to_one')
: t('restriction.controls.via_up_to_two');
}
function displayName(entityID, graph) {
var entity = graph.entity(entityID);
var name = utilDisplayName(entity) || '';
var matched = context.presets().match(entity, graph);
var type = (matched && matched.name()) || utilDisplayType(entity.id);
return name || type;
}
restrictions.entity = function(_) {
_intersection = null;
_fromWayID = null;
_oldTurns = null;
_vertexID = _.id;
};
@@ -236,17 +630,12 @@ export function uiFieldRestrictions(field, context) {
restrictions.off = function(selection) {
if (!initialized) return;
if (!_initialized) return;
selection.selectAll('.surface')
.call(hover.off)
.call(breathe.off)
.on('click.restrictions', null)
.on('mouseover.restrictions', null)
.on('mouseout.restrictions', null);
context.history()
.on('change.restrictions', null);
.on('mouseover.restrictions', null);
d3_select(window)
.on('resize.restrictions', null);
+1
View File
@@ -19,6 +19,7 @@ export { uiEntityEditor } from './entity_editor';
export { uiFeatureInfo } from './feature_info';
export { uiFeatureList } from './feature_list';
export { uiField } from './field';
export { uiFieldHelp } from './field_help';
export { uiFlash } from './flash';
export { uiFormFields } from './form_fields';
export { uiFullScreen } from './full_screen';
+24 -19
View File
@@ -1,4 +1,5 @@
import { interpolate as d3_interpolate } from 'd3-interpolate';
import { selectAll as d3_selectAll } from 'd3-selection';
import { uiEntityEditor } from './entity_editor';
import { uiPresetList } from './preset_list';
@@ -6,22 +7,22 @@ import { uiViewOnOSM } from './view_on_osm';
export function uiInspector(context) {
var presetList = uiPresetList(context),
entityEditor = uiEntityEditor(context),
state = 'select',
entityID,
newFeature = false;
var presetList = uiPresetList(context);
var entityEditor = uiEntityEditor(context);
var _state = 'select';
var _entityID;
var _newFeature = false;
function inspector(selection) {
presetList
.entityID(entityID)
.autofocus(newFeature)
.entityID(_entityID)
.autofocus(_newFeature)
.on('choose', setPreset);
entityEditor
.state(state)
.entityID(entityID)
.state(_state)
.entityID(_entityID)
.on('choose', showList);
var wrap = selection.selectAll('.panewrap')
@@ -44,8 +45,8 @@ export function uiInspector(context) {
var editorPane = wrap.selectAll('.entity-editor-pane');
var graph = context.graph(),
entity = context.entity(entityID),
showEditor = state === 'hover' ||
entity = context.entity(_entityID),
showEditor = _state === 'hover' ||
entity.isUsed(graph) ||
entity.isHighwayIntersection(graph);
@@ -66,7 +67,7 @@ export function uiInspector(context) {
.merge(footer);
footer
.call(uiViewOnOSM(context).entityID(entityID));
.call(uiViewOnOSM(context).entityID(_entityID));
function showList(preset) {
@@ -89,23 +90,27 @@ export function uiInspector(context) {
inspector.state = function(_) {
if (!arguments.length) return state;
state = _;
entityEditor.state(state);
if (!arguments.length) return _state;
_state = _;
entityEditor.state(_state);
// remove any old field help overlay that might have gotten attached to the inspector
d3_selectAll('.field-help-body').remove();
return inspector;
};
inspector.entityID = function(_) {
if (!arguments.length) return entityID;
entityID = _;
if (!arguments.length) return _entityID;
_entityID = _;
return inspector;
};
inspector.newFeature = function(_) {
if (!arguments.length) return newFeature;
newFeature = _;
if (!arguments.length) return _newFeature;
_newFeature = _;
return inspector;
};
+36 -37
View File
@@ -13,17 +13,18 @@ import { svgIcon } from '../svg';
export function uiTagReference(tag) {
var taginfo = services.taginfo,
tagReference = {},
button = d3_select(null),
body = d3_select(null),
loaded,
showing;
var taginfo = services.taginfo;
var tagReference = {};
var _button = d3_select(null);
var _body = d3_select(null);
var _loaded;
var _showing;
function findLocal(data) {
var locale = utilDetect().locale.toLowerCase(),
localized;
var locale = utilDetect().locale.toLowerCase();
var localized;
if (locale !== 'pt-br') { // see #3776, prefer 'pt' over 'pt-br'
localized = _find(data, function(d) {
@@ -52,7 +53,7 @@ export function uiTagReference(tag) {
function load(param) {
if (!taginfo) return;
button
_button
.classed('tag-reference-loading', true);
taginfo.docs(param, function show(err, data) {
@@ -61,13 +62,13 @@ export function uiTagReference(tag) {
docs = findLocal(data);
}
body.html('');
_body.html('');
if (!docs || !docs.title) {
if (param.hasOwnProperty('value')) {
load(_omit(param, 'value')); // retry with key only
} else {
body
_body
.append('p')
.attr('class', 'tag-reference-description')
.text(t('inspector.no_documentation_key'));
@@ -77,7 +78,7 @@ export function uiTagReference(tag) {
}
if (docs.image && docs.image.thumb_url_prefix) {
body
_body
.append('img')
.attr('class', 'tag-reference-wiki-image')
.attr('src', docs.image.thumb_url_prefix + '100' + docs.image.thumb_url_suffix)
@@ -87,12 +88,12 @@ export function uiTagReference(tag) {
done();
}
body
_body
.append('p')
.attr('class', 'tag-reference-description')
.text(docs.description || t('inspector.documentation_redirect'));
body
_body
.append('a')
.attr('class', 'tag-reference-link')
.attr('target', '_blank')
@@ -104,7 +105,7 @@ export function uiTagReference(tag) {
// Add link to info about "good changeset comments" - #2923
if (param.key === 'comment') {
body
_body
.append('a')
.attr('class', 'tag-reference-comment-link')
.attr('target', '_blank')
@@ -119,54 +120,54 @@ export function uiTagReference(tag) {
function done() {
loaded = true;
_loaded = true;
button
_button
.classed('tag-reference-loading', false);
body
_body
.classed('expanded', true)
.transition()
.duration(200)
.style('max-height', '200px')
.style('opacity', '1');
showing = true;
_showing = true;
}
function hide() {
body
_body
.transition()
.duration(200)
.style('max-height', '0px')
.style('opacity', '0')
.on('end', function () {
body.classed('expanded', false);
_body.classed('expanded', false);
});
showing = false;
_showing = false;
}
tagReference.button = function(selection) {
button = selection.selectAll('.tag-reference-button')
_button = selection.selectAll('.tag-reference-button')
.data([0]);
button = button.enter()
_button = _button.enter()
.append('button')
.attr('class', 'tag-reference-button')
.attr('tabindex', -1)
.call(svgIcon('#icon-inspect'))
.merge(button);
.merge(_button);
button
_button
.on('click', function () {
d3_event.stopPropagation();
d3_event.preventDefault();
if (showing) {
if (_showing) {
hide();
} else if (loaded) {
} else if (_loaded) {
done();
} else {
load(tag);
@@ -176,31 +177,29 @@ export function uiTagReference(tag) {
tagReference.body = function(selection) {
var tagid = tag.rtype || (tag.key + '-' + tag.value);
body = selection.selectAll('.tag-reference-body')
_body = selection.selectAll('.tag-reference-body')
.data([tagid], function(d) { return d; });
body.exit()
_body.exit()
.remove();
body = body.enter()
_body = _body.enter()
.append('div')
.attr('class', 'tag-reference-body cf')
.style('max-height', '0')
.style('opacity', '0')
.merge(body);
.merge(_body);
if (showing === false) {
if (_showing === false) {
hide();
}
};
tagReference.showing = function(_) {
if (!arguments.length) return showing;
showing = _;
if (!arguments.length) return _showing;
_showing = _;
return tagReference;
};
+3
View File
@@ -375,6 +375,9 @@
"turn-yes-u": { "viewBox": "200 344 32 32" },
"turn-no-u": { "viewBox": "232 344 32 32" },
"turn-only-u": { "viewBox": "264 344 32 32" },
"turn-shadow": { "viewBox": "296 344 37 11" },
"turn-shadow-shape": { "fill": "currentColor" },
"preset-icon-frame": { "viewBox": "340 320 45 45" },
Binary file not shown.
+9
View File
@@ -280,6 +280,15 @@
</g>
</g>
<g id="turns">
<g id="turn-shadow">
<path d="M327.5,344 C330.538,344 333,346.463 333,349.5 C333,352.538 330.538,355 327.5,355 L301.5,355 C298.462,355 296,352.538 296,349.5 C296,346.463 298.462,344 301.5,344 L327.5,344 z" fill="#000000" id="turn-shadow-shape"/>
<path d="M301.5,349.5 L327.5,349.5" fill-opacity="0" stroke="#000000" stroke-width="3" stroke-linecap="round"/>
<path d="M301.5,349.5 L327.5,349.5" fill-opacity="0" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/>
<path d="M303.5,349.5 C303.5,350.605 302.605,351.5 301.5,351.5 C300.395,351.5 299.5,350.605 299.5,349.5 C299.5,348.396 300.395,347.5 301.5,347.5 C302.605,347.5 303.5,348.396 303.5,349.5 z" fill="#000000"/>
<path d="M303,349.5 C303,350.329 302.328,351 301.5,351 C300.672,351 300,350.329 300,349.5 C300,348.672 300.672,348 301.5,348 C302.328,348 303,348.672 303,349.5 z" fill="#BBBBBB"/>
<path d="M329.5,349.5 C329.5,350.605 328.605,351.5 327.5,351.5 C326.395,351.5 325.5,350.605 325.5,349.5 C325.5,348.396 326.395,347.5 327.5,347.5 C328.605,347.5 329.5,348.396 329.5,349.5 z" fill="#000000"/>
<path d="M329,349.5 C329,350.329 328.328,351 327.5,351 C326.672,351 326,350.329 326,349.5 C326,348.672 326.672,348 327.5,348 C328.328,348 329,348.672 329,349.5 z" fill="#BBBBBB"/>
</g>
<g id="turn-only-u">
<path d="M280,344 C271.211,344 264,351.211 264,360 C264,368.789 271.211,376 280,376 C288.789,376 296,368.789 296,360 C296,351.211 288.789,344 280,344 z" fill="#000000" id="turn-only-u-shape1" opacity="0.5"/>
<path d="M268,360 C268,353.373 273.373,348 280,348 C286.627,348 292,353.373 292,360 C292,366.627 286.627,372 280,372 C273.373,372 268,366.627 268,360 z" fill="#7092FF" id="turn-only-u-shape2"/>

Before

Width:  |  Height:  |  Size: 287 KiB

After

Width:  |  Height:  |  Size: 288 KiB

+60 -463
View File
@@ -1,483 +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']})
]);
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.actionRestrictTurn({
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(r.memberByRole('from').id).to.eql('=');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('-');
expect(r.memberByRole('to').type).to.eql('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.actionRestrictTurn({
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(r.memberByRole('from').id).to.eql('=');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('-');
expect(r.memberByRole('to').type).to.eql('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.actionRestrictTurn({
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(r.memberByRole('from').id).to.eql('==');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('-');
expect(r.memberByRole('to').type).to.eql('way');
});
it('splits the from way when necessary (straight on 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.actionRestrictTurn({
from: {node: 'u', way: '=', newID: '=='},
via: {node: '*'},
to: {node: 'w', way: '='},
restriction: 'no_straight_on'
}, projection, 'r');
var turn = {
from: { node: 'u', way: '=' },
via: { node: '*'},
to: { node: 'w', way: '-' }
};
var action = iD.actionRestrictTurn(turn, 'no_straight_on', 'r');
graph = action(graph);
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_straight_on'});
expect(r.memberByRole('from').id).to.eql('=');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('==');
expect(r.memberByRole('to').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 t = r.memberByRole('to');
expect(t.id).to.eql('-');
expect(t.type).to.eql('way');
});
it('splits the from way when necessary (straight on 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: ['w', '*', 'u']}),
iD.Way({id: '-', nodes: ['*', 'x']})
]),
action = iD.actionRestrictTurn({
from: {node: 'u', way: '=', newID: '=='},
via: {node: '*'},
to: {node: 'w', way: '='},
restriction: 'no_straight_on'
}, projection, 'r');
graph = action(graph);
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_straight_on'});
expect(r.memberByRole('from').id).to.eql('==');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('=');
expect(r.memberByRole('to').type).to.eql('way');
});
it('splits the from way when necessary (vertex closes from)', function() {
it('adds a via way restriction to an unrestricted turn', function() {
//
// b -- c
// | |
// a -- * === w
// u === v1
// |
// w --- v2
//
var graph = iD.Graph([
iD.Node({id: 'a', loc: [-1, 0]}),
iD.Node({id: 'b', loc: [-1, 1]}),
iD.Node({id: 'c', loc: [ 0, 1]}),
iD.Node({id: '*', loc: [ 0, 0]}),
iD.Node({id: 'w', loc: [ 1, 0]}),
iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*']}),
iD.Way({id: '=', nodes: ['*', 'w']})
]),
action = iD.actionRestrictTurn({
from: {node: 'c', way: '-', newID: '--'},
via: {node: '*'},
to: {node: 'w', way: '='},
restriction: 'no_left_turn'
}, projection, 'r');
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']})
]);
graph = action(graph);
var r = graph.entity('r');
expect(r.tags).to.eql({type: 'restriction', restriction: 'no_left_turn'});
expect(r.memberByRole('from').id).to.eql('--');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('=');
expect(r.memberByRole('to').type).to.eql('way');
});
it('splits the from/to way when necessary (vertex closes from/to)', function() {
//
// b -- c
// | |
// a -- * === w
//
var graph = iD.Graph([
iD.Node({id: 'a', loc: [-1, 0]}),
iD.Node({id: 'b', loc: [-1, 1]}),
iD.Node({id: 'c', loc: [ 0, 1]}),
iD.Node({id: '*', loc: [ 0, 0]}),
iD.Node({id: 'w', loc: [ 1, 0]}),
iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*']}),
iD.Way({id: '=', nodes: ['*', 'w']})
]),
action = iD.actionRestrictTurn({
from: {node: 'a', way: '-', newID: '--'},
via: {node: '*'},
to: {node: 'c', 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(r.memberByRole('from').id).to.eql('-');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('--');
expect(r.memberByRole('to').type).to.eql('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.actionRestrictTurn({
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(r.memberByRole('from').id).to.eql('-');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('==');
expect(r.memberByRole('to').type).to.eql('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.actionRestrictTurn({
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(r.memberByRole('from').id).to.eql('-');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('=');
expect(r.memberByRole('to').type).to.eql('way');
});
it('splits the to way when necessary (vertex closes to)', function() {
//
// b -- c
// | |
// a -- * === w
//
var graph = iD.Graph([
iD.Node({id: 'a', loc: [-1, 0]}),
iD.Node({id: 'b', loc: [-1, 1]}),
iD.Node({id: 'c', loc: [ 0, 1]}),
iD.Node({id: '*', loc: [ 0, 0]}),
iD.Node({id: 'w', loc: [ 1, 0]}),
iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*']}),
iD.Way({id: '=', nodes: ['*', 'w']})
]),
action = iD.actionRestrictTurn({
from: {node: 'w', way: '='},
via: {node: '*'},
to: {node: 'c', 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(r.memberByRole('from').id).to.eql('=');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('--');
expect(r.memberByRole('to').type).to.eql('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.actionRestrictTurn({
from: {node: 'u', way: '='},
via: {node: '*'},
to: {node: 'u', way: '='},
restriction: 'no_u_turn'
}, projection, 'r');
var turn = {
from: { node: 'u', way: '=' },
via: { ways: ['|'] },
to: { node: 'w', way: '-' }
};
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'});
expect(r.memberByRole('from').id).to.eql('=');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('=');
expect(r.memberByRole('to').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('way');
var t = r.memberByRole('to');
expect(t.id).to.eql('-');
expect(t.type).to.eql('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.actionRestrictTurn({
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(r.memberByRole('from').id).to.eql('==');
expect(r.memberByRole('from').type).to.eql('way');
expect(r.memberByRole('via').id).to.eql('*');
expect(r.memberByRole('via').type).to.eql('node');
expect(r.memberByRole('to').id).to.eql('==');
expect(r.memberByRole('to').type).to.eql('way');
});
it('infers 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: [ 1, 0]}),
iD.Node({id: 'x', loc: [ 0, -1]}),
iD.Way({id: '=', nodes: ['u', '*']}),
iD.Way({id: '-', nodes: ['*', 'x']}),
iD.Way({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.Graph([
iD.Node({id: 'u', loc: [-1, -20]}),
iD.Node({id: '*', loc: [ 0, 0]}),
iD.Node({id: 'x', loc: [ 1, -20]}),
iD.Way({id: 'w1', nodes: ['x', '*'], tags: {oneway: 'yes'}}),
iD.Way({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.Graph([
iD.Node({id: 'u', loc: [-1, -20]}),
iD.Node({id: '*', loc: [ 0, 0]}),
iD.Node({id: 'x', loc: [ 1, -20]}),
iD.Way({id: 'w1', nodes: ['*', 'x'], tags: {oneway: '-1'}}),
iD.Way({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');
});
});
+16 -17
View File
@@ -1,24 +1,23 @@
describe('iD.actionUnrestrictTurn', 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.actionUnrestrictTurn({
restriction: 'r'
});
//
// u === * --- w
//
var graph = iD.coreGraph([
iD.osmNode({ id: 'u' }),
iD.osmNode({ id: '*' }),
iD.osmNode({ id: 'w' }),
iD.osmWay({ id: '=', nodes: ['u', '*'], tags: { highway: 'residential' } }),
iD.osmWay({ id: '-', nodes: ['*', 'w'], tags: { highway: 'residential' } }),
iD.osmRelation({ id: 'r', tags: { type: 'restriction' }, members: [
{ id: '=', role: 'from', type: 'way' },
{ id: '-', role: 'to', type: 'way' },
{ id: '*', role: 'via', type: 'node' }
]})
]);
var action = iD.actionUnrestrictTurn({ restrictionID: 'r' });
graph = action(graph);
expect(graph.hasEntity('r')).to.be.undefined;
});
});
File diff suppressed because it is too large Load Diff
+409 -257
View File
@@ -1,98 +1,98 @@
describe('iD.osmRelation', function () {
if (iD.debug) {
it('freezes nodes', function () {
expect(Object.isFrozen(iD.Relation().members)).to.be.true;
expect(Object.isFrozen(iD.osmRelation().members)).to.be.true;
});
}
it('returns a relation', function () {
expect(iD.Relation()).to.be.an.instanceOf(iD.Relation);
expect(iD.Relation().type).to.equal('relation');
expect(iD.osmRelation()).to.be.an.instanceOf(iD.osmRelation);
expect(iD.osmRelation().type).to.equal('relation');
});
it('defaults members to an empty array', function () {
expect(iD.Relation().members).to.eql([]);
expect(iD.osmRelation().members).to.eql([]);
});
it('sets members as specified', function () {
expect(iD.Relation({members: ['n-1']}).members).to.eql(['n-1']);
expect(iD.osmRelation({members: ['n-1']}).members).to.eql(['n-1']);
});
it('defaults tags to an empty object', function () {
expect(iD.Relation().tags).to.eql({});
expect(iD.osmRelation().tags).to.eql({});
});
it('sets tags as specified', function () {
expect(iD.Relation({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
expect(iD.osmRelation({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'});
});
describe('#copy', function () {
it('returns a new Relation', function () {
var r = iD.Relation({id: 'r'}),
result = r.copy(null, {});
var r = iD.osmRelation({id: 'r'});
var result = r.copy(null, {});
expect(result).to.be.an.instanceof(iD.Relation);
expect(result).to.be.an.instanceof(iD.osmRelation);
expect(result).not.to.equal(r);
});
it('adds the new Relation to input object', function () {
var r = iD.Relation({id: 'r'}),
copies = {},
result = r.copy(null, copies);
var r = iD.osmRelation({id: 'r'});
var copies = {};
var result = r.copy(null, copies);
expect(Object.keys(copies)).to.have.length(1);
expect(copies.r).to.equal(result);
});
it('returns an existing copy in input object', function () {
var r = iD.Relation({id: 'r'}),
copies = {},
result1 = r.copy(null, copies),
result2 = r.copy(null, copies);
var r = iD.osmRelation({id: 'r'});
var copies = {};
var result1 = r.copy(null, copies);
var result2 = r.copy(null, copies);
expect(Object.keys(copies)).to.have.length(1);
expect(result1).to.equal(result2);
});
it('deep copies members', function () {
var a = iD.Node({id: 'a'}),
b = iD.Node({id: 'b'}),
c = iD.Node({id: 'c'}),
w = iD.Way({id: 'w', nodes: ['a','b','c','a']}),
r = iD.Relation({id: 'r', members: [{id: 'w', role: 'outer'}]}),
graph = iD.Graph([a, b, c, w, r]),
copies = {},
result = r.copy(graph, copies);
var a = iD.osmNode({id: 'a'});
var b = iD.osmNode({id: 'b'});
var c = iD.osmNode({id: 'c'});
var w = iD.osmWay({id: 'w', nodes: ['a','b','c','a']});
var r = iD.osmRelation({id: 'r', members: [{id: 'w', role: 'outer'}]});
var graph = iD.coreGraph([a, b, c, w, r]);
var copies = {};
var result = r.copy(graph, copies);
expect(Object.keys(copies)).to.have.length(5);
expect(copies.w).to.be.an.instanceof(iD.Way);
expect(copies.a).to.be.an.instanceof(iD.Node);
expect(copies.b).to.be.an.instanceof(iD.Node);
expect(copies.c).to.be.an.instanceof(iD.Node);
expect(copies.w).to.be.an.instanceof(iD.osmWay);
expect(copies.a).to.be.an.instanceof(iD.osmNode);
expect(copies.b).to.be.an.instanceof(iD.osmNode);
expect(copies.c).to.be.an.instanceof(iD.osmNode);
expect(result.members[0].id).not.to.equal(r.members[0].id);
expect(result.members[0].role).to.equal(r.members[0].role);
});
it('deep copies non-tree relation graphs without duplicating children', function () {
var w = iD.Way({id: 'w'}),
r1 = iD.Relation({id: 'r1', members: [{id: 'r2'}, {id: 'w'}]}),
r2 = iD.Relation({id: 'r2', members: [{id: 'w'}]}),
graph = iD.Graph([w, r1, r2]),
copies = {};
var w = iD.osmWay({id: 'w'});
var r1 = iD.osmRelation({id: 'r1', members: [{id: 'r2'}, {id: 'w'}]});
var r2 = iD.osmRelation({id: 'r2', members: [{id: 'w'}]});
var graph = iD.coreGraph([w, r1, r2]);
var copies = {};
r1.copy(graph, copies);
expect(Object.keys(copies)).to.have.length(3);
expect(copies.r1).to.be.an.instanceof(iD.Relation);
expect(copies.r2).to.be.an.instanceof(iD.Relation);
expect(copies.w).to.be.an.instanceof(iD.Way);
expect(copies.r1).to.be.an.instanceof(iD.osmRelation);
expect(copies.r2).to.be.an.instanceof(iD.osmRelation);
expect(copies.w).to.be.an.instanceof(iD.osmWay);
expect(copies.r1.members[0].id).to.equal(copies.r2.id);
expect(copies.r1.members[1].id).to.equal(copies.w.id);
expect(copies.r2.members[0].id).to.equal(copies.w.id);
});
it('deep copies cyclical relation graphs without issue', function () {
var r1 = iD.Relation({id: 'r1', members: [{id: 'r2'}]}),
r2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}),
graph = iD.Graph([r1, r2]),
copies = {};
var r1 = iD.osmRelation({id: 'r1', members: [{id: 'r2'}]});
var r2 = iD.osmRelation({id: 'r2', members: [{id: 'r1'}]});
var graph = iD.coreGraph([r1, r2]);
var copies = {};
r1.copy(graph, copies);
expect(Object.keys(copies)).to.have.length(2);
@@ -101,9 +101,9 @@ describe('iD.osmRelation', function () {
});
it('deep copies self-referencing relations without issue', function () {
var r = iD.Relation({id: 'r', members: [{id: 'r'}]}),
graph = iD.Graph([r]),
copies = {};
var r = iD.osmRelation({id: 'r', members: [{id: 'r'}]});
var graph = iD.coreGraph([r]);
var copies = {};
r.copy(graph, copies);
expect(Object.keys(copies)).to.have.length(1);
@@ -113,53 +113,53 @@ describe('iD.osmRelation', function () {
describe('#extent', function () {
it('returns the minimal extent containing the extents of all members', function () {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [5, 10]}),
r = iD.Relation({members: [{id: a.id}, {id: b.id}]}),
graph = iD.Graph([a, b, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [5, 10]});
var r = iD.osmRelation({members: [{id: a.id}, {id: b.id}]});
var graph = iD.coreGraph([a, b, r]);
expect(r.extent(graph).equals([[0, 0], [5, 10]])).to.be.ok;
});
it('returns the known extent of incomplete relations', function () {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [5, 10]}),
r = iD.Relation({members: [{id: a.id}, {id: b.id}]}),
graph = iD.Graph([a, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [5, 10]});
var r = iD.osmRelation({members: [{id: a.id}, {id: b.id}]});
var graph = iD.coreGraph([a, r]);
expect(r.extent(graph).equals([[0, 0], [0, 0]])).to.be.ok;
});
it('does not error on self-referencing relations', function () {
var r = iD.Relation();
var r = iD.osmRelation();
r = r.addMember({id: r.id});
expect(r.extent(iD.Graph([r]))).to.eql(iD.geoExtent());
expect(r.extent(iD.coreGraph([r]))).to.eql(iD.geoExtent());
});
});
describe('#geometry', function () {
it('returns \'area\' for multipolygons', function () {
expect(iD.Relation({tags: {type: 'multipolygon'}}).geometry(iD.Graph())).to.equal('area');
expect(iD.osmRelation({tags: {type: 'multipolygon'}}).geometry(iD.coreGraph())).to.equal('area');
});
it('returns \'relation\' for other relations', function () {
expect(iD.Relation().geometry(iD.Graph())).to.equal('relation');
expect(iD.osmRelation().geometry(iD.coreGraph())).to.equal('relation');
});
});
describe('#isDegenerate', function () {
it('returns true for a relation without members', function () {
expect(iD.Relation().isDegenerate()).to.equal(true);
expect(iD.osmRelation().isDegenerate()).to.equal(true);
});
it('returns false for a relation with members', function () {
expect(iD.Relation({members: [{id: 'a', role: 'inner'}]}).isDegenerate()).to.equal(false);
expect(iD.osmRelation({members: [{id: 'a', role: 'inner'}]}).isDegenerate()).to.equal(false);
});
});
describe('#memberByRole', function () {
it('returns the first member with the given role', function () {
var r = iD.Relation({members: [
var r = iD.osmRelation({members: [
{id: 'a', role: 'inner'},
{id: 'b', role: 'outer'},
{id: 'c', role: 'outer'}]});
@@ -167,13 +167,13 @@ describe('iD.osmRelation', function () {
});
it('returns undefined if no members have the given role', function () {
expect(iD.Relation().memberByRole('outer')).to.be.undefined;
expect(iD.osmRelation().memberByRole('outer')).to.be.undefined;
});
});
describe('#memberById', function () {
it('returns the first member with the given id', function () {
var r = iD.Relation({members: [
var r = iD.osmRelation({members: [
{id: 'a', role: 'outer'},
{id: 'b', role: 'outer'},
{id: 'b', role: 'inner'}]});
@@ -181,101 +181,247 @@ describe('iD.osmRelation', function () {
});
it('returns undefined if no members have the given role', function () {
expect(iD.Relation().memberById('b')).to.be.undefined;
expect(iD.osmRelation().memberById('b')).to.be.undefined;
});
});
describe('#isRestriction', function () {
it('returns true for \'restriction\' type', function () {
expect(iD.Relation({tags: {type: 'restriction'}}).isRestriction()).to.be.true;
expect(iD.osmRelation({tags: {type: 'restriction'}}).isRestriction()).to.be.true;
});
it('returns true for \'restriction:type\' types', function () {
expect(iD.Relation({tags: {type: 'restriction:bus'}}).isRestriction()).to.be.true;
expect(iD.osmRelation({tags: {type: 'restriction:bus'}}).isRestriction()).to.be.true;
});
it('returns false otherwise', function () {
expect(iD.Relation().isRestriction()).to.be.false;
expect(iD.Relation({tags: {type: 'multipolygon'}}).isRestriction()).to.be.false;
expect(iD.osmRelation().isRestriction()).to.be.false;
expect(iD.osmRelation({tags: {type: 'multipolygon'}}).isRestriction()).to.be.false;
});
});
describe('#isValidRestriction', function () {
it('not a restriction', function () {
var r = iD.osmRelation({ id: 'r', tags: { type: 'multipolygon' }});
var graph = iD.coreGraph([r]);
expect(r.isValidRestriction(graph)).to.be.false;
});
it('typical restriction (from way, via node, to way) is valid', function () {
var f = iD.osmWay({id: 'f'});
var v = iD.osmNode({id: 'v'});
var t = iD.osmWay({id: 't'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_left_turn' },
members: [
{ role: 'from', id: 'f', type: 'way' },
{ role: 'via', id: 'v', type: 'node' },
{ role: 'to', id: 't', type: 'way' },
]
});
var graph = iD.coreGraph([f, v, t, r]);
expect(r.isValidRestriction(graph)).to.be.true;
});
it('multiple froms, normal restriction is invalid', function () {
var f1 = iD.osmWay({id: 'f1'});
var f2 = iD.osmWay({id: 'f2'});
var v = iD.osmNode({id: 'v'});
var t = iD.osmWay({id: 't'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_left_turn' },
members: [
{ role: 'from', id: 'f1', type: 'way' },
{ role: 'from', id: 'f2', type: 'way' },
{ role: 'via', id: 'v', type: 'node' },
{ role: 'to', id: 't', type: 'way' },
]
});
var graph = iD.coreGraph([f1, f2, v, t, r]);
expect(r.isValidRestriction(graph)).to.be.false;
});
it('multiple froms, no_entry restriction is valid', function () {
var f1 = iD.osmWay({id: 'f1'});
var f2 = iD.osmWay({id: 'f2'});
var v = iD.osmNode({id: 'v'});
var t = iD.osmWay({id: 't'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_entry' },
members: [
{ role: 'from', id: 'f1', type: 'way' },
{ role: 'from', id: 'f2', type: 'way' },
{ role: 'via', id: 'v', type: 'node' },
{ role: 'to', id: 't', type: 'way' },
]
});
var graph = iD.coreGraph([f1, f2, v, t, r]);
expect(r.isValidRestriction(graph)).to.be.true;
});
it('multiple tos, normal restriction is invalid', function () {
var f = iD.osmWay({id: 'f'});
var v = iD.osmNode({id: 'v'});
var t1 = iD.osmWay({id: 't1'});
var t2 = iD.osmWay({id: 't2'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_left_turn' },
members: [
{ role: 'from', id: 'f', type: 'way' },
{ role: 'via', id: 'v', type: 'node' },
{ role: 'to', id: 't1', type: 'way' },
{ role: 'to', id: 't2', type: 'way' },
]
});
var graph = iD.coreGraph([f, v, t1, t2, r]);
expect(r.isValidRestriction(graph)).to.be.false;
});
it('multiple tos, no_exit restriction is valid', function () {
var f = iD.osmWay({id: 'f'});
var v = iD.osmNode({id: 'v'});
var t1 = iD.osmWay({id: 't1'});
var t2 = iD.osmWay({id: 't2'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_exit' },
members: [
{ role: 'from', id: 'f', type: 'way' },
{ role: 'via', id: 'v', type: 'node' },
{ role: 'to', id: 't1', type: 'way' },
{ role: 'to', id: 't2', type: 'way' },
]
});
var graph = iD.coreGraph([f, v, t1, t2, r]);
expect(r.isValidRestriction(graph)).to.be.true;
});
it('multiple vias, with some as node is invalid', function () {
var f = iD.osmWay({id: 'f'});
var v1 = iD.osmNode({id: 'v1'});
var v2 = iD.osmWay({id: 'v2'});
var t = iD.osmWay({id: 't'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_left_turn' },
members: [
{ role: 'from', id: 'f', type: 'way' },
{ role: 'via', id: 'v1', type: 'node' },
{ role: 'via', id: 'v2', type: 'way' },
{ role: 'to', id: 't', type: 'way' },
]
});
var graph = iD.coreGraph([f, v1, v2, t, r]);
expect(r.isValidRestriction(graph)).to.be.false;
});
it('multiple vias, with all as way is valid', function () {
var f = iD.osmWay({id: 'f'});
var v1 = iD.osmWay({id: 'v1'});
var v2 = iD.osmWay({id: 'v2'});
var t = iD.osmWay({id: 't'});
var r = iD.osmRelation({
id: 'r',
tags: { type: 'restriction', restriction: 'no_left_turn' },
members: [
{ role: 'from', id: 'f', type: 'way' },
{ role: 'via', id: 'v1', type: 'way' },
{ role: 'via', id: 'v2', type: 'way' },
{ role: 'to', id: 't', type: 'way' },
]
});
var graph = iD.coreGraph([f, v1, v2, t, r]);
expect(r.isValidRestriction(graph)).to.be.true;
});
});
describe('#indexedMembers', function () {
it('returns an array of members extended with indexes', function () {
var r = iD.Relation({members: [{id: '1'}, {id: '3'}]});
var r = iD.osmRelation({members: [{id: '1'}, {id: '3'}]});
expect(r.indexedMembers()).to.eql([{id: '1', index: 0}, {id: '3', index: 1}]);
});
});
describe('#addMember', function () {
it('adds a member at the end of the relation', function () {
var r = iD.Relation();
var r = iD.osmRelation();
expect(r.addMember({id: '1'}).members).to.eql([{id: '1'}]);
});
it('adds a member at index 0', function () {
var r = iD.Relation({members: [{id: '1'}]});
var r = iD.osmRelation({members: [{id: '1'}]});
expect(r.addMember({id: '2'}, 0).members).to.eql([{id: '2'}, {id: '1'}]);
});
it('adds a member at a positive index', function () {
var r = iD.Relation({members: [{id: '1'}, {id: '3'}]});
var r = iD.osmRelation({members: [{id: '1'}, {id: '3'}]});
expect(r.addMember({id: '2'}, 1).members).to.eql([{id: '1'}, {id: '2'}, {id: '3'}]);
});
it('adds a member at a negative index', function () {
var r = iD.Relation({members: [{id: '1'}, {id: '3'}]});
var r = iD.osmRelation({members: [{id: '1'}, {id: '3'}]});
expect(r.addMember({id: '2'}, -1).members).to.eql([{id: '1'}, {id: '2'}, {id: '3'}]);
});
});
describe('#updateMember', function () {
it('updates the properties of the relation member at the specified index', function () {
var r = iD.Relation({members: [{role: 'forward'}]});
var r = iD.osmRelation({members: [{role: 'forward'}]});
expect(r.updateMember({role: 'backward'}, 0).members).to.eql([{role: 'backward'}]);
});
});
describe('#removeMember', function () {
it('removes the member at the specified index', function () {
var r = iD.Relation({members: [{id: 'a'}, {id: 'b'}, {id: 'c'}]});
var r = iD.osmRelation({members: [{id: 'a'}, {id: 'b'}, {id: 'c'}]});
expect(r.removeMember(1).members).to.eql([{id: 'a'}, {id: 'c'}]);
});
});
describe('#removeMembersWithID', function () {
it('removes members with the given ID', function () {
var r = iD.Relation({members: [{id: 'a'}, {id: 'b'}, {id: 'a'}]});
var r = iD.osmRelation({members: [{id: 'a'}, {id: 'b'}, {id: 'a'}]});
expect(r.removeMembersWithID('a').members).to.eql([{id: 'b'}]);
});
});
describe('#replaceMember', function () {
it('returns self if self does not contain needle', function () {
var r = iD.Relation({members: []});
var r = iD.osmRelation({members: []});
expect(r.replaceMember({id: 'a'}, {id: 'b'})).to.equal(r);
});
it('replaces a member which doesn\'t already exist', function () {
var r = iD.Relation({members: [{id: 'a', role: 'a'}]});
var r = iD.osmRelation({members: [{id: 'a', role: 'a'}]});
expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members)
.to.eql([{id: 'b', role: 'a', type: 'node'}]);
});
it('preserves the existing role', function () {
var r = iD.Relation({members: [{id: 'a', role: 'a', type: 'node'}]});
var r = iD.osmRelation({members: [{id: 'a', role: 'a', type: 'node'}]});
expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'node'}).members)
.to.eql([{id: 'b', role: 'a', type: 'node'}]);
});
it('uses the replacement type', function () {
var r = iD.Relation({members: [{id: 'a', role: 'a', type: 'node'}]});
var r = iD.osmRelation({members: [{id: 'a', role: 'a', type: 'node'}]});
expect(r.replaceMember({id: 'a'}, {id: 'b', type: 'way'}).members)
.to.eql([{id: 'b', role: 'a', type: 'way'}]);
});
it('removes members if replacing them would produce duplicates', function () {
var r = iD.Relation({members: [
var r = iD.osmRelation({members: [
{id: 'a', role: 'b', type: 'node'},
{id: 'b', role: 'b', type: 'node'}
]});
@@ -283,7 +429,7 @@ describe('iD.osmRelation', function () {
.to.eql([{id: 'b', role: 'b', type: 'node'}]);
});
it('keeps duplicate members if `keepDuplicates = true`', function () {
var r = iD.Relation({members: [
var r = iD.osmRelation({members: [
{id: 'a', role: 'b', type: 'node'},
{id: 'b', role: 'b', type: 'node'}
]});
@@ -294,7 +440,7 @@ describe('iD.osmRelation', function () {
describe('#asJXON', function () {
it('converts a relation to jxon', function() {
var relation = iD.Relation({id: 'r-1', members: [{id: 'w1', role: 'forward', type: 'way'}], tags: {type: 'route'}});
var relation = iD.osmRelation({id: 'r-1', members: [{id: 'w1', role: 'forward', type: 'way'}], tags: {type: 'route'}});
expect(relation.asJXON()).to.eql({relation: {
'@id': '-1',
'@version': 0,
@@ -303,56 +449,56 @@ describe('iD.osmRelation', function () {
});
it('includes changeset if provided', function() {
expect(iD.Relation().asJXON('1234').relation['@changeset']).to.equal('1234');
expect(iD.osmRelation().asJXON('1234').relation['@changeset']).to.equal('1234');
});
});
describe('#asGeoJSON', function (){
describe('#asGeoJSON', function () {
it('converts a multipolygon to a GeoJSON MultiPolygon geometry', function() {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w, r]),
json = r.asGeoJSON(g);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w, r]);
var json = r.asGeoJSON(g);
expect(json.type).to.equal('MultiPolygon');
expect(json.coordinates).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]);
});
it('forces clockwise winding order for outer multipolygon ways', function() {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [0, 1]}),
c = iD.Node({loc: [1, 0]}),
w = iD.Way({nodes: [a.id, c.id, b.id, a.id]}),
r = iD.Relation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w, r]),
json = r.asGeoJSON(g);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [0, 1]});
var c = iD.osmNode({loc: [1, 0]});
var w = iD.osmWay({nodes: [a.id, c.id, b.id, a.id]});
var r = iD.osmRelation({tags: {type: 'multipolygon'}, members: [{id: w.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w, r]);
var json = r.asGeoJSON(g);
expect(json.coordinates[0][0]).to.eql([a.loc, b.loc, c.loc, a.loc]);
});
it('forces counterclockwise winding order for inner multipolygon ways', function() {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [0, 1]}),
c = iD.Node({loc: [1, 0]}),
d = iD.Node({loc: [0.1, 0.1]}),
e = iD.Node({loc: [0.1, 0.2]}),
f = iD.Node({loc: [0.2, 0.1]}),
outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
inner = iD.Way({nodes: [d.id, e.id, f.id, d.id]}),
r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}),
g = iD.Graph([a, b, c, d, e, f, outer, inner, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [0, 1]});
var c = iD.osmNode({loc: [1, 0]});
var d = iD.osmNode({loc: [0.1, 0.1]});
var e = iD.osmNode({loc: [0.1, 0.2]});
var f = iD.osmNode({loc: [0.2, 0.1]});
var outer = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var inner = iD.osmWay({nodes: [d.id, e.id, f.id, d.id]});
var r = iD.osmRelation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]});
var g = iD.coreGraph([a, b, c, d, e, f, outer, inner, r]);
expect(r.multipolygon(g)[0][1]).to.eql([d.loc, f.loc, e.loc, d.loc]);
});
it('converts a relation to a GeoJSON FeatureCollection', function() {
var a = iD.Node({loc: [1, 1]}),
r = iD.Relation({tags: {type: 'type'}, members: [{id: a.id, role: 'role'}]}),
g = iD.Graph([a, r]),
json = r.asGeoJSON(g);
var a = iD.osmNode({loc: [1, 1]});
var r = iD.osmRelation({tags: {type: 'type'}, members: [{id: a.id, role: 'role'}]});
var g = iD.coreGraph([a, r]);
var json = r.asGeoJSON(g);
expect(json.type).to.equal('FeatureCollection');
expect(json.properties).to.eql({type: 'type'});
@@ -365,214 +511,220 @@ describe('iD.osmRelation', function () {
describe('#multipolygon', function () {
specify('single polygon consisting of a single way', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
r = iD.Relation({members: [{id: w.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: w.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]);
});
specify('single polygon consisting of multiple ways', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
w1 = iD.Way({nodes: [a.id, b.id]}),
w2 = iD.Way({nodes: [b.id, c.id, a.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var w1 = iD.osmWay({nodes: [a.id, b.id]});
var w2 = iD.osmWay({nodes: [b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]);
});
specify('single polygon consisting of multiple ways, one needing reversal', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
w1 = iD.Way({nodes: [a.id, b.id]}),
w2 = iD.Way({nodes: [a.id, c.id, b.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var w1 = iD.osmWay({nodes: [a.id, b.id]});
var w2 = iD.osmWay({nodes: [a.id, c.id, b.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]);
});
specify('multiple polygons consisting of single ways', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
d = iD.Node({loc: [4, 4]}),
e = iD.Node({loc: [6, 6]}),
f = iD.Node({loc: [5, 5]}),
w1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
w2 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, d, e, f, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var d = iD.osmNode({loc: [4, 4]});
var e = iD.osmNode({loc: [6, 6]});
var f = iD.osmNode({loc: [5, 5]});
var w1 = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var w2 = iD.osmWay({nodes: [d.id, e.id, f.id, d.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, d, e, f, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]], [[d.loc, e.loc, f.loc, d.loc]]]);
});
specify('invalid geometry: unclosed ring consisting of a single way', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
w = iD.Way({nodes: [a.id, b.id, c.id]}),
r = iD.Relation({members: [{id: w.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var w = iD.osmWay({nodes: [a.id, b.id, c.id]});
var r = iD.osmRelation({members: [{id: w.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]);
});
specify('invalid geometry: unclosed ring consisting of multiple ways', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [3, 3]}),
c = iD.Node({loc: [2, 2]}),
w1 = iD.Way({nodes: [a.id, b.id]}),
w2 = iD.Way({nodes: [b.id, c.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [3, 3]});
var c = iD.osmNode({loc: [2, 2]});
var w1 = iD.osmWay({nodes: [a.id, b.id]});
var w2 = iD.osmWay({nodes: [b.id, c.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]);
});
specify('invalid geometry: unclosed ring consisting of multiple ways, alternate order', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [2, 2]}),
c = iD.Node({loc: [3, 3]}),
d = iD.Node({loc: [4, 4]}),
w1 = iD.Way({nodes: [c.id, d.id]}),
w2 = iD.Way({nodes: [a.id, b.id, c.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, d, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var d = iD.osmNode({loc: [4, 4]});
var w1 = iD.osmWay({nodes: [c.id, d.id]});
var w2 = iD.osmWay({nodes: [a.id, b.id, c.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, d, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[d.loc, c.loc, b.loc, a.loc]]]);
});
specify('invalid geometry: unclosed ring consisting of multiple ways, one needing reversal', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [2, 2]}),
c = iD.Node({loc: [3, 3]}),
d = iD.Node({loc: [4, 4]}),
w1 = iD.Way({nodes: [a.id, b.id, c.id]}),
w2 = iD.Way({nodes: [d.id, c.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, d, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var d = iD.osmNode({loc: [4, 4]});
var w1 = iD.osmWay({nodes: [a.id, b.id, c.id]});
var w2 = iD.osmWay({nodes: [d.id, c.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, d, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[d.loc, c.loc, b.loc, a.loc]]]);
});
specify('invalid geometry: unclosed ring consisting of multiple ways, one needing reversal, alternate order', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [2, 2]}),
c = iD.Node({loc: [3, 3]}),
d = iD.Node({loc: [4, 4]}),
w1 = iD.Way({nodes: [c.id, d.id]}),
w2 = iD.Way({nodes: [c.id, b.id, a.id]}),
r = iD.Relation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]}),
g = iD.Graph([a, b, c, d, w1, w2, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var d = iD.osmNode({loc: [4, 4]});
var w1 = iD.osmWay({nodes: [c.id, d.id]});
var w2 = iD.osmWay({nodes: [c.id, b.id, a.id]});
var r = iD.osmRelation({members: [{id: w1.id, type: 'way'}, {id: w2.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, d, w1, w2, r]);
expect(r.multipolygon(g)).to.eql([[[d.loc, c.loc, b.loc, a.loc]]]);
});
specify('single polygon with single single-way inner', function () {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [0, 1]}),
c = iD.Node({loc: [1, 0]}),
d = iD.Node({loc: [0.1, 0.1]}),
e = iD.Node({loc: [0.2, 0.1]}),
f = iD.Node({loc: [0.1, 0.2]}),
outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
inner = iD.Way({nodes: [d.id, e.id, f.id, d.id]}),
r = iD.Relation({members: [{id: outer.id, type: 'way'}, {id: inner.id, role: 'inner', type: 'way'}]}),
g = iD.Graph([a, b, c, d, e, f, outer, inner, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [0, 1]});
var c = iD.osmNode({loc: [1, 0]});
var d = iD.osmNode({loc: [0.1, 0.1]});
var e = iD.osmNode({loc: [0.2, 0.1]});
var f = iD.osmNode({loc: [0.1, 0.2]});
var outer = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var inner = iD.osmWay({nodes: [d.id, e.id, f.id, d.id]});
var r = iD.osmRelation({members: [
{id: outer.id, type: 'way'},
{id: inner.id, role: 'inner', type: 'way'}
]});
var g = iD.coreGraph([a, b, c, d, e, f, outer, inner, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]);
});
specify('single polygon with single multi-way inner', function () {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [0, 1]}),
c = iD.Node({loc: [1, 0]}),
d = iD.Node({loc: [0.1, 0.1]}),
e = iD.Node({loc: [0.2, 0.1]}),
f = iD.Node({loc: [0.2, 0.1]}),
outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
inner1 = iD.Way({nodes: [d.id, e.id]}),
inner2 = iD.Way({nodes: [e.id, f.id, d.id]}),
r = iD.Relation({members: [
{id: outer.id, type: 'way'},
{id: inner1.id, role: 'inner', type: 'way'},
{id: inner2.id, role: 'inner', type: 'way'}]}),
graph = iD.Graph([a, b, c, d, e, f, outer, inner1, inner2, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [0, 1]});
var c = iD.osmNode({loc: [1, 0]});
var d = iD.osmNode({loc: [0.1, 0.1]});
var e = iD.osmNode({loc: [0.2, 0.1]});
var f = iD.osmNode({loc: [0.2, 0.1]});
var outer = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var inner1 = iD.osmWay({nodes: [d.id, e.id]});
var inner2 = iD.osmWay({nodes: [e.id, f.id, d.id]});
var r = iD.osmRelation({members: [
{id: outer.id, type: 'way'},
{id: inner1.id, role: 'inner', type: 'way'},
{id: inner2.id, role: 'inner', type: 'way'}
]});
var graph = iD.coreGraph([a, b, c, d, e, f, outer, inner1, inner2, r]);
expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]]]);
});
specify('single polygon with multiple single-way inners', function () {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [0, 1]}),
c = iD.Node({loc: [1, 0]}),
d = iD.Node({loc: [0.1, 0.1]}),
e = iD.Node({loc: [0.2, 0.1]}),
f = iD.Node({loc: [0.1, 0.2]}),
g = iD.Node({loc: [0.2, 0.2]}),
h = iD.Node({loc: [0.3, 0.2]}),
i = iD.Node({loc: [0.2, 0.3]}),
outer = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
inner1 = iD.Way({nodes: [d.id, e.id, f.id, d.id]}),
inner2 = iD.Way({nodes: [g.id, h.id, i.id, g.id]}),
r = iD.Relation({members: [
{id: outer.id, type: 'way'},
{id: inner1.id, role: 'inner', type: 'way'},
{id: inner2.id, role: 'inner', type: 'way'}]}),
graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer, inner1, inner2, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [0, 1]});
var c = iD.osmNode({loc: [1, 0]});
var d = iD.osmNode({loc: [0.1, 0.1]});
var e = iD.osmNode({loc: [0.2, 0.1]});
var f = iD.osmNode({loc: [0.1, 0.2]});
var g = iD.osmNode({loc: [0.2, 0.2]});
var h = iD.osmNode({loc: [0.3, 0.2]});
var i = iD.osmNode({loc: [0.2, 0.3]});
var outer = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var inner1 = iD.osmWay({nodes: [d.id, e.id, f.id, d.id]});
var inner2 = iD.osmWay({nodes: [g.id, h.id, i.id, g.id]});
var r = iD.osmRelation({members: [
{id: outer.id, type: 'way'},
{id: inner1.id, role: 'inner', type: 'way'},
{id: inner2.id, role: 'inner', type: 'way'}
]});
var graph = iD.coreGraph([a, b, c, d, e, f, g, h, i, outer, inner1, inner2, r]);
expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc], [g.loc, h.loc, i.loc, g.loc]]]);
});
specify('multiple polygons with single single-way inner', function () {
var a = iD.Node({loc: [0, 0]}),
b = iD.Node({loc: [0, 1]}),
c = iD.Node({loc: [1, 0]}),
d = iD.Node({loc: [0.1, 0.1]}),
e = iD.Node({loc: [0.2, 0.1]}),
f = iD.Node({loc: [0.1, 0.2]}),
g = iD.Node({loc: [0, 0]}),
h = iD.Node({loc: [0, -1]}),
i = iD.Node({loc: [-1, 0]}),
outer1 = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
outer2 = iD.Way({nodes: [g.id, h.id, i.id, g.id]}),
inner = iD.Way({nodes: [d.id, e.id, f.id, d.id]}),
r = iD.Relation({members: [
{id: outer1.id, type: 'way'},
{id: outer2.id, type: 'way'},
{id: inner.id, role: 'inner', type: 'way'}]}),
graph = iD.Graph([a, b, c, d, e, f, g, h, i, outer1, outer2, inner, r]);
var a = iD.osmNode({loc: [0, 0]});
var b = iD.osmNode({loc: [0, 1]});
var c = iD.osmNode({loc: [1, 0]});
var d = iD.osmNode({loc: [0.1, 0.1]});
var e = iD.osmNode({loc: [0.2, 0.1]});
var f = iD.osmNode({loc: [0.1, 0.2]});
var g = iD.osmNode({loc: [0, 0]});
var h = iD.osmNode({loc: [0, -1]});
var i = iD.osmNode({loc: [-1, 0]});
var outer1 = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var outer2 = iD.osmWay({nodes: [g.id, h.id, i.id, g.id]});
var inner = iD.osmWay({nodes: [d.id, e.id, f.id, d.id]});
var r = iD.osmRelation({members: [
{id: outer1.id, type: 'way'},
{id: outer2.id, type: 'way'},
{id: inner.id, role: 'inner', type: 'way'}
]});
var graph = iD.coreGraph([a, b, c, d, e, f, g, h, i, outer1, outer2, inner, r]);
expect(r.multipolygon(graph)).to.eql([[[a.loc, b.loc, c.loc, a.loc], [d.loc, e.loc, f.loc, d.loc]], [[g.loc, h.loc, i.loc, g.loc]]]);
});
specify('invalid geometry: unmatched inner', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [2, 2]}),
c = iD.Node({loc: [3, 3]}),
w = iD.Way({nodes: [a.id, b.id, c.id, a.id]}),
r = iD.Relation({members: [{id: w.id, role: 'inner', type: 'way'}]}),
g = iD.Graph([a, b, c, w, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var w = iD.osmWay({nodes: [a.id, b.id, c.id, a.id]});
var r = iD.osmRelation({members: [{id: w.id, role: 'inner', type: 'way'}]});
var g = iD.coreGraph([a, b, c, w, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc, a.loc]]]);
});
specify('incomplete relation', function () {
var a = iD.Node({loc: [1, 1]}),
b = iD.Node({loc: [2, 2]}),
c = iD.Node({loc: [3, 3]}),
w1 = iD.Way({nodes: [a.id, b.id, c.id]}),
w2 = iD.Way(),
r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}),
g = iD.Graph([a, b, c, w1, r]);
var a = iD.osmNode({loc: [1, 1]});
var b = iD.osmNode({loc: [2, 2]});
var c = iD.osmNode({loc: [3, 3]});
var w1 = iD.osmWay({nodes: [a.id, b.id, c.id]});
var w2 = iD.osmWay();
var r = iD.osmRelation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]});
var g = iD.coreGraph([a, b, c, w1, r]);
expect(r.multipolygon(g)).to.eql([[[a.loc, b.loc, c.loc]]]);
});
@@ -580,17 +732,17 @@ describe('iD.osmRelation', function () {
describe('.creationOrder comparator', function () {
specify('orders existing relations newest-first', function () {
var a = iD.Relation({ id: 'r1' }),
b = iD.Relation({ id: 'r2' });
expect(iD.Relation.creationOrder(a, b)).to.be.above(0);
expect(iD.Relation.creationOrder(b, a)).to.be.below(0);
var a = iD.osmRelation({ id: 'r1' });
var b = iD.osmRelation({ id: 'r2' });
expect(iD.osmRelation.creationOrder(a, b)).to.be.above(0);
expect(iD.osmRelation.creationOrder(b, a)).to.be.below(0);
});
specify('orders new relations newest-first', function () {
var a = iD.Relation({ id: 'r-1' }),
b = iD.Relation({ id: 'r-2' });
expect(iD.Relation.creationOrder(a, b)).to.be.above(0);
expect(iD.Relation.creationOrder(b, a)).to.be.below(0);
var a = iD.osmRelation({ id: 'r-1' });
var b = iD.osmRelation({ id: 'r-2' });
expect(iD.osmRelation.creationOrder(a, b)).to.be.above(0);
expect(iD.osmRelation.creationOrder(b, a)).to.be.below(0);
});
});
});