Deduplicate entities in serialized history

Fixes #1403
This commit is contained in:
John Firebaugh
2013-08-14 16:16:49 -07:00
parent 605e44952e
commit 2efafa087b
5 changed files with 198 additions and 32 deletions
+4 -2
View File
@@ -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()
+4 -18
View File
@@ -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;
}
};
+61 -9
View File
@@ -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();
+4 -1
View File
@@ -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) {
+125 -2
View File
@@ -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});
});
});
});