diff --git a/index.html b/index.html index e9598a0ec..3e20c81f7 100644 --- a/index.html +++ b/index.html @@ -151,6 +151,7 @@ + diff --git a/js/id/behavior/lasso.js b/js/id/behavior/lasso.js index 728b8ad15..0d234d1c4 100644 --- a/js/id/behavior/lasso.js +++ b/js/id/behavior/lasso.js @@ -49,7 +49,7 @@ iD.behavior.Lasso = function(context) { .on('mouseup.lasso', null); if (d3.event.clientX !== pos[0] || d3.event.clientY !== pos[1]) { - var selected = context.graph().intersects(extent).filter(function (entity) { + var selected = context.intersects(extent).filter(function (entity) { return entity.type === 'node'; }); diff --git a/js/id/core/difference.js b/js/id/core/difference.js index e8faef149..801d2f633 100644 --- a/js/id/core/difference.js +++ b/js/id/core/difference.js @@ -25,6 +25,18 @@ iD.Difference = function(base, head) { } }); + function addParents(parents, result) { + for (var i = 0; i < parents.length; i++) { + var parent = parents[i]; + + if (parent.id in result) + continue; + + result[parent.id] = parent; + addParents(head.parentRelations(parent), result); + } + } + var difference = {}; difference.length = function() { @@ -67,21 +79,18 @@ iD.Difference = function(base, head) { return result; }; + difference.addParents = function(entities) { + + for (var i in entities) { + addParents(head.parentWays(entities[i]), entities); + addParents(head.parentRelations(entities[i]), entities); + } + return entities; + }, + difference.complete = function(extent) { var result = {}, id, change; - function addParents(parents) { - for (var i = 0; i < parents.length; i++) { - var parent = parents[i]; - - if (parent.id in result) - continue; - - result[parent.id] = parent; - addParents(head.parentRelations(parent)); - } - } - for (id in changes) { change = changes[id]; @@ -99,10 +108,10 @@ iD.Difference = function(base, head) { if (entity.type === 'way') { var nh = h ? h.nodes : [], nb = b ? b.nodes : [], - diff; + diff, i; diff = _.difference(nh, nb); - for (var i = 0; i < diff.length; i++) { + for (i = 0; i < diff.length; i++) { result[diff[i]] = head.entity(diff[i]); } @@ -111,9 +120,9 @@ iD.Difference = function(base, head) { result[diff[i]] = head.entity(diff[i]); } } + addParents(head.parentWays(entity), result); + addParents(head.parentRelations(entity), result); - addParents(head.parentWays(entity)); - addParents(head.parentRelations(entity)); } return result; diff --git a/js/id/core/graph.js b/js/id/core/graph.js index a595f0df6..e96ef49d6 100644 --- a/js/id/core/graph.js +++ b/js/id/core/graph.js @@ -226,21 +226,10 @@ iD.Graph.prototype = { return this; }, - // get all objects that intersect an extent. - intersects: function(extent) { - var items = []; - for (var i in this.entities) { - var entity = this.entities[i]; - if (entity && this.hasAllChildren(entity) && entity.intersects(extent, this)) { - items.push(entity); - } - } - return items; - }, - hasAllChildren: function(entity) { // we're only checking changed entities, since we assume fetched data // must have all children present + var i; if (this.entities.hasOwnProperty(entity.id)) { if (entity.type === 'way') { for (i = 0; i < entity.nodes.length; i++) { diff --git a/js/id/core/history.js b/js/id/core/history.js index bfd0d5a8c..3ae02b42d 100644 --- a/js/id/core/history.js +++ b/js/id/core/history.js @@ -1,5 +1,5 @@ iD.History = function(context) { - var stack, index, + var stack, index, tree, imagery_used = 'Bing', dispatch = d3.dispatch('change', 'undone', 'redone'), lock = false; @@ -41,10 +41,19 @@ iD.History = function(context) { }, merge: function(entities) { + + + var base = stack[0].graph.base(), + newentities = Object.keys(entities).filter(function(i) { + return !base.entities[i]; + }); + for (var i = 0; i < stack.length; i++) { stack[i].graph.rebase(entities); } + tree.rebase(newentities); + dispatch.change(); }, @@ -124,6 +133,10 @@ iD.History = function(context) { } }, + intersects: function(extent) { + return tree.intersects(extent, stack[index].graph); + }, + difference: function() { var base = stack[0].graph, head = stack[index].graph; @@ -168,6 +181,7 @@ iD.History = function(context) { reset: function() { stack = [{graph: iD.Graph()}]; index = 0; + tree = iD.Tree(stack[0].graph); dispatch.change(); }, diff --git a/js/id/core/tree.js b/js/id/core/tree.js new file mode 100644 index 000000000..94b28f12e --- /dev/null +++ b/js/id/core/tree.js @@ -0,0 +1,92 @@ + + +iD.Tree = function(graph) { + + var rtree = new RTree(), + m = 1000 * 1000 * 100, + head = graph, + queuedCreated = [], + queuedModified = [], + x, y, dx, dy, rebased; + + function extentRectangle(extent) { + x = m * extent[0][0], + y = m * extent[0][1], + dx = m * extent[1][0] - x || 1, + dy = m * extent[1][1] - y || 1; + return new RTree.Rectangle(~~x, ~~y, ~~dx - 1, ~~dy - 1); + } + + function insert(entity) { + rtree.insert(extentRectangle(entity.extent(head)), entity.id); + } + + function remove(entity) { + rtree.remove(extentRectangle(entity.extent(graph)), entity.id); + } + + function reinsert(entity) { + remove(graph.entities[entity.id]); + insert(entity); + } + + var tree = { + + rebase: function(entities) { + for (var i = 0; i < entities.length; i++) { + if (!graph.entities.hasOwnProperty(entities[i])) { + insert(graph.entity(entities[i]), true); + } + } + rebased = true; + return tree; + }, + + intersects: function(extent, g) { + + head = g; + + if (graph !== head || rebased) { + var diff = iD.Difference(graph, head), + modified = {}; + + diff.modified().forEach(function(d) { + var loc = graph.entities[d.id].loc; + if (!loc || loc[0] !== d.loc[0] || loc[1] !== d.loc[1]) { + modified[d.id] = d; + } + }); + + var created = diff.created().concat(queuedCreated); + modified = d3.values(diff.addParents(modified)).concat(queuedModified); + queuedCreated = []; + queuedModified = []; + + modified.forEach(function(d) { + if (head.hasAllChildren(d)) reinsert(d); + else queuedModified.push(d); + }); + + created.forEach(function(d) { + if (head.hasAllChildren(d)) insert(d); + else queuedCreated.push(d); + }); + + diff.deleted().forEach(remove); + + graph = head; + rebased = false; + } + + return rtree.search(extentRectangle(extent)) + .map(function(id) { return graph.entity(id); }); + }, + + graph: function() { + return graph; + } + + }; + + return tree; +}; diff --git a/js/id/id.js b/js/id/id.js index e2144c237..29338b1a3 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -36,6 +36,7 @@ window.iD = function () { context.undo = history.undo; context.redo = history.redo; context.changes = history.changes; + context.intersects = history.intersects; /* Graph */ context.entity = function(id) { diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index c3d5ec696..d4c082fad 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -65,7 +65,7 @@ iD.Map = function(context) { graph = context.graph(); if (!difference) { - all = graph.intersects(extent); + all = context.intersects(extent); filter = d3.functor(true); } else { var complete = difference.complete(extent); diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index 2389f266a..cc44316c4 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -2,7 +2,7 @@ iD.ui.Contributors = function(context) { function update(selection) { var users = {}, limit = 3, - entities = context.graph().intersects(context.map().extent()); + entities = context.intersects(context.map().extent()); for (var i in entities) { if (entities[i].user) users[entities[i].user] = true; diff --git a/test/index.html b/test/index.html index 4d569f1f1..6ef656a35 100644 --- a/test/index.html +++ b/test/index.html @@ -137,6 +137,7 @@ + @@ -186,6 +187,7 @@ + diff --git a/test/spec/core/tree.js b/test/spec/core/tree.js new file mode 100644 index 000000000..8cd4e55b6 --- /dev/null +++ b/test/spec/core/tree.js @@ -0,0 +1,56 @@ +describe("iD.Tree", function() { + var tree; + + beforeEach(function() { + tree = iD.Tree(iD.Graph()); + }); + + describe("#rebase", function() { + it("adds entities to the tree", function() { + var node = iD.Node({ id: 'n', loc: [1, 1]}); + tree.graph().rebase({ 'n': node }); + tree.rebase(['n']); + expect(tree.intersects(iD.geo.Extent([0, 0], [2, 2]), tree.graph())).to.eql([node]); + }); + + it("does not insert if entity has a modified version", function() { + var node = iD.Node({ id: 'n', loc: [1, 1]}), + node_ = node.update({ loc: [10, 10]}), + g = tree.graph().replace(node_); + expect(tree.intersects(iD.geo.Extent([9, 9], [11, 11]), g)).to.eql([node_]); + tree.graph().rebase({ 'n': node }); + tree.rebase(['n']); + expect(tree.intersects(iD.geo.Extent([0, 0], [2, 2]), g)).to.eql([]); + expect(tree.intersects(iD.geo.Extent([0, 0], [11, 11]), g)).to.eql([node_]); + }); + }); + + describe("#intersects", function() { + it("excludes entities with missing children, adds them when all are present", function() { + var way = iD.Way({id: 'w1', nodes: ['n']}); + var g = tree.graph().replace(way); + expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([]); + var node = iD.Node({id: 'n', loc: [0.5, 0.5]}); + g = tree.graph().replace(node); + expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([way, node]); + }); + + it("includes entities that used to have missing children, after rebase added them", function() { + var base = tree.graph(); + var way = iD.Way({id: 'w1', nodes: ['n']}); + var g = base.replace(way); + expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([]); + var node = iD.Node({id: 'n', loc: [0.5, 0.5]}); + base.rebase({ 'n': node }); + tree.rebase(['n']); + expect(tree.intersects(iD.geo.Extent([0, 0], [1, 1]), g)).to.eql([way, node]); + }); + + it("includes entities within extent, excludes those without", function() { + var n1 = iD.Node({ id: 'n1', loc: [1, 1]}); + var n2 = iD.Node({ id: 'n2', loc: [3, 3]}); + var g = tree.graph().replace(n1).replace(n2); + expect(tree.intersects(iD.geo.Extent([0, 0], [1.1, 1.1]), g)).to.eql([n1]); + }); + }); +});