diff --git a/Makefile b/Makefile index 885df0a94..691a1a860 100644 --- a/Makefile +++ b/Makefile @@ -124,3 +124,6 @@ D3_FILES = \ js/lib/d3.v3.js: $(D3_FILES) node_modules/.bin/smash $(D3_FILES) > $@ @echo 'd3 rebuilt. Please reapply 7e2485d, 4da529f, and 223974d' + +js/lib/lodash.js: + node_modules/.bin/lodash --debug --output $@ include="any,assign,bind,clone,compact,contains,debounce,difference,each,every,extend,filter,find,first,forEach,groupBy,indexOf,intersection,isEmpty,isEqual,isFunction,keys,last,map,omit,pairs,pluck,reject,some,throttle,union,uniq,unique,values,without,flatten,value,chain,cloneDeep,merge,pick" exports="global,node" diff --git a/css/app.css b/css/app.css index e4fcec85a..096c4d1d2 100644 --- a/css/app.css +++ b/css/app.css @@ -39,6 +39,13 @@ body { height: 100%; } +#defs { + /* Can't be display: none or the clippaths are ignored. */ + position: absolute; + width: 0; + height: 0; +} + .spacer { height: 40px; margin-right: 10px; @@ -1527,6 +1534,26 @@ input[type=number] { border-bottom: 0; border-radius: 0 0 4px 0; } + +/* Restrictions editor */ + +.form-field-restrictions .preset-input-wrap { + position: relative; + height: 300px; +} + +.form-field-restrictions .restriction-help { + z-index: 1; + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 2px 6px; + background-color: rgba(255, 255, 255, .8); + color: #999; + text-align: center; +} + /* combobox dropdown */ div.combobox { @@ -2749,10 +2776,6 @@ img.wiki-image { fill: rgba(255,255,255,.5); } -.radial-menu .icon { - pointer-events: none; -} - .lasso-box { fill-opacity:0.1; stroke: #fff; diff --git a/css/feature-icons.css b/css/feature-icons.css index 6d3c638ea..9b7926f17 100644 --- a/css/feature-icons.css +++ b/css/feature-icons.css @@ -113,10 +113,6 @@ .feature-laundry{background-position:-108px -504px;} .feature-car{background-position:-162px -504px;} .feature-suitcase{background-position:-216px -504px;} -.feature-hairdresser{background-position:-0px -528px;} -.feature-chemist{background-position:-54px -528px;} -.feature-mobilephone{background-position:-108px -528px;} -.feature-scooter{background-position:-162px -528px;} .preset-icon-line.feature-highway-motorway{background-position:-20px -25px;} .preset-icon-line.feature-highway-trunk{background-position:-80px -25px;} .preset-icon-line.feature-highway-primary{background-position:-140px -25px;} @@ -172,3 +168,10 @@ .preset-icon-relation.feature-route-power{background-position:-800px -25px;} .preset-icon-relation.feature-route-pipeline{background-position:-860px -25px;} .preset-icon-relation.feature-route-master{background-position:-920px -25px;} +.preset-icon-relation.feature-restriction-no-straight-on{background-position:-980px -25px;} +.preset-icon-relation.feature-restriction-no-u-turn{background-position:-1040px -25px;} +.preset-icon-relation.feature-restriction-no-left-turn{background-position:-1100px -25px;} +.preset-icon-relation.feature-restriction-no-right-turn{background-position:-1160px -25px;} +.preset-icon-relation.feature-restriction-only-straight-ahead{background-position:-1220px -25px;} +.preset-icon-relation.feature-restriction-only-left-turn{background-position:-1280px -25px;} +.preset-icon-relation.feature-restriction-only-right-turn{background-position:-1340px -25px;} diff --git a/css/map.css b/css/map.css index fd4509065..b57f7fb1a 100644 --- a/css/map.css +++ b/css/map.css @@ -945,6 +945,19 @@ text.point { font-size: 10px; } +/* Turns */ + +g.turn rect, +g.turn circle { + fill: none; + pointer-events: all; +} + +.form-field-restrictions .vertex { + pointer-events: none; + cursor: auto !important; +} + /* Cursors */ #map { @@ -1092,6 +1105,16 @@ text.point { ) 9 9, crosshair; } +.turn rect, +.turn circle { + cursor: pointer; /* Opera */ + cursor: url(img/cursor-pointer.png) 6 1, pointer; /* FF */ + cursor: -webkit-image-set( + url(img/cursor-pointer.png) 1x, + url(img/cursor-pointer2x.png) 2x + ) 6 1, pointer; +} + .lasso #map { pointer-events: visibleStroke; } diff --git a/data/core.yaml b/data/core.yaml index 2c4edca74..5de2517ba 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -145,6 +145,15 @@ en: multiple: "Split {n} lines/area boundaries." not_eligible: Lines can't be split at their beginning or end. multiple_ways: There are too many lines here to split. + restriction: + 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 undo: tooltip: "Undo: {action}" nothing: Nothing to undo. diff --git a/data/feature-icons.json b/data/feature-icons.json index 001e6aecc..750d11baf 100644 --- a/data/feature-icons.json +++ b/data/feature-icons.json @@ -1 +1 @@ -{"circle-stroked":{"12":[42,0],"18":[24,0],"24":[0,0]},"circle":{"12":[96,0],"18":[78,0],"24":[54,0]},"square-stroked":{"12":[150,0],"18":[132,0],"24":[108,0]},"square":{"12":[204,0],"18":[186,0],"24":[162,0]},"triangle-stroked":{"12":[258,0],"18":[240,0],"24":[216,0]},"triangle":{"12":[42,24],"18":[24,24],"24":[0,24]},"star-stroked":{"12":[96,24],"18":[78,24],"24":[54,24]},"star":{"12":[150,24],"18":[132,24],"24":[108,24]},"cross":{"12":[204,24],"18":[186,24],"24":[162,24]},"marker-stroked":{"12":[258,24],"18":[240,24],"24":[216,24]},"marker":{"12":[42,48],"18":[24,48],"24":[0,48]},"religious-jewish":{"12":[96,48],"18":[78,48],"24":[54,48]},"religious-christian":{"12":[150,48],"18":[132,48],"24":[108,48]},"religious-muslim":{"12":[204,48],"18":[186,48],"24":[162,48]},"cemetery":{"12":[258,48],"18":[240,48],"24":[216,48]},"rocket":{"12":[42,72],"18":[24,72],"24":[0,72]},"airport":{"12":[96,72],"18":[78,72],"24":[54,72]},"heliport":{"12":[150,72],"18":[132,72],"24":[108,72]},"rail":{"12":[204,72],"18":[186,72],"24":[162,72]},"rail-metro":{"12":[258,72],"18":[240,72],"24":[216,72]},"rail-light":{"12":[42,96],"18":[24,96],"24":[0,96]},"bus":{"12":[96,96],"18":[78,96],"24":[54,96]},"fuel":{"12":[150,96],"18":[132,96],"24":[108,96]},"parking":{"12":[204,96],"18":[186,96],"24":[162,96]},"parking-garage":{"12":[258,96],"18":[240,96],"24":[216,96]},"airfield":{"12":[42,120],"18":[24,120],"24":[0,120]},"roadblock":{"12":[96,120],"18":[78,120],"24":[54,120]},"ferry":{"12":[150,120],"18":[132,120],"24":[108,120],"line":[2240,25]},"harbor":{"12":[204,120],"18":[186,120],"24":[162,120]},"bicycle":{"12":[258,120],"18":[240,120],"24":[216,120]},"park":{"12":[42,144],"18":[24,144],"24":[0,144]},"park2":{"12":[96,144],"18":[78,144],"24":[54,144]},"museum":{"12":[150,144],"18":[132,144],"24":[108,144]},"lodging":{"12":[204,144],"18":[186,144],"24":[162,144]},"monument":{"12":[258,144],"18":[240,144],"24":[216,144]},"zoo":{"12":[42,168],"18":[24,168],"24":[0,168]},"garden":{"12":[96,168],"18":[78,168],"24":[54,168]},"campsite":{"12":[150,168],"18":[132,168],"24":[108,168]},"theatre":{"12":[204,168],"18":[186,168],"24":[162,168]},"art-gallery":{"12":[258,168],"18":[240,168],"24":[216,168]},"pitch":{"12":[42,192],"18":[24,192],"24":[0,192]},"soccer":{"12":[96,192],"18":[78,192],"24":[54,192]},"america-football":{"12":[150,192],"18":[132,192],"24":[108,192]},"tennis":{"12":[204,192],"18":[186,192],"24":[162,192]},"basketball":{"12":[258,192],"18":[240,192],"24":[216,192]},"baseball":{"12":[42,216],"18":[24,216],"24":[0,216]},"golf":{"12":[96,216],"18":[78,216],"24":[54,216]},"swimming":{"12":[150,216],"18":[132,216],"24":[108,216]},"cricket":{"12":[204,216],"18":[186,216],"24":[162,216]},"skiing":{"12":[258,216],"18":[240,216],"24":[216,216]},"school":{"12":[42,240],"18":[24,240],"24":[0,240]},"college":{"12":[96,240],"18":[78,240],"24":[54,240]},"library":{"12":[150,240],"18":[132,240],"24":[108,240]},"post":{"12":[204,240],"18":[186,240],"24":[162,240]},"fire-station":{"12":[258,240],"18":[240,240],"24":[216,240]},"town-hall":{"12":[42,264],"18":[24,264],"24":[0,264]},"police":{"12":[96,264],"18":[78,264],"24":[54,264]},"prison":{"12":[150,264],"18":[132,264],"24":[108,264]},"embassy":{"12":[204,264],"18":[186,264],"24":[162,264]},"beer":{"12":[258,264],"18":[240,264],"24":[216,264]},"restaurant":{"12":[42,288],"18":[24,288],"24":[0,288]},"cafe":{"12":[96,288],"18":[78,288],"24":[54,288]},"shop":{"12":[150,288],"18":[132,288],"24":[108,288]},"fast-food":{"12":[204,288],"18":[186,288],"24":[162,288]},"bar":{"12":[258,288],"18":[240,288],"24":[216,288]},"bank":{"12":[42,312],"18":[24,312],"24":[0,312]},"grocery":{"12":[96,312],"18":[78,312],"24":[54,312]},"cinema":{"12":[150,312],"18":[132,312],"24":[108,312]},"pharmacy":{"12":[204,312],"18":[186,312],"24":[162,312]},"hospital":{"12":[258,312],"18":[240,312],"24":[216,312]},"danger":{"12":[42,336],"18":[24,336],"24":[0,336]},"industrial":{"12":[96,336],"18":[78,336],"24":[54,336]},"warehouse":{"12":[150,336],"18":[132,336],"24":[108,336]},"commercial":{"12":[204,336],"18":[186,336],"24":[162,336]},"building":{"12":[258,336],"18":[240,336],"24":[216,336]},"place-of-worship":{"12":[42,360],"18":[24,360],"24":[0,360]},"alcohol-shop":{"12":[96,360],"18":[78,360],"24":[54,360]},"logging":{"12":[150,360],"18":[132,360],"24":[108,360]},"oil-well":{"12":[204,360],"18":[186,360],"24":[162,360]},"slaughterhouse":{"12":[258,360],"18":[240,360],"24":[216,360]},"dam":{"12":[42,384],"18":[24,384],"24":[0,384]},"water":{"12":[96,384],"18":[78,384],"24":[54,384]},"wetland":{"12":[150,384],"18":[132,384],"24":[108,384]},"disability":{"12":[204,384],"18":[186,384],"24":[162,384]},"telephone":{"12":[258,384],"18":[240,384],"24":[216,384]},"emergency-telephone":{"12":[42,408],"18":[24,408],"24":[0,408]},"toilets":{"12":[96,408],"18":[78,408],"24":[54,408]},"waste-basket":{"12":[150,408],"18":[132,408],"24":[108,408]},"music":{"12":[204,408],"18":[186,408],"24":[162,408]},"land-use":{"12":[258,408],"18":[240,408],"24":[216,408]},"city":{"12":[42,432],"18":[24,432],"24":[0,432]},"town":{"12":[96,432],"18":[78,432],"24":[54,432]},"village":{"12":[150,432],"18":[132,432],"24":[108,432]},"farm":{"12":[204,432],"18":[186,432],"24":[162,432]},"bakery":{"12":[258,432],"18":[240,432],"24":[216,432]},"dog-park":{"12":[42,456],"18":[24,456],"24":[0,456]},"lighthouse":{"12":[96,456],"18":[78,456],"24":[54,456]},"clothing-store":{"12":[150,456],"18":[132,456],"24":[108,456]},"polling-place":{"12":[204,456],"18":[186,456],"24":[162,456]},"playground":{"12":[258,456],"18":[240,456],"24":[216,456]},"entrance":{"12":[42,480],"18":[24,480],"24":[0,480]},"heart":{"12":[96,480],"18":[78,480],"24":[54,480]},"london-underground":{"12":[150,480],"18":[132,480],"24":[108,480]},"minefield":{"12":[204,480],"18":[186,480],"24":[162,480]},"rail-underground":{"12":[258,480],"18":[240,480],"24":[216,480]},"rail-above":{"12":[42,504],"18":[24,504],"24":[0,504]},"camera":{"12":[96,504],"18":[78,504],"24":[54,504]},"laundry":{"12":[150,504],"18":[132,504],"24":[108,504]},"car":{"12":[204,504],"18":[186,504],"24":[162,504]},"suitcase":{"12":[258,504],"18":[240,504],"24":[216,504]},"hairdresser":{"12":[42,528],"18":[24,528],"24":[0,528]},"chemist":{"12":[96,528],"18":[78,528],"24":[54,528]},"mobilephone":{"12":[150,528],"18":[132,528],"24":[108,528]},"scooter":{"12":[204,528],"18":[186,528],"24":[162,528]},"highway-motorway":{"line":[20,25]},"highway-trunk":{"line":[80,25]},"highway-primary":{"line":[140,25]},"highway-secondary":{"line":[200,25]},"highway-tertiary":{"line":[260,25]},"highway-motorway-link":{"line":[320,25]},"highway-trunk-link":{"line":[380,25]},"highway-primary-link":{"line":[440,25]},"highway-secondary-link":{"line":[500,25]},"highway-tertiary-link":{"line":[560,25]},"highway-residential":{"line":[620,25]},"highway-unclassified":{"line":[680,25]},"highway-service":{"line":[740,25]},"highway-road":{"line":[800,25]},"highway-track":{"line":[860,25]},"highway-living-street":{"line":[920,25]},"highway-path":{"line":[980,25]},"highway-cycleway":{"line":[1040,25]},"highway-footway":{"line":[1100,25]},"highway-bridleway":{"line":[1160,25]},"highway-steps":{"line":[1220,25]},"railway-rail":{"line":[1280,25]},"railway-disused":{"line":[1340,25]},"railway-abandoned":{"line":[1400,25]},"railway-subway":{"line":[1460,25]},"railway-light-rail":{"line":[1520,25]},"railway-monorail":{"line":[1580,25]},"waterway-river":{"line":[1640,25]},"waterway-stream":{"line":[1700,25]},"waterway-canal":{"line":[1760,25]},"waterway-ditch":{"line":[1820,25]},"power-line":{"line":[1880,25]},"other-line":{"line":[1940,25]},"category-roads":{"line":[2000,25]},"category-rail":{"line":[2060,25]},"category-path":{"line":[2120,25]},"category-water":{"line":[2180,25]},"pipeline":{"line":[2300,25]},"relation":{"relation":[20,25]},"restriction":{"relation":[80,25]},"multipolygon":{"relation":[140,25]},"boundary":{"relation":[200,25]},"route":{"relation":[260,25]},"route-road":{"relation":[320,25]},"route-bicycle":{"relation":[380,25]},"route-foot":{"relation":[440,25]},"route-bus":{"relation":[500,25]},"route-train":{"relation":[560,25]},"route-detour":{"relation":[620,25]},"route-tram":{"relation":[680,25]},"route-ferry":{"relation":[740,25]},"route-power":{"relation":[800,25]},"route-pipeline":{"relation":[860,25]},"route-master":{"relation":[920,25]}} \ No newline at end of file +{"circle-stroked":{"12":[42,0],"18":[24,0],"24":[0,0]},"circle":{"12":[96,0],"18":[78,0],"24":[54,0]},"square-stroked":{"12":[150,0],"18":[132,0],"24":[108,0]},"square":{"12":[204,0],"18":[186,0],"24":[162,0]},"triangle-stroked":{"12":[258,0],"18":[240,0],"24":[216,0]},"triangle":{"12":[42,24],"18":[24,24],"24":[0,24]},"star-stroked":{"12":[96,24],"18":[78,24],"24":[54,24]},"star":{"12":[150,24],"18":[132,24],"24":[108,24]},"cross":{"12":[204,24],"18":[186,24],"24":[162,24]},"marker-stroked":{"12":[258,24],"18":[240,24],"24":[216,24]},"marker":{"12":[42,48],"18":[24,48],"24":[0,48]},"religious-jewish":{"12":[96,48],"18":[78,48],"24":[54,48]},"religious-christian":{"12":[150,48],"18":[132,48],"24":[108,48]},"religious-muslim":{"12":[204,48],"18":[186,48],"24":[162,48]},"cemetery":{"12":[258,48],"18":[240,48],"24":[216,48]},"rocket":{"12":[42,72],"18":[24,72],"24":[0,72]},"airport":{"12":[96,72],"18":[78,72],"24":[54,72]},"heliport":{"12":[150,72],"18":[132,72],"24":[108,72]},"rail":{"12":[204,72],"18":[186,72],"24":[162,72]},"rail-metro":{"12":[258,72],"18":[240,72],"24":[216,72]},"rail-light":{"12":[42,96],"18":[24,96],"24":[0,96]},"bus":{"12":[96,96],"18":[78,96],"24":[54,96]},"fuel":{"12":[150,96],"18":[132,96],"24":[108,96]},"parking":{"12":[204,96],"18":[186,96],"24":[162,96]},"parking-garage":{"12":[258,96],"18":[240,96],"24":[216,96]},"airfield":{"12":[42,120],"18":[24,120],"24":[0,120]},"roadblock":{"12":[96,120],"18":[78,120],"24":[54,120]},"ferry":{"12":[150,120],"18":[132,120],"24":[108,120],"line":[2240,25]},"harbor":{"12":[204,120],"18":[186,120],"24":[162,120]},"bicycle":{"12":[258,120],"18":[240,120],"24":[216,120]},"park":{"12":[42,144],"18":[24,144],"24":[0,144]},"park2":{"12":[96,144],"18":[78,144],"24":[54,144]},"museum":{"12":[150,144],"18":[132,144],"24":[108,144]},"lodging":{"12":[204,144],"18":[186,144],"24":[162,144]},"monument":{"12":[258,144],"18":[240,144],"24":[216,144]},"zoo":{"12":[42,168],"18":[24,168],"24":[0,168]},"garden":{"12":[96,168],"18":[78,168],"24":[54,168]},"campsite":{"12":[150,168],"18":[132,168],"24":[108,168]},"theatre":{"12":[204,168],"18":[186,168],"24":[162,168]},"art-gallery":{"12":[258,168],"18":[240,168],"24":[216,168]},"pitch":{"12":[42,192],"18":[24,192],"24":[0,192]},"soccer":{"12":[96,192],"18":[78,192],"24":[54,192]},"america-football":{"12":[150,192],"18":[132,192],"24":[108,192]},"tennis":{"12":[204,192],"18":[186,192],"24":[162,192]},"basketball":{"12":[258,192],"18":[240,192],"24":[216,192]},"baseball":{"12":[42,216],"18":[24,216],"24":[0,216]},"golf":{"12":[96,216],"18":[78,216],"24":[54,216]},"swimming":{"12":[150,216],"18":[132,216],"24":[108,216]},"cricket":{"12":[204,216],"18":[186,216],"24":[162,216]},"skiing":{"12":[258,216],"18":[240,216],"24":[216,216]},"school":{"12":[42,240],"18":[24,240],"24":[0,240]},"college":{"12":[96,240],"18":[78,240],"24":[54,240]},"library":{"12":[150,240],"18":[132,240],"24":[108,240]},"post":{"12":[204,240],"18":[186,240],"24":[162,240]},"fire-station":{"12":[258,240],"18":[240,240],"24":[216,240]},"town-hall":{"12":[42,264],"18":[24,264],"24":[0,264]},"police":{"12":[96,264],"18":[78,264],"24":[54,264]},"prison":{"12":[150,264],"18":[132,264],"24":[108,264]},"embassy":{"12":[204,264],"18":[186,264],"24":[162,264]},"beer":{"12":[258,264],"18":[240,264],"24":[216,264]},"restaurant":{"12":[42,288],"18":[24,288],"24":[0,288]},"cafe":{"12":[96,288],"18":[78,288],"24":[54,288]},"shop":{"12":[150,288],"18":[132,288],"24":[108,288]},"fast-food":{"12":[204,288],"18":[186,288],"24":[162,288]},"bar":{"12":[258,288],"18":[240,288],"24":[216,288]},"bank":{"12":[42,312],"18":[24,312],"24":[0,312]},"grocery":{"12":[96,312],"18":[78,312],"24":[54,312]},"cinema":{"12":[150,312],"18":[132,312],"24":[108,312]},"pharmacy":{"12":[204,312],"18":[186,312],"24":[162,312]},"hospital":{"12":[258,312],"18":[240,312],"24":[216,312]},"danger":{"12":[42,336],"18":[24,336],"24":[0,336]},"industrial":{"12":[96,336],"18":[78,336],"24":[54,336]},"warehouse":{"12":[150,336],"18":[132,336],"24":[108,336]},"commercial":{"12":[204,336],"18":[186,336],"24":[162,336]},"building":{"12":[258,336],"18":[240,336],"24":[216,336]},"place-of-worship":{"12":[42,360],"18":[24,360],"24":[0,360]},"alcohol-shop":{"12":[96,360],"18":[78,360],"24":[54,360]},"logging":{"12":[150,360],"18":[132,360],"24":[108,360]},"oil-well":{"12":[204,360],"18":[186,360],"24":[162,360]},"slaughterhouse":{"12":[258,360],"18":[240,360],"24":[216,360]},"dam":{"12":[42,384],"18":[24,384],"24":[0,384]},"water":{"12":[96,384],"18":[78,384],"24":[54,384]},"wetland":{"12":[150,384],"18":[132,384],"24":[108,384]},"disability":{"12":[204,384],"18":[186,384],"24":[162,384]},"telephone":{"12":[258,384],"18":[240,384],"24":[216,384]},"emergency-telephone":{"12":[42,408],"18":[24,408],"24":[0,408]},"toilets":{"12":[96,408],"18":[78,408],"24":[54,408]},"waste-basket":{"12":[150,408],"18":[132,408],"24":[108,408]},"music":{"12":[204,408],"18":[186,408],"24":[162,408]},"land-use":{"12":[258,408],"18":[240,408],"24":[216,408]},"city":{"12":[42,432],"18":[24,432],"24":[0,432]},"town":{"12":[96,432],"18":[78,432],"24":[54,432]},"village":{"12":[150,432],"18":[132,432],"24":[108,432]},"farm":{"12":[204,432],"18":[186,432],"24":[162,432]},"bakery":{"12":[258,432],"18":[240,432],"24":[216,432]},"dog-park":{"12":[42,456],"18":[24,456],"24":[0,456]},"lighthouse":{"12":[96,456],"18":[78,456],"24":[54,456]},"clothing-store":{"12":[150,456],"18":[132,456],"24":[108,456]},"polling-place":{"12":[204,456],"18":[186,456],"24":[162,456]},"playground":{"12":[258,456],"18":[240,456],"24":[216,456]},"entrance":{"12":[42,480],"18":[24,480],"24":[0,480]},"heart":{"12":[96,480],"18":[78,480],"24":[54,480]},"london-underground":{"12":[150,480],"18":[132,480],"24":[108,480]},"minefield":{"12":[204,480],"18":[186,480],"24":[162,480]},"rail-underground":{"12":[258,480],"18":[240,480],"24":[216,480]},"rail-above":{"12":[42,504],"18":[24,504],"24":[0,504]},"camera":{"12":[96,504],"18":[78,504],"24":[54,504]},"laundry":{"12":[150,504],"18":[132,504],"24":[108,504]},"car":{"12":[204,504],"18":[186,504],"24":[162,504]},"suitcase":{"12":[258,504],"18":[240,504],"24":[216,504]},"highway-motorway":{"line":[20,25]},"highway-trunk":{"line":[80,25]},"highway-primary":{"line":[140,25]},"highway-secondary":{"line":[200,25]},"highway-tertiary":{"line":[260,25]},"highway-motorway-link":{"line":[320,25]},"highway-trunk-link":{"line":[380,25]},"highway-primary-link":{"line":[440,25]},"highway-secondary-link":{"line":[500,25]},"highway-tertiary-link":{"line":[560,25]},"highway-residential":{"line":[620,25]},"highway-unclassified":{"line":[680,25]},"highway-service":{"line":[740,25]},"highway-road":{"line":[800,25]},"highway-track":{"line":[860,25]},"highway-living-street":{"line":[920,25]},"highway-path":{"line":[980,25]},"highway-cycleway":{"line":[1040,25]},"highway-footway":{"line":[1100,25]},"highway-bridleway":{"line":[1160,25]},"highway-steps":{"line":[1220,25]},"railway-rail":{"line":[1280,25]},"railway-disused":{"line":[1340,25]},"railway-abandoned":{"line":[1400,25]},"railway-subway":{"line":[1460,25]},"railway-light-rail":{"line":[1520,25]},"railway-monorail":{"line":[1580,25]},"waterway-river":{"line":[1640,25]},"waterway-stream":{"line":[1700,25]},"waterway-canal":{"line":[1760,25]},"waterway-ditch":{"line":[1820,25]},"power-line":{"line":[1880,25]},"other-line":{"line":[1940,25]},"category-roads":{"line":[2000,25]},"category-rail":{"line":[2060,25]},"category-path":{"line":[2120,25]},"category-water":{"line":[2180,25]},"pipeline":{"line":[2300,25]},"relation":{"relation":[20,25]},"restriction":{"relation":[80,25]},"multipolygon":{"relation":[140,25]},"boundary":{"relation":[200,25]},"route":{"relation":[260,25]},"route-road":{"relation":[320,25]},"route-bicycle":{"relation":[380,25]},"route-foot":{"relation":[440,25]},"route-bus":{"relation":[500,25]},"route-train":{"relation":[560,25]},"route-detour":{"relation":[620,25]},"route-tram":{"relation":[680,25]},"route-ferry":{"relation":[740,25]},"route-power":{"relation":[800,25]},"route-pipeline":{"relation":[860,25]},"route-master":{"relation":[920,25]},"restriction-no-straight-on":{"relation":[980,25]},"restriction-no-u-turn":{"relation":[1040,25]},"restriction-no-left-turn":{"relation":[1100,25]},"restriction-no-right-turn":{"relation":[1160,25]},"restriction-only-straight-ahead":{"relation":[1220,25]},"restriction-only-left-turn":{"relation":[1280,25]},"restriction-only-right-turn":{"relation":[1340,25]}} \ No newline at end of file diff --git a/data/operations-sprite.json b/data/operations-sprite.json index 23c5a816d..f9ae5a12c 100644 --- a/data/operations-sprite.json +++ b/data/operations-sprite.json @@ -23,5 +23,12 @@ "icon-operation-disabled-orthogonalize": [160, 160], "icon-operation-disabled-rotate": [180, 160], "icon-operation-disabled-simplify": [200, 160], - "icon-operation-disabled-continue": [220, 160] + "icon-operation-disabled-continue": [220, 160], + + "icon-restriction-yes": [50, 80], + "icon-restriction-no": [95, 80], + "icon-restriction-only": [140, 80], + "icon-restriction-yes-u": [185, 80], + "icon-restriction-no-u": [230, 80], + "icon-restriction-only-u": [275, 80] } \ No newline at end of file diff --git a/data/presets.yaml b/data/presets.yaml index 5bb6ecda0..52d1f948b 100644 --- a/data/presets.yaml +++ b/data/presets.yaml @@ -11,6 +11,8 @@ en: name: Path "category-rail": name: Rail + "category-restriction": + name: Restriction "category-road": name: Road "category-route": @@ -138,6 +140,8 @@ en: label: Emergency entrance: label: Type + except: + label: Exceptions fax: label: Fax placeholder: +31 42 123 4567 @@ -294,6 +298,8 @@ en: taoist: Taoist restriction: label: Type + restrictions: + label: Turn Restrictions route: label: Type route_master: @@ -1795,6 +1801,27 @@ en: type/restriction: name: Restriction terms: "" + type/restriction/no_left_turn: + name: No Left Turn + terms: "" + type/restriction/no_right_turn: + name: No Right Turn + terms: "" + type/restriction/no_straight_on: + name: No Straight On + terms: "" + type/restriction/no_u_turn: + name: "No U-turn" + terms: "" + type/restriction/only_left_turn: + name: Left Turn Only + terms: "" + type/restriction/only_right_turn: + name: Right Turn Only + terms: "" + type/restriction/only_straight_ahead: + name: No Turns + terms: "" type/route: name: Route terms: "" diff --git a/data/presets/categories.json b/data/presets/categories.json index 20e9ea3d7..ff320f113 100644 --- a/data/presets/categories.json +++ b/data/presets/categories.json @@ -68,6 +68,21 @@ "railway/abandoned" ] }, + "category-restriction": { + "geometry": "relation", + "name": "Restriction", + "icon": "restriction", + "members": [ + "type/restriction/no_left_turn", + "type/restriction/no_right_turn", + "type/restriction/no_straight_on", + "type/restriction/no_u_turn", + "type/restriction/only_left_turn", + "type/restriction/only_right_turn", + "type/restriction/only_straight_ahead", + "type/restriction" + ] + }, "category-road": { "geometry": "line", "name": "Road", diff --git a/data/presets/categories/restriction.json b/data/presets/categories/restriction.json new file mode 100644 index 000000000..9403d50e1 --- /dev/null +++ b/data/presets/categories/restriction.json @@ -0,0 +1,15 @@ +{ + "geometry": "relation", + "name": "Restriction", + "icon": "restriction", + "members": [ + "type/restriction/no_left_turn", + "type/restriction/no_right_turn", + "type/restriction/no_straight_on", + "type/restriction/no_u_turn", + "type/restriction/only_left_turn", + "type/restriction/only_right_turn", + "type/restriction/only_straight_ahead", + "type/restriction" + ] +} diff --git a/data/presets/defaults.json b/data/presets/defaults.json index 2fdd42049..00a385aca 100644 --- a/data/presets/defaults.json +++ b/data/presets/defaults.json @@ -40,8 +40,8 @@ ], "relation": [ "category-route", + "category-restriction", "type/boundary", - "type/restriction", "type/multipolygon", "relation" ] diff --git a/data/presets/fields.json b/data/presets/fields.json index bbdbc6ac6..4b80873f1 100644 --- a/data/presets/fields.json +++ b/data/presets/fields.json @@ -320,6 +320,11 @@ "type": "typeCombo", "label": "Type" }, + "except": { + "key": "except", + "type": "combo", + "label": "Exceptions" + }, "fax": { "key": "fax", "type": "tel", @@ -716,6 +721,15 @@ "type": "combo", "label": "Type" }, + "restrictions": { + "type": "restrictions", + "geometry": "vertex", + "icon": "restrictions", + "reference": { + "rtype": "restriction" + }, + "label": "Turn Restrictions" + }, "route": { "key": "route", "type": "combo", diff --git a/data/presets/fields/except.json b/data/presets/fields/except.json new file mode 100644 index 000000000..38458d088 --- /dev/null +++ b/data/presets/fields/except.json @@ -0,0 +1,5 @@ +{ + "key": "except", + "type": "combo", + "label": "Exceptions" +} \ No newline at end of file diff --git a/data/presets/fields/restrictions.json b/data/presets/fields/restrictions.json new file mode 100644 index 000000000..4873601f9 --- /dev/null +++ b/data/presets/fields/restrictions.json @@ -0,0 +1,9 @@ +{ + "type": "restrictions", + "geometry": "vertex", + "icon": "restrictions", + "reference": { + "rtype": "restriction" + }, + "label": "Turn Restrictions" +} diff --git a/data/presets/presets.json b/data/presets/presets.json index e6665a9d5..83df746ee 100644 --- a/data/presets/presets.json +++ b/data/presets/presets.json @@ -7987,9 +7987,108 @@ "name": "Restriction", "icon": "restriction", "fields": [ - "restriction" + "restriction", + "except" ] }, + "type/restriction/no_left_turn": { + "name": "No Left Turn", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_left_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-left-turn" + }, + "type/restriction/no_right_turn": { + "name": "No Right Turn", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_right_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-right-turn" + }, + "type/restriction/no_straight_on": { + "name": "No Straight On", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_straight_on" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-straight-on" + }, + "type/restriction/no_u_turn": { + "name": "No U-turn", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_u_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-u-turn" + }, + "type/restriction/only_left_turn": { + "name": "Left Turn Only", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "only_left_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-only-left-turn" + }, + "type/restriction/only_right_turn": { + "name": "Right Turn Only", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "only_right_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-only-right-turn" + }, + "type/restriction/only_straight_ahead": { + "name": "No Turns", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "only_straight_ahead" + }, + "fields": [ + "except" + ], + "icon": "restriction-only-straight-ahead" + }, "type/route": { "geometry": [ "relation" diff --git a/data/presets/presets/type/restriction.json b/data/presets/presets/type/restriction.json index 416a0add1..3ebf5e58a 100644 --- a/data/presets/presets/type/restriction.json +++ b/data/presets/presets/type/restriction.json @@ -8,6 +8,7 @@ "name": "Restriction", "icon": "restriction", "fields": [ - "restriction" + "restriction", + "except" ] } \ No newline at end of file diff --git a/data/presets/presets/type/restriction/no_left_turn.json b/data/presets/presets/type/restriction/no_left_turn.json new file mode 100644 index 000000000..68a035964 --- /dev/null +++ b/data/presets/presets/type/restriction/no_left_turn.json @@ -0,0 +1,14 @@ +{ + "name": "No Left Turn", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_left_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-left-turn" +} diff --git a/data/presets/presets/type/restriction/no_right_turn.json b/data/presets/presets/type/restriction/no_right_turn.json new file mode 100644 index 000000000..1fe2b3eb7 --- /dev/null +++ b/data/presets/presets/type/restriction/no_right_turn.json @@ -0,0 +1,14 @@ +{ + "name": "No Right Turn", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_right_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-right-turn" +} diff --git a/data/presets/presets/type/restriction/no_straight_on.json b/data/presets/presets/type/restriction/no_straight_on.json new file mode 100644 index 000000000..b6e20e952 --- /dev/null +++ b/data/presets/presets/type/restriction/no_straight_on.json @@ -0,0 +1,14 @@ +{ + "name": "No Straight On", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_straight_on" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-straight-on" +} diff --git a/data/presets/presets/type/restriction/no_u_turn.json b/data/presets/presets/type/restriction/no_u_turn.json new file mode 100644 index 000000000..ce526a5f6 --- /dev/null +++ b/data/presets/presets/type/restriction/no_u_turn.json @@ -0,0 +1,14 @@ +{ + "name": "No U-turn", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "no_u_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-no-u-turn" +} diff --git a/data/presets/presets/type/restriction/only_left_turn.json b/data/presets/presets/type/restriction/only_left_turn.json new file mode 100644 index 000000000..516f178f1 --- /dev/null +++ b/data/presets/presets/type/restriction/only_left_turn.json @@ -0,0 +1,14 @@ +{ + "name": "Left Turn Only", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "only_left_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-only-left-turn" +} diff --git a/data/presets/presets/type/restriction/only_right_turn.json b/data/presets/presets/type/restriction/only_right_turn.json new file mode 100644 index 000000000..428d274e7 --- /dev/null +++ b/data/presets/presets/type/restriction/only_right_turn.json @@ -0,0 +1,14 @@ +{ + "name": "Right Turn Only", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "only_right_turn" + }, + "fields": [ + "except" + ], + "icon": "restriction-only-right-turn" +} diff --git a/data/presets/presets/type/restriction/only_straight_ahead.json b/data/presets/presets/type/restriction/only_straight_ahead.json new file mode 100644 index 000000000..dcad31ab0 --- /dev/null +++ b/data/presets/presets/type/restriction/only_straight_ahead.json @@ -0,0 +1,14 @@ +{ + "name": "No Turns", + "geometry": [ + "relation" + ], + "tags": { + "type": "restriction", + "restriction": "only_straight_ahead" + }, + "fields": [ + "except" + ], + "icon": "restriction-only-straight-ahead" +} diff --git a/data/presets/schema/field.json b/data/presets/schema/field.json index 12a45ee5f..3cc081660 100644 --- a/data/presets/schema/field.json +++ b/data/presets/schema/field.json @@ -63,7 +63,8 @@ "textarea", "localized", "wikipedia", - "typeCombo" + "typeCombo", + "restrictions" ], "required": true }, diff --git a/data/relation-icons.json b/data/relation-icons.json index 79d4f115f..2715cc804 100644 --- a/data/relation-icons.json +++ b/data/relation-icons.json @@ -14,5 +14,12 @@ "route-ferry": [740, 25], "route-power": [800, 25], "route-pipeline": [860, 25], - "route-master": [920, 25] + "route-master": [920, 25], + "restriction-no-straight-on": [980, 25], + "restriction-no-u-turn": [1040, 25], + "restriction-no-left-turn": [1100, 25], + "restriction-no-right-turn": [1160, 25], + "restriction-only-straight-ahead": [1220, 25], + "restriction-only-left-turn": [1280, 25], + "restriction-only-right-turn": [1340, 25] } diff --git a/dist/img/maki-sprite.png b/dist/img/maki-sprite.png index e4028833d..af7b5322c 100644 Binary files a/dist/img/maki-sprite.png and b/dist/img/maki-sprite.png differ diff --git a/dist/img/relation-presets.png b/dist/img/relation-presets.png index 00c922b75..47f1d6e30 100644 Binary files a/dist/img/relation-presets.png and b/dist/img/relation-presets.png differ diff --git a/dist/img/sprite.svg b/dist/img/sprite.svg index f93948414..e6c15a29b 100644 --- a/dist/img/sprite.svg +++ b/dist/img/sprite.svg @@ -27,20 +27,20 @@ inkscape:window-width="1440" inkscape:window-height="856" id="namedview392" - showgrid="false" - inkscape:zoom="11.313708" - inkscape:cx="19.712517" - inkscape:cy="454.54715" - inkscape:window-x="298" - inkscape:window-y="6" + showgrid="true" + inkscape:zoom="2.8284271" + inkscape:cx="186.53252" + inkscape:cy="447.46176" + inkscape:window-x="276" + inkscape:window-y="71" inkscape:window-maximized="0" inkscape:current-layer="svg12393" - showguides="false" + showguides="true" inkscape:guide-bbox="true" inkscape:snap-global="true" inkscape:snap-bbox="true" inkscape:snap-bbox-midpoints="true" - inkscape:snap-nodes="false"> + inkscape:snap-nodes="true"> + + + + @@ -1881,4 +1897,140 @@ transform="matrix(0,1,-1,0,0,0)" rx="0.5" ry="0.5" /> + + + + + + + + + + + + + + + + + + + + + + + + + + ONLY + + + + + diff --git a/dist/locales/en.json b/dist/locales/en.json index 04b1b3571..da6c96980 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -182,6 +182,18 @@ }, "not_eligible": "Lines can't be split at their beginning or end.", "multiple_ways": "There are too many lines here to split." + }, + "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" + } } }, "undo": { @@ -416,6 +428,9 @@ "category-rail": { "name": "Rail" }, + "category-restriction": { + "name": "Restriction" + }, "category-road": { "name": "Road" }, @@ -598,6 +613,9 @@ "entrance": { "label": "Type" }, + "except": { + "label": "Exceptions" + }, "fax": { "label": "Fax", "placeholder": "+31 42 123 4567" @@ -822,6 +840,9 @@ "restriction": { "label": "Type" }, + "restrictions": { + "label": "Turn Restrictions" + }, "route": { "label": "Type" }, @@ -2834,6 +2855,34 @@ "name": "Restriction", "terms": "" }, + "type/restriction/no_left_turn": { + "name": "No Left Turn", + "terms": "" + }, + "type/restriction/no_right_turn": { + "name": "No Right Turn", + "terms": "" + }, + "type/restriction/no_straight_on": { + "name": "No Straight On", + "terms": "" + }, + "type/restriction/no_u_turn": { + "name": "No U-turn", + "terms": "" + }, + "type/restriction/only_left_turn": { + "name": "Left Turn Only", + "terms": "" + }, + "type/restriction/only_right_turn": { + "name": "Right Turn Only", + "terms": "" + }, + "type/restriction/only_straight_ahead": { + "name": "No Turns", + "terms": "" + }, "type/route": { "name": "Route", "terms": "" diff --git a/index.html b/index.html index fec78cf5c..48cd10b82 100644 --- a/index.html +++ b/index.html @@ -47,8 +47,9 @@ + - + @@ -58,14 +59,15 @@ + + + - - @@ -118,6 +120,7 @@ + @@ -135,6 +138,7 @@ + @@ -148,13 +152,14 @@ - - - - + + + + + diff --git a/js/id/actions/restrict_turn.js b/js/id/actions/restrict_turn.js new file mode 100644 index 000000000..b5837eb57 --- /dev/null +++ b/js/id/actions/restrict_turn.js @@ -0,0 +1,89 @@ +// Create a restriction relation for `turn`, which must have the following structure: +// +// { +// from: { node: , way: }, +// via: { node: }, +// to: { node: , way: }, +// restriction: <'no_right_turn', 'no_left_turn', etc.> +// } +// +// This specifies a restriction of type `restriction` when traveling from +// `from.node` in `from.way` toward `to.node` in `to.way` via `via.node`. +// (The action does not check that these entities form a valid intersection.) +// +// If `restriction` is not provided, it is automatically determined by the +// angle of the turn: +// +// 0-23 degrees: no_u_turn +// 23-158 degrees: no_right_turn +// 158-202 degrees: no_straight_on +// 202-326 degrees: no_left_turn +// 336-360 degrees: no_u_turn +// +// If necessary, the `from` and `to` ways are split. In these cases, `from.node` +// and `to.node` are used to determine which portion of the split ways become +// members of the restriction. +// +// For testing convenience, accepts an ID to assign to the new relation. +// Normally, this will be undefined and the relation will automatically +// be assigned a new ID. +// +iD.actions.RestrictTurn = function(turn, projection, restrictionId) { + return function(graph) { + var from = graph.entity(turn.from.way), + via = graph.entity(turn.via.node), + to = graph.entity(turn.to.way); + + function split(toOrFrom) { + var newID = toOrFrom.newID || iD.Way().id; + graph = iD.actions.Split(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)) { + 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 + var s = split(turn.from); + from = s[0]; + to = s[1]; + } else { + // Other + from = split(turn.from)[0]; + } + } + + if (!to.affix(via.id)) { + to = split(turn.to)[0]; + } + + return graph.replace(iD.Relation({ + id: restrictionId, + tags: { + type: 'restriction', + restriction: turn.restriction || + iD.geo.inferRestriction( + graph.entity(turn.from.node), + via, + graph.entity(turn.to.node), + projection) + }, + members: [ + {id: from.id, type: 'way', role: 'from'}, + {id: via.id, type: 'node', role: 'via'}, + {id: to.id, type: 'way', role: 'to'} + ] + })); + }; +}; diff --git a/js/id/actions/unrestrict_turn.js b/js/id/actions/unrestrict_turn.js new file mode 100644 index 000000000..f57186d7a --- /dev/null +++ b/js/id/actions/unrestrict_turn.js @@ -0,0 +1,23 @@ +// Remove the effects of `turn.restriction` on `turn`, which must have the +// following structure: +// +// { +// from: { node: , way: }, +// via: { node: }, +// to: { node: , way: }, +// restriction: +// } +// +// In the simple case, `restriction` is a reference to a `no_*` restriction +// on the turn itself. In this case, it is simply deleted. +// +// The more complex case is where `restriction` references an `only_*` +// restriction on a different turn in the same intersection. In that case, +// that restriction is also deleted, but at the same time restrictions on +// the turns other than the first two are created. +// +iD.actions.UnrestrictTurn = function(turn) { + return function(graph) { + return iD.actions.DeleteRelation(turn.restriction)(graph); + }; +}; diff --git a/js/id/geo.js b/js/id/geo.js index 2fa2e2ace..8978828b0 100644 --- a/js/id/geo.js +++ b/js/id/geo.js @@ -33,6 +33,14 @@ iD.geo.edgeEqual = function(a, b) { (a[0] === b[1] && a[1] === b[0]); }; +// Return the counterclockwise angle in the range (-pi, pi) +// between the positive X axis and the line intersecting a and b. +iD.geo.angle = function(a, b, projection) { + a = projection(a.loc); + b = projection(b.loc); + return Math.atan2(b[1] - a[1], b[0] - a[0]); +}; + // Choose the edge with the minimal distance from `point` to its orthogonal // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the diff --git a/js/id/geo/intersection.js b/js/id/geo/intersection.js new file mode 100644 index 000000000..1697cc7d4 --- /dev/null +++ b/js/id/geo/intersection.js @@ -0,0 +1,135 @@ +iD.geo.Turn = function(turn) { + if (!(this instanceof iD.geo.Turn)) + return new iD.geo.Turn(turn); + _.extend(this, turn); +}; + +iD.geo.Intersection = function(graph, vertexId) { + var vertex = graph.entity(vertexId), + highways = []; + + // 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. + graph.parentWays(vertex).forEach(function(way) { + if (!way.tags.highway || way.isArea() || way.isDegenerate()) + return; + + if (way.affix(vertexId)) { + highways.push(way); + } else { + var idx = _.indexOf(way.nodes, vertex.id, 1), + wayA = iD.Way({id: way.id + '-a', tags: way.tags, nodes: way.nodes.slice(0, idx + 1)}), + wayB = iD.Way({id: way.id + '-b', tags: way.tags, nodes: way.nodes.slice(idx)}); + + graph = graph.replace(wayA); + graph = graph.replace(wayB); + + highways.push(wayA); + highways.push(wayB); + } + }); + + var intersection = { + highways: highways, + graph: graph + }; + + intersection.turns = function(fromNodeID) { + if (!fromNodeID) + return []; + + var way = _.find(highways, function(way) { return way.contains(fromNodeID); }); + if (way.first() === vertex.id && way.tags.oneway === 'yes') + return []; + if (way.last() === vertex.id && way.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; + } + }); + + return iD.geo.Turn(turn); + } + + var from = { + node: way.nodes[way.first() === vertex.id ? 1 : way.nodes.length - 2], + way: way.id.split(/-(a|b)/)[0] + }, + via = {node: vertex.id}, + turns = []; + + highways.forEach(function(parent) { + if (parent === way) + return; + + var index = parent.nodes.indexOf(vertex.id); + + // backward + if (parent.first() !== vertex.id && parent.tags.oneway !== 'yes') { + turns.push(withRestriction({ + from: from, + via: via, + to: {node: parent.nodes[index - 1], way: parent.id.split(/-(a|b)/)[0]} + })); + } + + // forward + if (parent.last() !== vertex.id && parent.tags.oneway !== '-1') { + turns.push(withRestriction({ + from: from, + via: via, + to: {node: parent.nodes[index + 1], way: parent.id.split(/-(a|b)/)[0]} + })); + } + }); + + // U-turn + if (way.tags.oneway !== 'yes' && way.tags.oneway !== '-1') { + turns.push(withRestriction({ + from: from, + via: via, + to: from, + u: true + })); + } + + return turns; + }; + + return intersection; +}; + +iD.geo.inferRestriction = function(from, via, to, projection) { + var angle = iD.geo.angle(via, from, projection) - + iD.geo.angle(via, to, projection); + + angle = angle * 180 / Math.PI; + + if (angle > 158 || angle < -158) + return 'no_straight_on'; + if (angle > 23) + return 'no_right_turn'; + if (angle < -22) + return 'no_left_turn'; + + return 'no_u_turn'; +}; diff --git a/js/id/geo/raw_mercator.js b/js/id/geo/raw_mercator.js new file mode 100644 index 000000000..f34cb5d96 --- /dev/null +++ b/js/id/geo/raw_mercator.js @@ -0,0 +1,50 @@ +/* + Bypasses features of D3's default projection stream pipeline that are unnecessary: + * Antimeridian clipping + * Spherical rotation + * Resampling +*/ +iD.geo.RawMercator = function () { + var project = d3.geo.mercator.raw, + k = 512 / Math.PI, // scale + x = 0, y = 0, // translate + clipExtent = [[0, 0], [0, 0]]; + + function projection(point) { + point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180); + return [point[0] * k + x, y - point[1] * k]; + } + + projection.invert = function(point) { + point = project.invert((point[0] - x) / k, (y - point[1]) / k); + return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI]; + }; + + projection.scale = function(_) { + if (!arguments.length) return k; + k = +_; + return projection; + }; + + projection.translate = function(_) { + if (!arguments.length) return [x, y]; + x = +_[0]; + y = +_[1]; + return projection; + }; + + projection.clipExtent = function(_) { + if (!arguments.length) return clipExtent; + clipExtent = _; + return projection; + }; + + projection.stream = d3.geo.transform({ + point: function(x, y) { + x = projection([x, y]); + this.stream.point(x[0], x[1]); + } + }).stream; + + return projection; +}; diff --git a/js/id/geo/turn.js b/js/id/geo/turn.js deleted file mode 100644 index 296a43e31..000000000 --- a/js/id/geo/turn.js +++ /dev/null @@ -1,62 +0,0 @@ -iD.geo.turns = function(graph, entityID) { - var way = graph.entity(entityID); - if (way.type !== 'way' || !way.tags.highway || way.isArea()) - return []; - - function withRestriction(turn) { - graph.parentRelations(turn.from).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.id && - t && t.id === turn.to.id && - v && v.id === turn.via.id) { - turn.restriction = relation; - } - }); - - return turn; - } - - var turns = []; - - [way.first(), way.last()].forEach(function(nodeID) { - var node = graph.entity(nodeID); - graph.parentWays(node).forEach(function(parent) { - if (parent === way || parent.isDegenerate() || !parent.tags.highway) - return; - if (way.first() === node.id && way.tags.oneway === 'yes') - return; - if (way.last() === node.id && way.tags.oneway === '-1') - return; - - var index = parent.nodes.indexOf(node.id); - - // backward - if (parent.first() !== node.id && parent.tags.oneway !== 'yes') { - turns.push(withRestriction({ - from: way, - to: parent, - via: node, - toward: graph.entity(parent.nodes[index - 1]) - })); - } - - // forward - if (parent.last() !== node.id && parent.tags.oneway !== '-1') { - turns.push(withRestriction({ - from: way, - to: parent, - via: node, - toward: graph.entity(parent.nodes[index + 1]) - })); - } - }); - }); - - return turns; -}; diff --git a/js/id/id.js b/js/id/id.js index 980d1a792..2c545d251 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -197,52 +197,7 @@ window.iD = function () { }; /* Projection */ - function rawMercator() { - var project = d3.geo.mercator.raw, - k = 512 / Math.PI, // scale - x = 0, y = 0, // translate - clipExtent = [[0, 0], [0, 0]]; - - function projection(point) { - point = project(point[0] * Math.PI / 180, point[1] * Math.PI / 180); - return [point[0] * k + x, y - point[1] * k]; - } - - projection.invert = function(point) { - point = project.invert((point[0] - x) / k, (y - point[1]) / k); - return point && [point[0] * 180 / Math.PI, point[1] * 180 / Math.PI]; - }; - - projection.scale = function(_) { - if (!arguments.length) return k; - k = +_; - return projection; - }; - - projection.translate = function(_) { - if (!arguments.length) return [x, y]; - x = +_[0]; - y = +_[1]; - return projection; - }; - - projection.clipExtent = function(_) { - if (!arguments.length) return clipExtent; - clipExtent = _; - return projection; - }; - - projection.stream = d3.geo.transform({ - point: function(x, y) { - x = projection([x, y]); - this.stream.point(x[0], x[1]); - } - }).stream; - - return projection; - } - - context.projection = rawMercator(); + context.projection = iD.geo.RawMercator(); /* Background */ var background = iD.Background(context); diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 13516c9cc..15c41c067 100644 --- a/js/id/operations/delete.js +++ b/js/id/operations/delete.js @@ -36,15 +36,15 @@ iD.operations.Delete = function(selectedIDs, context) { } } - context.perform( - action, - annotation); - if (nextSelectedID && context.hasEntity(nextSelectedID)) { context.enter(iD.modes.Select(context, [nextSelectedID])); } else { context.enter(iD.modes.Browse(context)); } + + context.perform( + action, + annotation); }; operation.available = function() { diff --git a/js/id/presets/field.js b/js/id/presets/field.js index afbb9f4cb..a82813243 100644 --- a/js/id/presets/field.js +++ b/js/id/presets/field.js @@ -4,7 +4,7 @@ iD.presets.Field = function(id, field) { field.id = id; field.matchGeometry = function(geometry) { - return !field.geometry || field.geometry.indexOf(geometry) >= 0; + return !field.geometry || field.geometry === geometry; }; field.t = function(scope, options) { diff --git a/js/id/svg/defs.js b/js/id/svg/defs.js new file mode 100644 index 000000000..25ce53082 --- /dev/null +++ b/js/id/svg/defs.js @@ -0,0 +1,129 @@ +/* + A standalone SVG element that contains only a `defs` sub-element. To be + used once globally, since defs IDs must be unique within a document. +*/ +iD.svg.Defs = function(context) { + function autosize(image) { + var img = document.createElement('img'); + img.src = image.attr('xlink:href'); + img.onload = function() { + image.attr({ + width: img.width, + height: img.height + }); + }; + } + + function SpriteDefinition(id, href, data) { + return function(defs) { + defs.append('image') + .attr('id', id) + .attr('xlink:href', href) + .call(autosize); + + defs.selectAll() + .data(data) + .enter().append('use') + .attr('id', function(d) { return d.key; }) + .attr('transform', function(d) { return 'translate(-' + d.value[0] + ',-' + d.value[1] + ')'; }) + .attr('xlink:href', '#' + id); + }; + } + + return function (selection) { + var defs = selection.append('defs'); + + defs.append('marker') + .attr({ + id: 'oneway-marker', + viewBox: '0 0 10 10', + refY: 2.5, + refX: 5, + markerWidth: 2, + markerHeight: 2, + orient: 'auto' + }) + .append('path') + .attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z'); + + var patterns = defs.selectAll('pattern') + .data([ + // pattern name, pattern image name + ['wetland', 'wetland'], + ['construction', 'construction'], + ['cemetery', 'cemetery'], + ['orchard', 'orchard'], + ['farmland', 'farmland'], + ['beach', 'dots'], + ['scrub', 'dots'], + ['meadow', 'dots'] + ]) + .enter() + .append('pattern') + .attr({ + id: function (d) { + return 'pattern-' + d[0]; + }, + width: 32, + height: 32, + patternUnits: 'userSpaceOnUse' + }); + + patterns.append('rect') + .attr({ + x: 0, + y: 0, + width: 32, + height: 32, + 'class': function (d) { + return 'pattern-color-' + d[0]; + } + }); + + patterns.append('image') + .attr({ + x: 0, + y: 0, + width: 32, + height: 32 + }) + .attr('xlink:href', function (d) { + return context.imagePath('pattern/' + d[1] + '.png'); + }); + + defs.selectAll() + .data([12, 18, 20, 32, 45]) + .enter().append('clipPath') + .attr('id', function (d) { + return 'clip-square-' + d; + }) + .append('rect') + .attr('x', 0) + .attr('y', 0) + .attr('width', function (d) { + return d; + }) + .attr('height', function (d) { + return d; + }); + + var maki = []; + _.forEach(iD.data.featureIcons, function (dimensions, name) { + if (dimensions['12'] && dimensions['18'] && dimensions['24']) { + maki.push({key: 'maki-' + name + '-12', value: dimensions['12']}); + maki.push({key: 'maki-' + name + '-18', value: dimensions['18']}); + maki.push({key: 'maki-' + name + '-24', value: dimensions['24']}); + } + }); + + defs.call(SpriteDefinition( + 'sprite', + context.imagePath('sprite.svg'), + d3.entries(iD.data.operations))); + + defs.call(SpriteDefinition( + 'maki-sprite', + context.imagePath('maki-sprite.png'), + maki)); + }; +}; diff --git a/js/id/svg/restrictions.js b/js/id/svg/restrictions.js deleted file mode 100644 index 66c8289c5..000000000 --- a/js/id/svg/restrictions.js +++ /dev/null @@ -1,75 +0,0 @@ -iD.svg.Restrictions = function(context) { - var projection = context.projection; - - function drawRestrictions(surface) { - var turns = drawRestrictions.turns(context.graph(), context.selectedIDs()); - - var groups = surface.select('.layer-hit').selectAll('g.restriction') - .data(turns, iD.Entity.key); - - var enter = groups.enter().append('g') - .attr('class', 'restriction'); - - enter.append('circle') - .attr('class', 'restriction') - .attr('r', 4); - - groups - .attr('transform', function(restriction) { - var via = context.entity(restriction.memberByRole('via').id); - return iD.svg.PointTransform(projection)(via); - }); - - groups.exit() - .remove(); - - return this; - } - - drawRestrictions.turns = function (graph, selectedIDs) { - if (selectedIDs.length !== 1) - return []; - - var from = graph.entity(selectedIDs[0]); - if (from.type !== 'way') - return []; - - return graph.parentRelations(from).filter(function(relation) { - var f = relation.memberById(from.id), - t = relation.memberByRole('to'), - v = relation.memberByRole('via'); - - return relation.tags.type === 'restriction' && f.role === 'from' && - t && t.type === 'way' && graph.hasEntity(t.id) && - v && v.type === 'node' && graph.hasEntity(v.id) && - !graph.entity(t.id).isDegenerate() && - !graph.entity(f.id).isDegenerate() && - graph.entity(t.id).affix(v.id) && - graph.entity(f.id).affix(v.id); - }); - }; - - drawRestrictions.datum = function(graph, from, restriction, projection) { - var to = graph.entity(restriction.memberByRole('to').id), - a = graph.entity(restriction.memberByRole('via').id), - b; - - if (to.first() === a.id) { - b = graph.entity(to.nodes[1]); - } else { - b = graph.entity(to.nodes[to.nodes.length - 2]); - } - - a = projection(a.loc); - b = projection(b.loc); - - return { - from: from, - to: to, - restriction: restriction, - angle: Math.atan2(b[1] - a[1], b[0] - a[0]) - }; - }; - - return drawRestrictions; -}; diff --git a/js/id/svg/surface.js b/js/id/svg/surface.js index cac731c69..3b11461dd 100644 --- a/js/id/svg/surface.js +++ b/js/id/svg/surface.js @@ -1,114 +1,5 @@ -iD.svg.Surface = function(context) { - function autosize(image) { - var img = document.createElement('img'); - img.src = image.attr('xlink:href'); - img.onload = function() { - image.attr({ - width: img.width, - height: img.height - }); - }; - } - - function SpriteDefinition(id, href, data) { - return function(defs) { - defs.append('image') - .attr('id', id) - .attr('xlink:href', href) - .call(autosize); - - defs.selectAll() - .data(data) - .enter().append('use') - .attr('id', function(d) { return d.key; }) - .attr('transform', function(d) { return 'translate(-' + d.value[0] + ',-' + d.value[1] + ')'; }) - .attr('xlink:href', '#' + id); - }; - } - - return function drawSurface(selection) { - var defs = selection.append('defs'); - - defs.append('marker') - .attr({ - id: 'oneway-marker', - viewBox: '0 0 10 10', - refY: 2.5, - refX: 5, - markerWidth: 2, - markerHeight: 2, - orient: 'auto' - }) - .append('path') - .attr('d', 'M 5 3 L 0 3 L 0 2 L 5 2 L 5 0 L 10 2.5 L 5 5 z'); - - var patterns = defs.selectAll('pattern') - .data([ - // pattern name, pattern image name - ['wetland', 'wetland'], - ['construction', 'construction'], - ['cemetery', 'cemetery'], - ['orchard', 'orchard'], - ['farmland', 'farmland'], - ['beach', 'dots'], - ['scrub', 'dots'], - ['meadow', 'dots']]) - .enter() - .append('pattern') - .attr({ - id: function(d) { return 'pattern-' + d[0]; }, - width: 32, - height: 32, - patternUnits: 'userSpaceOnUse' - }); - - patterns.append('rect') - .attr({ - x: 0, - y: 0, - width: 32, - height: 32, - 'class': function(d) { return 'pattern-color-' + d[0]; } - }); - - patterns.append('image') - .attr({ - x: 0, - y: 0, - width: 32, - height: 32 - }) - .attr('xlink:href', function(d) { return context.imagePath('pattern/' + d[1] + '.png'); }); - - defs.selectAll() - .data([12, 18, 20]) - .enter().append('clipPath') - .attr('id', function(d) { return 'clip-square-' + d; }) - .append('rect') - .attr('x', 0) - .attr('y', 0) - .attr('width', function(d) { return d; }) - .attr('height', function(d) { return d; }); - - var maki = []; - _.forEach(iD.data.featureIcons, function(dimensions, name) { - if (dimensions['12'] && dimensions['18'] && dimensions['24']) { - maki.push({key: 'maki-' + name + '-12', value: dimensions['12']}); - maki.push({key: 'maki-' + name + '-18', value: dimensions['18']}); - maki.push({key: 'maki-' + name + '-24', value: dimensions['24']}); - } - }); - - defs.call(SpriteDefinition( - 'sprite', - context.imagePath('sprite.svg'), - d3.entries(iD.data.operations))); - - defs.call(SpriteDefinition( - 'maki-sprite', - context.imagePath('maki-sprite.png'), - maki)); - +iD.svg.Surface = function() { + return function (selection) { var layers = selection.selectAll('.layer') .data(['fill', 'shadow', 'casing', 'stroke', 'oneway', 'hit', 'halo', 'label']); diff --git a/js/id/svg/turns.js b/js/id/svg/turns.js new file mode 100644 index 000000000..555b75eef --- /dev/null +++ b/js/id/svg/turns.js @@ -0,0 +1,67 @@ +iD.svg.Turns = function(projection) { + return function(surface, graph, turns) { + function icon(turn) { + if (!turn.restriction) + return '#icon-restriction-yes'; + var restriction = graph.entity(turn.restriction).tags.restriction; + return '#icon-restriction-' + + (!turn.indirect_restriction && /^only_/.test(restriction) ? 'only' : 'no') + + (turn.u ? '-u' : ''); + } + + var groups = surface.select('.layer-hit').selectAll('g.turn') + .data(turns); + + // Enter + + var enter = groups.enter().append('g') + .attr('class', 'turn'); + + var nEnter = enter.filter(function (turn) { return !turn.u; }); + + nEnter.append('rect') + .attr('transform', 'translate(-12, -12)') + .attr('width', '45') + .attr('height', '25'); + + nEnter.append('use') + .attr('transform', 'translate(-12, -12)') + .attr('clip-path', 'url(#clip-square-45)'); + + var uEnter = enter.filter(function (turn) { return turn.u; }); + + uEnter.append('circle') + .attr('r', '16'); + + uEnter.append('use') + .attr('transform', 'translate(-16, -16)') + .attr('clip-path', 'url(#clip-square-32)'); + + // Update + + groups + .attr('transform', function (turn) { + var v = graph.entity(turn.via.node), + t = graph.entity(turn.to.node), + a = iD.geo.angle(v, t, projection), + p = projection(v.loc), + r = turn.u ? 0 : 60; + + return 'translate(' + (r * Math.cos(a) + p[0]) + ',' + (r * Math.sin(a) + p[1]) + ')' + + 'rotate(' + a * 180 / Math.PI + ')'; + }); + + groups.select('use') + .attr('xlink:href', icon); + + groups.select('rect'); + groups.select('circle'); + + // Exit + + groups.exit() + .remove(); + + return this; + }; +}; diff --git a/js/id/ui.js b/js/id/ui.js index 678c55ca4..01f8ed6df 100644 --- a/js/id/ui.js +++ b/js/id/ui.js @@ -12,6 +12,10 @@ iD.ui = function(context) { map.centerZoom([-77.02271, 38.90085], 20); } + container.append('svg') + .attr('id', 'defs') + .call(iD.svg.Defs(context)); + container.append('div') .attr('id', 'sidebar') .attr('class', 'col4') diff --git a/js/id/ui/entity_editor.js b/js/id/ui/entity_editor.js index 4f59f209e..272f3555a 100644 --- a/js/id/ui/entity_editor.js +++ b/js/id/ui/entity_editor.js @@ -5,6 +5,8 @@ iD.ui.EntityEditor = function(context) { preset, reference; + var presetEditor = iD.ui.preset(context) + .on('change', changeTags); var rawTagEditor = iD.ui.RawTagEditor(context) .on('change', changeTags); @@ -91,12 +93,11 @@ iD.ui.EntityEditor = function(context) { .text(preset.name()); $body.select('.inspector-preset') - .call(iD.ui.preset(context) + .call(presetEditor .preset(preset) .entityID(id) .tags(tags) - .state(state) - .on('change', changeTags)); + .state(state)); $body.select('.raw-tag-editor') .call(rawTagEditor diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index 0f53ef62c..5c4bff46a 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -31,7 +31,12 @@ iD.ui.Inspector = function(context) { var $presetPane = $wrap.select('.preset-list-pane'); var $editorPane = $wrap.select('.entity-editor-pane'); - var showEditor = state === 'hover' || context.entity(entityID).isUsed(context.graph()); + var graph = context.graph(), + entity = context.entity(entityID), + showEditor = state === 'hover' || + entity.isUsed(graph) || + entity.isIntersection(graph); + if (showEditor) { $wrap.style('right', '0%'); $editorPane.call(entityEditor); diff --git a/js/id/ui/preset.js b/js/id/ui/preset.js index 99ae2ff54..b0a372bc3 100644 --- a/js/id/ui/preset.js +++ b/js/id/ui/preset.js @@ -72,6 +72,10 @@ iD.ui.preset = function(context) { } }); + if (geometry === 'vertex' && entity.isIntersection(context.graph())) { + fields.push(UIField(context.presets().field('restrictions'), entity, true)); + } + context.presets().universal().forEach(function(field) { if (preset.fields.indexOf(field) < 0) { fields.push(UIField(field, entity)); @@ -177,7 +181,7 @@ iD.ui.preset = function(context) { function show(field) { field.show = true; - presets(selection); + context.presets()(selection); field.input.focus(); } @@ -196,6 +200,7 @@ iD.ui.preset = function(context) { presets.preset = function(_) { if (!arguments.length) return preset; + if (preset && preset.id === _.id) return presets; preset = _; fields = null; return presets; @@ -216,6 +221,7 @@ iD.ui.preset = function(context) { presets.entityID = function(_) { if (!arguments.length) return id; + if (id === _) return presets; id = _; fields = null; return presets; diff --git a/js/id/ui/preset/restrictions.js b/js/id/ui/preset/restrictions.js new file mode 100644 index 000000000..ef31b77aa --- /dev/null +++ b/js/id/ui/preset/restrictions.js @@ -0,0 +1,137 @@ +iD.ui.preset.restrictions = function(field, context) { + var event = d3.dispatch('change'), + vertexID, + fromNodeID; + + function restrictions(selection) { + var wrap = selection.selectAll('.preset-input-wrap') + .data([0]); + + var enter = wrap.enter().append('div') + .attr('class', 'preset-input-wrap'); + + enter.append('div') + .attr('class', 'restriction-help'); + + enter.append('svg') + .call(iD.svg.Surface(context)) + .call(iD.behavior.Hover(context)); + + var intersection = iD.geo.Intersection(context.graph(), vertexID), + graph = intersection.graph, + vertex = graph.entity(vertexID), + surface = wrap.selectAll('svg'), + filter = function () { return true; }, + extent = iD.geo.Extent(), + projection = iD.geo.RawMercator(), + lines = iD.svg.Lines(projection, context), + vertices = iD.svg.Vertices(projection, context), + turns = iD.svg.Turns(projection, context); + + var d = wrap.dimensions(), + c = [d[0] / 2, d[1] / 2], + z = 21; + + projection + .scale(256 * Math.pow(2, z) / (2 * Math.PI)); + + var s = projection(vertex.loc); + + projection + .translate([c[0] - s[0], c[1] - s[1]]) + .clipExtent([[0, 0], d]); + + surface + .call(vertices, graph, [vertex], filter, extent, z) + .call(lines, graph, intersection.highways, filter) + .call(turns, graph, intersection.turns(fromNodeID)); + + surface + .on('click.restrictions', click) + .on('mouseover.restrictions', mouseover) + .on('mouseout.restrictions', mouseout); + + surface + .selectAll('.selected') + .classed('selected', false); + + if (fromNodeID) { + surface + .selectAll('.' + _.find(intersection.highways, function(way) { return way.contains(fromNodeID); }).id) + .classed('selected', true); + } + + mouseout(); + + context.history() + .on('change.restrictions', render); + + d3.select(window) + .on('resize.restrictions', render); + + function click() { + var datum = d3.event.target.__data__; + if (datum instanceof iD.Entity) { + fromNodeID = datum.nodes[(datum.first() === vertexID) ? 1 : datum.nodes.length - 2]; + render(); + } else if (datum instanceof iD.geo.Turn) { + if (datum.restriction) { + context.perform( + iD.actions.UnrestrictTurn(datum, projection), + t('operations.restriction.annotation.delete')); + } else { + context.perform( + iD.actions.RestrictTurn(datum, projection), + t('operations.restriction.annotation.create')); + } + } + } + + function mouseover() { + var datum = d3.event.target.__data__; + if (datum instanceof iD.geo.Turn) { + var graph = context.graph(), + presets = context.presets(), + preset; + + if (datum.restriction) { + preset = presets.match(graph.entity(datum.restriction), graph); + } else { + preset = presets.item('type/restriction/' + + iD.geo.inferRestriction( + graph.entity(datum.from.node), + graph.entity(datum.via.node), + graph.entity(datum.to.node), + projection)); + } + + wrap.selectAll('.restriction-help') + .text(t('operations.restriction.help.' + + (datum.restriction ? 'toggle_off' : 'toggle_on'), + {restriction: preset.name()})); + } + } + + function mouseout() { + wrap.selectAll('.restriction-help') + .text(t('operations.restriction.help.' + + (fromNodeID ? 'toggle' : 'select'))); + } + + function render() { + restrictions(selection); + } + } + + restrictions.entity = function(_) { + if (!vertexID || vertexID !== _.id) { + fromNodeID = null; + vertexID = _.id; + } + }; + + restrictions.tags = function() {}; + restrictions.focus = function() {}; + + return d3.rebind(restrictions, event, 'on'); +}; diff --git a/js/lib/lodash.js b/js/lib/lodash.js index cda72370c..4c8cb489e 100644 --- a/js/lib/lodash.js +++ b/js/lib/lodash.js @@ -1,7 +1,7 @@ /** * @license * Lo-Dash 2.3.0 (Custom Build) - * Build: `lodash include="any,assign,bind,clone,compact,contains,debounce,difference,each,every,extend,filter,find,first,forEach,groupBy,indexOf,intersection,isEmpty,isEqual,isFunction,keys,last,map,omit,pairs,pluck,reject,some,throttle,union,uniq,unique,values,without,flatten,value,chain,cloneDeep,merge" exports="global,node"` + * Build: `lodash --debug --output js/lib/lodash.js include="any,assign,bind,clone,compact,contains,debounce,difference,each,every,extend,filter,find,first,forEach,groupBy,indexOf,intersection,isEmpty,isEqual,isFunction,keys,last,map,omit,pairs,pluck,reject,some,throttle,union,uniq,unique,values,without,flatten,value,chain,cloneDeep,merge,pick" exports="global,node"` * Copyright 2012-2013 The Dojo Foundation * Based on Underscore.js 1.5.2 * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors @@ -2304,6 +2304,57 @@ return result; } + /** + * Creates a shallow clone of `object` composed of the specified properties. + * Property names may be specified as individual arguments or as arrays of + * property names. If a callback is provided it will be executed for each + * property of `object` picking the properties the callback returns truey + * for. The callback is bound to `thisArg` and invoked with three arguments; + * (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The source object. + * @param {Function|...string|string[]} [callback] The function called per + * iteration or property names to pick, specified as individual property + * names or arrays of property names. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns an object composed of the picked properties. + * @example + * + * _.pick({ 'name': 'fred', '_userid': 'fred1' }, 'name'); + * // => { 'name': 'fred' } + * + * _.pick({ 'name': 'fred', '_userid': 'fred1' }, function(value, key) { + * return key.charAt(0) != '_'; + * }); + * // => { 'name': 'fred' } + */ + function pick(object, callback, thisArg) { + var result = {}; + if (typeof callback != 'function') { + var index = -1, + props = baseFlatten(arguments, true, false, 1), + length = isObject(object) ? props.length : 0; + + while (++index < length) { + var key = props[index]; + if (key in object) { + result[key] = object[key]; + } + } + } else { + callback = lodash.createCallback(callback, thisArg, 3); + forIn(object, function(value, key, object) { + if (callback(value, key, object)) { + result[key] = value; + } + }); + } + return result; + } + /** * Creates an array composed of the own enumerable property values of `object`. * @@ -3830,6 +3881,7 @@ lodash.merge = merge; lodash.omit = omit; lodash.pairs = pairs; + lodash.pick = pick; lodash.pluck = pluck; lodash.reject = reject; lodash.throttle = throttle; diff --git a/package.json b/package.json index 0512a11cc..051e999d0 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "devDependencies": { "d3": "3.4.6", "smash": "0.0", + "lodash-cli": "2.3.0", "uglify-js": "~2.2.5", "maki": "0.4", "jshint": "2.3.0", diff --git a/svg/relation-presets.svg b/svg/relation-presets.svg index 0953a252c..c791d2573 100644 --- a/svg/relation-presets.svg +++ b/svg/relation-presets.svg @@ -14,8 +14,11 @@ height="100" id="svg7013" version="1.1" - inkscape:version="0.48.4 r9939" - sodipodi:docname="relation-presets.svg"> + inkscape:version="0.48.2 r9819" + sodipodi:docname="relation-presets.svg" + inkscape:export-filename="/Users/saman/work_repos/iD/css/img/relation-presets@2x.png" + inkscape:export-xdpi="180" + inkscape:export-ydpi="180"> + + + + + + @@ -369,7 +396,7 @@ image/svg+xml - + @@ -1505,7 +1532,7 @@ id="path23512" transform="translate(-400,12.362183)" d="M 110 39.5 C 101.43958 39.5 94.5 46.439584 94.5 55 C 94.5 63.560416 101.43958 70.5 110 70.5 C 118.56042 70.5 125.5 63.560416 125.5 55 C 125.5 46.439584 118.56042 39.5 110 39.5 z M 110 43.5 C 116.35127 43.5 121.5 48.648725 121.5 55 C 121.5 57.454314 120.71623 59.726293 119.40625 61.59375 L 103.40625 45.59375 C 105.27371 44.283773 107.54569 43.5 110 43.5 z M 100.59375 48.40625 L 116.59375 64.40625 C 114.72629 65.716227 112.45431 66.5 110 66.5 C 103.64873 66.5 98.5 61.351275 98.5 55 C 98.5 52.545686 99.283773 50.273707 100.59375 48.40625 z " - style="color:#000000;fill:#e06d5f;fill-opacity:1;fill-rule:nonzero;stroke:#70372f;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + style="color:#000000;fill:#e06d5f;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/index.html b/test/index.html index 8fe2bc698..dec049083 100644 --- a/test/index.html +++ b/test/index.html @@ -45,8 +45,9 @@ + - + @@ -56,14 +57,14 @@ + + - - @@ -117,6 +118,7 @@ + @@ -130,13 +132,14 @@ - - - - + + + + + @@ -229,12 +232,14 @@ + + + - @@ -257,7 +262,6 @@ - diff --git a/test/index_packaged.html b/test/index_packaged.html index 55c067b52..b22e812ee 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -47,10 +47,11 @@ + + - @@ -73,7 +74,6 @@ - diff --git a/test/spec/actions/restrict_turn.js b/test/spec/actions/restrict_turn.js new file mode 100644 index 000000000..a147470cb --- /dev/null +++ b/test/spec/actions/restrict_turn.js @@ -0,0 +1,295 @@ +describe("iD.actions.RestrictTurn", function() { + var projection = d3.geo.mercator().scale(250 / Math.PI); + + it('adds a restriction to an unrestricted turn', function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*']}), + iD.Way({id: '-', nodes: ['*', 'w']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'}, + restriction: 'no_right_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '-', type: 'way'}); + }); + + it('splits the from way when necessary (forward)', function() { + // u====*===>w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*', 'w']}), + iD.Way({id: '-', nodes: ['*', 'x']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'x', way: '-'}, + restriction: 'no_right_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '-', type: 'way'}); + }); + + it('splits the from way when necessary (backward)', function() { + // u====*===>w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*', 'w']}), + iD.Way({id: '-', nodes: ['*', 'x']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'w', way: '=', newID: '=='}, + via: {node: '*'}, + to: {node: 'x', way: '-'}, + restriction: 'no_left_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_left_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '==', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '-', type: 'way'}); + }); + + it('splits the 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.actions.RestrictTurn({ + 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(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '==', type: 'way'}); + }); + + it('splits the from way when necessary (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.actions.RestrictTurn({ + 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(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '==', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + }); + + it('splits the to way when necessary (forward)', function() { + // u====*===>w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*', 'w']}), + iD.Way({id: '-', nodes: ['*', 'x']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'x', way: '-'}, + via: {node: '*'}, + to: {node: 'w', way: '=', newID: '=='}, + restriction: 'no_right_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_right_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '-', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '==', type: 'way'}); + }); + + it('splits the to way when necessary (backward)', function() { + // u====*===>w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*', 'w']}), + iD.Way({id: '-', nodes: ['*', 'x']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'x', way: '-'}, + via: {node: '*'}, + to: {node: 'u', way: '='}, + restriction: 'no_left_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_left_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '-', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + }); + + it('splits the from/to way of a U-turn (forward)', function() { + // u====*===>w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*', 'w']}), + iD.Way({id: '-', nodes: ['*', 'x']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'u', way: '='}, + restriction: 'no_u_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_u_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '=', type: 'way'}); + }); + + it('splits the from/to way of a U-turn (backward)', function() { + // u====*===>w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*', 'w']}), + iD.Way({id: '-', nodes: ['*', 'x']}) + ]), + action = iD.actions.RestrictTurn({ + from: {node: 'w', way: '=', newID: '=='}, + via: {node: '*'}, + to: {node: 'w', way: '=', newID: '~~'}, + restriction: 'no_u_turn' + }, projection, 'r'); + + graph = action(graph); + + var r = graph.entity('r'); + expect(r.tags).to.eql({type: 'restriction', restriction: 'no_u_turn'}); + expect(_.pick(r.memberByRole('from'), 'id', 'type')).to.eql({id: '==', type: 'way'}); + expect(_.pick(r.memberByRole('via'), 'id', 'type')).to.eql({id: '*', type: 'node'}); + expect(_.pick(r.memberByRole('to'), 'id', 'type')).to.eql({id: '==', type: 'way'}); + }); + + it('guesses the restriction type based on the turn angle', function() { + // u====*~~~~w + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u', loc: [-1, 0]}), + iD.Node({id: '*', loc: [ 0, 0]}), + iD.Node({id: 'w', loc: [ 0, 1]}), + iD.Node({id: 'x', loc: [ 0, -1]}), + iD.Way({id: '=', nodes: ['u', '*']}), + iD.Way({id: '-', nodes: ['*', 'x']}), + iD.Way({id: '~', nodes: ['*', 'w']}) + ]); + + var r = iD.actions.RestrictTurn({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'x', way: '-'} + }, projection, 'r')(graph); + expect(r.entity('r').tags.restriction).to.equal('no_right_turn'); + + var l = iD.actions.RestrictTurn({ + from: {node: 'x', way: '-'}, + via: {node: '*'}, + to: {node: 'u', way: '='} + }, projection, 'r')(graph); + expect(l.entity('r').tags.restriction).to.equal('no_left_turn'); + + var s = iD.actions.RestrictTurn({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '~'} + }, projection, 'r')(graph); + expect(s.entity('r').tags.restriction).to.equal('no_straight_on'); + + var u = iD.actions.RestrictTurn({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'u', way: '='} + }, projection, 'r')(graph); + expect(u.entity('r').tags.restriction).to.equal('no_u_turn'); + }); +}); diff --git a/test/spec/actions/unrestrict_turn.js b/test/spec/actions/unrestrict_turn.js new file mode 100644 index 000000000..f7c5a6830 --- /dev/null +++ b/test/spec/actions/unrestrict_turn.js @@ -0,0 +1,24 @@ +describe("iD.actions.UnrestrictTurn", function() { + it('removes a restriction from a restricted turn', function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}), + iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + {id: '=', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: '*', role: 'via', type: 'node'} + ]}) + ]), + action = iD.actions.UnrestrictTurn({ + restriction: 'r' + }); + + graph = action(graph); + + expect(graph.hasEntity('r')).to.be.undefined; + }); +}); diff --git a/test/spec/geo/intersection.js b/test/spec/geo/intersection.js new file mode 100644 index 000000000..bcf083542 --- /dev/null +++ b/test/spec/geo/intersection.js @@ -0,0 +1,376 @@ +describe("iD.geo.Intersection", function() { + describe('highways', function() { + it('excludes non-highways', function() { + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*']}), + iD.Way({id: '-', nodes: ['*', 'w']}) + ]); + expect(iD.geo.Intersection(graph, '*').highways).to.eql([]); + }); + + it("excludes degenerate highways", function() { + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*'], tags: {highway: 'residential'}}) + ]); + expect(_.pluck(iD.geo.Intersection(graph, '*').highways, 'id')).to.eql(['=']); + }); + + it('includes line highways', function() { + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w']}) + ]); + expect(_.pluck(iD.geo.Intersection(graph, '*').highways, 'id')).to.eql(['=']); + }); + + it('excludes area highways', function() { + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'pedestrian', area: 'yes'}}) + ]); + expect(iD.geo.Intersection(graph, '*').highways).to.eql([]); + }); + + it('auto-splits highways at the intersection', function() { + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*', 'w'], tags: {highway: 'residential'}}) + ]); + expect(_.pluck(iD.geo.Intersection(graph, '*').highways, 'id')).to.eql(['=-a', '=-b']); + }); + }); + + describe('#turns', function() { + it("permits turns onto a way forward", function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(2); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }); + }); + + it("permits turns onto a way backward", function() { + // u====*<---w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', '*'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(2); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }); + }); + + it("permits turns from a way that must be split", function() { + // w + // | + // u===* + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', '*', 'x'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('w'); + + expect(turns.length).to.eql(3); + expect(turns[0]).to.eql({ + from: {node: 'w', way: '-'}, + via: {node: '*'}, + to: {node: 'u', way: '='} + }); + expect(turns[1]).to.eql({ + from: {node: 'w', way: '-'}, + via: {node: '*'}, + to: {node: 'x', way: '-'} + }); + expect(turns[2]).to.eql({ + from: {node: 'w', way: '-'}, + via: {node: '*'}, + to: {node: 'w', way: '-'}, + u: true + }); + }); + + it("permits turns to a way that must be split", function() { + // w + // | + // u===* + // | + // x + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Node({id: 'x'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', '*', 'x'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(3); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }); + expect(turns[1]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'x', way: '-'} + }); + expect(turns[2]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'u', way: '='}, + u: true + }); + }); + + it("permits turns from a oneway forward", function() { + // u===>v----w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns).to.eql([{ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }]); + }); + + it("permits turns from a reverse oneway backward", function() { + // u<===*----w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential', oneway: '-1'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns).to.eql([{ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }]); + }); + + it("omits turns from a oneway backward", function() { + // u<===*----w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['*', 'u'], tags: {highway: 'residential', oneway: 'yes'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]); + expect(iD.geo.Intersection(graph, '*').turns('u')).to.eql([]); + }); + + it("omits turns from a reverse oneway forward", function() { + // u===>*----w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential', oneway: '-1'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]); + expect(iD.geo.Intersection(graph, '*').turns('u')).to.eql([]); + }); + + it("permits turns onto a oneway forward", function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential', oneway: 'yes'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(2); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }); + }); + + it("permits turns onto a reverse oneway backward", function() { + // u====*<---w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', '*'], tags: {highway: 'residential', oneway: '-1'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(2); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'} + }); + }); + + it("omits turns onto a oneway backward", function() { + // u====*<---w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', '*'], tags: {highway: 'residential', oneway: 'yes'}}) + ]); + expect(iD.geo.Intersection(graph, '*').turns('u').length).to.eql(1); + }); + + it("omits turns onto a reverse oneway forward", function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential', oneway: '-1'}}) + ]); + expect(iD.geo.Intersection(graph, '*').turns('u').length).to.eql(1); + }); + + it("includes U-turns", function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(2); + expect(turns[1]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'u', way: '='}, + u: true + }); + }); + + it("restricts turns with a restriction relation", function() { + // u====*--->w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: '*'}), + iD.Node({id: 'w'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['*', 'w'], tags: {highway: 'residential'}}), + iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ + {id: '=', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: '*', role: 'via', type: 'node'} + ]}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(2); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'}, + restriction: 'r' + }); + }); + + it("restricts turns affected by an only_* restriction relation", function() { + // u====*~~~~v + // | + // w + var graph = iD.Graph([ + iD.Node({id: 'u'}), + iD.Node({id: 'v'}), + iD.Node({id: 'w'}), + iD.Node({id: '*'}), + iD.Way({id: '=', nodes: ['u', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '~', nodes: ['v', '*'], tags: {highway: 'residential'}}), + iD.Way({id: '-', nodes: ['w', '*'], tags: {highway: 'residential'}}), + iD.Relation({id: 'r', tags: {type: 'restriction', restriction: 'only_right_turn'}, members: [ + {id: '=', role: 'from', type: 'way'}, + {id: '-', role: 'to', type: 'way'}, + {id: '*', role: 'via', type: 'node'} + ]}) + ]), + turns = iD.geo.Intersection(graph, '*').turns('u'); + + expect(turns.length).to.eql(3); + expect(turns[0]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'v', way: '~'}, + restriction: 'r', + indirect_restriction: true + }); + expect(turns[1]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'w', way: '-'}, + restriction: 'r' + }); + expect(turns[2]).to.eql({ + from: {node: 'u', way: '='}, + via: {node: '*'}, + to: {node: 'u', way: '='}, + restriction: 'r', + indirect_restriction: true, + u: true + }); + }); + }); +}); diff --git a/test/spec/geo/turn.js b/test/spec/geo/turn.js deleted file mode 100644 index 4d0c87cac..000000000 --- a/test/spec/geo/turn.js +++ /dev/null @@ -1,276 +0,0 @@ -describe("iD.geo.turns", function() { - it("returns an empty array for non-ways", function() { - var graph = iD.Graph([ - iD.Node({id: 'n'}) - ]); - expect(iD.geo.turns(graph, 'n')).to.eql([]); - }); - - it("returns an empty array for non-lines", function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', area: 'yes'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("returns an empty array for an unconnected way", function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Way({id: '=', nodes: ['u', 'v']}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("omits turns onto degenerate ways", function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("omits turns from non-highways", function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v']}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("omits turns onto non-highways", function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w']}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("omits turns onto non-lines", function() { - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Node({id: 'x'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w', 'x', 'v'], tags: {highway: 'residential', area: 'yes'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("permits turns onto a way forward", function() { - // u====v--->w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }]); - }); - - it("permits turns onto a way backward", function() { - // u====v<---w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }]); - }); - - it("permits turns onto a way in both directions", function() { - // w - // | - // u===v - // | - // x - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Node({id: 'x'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', 'v', 'x'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }, { - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('x') - }]); - }); - - it("permits turns from a oneway forward", function() { - // u===>v----w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', oneway: 'yes'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }]); - }); - - it("permits turns from a reverse oneway backward", function() { - // u<===v----w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['v', 'u'], tags: {highway: 'residential', oneway: '-1'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }]); - }); - - it("omits turns from a oneway backward", function() { - // u<===v----w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['v', 'u'], tags: {highway: 'residential', oneway: 'yes'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("omits turns from a reverse oneway forward", function() { - // u===>v----w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential', oneway: '-1'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("permits turns onto a oneway forward", function() { - // u====v--->w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential', oneway: 'yes'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }]); - }); - - it("permits turns onto a reverse oneway backward", function() { - // u====v<---w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential', oneway: '-1'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w') - }]); - }); - - it("omits turns onto a oneway backward", function() { - // u====v<---w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['w', 'v'], tags: {highway: 'residential', oneway: 'yes'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("omits turns onto a reverse oneway forward", function() { - // u====v--->w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential', oneway: '-1'}}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([]); - }); - - it("restricts turns with a restriction relation", function() { - // u====v--->w - var graph = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: '=', nodes: ['u', 'v'], tags: {highway: 'residential'}}), - iD.Way({id: '-', nodes: ['v', 'w'], tags: {highway: 'residential'}}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - {id: '=', role: 'from', type: 'way'}, - {id: '-', role: 'to', type: 'way'}, - {id: 'v', role: 'via', type: 'node'} - ]}) - ]); - expect(iD.geo.turns(graph, '=')).to.eql([{ - from: graph.entity('='), - to: graph.entity('-'), - via: graph.entity('v'), - toward: graph.entity('w'), - restriction: graph.entity('r') - }]); - }); - - // U-turns - // Self-intersections - // Split point -}); diff --git a/test/spec/svg/restrictions.js b/test/spec/svg/restrictions.js deleted file mode 100644 index 38f3bf1b9..000000000 --- a/test/spec/svg/restrictions.js +++ /dev/null @@ -1,151 +0,0 @@ -describe("iD.svg.Restrictions", function() { - var restrictions = iD.svg.Restrictions({}); - - describe("#turns", function() { - it("returns an empty array with no selection", function() { - var graph = iD.Graph(); - expect(restrictions.turns(graph, [])).to.eql([]); - }); - - it("returns an empty array with a multiselection", function() { - var graph = iD.Graph(); - expect(restrictions.turns(graph, ['a', 'b'])).to.eql([]); - }); - - var valid = iD.Graph([ - iD.Node({id: 'u'}), - iD.Node({id: 'v'}), - iD.Node({id: 'w'}), - iD.Way({id: 'f', nodes: ['u', 'v']}), - iD.Way({id: 't', nodes: ['v', 'w']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - { role: 'via', id: 'v', type: 'node' }, - { role: 'from', id: 'f', type: 'way' }, - { role: 'to', id: 't', type: 'way' } - ]}) - ]); - - it("returns a valid restriction when the selected way has role 'from'", function() { - expect(restrictions.turns(valid, ['f'])).to.eql([valid.entity('r')]); - }); - - it("returns an empty array when the selected way has role 'to'", function() { - expect(restrictions.turns(valid, ['t'])).to.eql([]); - }); - - it("ignores restrictions missing a 'to' role", function() { - var graph = valid.replace(valid.entity('r').removeMembersWithID('t')); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions with an incomplete 'to' role", function() { - var graph = valid.remove(valid.entity('t')); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions missing a 'via' role", function() { - var graph = valid.replace(valid.entity('r').removeMembersWithID('v')); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions with an incomplete 'via' role", function() { - var graph = valid.remove(valid.entity('v')); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions whose 'from' role is not a way", function() { - var graph = valid.replace(iD.Node({id: 'f2'})) - .replace(valid.entity('r').replaceMember({id: 'f'}, {id: 'f2', type: 'node'})); - expect(restrictions.turns(graph, ['f2'])).to.eql([]); - }); - - it("ignores restrictions whose 'to' role is not a way", function() { - var graph = valid.replace(iD.Node({id: 't2'})) - .replace(valid.entity('r').replaceMember({id: 't'}, {id: 't2', type: 'node'})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions whose 'via' role is not a node", function() { - var graph = valid.replace(iD.Way({id: 'v2'})) - .replace(valid.entity('r').replaceMember({id: 'v'}, {id: 'v2', type: 'way'})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions whose 'from' role does not start or end with the via node", function() { - var graph = valid.replace(valid.entity('f').update({nodes: ['o']})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions whose 'to' role does not start or end with the via node", function() { - var graph = valid.replace(valid.entity('t').update({nodes: ['o']})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions whose 'from' role has less than two nodes", function() { - var graph = valid.replace(valid.entity('f').update({nodes: ['v']})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restrictions whose 'to' role has less than two nodes", function() { - var graph = valid.replace(valid.entity('t').update({nodes: ['v']})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - - it("ignores restriction subtypes", function() { - var graph = valid.replace(valid.entity('r').update({tags: {type: 'restriction:hgv'}})); - expect(restrictions.turns(graph, ['f'])).to.eql([]); - }); - }); - - describe("#datum", function() { - function projection(x) { return x; } - - it("calculates the angle of a forward 'to' role", function() { - // w---x--->y - // | - // u====>v - // From = to - via v - - var graph = iD.Graph([ - iD.Node({id: 'u', loc: [0, 0]}), - iD.Node({id: 'v', loc: [1, 0]}), - iD.Node({id: 'w', loc: [1, 1]}), - iD.Node({id: 'x', loc: [2, 1]}), - iD.Node({id: 'y', loc: [3, 1]}), - iD.Way({id: '=', nodes: ['u', 'v']}), - iD.Way({id: '-', nodes: ['v', 'w', 'x', 'y']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - { role: 'via', id: 'v', type: 'node' }, - { role: 'from', id: '=', type: 'way' }, - { role: 'to', id: '-', type: 'way' } - ]}) - ]); - - expect(restrictions.datum(graph, graph.entity('='), graph.entity('r'), projection).angle).to.eql(Math.PI / 2); - }); - - it("calculates the angle of a reverse 'to' role", function() { - // w<---x---y - // | - // u====>v - // From = to - via v - - var graph = iD.Graph([ - iD.Node({id: 'u', loc: [0, 0]}), - iD.Node({id: 'v', loc: [1, 0]}), - iD.Node({id: 'w', loc: [1, 1]}), - iD.Node({id: 'x', loc: [2, 1]}), - iD.Node({id: 'y', loc: [3, 1]}), - iD.Way({id: '=', nodes: ['u', 'v']}), - iD.Way({id: '-', nodes: ['y', 'x', 'w', 'v']}), - iD.Relation({id: 'r', tags: {type: 'restriction'}, members: [ - { role: 'via', id: 'v', type: 'node' }, - { role: 'from', id: '=', type: 'way' }, - { role: 'to', id: '-', type: 'way' } - ]}) - ]); - - expect(restrictions.datum(graph, graph.entity('='), graph.entity('r'), projection).angle).to.eql(Math.PI / 2); - }); - }); -});