diff --git a/js/id/format/xml.js b/js/id/format/xml.js index 1c0fe8ad6..ec036120f 100644 --- a/js/id/format/xml.js +++ b/js/id/format/xml.js @@ -52,17 +52,17 @@ iD.format.XML = { '@version': 0.3, '@generator': 'iD', // TODO: copy elements first - create: nest(changes.create.map(function(c) { + create: nest(changes.created.map(function(c) { var x = iD.Entity(c); x.changeset = changeset_id; return x; }).map(iD.format.XML.rep)), - modify: changes.modify.map(function(c) { + modify: changes.modified.map(function(c) { var x = iD.Entity(c); x.changeset = changeset_id; return x; }).map(iD.format.XML.rep), - 'delete': changes['delete'].map(function(c) { + 'delete': changes.deleted.map(function(c) { var x = iD.Entity(c); x.changeset = changeset_id; return x; diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 0626a492f..593124927 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -24,14 +24,14 @@ iD.Graph.prototype = { parentWays: function(id) { // This is slow and a bad hack. return _.filter(this.entities, function(e) { - return e.type === 'way' && e.nodes.indexOf(id) !== -1; + return e && e.type === 'way' && e.nodes.indexOf(id) !== -1; }); }, parentRelations: function(id) { // This is slow and a bad hack. return _.filter(this.entities, function(e) { - return e.type === 'relation' && e.members.indexOf(id) !== -1; + return e && e.type === 'relation' && e.members.indexOf(id) !== -1; }); }, @@ -49,7 +49,13 @@ iD.Graph.prototype = { remove: function(entity) { var entities = _.clone(this.entities); - delete entities[entity.id]; + + if (entity.created()) { + delete entities[entity.id]; + } else { + entities[entity.id] = undefined; + } + return iD.Graph(entities); }, @@ -58,7 +64,7 @@ iD.Graph.prototype = { var items = []; for (var i in this.entities) { var entity = this.entities[i]; - if (entity.intersects(extent, this)) { + if (entity && entity.intersects(extent, this)) { items.push(this.fetch(entity.id)); } } @@ -68,26 +74,52 @@ iD.Graph.prototype = { // Resolve the id references in a way, replacing them with actual objects. fetch: function(id) { var entity = this.entities[id], nodes = []; - if (!entity.nodes || !entity.nodes.length) return entity; + if (!entity || !entity.nodes || !entity.nodes.length) return entity; for (var i = 0, l = entity.nodes.length; i < l; i++) { nodes[i] = this.fetch(entity.nodes[i]); } return iD.Entity(entity, {nodes: nodes}); }, - modifications: function() { - return _.filter(this.entities, function(entity) { - return entity.modified(); - }).map(function(e) { - return this.fetch(e.id); - }.bind(this)); + difference: function (graph) { + var result = []; + + _.each(this.entities, function(entity, id) { + if (entity !== graph.entities[id]) { + result.push(id); + } + }); + + _.each(graph.entities, function(entity, id) { + if (entity && !this.entities.hasOwnProperty(id)) { + result.push(id); + } + }, this); + + return result.sort(); }, - creations: function() { - return _.filter(this.entities, function(entity) { - return entity.created(); - }).map(function(e) { - return this.fetch(e.id); - }.bind(this)); + modified: function() { + var result = []; + _.each(this.entities, function(entity, id) { + if (entity && entity.modified()) result.push(id); + }); + return result; + }, + + created: function() { + var result = []; + _.each(this.entities, function(entity, id) { + if (entity && entity.created()) result.push(id); + }); + return result; + }, + + deleted: function() { + var result = []; + _.each(this.entities, function(entity, id) { + if (!entity) result.push(id); + }); + return result; } }; diff --git a/js/id/graph/history.js b/js/id/graph/history.js index b4f75d847..9836af4dd 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -19,10 +19,8 @@ iD.History = function() { return {graph: graph, annotation: annotation}; } - function maybeChange() { - if (stack[index].annotation) { - dispatch.change(); - } + function change(previous) { + dispatch.change(history.graph().difference(previous)); } var history = { @@ -37,32 +35,44 @@ iD.History = function() { }, perform: function () { + var previous = stack[index].graph; + stack = stack.slice(0, index + 1); stack.push(perform(arguments)); index++; - dispatch.change(); + + change(previous); }, replace: function () { + var previous = stack[index].graph; + // assert(index == stack.length - 1) stack[index] = perform(arguments); - dispatch.change(); + + change(previous); }, undo: function () { + var previous = stack[index].graph; + while (index > 0) { index--; if (stack[index].annotation) break; } - dispatch.change(); + + change(previous); }, redo: function () { + var previous = stack[index].graph; + while (index < stack.length - 1) { index++; if (stack[index].annotation) break; } - dispatch.change(); + + change(previous); }, undoAnnotation: function () { @@ -81,29 +91,20 @@ iD.History = function() { } }, - // generate reports of changes for changesets to use - modify: function () { - return stack[index].graph.modifications(); - }, - - create: function () { - return stack[index].graph.creations(); - }, - - 'delete': function () { - return _.difference( - _.pluck(stack[0].graph.entities, 'id'), - _.pluck(stack[index].graph.entities, 'id') - ).map(function (id) { - return stack[0].graph.fetch(id); - }); - }, - changes: function () { + var initial = stack[0].graph, + current = stack[index].graph; + return { - modify: this.modify(), - create: this.create(), - 'delete': this['delete']() + modified: current.modified().map(function (id) { + return current.fetch(id); + }), + created: current.created().map(function (id) { + return current.fetch(id); + }), + deleted: current.deleted().map(function (id) { + return initial.fetch(id); + }) }; }, diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 0bce46944..b21e5804c 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -69,21 +69,30 @@ iD.Map = function() { return 'M' + _.pluck(d.nodes, 'loc').map(projection).map(iD.util.geo.roundCoords).join('L'); } - function drawVector(only) { + function drawVector(difference) { if (surface.style(transformProp) != 'none') return; - var all = [], ways = [], areas = [], points = [], waynodes = [], + var filter, all, ways = [], areas = [], points = [], waynodes = [], extent = map.extent(), graph = history.graph(); - if (!only) { + if (!difference) { all = graph.intersects(extent); + filter = d3.functor(true); } else { - for (var id in only) all.push(graph.fetch(id)); + var only = {}; + difference.forEach(function (id) { + var entity = graph.fetch(id); + if (entity) { + only[id] = entity; + graph.parentWays(id).forEach(function (entity) { + only[entity.id] = graph.fetch(entity.id); + }); + } + }); + all = _.values(only); + filter = function(d) { return d.accuracy ? only[d.way.id] : only[d.id]; }; } - var filter = only ? - function(d) { return only[d.id]; } : function() { return true; }; - if (all.length > 200000) return hideVector(); for (var i = 0; i < all.length; i++) { @@ -151,7 +160,7 @@ iD.Map = function() { function drawAccuracyHandles(waynodes, filter) { var handles = g.hit.selectAll('circle.accuracy-handle') .filter(filter) - .data(waynodes, key); + .data(waynodes, function (d) { return [d.way.id, d.index].join(","); }); handles.exit().remove(); handles.enter().append('circle') .attr({ r: 2, 'class': 'accuracy-handle' }); @@ -241,14 +250,14 @@ iD.Map = function() { function connectionLoad(err, result) { history.merge(result); - drawVector(iD.util.trueObj(Object.keys(result.entities))); + drawVector(Object.keys(result.entities)); } function hoverIn() { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity) { hover = datum.id; - drawVector(iD.util.trueObj([hover])); + drawVector([hover]); d3.select('.messages').text(datum.tags.name || '#' + datum.id); } } @@ -257,7 +266,7 @@ iD.Map = function() { if (hover) { var oldHover = hover; hover = null; - drawVector(iD.util.trueObj([oldHover])); + drawVector([oldHover]); d3.select('.messages').text(''); } } @@ -299,12 +308,12 @@ iD.Map = function() { redraw(); } - function redraw() { + function redraw(difference) { dispatch.move(map); tilegroup.call(background); if (map.zoom() > 16) { connection.loadTiles(projection); - drawVector(); + drawVector(difference); } else { hideVector(); } diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 248056c0d..a1407c09a 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -21,35 +21,46 @@ describe('iD.Graph', function() { }); } - describe('operations', function() { - it('#remove', function() { - var entities = { 'n-1': { - type: 'node', - loc: [-80, 30], - id: 'n-1' - } - }; - var graph = iD.Graph(entities, 'first graph'); - var g2 = graph.remove(entities['n-1'], 'Removed node'); - expect(graph.entity('n-1')).to.equal(entities['n-1']); - expect(g2.entity('n-1')).to.equal(undefined); + describe("#remove", function () { + it("returns a new graph", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.remove(node)).not.to.equal(graph); }); - it('#replace', function() { - var entities = { 'n-1': { - type: 'node', - loc: [-80, 30], - id: 'n-1' - } - }; - var replacement = { - type: 'node', - loc: [-80, 40], - id: 'n-1' - }; - var graph = iD.Graph(entities, 'first graph'); - var g2 = graph.replace(replacement, 'Removed node'); - expect(graph.entity('n-1').loc[1]).to.equal(30); - expect(g2.entity('n-1').loc[1]).to.equal(40); + + it("doesn't modify the receiver", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + graph.remove(node); + expect(graph.entity(node.id)).to.equal(node); + }); + + it("removes the entity from the result", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.remove(node).entity(node.id)).to.be.undefined; + }); + }); + + describe("#replace", function () { + it("returns a new graph", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.replace(node)).not.to.equal(graph); + }); + + it("doesn't modify the receiver", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + graph.replace(node); + expect(graph.entity(node.id)).to.equal(node); + }); + + it("replaces the entity in the result", function () { + var node1 = iD.Node(), + node2 = node1.update({}), + graph = iD.Graph([node1]); + expect(graph.replace(node2).entity(node2.id)).to.equal(node2); }); }); @@ -82,21 +93,55 @@ describe('iD.Graph', function() { }); }); - describe("#modifications", function () { - it("filters entities by modified", function () { - var a = {id: 'a', modified: function () { return true; }}, - b = {id: 'b', modified: function () { return false; }}, - graph = iD.Graph({ 'a': a, 'b': b }); - expect(graph.modifications()).to.eql([graph.fetch('a')]); + describe("#difference", function () { + it("returns an Array of ids of changed entities", function () { + var initial = iD.Node({id: "n1"}), + updated = initial.update({}), + created = iD.Node(), + deleted = iD.Node({id: 'n2'}), + graph1 = iD.Graph([initial, deleted]), + graph2 = graph1.replace(updated).replace(created).remove(deleted); + expect(graph2.difference(graph1)).to.eql([created.id, updated.id, deleted.id]); + }); + + it("includes created entities that were subsequently deleted", function () { + var node = iD.Node(), + graph1 = iD.Graph([node]), + graph2 = graph1.remove(node); + expect(graph2.difference(graph1)).to.eql([node.id]); }); }); - describe("#creations", function () { - it("filters entities by created", function () { - var a = {id: 'a', created: function () { return true; }}, - b = {id: 'b', created: function () { return false; }}, - graph = iD.Graph({ 'a': a, 'b': b }); - expect(graph.creations()).to.eql([graph.fetch('a')]); + 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]); + }); + }); + + describe("#created", function () { + it("returns an Array of ids of created entities", function () { + var node1 = iD.Node({id: 'n-1', _updated: true}), + node2 = iD.Node({id: 'n2'}), + graph = iD.Graph([node1, node2]); + expect(graph.created()).to.eql([node1.id]); + }); + }); + + describe("#deleted", function () { + it("returns an Array of ids of deleted entities", function () { + var node1 = iD.Node({id: "n1"}), + node2 = iD.Node(), + graph = iD.Graph([node1, node2]).remove(node1); + expect(graph.deleted()).to.eql([node1.id]); + }); + + it("doesn't include created entities that were subsequently deleted", function () { + var node = iD.Node(), + graph = iD.Graph([node]).remove(node); + expect(graph.deleted()).to.eql([]); }); }); }); diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js index efabcc32a..15fe10964 100644 --- a/test/spec/graph/history.js +++ b/test/spec/graph/history.js @@ -28,7 +28,7 @@ describe("iD.History", function () { it("emits a change event", function () { history.on('change', spy); history.perform(action); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); }); it("performs multiple actions", function () { @@ -57,7 +57,7 @@ describe("iD.History", function () { it("emits a change event", function () { history.on('change', spy); history.replace(action); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); }); it("performs multiple actions", function () { @@ -87,7 +87,7 @@ describe("iD.History", function () { history.perform(action); history.on('change', spy); history.undo(); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); }); }); @@ -97,7 +97,32 @@ describe("iD.History", function () { history.undo(); history.on('change', spy); history.redo(); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); + }); + }); + + describe("#changes", function () { + it("includes created entities", function () { + var node = iD.Node(); + history.perform(function (graph) { return graph.replace(node); }); + expect(history.changes().created).to.eql([node]); + }); + + it("includes modified entities", function () { + var node1 = iD.Node({id: "n1"}), + node2 = node1.update({}), + graph = iD.Graph([node1]); + history.merge(graph); + 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); + history.perform(function (graph) { return graph.remove(node); }); + expect(history.changes().deleted).to.eql([node]); }); });