diff --git a/js/id/core/entity.js b/js/id/core/entity.js index cffb8eb5f..5fcb6df0e 100644 --- a/js/id/core/entity.js +++ b/js/id/core/entity.js @@ -3,7 +3,9 @@ iD.Entity = function(attrs) { if (this instanceof iD.Entity) return; // Create the appropriate subtype. - if (attrs && attrs.type) { + if (attrs && attrs.id) { + return iD.Entity[iD.Entity.id.type(attrs.id)].apply(this, arguments); + } else if (attrs && attrs.type) { return iD.Entity[attrs.type].apply(this, arguments); } @@ -31,7 +33,7 @@ iD.Entity.id.type = function(id) { // A function suitable for use as the second argument to d3.selection#data(). iD.Entity.key = function(entity) { - return entity.id + ',' + entity.v; + return entity.id + 'v' + (entity.v || 0); }; iD.Entity.areaPath = d3.geo.path() diff --git a/js/id/core/graph.js b/js/id/core/graph.js index 790e30ed5..b063e848b 100644 --- a/js/id/core/graph.js +++ b/js/id/core/graph.js @@ -263,28 +263,14 @@ iD.Graph.prototype = { // Obliterates any existing entities load: function(entities) { - - var base = this.base(), - i, entity, prefix; + var base = this.base(); this.entities = Object.create(base.entities); - for (i in entities) { - entity = entities[i]; - prefix = i[0]; - - if (entity === 'undefined') { - this.entities[i] = undefined; - } else if (prefix == 'n') { - this.entities[i] = new iD.Node(entity); - - } else if (prefix == 'w') { - this.entities[i] = new iD.Way(entity); - - } else if (prefix == 'r') { - this.entities[i] = new iD.Relation(entity); - } + for (var i in entities) { + this.entities[i] = entities[i]; this._updateCalculated(base.entities[i], this.entities[i]); } + return this; } }; diff --git a/js/id/core/history.js b/js/id/core/history.js index cea3be181..d99cdf014 100644 --- a/js/id/core/history.js +++ b/js/id/core/history.js @@ -187,33 +187,85 @@ iD.History = function(context) { toJSON: function() { if (stack.length <= 1) return; + var allEntities = {}; + var s = stack.map(function(i) { - var x = { entities: i.graph.entities }; + var modified = [], deleted = []; + + _.forEach(i.graph.entities, function(entity, id) { + if (entity) { + var key = iD.Entity.key(entity); + allEntities[key] = entity; + modified.push(key); + } else { + deleted.push(id); + } + }); + + var x = {}; + + if (modified.length) x.modified = modified; + if (deleted.length) x.deleted = deleted; if (i.imageryUsed) x.imageryUsed = i.imageryUsed; if (i.annotation) x.annotation = i.annotation; + return x; }); return JSON.stringify({ + version: 2, + entities: _.values(allEntities), stack: s, nextIDs: iD.Entity.id.next, index: index - }, function includeUndefined(key, value) { - if (typeof value === 'undefined') return 'undefined'; - return value; }); }, fromJSON: function(json) { - var h = JSON.parse(json); iD.Entity.id.next = h.nextIDs; index = h.index; - stack = h.stack.map(function(d) { - d.graph = iD.Graph(stack[0].graph).load(d.entities); - return d; - }); + + if (h.version === 2) { + var allEntities = {}; + + h.entities.forEach(function(entity) { + allEntities[iD.Entity.key(entity)] = iD.Entity(entity); + }); + + stack = h.stack.map(function(d) { + var entities = {}, entity; + + d.modified && d.modified.forEach(function(key) { + entity = allEntities[key]; + entities[entity.id] = entity; + }); + + d.deleted && d.deleted.forEach(function(id) { + entities[id] = undefined; + }); + + return { + graph: iD.Graph(stack[0].graph).load(entities), + annotation: d.annotation, + imageryUsed: d.imageryUsed + }; + }); + } else { // original version + stack = h.stack.map(function(d) { + var entities = {}; + + for (var i in d.entities) { + var entity = d.entities[i]; + entities[i] = entity === 'undefined' ? undefined : iD.Entity(entity); + } + + d.graph = iD.Graph(stack[0].graph).load(entities); + return d; + }); + } + stack[0].graph.inherited = false; dispatch.change(); diff --git a/test/spec/core/entity.js b/test/spec/core/entity.js index fc4822f00..bbaaec08f 100644 --- a/test/spec/core/entity.js +++ b/test/spec/core/entity.js @@ -1,8 +1,11 @@ describe('iD.Entity', function () { it("returns a subclass of the appropriate type", function () { - expect(iD.Entity({type: 'way'})).be.an.instanceOf(iD.Way); expect(iD.Entity({type: 'node'})).be.an.instanceOf(iD.Node); + expect(iD.Entity({type: 'way'})).be.an.instanceOf(iD.Way); expect(iD.Entity({type: 'relation'})).be.an.instanceOf(iD.Relation); + expect(iD.Entity({id: 'n1'})).be.an.instanceOf(iD.Node); + expect(iD.Entity({id: 'w1'})).be.an.instanceOf(iD.Way); + expect(iD.Entity({id: 'r1'})).be.an.instanceOf(iD.Relation); }); if (iD.debug) { diff --git a/test/spec/core/history.js b/test/spec/core/history.js index a19d919b2..024611543 100644 --- a/test/spec/core/history.js +++ b/test/spec/core/history.js @@ -252,7 +252,6 @@ describe("iD.History", function () { }); describe("#save", function() { - it("doesn't do anything if it doesn't have the lock", function() { var key = history._getKey('saved_history'); context.storage(key, null); @@ -270,7 +269,7 @@ describe("iD.History", function () { history.perform(iD.actions.AddEntity(node)); history.save(); var saved = JSON.parse(context.storage(history._getKey('saved_history'))); - expect(saved.stack[1].entities.n.id).to.eql('n'); + expect(saved.stack[1].modified[0]).to.eql('nv0'); }); }); @@ -290,4 +289,128 @@ describe("iD.History", function () { expect(history.graph().hasEntity('n2')).to.be.undefined; }); }); + + describe("#toJSON", function() { + it("generates v2 JSON", function() { + var node = iD.Node({id: 'n-1'}); + history.merge({n1: iD.Node({id: 'n1'})}); + history.perform(iD.actions.AddEntity(node)); + var json = JSON.parse(history.toJSON()); + expect(json.version).to.eql(2); + expect(json.entities).to.eql([node]); + }); + }); + + describe("#fromJSON", function() { + it("restores from v1 JSON (creation)", function() { + var json = { + "stack": [ + {"entities": {}}, + {"entities": {"n-1": {"loc": [1, 2], "id": "n-1"}}, "imageryUsed": ["Bing"], "annotation": "Added a point."} + ], + "nextIDs": {"node": -2, "way": -1, "relation": -1}, + "index": 1 + }; + history.fromJSON(JSON.stringify(json)); + expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [1, 2]})); + expect(history.undoAnnotation()).to.eql("Added a point."); + expect(history.imageryUsed()).to.eql(["Bing"]); + expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1}); + }); + + it("restores from v1 JSON (modification)", function() { + var json = { + "stack": [ + {"entities": {}}, + {"entities": {"n-1": {"loc": [1, 2], "id": "n-1"}}, "imageryUsed": ["Bing"], "annotation": "Added a point."}, + {"entities": {"n-1": {"loc": [2, 3], "id": "n-1", "v": 1}}, "imageryUsed": ["Bing"], "annotation": "Moved a point."} + ], + "nextIDs": {"node": -2, "way": -1, "relation": -1}, + "index": 2 + }; + history.fromJSON(JSON.stringify(json)); + expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [2, 3], v: 1})); + expect(history.undoAnnotation()).to.eql("Moved a point."); + expect(history.imageryUsed()).to.eql(["Bing"]); + expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1}); + }); + + it("restores from v1 JSON (deletion)", function() { + var json = { + "stack": [ + {"entities": {}}, + {"entities": {"n1": "undefined"}, "imageryUsed": ["Bing"], "annotation": "Deleted a point."} + ], + "nextIDs": {"node": -1, "way": -2, "relation": -3}, + "index": 1 + }; + history.fromJSON(JSON.stringify(json)); + history.merge({n1: iD.Node({id: 'n1'})}); + expect(history.graph().hasEntity('n1')).to.be.undefined; + expect(history.undoAnnotation()).to.eql("Deleted a point."); + expect(history.imageryUsed()).to.eql(["Bing"]); + expect(iD.Entity.id.next).to.eql({node: -1, way: -2, relation: -3}); + }); + + it("restores from v2 JSON (creation)", function() { + var json = { + "version": 2, + "entities": [ + {"loc": [1, 2], "id": "n-1"} + ], + "stack": [ + {}, + {"modified": ["n-1v0"], "imageryUsed": ["Bing"], "annotation": "Added a point."} + ], + "nextIDs": {"node": -2, "way": -1, "relation": -1}, + "index": 1 + }; + history.fromJSON(JSON.stringify(json)); + expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [1, 2]})); + expect(history.undoAnnotation()).to.eql("Added a point."); + expect(history.imageryUsed()).to.eql(["Bing"]); + expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1}); + }); + + it("restores from v2 JSON (modification)", function() { + var json = { + "version": 2, + "entities": [ + {"loc": [1, 2], "id": "n-1"}, + {"loc": [2, 3], "id": "n-1", "v": 1} + ], + "stack": [ + {}, + {"modified": ["n-1v0"], "imageryUsed": ["Bing"], "annotation": "Added a point."}, + {"modified": ["n-1v1"], "imageryUsed": ["Bing"], "annotation": "Moved a point."} + ], + "nextIDs": {"node": -2, "way": -1, "relation": -1}, + "index": 2 + }; + history.fromJSON(JSON.stringify(json)); + expect(history.graph().entity('n-1')).to.eql(iD.Node({id: 'n-1', loc: [2, 3], v: 1})); + expect(history.undoAnnotation()).to.eql("Moved a point."); + expect(history.imageryUsed()).to.eql(["Bing"]); + expect(iD.Entity.id.next).to.eql({node: -2, way: -1, relation: -1}); + }); + + it("restores from v2 JSON (deletion)", function() { + var json = { + "version": 2, + "entities": [], + "stack": [ + {}, + {"deleted": ["n1"], "imageryUsed": ["Bing"], "annotation": "Deleted a point."} + ], + "nextIDs": {"node": -1, "way": -2, "relation": -3}, + "index": 1 + }; + history.fromJSON(JSON.stringify(json)); + history.merge({n1: iD.Node({id: 'n1'})}); + expect(history.graph().hasEntity('n1')).to.be.undefined; + expect(history.undoAnnotation()).to.eql("Deleted a point."); + expect(history.imageryUsed()).to.eql(["Bing"]); + expect(iD.Entity.id.next).to.eql({node: -1, way: -2, relation: -3}); + }); + }); });