diff --git a/Makefile b/Makefile index 2819f2884..788eef43e 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ all: \ js/lib/d3.keybinding.js \ js/lib/d3.one.js \ js/lib/d3.size.js \ + js/lib/d3.tail.js \ js/lib/d3.trigger.js \ js/lib/d3.typeahead.js \ js/lib/jxon.js \ @@ -29,6 +30,8 @@ all: \ js/id/oauth.js \ js/id/services/*.js \ js/id/util.js \ + js/id/geo.js \ + js/id/geo/*.js \ js/id/actions.js \ js/id/actions/*.js \ js/id/behavior.js \ diff --git a/css/app.css b/css/app.css index d374eee13..90042557e 100644 --- a/css/app.css +++ b/css/app.css @@ -5,6 +5,7 @@ body { font:normal 12px/1.6666 'Helvetica Neue', Arial, sans-serif; margin:0; padding:0; + min-width: 768px; color:#222; /* text-rendering: optimizeLegibility; */ -webkit-font-smoothing: subpixel-antialiased; @@ -18,7 +19,7 @@ body { } .limiter { - max-width: 1400px; + max-width: 1200px; } div, textarea, input, span, ul, li, ol, a, button { @@ -135,7 +136,7 @@ table.tags, table.tags td, table.tags th { .col3 { float:left; width:25.0000%; } .col4 { float:left; width:33.3333%; } .col5 { float:left; width:41.6666%; } -.col6 { float:left; width:50.0000%; } +.col6 { float:left; width:50.0000%; max-width: 600px; } .col7 { float:left; width:58.3333%; } .col8 { float:left; width:66.6666%; } .col9 { float:left; width:75.0000%; } @@ -163,13 +164,15 @@ ul li { list-style: none;} ul.toggle-list li a { font-weight: bold; + color: #c1c1c1; padding: 10px; - border-top: 1px solid #CCC; + border-top: 1px solid white; display:block; + border-top: 1px solid rgba(0, 0, 0, .5); } -ul.toggle-list li a:hover { - background: #ececec; +ul.toggle-list li a.selected { + color: #333; } ul.toggle-list .icon { @@ -177,20 +180,15 @@ ul.toggle-list .icon { margin-right: 5px; } -a.selected { - color:#222; -} - - ul.link-list li { - float: left; display: inline-block; + float: right; margin-left: 10px; padding-left: 10px; border-left: 1px solid white; } -ul.link-list li:first-child { +ul.link-list li:last-child { border-left: 0; margin-left: 0; padding-left: 0; @@ -198,13 +196,20 @@ ul.link-list li:first-child { /* Utility Classes ------------------------------------------------------- */ +.fillL { + background: white; + color: #333; +} -.fillL { background-color: white;} -.fillL2 { background: #f7f7f7 url(../img/background-pattern-1.png) repeat;} -.fillD { - background-color: #222222; - background-color: rgba(0,0,0,.8); +.fillL2 { + background: #f7f7f7 url(../img/background-pattern-1.png) repeat; + color: #333; +} + +.content, .fillD { + background:rgba(0,0,0,.8); color: white; + border-radius: 4px; } .fl { float: left;} @@ -215,12 +220,6 @@ form.hide { display:none; } -.content { - background-color:#fff; - border-radius: 4px; - border: 1px solid #ccc; -} - .pad1 {padding: 10px;} .pad2 {padding: 20px;} .margin1 {margin: 10px;} @@ -230,8 +229,8 @@ form.hide { button { line-height:20px; - border:1px solid #aaa; - box-shadow: inset 0 0 0px 1px #fff; + position: relative; + border:0; color:#222; background: white; font-weight:bold; @@ -239,32 +238,30 @@ button { display: inline-block; height:40px; cursor:url(../img/cursor-pointer.png) 6 1, auto; + border-radius:4px; } button:hover { background-color: #ececec; } +button.col3:hover { + background: #bde5aa; +} + button.active { - box-shadow: inset 0 0 0px 1px #fff, inset 0 0 6px 1px rgba(0,0,0,.35); cursor:url(../img/cursor-pointing.png) 6 1, auto; } button.active:not([disabled]) { - background-color: #ececec; - box-shadow: inset 0 0 0px 1px #fff, inset 0 0 6px 1px rgba(0,0,0,.25); -} - -button.wide, -button.narrow { - border-radius:4px; + background: #6bc641; + color: white; } button.minor { border-radius:4px; height: 20px; width: 20px; - margin: 5px; border: 0; box-shadow: none; background-color: transparent; @@ -282,14 +279,15 @@ button.centered { .button-wrap { display: inline-block; - padding:10px 0 10px 10px; + padding-right:10px; margin: 0; } .button-wrap button:only-child { width: 100%;} +.button-wrap:last-child { padding-right: 0;} .joined button { - border-right-width: 0; + border-right: 1px solid rgba(0,0,0,.5); border-radius:0; } @@ -298,15 +296,16 @@ button.centered { } .joined button:last-child { - border-right-width: 1px; + border-right-width: 0px; border-radius:0 4px 4px 0; } button.action { - background: #444; + color: white; + background: #7092ff; } button.action:hover { - background: #222; + background: #597BE7; } button.delete { @@ -329,14 +328,31 @@ button.save .count { button.save.has-count .count { display: block; - color: #444; - background: #fff; - border-radius: 0 3px 3px 0; - padding: 9px; - float: right; - margin-left: 10px; - margin-top: -9px; - margin-right: -8px; + position: absolute; + left: 115%; + top: 0; + bottom: 0; + background: rgba(255,255,255,.5); + color: #333; + padding: 10px; + height: 30px; + line-height: 12px; + border-radius: 4px; + margin: auto; +} + +button.save.has-count .count::before { + content: ""; + margin: auto; + width: 0; + height: 0; + position: absolute; + left: -6px; + top: 0; + bottom: 0; + border-top: 6px solid transparent; + border-bottom: 6px solid transparent; + border-right: 6px solid rgba(255,255,255,.5); } button.close { @@ -345,28 +361,19 @@ button.close { right: 10px; } -button .label { - margin-right: 3px; -} - -button.action .label { +button.save .label { + display: inline-block; color: white; - text-shadow: 0 -1px 0 rgba(0,0,0,.25); } button[disabled] { cursor:auto; - background: white; + background: rgba(255,255,255,.5); pointer-events:none; } button[disabled] .label { - color:#ccc; - text-shadow: none; -} - -button[disabled]:hover { - background: white; + color: rgba(0,0,0,.5); } /* Icons */ @@ -387,17 +394,16 @@ button[disabled]:hover { } .icon.icon-pre-text { - margin-right: 3px; + margin-right: 3px; } /* Definitions for every icon */ - -.icon.browse { background-position: 0px 0px;} -.icon.add-point { background-position: -20px 0px;} -.icon.add-line { background-position: -40px 0px;} -.icon.add-area { background-position: -60px 0px;} -.icon.undo { background-position: -80px 0px;} -.icon.redo { background-position: -100px 0px;} +button.active:not([disabled]) .icon.browse { background-position: 0px -20px;} +button.active:not([disabled]) .icon.add-point { background-position: -20px -20px;} +button.active:not([disabled]) .icon.add-line { background-position: -40px -20px;} +button.active:not([disabled]) .icon.add-area { background-position: -60px -20px;} +button.active:not([disabled]) .icon.undo { background-position: -80px -20px;} +button.active:not([disabled]) .icon.redo { background-position: -100px -20px;} .icon.apply { background-position: -120px 0px;} .icon.save { background-position: -140px 0px;} .icon.close { background-position: -160px 0px;} @@ -411,24 +417,21 @@ button[disabled]:hover { .icon.avatar { background-position: -320px 0px;} .icon.nearby { background-position: -340px 0px;} -.fillD .icon.browse { background-position: 0px -20px;} -.fillD .icon.add-point { background-position: -20px -20px;} -.fillD .icon.add-line { background-position: -40px -20px;} -.fillD .icon.add-area { background-position: -60px -20px;} -.fillD .icon.undo { background-position: -80px -20px;} -.fillD .icon.redo { background-position: -100px -20px;} -.fillD .icon.apply { background-position: -120px -20px;} -.fillD .icon.save { background-position: -140px -20px;} -.fillD .icon.close { background-position: -160px -20px;} -.fillD .icon.delete { background-position: -180px -20px;} -.fillD .icon.remove { background-position: -200px -20px;} -.fillD .icon.inspect { background-position: -220px -20px;} -.fillD .icon.zoom-in { background-position: -240px -20px;} -.fillD .icon.zoom-out { background-position: -260px -20px;} -.fillD .icon.geocode { background-position: -280px -20px;} -.fillD .icon.layers { background-position: -300px -20px;} -.fillD .icon.avatar { background-position: -320px -20px;} -.fillD .icon.nearby { background-position: -340px -20px;} +.icon.browse { background-position: 0px 0px;} +.icon.add-point { background-position: -20px 0px;} +.icon.add-line { background-position: -40px 0px;} +.icon.add-area { background-position: -60px 0px;} +.icon.undo { background-position: -80px 0px;} +.icon.redo { background-position: -100px 0px;} + +.close-modal.icon.remove { background-position: -200px -20px;} +.map-control .icon.inspect { background-position: -220px -20px;} +.map-control .icon.zoom-in { background-position: -240px -20px;} +.map-control .icon.zoom-out { background-position: -260px -20px;} +.map-control .icon.geocode { background-position: -280px -20px;} +.map-control .icon.layers { background-position: -300px -20px;} +.fillD .icon.avatar { background-position: -320px -20px;} +.fillD .icon.nearby { background-position: -340px -20px;} button[disabled] .icon.browse { background-position: 0px -40px;} button[disabled] .icon.add-point { background-position: -20px -40px;} @@ -455,7 +458,7 @@ button[disabled] .icon.nearby { background-position: -340px -40px;} .icon.big-vertex { background-position: -120px -80px;} .icon.big-inspect { background-position: -160px -80px;} -/* Toggle Icon is special */ +/* Toggle icon is special */ .toggle.icon { background-position: 0px -180px;} a:hover .toggle.icon { background-position: -20px -180px;} .selected .toggle.icon, @@ -465,12 +468,12 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} ------------------------------------------------------- */ #bar { - border-bottom:1px solid #ccc; position:absolute; left:0px; top:0px; right:0; height:60px; + border-radius: 0; } /* Status box */ @@ -485,14 +488,13 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} opacity:0; display:none; padding-left: 10px; - max-width: 600px; + max-width: 500px; } .inspector { - border-left: 1px solid #ccc; - border-bottom: 1px solid #ccc; min-height: 60px; position: relative; + border-radius: 0 0 0 4px; } .inspector-inner { @@ -500,8 +502,6 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} } .inspector-inner.head { - border-bottom: 1px solid #ccc; - background:#fff; z-index:1; position:relative; } @@ -580,17 +580,8 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} right: 30px; } -.inspector-buttons { - border-top: 1px solid #ccc; - padding-right: 10px; -} - .inspector-buttons .button-wrap { - width: 50%; -} - -.inspector-buttons .button-wrap { - width: 50%; + width: 40%; } .inspector-inner .add-tag-row { @@ -609,12 +600,22 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} /* Map Controls */ .map-control { - left:10px; + left:0px; position:absolute; } .map-control button { width: 40px; + border-radius: 0 4px 4px 0; + background: rgba(0, 0, 0, .8); +} + +.map-control button:hover { + background: rgba(0, 0, 0, .9); +} + +.map-control button.active:hover { + background: #6bc641; } .map-overlay { @@ -623,6 +624,7 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} left:50px; top:0; display: block; + border-radius: 4px; } /* Zoomer */ @@ -633,12 +635,13 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} } .zoombuttons button.zoom-in { - border-radius:4px 4px 0 0; + border-radius:0 4px 0 0; + border-bottom: 1px solid rgba(0, 0, 0, .5); } .zoombuttons button.zoom-out { border-top:0; - border-radius:0 0 4px 4px; + border-radius:0 0 4px 0; } /* Layer Switcher */ @@ -647,28 +650,24 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} top:210px; } -.layerswitcher-control .adjustments { - padding:5px; - opacity:0.2; -} - -.layerswitcher-control .adjustments:hover { - opacity:1; -} - -.layerswitcher-control .adjustments .reset { +.layerswitcher-control .adjustments button { + opacity:0.5; height:20px; font-size:10px; font-weight:normal; - padding:0 5px; + padding:0 5px 3px 5px; + background: white; + border: 1px solid #ddd; + border-radius: 0; +} + +.layerswitcher-control .adjustments button:hover { + opacity: 1; } .layerswitcher-control .nudge { - height:20px; width:20px; - font-size:10px; margin-right:2px; - font-weight:normal; } .opacity-options-wrapper { @@ -676,7 +675,6 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} } .opacity-options { - border:1px solid #b0b0b0; background: url(../img/background-pattern-opacity.png) 0 0 repeat; height:20px; width:62px; @@ -695,14 +693,14 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} .opacity-options li .select-box{ position: absolute; width:20px; - height:18px; + height:20px; z-index: 9999; } .layerswitcher-control li:hover .select-box, .layerswitcher-control li.selected .select-box { - border: 2px solid #4672ff; - background: rgba(70, 114, 255, .5); + border: 2px solid #6bc641; + background: rgba(107, 198, 65, .5); opacity: .5; } .layerswitcher-control li.selected:hover .select-box, @@ -714,7 +712,7 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} background:#222; display:inline-block; width:20px; - height:18px; + height:20px; } /* Geocoder */ @@ -740,7 +738,7 @@ a.selected:hover .toggle.icon { background-position: -40px -180px;} display:block; position:absolute; overflow:hidden; - top:60px; + top:0px; left:0; right:0; bottom:0; @@ -765,29 +763,18 @@ img.tile { ------------------------------------------------------- */ .about-block { - float: right; height: 40px; -} - -#about { - border-radius:3px 0 0 0; -} - -#attrib-container { position: absolute; right:0px; bottom:0px; -} - -#user-list { - margin-right: 8px; - border-radius:3px 3px 0 0; - max-width: 400px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + border-radius: 0; } +#about { text-align: right;} + #user-list a:not(:last-child):after { content: ', '; } @@ -795,10 +782,7 @@ img.tile { /* Account Information */ .user-container { - position:absolute; - left:0px; - bottom:0px; - border-radius:0 3px 0 0; + float: left; } .user-container .logout { @@ -851,6 +835,7 @@ div.typeahead a:first-child { position:absolute; width: 50%; left: 25%; + max-width: 600px; top:80px; z-index: 3; } @@ -865,15 +850,16 @@ div.typeahead a:first-child { text-align: center; } -.modal button { margin-bottom: 0;} -.modal button:first-child { margin-left: 0;} - .modal button.close-modal { float:right; - margin-right:10px; - margin-top:10px; + position: absolute; + right:5px; + top:5px; border:0; } +.modal button.close-modal:hover { + background-color: transparent; +} .shaded { z-index: 2; @@ -918,10 +904,18 @@ div.typeahead a:first-child { .modal-section { padding: 20px; - border-bottom: 1px solid #ccc; + width: 100%; } -.modal-section:last-child { border-bottom: 0;} +.modal-section:first-child { + border-radius: 4px 4px 0 0; +} + +.modal-section:last-child { + border-radius: 0 0 4px 4px; +} + +.modal-section .buttons { padding-top: 20px;} .modal-section img.wiki-image { max-width: 400px; @@ -980,18 +974,16 @@ div.typeahead a:first-child { .tooltip { white-space: normal; position: absolute; + left: 0; right: 0; margin: auto; z-index: -1000; height: 0; padding: 5px; opacity: 0; display: block; - filter: alpha(opacity=0); - visibility: visible; } .tooltip.in { opacity: 0.8; - filter: alpha(opacity=80); z-index: 1030; height: auto; } @@ -1013,16 +1005,14 @@ div.typeahead a:first-child { } .tooltip-inner { - max-width: 200px; - min-width: 100px; + text-align: left; + width: 200px; font-size: 11px; font-weight: bold; line-height: 20px; padding: 5px 10px; - color: #ffffff; - text-align: center; - text-decoration: none; - background-color: #000000; + color: #333; + background-color: white; -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; @@ -1040,7 +1030,7 @@ div.typeahead a:first-child { bottom: 0; left: 50%; margin-left: -5px; - border-top-color: #000000; + border-top-color: white; border-width: 5px 5px 0; } @@ -1048,7 +1038,7 @@ div.typeahead a:first-child { top: 50%; left: 0; margin-top: -5px; - border-right-color: #000000; + border-right-color: white; border-width: 5px 5px 5px 0; } @@ -1056,7 +1046,7 @@ div.typeahead a:first-child { top: 50%; right: 0; margin-top: -5px; - border-left-color: #000000; + border-left-color: white; border-width: 5px 0 5px 5px; } @@ -1064,7 +1054,7 @@ div.typeahead a:first-child { top: 0; left: 50%; margin-left: -5px; - border-bottom-color: #000000; + border-bottom-color: white; border-width: 0 5px 5px; } @@ -1083,3 +1073,17 @@ div.typeahead a:first-child { -moz-border-radius: 4px; border-radius: 4px; } + +/* Media Queries +------------------------------------------------------- */ + +@media only screen and (max-width: 840px) { + span.label { + display: none; + } + .icon.icon-pre-text { + margin-right: 0px; + } +} + +} diff --git a/css/map.css b/css/map.css index b2929f149..bf600b18d 100644 --- a/css/map.css +++ b/css/map.css @@ -142,29 +142,41 @@ path.stroke.tag-railway-subway { stroke-dasharray: 8,8; } -path.area { +path.area, +path.multipolygon { stroke-width:2; stroke:#fff; fill:#fff; fill-opacity:0.3; } +path.multipolygon { + fill-rule: evenodd; +} + +path.area.member-type-multipolygon { + fill: none; +} + path.area.selected { stroke-width:4 !important; } -path.area.tag-natural { +path.area.tag-natural, +path.multipolygon.tag-natural { stroke: #ADD6A5; fill: #ADD6A5; stroke-width:1; } -path.area.tag-natural-water { +path.area.tag-natural-water, +path.multipolygon.tag-natural-water { stroke: #6382FF; fill: #ADBEFF; } -path.area.tag-building { +path.area.tag-building, +path.multipolygon.tag-building { stroke: #9E176A; stroke-width: 1; fill: #ff6ec7; @@ -173,7 +185,11 @@ path.area.tag-building { path.area.tag-landuse, path.area.tag-natural-wood, path.area.tag-natural-tree, -path.area.tag-natural-grassland { +path.area.tag-natural-grassland, +path.multipolygon.tag-landuse, +path.multipolygon.tag-natural-wood, +path.multipolygon.tag-natural-tree, +path.multipolygon.tag-natural-grassland { stroke: #006B34; stroke-width: 1; fill: #189E59; diff --git a/img/source/sprite.svg b/img/source/sprite.svg index f239c672f..3d2a96c84 100644 --- a/img/source/sprite.svg +++ b/img/source/sprite.svg @@ -39,8 +39,8 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1" - inkscape:cx="150.66428" - inkscape:cy="90.493266" + inkscape:cx="348.93799" + inkscape:cy="154.49862" inkscape:document-units="px" inkscape:current-layer="layer12" showgrid="false" @@ -165,7 +165,7 @@ image/svg+xml - + @@ -175,110 +175,6 @@ id="layer1" transform="translate(-25,-62.362183)" style="display:inline"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + style="color:#000000;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;opacity:1" /> @@ -935,26 +464,6 @@ inkscape:export-xdpi="90" inkscape:export-ydpi="90" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -1260,30 +548,12 @@ - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/img/sprite.png b/img/sprite.png index 2f933d9f8..7aab066d7 100644 Binary files a/img/sprite.png and b/img/sprite.png differ diff --git a/index.html b/index.html index 9db659489..16dce5779 100644 --- a/index.html +++ b/index.html @@ -32,6 +32,9 @@ + + + @@ -40,7 +43,9 @@ + + @@ -108,6 +113,7 @@ + diff --git a/js/id/geo.js b/js/id/geo.js new file mode 100644 index 000000000..06b63ae14 --- /dev/null +++ b/js/id/geo.js @@ -0,0 +1 @@ +iD.geo = {}; diff --git a/js/id/geo/extent.js b/js/id/geo/extent.js new file mode 100644 index 000000000..80bf631c6 --- /dev/null +++ b/js/id/geo/extent.js @@ -0,0 +1,37 @@ +iD.geo.Extent = function (min, max) { + if (!(this instanceof iD.geo.Extent)) return new iD.geo.Extent(min, max); + if (min instanceof iD.geo.Extent) { + return min; + } else if (min && min.length === 2 && min[0].length === 2 && min[1].length === 2) { + this[0] = min[0]; + this[1] = min[1]; + } else { + this[0] = min || [ Infinity, Infinity]; + this[1] = max || min || [-Infinity, -Infinity]; + } +}; + +iD.geo.Extent.prototype = [[], []]; + +_.extend(iD.geo.Extent.prototype, { + extend: function (obj) { + obj = iD.geo.Extent(obj); + return iD.geo.Extent([Math.min(obj[0][0], this[0][0]), + Math.min(obj[0][1], this[0][1])], + [Math.max(obj[1][0], this[1][0]), + Math.max(obj[1][1], this[1][1])]); + }, + + center: function () { + return [(this[0][0] + this[1][0]) / 2, + (this[0][1] + this[1][1]) / 2]; + }, + + intersects: function (obj) { + obj = iD.geo.Extent(obj); + return obj[0][0] <= this[1][0] && + obj[0][1] <= this[1][1] && + obj[1][0] >= this[0][0] && + obj[1][1] >= this[0][1]; + } +}); diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js index afc152589..8c8f9a6fb 100644 --- a/js/id/graph/entity.js +++ b/js/id/graph/entity.js @@ -74,11 +74,7 @@ iD.Entity.prototype = { }, intersects: function(extent, resolver) { - var _extent = this.extent(resolver); - return _extent[0][0] > extent[0][0] && - _extent[1][0] < extent[1][0] && - _extent[0][1] < extent[0][1] && - _extent[1][1] > extent[1][1]; + return this.extent(resolver).intersects(extent); }, hasInterestingTags: function() { diff --git a/js/id/graph/node.js b/js/id/graph/node.js index 9500991fa..fd3a1cd69 100644 --- a/js/id/graph/node.js +++ b/js/id/graph/node.js @@ -2,7 +2,7 @@ iD.Node = iD.Entity.extend({ type: "node", extent: function() { - return [this.loc, this.loc]; + return iD.geo.Extent(this.loc); }, geometry: function() { diff --git a/js/id/graph/relation.js b/js/id/graph/relation.js index 4788ad0d1..7d8a9aa89 100644 --- a/js/id/graph/relation.js +++ b/js/id/graph/relation.js @@ -2,8 +2,16 @@ iD.Relation = iD.Entity.extend({ type: "relation", members: [], - extent: function() { - return [[NaN, NaN], [NaN, NaN]]; + extent: function(resolver) { + return resolver.transient(this, 'extent', function() { + return this.members.reduce(function (extent, member) { + if (member = resolver.entity(member.id)) { + return extent.extend(member.extent(resolver)) + } else { + return extent; + } + }, iD.geo.Extent()); + }); }, geometry: function() { @@ -22,7 +30,7 @@ iD.Relation = iD.Entity.extend({ // multipolygon: function(resolver) { var members = this.members - .filter(function (m) { return m.type === 'way'; }) + .filter(function (m) { return m.type === 'way' && resolver.entity(m.id); }) .map(function (m) { return { role: m.role || 'outer', id: m.id, nodes: resolver.fetch(m.id).nodes }; }); function join(ways) { diff --git a/js/id/graph/validate.js b/js/id/graph/validate.js new file mode 100644 index 000000000..addb774ca --- /dev/null +++ b/js/id/graph/validate.js @@ -0,0 +1,47 @@ +iD.validate = function(changes) { + var warnings = [], change; + + // https://github.com/openstreetmap/josm/blob/mirror/src/org/ + // openstreetmap/josm/data/validation/tests/UnclosedWays.java#L80 + function tagSuggestsArea(change) { + if (_.isEmpty(change.tags)) return false; + var tags = change.tags; + var presence = ['landuse', 'amenities', 'tourism', 'shop']; + for (var i = 0; i < presence.length; i++) { + if (tags[presence[i]] !== undefined) { + return presence[i] + '=' + tags[presence[i]]; + } + } + if (tags.building && tags.building === 'yes') return 'building=yes'; + } + + if (changes.created.length) { + for (var i = 0; i < changes.created.length; i++) { + change = changes.created[i]; + + if (change.geometry() === 'point' && _.isEmpty(change.tags)) { + warnings.push({ + message: 'Untagged point which is not part of a line or area', + entity: change + }); + } + + if (change.geometry() === 'line' && _.isEmpty(change.tags)) { + warnings.push({ message: 'Untagged line', entity: change }); + } + + if (change.geometry() === 'area' && _.isEmpty(change.tags)) { + warnings.push({ message: 'Untagged area', entity: change }); + } + + if (change.geometry() === 'line' && tagSuggestsArea(change)) { + warnings.push({ + message: 'The tag ' + tagSuggestsArea(change) + ' suggests line should be area, but it is not and area', + entity: change + }); + } + } + } + + return warnings.length ? [warnings] : []; +}; diff --git a/js/id/graph/way.js b/js/id/graph/way.js index 5c3e1210b..305f12051 100644 --- a/js/id/graph/way.js +++ b/js/id/graph/way.js @@ -4,14 +4,11 @@ iD.Way = iD.Entity.extend({ extent: function(resolver) { return resolver.transient(this, 'extent', function() { - var extent = [[-Infinity, Infinity], [Infinity, -Infinity]]; + var extent = iD.geo.Extent(); for (var i = 0, l = this.nodes.length; i < l; i++) { var node = this.nodes[i]; if (node.loc === undefined) node = resolver.entity(node); - if (node.loc[0] > extent[0][0]) extent[0][0] = node.loc[0]; - if (node.loc[0] < extent[1][0]) extent[1][0] = node.loc[0]; - if (node.loc[1] < extent[0][1]) extent[0][1] = node.loc[1]; - if (node.loc[1] > extent[1][1]) extent[1][1] = node.loc[1]; + extent = extent.extend(node.loc); } return extent; }); diff --git a/js/id/id.js b/js/id/id.js index e5e6b0eb0..ff85a68d4 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -22,7 +22,8 @@ window.iD = function(container) { .call(map); var bar = container.append('div') - .attr('id', 'bar').attr('class', 'fillL2'); + .attr('id', 'bar') + .attr('class','pad1 fillD'); var limiter = bar.append('div') .attr('class', 'limiter'); @@ -79,27 +80,22 @@ window.iD = function(container) { undo_tooltip = bootstrap.tooltip().placement('bottom'); undo_buttons.append('button') - .attr({ id: 'undo', 'class': 'col6 narrow' }) + .attr({ id: 'undo', 'class': 'col6' }) .property('disabled', true) .html("") .on('click', history.undo) .call(undo_tooltip); undo_buttons.append('button') - .attr({ id: 'redo', 'class': 'col6 narrow' }) + .attr({ id: 'redo', 'class': 'col6' }) .property('disabled', true) .html("") .on('click', history.redo) .call(undo_tooltip); - container.append('div') - .attr('class', 'user-container pad1 fillD about-block') - .append('div') - .attr('class', 'hello'); - var save_button = limiter.append('div').attr('class','button-wrap col1').append('button') - .attr('class', 'save action wide col12') - .call(iD.ui.save().map(map)); + .attr('class', 'save action col12') + .call(iD.ui.save().map(map).controller(controller)); history.on('change.warn-unload', function() { var changes = history.changes(), @@ -122,7 +118,7 @@ window.iD = function(container) { .data([['zoom-in', '+', map.zoomIn, 'Zoom In'], ['zoom-out', '-', map.zoomOut, 'Zoom Out']]) .enter() .append('button') - .attr('class', function(d) { return d[0] + ' narrow'; }) + .attr('class', function(d) { return d[0]; }) .attr('title', function(d) { return d[3]; }) .on('click', function(d) { return d[2](); }) .append('span') @@ -145,22 +141,32 @@ window.iD = function(container) { .style('display', 'none') .attr('class', 'inspector-wrap fr col5'); - var about = container.append('div').attr('id', 'attrib-container'); + var about = container.append('div') + .attr('class','col12 about-block fillD pad1') - about.append('ul') - .attr('id','about') - .attr('class','pad1 fillD about-block link-list') - .html("
  • view code
  • " + + about.append('div') + .attr('class', 'user-container') + .append('div') + .attr('class', 'hello'); + + var aboutList = about.append('ul') + .attr('id','about') + .attr('class','link-list'); + + aboutList.html("
  • view code
  • " + "
  • report a bug
  • " + "
  • imagery provided by bing
  • "); - var contributors = about.append('div') + var contributors = aboutList.append('li') .attr('id', 'user-list') - .attr('class','about-block fillD pad1'); contributors.append('span') .attr('class', 'icon nearby icon-pre-text'); - contributors.append('pan') + contributors.append('span') .text('Viewing contributions by '); + contributors.append('span') + .attr('class', 'contributor-list'); + contributors.append('span') + .attr('class', 'contributor-count'); history.on('change.buttons', function() { var undo = history.undoAnnotation(), @@ -208,8 +214,7 @@ window.iD = function(container) { var hash = iD.Hash().map(map); if (!hash.hadHash) { - map.zoom(20) - .center([-77.02271,38.90085]); + map.centerZoom([-77.02271, 38.90085], 20); } d3.select('.user-container').call(iD.ui.userpanel(connection) diff --git a/js/id/modes/select.js b/js/id/modes/select.js index d091fdc57..4d567fd59 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -59,8 +59,8 @@ iD.modes.Select = function (entity) { map_size = mode.map.size(), entity_extent = entity.extent(mode.history.graph()), left_edge = map_size[0] - inspector_size[0], - left = mode.map.projection(entity_extent[1])[0], - right = mode.map.projection(entity_extent[0])[0]; + left = mode.map.projection(entity_extent[0])[0], + right = mode.map.projection(entity_extent[1])[0]; if (left > left_edge && right > left_edge) mode.map.centerEase( diff --git a/js/id/renderer/hash.js b/js/id/renderer/hash.js index 05691ef59..bb454811b 100644 --- a/js/id/renderer/hash.js +++ b/js/id/renderer/hash.js @@ -1,5 +1,5 @@ iD.Hash = function() { - var hash = {}, + var hash = { hadHash: false }, s0, // cached location.hash lat = 90 - 1e-8, // allowable latitude range map; @@ -10,8 +10,9 @@ iD.Hash = function() { if (args.length < 3 || args.some(isNaN)) { return true; // replace bogus hash } else { - map.zoom(args[0]) - .center([args[2], Math.min(lat, Math.max(-lat, args[1]))]); + map.centerZoom([args[2], + Math.min(lat, Math.max(-lat, args[1]))], + args[0]); } }; @@ -27,12 +28,13 @@ iD.Hash = function() { var move = _.throttle(function() { var s1 = formatter(map); if (s0 !== s1) location.replace(s0 = s1); // don't recenter the map! - }, 1000); + }, 100); function hashchange() { if (location.hash === s0) return; // ignore spurious hashchange events - if (parser(map, (s0 = location.hash).substring(2))) + if (parser(map, (s0 = location.hash).substring(2))) { move(); // replace bogus hash + } } hash.map = function(x) { diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 9edb347e4..729d37936 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -5,6 +5,7 @@ iD.Map = function() { translateStart, keybinding = d3.keybinding(), projection = d3.geo.mercator().scale(1024), + roundedProjection = iD.svg.RoundProjection(projection), zoom = d3.behavior.zoom() .translate(projection.translate()) .scale(projection.scale()) @@ -16,11 +17,12 @@ iD.Map = function() { background = iD.Background() .projection(projection), transformProp = iD.util.prefixCSSProperty('Transform'), - points = iD.svg.Points(), - vertices = iD.svg.Vertices(), - lines = iD.svg.Lines(), - areas = iD.svg.Areas(), - midpoints = iD.svg.Midpoints(), + points = iD.svg.Points(roundedProjection), + vertices = iD.svg.Vertices(roundedProjection), + lines = iD.svg.Lines(roundedProjection), + areas = iD.svg.Areas(roundedProjection), + multipolygons = iD.svg.Multipolygons(roundedProjection), + midpoints = iD.svg.Midpoints(roundedProjection), tail = d3.tail(), surface, tilegroup; @@ -67,26 +69,32 @@ iD.Map = function() { all = graph.intersects(extent); filter = d3.functor(true); } else { - var only = {}, - filterOnly = {}; - for (var j = 0; j < difference.length; j++) { - var id = difference[j], - entity = graph.fetch(id); - // Even if the entity is false (deleted), it needs to be - // removed from the surface - only[id] = entity; - if (entity && entity.intersects(extent, graph)) { - if (only[id].type === 'node') { - var parents = graph.parentWays(only[id]); - for (var k = 0; k < parents.length; k++) { - // Don't re-fetch parents - if (only[parents[k].id] === undefined) { - only[parents[k].id] = graph.fetch(parents[k].id); - } - } + var only = {}; + + function addParents(parents) { + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + if (only[parent.id] === undefined) { + only[parent.id] = graph.fetch(parent.id); + addParents(graph.parentRelations(parent)); } } } + + for (var j = 0; j < difference.length; j++) { + var id = difference[j], + entity = graph.fetch(id); + + // Even if the entity is false (deleted), it needs to be + // removed from the surface + only[id] = entity; + + if (entity && entity.intersects(extent, graph)) { + addParents(graph.parentWays(only[id])); + addParents(graph.parentRelations(only[id])); + } + } + all = _.compact(_.values(only)); filter = function(d) { return d.midpoint ? d.way in only : d.id in only; }; } @@ -97,11 +105,12 @@ iD.Map = function() { } surface - .call(points, graph, all, filter, projection) - .call(vertices, graph, all, filter, projection) - .call(lines, graph, all, filter, projection) - .call(areas, graph, all, filter, projection) - .call(midpoints, graph, all, filter, projection); + .call(points, graph, all, filter) + .call(vertices, graph, all, filter) + .call(lines, graph, all, filter) + .call(areas, graph, all, filter) + .call(multipolygons, graph, all, filter) + .call(midpoints, graph, all, filter); } function editOff() { @@ -152,7 +161,7 @@ iD.Map = function() { redraw(); } - function redraw(difference) { + var redraw = _.throttle(function(difference) { dispatch.move(map); surface.attr('data-zoom', ~~map.zoom()); tilegroup.call(background); @@ -163,7 +172,7 @@ iD.Map = function() { editOff(); } return map; - } + }, 10); function pointLocation(p) { var translate = projection.translate(), @@ -198,10 +207,7 @@ iD.Map = function() { return map; }; - map.zoom = function(z) { - if (!arguments.length) { - return Math.max(Math.log(projection.scale()) / Math.LN2 - 8, 0); - } + function setZoom(z) { var scale = 256 * Math.pow(2, z), center = pxCenter(), l = pointLocation(center); @@ -214,8 +220,17 @@ iD.Map = function() { t[1] += center[1] - l[1]; projection.translate(t); zoom.translate(projection.translate()); - return redraw(); - }; + } + + function setCenter(loc) { + var t = projection.translate(), + c = pxCenter(), + ll = projection(loc); + projection.translate([ + t[0] - ll[0] + c[0], + t[1] - ll[1] + c[1]]); + zoom.translate(projection.translate()); + } map.size = function(_) { if (!arguments.length) return dimensions; @@ -235,17 +250,25 @@ iD.Map = function() { if (!arguments.length) { return projection.invert(pxCenter()); } else { - var t = projection.translate(), - c = pxCenter(), - ll = projection(loc); - projection.translate([ - t[0] - ll[0] + c[0], - t[1] - ll[1] + c[1]]); - zoom.translate(projection.translate()); + setCenter(loc); return redraw(); } }; + map.zoom = function(z) { + if (!arguments.length) { + return Math.max(Math.log(projection.scale()) / Math.LN2 - 8, 0); + } + setZoom(z); + return redraw(); + }; + + map.centerZoom = function(loc, z) { + setCenter(loc); + setZoom(z); + return redraw(); + }; + map.centerEase = function(loc) { var from = map.center().slice(), t = 0; d3.timer(function() { @@ -254,26 +277,23 @@ iD.Map = function() { }, 20); }; - map.extent = function(tl, br) { + map.extent = function(_) { if (!arguments.length) { - return [projection.invert([0, 0]), projection.invert(dimensions)]; + return iD.geo.Extent(projection.invert([0, dimensions[1]]), + projection.invert([dimensions[0], 0])); } else { - - var TL = projection(tl), - BR = projection(br); + var extent = iD.geo.Extent(_), + tl = projection([extent[0][0], extent[1][1]]), + br = projection([extent[1][0], extent[0][1]]); // Calculate maximum zoom that fits extent - var hFactor = (BR[0] - TL[0]) / dimensions[0], - vFactor = (BR[1] - TL[1]) / dimensions[1], + var hFactor = (br[0] - tl[0]) / dimensions[0], + vFactor = (br[1] - tl[1]) / dimensions[1], hZoomDiff = Math.log(Math.abs(hFactor)) / Math.LN2, vZoomDiff = Math.log(Math.abs(vFactor)) / Math.LN2, newZoom = map.zoom() - Math.max(hZoomDiff, vZoomDiff); - // Calculate center of projected extent - var midPoint = [(TL[0] + BR[0]) / 2, (TL[1] + BR[1]) / 2], - midLoc = projection.invert(midPoint); - - map.zoom(newZoom).center(midLoc); + map.centerZoom(extent.center(), newZoom); } }; diff --git a/js/id/svg.js b/js/id/svg.js index 5860126e9..e2b1dc2ad 100644 --- a/js/id/svg.js +++ b/js/id/svg.js @@ -6,9 +6,24 @@ iD.svg = { }, PointTransform: function (projection) { - projection = iD.svg.RoundProjection(projection); return function (entity) { return 'translate(' + projection(entity.loc) + ')'; }; + }, + + LineString: function (projection) { + var cache = {}; + return function (entity) { + if (cache[entity.id] !== undefined) { + return cache[entity.id]; + } + + if (entity.nodes.length === 0) { + return (cache[entity.id] = null); + } + + return (cache[entity.id] = + 'M' + entity.nodes.map(function (n) { return projection(n.loc); }).join('L')); + } } }; diff --git a/js/id/svg/areas.js b/js/id/svg/areas.js index ff24afecc..5a3c31288 100644 --- a/js/id/svg/areas.js +++ b/js/id/svg/areas.js @@ -1,4 +1,4 @@ -iD.svg.Areas = function() { +iD.svg.Areas = function(projection) { var area_stack = { building: 0, @@ -26,7 +26,7 @@ iD.svg.Areas = function() { return as - bs; } - return function drawAreas(surface, graph, entities, filter, projection) { + return function drawAreas(surface, graph, entities, filter) { var areas = []; for (var i = 0; i < entities.length; i++) { @@ -38,20 +38,10 @@ iD.svg.Areas = function() { areas.sort(areastack); - var lineStrings = {}; - - function lineString(entity) { - if (lineStrings[entity.id] !== undefined) { - return lineStrings[entity.id]; - } - var nodes = _.pluck(entity.nodes, 'loc'); - if (nodes.length === 0) return (lineStrings[entity.id] = ''); - else return (lineStrings[entity.id] = - 'M' + nodes.map(iD.svg.RoundProjection(projection)).join('L')); - } + var lineString = iD.svg.LineString(projection); function drawPaths(group, areas, filter, classes) { - var paths = group.selectAll('path') + var paths = group.selectAll('path.area') .filter(filter) .data(areas, iD.Entity.key); @@ -62,7 +52,8 @@ iD.svg.Areas = function() { paths .order() .attr('d', lineString) - .call(iD.svg.TagClasses()); + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); paths.exit() .remove(); diff --git a/js/id/svg/lines.js b/js/id/svg/lines.js index 3f1b634d7..375a88f05 100644 --- a/js/id/svg/lines.js +++ b/js/id/svg/lines.js @@ -1,4 +1,4 @@ -iD.svg.Lines = function() { +iD.svg.Lines = function(projection) { var arrowtext = '►\u3000\u3000', alength; @@ -33,27 +33,27 @@ iD.svg.Lines = function() { return as - bs; } - function drawPaths(group, lines, filter, classes, lineString) { - var paths = group.selectAll('path') - .filter(filter) - .data(lines, iD.Entity.key); + return function drawLines(surface, graph, entities, filter) { + function drawPaths(group, lines, filter, classes, lineString) { + var paths = group.selectAll('path') + .filter(filter) + .data(lines, iD.Entity.key); - paths.enter() - .append('path') - .attr('class', classes); + paths.enter() + .append('path') + .attr('class', classes); - paths - .order() - .attr('d', lineString) - .call(iD.svg.TagClasses()); + paths + .order() + .attr('d', lineString) + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); - paths.exit() - .remove(); + paths.exit() + .remove(); - return paths; - } - - return function drawLines(surface, graph, entities, filter, projection) { + return paths; + } if (!alength) { var arrow = surface.append('text').text(arrowtext); @@ -73,15 +73,7 @@ iD.svg.Lines = function() { lines.sort(waystack); - function lineString(entity) { - if (lineStrings[entity.id] !== undefined) { - return lineStrings[entity.id]; - } - var nodes = _.pluck(entity.nodes, 'loc'); - if (nodes.length === 0) return (lineStrings[entity.id] = ''); - else return (lineStrings[entity.id] = - 'M' + nodes.map(iD.svg.RoundProjection(projection)).join('L')); - } + var lineString = iD.svg.LineString(projection); var casing = surface.select('.layer-casing'), stroke = surface.select('.layer-stroke'), diff --git a/js/id/svg/member_classes.js b/js/id/svg/member_classes.js new file mode 100644 index 000000000..b675288c3 --- /dev/null +++ b/js/id/svg/member_classes.js @@ -0,0 +1,32 @@ +iD.svg.MemberClasses = function(graph) { + var tagClassRe = /^member-?/; + + return function memberClassesSelection(selection) { + selection.each(function memberClassesEach(d, i) { + var classes, value = this.className; + + if (value.baseVal !== undefined) value = value.baseVal; + + classes = value.trim().split(/\s+/).filter(function(name) { + return name.length && !tagClassRe.test(name); + }).join(' '); + + var relations = graph.parentRelations(d); + + if (relations.length) { + classes += ' member'; + } + + relations.forEach(function (relation) { + classes += ' member-type-' + relation.tags.type; + classes += ' member-role-' + _.find(relation.members, function (member) { return member.id == d.id; }).role; + }); + + classes = classes.trim(); + + if (classes !== value) { + d3.select(this).attr('class', classes); + } + }); + }; +}; diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js index 9c060ea3b..321e75c8e 100644 --- a/js/id/svg/midpoints.js +++ b/js/id/svg/midpoints.js @@ -1,5 +1,5 @@ -iD.svg.Midpoints = function() { - return function drawMidpoints(surface, graph, entities, filter, projection) { +iD.svg.Midpoints = function(projection) { + return function drawMidpoints(surface, graph, entities, filter) { var midpoints = []; for (var i = 0; i < entities.length; i++) { diff --git a/js/id/svg/multipolygons.js b/js/id/svg/multipolygons.js new file mode 100644 index 000000000..6330cf685 --- /dev/null +++ b/js/id/svg/multipolygons.js @@ -0,0 +1,55 @@ +iD.svg.Multipolygons = function(projection) { + return function(surface, graph, entities, filter) { + var multipolygons = []; + + for (var i = 0; i < entities.length; i++) { + var entity = entities[i]; + if (entity.geometry() === 'relation' && entity.tags.type === 'multipolygon') { + multipolygons.push(entity); + } + } + + var lineStrings = {}; + + function lineString(entity) { + if (lineStrings[entity.id] !== undefined) { + return lineStrings[entity.id]; + } + + var multipolygon = entity.multipolygon(graph); + if (entity.members.length == 0 || !multipolygon) { + return (lineStrings[entity.id] = null); + } + + multipolygon = _.flatten(multipolygon, true); + return (lineStrings[entity.id] = + multipolygon.map(function (ring) { + return 'M' + ring.map(function (node) { return projection(node.loc); }).join('L'); + }).join("")); + } + + function drawPaths(group, multipolygons, filter, classes) { + var paths = group.selectAll('path.multipolygon') + .filter(filter) + .data(multipolygons, iD.Entity.key); + + paths.enter() + .append('path') + .attr('class', classes); + + paths + .order() + .attr('d', lineString) + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); + + paths.exit() + .remove(); + + return paths; + } + + var fill = surface.select('.layer-fill'), + paths = drawPaths(fill, multipolygons, filter, 'relation multipolygon'); + }; +}; diff --git a/js/id/svg/points.js b/js/id/svg/points.js index 7a2600814..5862a5cb9 100644 --- a/js/id/svg/points.js +++ b/js/id/svg/points.js @@ -1,4 +1,4 @@ -iD.svg.Points = function() { +iD.svg.Points = function(projection) { function imageHref(d) { // TODO: optimize for (var k in d.tags) { @@ -10,7 +10,7 @@ iD.svg.Points = function() { return 'icons/unknown.png'; } - return function drawPoints(surface, graph, entities, filter, projection) { + return function drawPoints(surface, graph, entities, filter) { var points = []; for (var i = 0; i < entities.length; i++) { @@ -45,7 +45,8 @@ iD.svg.Points = function() { .attr('transform', 'translate(-8, -8)'); groups.attr('transform', iD.svg.PointTransform(projection)) - .call(iD.svg.TagClasses()); + .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)); // Selecting the following implicitly // sets the data (point entity) on the element diff --git a/js/id/svg/tag_classes.js b/js/id/svg/tag_classes.js index 43562fd5c..0295ca7e6 100644 --- a/js/id/svg/tag_classes.js +++ b/js/id/svg/tag_classes.js @@ -1,7 +1,7 @@ iD.svg.TagClasses = function() { var keys = iD.util.trueObj([ 'highway', 'railway', 'motorway', 'amenity', 'natural', - 'landuse', 'building', 'oneway', 'bridge' + 'landuse', 'building', 'oneway', 'bridge', 'boundary' ]), tagClassRe = /^tag-/; return function tagClassesSelection(selection) { diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js index 700ef5538..ce81dface 100644 --- a/js/id/svg/vertices.js +++ b/js/id/svg/vertices.js @@ -1,5 +1,5 @@ -iD.svg.Vertices = function() { - return function drawVertices(surface, graph, entities, filter, projection) { +iD.svg.Vertices = function(projection) { + return function drawVertices(surface, graph, entities, filter) { var vertices = []; for (var i = 0; i < entities.length; i++) { @@ -31,6 +31,7 @@ iD.svg.Vertices = function() { groups.attr('transform', iD.svg.PointTransform(projection)) .call(iD.svg.TagClasses()) + .call(iD.svg.MemberClasses(graph)) .classed('shared', function(entity) { return graph.parentWays(entity).length > 1; }); // Selecting the following implicitly diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index 547f4505f..7fe7c3355 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -1,5 +1,5 @@ iD.ui.commit = function() { - var event = d3.dispatch('cancel', 'save'); + var event = d3.dispatch('cancel', 'save', 'fix'); function zipSame(d) { var c = [], n = -1; @@ -28,7 +28,7 @@ iD.ui.commit = function() { var changes = selection.datum(), connection = changes.connection, user = connection.user(), - header = selection.append('div').attr('class', 'header modal-section'), + header = selection.append('div').attr('class', 'header modal-section fillL'), body = selection.append('div').attr('class', 'body'); @@ -54,42 +54,74 @@ iD.ui.commit = function() { .append('div') .text(user.display_name); - header.append('h2').text('Upload Changes to OpenStreetMap'); + header.append('h2').text('Save Changes'); header.append('p').text('The changes you upload will be visible on all maps that use OpenStreetMap data.'); - var commit = body.append('div').attr('class','modal-section'); - commit.append('textarea') - .attr('class', 'changeset-comment') - .attr('placeholder', 'Brief Description of your contributions'); + // Comment Box + var comment_section = body.append('div').attr('class','modal-section cf'); + comment_section.append('textarea') + .attr('class', 'changeset-comment') + .attr('placeholder', 'Brief Description of your contributions'); - var buttonwrap = commit.append('div') - .attr('class', 'buttons'); + // Confirm / Cancel Buttons + var buttonwrap = comment_section.append('div') + .attr('class', 'buttons') + .append('div') + .attr('class', 'button-wrap joined col4'); - var savebutton = buttonwrap.append('button') - .attr('class', 'action wide') - .on('click.save', function() { - event.save({ - comment: d3.select('textarea.changeset-comment').node().value + var savebutton = buttonwrap + .append('button') + .attr('class', 'action col6 button') + .on('click.save', function() { + event.save({ + comment: d3.select('textarea.changeset-comment').node().value + }); }); - }); - savebutton.append('span').attr('class','icon save icon-pre-text'); - savebutton.append('span').attr('class','label').text('Save'); + savebutton.append('span').attr('class','icon save icon-pre-text'); + savebutton.append('span').attr('class','label').text('Save'); var cancelbutton = buttonwrap.append('button') - .attr('class', 'cancel wide') - .on('click.cancel', function() { - event.cancel(); - }); - cancelbutton.append('span').attr('class','icon close icon-pre-text'); - cancelbutton.append('span').attr('class','label').text('Cancel'); + .attr('class', 'cancel col6 button') + .on('click.cancel', function() { + event.cancel(); + }); + cancelbutton.append('span').attr('class','icon close icon-pre-text'); + cancelbutton.append('span').attr('class','label').text('Cancel'); + + var warnings = body.selectAll('div.warning-section') + .data(iD.validate(changes)) + .enter() + .append('div').attr('class', 'modal-section warning-section fillL'); + + warnings.append('h3') + .text('Warnings'); + + var warning_li = warnings.append('ul') + .attr('class', 'changeset-list') + .selectAll('li') + .data(function(d) { return d; }) + .enter() + .append('li'); + + warning_li.append('button') + .attr('class', 'minor') + .on('click', event.fix) + .append('span') + .attr('class', 'icon inspect'); + + warning_li.append('strong').text(function(d) { + return d.message; + }); var section = body.selectAll('div.commit-section') .data(['modified', 'deleted', 'created'].filter(changesLength)) .enter() .append('div').attr('class', 'commit-section modal-section fillL2'); - section.append('h3').text(String) + section.append('h3').text(function(d) { + return d.charAt(0).toUpperCase() + d.slice(1); + }) .append('small') .attr('class', 'count') .text(changesLength); diff --git a/js/id/ui/confirm.js b/js/id/ui/confirm.js index 11515996e..a3b97f5be 100644 --- a/js/id/ui/confirm.js +++ b/js/id/ui/confirm.js @@ -7,7 +7,7 @@ iD.ui.confirm = function() { .attr('class', 'description'); var nochanges = modal.select('.content') .append('button') - .attr('class','wide action centered') + .attr('class','action centered') .on('click.confirm', function() { modal.remove(); }); diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index 72dedf1d1..c0036a677 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -3,22 +3,48 @@ iD.ui.contributors = function(map) { function contributors(selection) { var users = {}, + limit = 3, entities = map.history().graph().intersects(map.extent()); + for (var i in entities) { - if (entities[i].user) { - users[entities[i].user] = true; - if (Object.keys(users).length > 10) break; - } + if (entities[i].user) users[entities[i].user] = true; } - var u = Object.keys(users); - var l = selection.selectAll('a.user-link').data(u); + + var u = Object.keys(users), + subset = u.slice(0, limit); + + var l = selection + .select('.contributor-list') + .selectAll('a.user-link') + .data(subset); + + l.enter().append('a') .attr('class', 'user-link') .attr('href', function(d) { return map.connection().userUrl(d); }) .attr('target', '_blank') .text(String); + l.exit().remove(); + selection + .select('.contributor-count') + .html(''); + + if (u.length > limit) { + selection + .select('.contributor-count') + .append('a') + .attr('target', '_blank') + .attr('href', function() { + var ext = map.extent(); + return 'http://www.openstreetmap.org/browse/changesets?bbox=' + [ + ext[0][0], ext[0][1], + ext[1][0], ext[1][1]]; + }) + .text(' and ' + (u.length - limit) + ' others'); + } + if (!u.length) { selection.transition().style('opacity', 0); } else if (selection.style('opacity') === '0') { diff --git a/js/id/ui/geocoder.js b/js/id/ui/geocoder.js index 87eaa7d74..41d7214e9 100644 --- a/js/id/ui/geocoder.js +++ b/js/id/ui/geocoder.js @@ -16,7 +16,7 @@ iD.ui.geocoder = function() { .text('No location found for "' + resp.query[0] + '"'); } var bounds = resp.results[0][0].bounds; - map.extent([bounds[0], bounds[3]], [bounds[2], bounds[1]]); + map.extent(iD.geo.Extent([bounds[0], bounds[1]], [bounds[2], bounds[3]])); }); } @@ -41,7 +41,6 @@ iD.ui.geocoder = function() { } var button = selection.append('button') - .attr('class', 'narrow') .attr('title', 'Find A Location') .html('') .on('click', toggle); diff --git a/js/id/ui/geolocate.js b/js/id/ui/geolocate.js index 906c031c4..2e5b48cca 100644 --- a/js/id/ui/geolocate.js +++ b/js/id/ui/geolocate.js @@ -10,7 +10,6 @@ iD.ui.geolocate = function(map) { selection .attr('class', 'geolocate-control map-control') .append('button') - .attr('class', 'narrow') .attr('title', 'Show My Location') .text('G') .on('click', function() { diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index 3aca8caca..192066fda 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -7,19 +7,11 @@ iD.ui.inspector = function() { function inspector(selection) { var entity = selection.datum(); - selection.html('').append('button') - .attr('class', 'narrow close') - .html("") - .on('click', function() { - event.close(entity); - }); - - var inspector = selection.append('div') - .attr('class','inspector fillL'); + .attr('class','inspector content'); inspector.append('div') - .attr('class', 'head inspector-inner') + .attr('class', 'head inspector-inner fillL') .call(drawHead); var inspectorbody = inspector.append('div') @@ -47,7 +39,9 @@ iD.ui.inspector = function() { drawTags(entity.tags); inspectorbody.append('div') - .attr('class', 'inspector-buttons') + .attr('class', 'inspector-buttons pad1') + .append('div') + .attr('class','button-wrap joined') .call(drawButtons); } @@ -83,19 +77,15 @@ iD.ui.inspector = function() { } function drawButtons(selection) { - var inspectorButton1 = selection.append('div') - .attr('class', 'button-wrap') - .append('button') - .attr('class', 'apply wide action') + var inspectorButton1 = selection.append('button') + .attr('class', 'apply col6 action') .on('click', apply); inspectorButton1.append('span').attr('class','icon icon-pre-text apply'); inspectorButton1.append('span').attr('class','label').text('Apply'); - var inspectorButton2 = selection.append('div') - .attr('class', 'button-wrap') - .append('button') - .attr('class', 'delete wide action') + var inspectorButton2 = selection.append('button') + .attr('class', 'delete col6 action') .on('click', function(entity) { event.remove(entity); }); inspectorButton2.append('span').attr('class','icon icon-pre-text delete'); @@ -126,12 +116,14 @@ iD.ui.inspector = function() { inputs.append('input') .property('type', 'text') .attr('class', 'key') + .attr('maxlength', 255) .property('value', function(d) { return d.key; }) .on('change', function(d) { d.key = this.value; }); inputs.append('input') .property('type', 'text') .attr('class', 'value') + .attr('maxlength', 255) .property('value', function(d) { return d.value; }) .on('change', function(d) { d.value = this.value; }) .on('keydown.push-more', pushMore); @@ -283,7 +275,7 @@ iD.ui.inspector = function() { inspector.tags = function (tags) { if (!arguments.length) { - var tags = {}; + tags = {}; tagList.selectAll('li').each(function() { var row = d3.select(this), key = row.selectAll('.key').property('value'), diff --git a/js/id/ui/layerswitcher.js b/js/id/ui/layerswitcher.js index a2acad1b5..82f85f82b 100644 --- a/js/id/ui/layerswitcher.js +++ b/js/id/ui/layerswitcher.js @@ -56,7 +56,7 @@ iD.ui.layerswitcher = function(map) { var opa = content .append('div') - .attr('class', 'opacity-options-wrapper fillL2'); + .attr('class', 'opacity-options-wrapper'); opa.append('h4').text('Layers'); @@ -102,7 +102,7 @@ iD.ui.layerswitcher = function(map) { content .append('ul') - .attr('class', 'toggle-list') + .attr('class', 'toggle-list fillL') .selectAll('a.layer') .data(sources) .enter() @@ -135,7 +135,7 @@ iD.ui.layerswitcher = function(map) { var adjustments = content .append('div') - .attr('class', 'adjustments'); + .attr('class', 'adjustments pad1'); var directions = [ ['←', [-1, 0]], diff --git a/js/id/ui/save.js b/js/id/ui/save.js index 95561da31..e711e4bde 100644 --- a/js/id/ui/save.js +++ b/js/id/ui/save.js @@ -1,6 +1,6 @@ iD.ui.save = function() { - var map; + var map, controller; function save(selection) { @@ -59,6 +59,12 @@ iD.ui.save = function() { .on('cancel', function() { modal.remove(); }) + .on('fix', function(d) { + map.extent(d.entity.extent(map.history().graph())); + if (map.zoom() > 19) map.zoom(19); + controller.enter(iD.modes.Select(d.entity)); + modal.remove(); + }) .on('save', commit)); }); } else { @@ -91,5 +97,11 @@ iD.ui.save = function() { return save; }; + save.controller = function(_) { + if (!arguments.length) return controller; + controller = _; + return save; + }; + return save; }; diff --git a/js/id/ui/success.js b/js/id/ui/success.js index cd4181bbe..b9a90c3ca 100644 --- a/js/id/ui/success.js +++ b/js/id/ui/success.js @@ -3,10 +3,10 @@ iD.ui.success = function() { function success(selection) { var changeset = selection.datum(), - header = selection.append('div').attr('class', 'header modal-section'), + header = selection.append('div').attr('class', 'header fillL modal-section'), body = selection.append('div').attr('class', 'body'); - var section = body.append('div').attr('class','modal-section'); + var section = body.append('div').attr('class','modal-section cf'); header.append('h2').text('You Just Edited OpenStreetMap!'); header.append('p').text('You just improved the world\'s best free map'); @@ -30,7 +30,7 @@ iD.ui.success = function() { .attr('class', 'buttons'); var okbutton = buttonwrap.append('button') - .attr('class', 'action wide') + .attr('class', 'action col2') .on('click.save', function() { event.cancel(); }); diff --git a/js/id/ui/userpanel.js b/js/id/ui/userpanel.js index bab5e28d9..57f368d65 100644 --- a/js/id/ui/userpanel.js +++ b/js/id/ui/userpanel.js @@ -7,20 +7,26 @@ iD.ui.userpanel = function(connection) { if (connection.authenticated()) { selection.style('display', 'block'); connection.userDetails(function(user_details) { - if (user_details.image_url) { - selection.append('img') - .attr('class', 'icon icon-pre-text') - .attr('src', user_details.image_url); - } else { - selection.append('span') - .attr('class','icon avatar icon-pre-text'); - } - selection.append('span') - .append('a') + + // Link + var userLink = selection.append('a') .attr('href', connection.url() + '/user/' + user_details.display_name) .attr('target', '_blank') - .text(user_details.display_name); + + // Add thumbnail or dont + if (user_details.image_url) { + userLink.append('img') + .attr('class', 'icon icon-pre-text') + .attr('src', user_details.image_url); + } else { + userLink.append('span') + .attr('class','icon avatar icon-pre-text'); + } + + // Add user name + userLink.append('span').attr('class','label').text(user_details.display_name); + selection .append('a') .attr('class', 'logout') diff --git a/test/index.html b/test/index.html index f8d570f36..0777751d5 100644 --- a/test/index.html +++ b/test/index.html @@ -25,6 +25,7 @@ + @@ -33,6 +34,9 @@ + + + @@ -41,7 +45,9 @@ + + @@ -107,7 +113,10 @@ @@ -134,6 +143,8 @@ + + @@ -147,7 +158,11 @@ + + + + diff --git a/test/index_packaged.html b/test/index_packaged.html index 92c61df79..630ed1088 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -20,7 +20,10 @@ @@ -47,6 +50,8 @@ + + @@ -60,7 +65,11 @@ + + + + diff --git a/test/spec/geo/extent.js b/test/spec/geo/extent.js new file mode 100644 index 000000000..54f328efb --- /dev/null +++ b/test/spec/geo/extent.js @@ -0,0 +1,100 @@ +describe("iD.geo.Extent", function () { + describe("constructor", function () { + it("defaults to infinitely empty extent", function () { + expect(iD.geo.Extent()).to.eql([[Infinity, Infinity], [-Infinity, -Infinity]]); + }); + + it("constructs via a point", function () { + var p = [0, 0]; + expect(iD.geo.Extent(p)).to.eql([p, p]); + }); + + it("constructs via two points", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent(min, max)).to.eql([min, max]); + }); + + it("constructs via an extent", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent([min, max])).to.eql([min, max]); + }); + + it("constructs via an iD.geo.Extent", function () { + var min = [0, 0], + max = [5, 10], + extent = iD.geo.Extent(min, max); + expect(iD.geo.Extent(extent)).to.equal(extent); + }); + + it("has length 2", function () { + expect(iD.geo.Extent().length).to.equal(2); + }); + + it("has min element", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent(min, max)[0]).to.equal(min); + }); + + it("has max element", function () { + var min = [0, 0], + max = [5, 10]; + expect(iD.geo.Extent(min, max)[1]).to.equal(max); + }); + }); + + describe("#center", function () { + it("returns the center point", function () { + expect(iD.geo.Extent([0, 0], [5, 10]).center()).to.eql([2.5, 5]); + }); + }); + + describe("#extend", function () { + it("does not modify self", function () { + var extent = iD.geo.Extent([0, 0], [0, 0]); + extent.extend([1, 1]); + expect(extent).to.eql([[0, 0], [0, 0]]); + }); + + it("returns the minimal extent containing self and the given point", function () { + expect(iD.geo.Extent().extend([0, 0])).to.eql([[0, 0], [0, 0]]); + expect(iD.geo.Extent([0, 0], [0, 0]).extend([5, 10])).to.eql([[0, 0], [5, 10]]); + }); + + it("returns the minimal extent containing self and the given extent", function () { + expect(iD.geo.Extent().extend([[0, 0], [5, 10]])).to.eql([[0, 0], [5, 10]]); + expect(iD.geo.Extent([0, 0], [0, 0]).extend([[4, -1], [5, 10]])).to.eql([[0, -1], [5, 10]]); + }); + }); + + describe('#intersects', function () { + it("returns true for a point inside self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([2, 2])).to.be.true; + }); + + it("returns true for a point on the boundary of self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([0, 0])).to.be.true; + }); + + it("returns false for a point outside self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([6, 6])).to.be.false; + }); + + it("returns true for an extent contained by self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[1, 1], [2, 2]])).to.be.true; + expect(iD.geo.Extent([1, 1], [2, 2]).intersects([[0, 0], [5, 5]])).to.be.true; + }); + + it("returns true for an extent intersected by self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[1, 1], [6, 6]])).to.be.true; + expect(iD.geo.Extent([1, 1], [6, 6]).intersects([[0, 0], [5, 5]])).to.be.true; + }); + + it("returns false for an extent not intersected by self", function () { + expect(iD.geo.Extent([0, 0], [5, 5]).intersects([[6, 6], [7, 7]])).to.be.false; + expect(iD.geo.Extent([[6, 6], [7, 7]]).intersects([[0, 0], [5, 5]])).to.be.false; + }); + }); +}); diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js index 929946b04..8045270b3 100644 --- a/test/spec/graph/node.js +++ b/test/spec/graph/node.js @@ -29,11 +29,11 @@ describe('iD.Node', function () { describe("#intersects", function () { it("returns true for a node within the given extent", function () { - expect(iD.Node({loc: [0, 0]}).intersects([[-180, 90], [180, -90]])).to.equal(true); + expect(iD.Node({loc: [0, 0]}).intersects([[-5, -5], [5, 5]])).to.equal(true); }); it("returns false for a node outside the given extend", function () { - expect(iD.Node({loc: [0, 0]}).intersects([[100, 90], [180, -90]])).to.equal(false); + expect(iD.Node({loc: [6, 6]}).intersects([[-5, -5], [5, 5]])).to.equal(false); }); }); diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js index 26a1a2022..8880fc98c 100644 --- a/test/spec/graph/relation.js +++ b/test/spec/graph/relation.js @@ -36,7 +36,23 @@ describe('iD.Relation', function () { }); describe("#extent", function () { - it("returns the minimal extent containing the extents of all members"); + it("returns the minimal extent containing the extents of all members", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [5, 10]}), + r = iD.Relation({members: [{id: a.id}, {id: b.id}]}), + graph = iD.Graph([a, b, r]); + + expect(r.extent(graph)).to.eql([[0, 0], [5, 10]]) + }); + + it("returns the known extent of incomplete relations", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [5, 10]}), + r = iD.Relation({members: [{id: a.id}, {id: b.id}]}), + graph = iD.Graph([a, r]); + + expect(r.extent(graph)).to.eql([[0, 0], [0, 0]]) + }); }); describe("#multipolygon", function () { @@ -232,5 +248,17 @@ describe('iD.Relation', function () { expect(r.multipolygon(graph)).to.eql([[[a, b, c, a], [d, e, f, d]], [[g, h, i, g]]]); }); + + specify("incomplete relation", function () { + var a = iD.Node(), + b = iD.Node(), + c = iD.Node(), + w1 = iD.Way({nodes: [a.id, b.id, c.id]}), + w2 = iD.Way(), + r = iD.Relation({members: [{id: w2.id, type: 'way'}, {id: w1.id, type: 'way'}]}), + g = iD.Graph([a, b, c, w1, r]); + + expect(r.multipolygon(g)).to.eql([[[a, b, c]]]); + }); }); }); diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js index 3aa8045fb..19a4514db 100644 --- a/test/spec/graph/way.js +++ b/test/spec/graph/way.js @@ -41,7 +41,7 @@ describe('iD.Way', function() { node2 = iD.Node({loc: [5, 10]}), way = iD.Way({nodes: [node1.id, node2.id]}), graph = iD.Graph([node1, node2, way]); - expect(way.extent(graph)).to.eql([[5, 0], [0, 10]]); + expect(way.extent(graph)).to.eql([[0, 0], [5, 10]]); }); }); @@ -50,14 +50,14 @@ describe('iD.Way', function() { var node = iD.Node({loc: [0, 0]}), way = iD.Way({nodes: [node.id]}), graph = iD.Graph([node, way]); - expect(way.intersects([[-180, 90], [180, -90]], graph)).to.equal(true); + expect(way.intersects([[-5, -5], [5, 5]], graph)).to.equal(true); }); it("returns false for way with no nodes within the given extent", function () { - var node = iD.Node({loc: [0, 0]}), + var node = iD.Node({loc: [6, 6]}), way = iD.Way({nodes: [node.id]}), graph = iD.Graph([node, way]); - expect(way.intersects([[100, 90], [180, -90]], graph)).to.equal(false); + expect(way.intersects([[-5, -5], [5, 5]], graph)).to.equal(false); }); }); diff --git a/test/spec/renderer/hash.js b/test/spec/renderer/hash.js index 64945683d..44d3c64a2 100644 --- a/test/spec/renderer/hash.js +++ b/test/spec/renderer/hash.js @@ -7,7 +7,8 @@ describe("hash", function () { on: function () { return map; }, off: function () { return map; }, zoom: function () { return arguments.length ? map : 0; }, - center: function () { return arguments.length ? map : [0, 0] } + center: function () { return arguments.length ? map : [0, 0] }, + centerZoom: function () { return arguments.length ? map : [0, 0] } }; }); @@ -28,18 +29,11 @@ describe("hash", function () { expect(hash.hadHash).to.be.true; }); - it("zooms map to requested level", function () { + it("centerZooms map to requested level", function () { location.hash = "?map=20.00/38.87952/-77.02405"; - sinon.spy(map, 'zoom'); + sinon.spy(map, 'centerZoom'); hash.map(map); - expect(map.zoom).to.have.been.calledWith(20.0); - }); - - it("centers map at requested coordinates", function () { - location.hash = "?map=20.00/38.87952/-77.02405"; - sinon.spy(map, 'center'); - hash.map(map); - expect(map.center).to.have.been.calledWith([-77.02405, 38.87952]); + expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0); }); it("binds the map's move event", function () { @@ -66,23 +60,13 @@ describe("hash", function () { d3.select(window).one("hashchange", fn); } - it("zooms map to requested level", function (done) { + it("centerZooms map at requested coordinates", function (done) { onhashchange(function () { - expect(map.zoom).to.have.been.calledWith(20.0); + expect(map.centerZoom).to.have.been.calledWith([-77.02405,38.87952], 20.0); done(); }); - sinon.spy(map, 'zoom'); - location.hash = "#?map=20.00/38.87952/-77.02405"; - }); - - it("centers map at requested coordinates", function (done) { - onhashchange(function () { - expect(map.center).to.have.been.calledWith([-77.02405, 38.87952]); - done(); - }); - - sinon.spy(map, 'center'); + sinon.spy(map, 'centerZoom'); location.hash = "#?map=20.00/38.87952/-77.02405"; }); }); diff --git a/test/spec/renderer/map.js b/test/spec/renderer/map.js index 4cec560e9..611a2d1ff 100644 --- a/test/spec/renderer/map.js +++ b/test/spec/renderer/map.js @@ -54,16 +54,17 @@ describe('Map', function() { describe('#extent', function() { it('gets and sets extent', function() { - expect(map.size([100, 100])).to.equal(map); - expect(map.center([0, 0])).to.equal(map); + map.size([100, 100]) + .center([0, 0]); + expect(map.extent()[0][0]).to.be.closeTo(-17.5, 0.5); expect(map.extent()[1][0]).to.be.closeTo(17.5, 0.5); - expect(map.extent([10, 1], [30, 1])); + expect(map.extent([[10, 1], [30, 1]])); expect(map.extent()[0][0]).to.be.closeTo(10, 0.1); expect(map.extent()[1][0]).to.be.closeTo(30, 0.1); - expect(map.extent([-1, -20], [1, -40])); - expect(map.extent()[0][1]).to.be.closeTo(-20, 0.1); - expect(map.extent()[1][1]).to.be.closeTo(-40, 0.1); + expect(map.extent([[-1, -40], [1, -20]])); + expect(map.extent()[0][1]).to.be.closeTo(-40, 1); + expect(map.extent()[1][1]).to.be.closeTo(-20, 1); }); }); diff --git a/test/spec/svg.js b/test/spec/svg.js new file mode 100644 index 000000000..265562071 --- /dev/null +++ b/test/spec/svg.js @@ -0,0 +1,17 @@ +describe("iD.svg.LineString", function () { + it("returns an SVG path description for the entity's nodes", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [2, 3]}), + way = iD.Way({nodes: [a, b]}), + projection = Object; + + expect(iD.svg.LineString(projection)(way)).to.equal("M0,0L2,3"); + }); + + it("returns null for an entity with no nodes", function () { + var way = iD.Way(), + projection = Object; + + expect(iD.svg.LineString(projection)(way)).to.be.null; + }); +}); diff --git a/test/spec/svg/areas.js b/test/spec/svg/areas.js index 54a7c1d35..4f984540a 100644 --- a/test/spec/svg/areas.js +++ b/test/spec/svg/areas.js @@ -1,6 +1,6 @@ describe("iD.svg.Areas", function () { var surface, - projection = d3.geo.mercator(), + projection = Object, filter = d3.functor(true); beforeEach(function () { @@ -8,13 +8,48 @@ describe("iD.svg.Areas", function () { .call(iD.svg.Surface()); }); + it("adds way and area classes", function () { + var area = iD.Way({tags: {area: 'yes'}}), + graph = iD.Graph([area]); + + surface.call(iD.svg.Areas(projection), graph, [area], filter); + + expect(surface.select('path')).to.be.classed('way'); + expect(surface.select('path')).to.be.classed('area'); + }); + it("adds tag classes", function () { var area = iD.Way({tags: {area: 'yes', building: 'yes'}}), graph = iD.Graph([area]); - surface.call(iD.svg.Areas(), graph, [area], filter, projection); + surface.call(iD.svg.Areas(projection), graph, [area], filter); expect(surface.select('.area')).to.be.classed('tag-building'); expect(surface.select('.area')).to.be.classed('tag-building-yes'); }); + + it("adds member classes", function () { + var area = iD.Way({tags: {area: 'yes'}}), + relation = iD.Relation({members: [{id: area.id, role: 'outer'}], tags: {type: 'multipolygon'}}), + graph = iD.Graph([area, relation]); + + surface.call(iD.svg.Areas(projection), graph, [area], filter); + + expect(surface.select('.area')).to.be.classed('member'); + expect(surface.select('.area')).to.be.classed('member-role-outer'); + expect(surface.select('.area')).to.be.classed('member-type-multipolygon'); + }); + + it("preserves non-area paths", function () { + var area = iD.Way({tags: {area: 'yes'}}), + graph = iD.Graph([area]); + + surface.select('.layer-fill') + .append('path') + .attr('class', 'other'); + + surface.call(iD.svg.Areas(projection), graph, [area], filter); + + expect(surface.selectAll('.other')[0].length).to.equal(1); + }); }); diff --git a/test/spec/svg/lines.js b/test/spec/svg/lines.js new file mode 100644 index 000000000..61c3229a9 --- /dev/null +++ b/test/spec/svg/lines.js @@ -0,0 +1,54 @@ +describe("iD.svg.Lines", function () { + var surface, + projection = Object, + filter = d3.functor(true); + + beforeEach(function () { + surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) + .call(iD.svg.Surface()); + }); + + it("adds way and area classes", function () { + var line = iD.Way(), + graph = iD.Graph([line]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('path')).to.be.classed('way'); + expect(surface.select('path')).to.be.classed('line'); + }); + + it("adds tag classes", function () { + var line = iD.Way({tags: {highway: 'residential'}}), + graph = iD.Graph([line]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('.line')).to.be.classed('tag-highway'); + expect(surface.select('.line')).to.be.classed('tag-highway-residential'); + }); + + it("adds member classes", function () { + var line = iD.Way(), + relation = iD.Relation({members: [{id: line.id}], tags: {type: 'route'}}), + graph = iD.Graph([line, relation]); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.select('.line')).to.be.classed('member'); + expect(surface.select('.line')).to.be.classed('member-type-route'); + }); + + it("preserves non-line paths", function () { + var line = iD.Way(), + graph = iD.Graph([line]); + + surface.select('.layer-fill') + .append('path') + .attr('class', 'other'); + + surface.call(iD.svg.Lines(projection), graph, [line], filter); + + expect(surface.selectAll('.other')[0].length).to.equal(1); + }); +}); diff --git a/test/spec/svg/member_classes.js b/test/spec/svg/member_classes.js new file mode 100644 index 000000000..651ba2342 --- /dev/null +++ b/test/spec/svg/member_classes.js @@ -0,0 +1,54 @@ +describe("iD.svg.MemberClasses", function () { + var selection; + + beforeEach(function () { + selection = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'g')); + }); + + it("adds no classes to elements that aren't a member of any relations", function() { + var node = iD.Node(), + graph = iD.Graph([node]); + + selection + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal(null); + }); + + it("adds tags for member, role, and type", function() { + var node = iD.Node(), + relation = iD.Relation({members: [{id: node.id, role: 'r'}], tags: {type: 't'}}), + graph = iD.Graph([node, relation]); + + selection + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal('member member-type-t member-role-r'); + }); + + it('removes classes for tags that are no longer present', function() { + var node = iD.Entity(), + graph = iD.Graph([node]); + + selection + .attr('class', 'member member-type-t member-role-r') + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal(''); + }); + + it("preserves existing non-'member-'-prefixed classes", function() { + var node = iD.Entity(), + graph = iD.Graph([node]); + + selection + .attr('class', 'selected') + .datum(node) + .call(iD.svg.MemberClasses(graph)); + + expect(selection.attr('class')).to.equal('selected'); + }); +}); diff --git a/test/spec/svg/multipolygons.js b/test/spec/svg/multipolygons.js new file mode 100644 index 000000000..67f44ebae --- /dev/null +++ b/test/spec/svg/multipolygons.js @@ -0,0 +1,43 @@ +describe("iD.svg.Multipolygons", function () { + var surface, + projection = Object, + filter = d3.functor(true); + + beforeEach(function () { + surface = d3.select(document.createElementNS('http://www.w3.org/2000/svg', 'svg')) + .call(iD.svg.Surface()); + }); + + it("adds relation and multipolygon classes", function () { + var relation = iD.Relation({tags: {type: 'multipolygon'}}), + graph = iD.Graph([relation]); + + surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); + + expect(surface.select('path')).to.be.classed('relation'); + expect(surface.select('path')).to.be.classed('multipolygon'); + }); + + it("adds tag classes", function () { + var relation = iD.Relation({tags: {type: 'multipolygon', boundary: "administrative"}}), + graph = iD.Graph([relation]); + + surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); + + expect(surface.select('.relation')).to.be.classed('tag-boundary'); + expect(surface.select('.relation')).to.be.classed('tag-boundary-administrative'); + }); + + it("preserves non-multipolygon paths", function () { + var relation = iD.Relation({tags: {type: 'multipolygon'}}), + graph = iD.Graph([relation]); + + surface.select('.layer-fill') + .append('path') + .attr('class', 'other'); + + surface.call(iD.svg.Multipolygons(projection), graph, [relation], filter); + + expect(surface.selectAll('.other')[0].length).to.equal(1); + }); +}); diff --git a/test/spec/svg/points.js b/test/spec/svg/points.js index 664f88a80..6e991962c 100644 --- a/test/spec/svg/points.js +++ b/test/spec/svg/points.js @@ -1,6 +1,6 @@ describe("iD.svg.Points", function () { var surface, - projection = d3.geo.mercator(), + projection = Object, filter = d3.functor(true); beforeEach(function () { @@ -12,7 +12,7 @@ describe("iD.svg.Points", function () { var node = iD.Node({tags: {amenity: "cafe"}, loc: [0, 0], _poi: true}), graph = iD.Graph([node]); - surface.call(iD.svg.Points(), graph, [node], filter, projection); + surface.call(iD.svg.Points(projection), graph, [node], filter); expect(surface.select('.point')).to.be.classed('tag-amenity'); expect(surface.select('.point')).to.be.classed('tag-amenity-cafe'); diff --git a/test/spec/svg/vertices.js b/test/spec/svg/vertices.js index 760734053..8ce5e3a80 100644 --- a/test/spec/svg/vertices.js +++ b/test/spec/svg/vertices.js @@ -1,6 +1,6 @@ describe("iD.svg.Vertices", function () { var surface, - projection = d3.geo.mercator(), + projection = Object, filter = d3.functor(true); beforeEach(function () { @@ -12,7 +12,7 @@ describe("iD.svg.Vertices", function () { var node = iD.Node({tags: {highway: "traffic_signals"}, loc: [0, 0]}), graph = iD.Graph([node]); - surface.call(iD.svg.Vertices(), graph, [node], filter, projection); + surface.call(iD.svg.Vertices(projection), graph, [node], filter); expect(surface.select('.vertex')).to.be.classed('tag-highway'); expect(surface.select('.vertex')).to.be.classed('tag-highway-traffic_signals'); @@ -24,7 +24,7 @@ describe("iD.svg.Vertices", function () { way2 = iD.Way({nodes: [node.id]}), graph = iD.Graph([node, way1, way2]); - surface.call(iD.svg.Vertices(), graph, [node], filter, projection); + surface.call(iD.svg.Vertices(projection), graph, [node], filter); expect(surface.select('.vertex')).to.be.classed('shared'); }); diff --git a/test/spec/util.js b/test/spec/util.js index 3ac1cdda2..2cfa8dc16 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -92,5 +92,25 @@ describe('Util', function() { expect(iD.util.geo.polygonContainsPolygon(outer, inner)).to.be.false; }); }); + + describe('#polygonIntersectsPolygon', function() { + it('says a polygon in a polygon intersects it', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('says a polygon that partially intersects does', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [1, 2], [2, 2], [2, 1], [1, 1]]; + expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.true; + }); + + it('says totally disjoint polygons do not intersect', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[-1, -1], [-1, -2], [-2, -2], [-2, -1], [-1, -1]]; + expect(iD.util.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); }); });