diff --git a/index.html b/index.html
index adfe92752..83ae066eb 100644
--- a/index.html
+++ b/index.html
@@ -121,6 +121,7 @@
+
diff --git a/js/id/actions/circularize.js b/js/id/actions/circularize.js
index edbd0d8b3..532dcdb45 100644
--- a/js/id/actions/circularize.js
+++ b/js/id/actions/circularize.js
@@ -1,12 +1,11 @@
-iD.actions.Circularize = function(wayId, map) {
+iD.actions.Circularize = function(wayId, projection) {
var action = function(graph) {
var way = graph.entity(wayId),
- nodes = graph.childNodes(way),
- tags = {}, key, role;
+ nodes = _.uniq(graph.childNodes(way));
var points = nodes.map(function(n) {
- return map.projection(n.loc);
+ return projection(n.loc);
}),
centroid = d3.geom.polygon(points).centroid(),
radius = d3.median(points, function(p) {
@@ -15,14 +14,12 @@ iD.actions.Circularize = function(wayId, map) {
circular_nodes = [];
for (var i = 0; i < 12; i++) {
- circular_nodes.push(iD.Node({ loc: map.projection.invert([
+ circular_nodes.push(iD.Node({ loc: projection.invert([
centroid[0] + Math.cos((i / 12) * Math.PI * 2) * radius,
centroid[1] + Math.sin((i / 12) * Math.PI * 2) * radius])
}));
}
- circular_nodes.push(circular_nodes[0]);
-
for (i = 0; i < nodes.length; i++) {
if (graph.parentWays(nodes[i]).length > 1) {
var closest, closest_dist = Infinity, dist;
@@ -34,10 +31,6 @@ iD.actions.Circularize = function(wayId, map) {
}
}
circular_nodes.splice(closest, 1, nodes[i]);
- if (closest === 0) circular_nodes.splice(circular_nodes.length - 1, 1, nodes[i]);
- else if (closest === circular_nodes.length - 1) circular_nodes.splice(0, 1, nodes[i]);
- } else {
- graph = graph.remove(nodes[i]);
}
}
@@ -45,9 +38,18 @@ iD.actions.Circularize = function(wayId, map) {
graph = graph.replace(circular_nodes[i]);
}
- return graph.replace(way.update({
- nodes: _.pluck(circular_nodes, 'id')
- }));
+ var ids = _.pluck(circular_nodes, 'id'),
+ difference = _.difference(_.uniq(way.nodes), ids);
+
+ ids.push(ids[0]);
+
+ graph = graph.replace(way.update({nodes: ids}));
+
+ for (i = 0; i < difference.length; i++) {
+ graph = iD.actions.DeleteNode(difference[i])(graph);
+ }
+
+ return graph;
};
action.enabled = function(graph) {
diff --git a/js/id/behavior/draw_way.js b/js/id/behavior/draw_way.js
index 9efcd5bb9..f724613da 100644
--- a/js/id/behavior/draw_way.js
+++ b/js/id/behavior/draw_way.js
@@ -16,10 +16,13 @@ iD.behavior.DrawWay = function(context, wayId, index, mode, baseGraph) {
function move(datum) {
var loc = context.map().mouseCoordinates();
- if (datum.type === 'node' || datum.type === 'midpoint') {
+ if (datum.type === 'node') {
loc = datum.loc;
- } else if (datum.type === 'way') {
- loc = iD.geo.chooseIndex(datum, d3.mouse(context.surface().node()), context).loc;
+ } else if (datum.type === 'midpoint' || datum.type === 'way') {
+ var way = datum.type === 'way' ?
+ datum :
+ baseGraph.entity(datum.ways[0].id);
+ loc = iD.geo.chooseIndex(way, d3.mouse(context.surface().node()), context).loc;
}
context.replace(iD.actions.MoveNode(nodeId, loc));
diff --git a/js/id/behavior/hash.js b/js/id/behavior/hash.js
index 9056db393..c8762fb13 100644
--- a/js/id/behavior/hash.js
+++ b/js/id/behavior/hash.js
@@ -45,7 +45,7 @@ iD.behavior.Hash = function(context) {
context.map().on('drawn.hash', function() {
if (!context.entity(id)) return;
selectoff();
- context.enter(iD.modes.Select([id]));
+ context.enter(iD.modes.Select(context, [id]));
});
context.on('enter.hash', function() {
diff --git a/js/id/graph/difference.js b/js/id/graph/difference.js
new file mode 100644
index 000000000..e2159271a
--- /dev/null
+++ b/js/id/graph/difference.js
@@ -0,0 +1,121 @@
+/*
+ iD.Difference represents the difference between two graphs.
+ It knows how to calculate the set of entities that were
+ created, modified, or deleted, and also contains the logic
+ for recursively extending a difference to the complete set
+ of entities that will require a redraw, taking into account
+ child and parent relationships.
+ */
+iD.Difference = function (base, head) {
+ var changes = {}, length = 0;
+
+ _.each(head.entities, function(h, id) {
+ var b = base.entities[id];
+ if (h !== b) {
+ changes[id] = {base: b, head: h};
+ length++;
+ }
+ });
+
+ _.each(base.entities, function(b, id) {
+ var h = head.entities[id];
+ if (!changes[id] && h !== b) {
+ changes[id] = {base: b, head: h};
+ length++;
+ }
+ });
+
+ var difference = {};
+
+ difference.length = function () {
+ return length;
+ };
+
+ difference.changes = function() {
+ return changes;
+ };
+
+ difference.extantIDs = function() {
+ var result = [];
+ _.each(changes, function(change, id) {
+ if (change.head) result.push(id);
+ });
+ return result;
+ };
+
+ difference.modified = function() {
+ var result = [];
+ _.each(changes, function(change) {
+ if (change.base && change.head) result.push(change.head);
+ });
+ return result;
+ };
+
+ difference.created = function() {
+ var result = [];
+ _.each(changes, function(change) {
+ if (!change.base && change.head) result.push(change.head);
+ });
+ return result;
+ };
+
+ difference.deleted = function() {
+ var result = [];
+ _.each(changes, function(change) {
+ if (change.base && !change.head) result.push(change.base);
+ });
+ return result;
+ };
+
+ 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];
+
+ var h = change.head,
+ b = change.base,
+ entity = h || b;
+
+ if (extent && !entity.intersects(extent, h ? head : base))
+ continue;
+
+ result[id] = h;
+
+ if (entity.type === 'way') {
+ var nh = h ? h.nodes : [],
+ nb = b ? b.nodes : [],
+ diff;
+
+ diff = _.difference(nh, nb);
+ for (var i = 0; i < diff.length; i++) {
+ result[diff[i]] = head.entity(diff[i]);
+ }
+
+ diff = _.difference(nb, nh);
+ for (var i = 0; i < diff.length; i++) {
+ result[diff[i]] = head.entity(diff[i]);
+ }
+ }
+
+ addParents(head.parentWays(entity));
+ addParents(head.parentRelations(entity));
+ }
+
+ return result;
+ };
+
+ return difference;
+};
diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js
index ae612c268..bce9675ee 100644
--- a/js/id/graph/entity.js
+++ b/js/id/graph/entity.js
@@ -43,7 +43,6 @@ iD.Entity.prototype = {
if (!this.id && this.type) {
this.id = iD.Entity.id(this.type);
- this._updated = true;
}
if (iD.debug) {
@@ -63,7 +62,7 @@ iD.Entity.prototype = {
},
update: function(attrs) {
- return iD.Entity(this, attrs, {_updated: true});
+ return iD.Entity(this, attrs);
},
mergeTags: function(tags) {
@@ -80,14 +79,6 @@ iD.Entity.prototype = {
return this.update({tags: merged});
},
- created: function() {
- return this._updated && this.osmId().charAt(0) === '-';
- },
-
- modified: function() {
- return this._updated && this.osmId().charAt(0) !== '-';
- },
-
intersects: function(extent, resolver) {
return this.extent(resolver).intersects(extent);
},
diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js
index a19bfe094..2692aeba6 100644
--- a/js/id/graph/graph.js
+++ b/js/id/graph/graph.js
@@ -232,65 +232,5 @@ iD.Graph.prototype = {
}
}
return items;
- },
-
- difference: function (graph) {
-
- function diff(a, b) {
- var result = [],
- keys = Object.keys(a.entities),
- entity, oldentity, id, i;
-
- for (i = 0; i < keys.length; i++) {
- id = keys[i];
- entity = a.entities[id];
- oldentity = b.entities[id];
- if (entity !== oldentity) {
-
- // maybe adding affected children better belongs in renderer/map.js?
- if (entity && entity.type === 'way' &&
- oldentity && oldentity.type === 'way') {
- result = result
- .concat(_.difference(entity.nodes, oldentity.nodes))
- .concat(_.difference(oldentity.nodes, entity.nodes));
-
- } else if (entity && entity.type === 'way') {
- result = result.concat(entity.nodes);
-
- } else if (oldentity && oldentity.type === 'way') {
- result = result.concat(oldentity.nodes);
- }
-
- result.push(id);
- }
- }
- return result;
- }
-
- return _.unique(diff(this, graph).concat(diff(graph, this)).sort());
- },
-
- modified: function() {
- var result = [], base = this.base().entities;
- _.each(this.entities, function(entity, id) {
- if (entity && base[id]) result.push(id);
- });
- return result;
- },
-
- created: function() {
- var result = [], base = this.base().entities;
- _.each(this.entities, function(entity, id) {
- if (entity && !base[id]) result.push(id);
- });
- return result;
- },
-
- deleted: function() {
- var result = [], base = this.base().entities;
- _.each(this.entities, function(entity, 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 42e1b1604..3cc1ace01 100644
--- a/js/id/graph/history.js
+++ b/js/id/graph/history.js
@@ -21,7 +21,9 @@ iD.History = function() {
}
function change(previous) {
- dispatch.change(history.graph().difference(previous));
+ var difference = iD.Difference(previous, history.graph());
+ dispatch.change(difference);
+ return difference;
}
var history = {
@@ -33,6 +35,8 @@ iD.History = function() {
for (var i = 0; i < stack.length; i++) {
stack[i].graph.rebase(entities);
}
+
+ dispatch.change();
},
perform: function () {
@@ -42,7 +46,7 @@ iD.History = function() {
stack.push(perform(arguments));
index++;
- change(previous);
+ return change(previous);
},
replace: function () {
@@ -51,7 +55,7 @@ iD.History = function() {
// assert(index == stack.length - 1)
stack[index] = perform(arguments);
- change(previous);
+ return change(previous);
},
pop: function () {
@@ -60,7 +64,7 @@ iD.History = function() {
if (index > 0) {
index--;
stack.pop();
- change(previous);
+ return change(previous);
}
},
@@ -80,7 +84,7 @@ iD.History = function() {
}
dispatch.undone();
- change(previous);
+ return change(previous);
},
redo: function () {
@@ -92,7 +96,7 @@ iD.History = function() {
}
dispatch.redone();
- change(previous);
+ return change(previous);
},
undoAnnotation: function () {
@@ -111,31 +115,27 @@ iD.History = function() {
}
},
- changes: function () {
- var initial = stack[0].graph,
- current = stack[index].graph;
+ difference: function () {
+ var base = stack[0].graph,
+ head = stack[index].graph;
+ return iD.Difference(base, head);
+ },
+ changes: function () {
+ var difference = history.difference();
return {
- modified: current.modified().map(function (id) {
- return current.entity(id);
- }),
- created: current.created().map(function (id) {
- return current.entity(id);
- }),
- deleted: current.deleted().map(function (id) {
- return initial.entity(id);
- })
- };
+ modified: difference.modified(),
+ created: difference.created(),
+ deleted: difference.deleted()
+ }
},
hasChanges: function() {
- return !!this.numChanges();
+ return this.difference().length() > 0;
},
numChanges: function() {
- return d3.sum(d3.values(this.changes()).map(function(c) {
- return c.length;
- }));
+ return this.difference().length();
},
imagery_used: function(source) {
diff --git a/js/id/id.js b/js/id/id.js
index 358610525..56d3e481a 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -16,6 +16,10 @@ window.iD = function () {
// the connection requires .storage() to be available on calling.
var connection = iD.Connection(context);
+ connection.on('load.context', function (err, result) {
+ history.merge(result);
+ });
+
/* Straight accessors. Avoid using these if you can. */
context.ui = function() { return ui; };
context.connection = function() { return connection; };
diff --git a/js/id/operations/circularize.js b/js/id/operations/circularize.js
index 419a6e9eb..2680da4ec 100644
--- a/js/id/operations/circularize.js
+++ b/js/id/operations/circularize.js
@@ -1,6 +1,6 @@
iD.operations.Circularize = function(selection, context) {
var entityId = selection[0],
- action = iD.actions.Circularize(entityId, context.map());
+ action = iD.actions.Circularize(entityId, context.projection);
var operation = function() {
var annotation = t('operations.circularize.annotation.' + context.geometry(entityId));
diff --git a/js/id/operations/merge.js b/js/id/operations/merge.js
index 08517e9b2..d02222368 100644
--- a/js/id/operations/merge.js
+++ b/js/id/operations/merge.js
@@ -2,9 +2,9 @@ iD.operations.Merge = function(selection, context) {
var action = iD.actions.Join(selection[0], selection[1]);
var operation = function() {
- context.perform(
- action,
- t('operations.merge.annotation', {n: selection.length}));
+ var annotation = t('operations.merge.annotation', {n: selection.length}),
+ difference = context.perform(action, annotation);
+ context.enter(iD.modes.Select(context, difference.extantIDs()));
};
operation.available = function() {
diff --git a/js/id/operations/split.js b/js/id/operations/split.js
index 07d7d56ff..f49f4b9ed 100644
--- a/js/id/operations/split.js
+++ b/js/id/operations/split.js
@@ -3,7 +3,9 @@ iD.operations.Split = function(selection, context) {
action = iD.actions.Split(entityId);
var operation = function() {
- context.perform(action, t('operations.split.annotation'));
+ var annotation = t('operations.split.annotation'),
+ difference = context.perform(action, annotation);
+ context.enter(iD.modes.Select(context, difference.extantIDs()));
};
operation.available = function() {
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index 1f22a7c81..b2561f21f 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -26,9 +26,6 @@ iD.Map = function(context) {
surface, tilegroup;
function map(selection) {
- context.connection()
- .on('load.tile', connectionLoad);
-
context.history()
.on('change.map', redraw);
@@ -64,44 +61,19 @@ iD.Map = function(context) {
extent = map.extent(),
graph = context.graph();
- function addParents(parents) {
- for (var i = 0; i < parents.length; i++) {
- var parent = parents[i];
- if (only[parent.id] === undefined) {
- only[parent.id] = parent;
- addParents(graph.parentRelations(parent));
- }
- }
- }
-
if (!difference) {
all = graph.intersects(extent);
filter = d3.functor(true);
} else {
- var only = {};
-
- for (var j = 0; j < difference.length; j++) {
- var id = difference[j],
- entity = graph.entity(id);
-
- // Even if the entity is false (deleted), it needs to be
- // removed from the surface
- only[id] = entity;
-
- if (entity && entity.intersects(extent, graph)) {
- addParents(graph.parentWays(only[id]));
- addParents(graph.parentRelations(only[id]));
- }
- }
-
- all = _.compact(_.values(only));
+ var complete = difference.complete(extent);
+ all = _.compact(_.values(complete));
filter = function(d) {
if (d.type === 'midpoint') {
for (var i = 0; i < d.ways.length; i++) {
- if (d.ways[i].id in only) return true;
+ if (d.ways[i].id in complete) return true;
}
} else {
- return d.id in only;
+ return d.id in complete;
}
};
}
@@ -125,11 +97,6 @@ iD.Map = function(context) {
surface.selectAll('.layer *').remove();
}
- function connectionLoad(err, result) {
- context.history().merge(result);
- redraw(Object.keys(result));
- }
-
function zoomPan() {
if (d3.event && d3.event.sourceEvent.type === 'dblclick') {
if (!dblclickEnabled) {
diff --git a/js/id/validate.js b/js/id/validate.js
index aa7d8771b..6329b626f 100644
--- a/js/id/validate.js
+++ b/js/id/validate.js
@@ -15,31 +15,29 @@ iD.validate = function(changes, graph) {
if (tags.building && tags.building === 'yes') return 'building=yes';
}
- if (changes.created.length) {
- for (var i = 0; i < changes.created.length; i++) {
- change = changes.created[i];
+ for (var i = 0; i < changes.created.length; i++) {
+ change = changes.created[i];
- if (change.geometry(graph) === 'point' && _.isEmpty(change.tags)) {
- warnings.push({
- message: t('validations.untagged_point'),
- entity: change
- });
- }
+ if (change.geometry(graph) === 'point' && _.isEmpty(change.tags)) {
+ warnings.push({
+ message: t('validations.untagged_point'),
+ entity: change
+ });
+ }
- if (change.geometry(graph) === 'line' && _.isEmpty(change.tags)) {
- warnings.push({ message: t('validations.untagged_line'), entity: change });
- }
+ if (change.geometry(graph) === 'line' && _.isEmpty(change.tags)) {
+ warnings.push({ message: t('validations.untagged_line'), entity: change });
+ }
- if (change.geometry(graph) === 'area' && _.isEmpty(change.tags)) {
- warnings.push({ message: t('validations.untagged_area'), entity: change });
- }
+ if (change.geometry(graph) === 'area' && _.isEmpty(change.tags)) {
+ warnings.push({ message: t('validations.untagged_area'), entity: change });
+ }
- if (change.geometry(graph) === 'line' && tagSuggestsArea(change)) {
- warnings.push({
- message: t('validations.tag_suggests_area', {tag: tagSuggestsArea(change)}),
- entity: change
- });
- }
+ if (change.geometry(graph) === 'line' && tagSuggestsArea(change)) {
+ warnings.push({
+ message: t('validations.tag_suggests_area', {tag: tagSuggestsArea(change)}),
+ entity: change
+ });
}
}
diff --git a/test/index.html b/test/index.html
index da3cd475c..b6d925923 100644
--- a/test/index.html
+++ b/test/index.html
@@ -115,6 +115,7 @@
+
@@ -164,6 +165,7 @@
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 948ed0ae1..ef91e2823 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -55,6 +55,7 @@
+
diff --git a/test/spec/graph/difference.js b/test/spec/graph/difference.js
new file mode 100644
index 000000000..c9647bbea
--- /dev/null
+++ b/test/spec/graph/difference.js
@@ -0,0 +1,229 @@
+describe("iD.Difference", function () {
+ describe("#changes", function () {
+ it("includes created entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph(),
+ head = base.replace(node),
+ diff = iD.Difference(base, head);
+ expect(diff.changes()).to.eql({n: {base: undefined, head: node}});
+ });
+
+ it("includes undone created entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph(),
+ head = base.replace(node),
+ diff = iD.Difference(head, base);
+ expect(diff.changes()).to.eql({n: {base: node, head: undefined}});
+ });
+
+ it("includes modified entities", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.update(),
+ base = iD.Graph([n1]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+ expect(diff.changes()).to.eql({n: {base: n1, head: n2}});
+ });
+
+ it("includes undone modified entities", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.update(),
+ base = iD.Graph([n1]),
+ head = base.replace(n2),
+ diff = iD.Difference(head, base);
+ expect(diff.changes()).to.eql({n: {base: n2, head: n1}});
+ });
+
+ it("includes deleted entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph([node]),
+ head = base.remove(node),
+ diff = iD.Difference(base, head);
+ expect(diff.changes()).to.eql({n: {base: node, head: undefined}});
+ });
+
+ it("includes undone deleted entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph([node]),
+ head = base.remove(node),
+ diff = iD.Difference(head, base);
+ expect(diff.changes()).to.eql({n: {base: undefined, head: node}});
+ });
+
+ it("doesn't include created entities that were subsequently deleted", function () {
+ var node = iD.Node(),
+ base = iD.Graph(),
+ head = base.replace(node).remove(node),
+ diff = iD.Difference(base, head);
+ expect(diff.changes()).to.eql({});
+ });
+ });
+
+ describe("#extantIDs", function () {
+ it("includes the ids of created entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph(),
+ head = base.replace(node),
+ diff = iD.Difference(base, head);
+ expect(diff.extantIDs()).to.eql(['n']);
+ });
+
+ it("includes the ids of modified entities", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ base = iD.Graph([n1]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+ expect(diff.extantIDs()).to.eql(['n']);
+ });
+
+ it("omits the ids of deleted entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph([node]),
+ head = base.remove(node),
+ diff = iD.Difference(base, head);
+ expect(diff.extantIDs()).to.eql([]);
+ });
+ });
+
+ describe("#created", function () {
+ it("returns an array of created entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph(),
+ head = base.replace(node),
+ diff = iD.Difference(base, head);
+ expect(diff.created()).to.eql([node]);
+ });
+ });
+
+ describe("#modified", function () {
+ it("returns an array of modified entities", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ base = iD.Graph([n1]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+ expect(diff.modified()).to.eql([n2]);
+ });
+ });
+
+ describe("#deleted", function () {
+ it("returns an array of deleted entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph([node]),
+ head = base.remove(node),
+ diff = iD.Difference(base, head);
+ expect(diff.deleted()).to.eql([node]);
+ });
+ });
+
+ describe("#complete", function () {
+ it("includes created entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph(),
+ head = base.replace(node),
+ diff = iD.Difference(base, head);
+ expect(diff.complete()['n']).to.equal(node);
+ });
+
+ it("includes modified entities", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ base = iD.Graph([n1]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+ expect(diff.complete()['n']).to.equal(n2);
+ });
+
+ it("includes deleted entities", function () {
+ var node = iD.Node({id: 'n'}),
+ base = iD.Graph([node]),
+ head = base.remove(node),
+ diff = iD.Difference(base, head);
+ expect(diff.complete()).to.eql({n: undefined});
+ });
+
+ it("includes nodes added to a way", function () {
+ var n1 = iD.Node({id: 'n1'}),
+ n2 = iD.Node({id: 'n2'}),
+ w1 = iD.Way({id: 'w', nodes: ['n1']}),
+ w2 = w1.addNode('n2'),
+ base = iD.Graph([n1, n2, w1]),
+ head = base.replace(w2),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()['n2']).to.equal(n2);
+ });
+
+ it("includes nodes removed from a way", function () {
+ var n1 = iD.Node({id: 'n1'}),
+ n2 = iD.Node({id: 'n2'}),
+ w1 = iD.Way({id: 'w', nodes: ['n1', 'n2']}),
+ w2 = w1.removeNode('n2'),
+ base = iD.Graph([n1, n2, w1]),
+ head = base.replace(w2),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()['n2']).to.equal(n2);
+ });
+
+ it("includes parent ways of modified nodes", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ way = iD.Way({id: 'w', nodes: ['n']}),
+ base = iD.Graph([n1, way]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()['w']).to.equal(way);
+ });
+
+ it("includes parent relations of modified entities", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ rel = iD.Relation({id: 'r', members: [{id: 'n'}]}),
+ base = iD.Graph([n1, rel]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()['r']).to.equal(rel);
+ });
+
+ it("includes parent relations of modified entities, recursively", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ rel1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}),
+ rel2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}),
+ base = iD.Graph([n1, rel1, rel2]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()['r2']).to.equal(rel2);
+ });
+
+ it("includes parent relations of parent ways of modified nodes", function () {
+ var n1 = iD.Node({id: 'n'}),
+ n2 = n1.move([1, 2]),
+ way = iD.Way({id: 'w', nodes: ['n']}),
+ rel = iD.Relation({id: 'r', members: [{id: 'w'}]}),
+ base = iD.Graph([n1, way, rel]),
+ head = base.replace(n2),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()['r']).to.equal(rel);
+ });
+
+ it("copes with recursive relations", function () {
+ var node = iD.Node({id: 'n'}),
+ rel1 = iD.Relation({id: 'r1', members: [{id: 'n'}, {id: 'r2'}]}),
+ rel2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}),
+ base = iD.Graph([node, rel1, rel2]),
+ head = base.replace(node.move([1, 2])),
+ diff = iD.Difference(base, head);
+
+ expect(diff.complete()).to.be.ok;
+ });
+
+ it("limits changes to those within a given extent");
+ });
+});
diff --git a/test/spec/graph/entity.js b/test/spec/graph/entity.js
index 088ab5516..f2ac85f17 100644
--- a/test/spec/graph/entity.js
+++ b/test/spec/graph/entity.js
@@ -52,12 +52,6 @@ describe('iD.Entity', function () {
expect(e.id).to.equal('w1');
});
- it("tags the entity as updated", function () {
- var tags = {foo: 'bar'},
- e = iD.Entity().update({tags: tags});
- expect(e._updated).to.to.be.true;
- });
-
it("doesn't modify the input", function () {
var attrs = {tags: {foo: 'bar'}},
e = iD.Entity().update(attrs);
@@ -104,42 +98,6 @@ describe('iD.Entity', function () {
});
});
- describe("#created", function () {
- it("returns falsy by default", function () {
- expect(iD.Entity({id: 'w1234'}).created()).not.to.be.ok;
- });
-
- it("returns falsy for an unmodified Entity", function () {
- expect(iD.Entity({id: 'w1234'}).created()).not.to.be.ok;
- });
-
- it("returns falsy for a modified Entity with positive ID", function () {
- expect(iD.Entity({id: 'w1234'}).update({}).created()).not.to.be.ok;
- });
-
- it("returns truthy for a modified Entity with negative ID", function () {
- expect(iD.Entity({id: 'w-1234'}).update({}).created()).to.be.ok;
- });
- });
-
- describe("#modified", function () {
- it("returns falsy by default", function () {
- expect(iD.Entity({id: 'w1234'}).modified()).not.to.be.ok;
- });
-
- it("returns falsy for an unmodified Entity", function () {
- expect(iD.Entity({id: 'w1234'}).modified()).not.to.be.ok;
- });
-
- it("returns truthy for a modified Entity with positive ID", function () {
- expect(iD.Entity({id: 'w1234'}).update({}).modified()).to.be.ok;
- });
-
- it("returns falsy for a modified Entity with negative ID", function () {
- expect(iD.Entity({id: 'w-1234'}).update({}).modified()).not.to.be.ok;
- });
- });
-
describe("#intersects", function () {
it("returns true for a way with a node within the given extent", function () {
var node = iD.Node({loc: [0, 0]}),
diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js
index 05bcbbe89..a357ff360 100644
--- a/test/spec/graph/graph.js
+++ b/test/spec/graph/graph.js
@@ -333,86 +333,4 @@ describe('iD.Graph', function() {
expect(graph.childNodes(way)).to.eql([node]);
});
});
-
- 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, and reverse", function () {
- var node = iD.Node(),
- graph1 = iD.Graph(),
- graph2 = graph1.replace(node);
- expect(graph2.difference(graph1)).to.eql([node.id]);
- expect(graph1.difference(graph2)).to.eql([node.id]);
- });
-
- it("includes entities changed from base, and reverse", function () {
- var node = iD.Node(),
- graph1 = iD.Graph(node),
- graph2 = graph1.replace(node.update());
- expect(graph2.difference(graph1)).to.eql([node.id]);
- expect(graph1.difference(graph2)).to.eql([node.id]);
- });
-
- it("includes already changed entities that were updated, and reverse", function () {
- var node = iD.Node(),
- graph1 = iD.Graph().replace(node),
- graph2 = graph1.replace(node.update());
- expect(graph2.difference(graph1)).to.eql([node.id]);
- expect(graph1.difference(graph2)).to.eql([node.id]);
- });
-
- it("includes affected child nodes", function () {
- var n = iD.Node({id: 'n'}),
- n2 = iD.Node({id: 'n2'}),
- w1 = iD.Way({id: 'w1', nodes: ['n']}),
- w1_ = iD.Way({id: 'w1', nodes: ['n', 'n2']}),
- graph1 = iD.Graph([n, n2, w1]),
- graph2 = graph1.replace(w1_);
- expect(graph2.difference(graph1)).to.eql(['n2', 'w1']);
- expect(graph1.difference(graph2)).to.eql(['n2', 'w1']);
- });
-
- });
-
- describe("#modified", function () {
- it("returns an Array of ids of modified entities", function () {
- 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'}),
- node2 = iD.Node({id: 'n2'}),
- graph = iD.Graph([node2]).replace(node1);
- 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().replace(node).remove(node);
- expect(graph.deleted()).to.eql([]);
- });
- });
});
diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js
index dc685b446..0ecbed5d3 100644
--- a/test/spec/graph/history.js
+++ b/test/spec/graph/history.js
@@ -13,7 +13,25 @@ describe("iD.History", function () {
});
});
+ describe("#merge", function () {
+ it("merges the entities into all graph versions", function () {
+ var n = iD.Node({id: 'n'});
+ history.merge({n: n});
+ expect(history.graph().entity('n')).to.equal(n);
+ });
+
+ it("emits a change event", function () {
+ history.on('change', spy);
+ history.merge({});
+ expect(spy).to.have.been.called;
+ });
+ });
+
describe("#perform", function () {
+ it("returns a difference", function () {
+ expect(history.perform(action).changes()).to.eql({});
+ });
+
it("updates the graph", function () {
var node = iD.Node();
history.perform(function (graph) { return graph.replace(node); });
@@ -27,8 +45,8 @@ describe("iD.History", function () {
it("emits a change event", function () {
history.on('change', spy);
- history.perform(action);
- expect(spy).to.have.been.calledWith([]);
+ var difference = history.perform(action);
+ expect(spy).to.have.been.calledWith(difference);
});
it("performs multiple actions", function () {
@@ -42,6 +60,10 @@ describe("iD.History", function () {
});
describe("#replace", function () {
+ it("returns a difference", function () {
+ expect(history.replace(action).changes()).to.eql({});
+ });
+
it("updates the graph", function () {
var node = iD.Node();
history.replace(function (graph) { return graph.replace(node); });
@@ -56,8 +78,8 @@ describe("iD.History", function () {
it("emits a change event", function () {
history.on('change', spy);
- history.replace(action);
- expect(spy).to.have.been.calledWith([]);
+ var difference = history.replace(action);
+ expect(spy).to.have.been.calledWith(difference);
});
it("performs multiple actions", function () {
@@ -71,6 +93,11 @@ describe("iD.History", function () {
});
describe("#pop", function () {
+ it("returns a difference", function () {
+ history.perform(action, "annotation");
+ expect(history.pop().changes()).to.eql({});
+ });
+
it("updates the graph", function () {
history.perform(action, "annotation");
history.pop();
@@ -86,12 +113,16 @@ describe("iD.History", function () {
it("emits a change event", function () {
history.perform(action);
history.on('change', spy);
- history.pop();
- expect(spy).to.have.been.calledWith([]);
+ var difference = history.pop();
+ expect(spy).to.have.been.calledWith(difference);
});
});
describe("#undo", function () {
+ it("returns a difference", function () {
+ expect(history.undo().changes()).to.eql({});
+ });
+
it("pops the undo stack", function () {
history.perform(action, "annotation");
history.undo();
@@ -121,12 +152,16 @@ describe("iD.History", function () {
it("emits a change event", function () {
history.perform(action);
history.on('change', spy);
- history.undo();
- expect(spy).to.have.been.calledWith([]);
+ var difference = history.undo();
+ expect(spy).to.have.been.calledWith(difference);
});
});
describe("#redo", function () {
+ it("returns a difference", function () {
+ expect(history.redo().changes()).to.eql({});
+ });
+
it("emits an redone event", function () {
history.perform(action);
history.undo();
@@ -139,8 +174,8 @@ describe("iD.History", function () {
history.perform(action);
history.undo();
history.on('change', spy);
- history.redo();
- expect(spy).to.have.been.calledWith([]);
+ var difference = history.redo();
+ expect(spy).to.have.been.calledWith(difference);
});
});
diff --git a/test/spec/graph/node.js b/test/spec/graph/node.js
index ad149261e..71a2c5e95 100644
--- a/test/spec/graph/node.js
+++ b/test/spec/graph/node.js
@@ -4,15 +4,6 @@ describe('iD.Node', function () {
expect(iD.Node().type).to.equal("node");
});
- it("returns a created Entity if no ID is specified", function () {
- expect(iD.Node().created()).to.be.ok;
- });
-
- it("returns an unmodified Entity if ID is specified", function () {
- expect(iD.Node({id: 'n1234'}).created()).not.to.be.ok;
- expect(iD.Node({id: 'n1234'}).modified()).not.to.be.ok;
- });
-
it("defaults tags to an empty object", function () {
expect(iD.Node().tags).to.eql({});
});
diff --git a/test/spec/graph/relation.js b/test/spec/graph/relation.js
index 8edf9c1e7..6e00dcacb 100644
--- a/test/spec/graph/relation.js
+++ b/test/spec/graph/relation.js
@@ -10,15 +10,6 @@ describe('iD.Relation', function () {
expect(iD.Relation().type).to.equal("relation");
});
- it("returns a created Entity if no ID is specified", function () {
- expect(iD.Relation().created()).to.be.ok;
- });
-
- it("returns an unmodified Entity if ID is specified", function () {
- expect(iD.Relation({id: 'r1234'}).created()).not.to.be.ok;
- expect(iD.Relation({id: 'r1234'}).modified()).not.to.be.ok;
- });
-
it("defaults members to an empty array", function () {
expect(iD.Relation().members).to.eql([]);
});
diff --git a/test/spec/graph/way.js b/test/spec/graph/way.js
index ec41edec6..6b6b1543f 100644
--- a/test/spec/graph/way.js
+++ b/test/spec/graph/way.js
@@ -10,15 +10,6 @@ describe('iD.Way', function() {
expect(iD.Way().type).to.equal("way");
});
- it("returns a created Entity if no ID is specified", function () {
- expect(iD.Way().created()).to.be.ok;
- });
-
- it("returns an unmodified Entity if ID is specified", function () {
- expect(iD.Way({id: 'w1234'}).created()).not.to.be.ok;
- expect(iD.Way({id: 'w1234'}).modified()).not.to.be.ok;
- });
-
it("defaults nodes to an empty array", function () {
expect(iD.Way().nodes).to.eql([]);
});