mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-15 05:30:35 +02:00
Merge pull request #4768 from openstreetmap/advanced_intersection
Add support for complex intersection and via way restrictions
This commit is contained in:
+32
-3
@@ -26,6 +26,11 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lasso #map {
|
||||
pointer-events: visibleStroke;
|
||||
}
|
||||
|
||||
|
||||
/* `.target` objects are interactive */
|
||||
/* They can be picked up, clicked, hovered, or things can connect to them */
|
||||
.node.target {
|
||||
@@ -242,7 +247,7 @@ text.point {
|
||||
}
|
||||
|
||||
|
||||
/* Turns */
|
||||
/* Turn Restrictions */
|
||||
|
||||
g.turn rect,
|
||||
g.turn circle {
|
||||
@@ -255,10 +260,34 @@ g.turn circle {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.lasso #map {
|
||||
pointer-events: visibleStroke;
|
||||
/* Turn restriction paths and vertices */
|
||||
.surface.tr .way.target,
|
||||
.surface.tr path.shadow.selected,
|
||||
.surface.tr path.shadow.related {
|
||||
stroke-width: 25px;
|
||||
}
|
||||
|
||||
.surface.tr path.shadow.selected,
|
||||
.surface.tr path.shadow.related,
|
||||
.surface.tr g.vertex.selected .shadow,
|
||||
.surface.tr g.vertex.related .shadow {
|
||||
stroke-opacity: 0.7;
|
||||
stroke: #777;
|
||||
}
|
||||
.surface.tr path.shadow.related.allow,
|
||||
.surface.tr g.vertex.related.allow .shadow {
|
||||
stroke: #5b3;
|
||||
}
|
||||
.surface.tr path.shadow.related.restrict,
|
||||
.surface.tr g.vertex.related.restrict .shadow {
|
||||
stroke: #d53;
|
||||
}
|
||||
.surface.tr path.shadow.related.only,
|
||||
.surface.tr g.vertex.related.only .shadow {
|
||||
stroke: #68f;
|
||||
}
|
||||
|
||||
|
||||
/* GPX Paths */
|
||||
|
||||
.layer-gpx {
|
||||
|
||||
+173
-6
@@ -681,12 +681,14 @@ button.save.has-count .count::before {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.field-help-title button.close,
|
||||
.entity-editor-pane .header button.preset-close,
|
||||
.preset-list-pane .header button.preset-choose {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
[dir='rtl'] .field-help-title button.close,
|
||||
[dir='rtl'] .entity-editor-pane .header button.preset-close,
|
||||
[dir='rtl'] .preset-list-pane .header button.preset-choose {
|
||||
left: 0;
|
||||
@@ -1266,6 +1268,7 @@ a.hide-toggle {
|
||||
border-top: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-field textarea {
|
||||
@@ -1834,9 +1837,48 @@ input[type=number] {
|
||||
|
||||
/* Restrictions editor */
|
||||
|
||||
.form-field-restrictions .preset-input-wrap {
|
||||
.form-field-restrictions .restriction-controls-container {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ccc;
|
||||
width: 100%;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.restriction-controls-container .restriction-controls {
|
||||
display: table;
|
||||
}
|
||||
|
||||
.restriction-controls .restriction-control {
|
||||
display: table-row;
|
||||
padding: 5px 10px;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.restriction-control input,
|
||||
.restriction-control span {
|
||||
display: table-cell;
|
||||
text-align: start;
|
||||
padding: 0px 5px;
|
||||
}
|
||||
|
||||
.restriction-control span.restriction-control-label {
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.restriction-control input {
|
||||
width: 60px;
|
||||
padding: 0;
|
||||
margin: 0px 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.form-field-restrictions .restriction-container {
|
||||
position: relative;
|
||||
height: 300px;
|
||||
height: 370px;
|
||||
}
|
||||
/* zero width space, so container takes up space */
|
||||
.form-field-restrictions .restriction-container:after {
|
||||
content: '\200b';
|
||||
}
|
||||
|
||||
.form-field-restrictions svg.surface {
|
||||
@@ -1844,7 +1886,7 @@ input[type=number] {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.form-field-restrictions .restriction-help {
|
||||
.restriction-container .restriction-help {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -1852,8 +1894,32 @@ input[type=number] {
|
||||
right: 0;
|
||||
padding: 2px 6px;
|
||||
background-color: rgba(255, 255, 255, .8);
|
||||
color: #999;
|
||||
color: #888;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.restriction-help span {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
.restriction-help .qualifier {
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
.restriction-help .qualifier.allow {
|
||||
color: #8b5;
|
||||
}
|
||||
.restriction-help .qualifier.restrict {
|
||||
color: #d53;
|
||||
}
|
||||
.restriction-help .qualifier.only {
|
||||
color: #78f;
|
||||
}
|
||||
|
||||
/* Changeset editor while comment text is empty */
|
||||
@@ -1926,7 +1992,7 @@ div.combobox {
|
||||
}
|
||||
|
||||
.combobox-caret::after {
|
||||
content:"";
|
||||
content: "";
|
||||
height: 0; width: 0;
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0; top: 0;
|
||||
@@ -1936,6 +2002,107 @@ div.combobox {
|
||||
border-right: 5px solid transparent;
|
||||
}
|
||||
|
||||
/* Field Help */
|
||||
|
||||
.field-help-body {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
margin: 5px;
|
||||
padding: 8px;
|
||||
border: 1px solid #ccc;
|
||||
border-top: 0;
|
||||
border-radius: 0 0 4px 4px;
|
||||
z-index: 20;
|
||||
background: rgba(255,255,255,0.95);
|
||||
box-shadow: 0 0 30px 5px rgba(0,0,0,.4);
|
||||
}
|
||||
|
||||
.field-help-title h2 {
|
||||
padding: 10px;
|
||||
margin-bottom: 0px;
|
||||
font-size: 17px;
|
||||
}
|
||||
.field-help-title button {
|
||||
width: 45px;
|
||||
height: 55px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.field-help-nav {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.field-help-nav-item {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
color: #666;
|
||||
}
|
||||
.field-help-nav-item.active {
|
||||
color: #7092ff;
|
||||
}
|
||||
.field-help-nav-item:hover {
|
||||
color: #597be7;
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
.field-help-content {
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.field-help-content h3 {
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.field-help-content p {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.field-help-content ul li {
|
||||
list-style: inside;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.field-help-content .field-help-image {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.field-help-content svg.turn {
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
}
|
||||
.field-help-content svg.shadow {
|
||||
opacity: 0.7;
|
||||
width: 60px;
|
||||
height: 20px;
|
||||
}
|
||||
.field-help-content svg.from {
|
||||
color: #777;
|
||||
}
|
||||
.field-help-content svg.allow {
|
||||
color: #5b3;
|
||||
}
|
||||
.field-help-content svg.restrict {
|
||||
color: #d53;
|
||||
}
|
||||
.field-help-content svg.only {
|
||||
color: #68f;
|
||||
}
|
||||
|
||||
.field-help-content p.from_shadow,
|
||||
.field-help-content p.allow_shadow,
|
||||
.field-help-content p.restrict_shadow,
|
||||
.field-help-content p.allow_turn,
|
||||
.field-help-content p.restrict_turn {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
||||
/* Raw Tag Editor */
|
||||
|
||||
.tag-list {
|
||||
@@ -2626,7 +2793,7 @@ div.full-screen > button:hover {
|
||||
}
|
||||
|
||||
.help-wrap .toc li a:hover,
|
||||
.help-wrap .nav a:hover {
|
||||
.help-wrap .nav a:hover {
|
||||
background: #ececec;
|
||||
}
|
||||
|
||||
|
||||
+64
-5
@@ -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
|
||||
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
Vendored
+74
-6
@@ -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": {
|
||||
|
||||
@@ -1,98 +1,50 @@
|
||||
import { actionSplit } from './split';
|
||||
|
||||
import {
|
||||
osmInferRestriction,
|
||||
osmRelation,
|
||||
osmWay
|
||||
} from '../osm';
|
||||
import { osmRelation } from '../osm';
|
||||
|
||||
|
||||
// Create a restriction relation for `turn`, which must have the following structure:
|
||||
// `actionRestrictTurn` creates a turn restriction relation.
|
||||
//
|
||||
// {
|
||||
// from: { node: <node ID>, way: <way ID> },
|
||||
// via: { node: <node ID> },
|
||||
// to: { node: <node ID>, way: <way ID> },
|
||||
// restriction: <'no_right_turn', 'no_left_turn', etc.>
|
||||
// }
|
||||
// `turn` must be an `osmTurn` object
|
||||
// see osm/intersection.js, pathToTurn()
|
||||
//
|
||||
// This specifies a restriction of type `restriction` when traveling from
|
||||
// `from.node` in `from.way` toward `to.node` in `to.way` via `via.node`.
|
||||
// `turn.from.way` toward `turn.to.way` via `turn.via.node` OR `turn.via.ways`.
|
||||
// (The action does not check that these entities form a valid intersection.)
|
||||
//
|
||||
// If `restriction` is not provided, it is automatically determined by
|
||||
// osmInferRestriction.
|
||||
// From, to, and via ways should be split before calling this action.
|
||||
// (old versions of the code would split the ways here, but we no longer do it)
|
||||
//
|
||||
// If necessary, the `from` and `to` ways are split. In these cases, `from.node`
|
||||
// and `to.node` are used to determine which portion of the split ways become
|
||||
// members of the restriction.
|
||||
// For testing convenience, accepts a restrictionID to assign to the new
|
||||
// relation. Normally, this will be undefined and the relation will
|
||||
// automatically be assigned a new ID.
|
||||
//
|
||||
// For testing convenience, accepts an ID to assign to the new relation.
|
||||
// Normally, this will be undefined and the relation will automatically
|
||||
// be assigned a new ID.
|
||||
//
|
||||
export function actionRestrictTurn(turn, projection, restrictionId) {
|
||||
export function actionRestrictTurn(turn, restrictionType, restrictionID) {
|
||||
|
||||
return function(graph) {
|
||||
var from = graph.entity(turn.from.way),
|
||||
via = graph.entity(turn.via.node),
|
||||
to = graph.entity(turn.to.way);
|
||||
var fromWay = graph.entity(turn.from.way);
|
||||
var toWay = graph.entity(turn.to.way);
|
||||
var viaNode = turn.via.node && graph.entity(turn.via.node);
|
||||
var viaWays = turn.via.ways && turn.via.ways.map(function(id) { return graph.entity(id); });
|
||||
var members = [];
|
||||
|
||||
function isClosingNode(way, nodeId) {
|
||||
return nodeId === way.first() && nodeId === way.last();
|
||||
members.push({ id: fromWay.id, type: 'way', role: 'from' });
|
||||
|
||||
if (viaNode) {
|
||||
members.push({ id: viaNode.id, type: 'node', role: 'via' });
|
||||
} else if (viaWays) {
|
||||
viaWays.forEach(function(viaWay) {
|
||||
members.push({ id: viaWay.id, type: 'way', role: 'via' });
|
||||
});
|
||||
}
|
||||
|
||||
function split(toOrFrom) {
|
||||
var newID = toOrFrom.newID || osmWay().id;
|
||||
graph = actionSplit(via.id, [newID])
|
||||
.limitWays([toOrFrom.way])(graph);
|
||||
|
||||
var a = graph.entity(newID),
|
||||
b = graph.entity(toOrFrom.way);
|
||||
|
||||
if (a.nodes.indexOf(toOrFrom.node) !== -1) {
|
||||
return [a, b];
|
||||
} else {
|
||||
return [b, a];
|
||||
}
|
||||
}
|
||||
|
||||
if (!from.affix(via.id) || isClosingNode(from, via.id)) {
|
||||
if (turn.from.node === turn.to.node) {
|
||||
// U-turn
|
||||
from = to = split(turn.from)[0];
|
||||
} else if (turn.from.way === turn.to.way) {
|
||||
// Straight-on or circular
|
||||
var s = split(turn.from);
|
||||
from = s[0];
|
||||
to = s[1];
|
||||
} else {
|
||||
// Other
|
||||
from = split(turn.from)[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!to.affix(via.id) || isClosingNode(to, via.id)) {
|
||||
to = split(turn.to)[0];
|
||||
}
|
||||
members.push({ id: toWay.id, type: 'way', role: 'to' });
|
||||
|
||||
return graph.replace(osmRelation({
|
||||
id: restrictionId,
|
||||
id: restrictionID,
|
||||
tags: {
|
||||
type: 'restriction',
|
||||
restriction: turn.restriction ||
|
||||
osmInferRestriction(
|
||||
graph,
|
||||
turn.from,
|
||||
turn.via,
|
||||
turn.to,
|
||||
projection)
|
||||
restriction: restrictionType
|
||||
},
|
||||
members: [
|
||||
{id: from.id, type: 'way', role: 'from'},
|
||||
{id: via.id, type: 'node', role: 'via'},
|
||||
{id: to.id, type: 'way', role: 'to'}
|
||||
]
|
||||
members: members
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,26 +1,13 @@
|
||||
import { actionDeleteRelation } from './delete_relation';
|
||||
|
||||
|
||||
// Remove the effects of `turn.restriction` on `turn`, which must have the
|
||||
// following structure:
|
||||
// `actionUnrestrictTurn` deletes a turn restriction relation.
|
||||
//
|
||||
// {
|
||||
// from: { node: <node ID>, way: <way ID> },
|
||||
// via: { node: <node ID> },
|
||||
// to: { node: <node ID>, way: <way ID> },
|
||||
// restriction: <relation ID>
|
||||
// }
|
||||
//
|
||||
// In the simple case, `restriction` is a reference to a `no_*` restriction
|
||||
// on the turn itself. In this case, it is simply deleted.
|
||||
//
|
||||
// The more complex case is where `restriction` references an `only_*`
|
||||
// restriction on a different turn in the same intersection. In that case,
|
||||
// that restriction is also deleted, but at the same time restrictions on
|
||||
// the turns other than the first two are created.
|
||||
// `turn` must be an `osmTurn` object with a `restrictionID` property.
|
||||
// see osm/intersection.js, pathToTurn()
|
||||
//
|
||||
export function actionUnrestrictTurn(turn) {
|
||||
return function(graph) {
|
||||
return actionDeleteRelation(turn.restriction)(graph);
|
||||
return actionDeleteRelation(turn.restrictionID)(graph);
|
||||
};
|
||||
}
|
||||
|
||||
+587
-162
@@ -1,197 +1,620 @@
|
||||
import _each from 'lodash-es/each';
|
||||
import _clone from 'lodash-es/clone';
|
||||
import _every from 'lodash-es/every';
|
||||
import _extend from 'lodash-es/extend';
|
||||
import _find from 'lodash-es/find';
|
||||
import _indexOf from 'lodash-es/indexOf';
|
||||
import _keys from 'lodash-es/keys';
|
||||
import _values from 'lodash-es/values';
|
||||
import _uniq from 'lodash-es/uniq';
|
||||
|
||||
import {
|
||||
actionDeleteRelation,
|
||||
actionReverse,
|
||||
actionSplit
|
||||
} from '../actions';
|
||||
|
||||
import { coreGraph } from '../core';
|
||||
|
||||
import {
|
||||
geoAngle,
|
||||
geoSphericalDistance,
|
||||
geoVecInterp
|
||||
} from '../geo';
|
||||
|
||||
import { osmEntity } from './entity';
|
||||
|
||||
import { geoAngle } from '../geo/index';
|
||||
import { osmWay } from './way';
|
||||
|
||||
|
||||
export function osmTurn(turn) {
|
||||
if (!(this instanceof osmTurn))
|
||||
if (!(this instanceof osmTurn)) {
|
||||
return new osmTurn(turn);
|
||||
}
|
||||
_extend(this, turn);
|
||||
}
|
||||
|
||||
|
||||
export function osmIntersection(graph, vertexId) {
|
||||
var vertex = graph.entity(vertexId),
|
||||
parentWays = graph.parentWays(vertex),
|
||||
coincident = [],
|
||||
highways = {};
|
||||
export function osmIntersection(graph, startVertexId, maxDistance) {
|
||||
maxDistance = maxDistance || 30; // in meters
|
||||
var vgraph = coreGraph(); // virtual graph
|
||||
var i, j, k;
|
||||
|
||||
function addHighway(way, adjacentNodeId) {
|
||||
if (highways[adjacentNodeId]) {
|
||||
coincident.push(adjacentNodeId);
|
||||
} else {
|
||||
highways[adjacentNodeId] = way;
|
||||
|
||||
function memberOfRestriction(entity) {
|
||||
return graph.parentRelations(entity)
|
||||
.some(function(r) { return r.isRestriction(); });
|
||||
}
|
||||
|
||||
function isRoad(way) {
|
||||
if (way.isArea() || way.isDegenerate()) return false;
|
||||
var roads = {
|
||||
'motorway': true,
|
||||
'motorway_link': true,
|
||||
'trunk': true,
|
||||
'trunk_link': true,
|
||||
'primary': true,
|
||||
'primary_link': true,
|
||||
'secondary': true,
|
||||
'secondary_link': true,
|
||||
'tertiary': true,
|
||||
'tertiary_link': true,
|
||||
'residential': true,
|
||||
'unclassified': true,
|
||||
'living_street': true,
|
||||
'service': true,
|
||||
'road': true,
|
||||
'track': true
|
||||
};
|
||||
return roads[way.tags.highway];
|
||||
}
|
||||
|
||||
|
||||
var startNode = graph.entity(startVertexId);
|
||||
var checkVertices = [startNode];
|
||||
var checkWays;
|
||||
var vertices = [];
|
||||
var vertexIds = [];
|
||||
var vertex;
|
||||
var ways = [];
|
||||
var wayIds = [];
|
||||
var way;
|
||||
var nodes = [];
|
||||
var node;
|
||||
var parents = [];
|
||||
var parent;
|
||||
|
||||
// `actions` will store whatever actions must be performed to satisfy
|
||||
// preconditions for adding a turn restriction to this intersection.
|
||||
// - Remove any existing degenerate turn restrictions (missing from/to, etc)
|
||||
// - Reverse oneways so that they are drawn in the forward direction
|
||||
// - Split ways on key vertices
|
||||
var actions = [];
|
||||
|
||||
|
||||
// STEP 1: walk the graph outwards from starting vertex to search
|
||||
// for more key vertices and ways to include in the intersection..
|
||||
|
||||
while (checkVertices.length) {
|
||||
vertex = checkVertices.pop();
|
||||
|
||||
// check this vertex for parent ways that are roads
|
||||
checkWays = graph.parentWays(vertex);
|
||||
var hasWays = false;
|
||||
for (i = 0; i < checkWays.length; i++) {
|
||||
way = checkWays[i];
|
||||
if (!isRoad(way) && !memberOfRestriction(way)) continue;
|
||||
|
||||
ways.push(way); // it's a road, or it's already in a turn restriction
|
||||
hasWays = true;
|
||||
|
||||
// check the way's children for more key vertices
|
||||
nodes = _uniq(graph.childNodes(way));
|
||||
for (j = 0; j < nodes.length; j++) {
|
||||
node = nodes[j];
|
||||
if (node === vertex) continue; // same thing
|
||||
if (vertices.indexOf(node) !== -1) continue; // seen it already
|
||||
if (node.loc && startNode.loc &&
|
||||
geoSphericalDistance(node.loc, startNode.loc) > maxDistance) continue; // too far from start
|
||||
|
||||
// a key vertex will have parents that are also roads
|
||||
var hasParents = false;
|
||||
parents = graph.parentWays(node);
|
||||
for (k = 0; k < parents.length; k++) {
|
||||
parent = parents[k];
|
||||
if (parent === way) continue; // same thing
|
||||
if (ways.indexOf(parent) !== -1) continue; // seen it already
|
||||
if (!isRoad(parent)) continue; // not a road
|
||||
hasParents = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hasParents) {
|
||||
checkVertices.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWays) {
|
||||
vertices.push(vertex);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-split ways that would need to be split in
|
||||
// order to add a restriction. The real split will
|
||||
// happen when the restriction is added.
|
||||
parentWays.forEach(function(way) {
|
||||
if (!way.tags.highway || way.isArea() || way.isDegenerate())
|
||||
return;
|
||||
|
||||
var isFirst = (vertexId === way.first()),
|
||||
isLast = (vertexId === way.last()),
|
||||
isAffix = (isFirst || isLast),
|
||||
isClosingNode = (isFirst && isLast);
|
||||
|
||||
if (isAffix && !isClosingNode) {
|
||||
var index = (isFirst ? 1 : way.nodes.length - 2);
|
||||
addHighway(way, way.nodes[index]);
|
||||
|
||||
} else {
|
||||
var splitIndex, wayA, wayB, indexA, indexB;
|
||||
if (isClosingNode) {
|
||||
splitIndex = Math.ceil(way.nodes.length / 2); // split at midpoint
|
||||
wayA = osmWay({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex)});
|
||||
wayB = osmWay({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
|
||||
indexA = 1;
|
||||
indexB = way.nodes.length - 2;
|
||||
} else {
|
||||
splitIndex = _indexOf(way.nodes, vertex.id, 1); // split at vertexid
|
||||
wayA = osmWay({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, splitIndex + 1)});
|
||||
wayB = osmWay({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(splitIndex)});
|
||||
indexA = splitIndex - 1;
|
||||
indexB = splitIndex + 1;
|
||||
}
|
||||
graph = graph.replace(wayA).replace(wayB);
|
||||
addHighway(wayA, way.nodes[indexA]);
|
||||
addHighway(wayB, way.nodes[indexB]);
|
||||
}
|
||||
});
|
||||
|
||||
// remove any ways from this intersection that are coincident
|
||||
// (i.e. any adjacent node used by more than one intersecting way)
|
||||
coincident.forEach(function (n) {
|
||||
delete highways[n];
|
||||
});
|
||||
vertices = _uniq(vertices);
|
||||
ways = _uniq(ways);
|
||||
|
||||
|
||||
var intersection = {
|
||||
highways: highways,
|
||||
ways: _values(highways),
|
||||
graph: graph
|
||||
};
|
||||
|
||||
|
||||
intersection.adjacentNodeId = function(fromWayId) {
|
||||
return _find(_keys(highways), function(k) {
|
||||
return highways[k].id === fromWayId;
|
||||
// STEP 2: Build a virtual graph containing only the entities in the intersection..
|
||||
// Everything done after this step should act on the virtual graph
|
||||
// Any actions that must be performed later to the main graph go in `actions` array
|
||||
ways.forEach(function(way) {
|
||||
graph.childNodes(way).forEach(function(node) {
|
||||
vgraph = vgraph.replace(node);
|
||||
});
|
||||
};
|
||||
|
||||
vgraph = vgraph.replace(way);
|
||||
|
||||
intersection.turns = function(fromNodeId) {
|
||||
var start = highways[fromNodeId];
|
||||
if (!start)
|
||||
return [];
|
||||
|
||||
if (start.first() === vertex.id && start.tags.oneway === 'yes')
|
||||
return [];
|
||||
if (start.last() === vertex.id && start.tags.oneway === '-1')
|
||||
return [];
|
||||
|
||||
function withRestriction(turn) {
|
||||
graph.parentRelations(graph.entity(turn.from.way)).forEach(function(relation) {
|
||||
if (relation.tags.type !== 'restriction')
|
||||
return;
|
||||
|
||||
var f = relation.memberByRole('from'),
|
||||
t = relation.memberByRole('to'),
|
||||
v = relation.memberByRole('via');
|
||||
|
||||
if (f && f.id === turn.from.way &&
|
||||
v && v.id === turn.via.node &&
|
||||
t && t.id === turn.to.way) {
|
||||
turn.restriction = relation.id;
|
||||
} else if (/^only_/.test(relation.tags.restriction) &&
|
||||
f && f.id === turn.from.way &&
|
||||
v && v.id === turn.via.node &&
|
||||
t && t.id !== turn.to.way) {
|
||||
turn.restriction = relation.id;
|
||||
turn.indirect_restriction = true;
|
||||
graph.parentRelations(way).forEach(function(relation) {
|
||||
if (relation.isRestriction()) {
|
||||
if (relation.isValidRestriction(graph)) {
|
||||
vgraph = vgraph.replace(relation);
|
||||
} else if (relation.isComplete(graph)) {
|
||||
actions.push(actionDeleteRelation(relation.id));
|
||||
}
|
||||
});
|
||||
|
||||
return osmTurn(turn);
|
||||
}
|
||||
|
||||
|
||||
var from = {
|
||||
node: fromNodeId,
|
||||
way: start.id.split(/-(a|b)/)[0]
|
||||
},
|
||||
via = { node: vertex.id },
|
||||
turns = [];
|
||||
|
||||
_each(highways, function(end, adjacentNodeId) {
|
||||
if (end === start)
|
||||
return;
|
||||
|
||||
// backward
|
||||
if (end.first() !== vertex.id && end.tags.oneway !== 'yes') {
|
||||
turns.push(withRestriction({
|
||||
from: from,
|
||||
via: via,
|
||||
to: {
|
||||
node: adjacentNodeId,
|
||||
way: end.id.split(/-(a|b)/)[0]
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// forward
|
||||
if (end.last() !== vertex.id && end.tags.oneway !== '-1') {
|
||||
turns.push(withRestriction({
|
||||
from: from,
|
||||
via: via,
|
||||
to: {
|
||||
node: adjacentNodeId,
|
||||
way: end.id.split(/-(a|b)/)[0]
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// U-turn
|
||||
if (start.tags.oneway !== 'yes' && start.tags.oneway !== '-1') {
|
||||
turns.push(withRestriction({
|
||||
from: from,
|
||||
via: via,
|
||||
to: from,
|
||||
u: true
|
||||
}));
|
||||
|
||||
// STEP 3: Force all oneways to be drawn in the forward direction
|
||||
ways.forEach(function(w) {
|
||||
var way = vgraph.entity(w.id);
|
||||
if (way.tags.oneway === '-1') {
|
||||
var action = actionReverse(way.id, { reverseOneway: true });
|
||||
actions.push(action);
|
||||
vgraph = action(vgraph);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// STEP 4: Split ways on key vertices
|
||||
var origCount = osmEntity.id.next.way;
|
||||
vertices.forEach(function(v) {
|
||||
// This is an odd way to do it, but we need to find all the ways that
|
||||
// will be split here, then split them one at a time to ensure that these
|
||||
// actions can be replayed on the main graph exactly in the same order.
|
||||
// (It is unintuitive, but the order of ways returned from graph.parentWays()
|
||||
// is arbitrary, depending on how the main graph and vgraph were built)
|
||||
var splitAll = actionSplit(v.id);
|
||||
if (!splitAll.disabled(vgraph)) {
|
||||
splitAll.ways(vgraph).forEach(function(way) {
|
||||
var splitOne = actionSplit(v.id).limitWays([way.id]);
|
||||
actions.push(splitOne);
|
||||
vgraph = splitOne(vgraph);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// In here is where we should also split the intersection at nearby junction.
|
||||
// for https://github.com/mapbox/iD-internal/issues/31
|
||||
// nearbyVertices.forEach(function(v) {
|
||||
// });
|
||||
|
||||
// Reasons why we reset the way id count here:
|
||||
// 1. Continuity with way ids created by the splits so that we can replay
|
||||
// these actions later if the user decides to create a turn restriction
|
||||
// 2. Avoids churning way ids just by hovering over a vertex
|
||||
// and displaying the turn restriction editor
|
||||
osmEntity.id.next.way = origCount;
|
||||
|
||||
|
||||
// STEP 5: Update arrays to point to vgraph entities
|
||||
vertexIds = vertices.map(function(v) { return v.id; });
|
||||
vertices = [];
|
||||
ways = [];
|
||||
|
||||
vertexIds.forEach(function(id) {
|
||||
var vertex = vgraph.entity(id);
|
||||
var parents = vgraph.parentWays(vertex);
|
||||
vertices.push(vertex);
|
||||
ways = ways.concat(parents);
|
||||
});
|
||||
|
||||
vertices = _uniq(vertices);
|
||||
ways = _uniq(ways);
|
||||
|
||||
vertexIds = vertices.map(function(v) { return v.id; });
|
||||
wayIds = ways.map(function(w) { return w.id; });
|
||||
|
||||
|
||||
// STEP 6: Update the ways with some metadata that will be useful for
|
||||
// walking the intersection graph later and rendering turn arrows.
|
||||
|
||||
function withMetadata(way, vertexIds) {
|
||||
var __oneWay = way.isOneWay();
|
||||
|
||||
// which affixes are key vertices?
|
||||
var __first = (vertexIds.indexOf(way.first()) !== -1);
|
||||
var __last = (vertexIds.indexOf(way.last()) !== -1);
|
||||
|
||||
// what roles is this way eligible for?
|
||||
var __via = (__first && __last);
|
||||
var __from = ((__first && !__oneWay) || __last);
|
||||
var __to = (__first || (__last && !__oneWay));
|
||||
|
||||
return way.update({
|
||||
__first: __first,
|
||||
__last: __last,
|
||||
__from: __from,
|
||||
__via: __via,
|
||||
__to: __to,
|
||||
__oneWay: __oneWay
|
||||
});
|
||||
}
|
||||
|
||||
ways = [];
|
||||
wayIds.forEach(function(id) {
|
||||
var way = withMetadata(vgraph.entity(id), vertexIds);
|
||||
vgraph = vgraph.replace(way);
|
||||
ways.push(way);
|
||||
});
|
||||
|
||||
|
||||
// STEP 7: Simplify - This is an iterative process where we:
|
||||
// 1. Find trivial vertices with only 2 parents
|
||||
// 2. trim off the leaf way from those vertices and remove from vgraph
|
||||
|
||||
var keepGoing;
|
||||
var removeWayIds = [];
|
||||
var removeVertexIds = [];
|
||||
|
||||
do {
|
||||
keepGoing = false;
|
||||
checkVertices = vertexIds.slice();
|
||||
|
||||
for (i = 0; i < checkVertices.length; i++) {
|
||||
var vertexId = checkVertices[i];
|
||||
vertex = vgraph.hasEntity(vertexId);
|
||||
|
||||
if (!vertex) {
|
||||
if (vertexIds.indexOf(vertexId) !== -1) {
|
||||
vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one
|
||||
}
|
||||
removeVertexIds.push(vertexId);
|
||||
continue;
|
||||
}
|
||||
|
||||
parents = vgraph.parentWays(vertex);
|
||||
if (parents.length < 3) {
|
||||
if (vertexIds.indexOf(vertexId) !== -1) {
|
||||
vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one
|
||||
}
|
||||
}
|
||||
|
||||
if (parents.length === 2) { // vertex with 2 parents is trivial
|
||||
var a = parents[0];
|
||||
var b = parents[1];
|
||||
var aIsLeaf = a && !a.__via;
|
||||
var bIsLeaf = b && !b.__via;
|
||||
var leaf, survivor;
|
||||
|
||||
if (aIsLeaf && !bIsLeaf) {
|
||||
leaf = a;
|
||||
survivor = b;
|
||||
} else if (!aIsLeaf && bIsLeaf) {
|
||||
leaf = b;
|
||||
survivor = a;
|
||||
}
|
||||
|
||||
if (leaf && survivor) {
|
||||
survivor = withMetadata(survivor, vertexIds); // update survivor way
|
||||
vgraph = vgraph.replace(survivor).remove(leaf); // update graph
|
||||
removeWayIds.push(leaf.id);
|
||||
keepGoing = true;
|
||||
}
|
||||
}
|
||||
|
||||
parents = vgraph.parentWays(vertex);
|
||||
|
||||
if (parents.length < 2) { // vertex is no longer a key vertex
|
||||
if (vertexIds.indexOf(vertexId) !== -1) {
|
||||
vertexIds.splice(vertexIds.indexOf(vertexId), 1); // stop checking this one
|
||||
}
|
||||
removeVertexIds.push(vertexId);
|
||||
keepGoing = true;
|
||||
}
|
||||
|
||||
if (parents.length < 1) { // vertex is no longer attached to anything
|
||||
vgraph = vgraph.remove(vertex);
|
||||
}
|
||||
|
||||
}
|
||||
} while (keepGoing);
|
||||
|
||||
|
||||
vertices = vertices
|
||||
.filter(function(vertex) { return removeVertexIds.indexOf(vertex.id) === -1; })
|
||||
.map(function(vertex) { return vgraph.entity(vertex.id); });
|
||||
ways = ways
|
||||
.filter(function(way) { return removeWayIds.indexOf(way.id) === -1; })
|
||||
.map(function(way) { return vgraph.entity(way.id); });
|
||||
|
||||
|
||||
// STEP 8: Extend leaf ways, so they don't end within the viewer
|
||||
ways.forEach(function(way) {
|
||||
var n1, n2;
|
||||
if (way.__via) return; // not a leaf
|
||||
if (way.__first) {
|
||||
n1 = vgraph.entity(way.nodes[way.nodes.length - 2]);
|
||||
n2 = vgraph.entity(way.nodes[way.nodes.length - 1]);
|
||||
} else {
|
||||
n1 = vgraph.entity(way.nodes[1]);
|
||||
n2 = vgraph.entity(way.nodes[0]);
|
||||
}
|
||||
|
||||
if (n1.loc && n2.loc && vgraph.parentWays(n2).length === 1) {
|
||||
var toLoc = geoVecInterp(n1.loc, n2.loc, 10); // extend 1000%
|
||||
n2 = n2.move(toLoc);
|
||||
vgraph = vgraph.replace(n2);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// OK! Here is our intersection..
|
||||
var intersection = {
|
||||
graph: vgraph,
|
||||
actions: actions,
|
||||
vertices: vertices,
|
||||
ways: ways,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Get all the valid turns through this intersection given a starting way id.
|
||||
// This operates on the virtual graph for everything.
|
||||
//
|
||||
// Basically, walk through all possible paths from starting way,
|
||||
// honoring the existing turn restrictions as we go (watch out for loops!)
|
||||
//
|
||||
// For each path found, generate and return a `osmTurn` datastructure.
|
||||
//
|
||||
intersection.turns = function(fromWayId, maxViaWay) {
|
||||
if (!fromWayId) return [];
|
||||
if (!maxViaWay) maxViaWay = 0;
|
||||
|
||||
var vgraph = intersection.graph;
|
||||
var keyVertexIds = intersection.vertices.map(function(v) { return v.id; });
|
||||
var keyWayIds = intersection.ways.map(function(w) { return w.id; });
|
||||
|
||||
var start = vgraph.entity(fromWayId);
|
||||
if (!start || !(start.__from || start.__via)) return [];
|
||||
|
||||
// maxViaWay=0 from-*-to (0 vias)
|
||||
// maxViaWay=1 from-*-via-*-to (1 via max)
|
||||
// maxViaWay=2 from-*-via-*-via-*-to (2 vias max)
|
||||
var maxPathLength = (maxViaWay * 2) + 3;
|
||||
var maxDistance = 30; // meters
|
||||
var turns = [];
|
||||
|
||||
step(start);
|
||||
return turns;
|
||||
|
||||
|
||||
// traverse the intersection graph and find all the valid paths
|
||||
function step(entity, currPath, currRestrictions, matchedRestriction) {
|
||||
currPath = _clone(currPath || []);
|
||||
if (currPath.length >= maxPathLength) return;
|
||||
currPath.push(entity.id);
|
||||
currRestrictions = _clone(currRestrictions || []);
|
||||
var i, j;
|
||||
|
||||
if (entity.type === 'node') {
|
||||
var parents = vgraph.parentWays(entity);
|
||||
var nextWays = [];
|
||||
|
||||
// which ways can we step into?
|
||||
for (i = 0; i < parents.length; i++) {
|
||||
var way = parents[i];
|
||||
|
||||
// if next way is a oneway incoming to this vertex, skip
|
||||
if (way.__oneWay && way.nodes[0] !== entity.id) continue;
|
||||
|
||||
// if we have seen it before (allowing for an initial u-turn), skip
|
||||
if (currPath.indexOf(way.id) !== -1 && currPath.length >= 3) continue;
|
||||
|
||||
// Check all "current" restrictions (where we've already walked the `from`)
|
||||
var restrict = undefined;
|
||||
for (j = 0; j < currRestrictions.length; j++) {
|
||||
var restriction = currRestrictions[j];
|
||||
var f = restriction.memberByRole('from');
|
||||
var v = restriction.membersByRole('via');
|
||||
var t = restriction.memberByRole('to');
|
||||
var isOnly = /^only_/.test(restriction.tags.restriction);
|
||||
|
||||
// Are all the vias part of this local intersection?
|
||||
// This matters for flagging "indirect" restrictions
|
||||
var isLocalVia;
|
||||
if (v.length === 1 && v[0].type === 'node') {
|
||||
isLocalVia = (keyVertexIds.indexOf(v[0].id) !== -1);
|
||||
} else {
|
||||
isLocalVia = _every(v, function(via) { return keyWayIds.indexOf(via.id) !== -1; });
|
||||
}
|
||||
|
||||
// Does the current path match this turn restriction?
|
||||
var matchesFrom = (f.id === fromWayId);
|
||||
var matchesViaTo = false;
|
||||
var isAlongOnlyPath = false;
|
||||
|
||||
if (t.id === way.id) { // match VIA, TO
|
||||
if (v.length === 1 && v[0].type === 'node' && v[0].id === entity.id) {
|
||||
matchesViaTo = true; // match VIA node
|
||||
} else if (_every(v, function(via) { return currPath.indexOf(via.id) !== -1; })) {
|
||||
matchesViaTo = true; // match all VIA ways
|
||||
}
|
||||
|
||||
} else if (isOnly) {
|
||||
for (k = 0; k < v.length; k++) {
|
||||
// way doesn't match TO, but is one of the via ways along the path of an "only"
|
||||
if (v[k].type === 'way' && v[k].id === way.id) {
|
||||
isAlongOnlyPath = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchesViaTo) {
|
||||
if (isOnly) {
|
||||
restrict = { id: restriction.id, direct: matchesFrom, from: f.id, only: true, end: true };
|
||||
} else {
|
||||
restrict = { id: restriction.id, direct: matchesFrom, from: f.id, no: true, end: true };
|
||||
}
|
||||
} else { // indirect - caused by a different nearby restriction
|
||||
if (isAlongOnlyPath) {
|
||||
restrict = { id: restriction.id, direct: false, from: f.id, only: true, end: false };
|
||||
} else if (isOnly && isLocalVia) {
|
||||
restrict = { id: restriction.id, direct: false, from: f.id, no: true, end: true };
|
||||
}
|
||||
}
|
||||
|
||||
// stop looking if we find a "direct" restriction (matching FROM, VIA, TO)
|
||||
if (restrict && restrict.direct)
|
||||
break;
|
||||
}
|
||||
|
||||
nextWays.push({ way: way, restrict: restrict });
|
||||
}
|
||||
|
||||
nextWays.forEach(function(nextWay) {
|
||||
step(nextWay.way, currPath, currRestrictions, nextWay.restrict);
|
||||
});
|
||||
|
||||
|
||||
} else { // entity.type === 'way'
|
||||
if (currPath.length >= 3) { // this is a "complete" path..
|
||||
var turnPath = _clone(currPath);
|
||||
|
||||
// an indirect restriction - only include the partial path (starting at FROM)
|
||||
if (matchedRestriction && matchedRestriction.direct === false) {
|
||||
for (i = 0; i < turnPath.length; i++) {
|
||||
if (turnPath[i] === matchedRestriction.from) {
|
||||
turnPath = turnPath.slice(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var turn = pathToTurn(turnPath);
|
||||
if (turn) {
|
||||
if (matchedRestriction) {
|
||||
turn.restrictionID = matchedRestriction.id;
|
||||
turn.no = matchedRestriction.no;
|
||||
turn.only = matchedRestriction.only;
|
||||
turn.direct = matchedRestriction.direct;
|
||||
}
|
||||
turns.push(osmTurn(turn));
|
||||
}
|
||||
|
||||
if (currPath[0] === currPath[2]) return; // if we made a u-turn - stop here
|
||||
}
|
||||
|
||||
if (matchedRestriction && matchedRestriction.end) return; // don't advance any further
|
||||
|
||||
// which nodes can we step into?
|
||||
var n1 = vgraph.entity(entity.first());
|
||||
var n2 = vgraph.entity(entity.last());
|
||||
var dist = n1.loc && n2.loc && geoSphericalDistance(n1.loc, n2.loc);
|
||||
var nextNodes = [];
|
||||
|
||||
if (currPath.length > 1) {
|
||||
if (dist > maxDistance) return; // the next node is too far
|
||||
if (!entity.__via) return; // this way is a leaf / can't be a via
|
||||
}
|
||||
|
||||
if (!entity.__oneWay && // bidirectional..
|
||||
keyVertexIds.indexOf(n1.id) !== -1 && // key vertex..
|
||||
currPath.indexOf(n1.id) === -1) { // haven't seen it yet..
|
||||
nextNodes.push(n1); // can advance to first node
|
||||
}
|
||||
if (keyVertexIds.indexOf(n2.id) !== -1 && // key vertex..
|
||||
currPath.indexOf(n2.id) === -1) { // haven't seen it yet..
|
||||
nextNodes.push(n2); // can advance to last node
|
||||
}
|
||||
|
||||
// gather restrictions FROM this way
|
||||
var fromRestrictions = vgraph.parentRelations(entity).filter(function(r) {
|
||||
if (!r.isRestriction()) return false;
|
||||
var f = r.memberByRole('from');
|
||||
return f && f.id === entity.id;
|
||||
});
|
||||
|
||||
nextNodes.forEach(function(node) {
|
||||
step(node, currPath, currRestrictions.concat(fromRestrictions), false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// assumes path is alternating way-node-way of odd length
|
||||
function pathToTurn(path) {
|
||||
if (path.length < 3) return;
|
||||
var fromWayId, fromNodeId, fromVertexId;
|
||||
var toWayId, toNodeId, toVertexId;
|
||||
var viaWayIds, viaNodeId, isUturn;
|
||||
|
||||
fromWayId = path[0];
|
||||
toWayId = path[path.length - 1];
|
||||
|
||||
if (path.length === 3 && fromWayId === toWayId) { // u turn
|
||||
var way = vgraph.entity(fromWayId);
|
||||
if (way.__oneWay) return null;
|
||||
|
||||
isUturn = true;
|
||||
viaNodeId = fromVertexId = toVertexId = path[1];
|
||||
fromNodeId = toNodeId = adjacentNode(fromWayId, viaNodeId);
|
||||
|
||||
} else {
|
||||
isUturn = false;
|
||||
fromVertexId = path[1];
|
||||
fromNodeId = adjacentNode(fromWayId, fromVertexId);
|
||||
toVertexId = path[path.length - 2];
|
||||
toNodeId = adjacentNode(toWayId, toVertexId);
|
||||
|
||||
if (path.length === 3) {
|
||||
viaNodeId = path[1];
|
||||
} else {
|
||||
viaWayIds = path.filter(function(entityId) { return entityId[0] === 'w'; });
|
||||
viaWayIds = viaWayIds.slice(1, viaWayIds.length - 1); // remove first, last
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: path.join('_'),
|
||||
path: path,
|
||||
from: { node: fromNodeId, way: fromWayId, vertex: fromVertexId },
|
||||
via: { node: viaNodeId, ways: viaWayIds },
|
||||
to: { node: toNodeId, way: toWayId, vertex: toVertexId },
|
||||
u: isUturn
|
||||
};
|
||||
|
||||
|
||||
function adjacentNode(wayId, affixId) {
|
||||
var nodes = vgraph.entity(wayId).nodes;
|
||||
return affixId === nodes[0] ? nodes[1] : nodes[nodes.length - 2];
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return intersection;
|
||||
}
|
||||
|
||||
|
||||
export function osmInferRestriction(graph, from, via, to, projection) {
|
||||
var fromWay = graph.entity(from.way),
|
||||
fromNode = graph.entity(from.node),
|
||||
toWay = graph.entity(to.way),
|
||||
toNode = graph.entity(to.node),
|
||||
viaNode = graph.entity(via.node),
|
||||
fromOneWay = (fromWay.tags.oneway === 'yes' && fromWay.last() === via.node) ||
|
||||
(fromWay.tags.oneway === '-1' && fromWay.first() === via.node),
|
||||
toOneWay = (toWay.tags.oneway === 'yes' && toWay.first() === via.node) ||
|
||||
(toWay.tags.oneway === '-1' && toWay.last() === via.node),
|
||||
angle = geoAngle(viaNode, fromNode, projection) -
|
||||
geoAngle(viaNode, toNode, projection);
|
||||
export function osmInferRestriction(graph, turn, projection) {
|
||||
var fromWay = graph.entity(turn.from.way);
|
||||
var fromNode = graph.entity(turn.from.node);
|
||||
var fromVertex = graph.entity(turn.from.vertex);
|
||||
var toWay = graph.entity(turn.to.way);
|
||||
var toNode = graph.entity(turn.to.node);
|
||||
var toVertex = graph.entity(turn.to.vertex);
|
||||
|
||||
angle = angle * 180 / Math.PI;
|
||||
var fromOneWay = (fromWay.tags.oneway === 'yes');
|
||||
var toOneWay = (toWay.tags.oneway === 'yes');
|
||||
var angle = (geoAngle(fromVertex, fromNode, projection) -
|
||||
geoAngle(toVertex, toNode, projection)) * 180 / Math.PI;
|
||||
|
||||
while (angle < 0)
|
||||
angle += 360;
|
||||
@@ -199,7 +622,9 @@ export function osmInferRestriction(graph, from, via, to, projection) {
|
||||
if (fromNode === toNode)
|
||||
return 'no_u_turn';
|
||||
if ((angle < 23 || angle > 336) && fromOneWay && toOneWay)
|
||||
return 'no_u_turn';
|
||||
return 'no_u_turn'; // wider tolerance for u-turn if both ways are oneway
|
||||
if ((angle < 40 || angle > 319) && fromOneWay && toOneWay && turn.from.vertex !== turn.to.vertex)
|
||||
return 'no_u_turn'; // even wider tolerance for u-turn if there is a via way (from !== to)
|
||||
if (angle < 158)
|
||||
return 'no_right_turn';
|
||||
if (angle > 202)
|
||||
|
||||
+32
-2
@@ -109,6 +109,16 @@ _extend(osmRelation.prototype, {
|
||||
}
|
||||
},
|
||||
|
||||
// Same as memberByRole, but returns all members with the given role
|
||||
membersByRole: function(role) {
|
||||
var result = [];
|
||||
for (var i = 0; i < this.members.length; i++) {
|
||||
if (this.members[i].role === role) {
|
||||
result.push(_extend({}, this.members[i], {index: i}));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
// Return the first member with the given id. A copy of the member object
|
||||
// is returned, extended with an 'index' property whose value is the member index.
|
||||
@@ -253,6 +263,26 @@ _extend(osmRelation.prototype, {
|
||||
},
|
||||
|
||||
|
||||
isValidRestriction: function() {
|
||||
if (!this.isRestriction()) return false;
|
||||
|
||||
var froms = this.members.filter(function(m) { return m.role === 'from'; });
|
||||
var vias = this.members.filter(function(m) { return m.role === 'via'; });
|
||||
var tos = this.members.filter(function(m) { return m.role === 'to'; });
|
||||
|
||||
if (froms.length !== 1 && this.tags.restriction !== 'no_entry') return false;
|
||||
if (froms.some(function(m) { return m.type !== 'way'; })) return false;
|
||||
|
||||
if (tos.length !== 1 && this.tags.restriction !== 'no_exit') return false;
|
||||
if (tos.some(function(m) { return m.type !== 'way'; })) return false;
|
||||
|
||||
if (vias.length === 0) return false;
|
||||
if (vias.length > 1 && vias.some(function(m) { return m.type !== 'way'; })) return false;
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
// Returns an array [A0, ... An], each Ai being an array of node arrays [Nds0, ... Ndsm],
|
||||
// where Nds0 is an outer ring and subsequent Ndsi's (if any i > 0) being inner rings.
|
||||
//
|
||||
@@ -264,8 +294,8 @@ _extend(osmRelation.prototype, {
|
||||
// rings not matched with the intended outer ring.
|
||||
//
|
||||
multipolygon: function(resolver) {
|
||||
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); }),
|
||||
inners = this.members.filter(function(m) { return 'inner' === m.role; });
|
||||
var outers = this.members.filter(function(m) { return 'outer' === (m.role || 'outer'); });
|
||||
var inners = this.members.filter(function(m) { return 'inner' === m.role; });
|
||||
|
||||
outers = osmJoinWays(outers, resolver);
|
||||
inners = osmJoinWays(inners, resolver);
|
||||
|
||||
+37
-22
@@ -1,26 +1,28 @@
|
||||
import { geoAngle } from '../geo';
|
||||
import { geoAngle, geoPathLength } from '../geo';
|
||||
|
||||
|
||||
export function svgTurns(projection) {
|
||||
|
||||
return function drawTurns(selection, graph, turns) {
|
||||
|
||||
function key(turn) {
|
||||
return [turn.from.node + turn.via.node + turn.to.node].join('-');
|
||||
}
|
||||
|
||||
function icon(turn) {
|
||||
var u = turn.u ? '-u' : '';
|
||||
if (!turn.restriction)
|
||||
return '#turn-yes' + u;
|
||||
var restriction = graph.entity(turn.restriction).tags.restriction;
|
||||
return '#turn-' +
|
||||
(!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + u;
|
||||
if (turn.no) return '#turn-no' + u;
|
||||
if (turn.only) return '#turn-only' + u;
|
||||
return '#turn-yes' + u;
|
||||
}
|
||||
|
||||
var layer = selection.selectAll('.layer-points .layer-points-turns');
|
||||
var layer = selection.selectAll('.data-layer-osm').selectAll('.layer-turns')
|
||||
.data([0]);
|
||||
|
||||
layer = layer.enter()
|
||||
.append('g')
|
||||
.attr('class', 'layer-osm layer-turns')
|
||||
.merge(layer);
|
||||
|
||||
|
||||
var groups = layer.selectAll('g.turn')
|
||||
.data(turns, key);
|
||||
.data(turns, function(d) { return d.key; });
|
||||
|
||||
groups.exit()
|
||||
.remove();
|
||||
@@ -28,10 +30,10 @@ export function svgTurns(projection) {
|
||||
|
||||
var enter = groups.enter()
|
||||
.append('g')
|
||||
.attr('class', 'turn');
|
||||
.attr('class', function(d) { return 'turn ' + d.key; });
|
||||
|
||||
var nEnter = enter
|
||||
.filter(function (turn) { return !turn.u; });
|
||||
.filter(function(d) { return !d.u; });
|
||||
|
||||
nEnter.append('rect')
|
||||
.attr('transform', 'translate(-22, -12)')
|
||||
@@ -45,7 +47,7 @@ export function svgTurns(projection) {
|
||||
|
||||
|
||||
var uEnter = enter
|
||||
.filter(function (turn) { return turn.u; });
|
||||
.filter(function(d) { return d.u; });
|
||||
|
||||
uEnter.append('circle')
|
||||
.attr('r', '16');
|
||||
@@ -60,14 +62,27 @@ export function svgTurns(projection) {
|
||||
.merge(enter);
|
||||
|
||||
groups
|
||||
.attr('transform', function (turn) {
|
||||
var v = graph.entity(turn.via.node),
|
||||
t = graph.entity(turn.to.node),
|
||||
a = geoAngle(v, t, projection),
|
||||
p = projection(v.loc),
|
||||
r = turn.u ? 0 : 60;
|
||||
.attr('opacity', function(d) {
|
||||
return d.direct === false ? '0.7' : null;
|
||||
})
|
||||
.attr('transform', function(d) {
|
||||
var pxRadius = 50;
|
||||
var toWay = graph.entity(d.to.way);
|
||||
var toPoints = graph.childNodes(toWay)
|
||||
.map(function (n) { return n.loc; })
|
||||
.map(projection);
|
||||
var toLength = geoPathLength(toPoints);
|
||||
var mid = toLength / 2; // midpoint of destination way
|
||||
|
||||
return 'translate(' + (r * Math.cos(a) + p[0]) + ',' + (r * Math.sin(a) + p[1]) + ') ' +
|
||||
var toNode = graph.entity(d.to.node);
|
||||
var toVertex = graph.entity(d.to.vertex);
|
||||
var a = geoAngle(toVertex, toNode, projection);
|
||||
var o = projection(toVertex.loc);
|
||||
var r = d.u ? 0 // u-turn: no radius
|
||||
: !toWay.__via ? pxRadius // leaf way: put marker at pxRadius
|
||||
: Math.min(mid, pxRadius); // via way: prefer pxRadius, fallback to mid for very short ways
|
||||
|
||||
return 'translate(' + (r * Math.cos(a) + o[0]) + ',' + (r * Math.sin(a) + o[1]) + ') ' +
|
||||
'rotate(' + a * 180 / Math.PI + ')';
|
||||
});
|
||||
|
||||
|
||||
+38
-20
@@ -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]; });
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
event as d3_event,
|
||||
select as d3_select
|
||||
} from 'd3-selection';
|
||||
|
||||
import marked from 'marked';
|
||||
import { t } from '../util/locale';
|
||||
import { svgIcon } from '../svg';
|
||||
import { icon } from 'intro/helper';
|
||||
|
||||
|
||||
// This currently only works with the 'restrictions' field
|
||||
// It borrows some code from uiHelp
|
||||
|
||||
export function uiFieldHelp(context, fieldName) {
|
||||
var fieldHelp = {};
|
||||
var _inspector = d3_select(null);
|
||||
var _wrap = d3_select(null);
|
||||
var _body = d3_select(null);
|
||||
|
||||
var fieldHelpKeys = {
|
||||
restrictions: [
|
||||
['about',[
|
||||
'about',
|
||||
'from_via_to',
|
||||
'maxdist',
|
||||
'maxvia'
|
||||
]],
|
||||
['inspecting',[
|
||||
'about',
|
||||
'from_shadow',
|
||||
'allow_shadow',
|
||||
'restrict_shadow',
|
||||
'only_shadow',
|
||||
'restricted',
|
||||
'only'
|
||||
]],
|
||||
['modifying',[
|
||||
'about',
|
||||
'indicators',
|
||||
'allow_turn',
|
||||
'restrict_turn',
|
||||
'only_turn'
|
||||
]],
|
||||
['tips',[
|
||||
'simple',
|
||||
'simple_example',
|
||||
'indirect',
|
||||
'indirect_example',
|
||||
'indirect_noedit'
|
||||
]]
|
||||
]
|
||||
};
|
||||
|
||||
var fieldHelpHeadings = {};
|
||||
|
||||
var replacements = {
|
||||
distField: t('restriction.controls.distance'),
|
||||
viaField: t('restriction.controls.via'),
|
||||
fromShadow: icon('#turn-shadow', 'pre-text shadow from'),
|
||||
allowShadow: icon('#turn-shadow', 'pre-text shadow allow'),
|
||||
restrictShadow: icon('#turn-shadow', 'pre-text shadow restrict'),
|
||||
onlyShadow: icon('#turn-shadow', 'pre-text shadow only'),
|
||||
allowTurn: icon('#turn-yes', 'pre-text turn'),
|
||||
restrictTurn: icon('#turn-no', 'pre-text turn'),
|
||||
onlyTurn: icon('#turn-only', 'pre-text turn')
|
||||
};
|
||||
|
||||
|
||||
// For each section, squash all the texts into a single markdown document
|
||||
var docs = fieldHelpKeys[fieldName].map(function(key) {
|
||||
var helpkey = 'help.field.' + fieldName + '.' + key[0];
|
||||
var text = key[1].reduce(function(all, part) {
|
||||
var subkey = helpkey + '.' + part;
|
||||
var depth = fieldHelpHeadings[subkey]; // is this subkey a heading?
|
||||
var hhh = depth ? Array(depth + 1).join('#') + ' ' : ''; // if so, prepend with some ##'s
|
||||
return all + hhh + t(subkey, replacements) + '\n\n';
|
||||
}, '');
|
||||
|
||||
return {
|
||||
key: helpkey,
|
||||
title: t(helpkey + '.title'),
|
||||
html: marked(text.trim())
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function show() {
|
||||
updatePosition();
|
||||
|
||||
_body
|
||||
.classed('hide', false)
|
||||
.style('opacity', '0')
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', '1');
|
||||
}
|
||||
|
||||
|
||||
function hide() {
|
||||
_body
|
||||
.classed('hide', true)
|
||||
.transition()
|
||||
.duration(200)
|
||||
.style('opacity', '0')
|
||||
.on('end', function () {
|
||||
_body.classed('hide', true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function clickHelp(index) {
|
||||
var d = docs[index];
|
||||
var tkeys = fieldHelpKeys[fieldName][index][1];
|
||||
|
||||
_body.selectAll('.field-help-nav-item')
|
||||
.classed('active', function(d, i) { return i === index; });
|
||||
|
||||
var content = _body.selectAll('.field-help-content')
|
||||
.html(d.html);
|
||||
|
||||
// class the paragraphs so we can find and style them
|
||||
content.selectAll('p')
|
||||
.attr('class', function(d, i) { return tkeys[i]; });
|
||||
|
||||
// insert special content for certain help sections
|
||||
if (d.key === 'help.field.restrictions.inspecting') {
|
||||
content
|
||||
.insert('img', 'p.from_shadow')
|
||||
.attr('class', 'field-help-image cf')
|
||||
.attr('src', context.imagePath('tr_inspect.gif'));
|
||||
|
||||
} else if (d.key === 'help.field.restrictions.modifying') {
|
||||
content
|
||||
.insert('img', 'p.allow_turn')
|
||||
.attr('class', 'field-help-image cf')
|
||||
.attr('src', context.imagePath('tr_modify.gif'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fieldHelp.button = function(selection) {
|
||||
if (_body.empty()) return;
|
||||
|
||||
var button = selection.selectAll('.field-help-button')
|
||||
.data([0]);
|
||||
|
||||
// enter/update
|
||||
button.enter()
|
||||
.append('button')
|
||||
.attr('class', 'field-help-button')
|
||||
.attr('tabindex', -1)
|
||||
.call(svgIcon('#icon-help'))
|
||||
.merge(button)
|
||||
.on('click', function () {
|
||||
d3_event.stopPropagation();
|
||||
d3_event.preventDefault();
|
||||
if (_body.classed('hide')) {
|
||||
show();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
function updatePosition() {
|
||||
var wrap = _wrap.node();
|
||||
var inspector = _inspector.node();
|
||||
var wRect = wrap.getBoundingClientRect();
|
||||
var iRect = inspector.getBoundingClientRect();
|
||||
|
||||
_body
|
||||
.style('top', wRect.top + inspector.scrollTop - iRect.top + 'px');
|
||||
}
|
||||
|
||||
|
||||
fieldHelp.body = function(selection) {
|
||||
// This control expects the field to have a preset-input-wrap div
|
||||
_wrap = selection.selectAll('.preset-input-wrap');
|
||||
if (_wrap.empty()) return;
|
||||
|
||||
// absolute position relative to the inspector, so it "floats" above the fields
|
||||
_inspector = d3_select('#sidebar .entity-editor-pane .inspector-body');
|
||||
if (_inspector.empty()) return;
|
||||
|
||||
_body = _inspector.selectAll('.field-help-body')
|
||||
.data([0]);
|
||||
|
||||
var enter = _body.enter()
|
||||
.append('div')
|
||||
.attr('class', 'field-help-body hide'); // initially hidden
|
||||
|
||||
var titleEnter = enter
|
||||
.append('div')
|
||||
.attr('class', 'field-help-title cf');
|
||||
|
||||
titleEnter
|
||||
.append('h2')
|
||||
.attr('class', 'fl')
|
||||
.text(t('help.field.' + fieldName + '.title'));
|
||||
|
||||
titleEnter
|
||||
.append('button')
|
||||
.attr('class', 'fr close')
|
||||
.on('click', function() {
|
||||
d3_event.stopPropagation();
|
||||
d3_event.preventDefault();
|
||||
hide();
|
||||
})
|
||||
.call(svgIcon('#icon-close'));
|
||||
|
||||
var navEnter = enter
|
||||
.append('div')
|
||||
.attr('class', 'field-help-nav cf');
|
||||
|
||||
var titles = docs.map(function(d) { return d.title; });
|
||||
navEnter.selectAll('.field-help-nav-item')
|
||||
.data(titles)
|
||||
.enter()
|
||||
.append('div')
|
||||
.attr('class', 'field-help-nav-item')
|
||||
.text(function(d) { return d; })
|
||||
.on('click', function(d, i) {
|
||||
d3_event.stopPropagation();
|
||||
d3_event.preventDefault();
|
||||
clickHelp(i);
|
||||
});
|
||||
|
||||
enter
|
||||
.append('div')
|
||||
.attr('class', 'field-help-content');
|
||||
|
||||
_body = _body
|
||||
.merge(enter);
|
||||
|
||||
clickHelp(0);
|
||||
};
|
||||
|
||||
|
||||
return fieldHelp;
|
||||
}
|
||||
+510
-121
@@ -1,3 +1,5 @@
|
||||
import _cloneDeep from 'lodash-es/cloneDeep';
|
||||
|
||||
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
||||
|
||||
import {
|
||||
@@ -6,30 +8,24 @@ import {
|
||||
} from 'd3-selection';
|
||||
|
||||
import { t } from '../../util/locale';
|
||||
|
||||
import {
|
||||
behaviorBreathe,
|
||||
behaviorHover
|
||||
} from '../../behavior';
|
||||
|
||||
import {
|
||||
osmEntity,
|
||||
osmIntersection,
|
||||
osmInferRestriction,
|
||||
osmTurn
|
||||
} from '../../osm';
|
||||
|
||||
import {
|
||||
actionRestrictTurn,
|
||||
actionUnrestrictTurn
|
||||
} from '../../actions';
|
||||
import { actionRestrictTurn, actionUnrestrictTurn } from '../../actions';
|
||||
import { behaviorBreathe } from '../../behavior';
|
||||
|
||||
import {
|
||||
geoExtent,
|
||||
geoRawMercator,
|
||||
geoVecScale,
|
||||
geoVecSubtract,
|
||||
geoZoomToScale
|
||||
} from '../../geo';
|
||||
|
||||
import {
|
||||
osmIntersection,
|
||||
osmInferRestriction,
|
||||
osmTurn,
|
||||
osmWay
|
||||
} from '../../osm';
|
||||
|
||||
import {
|
||||
svgLayers,
|
||||
svgLines,
|
||||
@@ -37,8 +33,15 @@ import {
|
||||
svgVertices
|
||||
} from '../../svg';
|
||||
|
||||
import { utilRebind } from '../../util/rebind';
|
||||
import { utilFunctor } from '../../util';
|
||||
import {
|
||||
utilDisplayName,
|
||||
utilDisplayType,
|
||||
utilEntitySelector,
|
||||
utilFunctor,
|
||||
utilRebind
|
||||
} from '../../util';
|
||||
|
||||
import { utilDetect } from '../../util/detect';
|
||||
|
||||
import {
|
||||
utilGetDimensions,
|
||||
@@ -49,103 +52,273 @@ import {
|
||||
export function uiFieldRestrictions(field, context) {
|
||||
var dispatch = d3_dispatch('change');
|
||||
var breathe = behaviorBreathe(context);
|
||||
var hover = behaviorHover(context);
|
||||
var initialized = false;
|
||||
var vertexID;
|
||||
var fromNodeID;
|
||||
var storedViaWay = context.storage('turn-restriction-via-way');
|
||||
var storedDistance = context.storage('turn-restriction-distance');
|
||||
|
||||
var _maxViaWay = storedViaWay !== null ? (+storedViaWay) : 1;
|
||||
var _maxDistance = storedDistance ? (+storedDistance) : 30;
|
||||
var _initialized = false;
|
||||
var _parent = d3_select(null); // the entire field
|
||||
var _container = d3_select(null); // just the map
|
||||
var _oldTurns;
|
||||
var _graph;
|
||||
var _vertexID;
|
||||
var _intersection;
|
||||
var _fromWayID;
|
||||
|
||||
|
||||
function restrictions(selection) {
|
||||
_parent = selection;
|
||||
|
||||
// try to reuse the intersection, but always rebuild it if the graph has changed
|
||||
if (_vertexID && (context.graph() !== _graph || !_intersection)) {
|
||||
_graph = context.graph();
|
||||
_intersection = osmIntersection(_graph, _vertexID, _maxDistance);
|
||||
}
|
||||
|
||||
// It's possible for there to be no actual intersection here.
|
||||
// for example, a vertex of two `highway=path`
|
||||
// In this case, hide the field.
|
||||
var isOK = (_intersection && _intersection.vertices.length && _intersection.ways.length);
|
||||
d3_select(selection.node().parentNode).classed('hide', !isOK);
|
||||
|
||||
// if form field is hidden or has detached from dom, clean up.
|
||||
if (!d3_select('.inspector-wrap.inspector-hidden').empty() || !selection.node().parentNode) {
|
||||
if (!isOK ||
|
||||
!d3_select('.inspector-wrap.inspector-hidden').empty() ||
|
||||
!selection.node().parentNode ||
|
||||
!selection.node().parentNode.parentNode) {
|
||||
selection.call(restrictions.off);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var wrap = selection.selectAll('.preset-input-wrap')
|
||||
.data([0]);
|
||||
|
||||
var enter = wrap.enter()
|
||||
wrap = wrap.enter()
|
||||
.append('div')
|
||||
.attr('class', 'preset-input-wrap');
|
||||
.attr('class', 'preset-input-wrap')
|
||||
.merge(wrap);
|
||||
|
||||
enter
|
||||
var container = wrap.selectAll('.restriction-container')
|
||||
.data([0]);
|
||||
|
||||
// enter
|
||||
var containerEnter = container.enter()
|
||||
.append('div')
|
||||
.attr('class', 'restriction-container');
|
||||
|
||||
containerEnter
|
||||
.append('div')
|
||||
.attr('class', 'restriction-help');
|
||||
|
||||
// update
|
||||
_container = containerEnter
|
||||
.merge(container)
|
||||
.call(renderViewer);
|
||||
|
||||
var intersection = osmIntersection(context.graph(), vertexID);
|
||||
var graph = intersection.graph;
|
||||
var vertex = graph.entity(vertexID);
|
||||
var controls = wrap.selectAll('.restriction-controls')
|
||||
.data([0]);
|
||||
|
||||
// enter/update
|
||||
controls.enter()
|
||||
.append('div')
|
||||
.attr('class', 'restriction-controls-container')
|
||||
.append('div')
|
||||
.attr('class', 'restriction-controls')
|
||||
.merge(controls)
|
||||
.call(renderControls);
|
||||
}
|
||||
|
||||
|
||||
function renderControls(selection) {
|
||||
var distControl = selection.selectAll('.restriction-distance')
|
||||
.data([0]);
|
||||
|
||||
distControl.exit()
|
||||
.remove();
|
||||
|
||||
var distControlEnter = distControl.enter()
|
||||
.append('div')
|
||||
.attr('class', 'restriction-control restriction-distance');
|
||||
|
||||
distControlEnter
|
||||
.append('span')
|
||||
.attr('class', 'restriction-control-label restriction-distance-label')
|
||||
.text(t('restriction.controls.distance') + ':');
|
||||
|
||||
distControlEnter
|
||||
.append('input')
|
||||
.attr('class', 'restriction-distance-input')
|
||||
.attr('type', 'range')
|
||||
.attr('min', '20')
|
||||
.attr('max', '50')
|
||||
.attr('step', '5');
|
||||
|
||||
distControlEnter
|
||||
.append('span')
|
||||
.attr('class', 'restriction-distance-text');
|
||||
|
||||
// update
|
||||
selection.selectAll('.restriction-distance-input')
|
||||
.property('value', _maxDistance)
|
||||
.on('input', function() {
|
||||
var val = d3_select(this).property('value');
|
||||
_maxDistance = +val;
|
||||
_intersection = null;
|
||||
_container.selectAll('.layer-osm .layer-turns *').remove();
|
||||
context.storage('turn-restriction-distance', _maxDistance);
|
||||
_parent.call(restrictions);
|
||||
});
|
||||
|
||||
selection.selectAll('.restriction-distance-text')
|
||||
.text(displayMaxDistance(_maxDistance));
|
||||
|
||||
|
||||
var viaControl = selection.selectAll('.restriction-via-way')
|
||||
.data([0]);
|
||||
|
||||
viaControl.exit()
|
||||
.remove();
|
||||
|
||||
var viaControlEnter = viaControl.enter()
|
||||
.append('div')
|
||||
.attr('class', 'restriction-control restriction-via-way');
|
||||
|
||||
viaControlEnter
|
||||
.append('span')
|
||||
.attr('class', 'restriction-control-label restriction-via-way-label')
|
||||
.text(t('restriction.controls.via') + ':');
|
||||
|
||||
viaControlEnter
|
||||
.append('input')
|
||||
.attr('class', 'restriction-via-way-input')
|
||||
.attr('type', 'range')
|
||||
.attr('min', '0')
|
||||
.attr('max', '2')
|
||||
.attr('step', '1');
|
||||
|
||||
viaControlEnter
|
||||
.append('span')
|
||||
.attr('class', 'restriction-via-way-text');
|
||||
|
||||
// update
|
||||
selection.selectAll('.restriction-via-way-input')
|
||||
.property('value', _maxViaWay)
|
||||
.on('input', function() {
|
||||
var val = d3_select(this).property('value');
|
||||
_maxViaWay = +val;
|
||||
_container.selectAll('.layer-osm .layer-turns *').remove();
|
||||
context.storage('turn-restriction-via-way', _maxViaWay);
|
||||
_parent.call(restrictions);
|
||||
});
|
||||
|
||||
selection.selectAll('.restriction-via-way-text')
|
||||
.text(displayMaxVia(_maxViaWay));
|
||||
}
|
||||
|
||||
|
||||
function renderViewer(selection) {
|
||||
if (!_intersection) return;
|
||||
|
||||
var vgraph = _intersection.graph;
|
||||
var filter = utilFunctor(true);
|
||||
var projection = geoRawMercator();
|
||||
|
||||
var d = utilGetDimensions(wrap.merge(enter));
|
||||
var c = [d[0] / 2, d[1] / 2];
|
||||
var z = 24;
|
||||
var d = utilGetDimensions(selection);
|
||||
var c = geoVecScale(d, 0.5);
|
||||
var z = 22;
|
||||
|
||||
projection.scale(geoZoomToScale(z));
|
||||
|
||||
// Calculate extent of all key vertices
|
||||
var extent = geoExtent();
|
||||
for (var i = 0; i < _intersection.vertices.length; i++) {
|
||||
extent._extend(_intersection.vertices[i].extent());
|
||||
}
|
||||
|
||||
// If this is a large intersection, adjust zoom to fit extent
|
||||
if (_intersection.vertices.length > 1) {
|
||||
var padding = 180; // in z22 pixels
|
||||
var tl = projection([extent[0][0], extent[1][1]]);
|
||||
var br = projection([extent[1][0], extent[0][1]]);
|
||||
var hFactor = (br[0] - tl[0]) / (d[0] - padding);
|
||||
var vFactor = (br[1] - tl[1]) / (d[1] - padding);
|
||||
var hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2;
|
||||
var vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2;
|
||||
z = z - Math.max(hZoomDiff, vZoomDiff);
|
||||
projection.scale(geoZoomToScale(z));
|
||||
}
|
||||
|
||||
var padTop = 35; // reserve top space for hint text
|
||||
var extentCenter = projection(extent.center());
|
||||
extentCenter[1] = extentCenter[1] - padTop;
|
||||
|
||||
projection
|
||||
.scale(geoZoomToScale(z));
|
||||
|
||||
var s = projection(vertex.loc);
|
||||
|
||||
projection
|
||||
.translate([c[0] - s[0], c[1] - s[1]])
|
||||
.translate(geoVecSubtract(c, extentCenter))
|
||||
.clipExtent([[0, 0], d]);
|
||||
|
||||
var extent = geoExtent(projection.invert([0, d[1]]), projection.invert([d[0], 0]));
|
||||
|
||||
var drawLayers = svgLayers(projection, context).only('osm').dimensions(d);
|
||||
var drawVertices = svgVertices(projection, context);
|
||||
var drawLines = svgLines(projection, context);
|
||||
var drawTurns = svgTurns(projection, context);
|
||||
|
||||
enter
|
||||
var firstTime = selection.selectAll('.surface').empty();
|
||||
|
||||
selection
|
||||
.call(drawLayers);
|
||||
|
||||
wrap = wrap
|
||||
.merge(enter);
|
||||
var surface = selection.selectAll('.surface')
|
||||
.classed('tr', true);
|
||||
|
||||
var surface = wrap.selectAll('.surface');
|
||||
if (firstTime) {
|
||||
_initialized = true;
|
||||
|
||||
if (!enter.empty()) {
|
||||
initialized = true;
|
||||
surface
|
||||
.call(breathe)
|
||||
.call(hover);
|
||||
.call(breathe);
|
||||
|
||||
d3_select(window)
|
||||
.on('resize.restrictions', function() {
|
||||
utilSetDimensions(_container, null);
|
||||
redraw();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// This can happen if we've lowered the detail while a FROM way
|
||||
// is selected, and that way is no longer part of the intersection.
|
||||
if (_fromWayID && !vgraph.hasEntity(_fromWayID)) {
|
||||
_fromWayID = null;
|
||||
_oldTurns = null;
|
||||
}
|
||||
|
||||
surface
|
||||
.call(utilSetDimensions, d)
|
||||
.call(drawVertices, graph, [vertex], filter, extent, true)
|
||||
.call(drawLines, graph, intersection.ways, filter)
|
||||
.call(drawTurns, graph, intersection.turns(fromNodeID));
|
||||
.call(drawVertices, vgraph, _intersection.vertices, filter, extent, z)
|
||||
.call(drawLines, vgraph, _intersection.ways, filter)
|
||||
.call(drawTurns, vgraph, _intersection.turns(_fromWayID, _maxViaWay));
|
||||
|
||||
surface
|
||||
.on('click.restrictions', click)
|
||||
.on('mouseover.restrictions', mouseover)
|
||||
.on('mouseout.restrictions', mouseout);
|
||||
.on('mouseover.restrictions', mouseover);
|
||||
|
||||
surface
|
||||
.selectAll('.selected')
|
||||
.classed('selected', false);
|
||||
|
||||
if (fromNodeID) {
|
||||
surface
|
||||
.selectAll('.related')
|
||||
.classed('related', false);
|
||||
|
||||
if (_fromWayID) {
|
||||
var way = vgraph.entity(_fromWayID);
|
||||
surface
|
||||
.selectAll('.' + intersection.highways[fromNodeID].id)
|
||||
.classed('selected', true);
|
||||
.selectAll('.' + _fromWayID)
|
||||
.classed('selected', true)
|
||||
.classed('related', true);
|
||||
}
|
||||
|
||||
mouseout();
|
||||
|
||||
context.history()
|
||||
.on('change.restrictions', render);
|
||||
|
||||
d3_select(window)
|
||||
.on('resize.restrictions', function() {
|
||||
utilSetDimensions(wrap, null);
|
||||
render();
|
||||
});
|
||||
updateHints(null);
|
||||
|
||||
|
||||
function click() {
|
||||
@@ -155,79 +328,300 @@ export function uiFieldRestrictions(field, context) {
|
||||
|
||||
var datum = d3_event.target.__data__;
|
||||
var entity = datum && datum.properties && datum.properties.entity;
|
||||
if (entity) datum = entity;
|
||||
if (entity) {
|
||||
datum = entity;
|
||||
}
|
||||
|
||||
if (datum instanceof osmEntity) {
|
||||
fromNodeID = intersection.adjacentNodeId(datum.id);
|
||||
render();
|
||||
if (datum instanceof osmWay && (datum.__from || datum.__via)) {
|
||||
_fromWayID = datum.id;
|
||||
_oldTurns = null;
|
||||
redraw();
|
||||
|
||||
} else if (datum instanceof osmTurn) {
|
||||
if (datum.restriction) {
|
||||
context.perform(
|
||||
actionUnrestrictTurn(datum, projection),
|
||||
t('operations.restriction.annotation.delete')
|
||||
);
|
||||
} else {
|
||||
context.perform(
|
||||
actionRestrictTurn(datum, projection),
|
||||
var actions, extraActions, turns, i;
|
||||
var restrictionType = osmInferRestriction(vgraph, datum, projection);
|
||||
|
||||
if (datum.restrictionID && !datum.direct) {
|
||||
return;
|
||||
|
||||
} else if (datum.restrictionID && !datum.only) { // NO -> ONLY
|
||||
var datumOnly = _cloneDeep(datum);
|
||||
datumOnly.only = true;
|
||||
restrictionType = restrictionType.replace(/^no/, 'only');
|
||||
|
||||
// Adding an ONLY restriction should destroy all other direct restrictions from the FROM.
|
||||
// We will remember them in _oldTurns, and restore them if the user clicks again.
|
||||
turns = _intersection.turns(_fromWayID, 2);
|
||||
extraActions = [];
|
||||
_oldTurns = [];
|
||||
for (i = 0; i < turns.length; i++) {
|
||||
if (turns[i].direct) {
|
||||
turns[i].restrictionType = osmInferRestriction(vgraph, turns[i], projection);
|
||||
_oldTurns.push(turns[i]);
|
||||
extraActions.push(actionUnrestrictTurn(turns[i]));
|
||||
}
|
||||
}
|
||||
|
||||
actions = _intersection.actions.concat(extraActions, [
|
||||
actionRestrictTurn(datumOnly, restrictionType),
|
||||
t('operations.restriction.annotation.create')
|
||||
);
|
||||
]);
|
||||
|
||||
} else if (datum.restrictionID) { // ONLY -> Allowed
|
||||
// Restore whatever restrictions we might have destroyed by cycling thru the ONLY state.
|
||||
// This relies on the assumption that the intersection was already split up when we
|
||||
// performed the previous action (NO -> ONLY), so the IDs in _oldTurns shouldn't have changed.
|
||||
turns = _oldTurns || [];
|
||||
extraActions = [];
|
||||
for (i = 0; i < turns.length; i++) {
|
||||
if (turns[i].key !== datum.key) {
|
||||
extraActions.push(actionRestrictTurn(turns[i], turns[i].restrictionType));
|
||||
}
|
||||
}
|
||||
_oldTurns = null;
|
||||
|
||||
actions = _intersection.actions.concat(extraActions, [
|
||||
actionUnrestrictTurn(datum),
|
||||
t('operations.restriction.annotation.delete')
|
||||
]);
|
||||
|
||||
} else { // Allowed -> NO
|
||||
actions = _intersection.actions.concat([
|
||||
actionRestrictTurn(datum, restrictionType),
|
||||
t('operations.restriction.annotation.create')
|
||||
]);
|
||||
}
|
||||
|
||||
context.perform.apply(context, actions);
|
||||
|
||||
// At this point the datum will be changed, but will have same key..
|
||||
// Refresh it and update the help..
|
||||
var s = surface.selectAll('.' + datum.key);
|
||||
datum = s.empty() ? null : s.datum();
|
||||
updateHints(datum);
|
||||
|
||||
} else {
|
||||
_fromWayID = null;
|
||||
_oldTurns = null;
|
||||
redraw();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mouseover() {
|
||||
var datum = d3_event.target.__data__;
|
||||
if (datum instanceof osmTurn) {
|
||||
var graph = context.graph();
|
||||
var presets = context.presets();
|
||||
var preset;
|
||||
updateHints(datum);
|
||||
}
|
||||
|
||||
if (datum.restriction) {
|
||||
preset = presets.match(graph.entity(datum.restriction), graph);
|
||||
} else {
|
||||
preset = presets.item('type/restriction/' +
|
||||
osmInferRestriction(
|
||||
graph,
|
||||
datum.from,
|
||||
datum.via,
|
||||
datum.to,
|
||||
projection
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
wrap.selectAll('.restriction-help')
|
||||
.text(t('operations.restriction.help.' +
|
||||
(datum.restriction ? 'toggle_off' : 'toggle_on'),
|
||||
{ restriction: preset.name() })
|
||||
);
|
||||
function redraw() {
|
||||
if (context.hasEntity(_vertexID)) {
|
||||
_container.call(renderViewer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function mouseout() {
|
||||
wrap.selectAll('.restriction-help')
|
||||
.text(t('operations.restriction.help.' +
|
||||
(fromNodeID ? 'toggle' : 'select'))
|
||||
);
|
||||
function highlightPathsFrom(wayID) {
|
||||
surface.selectAll('.related')
|
||||
.classed('related', false)
|
||||
.classed('allow', false)
|
||||
.classed('restrict', false)
|
||||
.classed('only', false);
|
||||
|
||||
surface.selectAll('.' + wayID)
|
||||
.classed('related', true);
|
||||
|
||||
if (wayID) {
|
||||
var turns = _intersection.turns(wayID, _maxViaWay);
|
||||
for (var i = 0; i < turns.length; i++) {
|
||||
var turn = turns[i];
|
||||
var ids = [turn.to.way];
|
||||
var klass = (turn.no ? 'restrict' : (turn.only ? 'only' : 'allow'));
|
||||
|
||||
if (turn.only || turns.length === 1) {
|
||||
if (turn.via.ways) {
|
||||
ids = ids.concat(turn.via.ways);
|
||||
}
|
||||
} else if (turn.to.way === wayID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
surface.selectAll(utilEntitySelector(ids))
|
||||
.classed('related', true)
|
||||
.classed('allow', (klass === 'allow'))
|
||||
.classed('restrict', (klass === 'restrict'))
|
||||
.classed('only', (klass === 'only'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function render() {
|
||||
if (context.hasEntity(vertexID)) {
|
||||
restrictions(selection);
|
||||
function updateHints(datum) {
|
||||
var help = _container.selectAll('.restriction-help').html('');
|
||||
|
||||
var placeholders = {};
|
||||
['from', 'via', 'to'].forEach(function(k) {
|
||||
placeholders[k] = '<span class="qualifier">' + t('restriction.help.' + k) + '</span>';
|
||||
});
|
||||
|
||||
var entity = datum && datum.properties && datum.properties.entity;
|
||||
if (entity) {
|
||||
datum = entity;
|
||||
}
|
||||
|
||||
if (_fromWayID) {
|
||||
way = vgraph.entity(_fromWayID);
|
||||
surface
|
||||
.selectAll('.' + _fromWayID)
|
||||
.classed('selected', true)
|
||||
.classed('related', true);
|
||||
}
|
||||
|
||||
// Hovering a way
|
||||
if (datum instanceof osmWay && datum.__from) {
|
||||
way = datum;
|
||||
|
||||
highlightPathsFrom(_fromWayID ? null : way.id);
|
||||
surface.selectAll('.' + way.id)
|
||||
.classed('related', true);
|
||||
|
||||
var clickSelect = (!_fromWayID || _fromWayID !== way.id);
|
||||
help
|
||||
.append('div') // "Click to select FROM {fromName}." / "FROM {fromName}"
|
||||
.html(t('restriction.help.' + (clickSelect ? 'select_from_name' : 'from_name'), {
|
||||
from: placeholders.from,
|
||||
fromName: displayName(way.id, vgraph)
|
||||
}));
|
||||
|
||||
|
||||
// Hovering a turn arrow
|
||||
} else if (datum instanceof osmTurn) {
|
||||
var restrictionType = osmInferRestriction(vgraph, datum, projection);
|
||||
var turnType = restrictionType.replace(/^(only|no)\_/, '');
|
||||
var indirect = (datum.direct === false ? t('restriction.help.indirect') : '');
|
||||
var klass, turnText, nextText;
|
||||
|
||||
if (datum.no) {
|
||||
klass = 'restrict';
|
||||
turnText = t('restriction.help.turn.no_' + turnType, { indirect: indirect });
|
||||
nextText = t('restriction.help.turn.only_' + turnType, { indirect: '' });
|
||||
} else if (datum.only) {
|
||||
klass = 'only';
|
||||
turnText = t('restriction.help.turn.only_' + turnType, { indirect: indirect });
|
||||
nextText = t('restriction.help.turn.allowed_' + turnType, { indirect: '' });
|
||||
} else {
|
||||
klass = 'allow';
|
||||
turnText = t('restriction.help.turn.allowed_' + turnType, { indirect: indirect });
|
||||
nextText = t('restriction.help.turn.no_' + turnType, { indirect: '' });
|
||||
}
|
||||
|
||||
help
|
||||
.append('div') // "NO Right Turn (indirect)"
|
||||
.attr('class', 'qualifier ' + klass)
|
||||
.text(turnText);
|
||||
|
||||
help
|
||||
.append('div') // "FROM {fromName} TO {toName}"
|
||||
.html(t('restriction.help.from_name_to_name', {
|
||||
from: placeholders.from,
|
||||
fromName: displayName(datum.from.way, vgraph),
|
||||
to: placeholders.to,
|
||||
toName: displayName(datum.to.way, vgraph)
|
||||
}));
|
||||
|
||||
if (datum.via.ways && datum.via.ways.length) {
|
||||
var names = [];
|
||||
for (var i = 0; i < datum.via.ways.length; i++) {
|
||||
var prev = names[names.length - 1];
|
||||
var curr = displayName(datum.via.ways[i], vgraph);
|
||||
if (!prev || curr !== prev) // collapse identical names
|
||||
names.push(curr);
|
||||
}
|
||||
|
||||
help
|
||||
.append('div') // "VIA {viaNames}"
|
||||
.html(t('restriction.help.via_names', {
|
||||
via: placeholders.via,
|
||||
viaNames: names.join(', ')
|
||||
}));
|
||||
}
|
||||
|
||||
if (!indirect) {
|
||||
help
|
||||
.append('div') // Click for "No Right Turn"
|
||||
.text(t('restriction.help.toggle', { turn: nextText.trim() }));
|
||||
}
|
||||
|
||||
highlightPathsFrom(null);
|
||||
var alongIDs = datum.path.slice();
|
||||
surface.selectAll(utilEntitySelector(alongIDs))
|
||||
.classed('related', true)
|
||||
.classed('allow', (klass === 'allow'))
|
||||
.classed('restrict', (klass === 'restrict'))
|
||||
.classed('only', (klass === 'only'));
|
||||
|
||||
|
||||
// Hovering empty surface
|
||||
} else {
|
||||
highlightPathsFrom(null);
|
||||
if (_fromWayID) {
|
||||
help
|
||||
.append('div') // "FROM {fromName}"
|
||||
.html(t('restriction.help.from_name', {
|
||||
from: placeholders.from,
|
||||
fromName: displayName(_fromWayID, vgraph)
|
||||
}));
|
||||
|
||||
} else {
|
||||
help
|
||||
.append('div') // "Click to select a FROM segment."
|
||||
.html(t('restriction.help.select_from', {
|
||||
from: placeholders.from
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
restrictions.entity = function(_) {
|
||||
if (!vertexID || vertexID !== _.id) {
|
||||
fromNodeID = null;
|
||||
vertexID = _.id;
|
||||
function displayMaxDistance(maxDist) {
|
||||
var isImperial = (utilDetect().locale.toLowerCase() === 'en-us');
|
||||
var opts;
|
||||
|
||||
if (isImperial) {
|
||||
var distToFeet = { // imprecise conversion for prettier display
|
||||
20: 70, 25: 85, 30: 100, 35: 115, 40: 130, 45: 145, 50: 160
|
||||
}[maxDist];
|
||||
opts = { distance: t('units.feet', { quantity: distToFeet }) };
|
||||
} else {
|
||||
opts = { distance: t('units.meters', { quantity: maxDist }) };
|
||||
}
|
||||
|
||||
return t('restriction.controls.distance_up_to', opts);
|
||||
}
|
||||
|
||||
|
||||
function displayMaxVia(maxVia) {
|
||||
return maxVia === 0 ? t('restriction.controls.via_node_only')
|
||||
: maxVia === 1 ? t('restriction.controls.via_up_to_one')
|
||||
: t('restriction.controls.via_up_to_two');
|
||||
}
|
||||
|
||||
|
||||
function displayName(entityID, graph) {
|
||||
var entity = graph.entity(entityID);
|
||||
var name = utilDisplayName(entity) || '';
|
||||
var matched = context.presets().match(entity, graph);
|
||||
var type = (matched && matched.name()) || utilDisplayType(entity.id);
|
||||
return name || type;
|
||||
}
|
||||
|
||||
|
||||
restrictions.entity = function(_) {
|
||||
_intersection = null;
|
||||
_fromWayID = null;
|
||||
_oldTurns = null;
|
||||
_vertexID = _.id;
|
||||
};
|
||||
|
||||
|
||||
@@ -236,17 +630,12 @@ export function uiFieldRestrictions(field, context) {
|
||||
|
||||
|
||||
restrictions.off = function(selection) {
|
||||
if (!initialized) return;
|
||||
if (!_initialized) return;
|
||||
|
||||
selection.selectAll('.surface')
|
||||
.call(hover.off)
|
||||
.call(breathe.off)
|
||||
.on('click.restrictions', null)
|
||||
.on('mouseover.restrictions', null)
|
||||
.on('mouseout.restrictions', null);
|
||||
|
||||
context.history()
|
||||
.on('change.restrictions', null);
|
||||
.on('mouseover.restrictions', null);
|
||||
|
||||
d3_select(window)
|
||||
.on('resize.restrictions', null);
|
||||
|
||||
@@ -19,6 +19,7 @@ export { uiEntityEditor } from './entity_editor';
|
||||
export { uiFeatureInfo } from './feature_info';
|
||||
export { uiFeatureList } from './feature_list';
|
||||
export { uiField } from './field';
|
||||
export { uiFieldHelp } from './field_help';
|
||||
export { uiFlash } from './flash';
|
||||
export { uiFormFields } from './form_fields';
|
||||
export { uiFullScreen } from './full_screen';
|
||||
|
||||
+24
-19
@@ -1,4 +1,5 @@
|
||||
import { interpolate as d3_interpolate } from 'd3-interpolate';
|
||||
import { selectAll as d3_selectAll } from 'd3-selection';
|
||||
|
||||
import { uiEntityEditor } from './entity_editor';
|
||||
import { uiPresetList } from './preset_list';
|
||||
@@ -6,22 +7,22 @@ import { uiViewOnOSM } from './view_on_osm';
|
||||
|
||||
|
||||
export function uiInspector(context) {
|
||||
var presetList = uiPresetList(context),
|
||||
entityEditor = uiEntityEditor(context),
|
||||
state = 'select',
|
||||
entityID,
|
||||
newFeature = false;
|
||||
var presetList = uiPresetList(context);
|
||||
var entityEditor = uiEntityEditor(context);
|
||||
var _state = 'select';
|
||||
var _entityID;
|
||||
var _newFeature = false;
|
||||
|
||||
|
||||
function inspector(selection) {
|
||||
presetList
|
||||
.entityID(entityID)
|
||||
.autofocus(newFeature)
|
||||
.entityID(_entityID)
|
||||
.autofocus(_newFeature)
|
||||
.on('choose', setPreset);
|
||||
|
||||
entityEditor
|
||||
.state(state)
|
||||
.entityID(entityID)
|
||||
.state(_state)
|
||||
.entityID(_entityID)
|
||||
.on('choose', showList);
|
||||
|
||||
var wrap = selection.selectAll('.panewrap')
|
||||
@@ -44,8 +45,8 @@ export function uiInspector(context) {
|
||||
var editorPane = wrap.selectAll('.entity-editor-pane');
|
||||
|
||||
var graph = context.graph(),
|
||||
entity = context.entity(entityID),
|
||||
showEditor = state === 'hover' ||
|
||||
entity = context.entity(_entityID),
|
||||
showEditor = _state === 'hover' ||
|
||||
entity.isUsed(graph) ||
|
||||
entity.isHighwayIntersection(graph);
|
||||
|
||||
@@ -66,7 +67,7 @@ export function uiInspector(context) {
|
||||
.merge(footer);
|
||||
|
||||
footer
|
||||
.call(uiViewOnOSM(context).entityID(entityID));
|
||||
.call(uiViewOnOSM(context).entityID(_entityID));
|
||||
|
||||
|
||||
function showList(preset) {
|
||||
@@ -89,23 +90,27 @@ export function uiInspector(context) {
|
||||
|
||||
|
||||
inspector.state = function(_) {
|
||||
if (!arguments.length) return state;
|
||||
state = _;
|
||||
entityEditor.state(state);
|
||||
if (!arguments.length) return _state;
|
||||
_state = _;
|
||||
entityEditor.state(_state);
|
||||
|
||||
// remove any old field help overlay that might have gotten attached to the inspector
|
||||
d3_selectAll('.field-help-body').remove();
|
||||
|
||||
return inspector;
|
||||
};
|
||||
|
||||
|
||||
inspector.entityID = function(_) {
|
||||
if (!arguments.length) return entityID;
|
||||
entityID = _;
|
||||
if (!arguments.length) return _entityID;
|
||||
_entityID = _;
|
||||
return inspector;
|
||||
};
|
||||
|
||||
|
||||
inspector.newFeature = function(_) {
|
||||
if (!arguments.length) return newFeature;
|
||||
newFeature = _;
|
||||
if (!arguments.length) return _newFeature;
|
||||
_newFeature = _;
|
||||
return inspector;
|
||||
};
|
||||
|
||||
|
||||
+36
-37
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -375,6 +375,9 @@
|
||||
"turn-yes-u": { "viewBox": "200 344 32 32" },
|
||||
"turn-no-u": { "viewBox": "232 344 32 32" },
|
||||
"turn-only-u": { "viewBox": "264 344 32 32" },
|
||||
"turn-shadow": { "viewBox": "296 344 37 11" },
|
||||
|
||||
"turn-shadow-shape": { "fill": "currentColor" },
|
||||
|
||||
"preset-icon-frame": { "viewBox": "340 320 45 45" },
|
||||
|
||||
|
||||
Binary file not shown.
@@ -280,6 +280,15 @@
|
||||
</g>
|
||||
</g>
|
||||
<g id="turns">
|
||||
<g id="turn-shadow">
|
||||
<path d="M327.5,344 C330.538,344 333,346.463 333,349.5 C333,352.538 330.538,355 327.5,355 L301.5,355 C298.462,355 296,352.538 296,349.5 C296,346.463 298.462,344 301.5,344 L327.5,344 z" fill="#000000" id="turn-shadow-shape"/>
|
||||
<path d="M301.5,349.5 L327.5,349.5" fill-opacity="0" stroke="#000000" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M301.5,349.5 L327.5,349.5" fill-opacity="0" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M303.5,349.5 C303.5,350.605 302.605,351.5 301.5,351.5 C300.395,351.5 299.5,350.605 299.5,349.5 C299.5,348.396 300.395,347.5 301.5,347.5 C302.605,347.5 303.5,348.396 303.5,349.5 z" fill="#000000"/>
|
||||
<path d="M303,349.5 C303,350.329 302.328,351 301.5,351 C300.672,351 300,350.329 300,349.5 C300,348.672 300.672,348 301.5,348 C302.328,348 303,348.672 303,349.5 z" fill="#BBBBBB"/>
|
||||
<path d="M329.5,349.5 C329.5,350.605 328.605,351.5 327.5,351.5 C326.395,351.5 325.5,350.605 325.5,349.5 C325.5,348.396 326.395,347.5 327.5,347.5 C328.605,347.5 329.5,348.396 329.5,349.5 z" fill="#000000"/>
|
||||
<path d="M329,349.5 C329,350.329 328.328,351 327.5,351 C326.672,351 326,350.329 326,349.5 C326,348.672 326.672,348 327.5,348 C328.328,348 329,348.672 329,349.5 z" fill="#BBBBBB"/>
|
||||
</g>
|
||||
<g id="turn-only-u">
|
||||
<path d="M280,344 C271.211,344 264,351.211 264,360 C264,368.789 271.211,376 280,376 C288.789,376 296,368.789 296,360 C296,351.211 288.789,344 280,344 z" fill="#000000" id="turn-only-u-shape1" opacity="0.5"/>
|
||||
<path d="M268,360 C268,353.373 273.373,348 280,348 C286.627,348 292,353.373 292,360 C292,366.627 286.627,372 280,372 C273.373,372 268,366.627 268,360 z" fill="#7092FF" id="turn-only-u-shape2"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 287 KiB After Width: | Height: | Size: 288 KiB |
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
|
||||
+529
-404
File diff suppressed because it is too large
Load Diff
+409
-257
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user