diff --git a/css/app.css b/css/app.css index 0ba223a99..4c149843d 100644 --- a/css/app.css +++ b/css/app.css @@ -246,7 +246,6 @@ form.hide { button { line-height:20px; - position: relative; border:0; color:#222; background: white; @@ -346,16 +345,16 @@ button.save .count { button.save.has-count .count { display: block; position: absolute; - left: 115%; top: 0; bottom: 0; - background: rgba(255,255,255,.5); + background: rgba(255, 255, 255, .5); color: #333; padding: 10px; height: 30px; line-height: 12px; border-radius: 4px; margin: auto; + margin-left: 8.3333%; } button.save.has-count .count::before { @@ -929,50 +928,6 @@ div.typeahead a:first-child { left:0px; right:0px; top:0px; bottom:0px; } -.commit-modal .user-info { - display: inline-block; -} - -.commit-modal .commit-info { - margin-top: 10px; -} - -.commit-modal .user-info img { - float: left; -} - -.commit-modal h3 small.count { - margin-right: 10px; - text-align: center; - float: left; - height: 12px; - min-width: 12px; - font-size:12px; - line-height: 12px; - border-radius:24px; - padding:5px; - background:#7092ff; - color:#fff; -} - -.commit-modal .changeset-list { - overflow: auto; - border:1px solid #ccc; - background:#fff; - max-height: 160px; -} - -.commit-modal .warning-section .changeset-list { - margin-right: 20px; - overflow-x: visible; -} - -.commit-section.modal-section { - padding-bottom: 0; -} - -.commit-section.modal-section:last-child { padding-bottom: 20px;} - .modal-section { padding: 20px; } @@ -1007,6 +962,56 @@ div.typeahead a:first-child { display:none; } +.loading-modal { + text-align: center; +} + +/* Commit Modal +------------------------------------------------------- */ + +.commit-modal .user-info { + display: inline-block; +} + +.commit-modal .commit-info { + margin-top: 10px; +} + +.commit-modal .user-info img { + float: left; +} + +.commit-modal h3 small.count { + margin-right: 10px; + text-align: center; + float: left; + height: 12px; + min-width: 12px; + font-size:12px; + line-height: 12px; + border-radius:24px; + padding:5px; + background:#7092ff; + color:#fff; +} + +.commit-modal .changeset-list { + overflow: auto; + border:1px solid #ccc; + background:#fff; + max-height: 160px; +} + +.commit-modal .warning-section .changeset-list button { + float: right; +} + +.commit-section.modal-section { + padding-bottom: 0; +} + +.commit-section.modal-section:last-child { padding-bottom: 20px;} + .changeset-list li { border-top:1px solid #ccc; padding:5px 10px; @@ -1029,10 +1034,6 @@ div.typeahead a:first-child { font:normal 12px/20px 'Helvetica Neue', Arial, sans-serif; } -.loading-modal { - text-align: center; -} - /* Success ------------------------------------------------------- */ a.success-action { @@ -1051,7 +1052,8 @@ a.success-action { text-align:center; } -.notice .notice-inner { +.notice .zoom-to { + width:100%; height: 40px; border-radius: 5px; line-height: 40px; @@ -1060,22 +1062,27 @@ a.success-action { opacity: 0.9; } -.notice .notice-inner .zoom-to { - width:40px; - height:40px; +.notice .zoom-to:hover { + background: #bde5aa; +} + +.notice .zoom-to .icon { + margin-top:10px; margin-right:10px; } +.icon.zoom-in-invert { + background-position: -240px -40px; +} + /* Tooltips ------------------------------------------------------- */ .tooltip { - white-space: normal; + width: 200px; position: absolute; - left: 0; right: 0; margin: auto; z-index: -1000; height: 0; - padding: 5px; opacity: 0; display: block; } @@ -1087,98 +1094,130 @@ a.success-action { } .tooltip.top { - margin-top: -5px; + margin-top: -10px; + text-align: center; } .tooltip.right { - margin-left: 5px; + margin-left: 10px; + text-align: left; } .tooltip.bottom { - margin-top: 5px; + margin-top: 10px; + text-align: center; } .tooltip.left { - margin-left: -5px; + margin-left: -10px; + text-align: right; } .tooltip-inner { - text-align: left; - width: 200px; - font-size: 11px; - font-weight: bold; - line-height: 20px; - padding: 5px 10px; - color: #333; - background-color: white; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; + color: #333; + display: inline-block; + padding: 5px 10px; + font-size: 11px; + font-weight: bold; + line-height: 20px; + background-color: white; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + } .tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; } .tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-top-color: white; - border-width: 5px 5px 0; + bottom: -5px; + left: 50%; + margin-left: -5px; + border-top-color: white; + border-width: 5px 5px 0; } .tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-right-color: white; - border-width: 5px 5px 5px 0; + top: 50%; + left: -5px; + margin-top: -5px; + border-right-color: white; + border-width: 5px 5px 5px 0; } .tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-left-color: white; - border-width: 5px 0 5px 5px; + top: 50%; + right: 5px; + margin-top: -5px; + border-left-color: white; + border-width: 5px 0 5px 5px; } .tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-bottom-color: white; - border-width: 0 5px 5px; + top: -5px; + left: 50%; + margin-left: -5px; + border-bottom-color: white; + border-width: 0 5px 5px; } +.Browse .tooltip { + left: -20px !important; } .Browse .tooltip .tooltip-arrow { - left: 30px; + left: 60px; +} + +.tooltip .keyhint-wrap { + padding: 5px 0 5px 0; } .tooltip .keyhint { - float: right; - background: #eee; + display: block; + color: #222; font-size: 10px; - padding: 0 4px; - background:#aaa; - color:#fff; + padding: 0px 7px; + text-transform: uppercase; + font-weight: bold; + display: inline-block; border-radius: 2px; - margin-left: 4px; + border: 1px solid #CCC; + position: relative; + z-index: 1; + text-align: left; + clear: both; +} + +.tooltip .keyhint .keyhint-label{ + display: inline-block; +} + +.tooltip .keyhint::after { + content: ""; + position: absolute; + border-radius: 2px; + height: 10px; + width: 100%; + z-index: 0; + bottom: -4px; + left: -1px; + border: 1px solid #CCC; + border-top: 0; } .tail { - pointer-events:none; - position: absolute; - background: rgba(255, 255, 255, 0.7); - max-width: 250px; - margin-top: -15px; - padding: 5px; - -webkit-border-radius: 4px; + pointer-events:none; + position: absolute; + background: rgba(255, 255, 255, 0.7); + max-width: 250px; + margin-top: -15px; + padding: 5px; + -webkit-border-radius: 4px; -moz-border-radius: 4px; border-radius: 4px; } diff --git a/img/source/sprite.svg b/img/source/sprite.svg index 4cc5abb47..01a28e513 100644 --- a/img/source/sprite.svg +++ b/img/source/sprite.svg @@ -39,8 +39,8 @@ inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="4" - inkscape:cx="230.7911" - inkscape:cy="190.13176" + inkscape:cx="332.2911" + inkscape:cy="175.13176" inkscape:document-units="px" inkscape:current-layer="layer12" showgrid="false" @@ -733,7 +733,7 @@ inkscape:export-xdpi="90" inkscape:export-ydpi="90" /> diff --git a/img/sprite.png b/img/sprite.png index b0f45f2cd..5922c06c1 100644 Binary files a/img/sprite.png and b/img/sprite.png differ diff --git a/index.html b/index.html index 438b54e90..9606fe7b6 100644 --- a/index.html +++ b/index.html @@ -75,6 +75,7 @@ + diff --git a/js/id/actions/add_midpoint.js b/js/id/actions/add_midpoint.js new file mode 100644 index 000000000..c0cb97f59 --- /dev/null +++ b/js/id/actions/add_midpoint.js @@ -0,0 +1,11 @@ +iD.actions.AddMidpoint = function(midpoint, node) { + return function(graph) { + graph = graph.replace(node.move(midpoint.loc)); + + midpoint.ways.forEach(function(way) { + graph = graph.replace(graph.entity(way.id).addNode(node.id, way.index)); + }); + + return graph; + }; +}; diff --git a/js/id/behavior/add_way.js b/js/id/behavior/add_way.js index 021377890..fa038d201 100644 --- a/js/id/behavior/add_way.js +++ b/js/id/behavior/add_way.js @@ -1,25 +1,17 @@ iD.behavior.AddWay = function(mode) { var map = mode.map, - history = mode.history, controller = mode.controller, - event = d3.dispatch('startFromNode', 'startFromWay', 'start'), - draw; - - function add(datum) { - if (datum.type === 'node') { - event.startFromNode(datum); - } else if (datum.type === 'way') { - var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); - event.startFromWay(datum, choice.loc, choice.index); - } else if (datum.midpoint) { - var way = history.graph().entity(datum.way); - event.startFromWay(way, datum.loc, datum.index); - } else { - event.start(map.mouseCoordinates()); - } - } + event = d3.dispatch('start', 'startFromWay', 'startFromNode', 'startFromMidpoint'), + draw = iD.behavior.Draw(map); var addWay = function(surface) { + draw.on('click', event.start) + .on('clickWay', event.startFromWay) + .on('clickNode', event.startFromNode) + .on('clickMidpoint', event.startFromMidpoint) + .on('cancel', addWay.cancel) + .on('finish', addWay.cancel); + map.fastEnable(false) .minzoom(16) .dblclickEnable(false); @@ -43,10 +35,5 @@ iD.behavior.AddWay = function(mode) { controller.exit(); }; - draw = iD.behavior.Draw() - .on('add', add) - .on('cancel', addWay.cancel) - .on('finish', addWay.cancel); - return d3.rebind(addWay, event, 'on'); }; diff --git a/js/id/behavior/drag_midpoint.js b/js/id/behavior/drag_midpoint.js index 9f73a7115..bd1a40bd9 100644 --- a/js/id/behavior/drag_midpoint.js +++ b/js/id/behavior/drag_midpoint.js @@ -1,29 +1,21 @@ iD.behavior.DragMidpoint = function(mode) { var history = mode.history, - projection = mode.map.projection, - behavior = iD.behavior.drag() + projection = mode.map.projection; + + var behavior = iD.behavior.drag() .delegate(".midpoint") .origin(function(d) { return projection(d.loc); }) .on('start', function(d) { - var w, nds; - d.node = iD.Node({loc: d.loc}); - var args = [iD.actions.AddNode(d.node)]; - for (var i = 0; i < d.ways.length; i++) { - w = d.ways[i], nds = w.nodes; - for (var j = 0; j < nds.length; j++) { - if ((nds[j] === d.nodes[0] && nds[j + 1] === d.nodes[1]) || - (nds[j] === d.nodes[1] && nds[j + 1] === d.nodes[0])) { - args.push(iD.actions.AddWayNode(w.id, d.node.id, j + 1)); - } - } - } - history.perform.apply(history, args); - var node = d3.selectAll('.node.vertex') - .filter(function(data) { return data.id === d.node.id; }); - behavior.target(node.node(), node.datum()); + var node = iD.Node(); + history.perform(iD.actions.AddMidpoint(d, node)); + + var vertex = d3.selectAll('.vertex') + .filter(function(data) { return data.id === node.id; }); + + behavior.target(vertex.node(), vertex.datum()); }) .on('move', function(d) { d3.event.sourceEvent.stopPropagation(); @@ -33,7 +25,8 @@ iD.behavior.DragMidpoint = function(mode) { .on('end', function() { history.replace( iD.actions.Noop(), - 'added a node to a way'); + 'Added a node to a way.'); }); + return behavior; }; diff --git a/js/id/behavior/draw.js b/js/id/behavior/draw.js index 4d0153505..bce4470b6 100644 --- a/js/id/behavior/draw.js +++ b/js/id/behavior/draw.js @@ -1,5 +1,5 @@ -iD.behavior.Draw = function () { - var event = d3.dispatch('move', 'add', 'undo', 'cancel', 'finish'), +iD.behavior.Draw = function(map) { + var event = d3.dispatch('move', 'click', 'clickWay', 'clickNode', 'clickMidpoint', 'undo', 'cancel', 'finish'), keybinding = d3.keybinding('draw'), down, surface, hover; @@ -26,7 +26,20 @@ iD.behavior.Draw = function () { } function click() { - event.add(datum()); + var d = datum(); + if (d.type === 'way') { + var choice = iD.geo.chooseIndex(d, d3.mouse(map.surface.node()), map); + event.clickWay(d, choice.loc, choice.index); + + } else if (d.type === 'node') { + event.clickNode(d); + + } else if (d.type === 'midpoint') { + event.clickMidpoint(d); + + } else { + event.click(map.mouseCoordinates()); + } } function keydown() { diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js index 63526df7a..21c13d2fa 100644 --- a/js/id/behavior/draw_way.js +++ b/js/id/behavior/draw_way.js @@ -1,11 +1,11 @@ -iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { +iD.behavior.DrawWay = function(wayId, index, mode, baseGraph) { var map = mode.map, history = mode.history, controller = mode.controller, - event = d3.dispatch('add', 'addHead', 'addTail', 'addNode', 'addWay'), way = mode.history.graph().entity(wayId), finished = false, - draw; + annotation = 'Added to a way.', + draw = iD.behavior.Draw(map); var node = iD.Node({loc: map.mouseCoordinates()}), nodeId = node.id; @@ -17,7 +17,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { function move(datum) { var loc = map.mouseCoordinates(); - if (datum.type === 'node' || datum.midpoint) { + if (datum.type === 'node' || datum.type === 'midpoint') { loc = datum.loc; } else if (datum.type === 'way') { loc = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map).loc; @@ -26,29 +26,20 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { history.replace(iD.actions.MoveNode(nodeId, loc)); } - function add(datum) { - if (datum.id === headId) { - event.addHead(datum); - } else if (datum.id === tailId) { - event.addTail(datum); - } else if (datum.type === 'node' && datum.id !== nodeId) { - event.addNode(datum); - } else if (datum.type === 'way') { - var choice = iD.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); - event.addWay(datum, choice.loc, choice.index); - } else if (datum.midpoint) { - var way = history.graph().entity(datum.way); - event.addWay(way, datum.loc, datum.index); - } else { - event.add(map.mouseCoordinates()); - } - } - function undone() { controller.enter(iD.modes.Browse()); } var drawWay = function(surface) { + draw.on('move', move) + .on('click', drawWay.add) + .on('clickWay', drawWay.addWay) + .on('clickNode', drawWay.addNode) + .on('clickMidpoint', drawWay.addMidpoint) + .on('undo', history.undo) + .on('cancel', drawWay.cancel) + .on('finish', drawWay.finish); + map.fastEnable(false) .minzoom(16) .dblclickEnable(false); @@ -80,6 +71,12 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { history.on('undone.draw', null); }; + drawWay.annotation = function(_) { + if (!arguments.length) return annotation; + annotation = _; + return drawWay; + }; + function ReplaceTemporaryNode(newNode) { return function(graph) { return graph @@ -88,10 +85,13 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { } } - // Connect the way to an existing node and continue drawing. - drawWay.addNode = function(node, annotation) { - history.perform( - ReplaceTemporaryNode(node), + // Accept the current position of the temporary node and continue drawing. + drawWay.add = function(loc) { + var newNode = iD.Node({loc: loc}); + + history.replace( + iD.actions.AddNode(newNode), + ReplaceTemporaryNode(newNode), annotation); finished = true; @@ -99,7 +99,7 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { }; // Connect the way to an existing way. - drawWay.addWay = function(way, loc, wayIndex, annotation) { + drawWay.addWay = function(way, loc, wayIndex) { var newNode = iD.Node({loc: loc}); history.perform( @@ -112,13 +112,23 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { controller.enter(mode); }; - // Accept the current position of the temporary node and continue drawing. - drawWay.add = function(loc, annotation) { - var newNode = iD.Node({loc: loc}); + // Connect the way to an existing node and continue drawing. + drawWay.addNode = function(node) { + history.perform( + ReplaceTemporaryNode(node), + annotation); - history.replace( - iD.actions.AddNode(newNode), - ReplaceTemporaryNode(newNode), + finished = true; + controller.enter(mode); + }; + + // Add a midpoint, connect the way to it, and continue drawing. + drawWay.addMidpoint = function(midpoint) { + var node = iD.Node(); + + history.perform( + iD.actions.AddMidpoint(midpoint, node), + ReplaceTemporaryNode(node), annotation); finished = true; @@ -143,18 +153,11 @@ iD.behavior.DrawWay = function(wayId, headId, tailId, index, mode, baseGraph) { drawWay.cancel = function() { history.perform( d3.functor(baseGraph), - 'cancelled drawing'); + 'Cancelled drawing.'); finished = true; controller.enter(iD.modes.Browse()); }; - draw = iD.behavior.Draw() - .on('move', move) - .on('add', add) - .on('undo', history.undo) - .on('cancel', drawWay.cancel) - .on('finish', drawWay.finish); - return d3.rebind(drawWay, event, 'on'); }; diff --git a/js/id/connection.js b/js/id/connection.js index 07cd332b7..d91d3388d 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -125,7 +125,7 @@ iD.Connection = function() { } } - return iD.Graph(entities); + return entities; } function authenticated() { diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 120fa2fe7..5e865b9ec 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -1,19 +1,30 @@ -iD.Graph = function(entities, mutable) { - if (!(this instanceof iD.Graph)) return new iD.Graph(entities, mutable); +iD.Graph = function(other, mutable) { + if (!(this instanceof iD.Graph)) return new iD.Graph(other, mutable); + + if (other instanceof iD.Graph) { + var base = other.base(); + this.entities = _.assign(Object.create(base.entities), other.entities); + this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays); + this._parentRels = _.assign(Object.create(base.parentRels), other._parentRels); + this.inherited = true; - if (_.isArray(entities)) { - this.entities = {}; - for (var i = 0; i < entities.length; i++) { - this.entities[entities[i].id] = entities[i]; - } } else { - this.entities = entities || {}; + if (_.isArray(other)) { + var entities = {}; + for (var i = 0; i < other.length; i++) { + entities[other[i].id] = other[i]; + } + other = entities; + } + this.entities = Object.create({}); + this._parentWays = Object.create({}); + this._parentRels = Object.create({}); + this.rebase(other || {}); } this.transients = {}; - this._parentWays = {}; - this._parentRels = {}; this._childNodes = {}; + this.getEntity = _.bind(this.entity, this); if (!mutable) { this.freeze(); @@ -38,51 +49,21 @@ iD.Graph.prototype = { }, parentWays: function(entity) { - var ent, id, parents; - - if (!this._parentWays.calculated) { - for (var i in this.entities) { - ent = this.entities[i]; - if (ent && ent.type === 'way') { - for (var j = 0; j < ent.nodes.length; j++) { - id = ent.nodes[j]; - parents = this._parentWays[id] = this._parentWays[id] || []; - if (parents.indexOf(ent) < 0) { - parents.push(ent); - } - } - } - } - this._parentWays.calculated = true; - } - - return this._parentWays[entity.id] || []; + return _.map(this._parentWays[entity.id], this.getEntity); }, isPoi: function(entity) { - return this.parentWays(entity).length === 0; + var parentWays = this._parentWays[entity.id]; + return !parentWays || parentWays.length === 0; + }, + + isShared: function(entity) { + var parentWays = this._parentWays[entity.id]; + return parentWays && parentWays.length > 1; }, parentRelations: function(entity) { - var ent, id, parents; - - if (!this._parentRels.calculated) { - for (var i in this.entities) { - ent = this.entities[i]; - if (ent && ent.type === 'relation') { - for (var j = 0; j < ent.members.length; j++) { - id = ent.members[j].id; - parents = this._parentRels[id] = this._parentRels[id] || []; - if (parents.indexOf(ent) < 0) { - parents.push(ent); - } - } - } - } - this._parentRels.calculated = true; - } - - return this._parentRels[entity.id] || []; + return _.map(this._parentRels[entity.id], this.getEntity); }, childNodes: function(entity) { @@ -97,30 +78,132 @@ iD.Graph.prototype = { return (this._childNodes[entity.id] = nodes); }, - merge: function(graph) { - return this.update(function () { - _.defaults(this.entities, graph.entities); - }); + base: function() { + return { + 'entities': iD.util.getPrototypeOf(this.entities), + 'parentWays': iD.util.getPrototypeOf(this._parentWays), + 'parentRels': iD.util.getPrototypeOf(this._parentRels) + }; + }, + + // Unlike other graph methods, rebase mutates in place. This is because it + // is used only during the history operation that merges newly downloaded + // data into each state. To external consumers, it should appear as if the + // graph always contained the newly downloaded data. + rebase: function(entities) { + var base = this.base(), + i, k, child, id, keys; + // Merging of data only needed if graph is the base graph + if (!this.inherited) { + for (i in entities) { + if (!base.entities[i]) { + base.entities[i] = entities[i]; + this._updateCalculated(undefined, entities[i], + base.parentWays, base.parentRels); + } + } + } + + keys = Object.keys(this._parentWays); + for (i = 0; i < keys.length; i++) { + child = keys[i]; + if (base.parentWays[child]) { + for (k = 0; k < base.parentWays[child].length; k++) { + id = base.parentWays[child][k]; + if (this.entity(id) && !_.contains(this._parentWays[child], id)) { + this._parentWays[child].push(id); + } + } + } + } + + keys = Object.keys(this._parentRels); + for (i = 0; i < keys.length; i++) { + child = keys[i]; + if (base.parentRels[child]) { + for (k = 0; k < base.parentRels[child].length; k++) { + id = base.parentRels[child][k]; + if (this.entity(id) && !_.contains(this._parentRels[child], id)) { + this._parentRels[child].push(id); + } + } + } + } + }, + + // Updates calculated properties (parentWays, parentRels) for the specified change + _updateCalculated: function(oldentity, entity, parentWays, parentRels) { + + parentWays = parentWays || this._parentWays; + parentRels = parentRels || this._parentRels; + + var type = entity && entity.type || oldentity && oldentity.type, + removed, added, ways, rels, i; + + + if (type === 'way') { + + // Update parentWays + if (oldentity && entity) { + removed = _.difference(oldentity.nodes, entity.nodes); + added = _.difference(entity.nodes, oldentity.nodes); + } else if (oldentity) { + removed = oldentity.nodes; + added = []; + } else if (entity) { + removed = []; + added = entity.nodes; + } + for (i = 0; i < removed.length; i++) { + parentWays[removed[i]] = _.without(parentWays[removed[i]], oldentity.id); + } + for (i = 0; i < added.length; i++) { + ways = _.without(parentWays[added[i]], entity.id); + ways.push(entity.id); + parentWays[added[i]] = ways; + } + } else if (type === 'node') { + + } else if (type === 'relation') { + + // Update parentRels + if (oldentity && entity) { + removed = _.difference(oldentity.members, entity.members); + added = _.difference(entity.members, oldentity); + } else if (oldentity) { + removed = oldentity.members; + added = []; + } else if (entity) { + removed = []; + added = entity.members; + } + for (i = 0; i < removed.length; i++) { + parentRels[removed[i].id] = _.without(parentRels[removed[i].id], oldentity.id); + } + for (i = 0; i < added.length; i++) { + rels = _.without(parentRels[added[i].id], entity.id); + rels.push(entity.id); + parentRels[added[i].id] = rels; + } + } }, replace: function(entity) { return this.update(function () { + this._updateCalculated(this.entities[entity.id], entity); this.entities[entity.id] = entity; }); }, remove: function(entity) { return this.update(function () { - if (entity.created()) { - delete this.entities[entity.id]; - } else { - this.entities[entity.id] = undefined; - } + this._updateCalculated(entity, undefined); + this.entities[entity.id] = undefined; }); }, update: function() { - var graph = this.frozen ? iD.Graph(_.clone(this.entities), true) : this; + var graph = this.frozen ? iD.Graph(this, true) : this; for (var i = 0; i < arguments.length; i++) { arguments[i].call(graph, graph); @@ -133,7 +216,6 @@ iD.Graph.prototype = { this.frozen = true; if (iD.debug) { - Object.freeze(this); Object.freeze(this.entities); } @@ -153,9 +235,12 @@ iD.Graph.prototype = { }, difference: function (graph) { - var result = [], entity, oldentity, id; + var result = [], + keys = Object.keys(this.entities), + entity, oldentity, id, i; - for (id in this.entities) { + for (i = 0; i < keys.length; i++) { + id = keys[i]; entity = this.entities[id]; oldentity = graph.entities[id]; if (entity !== oldentity) { @@ -177,7 +262,9 @@ iD.Graph.prototype = { } } - for (id in graph.entities) { + keys = Object.keys(graph.entities); + for (i = 0; i < keys.length; i++) { + id = keys[i]; entity = graph.entities[id]; if (entity && !this.entities.hasOwnProperty(id)) { result.push(id); @@ -189,25 +276,25 @@ iD.Graph.prototype = { }, modified: function() { - var result = []; + var result = [], base = this.base().entities; _.each(this.entities, function(entity, id) { - if (entity && entity.modified()) result.push(id); + if (entity && base[id]) result.push(id); }); return result; }, created: function() { - var result = []; + var result = [], base = this.base().entities; _.each(this.entities, function(entity, id) { - if (entity && entity.created()) result.push(id); + if (entity && !base[id]) result.push(id); }); return result; }, deleted: function() { - var result = []; + var result = [], base = this.base().entities; _.each(this.entities, function(entity, id) { - if (!entity) result.push(id); + if (!entity && base[id]) result.push(id); }); return result; } diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 29b652554..42e1b1604 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -29,9 +29,9 @@ iD.History = function() { return stack[index].graph; }, - merge: function (graph) { + merge: function (entities) { for (var i = 0; i < stack.length; i++) { - stack[i].graph = stack[i].graph.merge(graph); + stack[i].graph.rebase(entities); } }, diff --git a/js/id/id.js b/js/id/id.js index ab6dd37f2..69e64e5fe 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -22,7 +22,7 @@ window.iD = function(container) { } function hintprefix(x, y) { - return '' + x + ' ' + y; + return '' + y + '' + '
' + x + '
'; } var m = container.append('div') @@ -44,10 +44,10 @@ window.iD = function(container) { .enter().append('button') .attr('tabindex', -1) .attr('class', function (mode) { return mode.title + ' add-button col3'; }) + .call(bootstrap.tooltip().placement('bottom').html(true)) .attr('data-original-title', function (mode) { return hintprefix(mode.key, mode.description); }) - .call(bootstrap.tooltip().placement('bottom').html(true)) .on('click.editor', function (mode) { controller.enter(mode); }); function disableTooHigh() { @@ -207,12 +207,12 @@ window.iD = function(container) { limiter.select('#undo') .property('disabled', !undo) - .attr('data-original-title', hintprefix('⌘Z', undo)) + .attr('data-original-title', hintprefix('⌘ + Z', undo)) .call(refreshTooltip); limiter.select('#redo') .property('disabled', !redo) - .attr('data-original-title', hintprefix('⌘⇧Z', redo)) + .attr('data-original-title', hintprefix('⌘ + ⇧ + Z', redo)) .call(refreshTooltip); }); diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index d1a3efb08..614c66677 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -15,11 +15,13 @@ iD.modes.AddArea = function() { history = mode.history, controller = mode.controller; - function startFromNode(node) { + function start(loc) { var graph = history.graph(), + node = iD.Node({loc: loc}), way = iD.Way({tags: defaultTags}); history.perform( + iD.actions.AddNode(node), iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, node.id), iD.actions.AddWayNode(way.id, node.id)); @@ -42,13 +44,25 @@ iD.modes.AddArea = function() { controller.enter(iD.modes.DrawArea(way.id, graph)); } - function start(loc) { + function startFromNode(node) { var graph = history.graph(), - node = iD.Node({loc: loc}), way = iD.Way({tags: defaultTags}); history.perform( - iD.actions.AddNode(node), + iD.actions.AddWay(way), + iD.actions.AddWayNode(way.id, node.id), + iD.actions.AddWayNode(way.id, node.id)); + + controller.enter(iD.modes.DrawArea(way.id, graph)); + } + + function startFromMidpoint(midpoint) { + var graph = history.graph(), + node = iD.Node(), + way = iD.Way({tags: defaultTags}); + + history.perform( + iD.actions.AddMidpoint(midpoint, node), iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, node.id), iD.actions.AddWayNode(way.id, node.id)); @@ -57,9 +71,10 @@ iD.modes.AddArea = function() { } behavior = iD.behavior.AddWay(mode) - .on('startFromNode', startFromNode) + .on('start', start) .on('startFromWay', startFromWay) - .on('start', start); + .on('startFromNode', startFromNode) + .on('startFromMidpoint', startFromMidpoint); mode.map.surface.call(behavior); mode.map.tail('Click on the map to start drawing an area, like a park, lake, or building.', true); diff --git a/js/id/modes/add_line.js b/js/id/modes/add_line.js index bf419d2bb..90adac738 100644 --- a/js/id/modes/add_line.js +++ b/js/id/modes/add_line.js @@ -15,6 +15,33 @@ iD.modes.AddLine = function() { history = mode.history, controller = mode.controller; + function start(loc) { + var graph = history.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + history.perform( + iD.actions.AddNode(node), + iD.actions.AddWay(way), + iD.actions.AddWayNode(way.id, node.id)); + + controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); + } + + function startFromWay(other, loc, index) { + var graph = history.graph(), + node = iD.Node({loc: loc}), + way = iD.Way({tags: defaultTags}); + + history.perform( + iD.actions.AddNode(node), + iD.actions.AddWay(way), + iD.actions.AddWayNode(way.id, node.id), + iD.actions.AddWayNode(other.id, node.id, index)); + + controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); + } + function startFromNode(node) { var graph = history.graph(), parent = graph.parentWays(node)[0], @@ -37,27 +64,13 @@ iD.modes.AddLine = function() { } } - function startFromWay(other, loc, index) { + function startFromMidpoint(midpoint) { var graph = history.graph(), - node = iD.Node({loc: loc}), + node = iD.Node(), way = iD.Way({tags: defaultTags}); history.perform( - iD.actions.AddNode(node), - iD.actions.AddWay(way), - iD.actions.AddWayNode(way.id, node.id), - iD.actions.AddWayNode(other.id, node.id, index)); - - controller.enter(iD.modes.DrawLine(way.id, 'forward', graph)); - } - - function start(loc) { - var graph = history.graph(), - node = iD.Node({loc: loc}), - way = iD.Way({tags: defaultTags}); - - history.perform( - iD.actions.AddNode(node), + iD.actions.AddMidpoint(midpoint, node), iD.actions.AddWay(way), iD.actions.AddWayNode(way.id, node.id)); @@ -65,9 +78,10 @@ iD.modes.AddLine = function() { } behavior = iD.behavior.AddWay(mode) - .on('startFromNode', startFromNode) + .on('start', start) .on('startFromWay', startFromWay) - .on('start', start); + .on('startFromNode', startFromNode) + .on('startFromMidpoint', startFromMidpoint); mode.map.surface.call(behavior); mode.map.tail('Click on the map to start drawing an road, path, or route.', true); diff --git a/js/id/modes/add_point.js b/js/id/modes/add_point.js index 1a51c314d..1b9781a9d 100644 --- a/js/id/modes/add_point.js +++ b/js/id/modes/add_point.js @@ -16,22 +16,33 @@ iD.modes.AddPoint = function() { map.tail('Click on the map to add a point.', true); - function add() { - var node = iD.Node({loc: map.mouseCoordinates()}); + function add(loc) { + var node = iD.Node({loc: loc}); history.perform( iD.actions.AddNode(node), - 'added a point'); + 'Added a point.'); controller.enter(iD.modes.Select(node, true)); } + function addWay(way, loc, index) { + add(loc); + } + + function addNode(node) { + add(node.loc); + } + function cancel() { controller.exit(); } - behavior = iD.behavior.Draw() - .on('add', add) + behavior = iD.behavior.Draw(map) + .on('click', add) + .on('clickWay', addWay) + .on('clickNode', addNode) + .on('clickMidpoint', addNode) .on('cancel', cancel) .on('finish', cancel) (surface); diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 9782bac27..ab6bc80cb 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -3,7 +3,7 @@ iD.modes.Browse = function() { button: 'browse', id: 'browse', title: 'Browse', - description: 'Pan and zoom the map', + description: 'Pan and zoom the map.', key: 'b' }; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 721452bae..8423ff436 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -8,33 +8,21 @@ iD.modes.DrawArea = function(wayId, baseGraph) { mode.enter = function() { var way = mode.history.graph().entity(wayId), - index = -1, headId = way.nodes[way.nodes.length - 2], - tailId = way.first(), - annotation = way.isDegenerate() ? 'started an area' : 'continued an area'; + tailId = way.first(); - function addHeadTail() { - behavior.finish(); - } + behavior = iD.behavior.DrawWay(wayId, -1, mode, baseGraph) + .annotation(way.isDegenerate() ? 'started an area' : 'continued an area'); - function addNode(node) { - behavior.addNode(node, annotation); - } + var addNode = behavior.addNode; - function addWay(way, loc, index) { - behavior.addWay(way, loc, index, annotation); - } - - function add(loc) { - behavior.add(loc, annotation); - } - - behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph) - .on('addHead', addHeadTail) - .on('addTail', addHeadTail) - .on('addNode', addNode) - .on('addWay', addWay) - .on('add', add); + behavior.addNode = function(node) { + if (node.id === headId || node.id === tailId) { + behavior.finish(); + } else { + addNode(node); + } + }; mode.map.surface.call(behavior); mode.map.tail('Click to add points to your area. Click the first point to finish the area.', true); diff --git a/js/id/modes/draw_line.js b/js/id/modes/draw_line.js index 289d91c14..ae54ab2e1 100644 --- a/js/id/modes/draw_line.js +++ b/js/id/modes/draw_line.js @@ -9,41 +9,20 @@ iD.modes.DrawLine = function(wayId, direction, baseGraph) { mode.enter = function() { var way = mode.history.graph().entity(wayId), index = (direction === 'forward') ? undefined : 0, - headId = (direction === 'forward') ? way.last() : way.first(), - tailId = (direction === 'forward') ? way.first() : way.last(), - annotation = way.isDegenerate() ? 'started a line' : 'continued a line'; + headId = (direction === 'forward') ? way.last() : way.first(); - function addHead() { - behavior.finish(); - } + behavior = iD.behavior.DrawWay(wayId, index, mode, baseGraph) + .annotation(way.isDegenerate() ? 'Started a line.' : 'Continued a line.'); - function addTail(node) { - // connect the way in a loop - if (way.nodes.length > 2) { - behavior.addNode(node, annotation); + var addNode = behavior.addNode; + + behavior.addNode = function(node) { + if (node.id === headId) { + behavior.finish(); } else { - behavior.cancel(); + addNode(node); } - } - - function addNode(node) { - behavior.addNode(node, annotation); - } - - function addWay(way, loc, index) { - behavior.addWay(way, loc, index, annotation); - } - - function add(loc) { - behavior.add(loc, annotation); - } - - behavior = iD.behavior.DrawWay(wayId, headId, tailId, index, mode, baseGraph) - .on('addHead', addHead) - .on('addTail', addTail) - .on('addNode', addNode) - .on('addWay', addWay) - .on('add', add); + }; mode.map.surface.call(behavior); mode.map.tail('Click to add more points to the line. ' + diff --git a/js/id/modes/move_way.js b/js/id/modes/move_way.js index 1f559da82..18c6ee619 100644 --- a/js/id/modes/move_way.js +++ b/js/id/modes/move_way.js @@ -18,7 +18,7 @@ iD.modes.MoveWay = function(wayId) { history.perform( iD.actions.Noop(), - 'moved a way'); + 'Moved a way.'); function move() { var p = d3.mouse(selection.node()), @@ -29,7 +29,7 @@ iD.modes.MoveWay = function(wayId) { history.replace( iD.actions.MoveWay(wayId, delta, projection), - 'moved a way'); + 'Moved a way.'); } function finish() { diff --git a/js/id/modes/select.js b/js/id/modes/select.js index 3b1fca4b2..5f33df018 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -14,7 +14,7 @@ iD.modes.Select = function(entity, initial) { if (!_.isEqual(entity.tags, tags)) { mode.history.perform( iD.actions.ChangeEntityTags(d.id, tags), - 'changed tags'); + 'Changed tags.'); } } @@ -116,7 +116,7 @@ iD.modes.Select = function(entity, initial) { history.perform( iD.actions.AddNode(node), iD.actions.AddWayNode(datum.id, node.id, choice.index), - 'added a point to a road'); + 'Added a point to a road.'); d3.event.preventDefault(); d3.event.stopPropagation(); diff --git a/js/id/operations/circularize.js b/js/id/operations/circularize.js index 49956430e..ce161aef4 100644 --- a/js/id/operations/circularize.js +++ b/js/id/operations/circularize.js @@ -10,12 +10,12 @@ iD.operations.Circularize = function(entityId, mode) { if (geometry === 'line') { history.perform( action, - 'made a line circular'); + 'Made a line circular.'); } else if (geometry === 'area') { history.perform( action, - 'made an area circular'); + 'Made an area circular.'); } }; diff --git a/js/id/operations/delete.js b/js/id/operations/delete.js index 03d788261..cc3750360 100644 --- a/js/id/operations/delete.js +++ b/js/id/operations/delete.js @@ -9,22 +9,22 @@ iD.operations.Delete = function(entityId, mode) { if (geometry === 'vertex') { history.perform( iD.actions.DeleteNode(entityId), - 'deleted a vertex'); + 'Deleted a vertex.'); } else if (geometry === 'point') { history.perform( iD.actions.DeleteNode(entityId), - 'deleted a point'); + 'Deleted a point.'); } else if (geometry === 'line') { history.perform( iD.actions.DeleteWay(entityId), - 'deleted a line'); + 'Deleted a line.'); } else if (geometry === 'area') { history.perform( iD.actions.DeleteWay(entityId), - 'deleted an area'); + 'Deleted an area.'); } }; @@ -41,7 +41,7 @@ iD.operations.Delete = function(entityId, mode) { operation.id = "delete"; operation.key = "⌫"; operation.title = "Delete"; - operation.description = "Remove this from the map"; + operation.description = "Remove this from the map."; return operation; }; diff --git a/js/id/operations/reverse.js b/js/id/operations/reverse.js index b36dfd60c..62fd8810b 100644 --- a/js/id/operations/reverse.js +++ b/js/id/operations/reverse.js @@ -4,7 +4,7 @@ iD.operations.Reverse = function(entityId, mode) { var operation = function() { history.perform( iD.actions.ReverseWay(entityId), - 'reversed a line'); + 'Reversed a line.'); }; operation.available = function() { @@ -20,7 +20,7 @@ iD.operations.Reverse = function(entityId, mode) { operation.id = "reverse"; operation.key = "V"; operation.title = "Reverse"; - operation.description = "Make this way go in the opposite direction"; + operation.description = "Make this way go in the opposite direction."; return operation; }; diff --git a/js/id/operations/split.js b/js/id/operations/split.js index 3dcab7481..40aa55fd0 100644 --- a/js/id/operations/split.js +++ b/js/id/operations/split.js @@ -3,7 +3,7 @@ iD.operations.Split = function(entityId, mode) { action = iD.actions.SplitWay(entityId); var operation = function() { - history.perform(action, 'split a way'); + history.perform(action, 'Split a way.'); }; operation.available = function() { @@ -20,7 +20,7 @@ iD.operations.Split = function(entityId, mode) { operation.id = "split"; operation.key = "X"; operation.title = "Split"; - operation.description = "Split this into two ways at this point"; + operation.description = "Split this into two ways at this point."; return operation; }; diff --git a/js/id/operations/unjoin.js b/js/id/operations/unjoin.js index c18de3029..f38333bb4 100644 --- a/js/id/operations/unjoin.js +++ b/js/id/operations/unjoin.js @@ -3,7 +3,7 @@ iD.operations.Unjoin = function(entityId, mode) { action = iD.actions.UnjoinNode(entityId); var operation = function() { - history.perform(action, 'unjoined lines'); + history.perform(action, 'Unjoined lines.'); }; operation.available = function() { @@ -20,7 +20,7 @@ iD.operations.Unjoin = function(entityId, mode) { operation.id = "unjoin"; operation.key = "⇧-J"; operation.title = "Unjoin"; - operation.description = "Disconnect these ways from each other"; + operation.description = "Disconnect these ways from each other."; return operation; }; diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 6476e7a28..752853e6b 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -1,4 +1,10 @@ iD.Background = function() { + + var deviceRatio = (window.devicePixelRatio && + window.devicePixelRatio === 2) ? 0.5 : 1; + // tileSize = (deviceRatio === 0.5) ? [128,128] : [256,256]; + tileSize = [256, 256]; + var tile = d3.geo.tile(), projection, cache = {}, @@ -13,7 +19,12 @@ iD.Background = function() { '-o-transform-origin:0 0;' + '-webkit-user-select: none;' + '-webkit-user-drag: none;' + - '-moz-user-drag: none;'; + '-moz-user-drag: none;' + + 'opacity:0;'; + + function tileSizeAtZoom(d, z) { + return Math.ceil(tileSize[0] * Math.pow(2, z - d[2])) / tileSize[0]; + } function atZoom(t, distance) { var power = Math.pow(2, distance); @@ -21,108 +32,101 @@ iD.Background = function() { Math.floor(t[0] * power), Math.floor(t[1] * power), t[2] + distance]; - az.push(source(az)); return az; } - function upZoom(t, distance) { - var az = atZoom(t, distance), tiles = []; - for (var x = 0; x < 2; x++) { - for (var y = 0; y < 2; y++) { - var up = [az[0] + x, az[1] + y, az[2]]; - up.push(source(up)); - tiles.push(up); - } - } - return tiles; - } - - function tileSize(d, z) { - return Math.ceil(256 * Math.pow(2, z - d[2])) / 256; - } - function lookUp(d) { for (var up = -1; up > -d[2]; up--) { if (cache[atZoom(d, up)] !== false) return atZoom(d, up); } } + function uniqueBy(a, n) { + var o = [], seen = {}; + for (var i = 0; i < a.length; i++) { + if (seen[a[i][n]] === undefined) { + o.push(a[i]); + seen[a[i][n]] = true; + } + } + return o; + } + + function addSource(d) { + d.push(source(d)); + return d; + } + // derive the tiles onscreen, remove those offscreen and position tiles // correctly for the currentstate of `projection` function background() { - var tiles = tile + var sel = this, + tiles = tile .scale(projection.scale()) .scaleExtent(source.scaleExtent || [1, 17]) .translate(projection.translate())(), + requests = [], scaleExtent = tile.scaleExtent(), z = Math.max(Math.log(projection.scale()) / Math.log(2) - 8, 0), - rz = Math.max(scaleExtent[0], Math.min(scaleExtent[1], Math.floor(z))), - ts = 256 * Math.pow(2, z - rz), + rz = Math.max(scaleExtent[0], + Math.min(scaleExtent[1], Math.floor(z))), + ts = tileSize[0] * Math.pow(2, z - rz), tile_origin = [ projection.scale() / 2 - projection.translate()[0], - projection.scale() / 2 - projection.translate()[1]], - ups = {}; + projection.scale() / 2 - projection.translate()[1]]; tiles.forEach(function(d) { - - if (cache[d] === true) { - d.push(source(d)); - } else if (cache[d] === false && - cache[atZoom(d, -1)] !== false && - !ups[atZoom(d, -1)]) { - - ups[atZoom(d, -1)] = true; - tiles.push(atZoom(d, -1)); - - } else if (cache[d] === undefined && - lookUp(d)) { - - var upTile = lookUp(d); - if (!ups[upTile]) { - ups[upTile] = true; - tiles.push(upTile); - } - - } else if (cache[d] === undefined || - cache[d] === false) { - upZoom(d, 1).forEach(function(u) { - if (cache[u] && !ups[u]) { - ups[u] = true; - tiles.push(u); - } - }); + addSource(d); + requests.push(d); + if (!cache[d[3]] && lookUp(d)) { + requests.push(addSource(lookUp(d))); } }); - var image = this - .selectAll('img') - .data(tiles, function(d) { return d; }); + requests = uniqueBy(requests, 3); function load(d) { - cache[d.slice(0, 3)] = true; - d3.select(this).on('load', null); + cache[d[3]] = true; + d3.select(this) + .on('load', null) + .transition() + .style('opacity', 1); + background.apply(sel); } function error(d) { - cache[d.slice(0, 3)] = false; + cache[d[3]] = false; + d3.select(this).on('load', null); d3.select(this).remove(); + background.apply(sel); } + function imageTransform(d) { + var _ts = tileSize[0] * Math.pow(2, z - d[2]); + var scale = tileSizeAtZoom(d, z); + return 'translate(' + + (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' + + (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px)' + + 'scale(' + scale + ',' + scale + ')'; + } + + var image = this + .selectAll('img') + .data(requests, function(d) { return d[3]; }); + + image.exit() + .style(transformProp, imageTransform) + .transition() + .style('opacity', 0) + .remove(); + image.enter().append('img') .attr('style', imgstyle) .attr('src', function(d) { return d[3]; }) .on('error', error) .on('load', load); - - image.exit().remove(); - - image.style(transformProp, function(d) { - var _ts = 256 * Math.pow(2, z - d[2]); - var scale = tileSize(d, z); - return 'translate(' + - (Math.round((d[0] * _ts) - tile_origin[0]) + offset[0]) + 'px,' + - (Math.round((d[1] * _ts) - tile_origin[1]) + offset[1]) + 'px) scale(' + scale + ',' + scale + ')'; - }); + + image.style(transformProp, imageTransform); if (Object.keys(cache).length > 100) cache = {}; } diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 487ac736a..08c18e915 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -64,7 +64,7 @@ iD.Map = function() { for (var i = 0; i < parents.length; i++) { var parent = parents[i]; if (only[parent.id] === undefined) { - only[parent.id] = graph.entity(parent.id); + only[parent.id] = parent; addParents(graph.parentRelations(parent)); } } @@ -92,7 +92,7 @@ iD.Map = function() { all = _.compact(_.values(only)); filter = function(d) { - if (d.midpoint) { + if (d.type === 'midpoint') { for (var i = 0; i < d.ways.length; i++) { if (d.ways[i].id in only) return true; } @@ -112,7 +112,7 @@ iD.Map = function() { .call(areas, graph, all, filter) .call(multipolygons, graph, all, filter) .call(midpoints, graph, all, filter) - .call(labels, graph, all, filter, dimensions); + .call(labels, graph, all, filter, dimensions, !difference); } dispatch.drawn(map); } @@ -123,7 +123,7 @@ iD.Map = function() { function connectionLoad(err, result) { history.merge(result); - redraw(Object.keys(result.entities)); + redraw(Object.keys(result)); } function zoomPan() { @@ -165,7 +165,8 @@ iD.Map = function() { } function resetTransform() { - if (!surface.style(transformProp)) return false; + var prop = surface.style(transformProp); + if (!prop || prop === 'none') return false; surface.style(transformProp, ''); tilegroup.style(transformProp, ''); return true; diff --git a/js/id/svg/labels.js b/js/id/svg/labels.js index 2fcc2fcc1..3962c4915 100644 --- a/js/id/svg/labels.js +++ b/js/id/svg/labels.js @@ -156,7 +156,7 @@ iD.svg.Labels = function(projection) { for (var i = 0; i < nodes.length - 1; i++) { var current = segmentLength(i); var portion; - if (!start && sofar + current > from) { + if (!start && sofar + current >= from) { portion = (from - sofar) / current; start = [ nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), @@ -164,7 +164,7 @@ iD.svg.Labels = function(projection) { ]; i0 = i + 1; } - if (!end && sofar + current > to) { + if (!end && sofar + current >= to) { portion = (to - sofar) / current; end = [ nodes[i][0] + portion * (nodes[i + 1][0] - nodes[i][0]), @@ -183,14 +183,26 @@ iD.svg.Labels = function(projection) { } - return function drawLabels(surface, graph, entities, filter, dimensions) { + var rtree = new RTree(), + rectangles = {}; + + return function drawLabels(surface, graph, entities, filter, dimensions, fullRedraw) { + - var rtree = new RTree(); var hidePoints = !d3.select('.node.point').node(); var labelable = [], i, k, entity; for (i = 0; i < label_stack.length; i++) labelable.push([]); + if (fullRedraw) { + rtree = new RTree(); + rectangles = {}; + } else { + for (i = 0; i < entities.length; i++) { + rtree.remove(rectangles[entities[i].id], entities[i].id); + } + } + // Split entities into groups specified by label_stack for (i = 0; i < entities.length; i++) { entity = entities[i]; @@ -252,7 +264,7 @@ iD.svg.Labels = function(projection) { textAnchor: offset[2] }; var rect = new RTree.Rectangle(p.x - m, p.y - m, width + 2*m, height + 2*m); - if (tryInsert(rect)) return p; + if (tryInsert(rect, entity.id)) return p; } @@ -275,7 +287,7 @@ iD.svg.Labels = function(projection) { Math.abs(sub[0][1] - sub[sub.length - 1][1]) + 30 ); if (rev) sub = sub.reverse(); - if (tryInsert(rect)) return { + if (tryInsert(rect, entity.id)) return { 'font-size': height + 2, lineString: lineString(sub), startOffset: offset + '%' @@ -298,16 +310,19 @@ iD.svg.Labels = function(projection) { height: height }; var rect = new RTree.Rectangle(p.x - width/2, p.y, width, height); - if (tryInsert(rect)) return p; + if (tryInsert(rect, entity.id)) return p; } - function tryInsert(rect) { + function tryInsert(rect, id) { // Check that label is visible if (rect.x1 < 0 || rect.y1 < 0 || rect.x2 > dimensions[0] || rect.y2 > dimensions[1]) return false; var v = rtree.search(rect, true).length === 0; - if (v) rtree.insert(rect); + if (v) { + rtree.insert(rect, id); + rectangles[id] = rect; + } return v; } diff --git a/js/id/svg/midpoints.js b/js/id/svg/midpoints.js index 655e35013..ee8d2c5e2 100644 --- a/js/id/svg/midpoints.js +++ b/js/id/svg/midpoints.js @@ -15,19 +15,15 @@ iD.svg.Midpoints = function(projection) { b = nodes[j + 1], id = [a.id, b.id].sort().join('-'); - if (!midpoints[id] && - iD.geo.dist(projection(a.loc), projection(b.loc)) > 40) { - - var midpoint_loc = iD.geo.interp(a.loc, b.loc, 0.5), - parents = _.intersection(graph.parentWays(a), - graph.parentWays(b)); + if (midpoints[id]) { + midpoints[id].ways.push({id: entity.id, index: j + 1}); + } else if (iD.geo.dist(projection(a.loc), projection(b.loc)) > 40) { midpoints[id] = { - loc: midpoint_loc, - ways: parents, - nodes: [a.id, b.id], + type: 'midpoint', id: id, - midpoint: true + loc: iD.geo.interp(a.loc, b.loc, 0.5), + ways: [{id: entity.id, index: j + 1}] }; } } @@ -35,7 +31,7 @@ iD.svg.Midpoints = function(projection) { var groups = surface.select('.layer-hit').selectAll('g.midpoint') .filter(filter) - .data(_.values(midpoints), function (d) { return [d.parents, d.id].join(","); }); + .data(_.values(midpoints), function (d) { return d.id; }); var group = groups.enter() .insert('g', ':first-child') diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js index 06be536ec..ba28c968a 100644 --- a/js/id/svg/vertices.js +++ b/js/id/svg/vertices.js @@ -36,7 +36,7 @@ iD.svg.Vertices = function(projection) { 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; }); + .classed('shared', function(entity) { return graph.isShared(entity); }); // Selecting the following implicitly // sets the data (vertix entity) on the elements diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index c0036a677..c64d8a2ac 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -16,7 +16,7 @@ iD.ui.contributors = function(map) { var l = selection .select('.contributor-list') .selectAll('a.user-link') - .data(subset); + .data(subset, function(d) { return d; }); l.enter().append('a') @@ -43,6 +43,10 @@ iD.ui.contributors = function(map) { ext[1][0], ext[1][1]]; }) .text(' and ' + (u.length - limit) + ' others'); + } else { + selection + .select('.contributor-count') + .html(''); } if (!u.length) { diff --git a/js/id/ui/notice.js b/js/id/ui/notice.js index bcf44b7c9..12b1a2c09 100644 --- a/js/id/ui/notice.js +++ b/js/id/ui/notice.js @@ -4,20 +4,17 @@ iD.ui.notice = function(selection) { notice = {}; var div = selection.append('div') - .attr('class', 'notice') - .append('div') - .attr('class', 'notice-inner'); + .attr('class', 'notice'); - div.append('button') - .attr('class', 'zoom-to') - .on('click', function() { - event.zoom(); - }) - .append('span') - .attr('class', 'icon invert zoom-in'); + var button = div.append('button') + .attr('class', 'zoom-to notice') + .on('click', event.zoom); - div.append('span') - .attr('class', 'notice-text') + button.append('span') + .attr('class', 'icon zoom-in-invert'); + + button.append('span') + .attr('class', 'label') .text(t('zoom_in_edit')); notice.message = function(_) { diff --git a/js/id/util.js b/js/id/util.js index fac92fba0..f5cfd4450 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -94,3 +94,5 @@ iD.util.editDistance = function(a, b) { } return matrix[b.length][a.length]; }; + +iD.util.getPrototypeOf = Object.getPrototypeOf || function(obj) { return obj.__proto__; }; diff --git a/presets/convert_potlatch.py b/presets/convert_potlatch.py new file mode 100644 index 000000000..521081429 --- /dev/null +++ b/presets/convert_potlatch.py @@ -0,0 +1,47 @@ +from xml.dom.minidom import parse +import json + +dom1 = parse('potlatch.xml') + +inputSets = dom1.getElementsByTagName('inputSet') + +jsonOutput = [] + +for inputSet in inputSets: + setId = inputSet.getAttribute('id') + inputs = inputSet.getElementsByTagName('input') + for i in inputs: + jsonInput = {} + inputType = i.getAttribute('type') + if inputType == 'choice': + choices = i.getElementsByTagName('choice') + jsonInput['type'] = 'choice' + jsonInput['description'] = i.getAttribute('description') + jsonInput['name'] = i.getAttribute('name') + jsonInput['key'] = i.getAttribute('key') + jsonInput['choices'] = [] + for c in choices: + jsonInput['choices'].append({ + "value": c.getAttribute('value'), + "text": c.getAttribute('text') + }) + elif inputType == 'freetext': + jsonInput['type'] = 'freetext' + jsonInput['description'] = i.getAttribute('description') + jsonInput['name'] = i.getAttribute('name') + jsonInput['key'] = i.getAttribute('key') + elif inputType == 'checkbox': + jsonInput['type'] = 'checkbox' + jsonInput['description'] = i.getAttribute('description') + jsonInput['name'] = i.getAttribute('name') + jsonInput['key'] = i.getAttribute('key') + elif inputType == 'number': + jsonInput['type'] = 'number' + jsonInput['description'] = i.getAttribute('description') + jsonInput['name'] = i.getAttribute('name') + jsonInput['minimum'] = i.getAttribute('minimum') + jsonInput['maximum'] = i.getAttribute('maximum') + jsonInput['key'] = i.getAttribute('key') + jsonOutput.append(jsonInput) + +json.dump(jsonOutput, open('presets_potlatch.json', 'w'), indent=4) diff --git a/presets/potlatch.xml b/presets/potlatch.xml new file mode 100644 index 000000000..ed01d3e1a --- /dev/null +++ b/presets/potlatch.xml @@ -0,0 +1,693 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://wiki.openstreetmap.org/wiki/Key:cuisine + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${ref}
+ ${name} +
+ + + + + + + ${ref}
+ ${name} +
+ + + + + + + ${ref}
+ ${name} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${operator} ${ref} + + + + + + + + + + ${operator} ${ref} + + + + + + + + + + ${name|operator} (${ref}) + + + + + + + + + + + + ${ref}
+ ${name} +
+ + + + + + + ${ref}
+ ${name} +
+ + + + + + + ${ref}
+ ${name} +
+ + + + + + ${ref}
+ ${name} +
+ +
+ + + + + + + + + http://wiki.openstreetmap.org/wiki/Key:access + + + + + + + + + + + + http://wiki.openstreetmap.org/wiki/Key:cycleway + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + http://wiki.openstreetmap.org/wiki/Key:building + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + advanced + + + + + +
diff --git a/presets/presets_potlatch.json b/presets/presets_potlatch.json new file mode 100644 index 000000000..ac674d5cc --- /dev/null +++ b/presets/presets_potlatch.json @@ -0,0 +1,1265 @@ +[ + { + "type": "freetext", + "description": "The name", + "key": "name", + "name": "Name" + }, + { + "type": "freetext", + "description": "A reference number or code used to identify this thing.", + "key": "ref", + "name": "Reference number" + }, + { + "type": "freetext", + "description": "Brand, i.e. Acme", + "key": "brand", + "name": "Brand" + }, + { + "type": "freetext", + "description": "Operator, i.e. Acme Springfield Ltd", + "key": "operator", + "name": "Operator" + }, + { + "type": "freetext", + "description": "The primary source of information for this object (GPS, survey, Bing, ...)", + "key": "source", + "name": "Source" + }, + { + "type": "freetext", + "description": "The official designation or classification (if any). Only use this if the organisation that runs it has its own classification system.", + "key": "designation", + "name": "Official classification" + }, + { + "type": "freetext", + "description": "The most common name", + "key": "name", + "name": "Name" + }, + { + "type": "freetext", + "description": "The internationally recognised name", + "key": "int_name", + "name": "International Name" + }, + { + "type": "freetext", + "description": "The historic or previous name", + "key": "old_name", + "name": "Historical Name" + }, + { + "type": "freetext", + "description": "An alternative, currently used, name", + "key": "alt_name", + "name": "Alternative Name" + }, + { + "choices": [ + { + "text": "Free", + "value": "free" + }, + { + "text": "Yes", + "value": "yes" + }, + { + "text": "No", + "value": "no" + } + ], + "type": "choice", + "description": "", + "key": "wifi", + "name": "Wifi" + }, + { + "type": "freetext", + "description": "The number of the house, e.g. 156 or 10-12", + "key": "addr:housenumber", + "name": "House Number" + }, + { + "type": "freetext", + "description": "The name of the house, e.g. Riverbank Cottage", + "key": "addr:housename", + "name": "Building Name" + }, + { + "type": "freetext", + "description": "The Street Name (optional)", + "key": "addr:street", + "name": "Street Name" + }, + { + "type": "freetext", + "description": "The postcode", + "key": "addr:postcode", + "name": "Postcode" + }, + { + "type": "freetext", + "description": "The URL of the website", + "key": "website", + "name": "Website" + }, + { + "choices": [ + { + "text": "Burger", + "value": "burger" + }, + { + "text": "Chicken", + "value": "chicken" + }, + { + "text": "Chinese", + "value": "chinese" + }, + { + "text": "Coffee Shop", + "value": "coffee_shop" + }, + { + "text": "Greek", + "value": "greek" + }, + { + "text": "Pizza", + "value": "pizza" + }, + { + "text": "Sandwich", + "value": "sandwich" + }, + { + "text": "Sea Food", + "value": "seafood" + }, + { + "text": "Regional", + "value": "regional" + }, + { + "text": "Italian", + "value": "italian" + }, + { + "text": "German", + "value": "german" + }, + { + "text": "Kebab/souvlaki/gyro", + "value": "kebab" + }, + { + "text": "Indian", + "value": "indian" + }, + { + "text": "Asian", + "value": "asian" + }, + { + "text": "Mexican", + "value": "mexican" + }, + { + "text": "Thai", + "value": "thai" + }, + { + "text": "Japanese", + "value": "japanese" + }, + { + "text": "Ice-cream", + "value": "ice_cream" + }, + { + "text": "Fish & Chips", + "value": "fish_and_chips" + }, + { + "text": "Turkish", + "value": "turkish" + }, + { + "text": "French", + "value": "french" + }, + { + "text": "Sushi", + "value": "sushi" + }, + { + "text": "American", + "value": "american" + }, + { + "text": "Steak House", + "value": "steak_house" + }, + { + "text": "International", + "value": "international" + }, + { + "text": "Spanish", + "value": "spanish" + }, + { + "text": "Vietnamese", + "value": "vietnamese" + }, + { + "text": "Fish", + "value": "fish" + }, + { + "text": "Bavarian", + "value": "bavarian" + }, + { + "text": "Vegetarian", + "value": "vegetarian" + } + ], + "type": "choice", + "description": "The type of food that they serve", + "key": "cuisine", + "name": "Cuisine" + }, + { + "type": "freetext", + "description": "The official reference number", + "key": "ref", + "name": "Reference" + }, + { + "type": "freetext", + "description": "The official international reference number", + "key": "int_ref", + "name": "International Reference" + }, + { + "type": "freetext", + "description": "The historic or previous reference number", + "key": "old_ref", + "name": "Old Reference" + }, + { + "type": "freetext", + "description": "Width of the road", + "key": "width", + "name": "Width" + }, + { + "choices": [ + { + "text": "Unpaved", + "value": "unpaved" + }, + { + "text": "Paved", + "value": "paved" + }, + { + "text": "Asphalt", + "value": "asphalt" + }, + { + "text": "Concrete", + "value": "concrete" + }, + { + "text": "Paving stones", + "value": "paving_stones" + }, + { + "text": "Cobblestone", + "value": "cobblestone" + }, + { + "text": "Sand", + "value": "sand" + }, + { + "text": "Gravel", + "value": "gravel" + }, + { + "text": "Dirt", + "value": "dirt" + }, + { + "text": "Grass", + "value": "grass" + } + ], + "type": "choice", + "description": "Type of road surface", + "key": "surface", + "name": "Surface" + }, + { + "type": "checkbox", + "description": "The way is a large open space, like at a dock, where vehicles can move anywhere within the space, rather than just along the edge.", + "key": "area", + "name": "Open area" + }, + { + "description": "Total number of lanes, counting both directions", + "maximum": 10.0, + "minimum": 1.0, + "key": "lanes", + "type": "number", + "name": "Lanes" + }, + { + "choices": [ + { + "text": "Generic Bridge", + "value": "yes" + }, + { + "text": "Viaduct", + "value": "viaduct" + }, + { + "text": "Suspension bridge", + "value": "suspension" + } + ], + "type": "choice", + "description": "Road goes over a bridge", + "key": "bridge", + "name": "Bridge" + }, + {}, + { + "choices": [ + { + "text": "Tunnel", + "value": "yes" + } + ], + "type": "choice", + "description": "Road goes into a tunnel", + "key": "tunnel", + "name": "Tunnel" + }, + { + "choices": [ + { + "text": "Embankment", + "value": "yes" + } + ], + "type": "choice", + "description": "Road supported on a raised bed of earth and rock.", + "key": "embankment", + "name": "Embankment" + }, + { + "choices": [ + { + "text": "Cutting", + "value": "yes" + } + ], + "type": "choice", + "description": "Road carved out of hill on one or both sides.", + "key": "cutting", + "name": "Cutting" + }, + { + "choices": [ + { + "text": "Yes", + "value": "yes" + }, + { + "text": "Overhead line", + "value": "contact_line" + }, + { + "text": "Third rail", + "value": "rail" + }, + { + "text": "No", + "value": "no" + } + ], + "type": "choice", + "description": "Is the track electrified (whether by 3rd rail, overhead wires, etc)?", + "key": "electrified", + "name": "Electrified" + }, + { + "choices": [ + { + "text": "600V", + "value": "600" + }, + { + "text": "750V", + "value": "750" + }, + { + "text": "1500V", + "value": "1500" + }, + { + "text": "3000V", + "value": "3000" + }, + { + "text": "12kV", + "value": "12000" + }, + { + "text": "15kV", + "value": "15000" + }, + { + "text": "25kV", + "value": "25000" + } + ], + "type": "choice", + "description": "Nominal voltage of electric wires", + "key": "voltage", + "name": "Voltage" + }, + { + "choices": [ + { + "text": "DC", + "value": "0" + }, + { + "text": "16.67 Hz", + "value": "16.67" + }, + { + "text": "16.7 Hz", + "value": "16.7" + }, + { + "text": "25 Hz", + "value": "25" + }, + { + "text": "50 Hz", + "value": "50" + }, + { + "text": "60 Hz", + "value": "60" + } + ], + "type": "choice", + "description": "Frequency in Hertz of alternating current power supply", + "key": "frequency", + "name": "Frequency" + }, + { + "type": "freetext", + "description": "The charge/cost of using this amenity", + "key": "fee", + "name": "Fee" + }, + { + "choices": [ + { + "text": "One way", + "value": "yes" + }, + { + "text": "Two way", + "value": "no" + }, + { + "text": "One way reverse", + "value": "-1" + } + ], + "type": "choice", + "description": "Oneway roads", + "key": "oneway", + "name": "Oneway" + }, + {}, + { + "choices": [ + { + "text": "Yes", + "value": "roundabout" + } + ], + "type": "choice", + "description": "Whether this road is a roundabout. Make the way face the direction appropriate for the country.", + "key": "junction", + "name": "Roundabout" + }, + {}, + { + "choices": [ + { + "text": "Yes", + "value": "traffic_signals" + } + ], + "type": "choice", + "description": "Intersection controlled by traffic lights", + "key": "highway", + "name": "Traffic signals" + }, + { + "choices": [ + { + "text": "Allowed", + "value": "yes" + }, + { + "text": "Prohibited", + "value": "no" + }, + { + "text": "Designated", + "value": "designated" + } + ], + "type": "choice", + "description": "Can pedestrians use this road, including footpaths if any?", + "key": "foot", + "name": "Pedestrians permitted" + }, + { + "choices": [ + { + "text": "Both", + "value": "both" + }, + { + "text": "Left", + "value": "left" + }, + { + "text": "Right", + "value": "right" + }, + { + "text": "Separate", + "value": "separate" + }, + { + "text": "None", + "value": "none" + } + ], + "type": "choice", + "description": "Whether there is a sidewalk at the side of the street", + "key": "sidewalk", + "name": "Sidewalks" + }, + {}, + {}, + {}, + { + "type": "freetext", + "description": "12 character internal Naptan ID", + "key": "naptan:AtcoCode", + "name": "Atco Code" + }, + { + "choices": [ + { + "text": "N", + "value": "N" + }, + { + "text": "NE", + "value": "NE" + }, + { + "text": "E", + "value": "E" + }, + { + "text": "SE", + "value": "SE" + }, + { + "text": "S", + "value": "S" + }, + { + "text": "SW", + "value": "SW" + }, + { + "text": "W", + "value": "W" + }, + { + "text": "NW", + "value": "NW" + } + ], + "type": "choice", + "description": "The eight-point compass bearning", + "key": "naptan:Bearing", + "name": "Naptan Bearing" + }, + { + "type": "freetext", + "description": "The naptan common name", + "key": "naptan:CommonName", + "name": "Naptan Common Name (read-only)" + }, + { + "type": "freetext", + "description": "", + "key": "naptan:Indicator", + "name": "Naptan Indicator (read-only)" + }, + { + "type": "freetext", + "description": "", + "key": "naptan:Street", + "name": "Naptan Street (read-only)" + }, + { + "type": "freetext", + "description": "Delete this when the details have been verified on-the-ground", + "key": "naptan:verified", + "name": "Naptan Verified?" + }, + { + "type": "freetext", + "description": "The name of the bus stop", + "key": "name", + "name": "Stop Name" + }, + { + "type": "freetext", + "description": "The local reference of the stop, usually one or two letters above the main flag, used at bus interchanges, e.g. L, BX", + "key": "local_ref", + "name": "Local Ref" + }, + {}, + {}, + {}, + {}, + {}, + {}, + {}, + { + "choices": [ + { + "text": "Allowed", + "value": "yes" + }, + { + "text": "Prohibited", + "value": "no" + }, + { + "text": "Private", + "value": "private" + }, + { + "text": "Cyclists dismount", + "value": "dismount" + }, + { + "text": "Designated", + "value": "designated" + } + ], + "type": "choice", + "description": "Are bicyles allowed to use this road (regardless of physical suitability)?", + "key": "bicycle", + "name": "Bicycles permitted" + }, + { + "choices": [ + { + "text": "No bike lanes", + "value": "no" + }, + { + "text": "On-road bike lane", + "value": "lane" + }, + { + "text": "Parallel track", + "value": "track" + }, + { + "text": "Contraflow lane", + "value": "opposite_lane" + }, + { + "text": "Contraflow track", + "value": "opposite_track" + }, + { + "text": "Contraflow unmarked", + "value": "opposite" + } + ], + "type": "choice", + "description": "Road has bike lanes within the road surface", + "key": "cycleway", + "name": "Bike lanes" + }, + { + "type": "freetext", + "description": "The width in metres", + "key": "width", + "name": "Width" + }, + { + "choices": [ + { + "text": "Allowed", + "value": "yes" + }, + { + "text": "Prohibited", + "value": "no" + }, + { + "text": "Permissive", + "value": "permissive" + }, + { + "text": "Private", + "value": "private" + }, + { + "text": "Designated", + "value": "designated" + } + ], + "type": "choice", + "description": "Are boats allowed to use this waterway?", + "key": "boat", + "name": "Boat permission" + }, + { + "choices": [ + { + "text": "Allowed", + "value": "yes" + }, + { + "text": "Prohibited", + "value": "no" + }, + { + "text": "Permissive", + "value": "permissive" + }, + { + "text": "Private", + "value": "private" + }, + { + "text": "Designated", + "value": "designated" + } + ], + "type": "choice", + "description": "Is there a general right of access, regardless of mode of transport?", + "key": "access", + "name": "General access" + }, + { + "choices": [ + { + "text": "Allowed", + "value": "yes" + }, + { + "text": "Prohibited", + "value": "no" + }, + { + "text": "Permissive", + "value": "permissive" + }, + { + "text": "Private", + "value": "private" + }, + { + "text": "Designated", + "value": "designated" + } + ], + "type": "choice", + "description": "Are cars and other private vehicles allowed?", + "key": "motor_vehicle", + "name": "Motor vehicles" + }, + { + "choices": [ + { + "text": "Allowed", + "value": "yes" + }, + { + "text": "Prohibited", + "value": "no" + }, + { + "text": "Permissive", + "value": "permissive" + }, + { + "text": "Private", + "value": "private" + }, + { + "text": "Designated", + "value": "designated" + } + ], + "type": "choice", + "description": "Are horses allowed?", + "key": "horse", + "name": "Horses" + }, + { + "choices": [ + { + "text": "Locality", + "value": "locality" + }, + { + "text": "Hamlet", + "value": "hamlet" + }, + { + "text": "Village", + "value": "village" + }, + { + "text": "Suburb", + "value": "suburb" + }, + { + "text": "Town", + "value": "town" + }, + { + "text": "City", + "value": "city" + }, + { + "text": "County", + "value": "county" + }, + { + "text": "Region", + "value": "region" + }, + { + "text": "State", + "value": "state" + }, + { + "text": "Country", + "value": "country" + }, + { + "text": "Continent", + "value": "continent" + }, + { + "text": "Island", + "value": "island" + }, + { + "text": "Islet", + "value": "islet" + } + ], + "type": "choice", + "description": "", + "key": "place", + "name": "Type of Place" + }, + { + "choices": [ + { + "text": "Generic building", + "value": "yes" + }, + { + "text": "Generic residential", + "value": "residential" + }, + { + "text": "Big apartments house", + "value": "apartments" + }, + { + "text": "Terraced house", + "value": "terrace" + }, + { + "text": "Family house", + "value": "house" + }, + { + "text": "Small hut", + "value": "hut" + }, + { + "text": "A garage", + "value": "garage" + }, + { + "text": "Block of garages", + "value": "garages" + }, + { + "text": "Office building", + "value": "office" + }, + { + "text": "Public building", + "value": "public" + }, + { + "text": "Generic industrial", + "value": "industrial" + }, + { + "text": "Manufacture", + "value": "manufacture" + }, + { + "text": "Warehouse", + "value": "warehouse" + }, + { + "text": "Hangar", + "value": "hangar" + }, + { + "text": "Fluids storage tank", + "value": "storage_tank" + }, + { + "text": "Retail", + "value": "retail" + }, + { + "text": "Supermarket", + "value": "supermarket" + }, + { + "text": "Train station", + "value": "train_station" + }, + { + "text": "Church", + "value": "church" + }, + { + "text": "School", + "value": "school" + }, + { + "text": "Military bunker", + "value": "bunker" + }, + { + "text": "Collapsed building", + "value": "collapsed" + }, + { + "text": "Just a roof", + "value": "roof" + } + ], + "type": "choice", + "description": "", + "key": "building", + "name": "Building type, if it is one" + }, + { + "choices": [ + { + "text": "2", + "value": "2" + }, + { + "text": "3", + "value": "3" + }, + { + "text": "4", + "value": "4" + }, + { + "text": "6", + "value": "6" + }, + { + "text": "8", + "value": "8" + }, + { + "text": "10", + "value": "10" + }, + { + "text": "12", + "value": "12" + } + ], + "type": "choice", + "description": "", + "key": "cables", + "name": "Cables" + }, + { + "choices": [ + { + "text": "400 V", + "value": "400" + }, + { + "text": "600 V", + "value": "600" + }, + { + "text": "750 V", + "value": "750" + }, + { + "text": "1500 V", + "value": "1500" + }, + { + "text": "3000 V", + "value": "3000" + }, + { + "text": "15 kV", + "value": "15000" + }, + { + "text": "20 kV", + "value": "20000" + }, + { + "text": "35 kV", + "value": "35000" + }, + { + "text": "110 kV", + "value": "110000" + }, + { + "text": "132 kV", + "value": "132000" + }, + { + "text": "138 kV", + "value": "138000" + }, + { + "text": "220 kV", + "value": "220000" + }, + { + "text": "380 kV", + "value": "380000" + }, + { + "text": "500 kV", + "value": "500000" + } + ], + "type": "choice", + "description": "", + "key": "voltage", + "name": "Voltage" + }, + { + "choices": [ + { + "text": "9 pin bowling", + "value": "9pin" + }, + { + "text": "10 pin bowling", + "value": "10pin" + }, + { + "text": "American football", + "value": "american_football" + }, + { + "text": "Archery", + "value": "archery" + }, + { + "text": "Athletics", + "value": "athletics" + }, + { + "text": "Australian Rules Football", + "value": "australian_football" + }, + { + "text": "Baseball", + "value": "baseball" + }, + { + "text": "Basketball", + "value": "basketball" + }, + { + "text": "Beach volleyball", + "value": "beachvolleyball" + }, + { + "text": "Boules/petanque/bocci", + "value": "boules" + }, + { + "text": "Lawn bowls", + "value": "bowls" + }, + { + "text": "Canadian football", + "value": "canadian_football" + }, + { + "text": "Chess", + "value": "chess" + }, + { + "text": "Cricket", + "value": "cricket" + }, + { + "text": "Cricket nets", + "value": "cricket_nets" + }, + { + "text": "Croquet", + "value": "croquet" + }, + { + "text": "Equestrian", + "value": "equestrian" + }, + { + "text": "Gaelic football", + "value": "gaelic_football" + }, + { + "text": "Gymnastics", + "value": "gymnastics" + }, + { + "text": "(Team) handball", + "value": "team_handball" + }, + { + "text": "(Field) hockey", + "value": "hockey" + }, + { + "text": "Korball", + "value": "korfball" + }, + { + "text": "Pelota", + "value": "pelota" + }, + { + "text": "Rugby league", + "value": "rugby_league" + }, + { + "text": "Rugby union", + "value": "rugby_union" + }, + { + "text": "Shooting", + "value": "shooting" + }, + { + "text": "Ice skating", + "value": "skating" + }, + { + "text": "Skateboarding", + "value": "skateboard" + }, + { + "text": "Soccer/football", + "value": "soccer" + }, + { + "text": "Swimming", + "value": "swimming" + }, + { + "text": "Table tennis", + "value": "table_tennis" + }, + { + "text": "Tennis", + "value": "tennis" + }, + { + "text": "Volleyball", + "value": "volleyball" + } + ], + "type": "choice", + "description": "The sport that is predominantly played here.", + "key": "sport", + "name": "Sport" + }, + { + "choices": [ + { + "text": "Yes: ramps/elevators/etc", + "value": "yes" + }, + { + "text": "No: inaccessible to wheelchairs", + "value": "no" + }, + { + "text": "Limited accessibility", + "value": "limited" + } + ], + "type": "choice", + "description": "", + "key": "wheelchair", + "name": "Wheelchair" + } +] \ No newline at end of file diff --git a/test/index.html b/test/index.html index 4bdd4b5ba..d8d3f8f6f 100644 --- a/test/index.html +++ b/test/index.html @@ -68,6 +68,7 @@ + @@ -137,6 +138,7 @@ + @@ -170,6 +172,7 @@ + @@ -182,6 +185,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index 7ae72a056..719a5a999 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -32,6 +32,7 @@ + @@ -65,6 +66,7 @@ + @@ -77,6 +79,7 @@ + diff --git a/test/spec/actions/add_midpoint.js b/test/spec/actions/add_midpoint.js new file mode 100644 index 000000000..1f749217e --- /dev/null +++ b/test/spec/actions/add_midpoint.js @@ -0,0 +1,22 @@ +describe("iD.actions.AddMidpoint", function () { + it("adds the node at the midpoint location", function () { + var node = iD.Node(), + midpoint = {loc: [1, 2], ways: []}, + graph = iD.actions.AddMidpoint(midpoint, node)(iD.Graph()); + + expect(graph.entity(node.id).loc).to.eql([1, 2]); + }); + + it("adds the node to all ways at the respective indexes", function () { + var node = iD.Node(), + a = iD.Node(), + b = iD.Node(), + w1 = iD.Way(), + w2 = iD.Way({nodes: [a.id, b.id]}), + midpoint = {loc: [1, 2], ways: [{id: w1.id, index: 0}, {id: w2.id, index: 1}]}, + graph = iD.actions.AddMidpoint(midpoint, node)(iD.Graph([a, b, w1, w2])); + + expect(graph.entity(w1.id).nodes).to.eql([node.id]); + expect(graph.entity(w2.id).nodes).to.eql([a.id, node.id, b.id]); + }); +}); diff --git a/test/spec/actions/delete_way.js b/test/spec/actions/delete_way.js index 863b0fa2f..d550803fa 100644 --- a/test/spec/actions/delete_way.js +++ b/test/spec/actions/delete_way.js @@ -31,8 +31,7 @@ describe("iD.actions.DeleteWay", function () { expect(graph.entity(node.id)).not.to.be.undefined; }); - // See #508 - xit("deletes multiple member nodes", function () { + it("deletes multiple member nodes", function () { var a = iD.Node(), b = iD.Node(), way = iD.Way({nodes: [a.id, b.id]}), @@ -42,7 +41,7 @@ describe("iD.actions.DeleteWay", function () { expect(graph.entity(b.id)).to.be.undefined; }); - xit("deletes a circular way's start/end node", function () { + it("deletes a circular way's start/end node", function () { var a = iD.Node(), b = iD.Node(), c = iD.Node(), diff --git a/test/spec/connection.js b/test/spec/connection.js index 2f5d93808..652c1419b 100644 --- a/test/spec/connection.js +++ b/test/spec/connection.js @@ -26,24 +26,24 @@ describe('iD.Connection', function () { c.loadFromURL('data/node.xml', done); }); - it('returns a graph', function (done) { + it('returns an object', function (done) { c.loadFromURL('data/node.xml', function (err, graph) { expect(err).to.not.be.ok; - expect(graph).to.be.instanceOf(iD.Graph); + expect(typeof graph).to.eql('object'); done(); }); }); it('parses a node', function (done) { c.loadFromURL('data/node.xml', function (err, graph) { - expect(graph.entity('n356552551')).to.be.instanceOf(iD.Entity); + expect(graph.n356552551).to.be.instanceOf(iD.Entity); done(); }); }); it('parses a way', function (done) { c.loadFromURL('data/way.xml', function (err, graph) { - expect(graph.entity('w19698713')).to.be.instanceOf(iD.Entity); + expect(graph.w19698713).to.be.instanceOf(iD.Entity); done(); }); }); diff --git a/test/spec/geo.js b/test/spec/geo.js new file mode 100644 index 000000000..113d4795d --- /dev/null +++ b/test/spec/geo.js @@ -0,0 +1,96 @@ +describe('iD.geo', function() { + describe('.roundCoords', function() { + expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]); + expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]); + }); + + describe('.interp', function() { + it('interpolates halfway', function() { + var a = [0, 0], + b = [10, 10]; + expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]); + }); + it('interpolates to one side', function() { + var a = [0, 0], + b = [10, 10]; + expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]); + }); + }); + + describe('.dist', function() { + it('distance between two same points is zero', function() { + var a = [0, 0], + b = [0, 0]; + expect(iD.geo.dist(a, b)).to.eql(0); + }); + it('a straight 10 unit line is 10', function() { + var a = [0, 0], + b = [10, 0]; + expect(iD.geo.dist(a, b)).to.eql(10); + }); + it('a pythagorean triangle is right', function() { + var a = [0, 0], + b = [4, 3]; + expect(iD.geo.dist(a, b)).to.eql(5); + }); + }); + + describe('.pointInPolygon', function() { + it('says a point in a polygon is on a polygon', function() { + var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; + var point = [0.5, 0.5]; + expect(iD.geo.pointInPolygon(point, poly)).to.be.true; + }); + it('says a point outside of a polygon is outside', function() { + var poly = [ + [0, 0], + [0, 1], + [1, 1], + [1, 0], + [0, 0]]; + var point = [0.5, 1.5]; + expect(iD.geo.pointInPolygon(point, poly)).to.be.false; + }); + }); + + describe('.polygonContainsPolygon', function() { + it('says a polygon in a polygon is in', 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.geo.polygonContainsPolygon(outer, inner)).to.be.true; + }); + it('says a polygon outside of a polygon is out', function() { + var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; + var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; + expect(iD.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.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.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.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; + }); + }); + + describe('.pathLength', function() { + it('calculates a simple path length', function() { + var path = [[0, 0], [0, 1], [3, 5]]; + expect(iD.geo.pathLength(path)).to.eql(6); + }); + }); +}); diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 25382e7c9..327d66295 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -1,25 +1,164 @@ describe('iD.Graph', function() { - it("can be constructed with an entities Object", function () { - var entity = iD.Entity(), - graph = iD.Graph({'n-1': entity}); - expect(graph.entity('n-1')).to.equal(entity); - }); - - it("can be constructed with an entities Array", function () { - var entity = iD.Entity(), - graph = iD.Graph([entity]); - expect(graph.entity(entity.id)).to.equal(entity); - }); - - if (iD.debug) { - it("is frozen", function () { - expect(Object.isFrozen(iD.Graph())).to.be.true; + describe("constructor", function () { + it("accepts an entities Object", function () { + var entity = iD.Entity(), + graph = iD.Graph({'n-1': entity}); + expect(graph.entity('n-1')).to.equal(entity); }); - it("freezes entities", function () { - expect(Object.isFrozen(iD.Graph().entities)).to.be.true; + it("accepts an entities Array", function () { + var entity = iD.Entity(), + graph = iD.Graph([entity]); + expect(graph.entity(entity.id)).to.equal(entity); }); - } + + it("accepts a Graph", function () { + var entity = iD.Entity(), + graph = iD.Graph(iD.Graph([entity])); + expect(graph.entity(entity.id)).to.equal(entity); + }); + + it("copies other's entities", function () { + var entity = iD.Entity(), + base = iD.Graph([entity]), + graph = iD.Graph(base); + expect(graph.entities).not.to.equal(base.entities); + }); + + it("rebases on other's base", function () { + var base = iD.Graph(), + graph = iD.Graph(base); + expect(graph.base().entities).to.equal(base.base().entities); + }); + + it("freezes by default", function () { + expect(iD.Graph().frozen).to.be.true; + }); + + it("remains mutable if passed true as second argument", function () { + expect(iD.Graph([], true).frozen).not.to.be.true; + }); + }); + + describe("#freeze", function () { + it("sets the frozen flag", function () { + expect(iD.Graph([], true).freeze().frozen).to.be.true; + }); + + if (iD.debug) { + it("freezes entities", function () { + expect(Object.isFrozen(iD.Graph().entities)).to.be.true; + }); + } + }); + + describe("#rebase", function () { + it("preserves existing entities", function () { + var node = iD.Node({id: 'n'}), + graph = iD.Graph([node]); + graph.rebase({}); + expect(graph.entity('n')).to.equal(node); + }); + + it("includes new entities", function () { + var node = iD.Node({id: 'n'}), + graph = iD.Graph(); + graph.rebase({'n': node}); + expect(graph.entity('n')).to.equal(node); + }); + + it("gives precedence to existing entities", function () { + var a = iD.Node({id: 'n'}), + b = iD.Node({id: 'n'}), + graph = iD.Graph([a]); + graph.rebase({'n': b}); + expect(graph.entity('n')).to.equal(a); + }); + + it("inherits entities from base prototypally", function () { + var graph = iD.Graph(); + graph.rebase({'n': iD.Node()}); + expect(graph.entities).not.to.have.ownProperty('n'); + }); + + it("updates parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + w2 = iD.Way({id: 'w2', nodes: ['n']}), + graph = iD.Graph([n, w1]); + + graph.rebase({ 'w2': w2 }); + expect(graph.parentWays(n)).to.eql([w1, w2]); + expect(graph._parentWays.hasOwnProperty('n')).to.be.false; + }); + + it("avoids adding duplicate parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]); + graph.rebase({ 'w1': w1 }); + expect(graph.parentWays(n)).to.eql([w1]); + }); + + it("updates parentWays for nodes with modified parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + w2 = iD.Way({id: 'w2', nodes: ['n']}), + w3 = iD.Way({id: 'w3', nodes: ['n']}), + graph = iD.Graph([n, w1]), + graph2 = graph.replace(w2); + graph.rebase({ 'w3': w3 }); + graph2.rebase({ 'w3': w3 }); + + expect(graph2.parentWays(n)).to.eql([w1, w2, w3]); + }); + + it("avoids re-adding removed parentWays", function() { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]), + graph2 = graph.remove(w1); + graph.rebase({ 'w1': w1 }); + graph2.rebase({ 'w1': w1 }); + expect(graph2.parentWays(n)).to.eql([]); + }); + + it("updates parentRelations", function () { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]); + + graph.rebase({'r2': r2}); + + expect(graph.parentRelations(n)).to.eql([r1, r2]); + expect(graph._parentRels.hasOwnProperty('n')).to.be.false; + }); + + it("avoids re-adding removed parentRels", function() { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]), + graph2 = graph.remove(r1); + graph.rebase({ 'w1': r1 }); + graph2.rebase({ 'w1': r1 }); + expect(graph2.parentWays(n)).to.eql([]); + }); + + it("updates parentRels for nodes with modified parentWays", function () { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), + r3 = iD.Relation({id: 'r3', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]), + graph2 = graph.replace(r2); + + graph.rebase({'r3': r3}); + graph2.rebase({'r3': r3}); + expect(graph2.parentRelations(n)).to.eql([r1, r2, r3]); + }); + + }); describe("#remove", function () { it("returns a new graph", function () { @@ -40,6 +179,20 @@ describe('iD.Graph', function() { graph = iD.Graph([node]); expect(graph.remove(node).entity(node.id)).to.be.undefined; }); + + it("removes the entity as a parentWay", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.remove(w1).parentWays(node)).to.eql([]); + }); + + it("removes the entity as a parentRelation", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n' }]}), + graph = iD.Graph([node, r1]); + expect(graph.remove(r1).parentRelations(node)).to.eql([]); + }); }); describe("#replace", function () { @@ -62,6 +215,49 @@ describe('iD.Graph', function() { graph = iD.Graph([node1]); expect(graph.replace(node2).entity(node2.id)).to.equal(node2); }); + + it("adds parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node]); + expect(graph.replace(w1).parentWays(node)).to.eql([w1]); + }); + + it("removes parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.remove(w1).parentWays(node)).to.eql([]); + }); + + it("doesn't add duplicate parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.replace(w1).parentWays(node)).to.eql([w1]); + }); + + it("adds parentRels", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node]); + expect(graph.replace(r1).parentRelations(node)).to.eql([r1]); + }); + + it("removes parentRelations", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node, r1]); + expect(graph.remove(r1).parentRelations(node)).to.eql([]); + }); + + it("doesn't add duplicate parentRelations", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node, r1]); + expect(graph.replace(r1).parentRelations(node)).to.eql([r1]); + }); + }); describe("#update", function () { @@ -159,18 +355,18 @@ describe('iD.Graph', function() { describe("#modified", function () { it("returns an Array of ids of modified entities", function () { - var node1 = iD.Node({id: 'n1', _updated: true}), - node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node1, node2]); - expect(graph.modified()).to.eql([node1.id]); + var node = iD.Node({id: 'n1'}), + node_ = iD.Node({id: 'n1'}), + graph = iD.Graph([node]).replace(node_); + expect(graph.modified()).to.eql([node.id]); }); }); describe("#created", function () { it("returns an Array of ids of created entities", function () { - var node1 = iD.Node({id: 'n-1', _updated: true}), + var node1 = iD.Node({id: 'n-1'}), node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node1, node2]); + graph = iD.Graph([node2]).replace(node1); expect(graph.created()).to.eql([node1.id]); }); }); @@ -185,7 +381,7 @@ describe('iD.Graph', function() { it("doesn't include created entities that were subsequently deleted", function () { var node = iD.Node(), - graph = iD.Graph([node]).remove(node); + graph = iD.Graph().replace(node).remove(node); expect(graph.deleted()).to.eql([]); }); }); diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js index b59190c0f..dc685b446 100644 --- a/test/spec/graph/history.js +++ b/test/spec/graph/history.js @@ -153,17 +153,15 @@ describe("iD.History", function () { it("includes modified entities", function () { var node1 = iD.Node({id: "n1"}), - node2 = node1.update({}), - graph = iD.Graph([node1]); - history.merge(graph); + node2 = node1.update({}); + history.merge({ n1: node1}); history.perform(function (graph) { return graph.replace(node2); }); expect(history.changes().modified).to.eql([node2]); }); it("includes deleted entities", function () { - var node = iD.Node({id: "n1"}), - graph = iD.Graph([node]); - history.merge(graph); + var node = iD.Node({id: "n1"}); + history.merge({ n1: node }); history.perform(function (graph) { return graph.remove(node); }); expect(history.changes().deleted).to.eql([node]); }); @@ -189,7 +187,7 @@ describe("iD.History", function () { it("is the sum of all types of changes", function() { var node1 = iD.Node({id: "n1"}), node2 = iD.Node(); - history.merge(iD.Graph([node1])); + history.merge({ n1: node1 }); history.perform(function (graph) { return graph.remove(node1); }); expect(history.numChanges()).to.eql(1); history.perform(function (graph) { return graph.replace(node2); }); diff --git a/test/spec/svg/midpoints.js b/test/spec/svg/midpoints.js new file mode 100644 index 000000000..c34ac0350 --- /dev/null +++ b/test/spec/svg/midpoints.js @@ -0,0 +1,52 @@ +describe("iD.svg.Midpoints", 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("finds the location of the midpoints", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [50, 0]}), + line = iD.Way({nodes: [a.id, b.id]}), + graph = iD.Graph([a, b, line]); + + surface.call(iD.svg.Midpoints(projection), graph, [line], filter); + + expect(surface.select('.midpoint').datum().loc).to.eql([25, 0]); + }); + + it("doesn't create midpoints on segments with pixel length less than 40", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [39, 0]}), + line = iD.Way({nodes: [a.id, b.id]}), + graph = iD.Graph([a, b, line]); + + surface.call(iD.svg.Midpoints(projection), graph, [line], filter); + + expect(surface.selectAll('.midpoint')[0]).to.have.length(0); + }); + + it("binds a datum whose 'ways' property lists ways which include the segement", function () { + var a = iD.Node({loc: [0, 0]}), + b = iD.Node({loc: [50, 0]}), + c = iD.Node({loc: [1, 1]}), + d = iD.Node({loc: [2, 2]}), + l1 = iD.Way({nodes: [a.id, b.id]}), + l2 = iD.Way({nodes: [b.id, a.id]}), + l3 = iD.Way({nodes: [c.id, a.id, b.id, d.id]}), + l4 = iD.Way({nodes: [a.id, d.id, b.id]}), + graph = iD.Graph([a, b, c, d, l1, l2, l3, l4]), + ab = function (d) { return d.id === [a.id, b.id].sort().join("-"); }; + + surface.call(iD.svg.Midpoints(projection), graph, [l1, l2, l3, l4], filter); + + expect(surface.selectAll('.midpoint').filter(ab).datum().ways).to.eql([ + {id: l1.id, index: 1}, + {id: l2.id, index: 1}, + {id: l3.id, index: 2}]); + }); +}); diff --git a/test/spec/util.js b/test/spec/util.js index 9d5cf082b..de3fab735 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -1,123 +1,24 @@ describe('iD.Util', function() { - var util; - - it('#trueObj', function() { + it('.trueObj', function() { expect(iD.util.trueObj(['a', 'b', 'c'])).to.eql({ a: true, b: true, c: true }); expect(iD.util.trueObj([])).to.eql({}); }); - it('#tagText', function() { + it('.tagText', function() { expect(iD.util.tagText({})).to.eql(''); expect(iD.util.tagText({tags:{foo:'bar'}})).to.eql('foo: bar'); expect(iD.util.tagText({tags:{foo:'bar',two:'three'}})).to.eql('foo: bar\ntwo: three'); }); - it('#stringQs', function() { + it('.stringQs', function() { expect(iD.util.stringQs('foo=bar')).to.eql({foo: 'bar'}); expect(iD.util.stringQs('foo=bar&one=2')).to.eql({foo: 'bar', one: '2' }); expect(iD.util.stringQs('')).to.eql({}); }); - it('#qsString', function() { + it('.qsString', function() { expect(iD.util.qsString({ foo: 'bar' })).to.eql('foo=bar'); expect(iD.util.qsString({ foo: 'bar', one: 2 })).to.eql('foo=bar&one=2'); expect(iD.util.qsString({})).to.eql(''); }); - - describe('geo', function() { - describe('#roundCoords', function() { - expect(iD.geo.roundCoords([0.1, 1])).to.eql([0, 1]); - expect(iD.geo.roundCoords([0, 1])).to.eql([0, 1]); - expect(iD.geo.roundCoords([0, 1.1])).to.eql([0, 1]); - }); - - describe('#interp', function() { - it('interpolates halfway', function() { - var a = [0, 0], - b = [10, 10]; - expect(iD.geo.interp(a, b, 0.5)).to.eql([5, 5]); - }); - it('interpolates to one side', function() { - var a = [0, 0], - b = [10, 10]; - expect(iD.geo.interp(a, b, 0)).to.eql([0, 0]); - }); - }); - - describe('#dist', function() { - it('distance between two same points is zero', function() { - var a = [0, 0], - b = [0, 0]; - expect(iD.geo.dist(a, b)).to.eql(0); - }); - it('a straight 10 unit line is 10', function() { - var a = [0, 0], - b = [10, 0]; - expect(iD.geo.dist(a, b)).to.eql(10); - }); - it('a pythagorean triangle is right', function() { - var a = [0, 0], - b = [4, 3]; - expect(iD.geo.dist(a, b)).to.eql(5); - }); - }); - - describe('#pointInPolygon', function() { - it('says a point in a polygon is on a polygon', function() { - var poly = [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]; - var point = [0.5, 0.5]; - expect(iD.geo.pointInPolygon(point, poly)).to.be.true; - }); - it('says a point outside of a polygon is outside', function() { - var poly = [ - [0, 0], - [0, 1], - [1, 1], - [1, 0], - [0, 0]]; - var point = [0.5, 1.5]; - expect(iD.geo.pointInPolygon(point, poly)).to.be.false; - }); - }); - - describe('#polygonContainsPolygon', function() { - it('says a polygon in a polygon is in', 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.geo.polygonContainsPolygon(outer, inner)).to.be.true; - }); - it('says a polygon outside of a polygon is out', function() { - var outer = [[0, 0], [0, 3], [3, 3], [3, 0], [0, 0]]; - var inner = [[1, 1], [1, 9], [2, 2], [2, 1], [1, 1]]; - expect(iD.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.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.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.geo.polygonIntersectsPolygon(outer, inner)).to.be.false; - }); - }); - - describe('#pathLength', function() { - it('calculates a simple path length', function() { - var path = [[0, 0], [0, 1], [3, 5]]; - expect(iD.geo.pathLength(path)).to.eql(6); - }); - }); - }); });