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([]); });