Re-introduce idea of operations, add 'add node' operation

This commit is contained in:
Tom MacWright
2012-11-05 11:23:50 -05:00
parent c8b5e238b3
commit e7a895c884
14 changed files with 71 additions and 468 deletions

View File

@@ -46,7 +46,7 @@
<script type='text/javascript' src='js/iD/ui/Inspector.js'></script>
<script type='text/javascript' src='js/iD/actions/actions.js'></script>
<script type='text/javascript' src='js/iD/actions/AddPlace.js'></script>
<script type='text/javascript' src='js/iD/actions/operations.js'></script>
<script type='text/javascript' src='js/iD/format/GeoJSON.js'></script>
<script type='text/javascript' src='js/iD/format/XML.js'></script>

View File

@@ -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) {

View File

@@ -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
});

View File

@@ -1 +0,0 @@

View File

@@ -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
});

View File

@@ -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
});

View File

@@ -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;
};

View File

@@ -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
});

View File

@@ -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;
};

View File

@@ -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');
};

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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';
}
}
};

View File

@@ -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,