diff --git a/css/20_map.css b/css/20_map.css index 5cd5771ac..44c4f50ae 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -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 { diff --git a/css/80_app.css b/css/80_app.css index 6fc8cc30b..f0fa88be1 100644 --- a/css/80_app.css +++ b/css/80_app.css @@ -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; } diff --git a/data/core.yaml b/data/core.yaml index cc9511458..4d487fa0d 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -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 diff --git a/dist/img/tr_inspect.gif b/dist/img/tr_inspect.gif new file mode 100644 index 000000000..57c889c12 Binary files /dev/null and b/dist/img/tr_inspect.gif differ diff --git a/dist/img/tr_modify.gif b/dist/img/tr_modify.gif new file mode 100644 index 000000000..5931416f7 Binary files /dev/null and b/dist/img/tr_modify.gif differ diff --git a/dist/locales/en.json b/dist/locales/en.json index bf48be27f..a5f9bcbe6 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -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": { diff --git a/modules/actions/restrict_turn.js b/modules/actions/restrict_turn.js index eb59644a4..c21ace82c 100644 --- a/modules/actions/restrict_turn.js +++ b/modules/actions/restrict_turn.js @@ -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: , way: }, -// via: { node: }, -// to: { node: , way: }, -// 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 })); }; } diff --git a/modules/actions/split.js b/modules/actions/split.js index 1763274ac..982632484 100644 --- a/modules/actions/split.js +++ b/modules/actions/split.js @@ -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 diff --git a/modules/actions/unrestrict_turn.js b/modules/actions/unrestrict_turn.js index 3e0a794c5..8d980f459 100644 --- a/modules/actions/unrestrict_turn.js +++ b/modules/actions/unrestrict_turn.js @@ -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: , way: }, -// via: { node: }, -// to: { node: , way: }, -// restriction: -// } -// -// In the simple case, `restriction` is a reference to a `no_*` restriction -// on the turn itself. In this case, it is simply deleted. -// -// The more complex case is where `restriction` references an `only_*` -// restriction on a different turn in the same intersection. In that case, -// that restriction is also deleted, but at the same time restrictions on -// the turns other than the first two are created. +// `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); }; } diff --git a/modules/osm/intersection.js b/modules/osm/intersection.js index 59e87e525..17c42beb2 100644 --- a/modules/osm/intersection.js +++ b/modules/osm/intersection.js @@ -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) diff --git a/modules/osm/relation.js b/modules/osm/relation.js index b44dbd420..8532990ed 100644 --- a/modules/osm/relation.js +++ b/modules/osm/relation.js @@ -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); diff --git a/modules/svg/turns.js b/modules/svg/turns.js index cd16bd0dd..007733afc 100644 --- a/modules/svg/turns.js +++ b/modules/svg/turns.js @@ -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 + ')'; }); diff --git a/modules/ui/field.js b/modules/ui/field.js index bffaf4a9d..4936dd51a 100644 --- a/modules/ui/field.js +++ b/modules/ui/field.js @@ -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]; }); }; diff --git a/modules/ui/field_help.js b/modules/ui/field_help.js new file mode 100644 index 000000000..803e40c67 --- /dev/null +++ b/modules/ui/field_help.js @@ -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; +} diff --git a/modules/ui/fields/restrictions.js b/modules/ui/fields/restrictions.js index dde654d45..0905d5b1e 100644 --- a/modules/ui/fields/restrictions.js +++ b/modules/ui/fields/restrictions.js @@ -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] = '' + t('restriction.help.' + k) + ''; + }); + + 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); diff --git a/modules/ui/index.js b/modules/ui/index.js index 8a3dd80c6..d908b7078 100644 --- a/modules/ui/index.js +++ b/modules/ui/index.js @@ -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'; diff --git a/modules/ui/inspector.js b/modules/ui/inspector.js index b2a398db2..89d1f28ed 100644 --- a/modules/ui/inspector.js +++ b/modules/ui/inspector.js @@ -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; }; diff --git a/modules/ui/tag_reference.js b/modules/ui/tag_reference.js index 4c5b39dcd..81cf4659b 100644 --- a/modules/ui/tag_reference.js +++ b/modules/ui/tag_reference.js @@ -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; }; diff --git a/svg/iD-sprite.json b/svg/iD-sprite.json index 601522a96..58bc82e0a 100644 --- a/svg/iD-sprite.json +++ b/svg/iD-sprite.json @@ -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" }, diff --git a/svg/iD-sprite.src.idraw b/svg/iD-sprite.src.idraw index fbd4f988d..bb831b58e 100644 Binary files a/svg/iD-sprite.src.idraw and b/svg/iD-sprite.src.idraw differ diff --git a/svg/iD-sprite.src.svg b/svg/iD-sprite.src.svg index c71269ebe..dc4bfe7af 100644 --- a/svg/iD-sprite.src.svg +++ b/svg/iD-sprite.src.svg @@ -280,6 +280,15 @@ + + + + + + + + + diff --git a/test/spec/actions/restrict_turn.js b/test/spec/actions/restrict_turn.js index b5f685d96..46f273be7 100644 --- a/test/spec/actions/restrict_turn.js +++ b/test/spec/actions/restrict_turn.js @@ -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'); - }); - }); diff --git a/test/spec/actions/unrestrict_turn.js b/test/spec/actions/unrestrict_turn.js index 991439da8..3c617618d 100644 --- a/test/spec/actions/unrestrict_turn.js +++ b/test/spec/actions/unrestrict_turn.js @@ -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; }); }); diff --git a/test/spec/osm/intersection.js b/test/spec/osm/intersection.js index ac4d481e0..ba4785a49 100644 --- a/test/spec/osm/intersection.js +++ b/test/spec/osm/intersection.js @@ -1,108 +1,103 @@ describe('iD.osmIntersection', function() { describe('highways', function() { it('excludes non-highways', function() { - 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']}) + 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']}) ]); expect(iD.osmIntersection(graph, '*').ways).to.eql([]); }); it('excludes degenerate highways', function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: '*'}), - iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['*'], tags: {highway: 'residential'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '-', nodes: ['*'], tags: {highway: 'residential'}}) ]); - var ids = iD.osmIntersection(graph, '*').ways.map(function (w) { return w.id; }); - expect(ids).to.have.same.members(['=']); - }); - - it('excludes coincident highways', function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: '*'}), - iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['u', '*'], tags: {highway: 'residential'}}) - ]); - expect(iD.osmIntersection(graph, '*').ways).to.eql([]); + var result = iD.osmIntersection(graph, '*').ways; + expect(result.map(function(i) { return i.id; })).to.eql(['=']); }); it('includes line highways', function() { - 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']}) + 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']}) ]); - var ids = iD.osmIntersection(graph, '*').ways.map(function (w) { return w.id; }); - expect(ids).to.have.same.members(['=']); + var result = iD.osmIntersection(graph, '*').ways; + expect(result.map(function(i) { return i.id; })).to.eql(['=']); }); it('excludes area highways', function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: '*'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'pedestrian', area: 'yes'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmWay({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'pedestrian', area: 'yes'}}) ]); expect(iD.osmIntersection(graph, '*').ways).to.eql([]); }); it('auto-splits highways at the intersection', function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: '*'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'residential'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmWay({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'residential'}}) ]); - var ids = iD.osmIntersection(graph, '*').ways.map(function (w) { return w.id; }); - expect(ids).to.have.ordered.members(['=-a', '=-b']); + expect(iD.osmIntersection(graph, '*').ways.length).to.eql(2); }); }); describe('#turns', function() { it('permits turns onto a way forward', 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'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + 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'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_-'); + expect(turns[1].u).to.be.not.ok; }); it('permits turns onto a way backward', 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'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + 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'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_-'); + expect(turns[1].u).to.be.not.ok; }); it('permits turns from a way that must be split', function() { @@ -111,33 +106,29 @@ describe('iD.osmIntersection', function() { // u===* // | // 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', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', '*', 'x'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('w'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmNode({id: 'x'}), + iD.osmWay({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '-', nodes: ['w', '*', 'x'], tags: {highway: 'residential'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('-'); expect(turns.length).to.eql(3); - expect(turns[0]).to.eql({ - from: {node: 'w', way: '-'}, - via: {node: '*'}, - to: {node: 'u', way: '='} - }); - expect(turns[1]).to.eql({ - from: {node: 'w', way: '-'}, - via: {node: '*'}, - to: {node: 'x', way: '-'} - }); - expect(turns[2]).to.eql({ - from: {node: 'w', way: '-'}, - via: {node: '*'}, - to: {node: 'w', way: '-'}, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('-_*_='); + expect(turns[0].u).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('-_*_-'); + expect(turns[1].u).to.be.true; + + expect(turns[2]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[2].key).to.match(/^-\_\*\_w-\d+$/); // new way + expect(turns[2].u).to.be.not.ok; }); it('permits turns to a way that must be split', function() { @@ -146,244 +137,242 @@ describe('iD.osmIntersection', function() { // u===* // | // 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', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', '*', 'x'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmNode({id: 'x'}), + iD.osmWay({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '-', nodes: ['w', '*', 'x'], tags: {highway: 'residential'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(3); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'x', way: '-'} - }); - expect(turns[2]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'u', way: '='}, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_-'); + expect(turns[1].u).to.be.not.ok; + + expect(turns[2]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[2].key).to.match(/^=\_\*\_w-\d+$/); // new way + expect(turns[2].u).to.be.not.ok; }); it('permits turns from a oneway forward', function() { // u===>v----w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: '*'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential', oneway: 'yes'}}), - iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmWay({id: '=', nodes: ['u', '*'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.osmWay({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]); - expect(turns).to.eql([{ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }]); + var turns = iD.osmIntersection(graph, '*').turns('='); + expect(turns.length).to.eql(1); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_-'); + expect(turns[0].u).to.be.not.ok; }); it('permits turns from a reverse oneway backward', 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', oneway: '-1'}}), - iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential', oneway: '-1'}}), + iD.osmWay({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]); - expect(turns).to.eql([{ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }]); + var turns = iD.osmIntersection(graph, '*').turns('='); + expect(turns.length).to.eql(1); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_-'); + expect(turns[0].u).to.be.not.ok; }); it('omits turns from a oneway backward', 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', oneway: 'yes'}}), - iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.osmWay({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) ]); expect(iD.osmIntersection(graph, '*').turns('u')).to.eql([]); }); it('omits turns from a reverse oneway forward', 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', oneway: '-1'}}), - iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'w'}), + iD.osmWay({id: '=', nodes: ['u', '*'], tags: {highway: 'residential', oneway: '-1'}}), + iD.osmWay({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) ]); expect(iD.osmIntersection(graph, '*').turns('u')).to.eql([]); }); it('permits turns onto a oneway forward', 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', oneway: 'yes'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + 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', oneway: 'yes'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_-'); + expect(turns[1].u).to.be.not.ok; }); it('permits turns onto a reverse oneway backward', 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', oneway: '-1'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + 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', oneway: '-1'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'} - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_-'); + expect(turns[1].u).to.be.not.ok; }); it('omits turns onto a oneway backward', 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', oneway: 'yes'}}) + 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', oneway: 'yes'}}) ]); - expect(iD.osmIntersection(graph, '*').turns('u').length).to.eql(1); + + var turns = iD.osmIntersection(graph, '*').turns('='); + expect(turns.length).to.eql(1); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; }); it('omits turns onto a reverse oneway forward', 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', oneway: '-1'}}) + 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', oneway: '-1'}}) ]); - expect(iD.osmIntersection(graph, '*').turns('u').length).to.eql(1); - }); - it('includes U-turns', 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'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var turns = iD.osmIntersection(graph, '*').turns('='); + expect(turns.length).to.eql(1); - expect(turns.length).to.eql(2); - expect(turns[1]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'u', way: '='}, - u: true - }); + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; }); it('restricts turns with a restriction relation', 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'} - ]}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + 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 turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'}, - restriction: 'r' - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_-'); + expect(turns[1].u).to.be.not.ok; + expect(turns[1].restrictionID).to.eql('r'); + expect(turns[1].direct).to.be.true; + expect(turns[1].only).to.be.not.ok; }); it('restricts turns affected by an only_* restriction relation', function() { // u====*~~~~v // | // w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Node({id: '*'}), - iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '~', nodes: ['v', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', '*'], tags: {highway: 'residential'}}), - iD.Relation({id: 'r', tags: {type: 'restriction', restriction: 'only_right_turn'}, members: [ - {id: '=', role: 'from', type: 'way'}, - {id: '-', role: 'to', type: 'way'}, - {id: '*', role: 'via', type: 'node'} - ]}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'u'}), + iD.osmNode({id: 'v'}), + iD.osmNode({id: 'w'}), + iD.osmNode({id: '*'}), + iD.osmWay({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '~', nodes: ['v', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '-', nodes: ['w', '*'], tags: {highway: 'residential'}}), + iD.osmRelation({id: 'r', tags: {type: 'restriction', restriction: 'only_right_turn'}, members: [ + {id: '=', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: '*', role: 'via', type: 'node'} + ]}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(3); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'v', way: '~'}, - restriction: 'r', - indirect_restriction: true - }); - expect(turns[1]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'w', way: '-'}, - restriction: 'r' - }); - expect(turns[2]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'u', way: '='}, - restriction: 'r', - indirect_restriction: true, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_='); + expect(turns[0].u).to.be.true; + expect(turns[1].direct).to.be.false; + expect(turns[1].only).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_~'); + expect(turns[1].restrictionID).to.eql('r'); + expect(turns[1].u).to.be.not.ok; + expect(turns[1].direct).to.be.false; + expect(turns[1].only).to.be.not.ok; + + expect(turns[2]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[2].key).to.eql('=_*_-'); + expect(turns[2].restrictionID).to.eql('r'); + expect(turns[2].u).to.be.not.ok; + expect(turns[2].direct).to.be.true; + expect(turns[2].only).to.be.true; }); it('permits turns to a circular way', function() { @@ -392,34 +381,30 @@ describe('iD.osmIntersection', function() { // | | // a -- * === u // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Node({id: 'u'}), - iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'u'}), + iD.osmWay({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(3); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'a', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'c', way: '-'} - }); - expect(turns[2]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'u', way: '='}, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_-'); + expect(turns[0].u).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_='); + expect(turns[1].u).to.be.true; + + expect(turns[2]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[2].key).to.match(/^=\_\*\_w-\d+$/); // new way + expect(turns[2].u).to.be.not.ok; }); it('permits turns from a circular way', function() { @@ -428,34 +413,30 @@ describe('iD.osmIntersection', function() { // | | // a -- * === u // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Node({id: 'u'}), - iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential'}}), - iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('a'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'u'}), + iD.osmWay({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential'}}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('-'); expect(turns.length).to.eql(3); - expect(turns[0]).to.eql({ - from: {node: 'a', way: '-'}, - via: {node: '*'}, - to: {node: 'c', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'a', way: '-'}, - via: {node: '*'}, - to: {node: 'u', way: '='} - }); - expect(turns[2]).to.eql({ - from: {node: 'a', way: '-'}, - via: {node: '*'}, - to: {node: 'a', way: '-'}, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('-_*_-'); + expect(turns[0].u).to.be.true; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('-_*_='); + expect(turns[1].u).to.be.not.ok; + + expect(turns[2]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[2].key).to.match(/^-\_\*\_w-\d+$/); // new way + expect(turns[2].u).to.be.not.ok; }); it('permits turns to a oneway circular way', function() { @@ -464,29 +445,26 @@ describe('iD.osmIntersection', function() { // | | // a -- * === u // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Node({id: 'u'}), - iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: 'yes'}}), - iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'u'}), + iD.osmWay({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'a', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'u', way: '='}, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_-'); + expect(turns[0].u).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_='); + expect(turns[1].u).to.be.true; }); it('permits turns to a reverse oneway circular way', function() { @@ -495,29 +473,26 @@ describe('iD.osmIntersection', function() { // | | // a -- * === u // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Node({id: 'u'}), - iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: '-1'}}), - iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('u'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'u'}), + iD.osmWay({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: '-1'}}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) + ]); + var turns = iD.osmIntersection(graph, '*').turns('='); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'c', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'u', way: '='}, - via: {node: '*'}, - to: {node: 'u', way: '='}, - u: true - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql('=_*_-'); + expect(turns[0].u).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql('=_*_='); + expect(turns[1].u).to.be.true; }); it('permits turns from a oneway circular way', function() { @@ -526,28 +501,28 @@ describe('iD.osmIntersection', function() { // | | // a -- * === u // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Node({id: 'u'}), - iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: 'yes'}}), - iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('c'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'u'}), + iD.osmWay({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) + ]); + var intersection = iD.osmIntersection(graph, '*'); + var newWay = intersection.ways.find(function(w) { return /^w-\d+$/.test(w.id); }); + var turns = iD.osmIntersection(graph, '*').turns(newWay.id); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'c', way: '-'}, - via: {node: '*'}, - to: {node: 'a', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'c', way: '-'}, - via: {node: '*'}, - to: {node: 'u', way: '='} - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql(newWay.id + '_*_-'); + expect(turns[0].u).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql(newWay.id + '_*_='); + expect(turns[1].u).to.be.not.ok; }); it('permits turns from a reverse oneway circular way', function() { @@ -556,29 +531,179 @@ describe('iD.osmIntersection', function() { // | | // a -- * === u // - var graph = iD.Graph([ - iD.Node({id: 'a'}), - iD.Node({id: 'b'}), - iD.Node({id: 'c'}), - iD.Node({id: '*'}), - iD.Node({id: 'u'}), - iD.Way({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: '-1'}}), - iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) - ]), - turns = iD.osmIntersection(graph, '*').turns('a'); + var graph = iD.coreGraph([ + iD.osmNode({id: 'a'}), + iD.osmNode({id: 'b'}), + iD.osmNode({id: 'c'}), + iD.osmNode({id: '*'}), + iD.osmNode({id: 'u'}), + iD.osmWay({id: '-', nodes: ['*', 'a', 'b', 'c', '*'], tags: {highway: 'residential', oneway: '-1'}}), + iD.osmWay({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential'}}) + ]); + var intersection = iD.osmIntersection(graph, '*'); + var newWay = intersection.ways.find(function(w) { return /^w-\d+$/.test(w.id); }); + var turns = iD.osmIntersection(graph, '*').turns(newWay.id); expect(turns.length).to.eql(2); - expect(turns[0]).to.eql({ - from: {node: 'a', way: '-'}, - via: {node: '*'}, - to: {node: 'c', way: '-'} - }); - expect(turns[1]).to.eql({ - from: {node: 'a', way: '-'}, - via: {node: '*'}, - to: {node: 'u', way: '='} - }); + + expect(turns[0]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[0].key).to.eql(newWay.id + '_*_-'); + expect(turns[0].u).to.be.not.ok; + + expect(turns[1]).to.be.an.instanceOf(iD.osmTurn); + expect(turns[1].key).to.eql(newWay.id + '_*_='); + expect(turns[1].u).to.be.not.ok; }); }); }); + + +describe('iD.osmInferRestriction', function() { + var projection = d3.geoMercator().scale(250 / Math.PI); + + it('infers the restriction type based on the turn angle', function() { + // + // u === * ~~~ w + // | + // x + // + var graph = iD.coreGraph([ + iD.osmNode({id: 'u', loc: [-1, 0]}), + iD.osmNode({id: '*', loc: [ 0, 0]}), + iD.osmNode({id: 'w', loc: [ 1, 0]}), + iD.osmNode({id: 'x', loc: [ 0, -1]}), + iD.osmWay({id: '=', nodes: ['u', '*']}), + iD.osmWay({id: '-', nodes: ['*', 'x']}), + iD.osmWay({id: '~', nodes: ['*', 'w']}) + ]); + + var r1 = iD.osmInferRestriction(graph, { + from: { node: 'u', way: '=', vertex: '*' }, + to: { node: 'x', way: '-', vertex: '*' } + }, projection); + expect(r1).to.equal('no_right_turn'); + + var r2 = iD.osmInferRestriction(graph, { + from: { node: 'x', way: '-', vertex: '*' }, + to: { node: 'w', way: '~', vertex: '*' } + }, projection); + expect(r2).to.equal('no_right_turn'); + + var l1 = iD.osmInferRestriction(graph, { + from: { node: 'x', way: '-', vertex: '*' }, + to: { node: 'u', way: '=', vertex: '*' } + }, projection); + expect(l1).to.equal('no_left_turn'); + + var l2 = iD.osmInferRestriction(graph, { + from: { node: 'w', way: '~', vertex: '*' }, + to: { node: 'x', way: '-', vertex: '*' } + }, projection); + expect(l2).to.equal('no_left_turn'); + + var s = iD.osmInferRestriction(graph, { + from: { node: 'u', way: '=', vertex: '*' }, + to: { node: 'w', way: '~', vertex: '*' } + }, projection); + expect(s).to.equal('no_straight_on'); + + var u = iD.osmInferRestriction(graph, { + from: { node: 'u', way: '=', vertex: '*' }, + to: { node: 'u', way: '=', vertex: '*' } + }, projection); + expect(u).to.equal('no_u_turn'); + }); + + + it('infers no_u_turn from sharply acute angle made by forward oneways', function() { + // * + // / \ + // w2/ \w1 angle ≈22.6° + // / \ + // u x + var graph = iD.coreGraph([ + iD.osmNode({ id: 'u', loc: [0, -5] }), + iD.osmNode({ id: '*', loc: [1, 0] }), + iD.osmNode({ id: 'x', loc: [2, -5] }), + iD.osmWay({ id: 'w1', nodes: ['x', '*'], tags: { oneway: 'yes' } }), + iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } }) + ]); + + var r = iD.osmInferRestriction(graph, { + from: { node: 'x', way: 'w1', vertex: '*' }, + to: { node: 'u', way: 'w2', vertex: '*' } + }, projection); + expect(r).to.equal('no_u_turn'); + }); + + + it('does not infer no_u_turn from widely acute angle made by forward oneways', function() { + // * + // / \ + // w2/ \w1 angle ≈36.9° + // / \ + // u x + var graph = iD.coreGraph([ + iD.osmNode({ id: 'u', loc: [0, -3] }), + iD.osmNode({ id: '*', loc: [1, 0] }), + iD.osmNode({ id: 'x', loc: [2, -3] }), + iD.osmWay({ id: 'w1', nodes: ['x', '*'], tags: { oneway: 'yes' } }), + iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } }) + ]); + + var r = iD.osmInferRestriction(graph, { + from: { node: 'x', way: 'w1', vertex: '*' }, + to: { node: 'u', way: 'w2', vertex: '*' } + }, projection); + expect(r).to.equal('no_left_turn'); + }); + + + it('infers no_u_turn from sharply acute angle made by forward oneways with a via way', function() { + // * -- + + // / \ + // w2/ \w1 angle ≈22.6° + // / \ + // u x + var graph = iD.coreGraph([ + iD.osmNode({ id: 'u', loc: [0, -5] }), + iD.osmNode({ id: '*', loc: [1, 0] }), + iD.osmNode({ id: '+', loc: [2, 0] }), + iD.osmNode({ id: 'x', loc: [3, -5] }), + iD.osmWay({ id: 'w1', nodes: ['x', '+'], tags: { oneway: 'yes' } }), + iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } }), + iD.osmWay({ id: '-', nodes: ['*', '+'] }) + ]); + + var r = iD.osmInferRestriction(graph, { + from: { node: 'x', way: 'w1', vertex: '+' }, + to: { node: 'u', way: 'w2', vertex: '*' } + }, projection); + expect(r).to.equal('no_u_turn'); + }); + + + it('infers no_u_turn from widely acute angle made by forward oneways with a via way', function() { + // * -- + + // / \ + // w2/ \w1 angle ≈36.9° + // / \ + // u x + var graph = iD.coreGraph([ + iD.osmNode({ id: 'u', loc: [0, -3] }), + iD.osmNode({ id: '*', loc: [1, 0] }), + iD.osmNode({ id: '+', loc: [2, 0] }), + iD.osmNode({ id: 'x', loc: [3, -3] }), + iD.osmWay({ id: 'w1', nodes: ['x', '+'], tags: { oneway: 'yes' } }), + iD.osmWay({ id: 'w2', nodes: ['*', 'u'], tags: { oneway: 'yes' } }), + iD.osmWay({ id: '-', nodes: ['*', '+'] }) + ]); + + var r = iD.osmInferRestriction(graph, { + from: { node: 'x', way: 'w1', vertex: '+' }, + to: { node: 'u', way: 'w2', vertex: '*' } + }, projection); + expect(r).to.equal('no_u_turn'); + }); +}); diff --git a/test/spec/osm/relation.js b/test/spec/osm/relation.js index 4d449d226..17fcc222c 100644 --- a/test/spec/osm/relation.js +++ b/test/spec/osm/relation.js @@ -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); }); }); });