From e7a895c884e669c6c67cc27174624003163473d5 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Mon, 5 Nov 2012 11:23:50 -0500 Subject: [PATCH] Re-introduce idea of operations, add 'add node' operation --- index.html | 2 +- js/iD/Util.js | 4 +- js/iD/actions/AddNodeToWayAction.js | 69 --------------- js/iD/actions/AddPlace.js | 1 - js/iD/actions/CreateEntityAction.js | 50 ----------- js/iD/actions/MoveNodeAction.js | 62 ------------- js/iD/actions/UndoStack.js | 104 ---------------------- js/iD/actions/UndoableAction.js | 133 ---------------------------- js/iD/actions/actions.js | 42 ++++----- js/iD/actions/operations.js | 9 ++ js/iD/graph/Graph.js | 26 ++++-- js/iD/renderer/Map.js | 16 ++-- js/iD/renderer/markers.js | 10 --- js/iD/renderer/style.js | 11 +++ 14 files changed, 71 insertions(+), 468 deletions(-) delete mode 100644 js/iD/actions/AddNodeToWayAction.js delete mode 100644 js/iD/actions/AddPlace.js delete mode 100644 js/iD/actions/CreateEntityAction.js delete mode 100644 js/iD/actions/MoveNodeAction.js delete mode 100644 js/iD/actions/UndoStack.js delete mode 100644 js/iD/actions/UndoableAction.js create mode 100644 js/iD/actions/operations.js diff --git a/index.html b/index.html index 9c5d8ff83..9295cdbb8 100755 --- a/index.html +++ b/index.html @@ -46,7 +46,7 @@ - + diff --git a/js/iD/Util.js b/js/iD/Util.js index f4d29b3cf..55f908233 100644 --- a/js/iD/Util.js +++ b/js/iD/Util.js @@ -1,9 +1,9 @@ iD.Util = {}; -iD.Util._id = 0; +iD.Util._id = -1; iD.Util.id = function() { - return iD.Util._id++; + return iD.Util._id--; }; iD.Util.friendlyName = function(entity) { diff --git a/js/iD/actions/AddNodeToWayAction.js b/js/iD/actions/AddNodeToWayAction.js deleted file mode 100644 index e719bb906..000000000 --- a/js/iD/actions/AddNodeToWayAction.js +++ /dev/null @@ -1,69 +0,0 @@ -// iD/actions/AddNodeToWayAction.js - -define(['dojo/_base/declare','iD/actions/UndoableAction'], function(declare){ - - // ---------------------------------------------------------------------- - // AddNodeToWayAction class - - declare("iD.actions.AddNodeToWayAction", [iD.actions.UndoableEntityAction], { - - node: null, - nodeList: null, - index: 0, - firstNode: null, - autoDelete: true, - - constructor: function(way, node, nodeList, index, autoDelete) { - // summary: Add a node to a way at a specified index, or -1 for the end of the way. - this.entity = way; - this.node = node; - this.nodeList = nodeList; - this.index = index; - this.autoDelete = autoDelete; - }, - - doAction: function() { - var way = this.entity; // shorthand - - // undelete way if it was deleted before (only happens on redo) - if (way.deleted) { - way.setDeletedState(false); - if (!this.firstNode.hasParentWays()) { - this.firstNode.connection.unregisterPOI(firstNode); - } - this.firstNode.addParent(way); - } - - // add the node - if (this.index === -1) this.index = this.nodeList.length; - this.node.entity.addParent(way); - this.node.connection.unregisterPOI(this.node); - this.nodeList.splice(this.index, 0, this.node); - this.markDirty(); - way.expandBbox(this.node); - - return this.SUCCESS; - }, - - undoAction: function() { - // summary: Remove the added node. Fixme: if the way is now 1-length, we should - // do something like deleting it and converting the remaining node to a POI. - var way=this.entity; // shorthand - if (this.autoDelete && way.length() === 2 && - way.parentRelations().length()) return this.FAIL; - - // remove node - var removed=nodeList.splice(index, 1); - if (!_.contains(this.nodeList, removed[0])) { - removed[0].removeParent(way); - } - this.markClean(); - way.connection.refreshEntity(way); - - return this.SUCCESS; - } - }); - - // ---------------------------------------------------------------------- - // End of module -}); diff --git a/js/iD/actions/AddPlace.js b/js/iD/actions/AddPlace.js deleted file mode 100644 index 8b1378917..000000000 --- a/js/iD/actions/AddPlace.js +++ /dev/null @@ -1 +0,0 @@ - diff --git a/js/iD/actions/CreateEntityAction.js b/js/iD/actions/CreateEntityAction.js deleted file mode 100644 index 407b1bb8a..000000000 --- a/js/iD/actions/CreateEntityAction.js +++ /dev/null @@ -1,50 +0,0 @@ -// iD/actions/UndoableAction.js - -define(['dojo/_base/declare','iD/actions/UndoableAction'], function(declare) { - - // ---------------------------------------------------------------------- - // CreateEntityAction class - - declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], { - - setCreate:null, - deleteAction:null, - - constructor: function(entity,setCreate) { - // summary: Create a new entity - way, node or relation. - this.setCreate = setCreate; - this.setName("Create " + entity.entityType); - }, - - run: function() { - // summary: Call out to the specified method (in the Controller) to create the entity. - // See undoAction for explanation of special redo handling. - if (this.deleteAction!==null) { - this.deleteAction.undoAction(); // redo - } else { - this.setCreate(this.entity, false); // first time - } - this.markDirty(); - return this.SUCCESS; - }, - - undo: function() { - // summary: Special handling for undoing a create. When undo is called, instead - // of simply removing the entity, we work through to make a Delete[Entity]Action, - // call that, and store it for later. Then, when this action is called again - // (i.e. a redo), instead of creating yet another entity, we call the deleteAction.undoAction. - if (this.deleteAction===null) { this.entity.remove(this.setAction); } - this.deleteAction.doAction(); - this.markClean(); - return this.SUCCESS; - }, - - setAction: function(action) { - // summary: Set the associated delete action (see undoAction for explanation). - deleteAction = action; - } - }); - - // ---------------------------------------------------------------------- - // End of module -}); diff --git a/js/iD/actions/MoveNodeAction.js b/js/iD/actions/MoveNodeAction.js deleted file mode 100644 index d377f4dc4..000000000 --- a/js/iD/actions/MoveNodeAction.js +++ /dev/null @@ -1,62 +0,0 @@ -// iD/actions/MoveNodeAction.js - -define(['dojo/_base/declare','iD/actions/UndoableAction'], - function(declare) { - - // ---------------------------------------------------------------------- - // MoveNodeAction class - - declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], { - - createTime: NaN, - oldLat: NaN, - oldLon: NaN, - newLat: NaN, - newLon: NaN, - setLatLon: null, - - constructor: function(node, newLat, newLon, setLatLon) { - // summary: Move a node to a new position. - this.entity = node; - this.newLat = newLat; - this.newLon = newLon; - this.setLatLon = setLatLon; - this.createTime = new Date().getTime(); - }, - - run: function() { - var node = this.entity; - this.oldLat = node.lat; - this.oldLon = node.lon; - if (this.oldLat === this.newLat && - this.oldLon === this.newLon) { - return NO_CHANGE; - } - this.setLatLon(this.newLat, this.newLon); - this.markDirty(); - node.refresh(); - return true; - }, - - undo: function() { - this.setLatLon(this.oldLat, this.oldLon); - this.markClean(); - this.refresh(); - return true; - }, - - mergePrevious: function(prev) { - if (prev.declaredClass!=this.declaredClass) { return false; } - - if (prev.entity == this.entity && prev.createTime+1000>this.createTime) { - this.oldLat = prev.oldLat; - this.oldLon = prev.oldLon; - return true; - } - return false; - } - }); - - // ---------------------------------------------------------------------- - // End of module - }); diff --git a/js/iD/actions/UndoStack.js b/js/iD/actions/UndoStack.js deleted file mode 100644 index 9129dab59..000000000 --- a/js/iD/actions/UndoStack.js +++ /dev/null @@ -1,104 +0,0 @@ -// iD/actions/UndoStack.js -// ** FIXME: a couple of AS3-isms in undoIfAction/removeLastIfAction - -// ---------------------------------------------------------------------- -// UndoStack base class - -if (typeof iD === 'undefined') iD = {}; - -iD.UndoStack = function() { - - var stack = {}, - undoActions = [], - redoActions = []; - - var FAIL = 0, - SUCCESS = 1, - NO_CHANGE = 2; - - stack.add = function(action) { - // summary: Add an action to the undo stack - if (undoActions.length > 0) { - var previous = undoActions[undoActions.length - 1]; - if (action.mergePrevious(previous)) { - action.wasDirty = previous.wasDirty; - action.connectionWasDirty = previous.connectionWasDirty; - undoActions.pop(); - } - } - undoActions.push(action); - redoActions = []; - }; - - stack.breakUndo = function() { - // summary: Wipe the undo stack - typically used after saving. - undoActions = []; - redoActions = []; - }; - - stack.canUndo = function() { - // summary: Are there any items on the undo stack? - return undoActions.length > 0; - }; - - stack.canRedo = function() { - // summary: Are there any redoable actions? - return redoActions.length > 0; - }; - - stack.undo = function() { - // summary: Undo the most recent action, and add it to the top of the redo stack. - if (!stack.canUndo()) { return; } - var action = undoActions.pop(); - action.undo(); - redoActions.push(action); - }; - - stack.undoIfAction = function(_action) { - // summary: Undo the most recent action _only_ if it was of a certain type. - // Fixme: isInstanceOf needs to be made into JavaScript. - if (!undoActions.length) { return; } - if (undoActions[undoActions.length-1].isInstanceOf(_action)) { - undo(); - return true; - } - return false; - }; - - stack.removeLastIfAction = function(_action) { - // summary: Remove the most recent action from the stack _only_ if it was of a certain type. - // Fixme: isInstanceOf needs to be made into JavaScript. - if (undoActions.length && - undoActions[undoActions.length - 1].isInstanceOf(_action)) { - undoActions.pop(); - } - }; - - stack.getUndoDescription = function() { - // summary: Get the name of the topmost item on the undo stack. - if (!undoActions.length) return null; - if (undoActions[undoActions.length - 1].name) { - return undoActions[undoActions.length - 1].name; - } - return null; - }; - - stack.getRedoDescription = function() { - // summary: Get the name of the topmost item on the redo stack. - if (!redoActions.length) return null; - if (redoActions[redoActions.length - 1].name) { - return redoActions[redoActions.length - 1].name; - } - return null; - }; - - stack.redo = function() { - // summary: Takes the action most recently undone, does it, and adds it to the undo stack. - if (!stack.canRedo()) { return; } - var action = redoActions.pop(); - action.run(); - undoActions.push(action); - }; - - return stack; -}; diff --git a/js/iD/actions/UndoableAction.js b/js/iD/actions/UndoableAction.js deleted file mode 100644 index 0971af7e8..000000000 --- a/js/iD/actions/UndoableAction.js +++ /dev/null @@ -1,133 +0,0 @@ -// iD/actions/UndoableAction.js -// we don't currently do connectionWasDirty, and we should - -define(['dojo/_base/declare'], function(declare){ - -// ---------------------------------------------------------------------- -// UndoableAction base class - -declare("iD.actions.UndoableAction", null, { - mergePrevious:function() { - // summary: If two successive actions can be merged (in particular, two successive node moves), do that. - return false; - }, - setName:function(_name) { - // summary: Set the name of an action. For UI and debugging purposes. - this.name=_name; - } -}); - -// ---------------------------------------------------------------------- -// UndoableEntityAction class - -declare("iD.actions.UndoableEntityAction", [iD.actions.UndoableAction], { - - wasDirty: false, - connectionWasDirty: false, - initialised: false, - entity: null, - - constructor:function(_entity) { - // summary: An UndoableEntityAction is a user-induced change to the map data - // (held within the Connection) which can be undone, with a reference - // to a particular entity (to which the change was made). - this.entity=_entity; - }, - - markDirty:function() { - // summary: Mark a change to the entity ('dirtying' it). - if (!this.initialised) this.init(); - if (!this.wasDirty) this.entity._markDirty(); - if (!this.connectionWasDirty) this.entity.connection.modified = true; - }, - - markClean:function() { - // summary: If the entity was clean before, revert the dirty flag to that state. - if (!this.initialised) this.init(); - if (!this.wasDirty) this.entity._markClean(); - if (!this.connectionWasDirty) this.entity.connection.modified = false; - }, - - init:function() { - // summary: Record whether or not the entity and connection were clean before this action started - this.wasDirty = this.entity.modified; - this.connectionWasDirty = this.entity.connection.modified; - this.initialised = true; - }, - - toString:function() { - return this.name + " " + this.entity.entityType + " " + this.entity.id; - } - -}); - - -// ---------------------------------------------------------------------- -// CompositeUndoableAction class - -declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], { - - actions: null, - actionsDone: false, - - constructor:function() { - // summary: A CompositeUndoableAction is a group of user-induced changes to the map data - // (held within the Connection) which are executed, and therefore undone, - // in a batch. For example, creating a new node and a new way containing it. - this.actions = []; - }, - - push:function(action) { - // summary: Add a single action to the list of actions comprising this CompositeUndoableAction. - this.actions.push(action); - }, - - clearActions:function() { - // summary: Clear the list of actions. - this.actions = []; - }, - - doAction:function() { - // summary: Execute all the actions one-by-one as a transaction (i.e. roll them all back if one fails). - if (this.actionsDone) { return this.FAIL; } - var somethingDone = false; - for (var i = 0; i < this.actions.length; i++) { - var action = this.actions[i]; - var result = action.doAction(); - switch (result) { - case this.NO_CHANGE: - // splice this one out as it doesn't do anything - this.actions.splice(i,1); - i--; - break; - case this.FAIL: - this._undoFrom(i); - return this.FAIL; - default: - somethingDone=true; - break; - } - } - this.actionsDone = true; - return somethingDone ? this.SUCCESS : this.NO_CHANGE; - }, - - undoAction:function() { - // summary: Roll back all the actions one-by-one. - if (!this.actionsDone) { return this.FAIL; } - this._undoFrom(this.actions.length); - return true; - }, - - _undoFrom:function(index) { - // summary: Undo all the actions after a certain index. - for (var i=index-1; i>=0; i--) { - this.actions[i].undoAction(); - } - this.actionsDone=false; - } -}); - -// ---------------------------------------------------------------------- -// End of module -}); diff --git a/js/iD/actions/actions.js b/js/iD/actions/actions.js index 156cd532c..2584b35ed 100644 --- a/js/iD/actions/actions.js +++ b/js/iD/actions/actions.js @@ -8,36 +8,49 @@ iD.actions.AddPlace = { iD.actions.AddPlace.enter(); }); }, + node: function(ll) { + return { + type: 'node', + lat: ll[1], + lon: ll[0], + id: iD.Util.id(), + tags: {} + }; + }, enter: function() { d3.selectAll('button').classed('active', false); d3.selectAll('button#place').classed('active', true); var surface = this.map.surface; var teaser = surface.selectAll('g#temp-g') - .append('g').attr('id', 'teaser-g'); + .append('g').attr('id', 'addplace'); teaser.append('circle') .attr('class', 'teaser-point') .attr('r', 10); - surface.on('mousemove.shift', function() { + surface.on('mousemove.addplace', function() { teaser.attr('transform', function() { var off = d3.mouse(surface.node()); return 'translate(' + off + ')'; }); }); - surface.on('click', function() { - var off = d3.mouse(surface.node()); + surface.on('click.addplace', function() { + var ll = this.map.projection.invert( + d3.mouse(surface.node())); + iD.operations.addNode(this.map, this.node(ll)); this.exit(); }.bind(this)); - // Bind clicks to the map to 'add a place' and - // add little floaty place + d3.select(document).on('keydown.addplace', function() { + if (d3.event.keyCode === 27) this.exit(); + }.bind(this)); }, exit: function() { - this.map.surface.on('mousemove.shift', null); - d3.selectAll('#teaser-g').remove(); + this.map.surface.on('.addplace', null); + d3.select(document).on('.addplace', null); + d3.selectAll('#addplace').remove(); d3.selectAll('button#place').classed('active', false); } }; @@ -53,9 +66,6 @@ iD.actions.AddRoad = { enter: function() { d3.selectAll('button').classed('active', false); d3.selectAll('button#road').classed('active', true); - - // Bind clicks to the map to 'add a road' and - // add little floaty point }, exit: function() { d3.selectAll('button#road').classed('active', false); @@ -73,9 +83,6 @@ iD.actions.AddArea = { enter: function() { d3.selectAll('button').classed('active', false); d3.selectAll('button#area').classed('active', true); - - // Bind clicks to the map to 'add an area' and - // add little floaty point }, exit: function() { d3.selectAll('button#area').classed('active', false); @@ -108,12 +115,5 @@ iD.controller = function(map) { controller.go(iD.actions.Move); - // Pressing 'escape' should exit any action. - d3.select(document).on('keydown', function() { - if (d3.event.keyCode === 27) { - controller.go(iD.actions.Move); - } - }); - return controller; }; diff --git a/js/iD/actions/operations.js b/js/iD/actions/operations.js new file mode 100644 index 000000000..a10d56bb3 --- /dev/null +++ b/js/iD/actions/operations.js @@ -0,0 +1,9 @@ +iD.operations = {}; + +iD.operations.addNode = function(map, node) { + map.graph.modify(function(graph) { + var o = {}; + o[node.id] = node; + return graph.set(o); + }, 'Added a new unidentified place'); +}; diff --git a/js/iD/graph/Graph.js b/js/iD/graph/Graph.js index 3a4036e8c..88ac49b16 100644 --- a/js/iD/graph/Graph.js +++ b/js/iD/graph/Graph.js @@ -7,6 +7,9 @@ iD.Graph.prototype = { // stack of previous versions of this datastructure prev: [], + // messages + annotations: [], + insert: function(a) { for (var i = 0; i < a.length; i++) { if (this.head[a[i].id]) return; @@ -14,13 +17,24 @@ iD.Graph.prototype = { } }, - modify: function(callback) { - // Previous version pushed onto stack - var o = pdata.object(this.head).get(); - prev.push(o); + modify: function(callback, annotation) { + // create a pdata wrapper of current head + var o = pdata.object(this.head); - // Make head a copy with no common history - this.head = pdata.object(this.head).get(); + // Archive current version + this.prev.push(o.get()); + + // Let the operation make modification of a safe + // copy + var modified = callback(o); + + // Archive this version + this.prev.push(modified.get()); + // Annotate this version + this.annotations.push(annotation); + + // Make head the top of the previous stack + this.head = this.prev[this.prev.length - 1]; }, intersects: function(version, extent) { diff --git a/js/iD/renderer/Map.js b/js/iD/renderer/Map.js index 8f2e1f363..8cfe517ec 100755 --- a/js/iD/renderer/Map.js +++ b/js/iD/renderer/Map.js @@ -56,9 +56,7 @@ iD.Map = function(elem) { nodeline = function(d) { return linegen(d.nodes); }, - // Abstract a key function that looks for uids. This is given - // as a second argument to `.data()`. - key = function(d) { return d.uid; }; + key = function(d) { return d.id; }; // Creating containers // ------------------- @@ -112,7 +110,7 @@ iD.Map = function(elem) { casings = casing_g.selectAll('path.casing').data(ways, key), strokes = stroke_g.selectAll('path.stroke').data(ways, key), markers = hit_g.selectAll('image.marker') - .data(points.filter(iD.markerimage), key); + .data(points, key); // Fills fills.exit().remove(); @@ -142,16 +140,16 @@ iD.Map = function(elem) { .attr('class', class_marker) .on('click', selectClick) .attr({ width: 16, height: 16 }) - .attr('xlink:href', iD.markerimage) + .attr('xlink:href', iD.Style.markerimage) .call(dragbehavior); markers.attr('transform', function(d) { return 'translate(' + projection([d.lon, d.lat]) + ')'; }); if (selection.length) { - var uid = selection[0]; + var id = selection[0]; var active_entity = all.filter(function(a) { - return a.uid === uid && a.entityType === 'way'; + return a.id === id && a.entityType === 'way'; }); var handles = hit_g.selectAll('circle.handle') @@ -189,7 +187,7 @@ iD.Map = function(elem) { } function selectClick(d) { - selection = [d.uid]; + selection = [d.id]; drawVector(); d3.select('.inspector-wrap') .style('display', 'block') @@ -201,7 +199,7 @@ iD.Map = function(elem) { function augmentSelect(fn) { return function(d) { var c = fn(d); - if (selection.indexOf(d.uid) !== -1) { + if (selection.indexOf(d.id) !== -1) { c += ' active'; } return c; diff --git a/js/iD/renderer/markers.js b/js/iD/renderer/markers.js index bcb4a18c4..15c31220c 100644 --- a/js/iD/renderer/markers.js +++ b/js/iD/renderer/markers.js @@ -562,13 +562,3 @@ iD._markertable = (function(markers) { } return table; })(iD._markers); - -iD.markerimage = function(d) { - // TODO: optimize - for (var k in d.tags) { - var key = k + '=' + d.tags[k]; - if (iD._markertable[key]) { - return 'icons/' + iD._markertable[key] + '.png'; - } - } -}; diff --git a/js/iD/renderer/style.js b/js/iD/renderer/style.js index 1999ba232..3abfab6fc 100644 --- a/js/iD/renderer/style.js +++ b/js/iD/renderer/style.js @@ -30,6 +30,17 @@ iD.Style.waystack = function(a, b) { }; +iD.Style.markerimage = function(d) { + // TODO: optimize + for (var k in d.tags) { + var key = k + '=' + d.tags[k]; + if (iD._markertable[key]) { + return 'icons/' + iD._markertable[key] + '.png'; + } + } + return 'icons/unknown.png'; +}; + iD.Style.TAG_CLASSES = { 'highway': true, 'railway': true,