Merge pull request #32 from systemed/simple-actions

Refactoring actions and undoing
This commit is contained in:
Tom MacWright
2012-10-23 13:14:03 -07:00
22 changed files with 738 additions and 893 deletions
+1 -1
View File
@@ -77,7 +77,7 @@ table th {
}
#modebuttons {
width:300px;
width:500px;
position:absolute;
left:0px;
top:0px;
+16 -1
View File
@@ -12,6 +12,7 @@
<div id="appLayout" class="demoLayout">
<script type="text/javascript" src="js/lib/underscore-min.js"></script>
<script type="text/javascript" src="js/lib/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="js/iD/actions/UndoStack.js"></script>
<script type="text/javascript" src="js/iD/Util.js"></script>
<script type="text/javascript" src="js/iD/Taginfo.js"></script>
<script type="text/javascript" src="js/iD/Node.js"></script>
@@ -86,6 +87,16 @@ require(["dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/dom","dojo/Evented
controller.setState(new iD.controller.shape.NoSelection());
});
$('#undo').click(function() {
controller.undoStack.undo();
map.updateUIs(true, true);
});
$('#redo').click(function() {
controller.undoStack.redo();
map.updateUIs(true, true);
});
// ----------------------------------------------------
// Map control handlers
@@ -118,7 +129,11 @@ require(["dojo/dom-geometry","dojo/dom-class","dojo/on","dojo/dom","dojo/Evented
<button id='add-place'>
+ Place</button><button id="add-road">
+ Road</button><button id="add-area">
+ Area</button>
+ Area</button><button id="undo">
&larr;</button><button id="redo">
&rarr;</button>
<div id='addPOI'>
<table id='dndgrid'>
</table>
+16 -26
View File
@@ -12,8 +12,7 @@ iD.Connection = function(apiURL) {
var nextNode = -1, // next negative ids
nextWay = -1, // |
nextRelation = -1, // |
nodes = {},
ways = {},
entities = {},
relations = {},
pois = {},
modified = false,
@@ -24,20 +23,20 @@ iD.Connection = function(apiURL) {
function assign(obj) {
// summary: Save an entity to the data store.
switch (obj.entityType) {
case "node": nodes[obj.id]=obj; break;
case "way": ways[obj.id]=obj; break;
case "relation": relations[obj.id]=obj; break;
case "node": entities[obj.id] = obj; break;
case "way": entities[obj.id] = obj; break;
case "relation": relations[obj.id] = obj; break;
}
}
function getOrCreate(id, type) {
// summary: Return an entity if it exists: if not, create an empty one with the given id, and return that.
if (type === 'node') {
if (!nodes[id]) assign(new iD.Node(connection, id, NaN, NaN, {}, false));
return nodes[id];
if (!entities[id]) assign(new iD.Node(connection, id, NaN, NaN, {}, false));
return entities[id];
} else if (type === 'way') {
if (!ways[id]) assign(new iD.Way(connection, id, [], {}, false));
return ways[id];
if (!entities[id]) assign(new iD.Way(connection, id, [], {}, false));
return entities[id];
} else if (type === 'relation') {
if (!relations[id]) assign(new iD.Relation(connection, id, [], {}, false));
return relations[id];
@@ -66,23 +65,15 @@ iD.Connection = function(apiURL) {
}
function getObjectsByBbox(left,right,top,bottom) {
// summary: Find all drawable entities that are within a given bounding box.
// returns: Object An object with four properties: .poisInside, .poisOutside, .waysInside, .waysOutside.
// summary: Find all drawable entities that are within a given bounding box.
// Each one is an array of entities.
var o = {
poisInside: [],
poisOutside: [],
waysInside: [],
waysOutside: []
inside: [],
outside: []
};
for (var id in ways) {
var way = ways[id];
if (way.within(left,right,top,bottom)) { o.waysInside.push(way); }
else { o.waysOutside.push(way); }
}
_.each(pois, function(node) {
if (node.within(left,right,top,bottom)) { o.poisInside.push(node); }
else { o.poisOutside.push(node); }
_.each(this.entities, function(e, id) {
if (e.within(left, right, top, bottom)) { o.inside.push(e); }
else { o.outside.push(e); }
});
return o;
}
@@ -183,7 +174,7 @@ iD.Connection = function(apiURL) {
return _(obj.childNodes).chain()
.filter(filterNodeName('nd'))
.map(function(item) {
return nodes[getAttribute(item,'ref')];
return entities[getAttribute(item,'ref')];
}).value();
}
@@ -202,8 +193,7 @@ iD.Connection = function(apiURL) {
};
}
connection.nodes = nodes;
connection.ways = ways;
connection.entities = entities;
connection.relations = relations;
connection.loadFromAPI = loadFromAPI;
connection.loadFromURL = loadFromURL;
+4 -6
View File
@@ -14,7 +14,7 @@ declare("iD.Controller", [Evented], {
constructor:function(map) {
// summary: The Controller marshalls ControllerStates and passes events to them.
this.map = map;
this.undoStack = new iD.actions.UndoStack();
this.undoStack = new iD.UndoStack();
this.editorCache = {};
},
@@ -29,17 +29,15 @@ declare("iD.Controller", [Evented], {
if (this.state) {
this.emit("exitState", {
bubbles: true,
cancelable: true,
state: this.state.stateNameAsArray()
cancelable: true
});
}
newState.setController(this);
newState.controller = this;
this.state = newState;
newState.enterState();
this.emit("enterState", {
bubbles: true,
cancelable: true,
state: this.state.stateNameAsArray()
cancelable: true
});
},
+1 -1
View File
@@ -48,7 +48,7 @@ iD.Way.prototype = {
// ---------------------
// Bounding-box handling
within:function(left,right,top,bottom) {
within: function(left,right,top,bottom) {
// TODO invert and just return
if (!this.extent.west ||
(this.extent.west < left && this.extent.east < left ) ||
+2 -3
View File
@@ -16,7 +16,7 @@ declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
this.setName("Create " + entity.entityType);
},
doAction: function() {
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) {
@@ -28,7 +28,7 @@ declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
return this.SUCCESS;
},
undoAction: function() {
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
@@ -43,7 +43,6 @@ declare("iD.actions.CreateEntityAction", [iD.actions.UndoableEntityAction], {
// summary: Set the associated delete action (see undoAction for explanation).
deleteAction = action;
}
});
// ----------------------------------------------------------------------
+6 -9
View File
@@ -17,34 +17,31 @@ declare("iD.actions.CreatePOIAction", [iD.actions.CompositeUndoableAction], {
constructor: function(connection, tags, lat, lon) {
// summary: Create a new node and set it as a POI. Used by drag-and-drop. Note that the
// node is remembered, so that on redo we can just reinstate it.
this.setName('Create POI');
this.setName('Create POI: ' + iD.Util.friendlyName(tags));
this.connection = connection;
this.tags = tags;
this.lat = lat;
this.lon = lon;
},
doAction:function() {
run: function() {
if (this.newNode === null) {
this.newNode = this.connection.doCreateNode(this.tags,
this.lat, this.lon,
_.bind(this.push, this));
}
this.inherited(arguments);
this.connection.registerPOI(this.newNode);
return this.SUCCESS;
return true;
},
undoAction:function() {
this.inherited(arguments);
undo: function() {
this.connection.unregisterPOI(this.newNode);
return this.SUCCESS;
return true;
},
getNode:function() {
getNode: function() {
return this.newNode;
}
});
// ----------------------------------------------------------------------
+10 -8
View File
@@ -15,7 +15,7 @@ declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], {
newLon: NaN,
setLatLon: null,
constructor:function(node, newLat, newLon, setLatLon) {
constructor: function(node, newLat, newLon, setLatLon) {
// summary: Move a node to a new position.
this.entity = node;
this.newLat = newLat;
@@ -24,25 +24,28 @@ declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], {
this.createTime = new Date().getTime();
},
doAction:function() {
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; }
if (this.oldLat === this.newLat &&
this.oldLon === this.newLon) {
return NO_CHANGE;
}
this.setLatLon(this.newLat, this.newLon);
this.markDirty();
node.refresh();
return this.SUCCESS;
return true;
},
undoAction:function() {
undo: function() {
this.setLatLon(this.oldLat, this.oldLon);
this.markClean();
this.refresh();
return this.SUCCESS;
return true;
},
mergePrevious:function(prev) {
mergePrevious: function(prev) {
if (prev.declaredClass!=this.declaredClass) { return false; }
if (prev.entity == this.entity && prev.createTime+1000>this.createTime) {
@@ -52,7 +55,6 @@ declare("iD.actions.MoveNodeAction", [iD.actions.UndoableEntityAction], {
}
return false;
}
});
// ----------------------------------------------------------------------
+85 -105
View File
@@ -1,124 +1,104 @@
// iD/actions/UndoStack.js
// ** FIXME: a couple of AS3-isms in undoIfAction/removeLastIfAction
define(['dojo/_base/declare'], function(declare){
// ----------------------------------------------------------------------
// UndoStack base class
declare("iD.actions.UndoStack", null, {
undoActions: null,
redoActions: null,
if (typeof iD === 'undefined') iD = {};
FAIL: 0,
SUCCESS: 1,
NO_CHANGE: 2,
iD.UndoStack = function() {
constructor: function() {
// summary: An undo stack. There can be any number of these, but almost all operations will
// take place on the global undo stack - implemented as a singleton-like property of
// the Controller.
this.undoActions=[];
this.redoActions=[];
},
addAction: function(_action) {
// summary: Do an action, and add it to the undo stack if it succeeded.
var result = _action.doAction();
switch (result) {
case this.FAIL:
// do something bad
break;
var stack = {},
undoActions = [],
redoActions = [];
case this.NO_CHANGE:
break;
var FAIL = 0,
SUCCESS = 1,
NO_CHANGE = 2;
case this.SUCCESS:
default:
if (this.undoActions.length>0) {
var previous = this.undoActions[this.undoActions.length-1];
if (_action.mergePrevious(previous)) {
_action.wasDirty = previous.wasDirty;
_action.connectionWasDirty = previous.connectionWasDirty;
this.undoActions.pop();
}
}
this.undoActions.push(_action);
this.redoActions=[];
break;
}
},
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 = [];
};
breakUndo: function() {
// summary: Wipe the undo stack - typically used after saving.
this.undoActions = [];
this.redoActions = [];
},
stack.breakUndo = function() {
// summary: Wipe the undo stack - typically used after saving.
undoActions = [];
redoActions = [];
};
canUndo: function() {
// summary: Are there any items on the undo stack?
return this.undoActions.length > 0;
},
stack.canUndo = function() {
// summary: Are there any items on the undo stack?
return undoActions.length > 0;
};
canRedo: function() {
// summary: Are there any redoable actions?
return this.redoActions.length > 0;
},
stack.canRedo = function() {
// summary: Are there any redoable actions?
return redoActions.length > 0;
};
undo: function() {
// summary: Undo the most recent action, and add it to the top of the redo stack.
if (!this.undoActions.length) { return; }
var action = undoActions.pop();
action.undoAction();
redoActions.push(action);
},
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 (!this.undoActions.length) { return; }
if (this.undoActions[this.undoActions.length-1].isInstanceOf(_action)) {
this.undo();
return true;
}
return false;
},
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 (this.undoActions.length && this.undoActions[this.undoActions.length-1].isInstanceOf(_action)) {
this.undoActions.pop();
}
},
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);
};
getUndoDescription: function() {
// summary: Get the name of the topmost item on the undo stack.
if (!this.undoActions.length) return null;
if (this.undoActions[this.undoActions.length-1].name) {
return this.undoActions[this.undoActions.length-1].name;
}
return null;
},
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;
};
getRedoDescription: function() {
// summary: Get the name of the topmost item on the redo stack.
if (!this.redoActions.length) return null;
if (this.redoActions[this.redoActions.length-1].name) {
return this.redoActions[this.redoActions.length-1].name;
}
return null;
},
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();
}
};
redo: function() {
// summary: Takes the action most recently undone, does it, and adds it to the undo stack.
if (!this.redoActions.length) { return; }
var action = this.redoActions.pop();
action.doAction();
this.undoActions.push(action);
}
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;
};
// ----------------------------------------------------------------------
// End of module
});
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;
};
+7 -36
View File
@@ -7,42 +7,14 @@ define(['dojo/_base/declare'], function(declare){
// UndoableAction base class
declare("iD.actions.UndoableAction", null, {
FAIL: 0,
SUCCESS: 1,
NO_CHANGE: 2,
name: "",
constructor:function() {
// summary: An UndoableAction is a user-induced change to the map data (held within the Connection)
// which can be undone. The UndoableAction doesn't actually change the data: it provides
// the logic around the change, but the change itself is generally made by the model.
// Therefore the UndoableAction constructor needs to be passed a reference
// to a method that actually makes the change - for example, MoveNodeAction will be
// passed a reference to Node._setLatLonImmediate.
},
doAction:function() {
// summary: Do the action.
return FAIL;
},
undoAction:function() {
// summary: Undo the action.
return FAIL;
},
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;
}
});
// ----------------------------------------------------------------------
@@ -63,14 +35,14 @@ declare("iD.actions.UndoableEntityAction", [iD.actions.UndoableAction], {
},
markDirty:function() {
// summary: Mark a change to the entity ('dirtying' it).
// 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.
// 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;
@@ -102,7 +74,7 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
// 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=[];
this.actions = [];
},
push:function(action) {
@@ -112,14 +84,14 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
clearActions:function() {
// summary: Clear the list of actions.
this.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 somethingDone = false;
for (var i = 0; i < this.actions.length; i++) {
var action = this.actions[i];
var result = action.doAction();
switch (result) {
@@ -144,7 +116,7 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
// summary: Roll back all the actions one-by-one.
if (!this.actionsDone) { return this.FAIL; }
this._undoFrom(this.actions.length);
return this.SUCCESS;
return true;
},
_undoFrom:function(index) {
@@ -154,7 +126,6 @@ declare("iD.actions.CompositeUndoableAction", [iD.actions.UndoableAction], {
}
this.actionsDone=false;
}
});
// ----------------------------------------------------------------------
-62
View File
@@ -1,62 +0,0 @@
// iD/controller/ControllerState.js
define(['dojo/_base/declare'], function(declare) {
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.ControllerState", null, {
controller: null, // parent Controller
constructor:function() {
// summary: Base class for ControllerStates.
},
setController:function(_controller) {
// summary: Set a reference to the parent Controller.
this.controller=_controller;
},
processMouseEvent:function(event,entityUI) {
// summary: Process mouse events. Most of the UI handling goes on in here.
// returns: iD.controller.ControllerState The ControllerState to move to as a result of the user's actions (or just 'this' for no change).
},
enterState:function() {
// summary: Do any work required for entering the ControllerState, such as highlighting the selected entity.
},
exitState:function(newState) {
// summary: Do any work required to leave the ControllerState clearly, such as unhighlighting the selected entity.
},
stateName:function() {
// summary: Return the name of this state as a string, e.g. 'edit.NoSelection'.
// return: String
return this.stateNameAsArray.join('.');
},
stateNameAsArray:function() {
// summary: Return the name of this state as an array, e.g. ['edit','NoSelection'].
// return: Array
return this.__proto__.declaredClass.split('.').slice(2);
},
getConnection: function() {
// summary: Shorthand to return the Connection associated with this Controller (via its Map object).
// return: iD.Connection
return this.controller.map.connection;
},
undoAdder:function() {
// summary: Shorthand for adding an action to the global undo stack, setting the scope correctly.
// return: Function
return _.bind(this.controller.undoStack.addAction,
this.controller.undoStack);
}
});
// ----------------------------------------------------------------------
// End of module
});
+4 -6
View File
@@ -1,13 +1,13 @@
// iD/controller/edit/EditBaseState.js
define(['dojo/_base/declare','dojo/_base/lang','dojo/_base/array','dojo/on',
'dijit/registry','dijit/TooltipDialog','dijit/Dialog','dijit/popup',
'iD/controller/ControllerState'], function(declare,lang,array,on,registry){
'dijit/registry'
], function(declare,lang,array,on,registry){
// ----------------------------------------------------------------------
// EditBaseState class - provides shared UI functions to edit mode states
declare("iD.controller.edit.EditBaseState", [iD.controller.ControllerState], {
declare("iD.controller.edit.EditBaseState", null, {
editortooltip: null,
@@ -16,10 +16,8 @@ declare("iD.controller.edit.EditBaseState", [iD.controller.ControllerState], {
// object is selected and the user is making changes to it.
},
openEditorTooltip: function(x, y, entity) {
openEditorTooltip: function(entity) {
// summary: Open the initial 'Edit tags/Edit shape' tooltip.
// x: Number Screen co-ordinate.
// y: Number Screen co-ordinate.
// entity: iD.Entity The entity to be edited.
$('.edit-pane h2').text(iD.Util.friendlyName(entity));
$('.edit-pane').show().addClass('active');
+1 -1
View File
@@ -13,7 +13,7 @@ define(['dojo/_base/declare',
declare("iD.controller.edit.NoSelection", [iD.controller.edit.EditBaseState], {
constructor: function() {
// summary: In 'Edit object' mode but nothing selected.
// summary: In 'Edit object' mode but nothing selected.
},
enterState: function() {
+4 -6
View File
@@ -20,9 +20,7 @@ define(['dojo/_base/declare','iD/controller/edit/EditBaseState'], function(decla
this.nodeUI = map.getUI(this.node);
this.nodeUI.setStateClass('selected')
.redraw();
this.openEditorTooltip(
map.lon2screen(this.node.lon),
map.lat2screen(this.node.lat), this.node);
this.openEditorTooltip(this.node);
return this;
},
@@ -33,10 +31,10 @@ define(['dojo/_base/declare','iD/controller/edit/EditBaseState'], function(decla
return this;
},
processMouseEvent: function(event,entityUI) {
processMouseEvent: function(event, entityUI) {
if (event.type !== 'click') return this;
var entity=entityUI ? entityUI.entity : null;
var entityType=entity ? entity.entityType : null;
var entity = entityUI ? entityUI.entity : null;
var entityType = entity ? entity.entityType : null;
switch (entityType) {
case null: return new iD.controller.edit.NoSelection();
case 'node': return new iD.controller.edit.SelectedPOINode(entityUI.entity);
+2 -2
View File
@@ -14,13 +14,13 @@
*/
define(['dojo/_base/declare', 'dojo/_base/lang', 'dojox/gfx/shape', 'iD/controller/ControllerState'],
define(['dojo/_base/declare', 'dojo/_base/lang', 'dojox/gfx/shape'],
function(declare, lang, shape){
// ----------------------------------------------------------------------
// DrawWay class
declare("iD.controller.shape.DrawWay", [iD.controller.ControllerState], {
declare("iD.controller.shape.DrawWay", null, {
way: null,
wayUI: null,
+14 -12
View File
@@ -15,7 +15,6 @@
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState',
'iD/controller/shape/DrawWay',
'iD/controller/shape/SelectedWay',
'iD/controller/shape/SelectedPOINode'
@@ -24,7 +23,7 @@ define(['dojo/_base/declare',
// ----------------------------------------------------------------------
// ControllerState base class
declare("iD.controller.shape.NoSelection", [iD.controller.ControllerState], {
declare("iD.controller.shape.NoSelection", null, {
constructor: function(intent) {
// summary: In 'Draw shape' mode but nothing is selected.
@@ -48,6 +47,7 @@ declare("iD.controller.shape.NoSelection", [iD.controller.ControllerState], {
var entity = entityUI ? entityUI.entity : null;
var entityType = entity ? entity.entityType : null;
var map = this.controller.map;
var connection = map.connection;
if (event.type === 'click') {
if (entityType === 'node') {
@@ -66,24 +66,26 @@ declare("iD.controller.shape.NoSelection", [iD.controller.ControllerState], {
if (this.intent === 'way') {
// Click to start a new way
var undo = new iD.actions.CompositeUndoableAction();
var startNode = this.getConnection().doCreateNode({},
var startNode = connection.doCreateNode({},
map.coord2lat(map.mouseY(event)),
map.coord2lon(map.mouseX(event)), _.bind(undo.push, undo) );
var way = this.getConnection().doCreateWay({}, [startNode], _.bind(undo.push, undo) );
var way = connection.doCreateWay({}, [startNode], _.bind(undo.push, undo) );
this.controller.undoStack.addAction(undo);
this.controller.map.createUI(way);
return new iD.controller.shape.DrawWay(way);
} else if (this.intent === 'node') {
var action = new iD.actions.CreatePOIAction(this.getConnection(), {},
var action = new iD.actions.CreatePOIAction(connection, {},
map.coord2lat(map.mouseY(event)),
map.coord2lon(map.mouseX(event)));
this.controller.undoStack.addAction(action);
var node = action.getNode();
this.controller.map.createUI(node);
var state = new iD.controller.edit.SelectedPOINode(node);
state.controller = this.controller;
state.enterState();
return state;
if (action.run()) {
this.controller.undoStack.add(action);
var node = action.getNode();
this.controller.map.createUI(node);
var state = new iD.controller.edit.SelectedPOINode(node);
state.controller = this.controller;
state.enterState();
return state;
}
}
}
}
+2 -3
View File
@@ -8,14 +8,13 @@
*/
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState'
'iD/actions/UndoableAction'
], function(declare){
// ----------------------------------------------------------------------
// SelectedPOINode class
declare("iD.controller.shape.SelectedPOINode", [iD.controller.ControllerState], {
declare("iD.controller.shape.SelectedPOINode", null, {
constructor:function() {
// summary: In 'Draw shape' mode and a POI node is selected, to be converted into a way.
+2 -3
View File
@@ -7,14 +7,13 @@
// to.
define(['dojo/_base/declare',
'iD/actions/UndoableAction',
'iD/controller/ControllerState'
'iD/actions/UndoableAction'
], function(declare) {
// ----------------------------------------------------------------------
// SelectedWayNode class
declare("iD.controller.shape.SelectedWay", [iD.controller.ControllerState], {
declare("iD.controller.shape.SelectedWay", null, {
way: null,
wayUI: null,
+117 -116
View File
@@ -8,126 +8,127 @@
// opacity
define(['dojo/_base/declare','iD/Entity','iD/renderer/Map'],
function(declare) {
function(declare) {
// ----------------------------------------------------------------------
// EntityUI base class
// ----------------------------------------------------------------------
// EntityUI base class
declare("iD.renderer.EntityUI", null, {
declare("iD.renderer.EntityUI", null, {
entity:null, // Entity this represents
map:null, // the Map object containing this
layer:0, // OSM layer
sprites:null, // array of sprites created for this EntityUI
styleList:null, // current StyleList
stateClasses:null, // list of stateClass tags to apply
entity:null, // Entity this represents
map:null, // the Map object containing this
layer:0, // OSM layer
sprites:null, // array of sprites created for this EntityUI
styleList:null, // current StyleList
stateClasses:null, // list of stateClass tags to apply
constructor:function(entity,map,stateClasses) {
// summary: Base class for a UI representing an entity.
this.entity=entity;
this.map=map;
this.stateClasses=stateClasses ? stateClasses.slice() : [];
this.sprites=[];
},
getConnection:function() {
// summary: Get the Connection from where the map draws its data.
return this.map.connection; // iD.Connection
},
targetGroup:function(groupType,sublayer) {
// summary: Find a gfx.Group to render on.
return this.map.sublayer(this.layer,groupType,sublayer); // dojox.gfx.Group
},
recordSprite:function(sprite) {
// summary: Record that an individual sprite (one stroke, icon or text item) has been added.
if (!_.include(this.sprites, sprite)) {
this.sprites.push(sprite);
constructor: function(entity,map,stateClasses) {
// summary: Base class for a UI representing an entity.
this.entity=entity;
this.map=map;
this.stateClasses=stateClasses ? stateClasses.slice() : [];
this.sprites=[];
},
getConnection: function() {
// summary: Get the Connection from where the map draws its data.
return this.map.connection; // iD.Connection
},
targetGroup: function(groupType,sublayer) {
// summary: Find a gfx.Group to render on.
return this.map.sublayer(this.layer,groupType,sublayer); // dojox.gfx.Group
},
recordSprite: function(sprite) {
// summary: Record that an individual sprite (one stroke, icon or text item) has been added.
if (!_.include(this.sprites, sprite)) {
this.sprites.push(sprite);
}
return sprite;
},
removeSprites: function() {
// summary: Clear all sprites currently used.
for (var i=0; i<this.sprites.length; i++) {
this.sprites[i].removeShape();
}
this.sprites=[];
},
refreshStyleList: function(tags) {
// summary: Calculate the list of styles that apply to this UI at this zoom level.
if (!this.styleList || !this.styleList.isValidAt(this.map.zoom)) {
this.styleList=this.map.ruleset.getStyles(this.entity,tags, this.map.zoom);
}
this.layer=this.styleList.layerOverride();
if (isNaN(this.layer)) {
this.layer=0;
if (tags.layer) { this.layer = +tags.layer; }
}
// Iterate through each subpart, drawing any styles on that layer
var drawn=false;
for (i=0; i<this.styleList.subparts.length; i++) {
var subpart=this.styleList.subparts[i];
}
},
getEnhancedTags: function() {
// summary: Return tags for this entity augmented by the EntityUI's state classes.
var tags = _.clone(this.entity.tags);
// Apply stateClasses (hover, selected, hoverway, selectedway)
for (var i in this.stateClasses) {
tags[':'+this.stateClasses[i]] = 'yes';
}
// todo - Add any common 'special-case' tags, e.g. :hasTags
return tags; // Object
},
// --------------------
// State class handling
setStateClasses:function(stateClasses) {
// summary: Set all state classes at once, and prompt a redraw if they're different to previously,
if (stateClasses && this.stateClasses.join(',')!=stateClasses.join(',')) {
this.stateClasses=stateClasses.slice();
this.invalidateStyleList();
}
return this;
},
setStateClass:function(sc) {
// summary: Set a single state class, and prompt a redraw if it wasn't set previously.
if (this.stateClasses.indexOf(sc)==-1) {
this.stateClasses.push(sc);
this.invalidateStyleList();
}
return this;
},
resetStateClass:function(sc) {
// summary: Reset a single state class, and prompt a redraw if it was set previously.
if (this.stateClasses.indexOf(sc)>-1) {
this.stateClasses.splice(this.stateClasses.indexOf(sc),1);
this.invalidateStyleList();
}
return this;
},
hasStateClass:function(sc) {
// summary: Is a particular state class set for this UI?
return this.stateClasses.indexOf(sc) > -1;
},
invalidateStyleList:function() {
// summary: Invalidate the StyleList so it's recalculated on next redraw.
this.styleList = null;
},
// --------------------
// Mouse event handling
entityMouseEvent:function(event) {
// summary: Receive a mouse event (e.g. clicking on the UI), and forward it to the Controller.
this.map.controller.entityMouseEvent(event, event.gfxTarget.source);
event.stopPropagation();
}
return sprite;
},
removeSprites:function() {
// summary: Clear all sprites currently used.
for (var i=0; i<this.sprites.length; i++) {
this.sprites[i].removeShape();
}
this.sprites=[];
},
refreshStyleList:function(tags) {
// summary: Calculate the list of styles that apply to this UI at this zoom level.
if (!this.styleList || !this.styleList.isValidAt(this.map.zoom)) {
this.styleList=this.map.ruleset.getStyles(this.entity,tags, this.map.zoom);
}
this.layer=this.styleList.layerOverride();
if (isNaN(this.layer)) {
this.layer=0;
if (tags.layer) { this.layer = +tags.layer; }
}
});
// Iterate through each subpart, drawing any styles on that layer
var drawn=false;
for (i=0; i<this.styleList.subparts.length; i++) {
var subpart=this.styleList.subparts[i];
}
},
getEnhancedTags:function() {
// summary: Return tags for this entity augmented by the EntityUI's state classes.
var tags = _.clone(this.entity.tags);
// Apply stateClasses (hover, selected, hoverway, selectedway)
for (var i in this.stateClasses) {
tags[':'+this.stateClasses[i]] = 'yes';
}
// todo - Add any common 'special-case' tags, e.g. :hasTags
return tags; // Object
},
// --------------------
// State class handling
setStateClasses:function(stateClasses) {
// summary: Set all state classes at once, and prompt a redraw if they're different to previously,
if (stateClasses && this.stateClasses.join(',')!=stateClasses.join(',')) {
this.stateClasses=stateClasses.slice();
this.invalidateStyleList();
}
return this;
},
setStateClass:function(sc) {
// summary: Set a single state class, and prompt a redraw if it wasn't set previously.
if (this.stateClasses.indexOf(sc)==-1) {
this.stateClasses.push(sc);
this.invalidateStyleList();
}
return this;
},
resetStateClass:function(sc) {
// summary: Reset a single state class, and prompt a redraw if it was set previously.
if (this.stateClasses.indexOf(sc)>-1) {
this.stateClasses.splice(this.stateClasses.indexOf(sc),1);
this.invalidateStyleList();
}
return this;
},
hasStateClass:function(sc) {
// summary: Is a particular state class set for this UI?
return this.stateClasses.indexOf(sc)>-1;
},
invalidateStyleList:function() {
// summary: Invalidate the StyleList so it's recalculated on next redraw.
this.styleList=null;
},
// --------------------
// Mouse event handling
entityMouseEvent:function(event) {
// summary: Receive a mouse event (e.g. clicking on the UI), and forward it to the Controller.
this.map.controller.entityMouseEvent(event, event.gfxTarget.source);
event.stopPropagation();
}
});
// ----------------------------------------------------------------------
// End of module
// ----------------------------------------------------------------------
// End of module
});
+440 -482
View File
@@ -2,342 +2,300 @@
// at present this combines P2's Map and MapPaint functionality
define(['dojo/_base/declare','dojo/_base/event',
'dojo/dom-geometry',
'dojox/gfx','dojox/gfx/matrix',
'iD/Connection','iD/Entity','iD/renderer/EntityUI','iD/renderer/WayUI','iD/renderer/NodeUI'],
function(declare, Event, domGeom, Gfx, Matrix){
'dojo/dom-geometry',
'dojox/gfx','dojox/gfx/matrix',
'iD/Connection','iD/Entity','iD/renderer/EntityUI','iD/renderer/WayUI','iD/renderer/NodeUI'],
function(declare, Event, domGeom, Gfx, Matrix){
// ----------------------------------------------------------------------
// Connection base class
// ----------------------------------------------------------------------
// Connection base class
declare("iD.renderer.Map", null, {
declare("iD.renderer.Map", null, {
MASTERSCALE: 5825.4222222222,
MINSCALE: 14,
MAXSCALE: 23,
zoom: NaN,
zoomfactor: NaN,
baselon: NaN, // original longitude at top left of viewport
baselat: NaN, // original latitude at top left of viewport
baselatp: NaN, // original projected latitude at top left of viewport
MASTERSCALE: 5825.4222222222,
MINSCALE: 14,
MAXSCALE: 23,
zoom: NaN,
zoomfactor: NaN,
baselon: NaN, // original longitude at top left of viewport
baselat: NaN, // original latitude at top left of viewport
baselatp: NaN, // original projected latitude at top left of viewport
div: '', // <div> of this map
surface: null, // <div>.surface containing the rendering
container: null, // root-level group within the surface
backdrop: null, // coloured backdrop (MapCSS canvas element)
connection: null, // data store
controller: null, // UI controller
nodeuis: {}, // graphic representations of data
wayuis: {}, // |
div: '', // <div> of this map
surface: null, // <div>.surface containing the rendering
container: null, // root-level group within the surface
backdrop: null, // coloured backdrop (MapCSS canvas element)
connection: null, // data store
controller: null, // UI controller
uis: {},
tilegroup: null, // group within container for adding bitmap tiles
tiles: {}, // index of tile objects
tilebaseURL: 'http://ecn.t0.tiles.virtualearth.net/tiles/a$quadkey.jpeg?g=587&mkt=en-gb&n=z', // Bing imagery URL
tilegroup: null, // group within container for adding bitmap tiles
tiles: {}, // index of tile objects
tilebaseURL: 'http://ecn.t0.tiles.virtualearth.net/tiles/a$quadkey.jpeg?g=587&mkt=en-gb&n=z', // Bing imagery URL
dragging: false, // current drag state
dragged: false, // was most recent click a drag?
dragx: NaN, // click co-ordinates at previously recorded drag event
dragy: NaN, // |
startdragx: NaN, // click co-ordinates at start of drag
startdragy: NaN, // |
dragtime: NaN, // timestamp of mouseup (compared to stop resulting click from firing)
dragconnect: null, // event listener for endDrag
dragging: false, // current drag state
dragged: false, // was most recent click a drag?
dragx: NaN, // click co-ordinates at previously recorded drag event
dragy: NaN, // |
startdragx: NaN, // click co-ordinates at start of drag
startdragy: NaN, // |
dragtime: NaN, // timestamp of mouseup (compared to stop resulting click from firing)
dragconnect: null, // event listener for endDrag
containerx: 0, // screen co-ordinates of container
containery: 0, // |
centrelat: NaN, // lat/long and bounding box of map
centrelon: NaN, // |
extent: {}, // |
mapheight: NaN, // size of map object in pixels
mapwidth: NaN, // |
containerx: 0, // screen co-ordinates of container
containery: 0, // |
centrelat: NaN, // lat/long and bounding box of map
centrelon: NaN, // |
extent: {}, // |
mapheight: NaN, // size of map object in pixels
mapwidth: NaN, // |
layers: null, // array-like object of Groups, one for each OSM layer
minlayer: -5, // minimum OSM layer supported
maxlayer: 5, // maximum OSM layer supported
layers: null, // array-like object of Groups, one for each OSM layer
minlayer: -5, // minimum OSM layer supported
maxlayer: 5, // maximum OSM layer supported
elastic: null, // Group for drawing elastic band
elastic: null, // Group for drawing elastic band
ruleset: null, // map style
ruleset: null, // map style
constructor: function(obj) {
// summary: The main map display, containing the individual sprites (UIs) for each entity.
// obj: Object An object containing .lat, .lon, .scale, .div (the name of the <div> to be used),
// .connection, .width (px) and .height (px) properties.
constructor: function(obj) {
// summary: The main map display, containing the individual sprites (UIs) for each entity.
// obj: Object An object containing .lat, .lon, .scale, .div (the name of the <div> to be used),
// .connection, .width (px) and .height (px) properties.
this.mapwidth = obj.width ? obj.width : 800;
this.mapheight = obj.height ? obj.height : 400;
this.mapwidth = obj.width ? obj.width : 800;
this.mapheight = obj.height ? obj.height : 400;
// Initialise variables
this.nodeuis = {},
this.wayuis = {},
this.div=document.getElementById(obj.div);
this.surface=Gfx.createSurface(obj.div, this.mapwidth, this.mapheight);
this.backdrop=this.surface.createRect({
x: 0,
y: 0,
width: this.mapwidth,
height: this.mapheight
}).setFill(new dojo.Color([255,255,245,1]));
this.tilegroup = this.surface.createGroup();
this.container = this.surface.createGroup();
this.connection = obj.connection;
this.zoom = obj.zoom ? obj.zoom : 17;
this.baselon = obj.lon;
this.baselat = obj.lat;
this.baselatp = this.lat2latp(obj.lat);
this._setScaleFactor();
this.updateCoordsFromViewportPosition();
// Initialise variables
this.uis = {};
this.div=document.getElementById(obj.div);
this.surface=Gfx.createSurface(obj.div, this.mapwidth, this.mapheight);
this.backdrop=this.surface.createRect({
x: 0,
y: 0,
width: this.mapwidth,
height: this.mapheight
}).setFill(new dojo.Color([255,255,245,1]));
this.tilegroup = this.surface.createGroup();
this.container = this.surface.createGroup();
this.connection = obj.connection;
this.zoom = obj.zoom ? obj.zoom : 17;
this.baselon = obj.lon;
this.baselat = obj.lat;
this.baselatp = this.lat2latp(obj.lat);
this._setScaleFactor();
this.updateCoordsFromViewportPosition();
// Cache the margin box, since this is expensive.
this.marginBox = domGeom.getMarginBox(this.div);
// Cache the margin box, since this is expensive.
this.marginBox = domGeom.getMarginBox(this.div);
// Initialise layers
this.layers={};
for (var l=this.minlayer; l<=this.maxlayer; l++) {
var r=this.container.createGroup();
this.layers[l]={
root: r,
fill: r.createGroup(),
casing: r.createGroup(),
stroke: r.createGroup(),
text: r.createGroup(),
hit: r.createGroup()
};
}
// Create group for elastic band
this.elastic = this.container.createGroup();
// Make draggable
this.backdrop.connect("onmousedown", _.bind(this.startDrag, this));
this.tilegroup.connect("onmousedown", _.bind(this.startDrag, this));
this.surface.connect("onclick", _.bind(this.clickSurface, this));
this.surface.connect("onmousemove", _.bind(this.processMove, this));
this.surface.connect("onmousedown", _.bind(this._mouseEvent, this));
this.surface.connect("onmouseup", _.bind(this._mouseEvent, this));
},
setController:function(controller) {
// summary: Set the controller that will handle events on the map (e.g. mouse clicks).
this.controller = controller;
},
_moveToPosition:function(group, position) {
// summary: Supplementary method for dojox.gfx.
// This should ideally be core Dojo stuff: see http://bugs.dojotoolkit.org/ticket/15296
var parent=group.getParent();
if (!parent) { return; }
this._moveChildToPosition(parent,group,position);
if (position === group.rawNode.parentNode.childNodes.length) {
group.rawNode.parentNode.appendChild(group.rawNode);
} else {
group.rawNode.parentNode.insertBefore(group.rawNode, group.rawNode.parentNode.childNodes[position]);
}
},
_moveChildToPosition: function(parent, child, position) {
for (var i = 0; i < parent.children.length; ++i){
if (parent.children[i] === child){
parent.children.splice(i, 1);
parent.children.splice(position, 0, child);
break;
}
}
},
// ----------------------------
// Sprite and EntityUI handling
sublayer:function(layer,groupType,sublayer) {
// summary: Find the gfx.Group for a given OSM layer and rendering sublayer, creating it
// if necessary. Note that sublayers are only implemented for stroke and fill.
// groupType: String 'casing','text','hit','stroke', or 'fill'
var collection = this.layers[layer][groupType], sub;
switch (groupType) {
case 'casing':
case 'text':
case 'hit':
return collection;
}
// Find correct sublayer, inserting if necessary
var insertAt=collection.children.length;
for (var i = 0; i < collection.children.length; i++) {
sub=collection.children[i];
if (sub.sublayer==sublayer) { return sub; }
else if (sub.sublayer>sublayer) {
sub = collection.createGroup();
this._moveToPosition(sub,i);
sub.sublayer=sublayer;
return sub;
}
}
sub=collection.createGroup().moveToFront();
sub.sublayer=sublayer;
return sub; // dojox.gfx.Group
},
createUI: function(entity,stateClasses) {
// summary: Create a UI (sprite) for an entity, assigning any specified state classes
// (temporary attributes such as ':hover' or ':selected')
var id = entity.id;
if (entity.entityType === 'node') {
if (!this.nodeuis[id]) {
this.nodeuis[id] = new iD.renderer.NodeUI(entity,this,stateClasses);
} else {
this.nodeuis[id].setStateClasses(stateClasses).redraw();
// Initialise layers
this.layers={};
for (var l=this.minlayer; l<=this.maxlayer; l++) {
var r=this.container.createGroup();
this.layers[l]={
root: r,
fill: r.createGroup(),
casing: r.createGroup(),
stroke: r.createGroup(),
text: r.createGroup(),
hit: r.createGroup()
};
}
return this.nodeuis[id]; // iD.renderer.EntityUI
} else if (entity.entityType === 'way') {
if (!this.wayuis[id]) {
this.wayuis[id] = new iD.renderer.WayUI(entity,this,stateClasses);
// Create group for elastic band
this.elastic = this.container.createGroup();
// Make draggable
this.backdrop.connect("onmousedown", _.bind(this.startDrag, this));
this.tilegroup.connect("onmousedown", _.bind(this.startDrag, this));
this.surface.connect("onclick", _.bind(this.clickSurface, this));
this.surface.connect("onmousemove", _.bind(this.processMove, this));
this.surface.connect("onmousedown", _.bind(this._mouseEvent, this));
this.surface.connect("onmouseup", _.bind(this._mouseEvent, this));
},
setController:function(controller) {
// summary: Set the controller that will handle events on the map (e.g. mouse clicks).
this.controller = controller;
},
_moveToPosition:function(group, position) {
// summary: Supplementary method for dojox.gfx.
// This should ideally be core Dojo stuff: see http://bugs.dojotoolkit.org/ticket/15296
var parent=group.getParent();
if (!parent) { return; }
this._moveChildToPosition(parent,group,position);
if (position === group.rawNode.parentNode.childNodes.length) {
group.rawNode.parentNode.appendChild(group.rawNode);
} else {
this.wayuis[id].setStateClasses(stateClasses).redraw();
group.rawNode.parentNode.insertBefore(group.rawNode, group.rawNode.parentNode.childNodes[position]);
}
return this.wayuis[id]; // iD.renderer.EntityUI
}
},
},
getUI: function(entity) {
// summary: Return the UI for an entity, if it exists.
if (entity.entityType === 'node') {
return this.nodeuis[entity.id]; // iD.renderer.EntityUI
} else if (entity.entityType === 'way') {
return this.wayuis[entity.id]; // iD.renderer.EntityUI
}
return null;
},
refreshUI: function(entity) {
// summary: Redraw the UI for an entity.
if (entity.entityType === 'node') {
if (this.nodeuis[entity.id]) { this.nodeuis[entity.id].redraw(); }
} else if (entity.entityType === 'way') {
if (this.wayuis[entity.id] ) { this.wayuis[entity.id].redraw(); }
}
},
deleteUI: function(entity) {
// summary: Delete the UI for an entity.
var uis = { node: 'nodeuis', way: 'wayuis' }[entity.entityType];
if (uis && this[uis][entity.id]) {
this[uis][entity.id].removeSprites();
delete this[uis][entity.id];
}
},
download: function() {
// summary: Ask the connection to download data for the current viewport.
$('#progress').show().addClass('spinner');
this.connection.loadFromAPI(this.extent, _.bind(this.updateUIs, this));
},
updateUIs: function(redraw, remove) {
// summary: Draw/refresh all EntityUIs within the bbox, and remove any others.
// redraw: Boolean Should we redraw any UIs that are already present?
// remove: Boolean Should we delete any UIs that are no longer in the bbox?
$('#progress').hide().removeClass('spinner');
var m = this;
var way, poi;
var o = this.connection.getObjectsByBbox(this.extent);
_(o.waysInside).chain()
.filter(function(w) { return w.loaded; })
.each(function(way) {
if (!m.wayuis[way.id]) { m.createUI(way); }
else if (redraw) { m.wayuis[way.id].recalculate(); m.wayuis[way.id].redraw(); }
});
if (remove !== false) {
_.each(o.waysOutside, function(way) {
if (m.wayuis[way.id]) { // && !m.wayuis[way.id].purgable
if (redraw) {
m.wayuis[way.id].recalculate();
m.wayuis[way.id].redraw();
}
} else {
m.deleteUI(way);
_moveChildToPosition: function(parent, child, position) {
for (var i = 0; i < parent.children.length; ++i){
if (parent.children[i] === child){
parent.children.splice(i, 1);
parent.children.splice(position, 0, child);
break;
}
});
}
}
},
_.each(o.poisInside, function(poi) {
if (!poi.loaded) return;
if (!m.nodeuis[poi.id]) { m.createUI(poi); }
else if (redraw) { m.nodeuis[poi.id].redraw(); }
});
// ----------------------------
// Sprite and EntityUI handling
if (remove !== false) {
_.each(o.poisOutside, function(poi) {
if (m.nodeuis[poi.id]) { // && !m.nodeuis[poi.id].purgable
if (redraw) { m.nodeuis[poi.id].redraw(); }
} else { m.deleteUI(poi); }
});
}
},
sublayer:function(layer,groupType,sublayer) {
// summary: Find the gfx.Group for a given OSM layer and rendering sublayer, creating it
// if necessary. Note that sublayers are only implemented for stroke and fill.
// groupType: String 'casing','text','hit','stroke', or 'fill'
var collection = this.layers[layer][groupType], sub;
switch (groupType) {
case 'casing':
case 'text':
case 'hit':
return collection;
}
// Find correct sublayer, inserting if necessary
var insertAt=collection.children.length;
for (var i = 0; i < collection.children.length; i++) {
sub=collection.children[i];
if (sub.sublayer==sublayer) { return sub; }
else if (sub.sublayer>sublayer) {
sub = collection.createGroup();
this._moveToPosition(sub,i);
sub.sublayer=sublayer;
return sub;
}
}
sub = collection.createGroup().moveToFront();
sub.sublayer=sublayer;
return sub; // dojox.gfx.Group
},
// -------------
// Zoom handling
createUI: function(e, stateClasses) {
// summary: Create a UI (sprite) for an entity, assigning any specified state classes
// (temporary attributes such as ':hover' or ':selected')
if (!this.uis[e.id]) {
if (e.entityType === 'node') {
this.uis[e.id] = new iD.renderer.NodeUI(e, this, stateClasses);
} else if (e.entityType === 'way') {
this.uis[e.id] = new iD.renderer.WayUI(e, this, stateClasses);
}
} else {
this.uis[e.id].setStateClasses(stateClasses).redraw();
}
},
zoomIn: function() {
// summary: Zoom in by one level (unless maximum reached).
return this.setZoom(this.zoom + 1);
},
getUI: function(e) {
// summary: Return the UI for an entity, if it exists.
return this.uis[e.id]; // iD.renderer.EntityUI
},
zoomOut: function() {
// summary: Zoom out by one level (unless minimum reached).
this.setZoom(this.zoom - 1);
this.download();
return this;
},
refreshUI: function(e) {
// summary: Redraw the UI for an entity.
if (this.uis[e.id]) { this.uis[e.id].redraw(); }
},
setZoom: function(zoom) {
if (zoom < this.MINSCALE || zoom > this.MAXSCALE) return this;
// summary: Redraw the map at a new zoom level.
this.zoom = zoom;
this._setScaleFactor();
this._blankTiles();
this.setCentre({
lat: this.centrelat,
lon: this.centrelon
});
this.updateUIs(true, true);
return this;
},
deleteUI: function(e) {
// summary: Delete the UI for an entity.
if (this.uis[e.id]) {
this.uis[e.id].removeSprites();
delete this.uis[e.id];
}
},
_setScaleFactor: function() {
// summary: Calculate the scaling factor for this zoom level.
this.zoomfactor = this.MASTERSCALE/Math.pow(2, 13 - this.zoom);
},
download: function() {
// summary: Ask the connection to download data for the current viewport.
$('#progress').show().addClass('spinner');
this.connection.loadFromAPI(this.extent, _.bind(this.updateUIs, this));
},
// ----------------------
// Elastic band redrawing
updateUIs: function() {
// summary: Draw/refresh all EntityUIs within the bbox, and remove any others.
// redraw: Boolean Should we redraw any UIs that are already present?
// remove: Boolean Should we delete any UIs that are no longer in the bbox?
$('#progress').hide().removeClass('spinner');
var o = this.connection.getObjectsByBbox(this.extent);
var touch = _(o.inside).chain()
.filter(function(w) { return w.loaded; })
.map(_.bind(function(e) {
if (!this.uis[e.id]) {
this.createUI(e);
} else {
this.uis[e.id].redraw();
}
return '' + e.id;
}, this)).value();
_.each(_.difference(_.keys(this.uis), touch), _.bind(function(k) {
console.log(k);
this.deleteUI(k);
}, this));
},
clearElastic: function() {
// summary: Remove the elastic band used to draw new ways.
this.elastic.clear();
},
// -------------
// Zoom handling
drawElastic: function(x1,y1,x2,y2) {
// summary: Draw the elastic band (for new ways) between two points.
this.elastic.clear();
// **** Next line is SVG-specific
this.elastic.rawNode.setAttribute("pointer-events","none");
this.elastic.createPolyline( [{ x:x1, y:y1 }, { x:x2, y:y2 }] ).setStroke( {
color: [0, 0, 0, 1],
style: 'Solid',
width: 1
});
},
zoomIn: function() {
// summary: Zoom in by one level (unless maximum reached).
return this.setZoom(this.zoom + 1);
},
// -------------
// Tile handling
// ** FIXME: see docs
loadTiles: function() {
// summary: Load all tiles for the current viewport. This is a bare-bones function
// at present: it needs configurable URLs (not just Bing), attribution/logo
// support, and to be 'nudgable' (i.e. adjust the offset).
var tl = this.locationCoord({
zoomOut: function() {
// summary: Zoom out by one level (unless minimum reached).
this.setZoom(this.zoom - 1);
this.download();
return this;
},
setZoom: function(zoom) {
if (zoom < this.MINSCALE || zoom > this.MAXSCALE) return this;
// summary: Redraw the map at a new zoom level.
this.zoom = zoom;
this._setScaleFactor();
this._blankTiles();
this.setCentre({
lat: this.centrelat,
lon: this.centrelon
});
this.updateUIs(true, true);
return this;
},
_setScaleFactor: function() {
// summary: Calculate the scaling factor for this zoom level.
this.zoomfactor = this.MASTERSCALE/Math.pow(2, 13 - this.zoom);
},
// ----------------------
// Elastic band redrawing
clearElastic: function() {
// summary: Remove the elastic band used to draw new ways.
this.elastic.clear();
},
drawElastic: function(x1,y1,x2,y2) {
// summary: Draw the elastic band (for new ways) between two points.
this.elastic.clear();
// **** Next line is SVG-specific
this.elastic.rawNode.setAttribute("pointer-events","none");
this.elastic.createPolyline( [{ x:x1, y:y1 }, { x:x2, y:y2 }] ).setStroke( {
color: [0, 0, 0, 1],
style: 'Solid',
width: 1
});
},
// -------------
// Tile handling
// ** FIXME: see docs
loadTiles: function() {
// summary: Load all tiles for the current viewport. This is a bare-bones function
// at present: it needs configurable URLs (not just Bing), attribution/logo
// support, and to be 'nudgable' (i.e. adjust the offset).
var tl = this.locationCoord({
lat: this.extent.north,
lon: this.extent.west
}, this.zoom),
@@ -349,208 +307,208 @@ declare("iD.renderer.Map", null, {
seen = [],
coord = { z: this.zoom };
for (coord.x = tl.x; coord.x <= br.x; coord.x++) {
for (coord.y = tl.y; coord.y <= br.y; coord.y++) {
if (!this._getTile(coord)) {
this._fetchTile(coord);
for (coord.x = tl.x; coord.x <= br.x; coord.x++) {
for (coord.y = tl.y; coord.y <= br.y; coord.y++) {
if (!this._getTile(coord)) {
this._fetchTile(coord);
}
seen.push(iD.Util.tileKey(coord));
}
seen.push(iD.Util.tileKey(coord));
}
}
_.each(_.without(tileKeys, seen), _.bind(function(key) {
delete this.tiles[key];
}, this));
},
_.each(_.without(tileKeys, seen), _.bind(function(key) {
delete this.tiles[key];
}, this));
},
_fetchTile: function(coord) {
// summary: Load a tile image at the given tile co-ordinates.
var t = this.tilegroup.createImage({
x: Math.floor(this.lon2coord(this.tile2lon(coord.x))),
y: Math.floor(this.lat2coord(this.tile2lat(coord.y))),
width: 256,
height: 256,
src: this._tileURL(coord)
});
this._assignTile(coord, t);
},
_fetchTile: function(coord) {
// summary: Load a tile image at the given tile co-ordinates.
var t = this.tilegroup.createImage({
x: Math.floor(this.lon2coord(this.tile2lon(coord.x))),
y: Math.floor(this.lat2coord(this.tile2lat(coord.y))),
width: 256,
height: 256,
src: this._tileURL(coord)
});
this._assignTile(coord, t);
},
_getTile: function(coord) {
// summary: See if this tile is already loaded.
return this.tiles[iD.Util.tileKey(coord)];
},
_getTile: function(coord) {
// summary: See if this tile is already loaded.
return this.tiles[iD.Util.tileKey(coord)];
},
_assignTile: function(coord, t) {
// summary: Store a reference to the tile so we know it's loaded.
this.tiles[iD.Util.tileKey(coord)] = t;
},
_assignTile: function(coord, t) {
// summary: Store a reference to the tile so we know it's loaded.
this.tiles[iD.Util.tileKey(coord)] = t;
},
_tileURL: function(coord) {
// summary: Calculate the URL for a tile at the given co-ordinates.
var u = '';
for (var zoom = coord.z; zoom > 0; zoom--) {
var byte = 0;
var mask = 1 << (zoom - 1);
if ((coord.x & mask) !== 0) byte++;
if ((coord.y & mask) !== 0) byte += 2;
u += byte.toString();
}
return this.tilebaseURL
.replace('$z', coord.z)
.replace('$x', coord.x)
.replace('$y', coord.y)
.replace('$quadkey', u);
},
_tileURL: function(coord) {
// summary: Calculate the URL for a tile at the given co-ordinates.
var u = '';
for (var zoom = coord.z; zoom > 0; zoom--) {
var byte = 0;
var mask = 1 << (zoom - 1);
if ((coord.x & mask) !== 0) byte++;
if ((coord.y & mask) !== 0) byte += 2;
u += byte.toString();
}
return this.tilebaseURL
.replace('$z', coord.z)
.replace('$x', coord.x)
.replace('$y', coord.y)
.replace('$quadkey', u);
},
_blankTiles: function() {
// summary: Unload all tiles and remove from the display.
this.tilegroup.clear();
this.tiles = {};
},
_blankTiles: function() {
// summary: Unload all tiles and remove from the display.
this.tilegroup.clear();
this.tiles = {};
},
// -------------------------------------------
// Co-ordinate management, dragging and redraw
// -------------------------------------------
// Co-ordinate management, dragging and redraw
startDrag: function(e) {
// summary: Start dragging the map in response to a mouse-down.
// e: MouseEvent The mouse-down event that triggered it.
var srcElement = (e.gfxTarget === this.backdrop) ?
e.gfxTarget : e.gfxTarget.parent;
Event.stop(e);
this.dragging = true;
this.dragged = false;
this.dragx = this.dragy=NaN;
this.startdragx = e.clientX;
this.startdragy = e.clientY;
this.dragconnect = srcElement.connect("onmouseup", _.bind(this.endDrag, this));
},
startDrag: function(e) {
// summary: Start dragging the map in response to a mouse-down.
// e: MouseEvent The mouse-down event that triggered it.
var srcElement = (e.gfxTarget === this.backdrop) ?
e.gfxTarget : e.gfxTarget.parent;
Event.stop(e);
this.dragging = true;
this.dragged = false;
this.dragx = this.dragy=NaN;
this.startdragx = e.clientX;
this.startdragy = e.clientY;
this.dragconnect = srcElement.connect("onmouseup", _.bind(this.endDrag, this));
},
endDrag: function(e) {
// summary: Stop dragging the map in response to a mouse-up.
// e: MouseEvent The mouse-up event that triggered it.
Event.stop(e);
dojo.disconnect(this.dragconnect);
this.dragging=false;
this.dragtime=e.timeStamp;
this.updateCoordsFromViewportPosition();
if (Math.abs(e.clientX - this.startdragx) < 3 &&
Math.abs(e.clientY - this.startdragy) < 3) {
return;
}
this.download();
},
endDrag: function(e) {
// summary: Stop dragging the map in response to a mouse-up.
// e: MouseEvent The mouse-up event that triggered it.
Event.stop(e);
dojo.disconnect(this.dragconnect);
this.dragging=false;
this.dragtime=e.timeStamp;
this.updateCoordsFromViewportPosition();
if (Math.abs(e.clientX - this.startdragx) < 3 &&
Math.abs(e.clientY - this.startdragy) < 3) {
return;
}
this.download();
},
processMove: function(e) {
// summary: Drag the map to a new origin.
// e: MouseEvent The mouse-move event that triggered it.
var x = e.clientX;
var y = e.clientY;
if (this.dragging) {
if (this.dragx) {
this.containerx += (x - this.dragx);
this.containery += (y - this.dragy);
this.updateOrigin();
this.dragged=true;
}
this.dragx = x;
this.dragy = y;
} else {
this.controller.entityMouseEvent(e,null);
}
},
processMove: function(e) {
// summary: Drag the map to a new origin.
// e: MouseEvent The mouse-move event that triggered it.
var x = e.clientX;
var y = e.clientY;
if (this.dragging) {
if (this.dragx) {
this.containerx += (x - this.dragx);
this.containery += (y - this.dragy);
this.updateOrigin();
this.dragged=true;
}
this.dragx = x;
this.dragy = y;
} else {
this.controller.entityMouseEvent(e,null);
}
},
updateOrigin: function() {
// summary: Tell Dojo to update the viewport origin.
this.container.setTransform([Matrix.translate(this.containerx, this.containery)]);
this.tilegroup.setTransform([Matrix.translate(this.containerx, this.containery)]);
},
updateOrigin: function() {
// summary: Tell Dojo to update the viewport origin.
this.container.setTransform([Matrix.translate(this.containerx, this.containery)]);
this.tilegroup.setTransform([Matrix.translate(this.containerx, this.containery)]);
},
_mouseEvent: function(e) {
// summary: Catch mouse events on the surface but not the tiles - in other words,
// on drawn items that don't have their own hitzones, like the fill of a shape.
if (e.type=='mousedown') { this.startDrag(e); }
// ** FIXME: we may want to reinstate this at some point...
// this.controller.entityMouseEvent(e,null);
},
_mouseEvent: function(e) {
// summary: Catch mouse events on the surface but not the tiles - in other words,
// on drawn items that don't have their own hitzones, like the fill of a shape.
if (e.type=='mousedown') { this.startDrag(e); }
// ** FIXME: we may want to reinstate this at some point...
// this.controller.entityMouseEvent(e,null);
},
updateCoordsFromViewportPosition: function(e) {
// summary: Update centre and bbox from the current viewport origin.
this._updateCoords(this.containerx, this.containery);
},
updateCoordsFromViewportPosition: function(e) {
// summary: Update centre and bbox from the current viewport origin.
this._updateCoords(this.containerx, this.containery);
},
setCentre: function(loc) {
// summary: Update centre and bbox to a specified lat/lon.
var coord = this.locationCoord(loc, this.zoom);
this._updateCoords(
-coord.x - this.mapwidth / 2,
-coord.y - this.mapheight / 2);
return this;
},
setCentre: function(loc) {
// summary: Update centre and bbox to a specified lat/lon.
var coord = this.locationCoord(loc, this.zoom);
this._updateCoords(
-coord.x - this.mapwidth / 2,
-coord.y - this.mapheight / 2);
return this;
},
setCenter: function(loc) { this.setCentre(loc); },
setCenter: function(loc) { this.setCentre(loc); },
_updateCoords:function(x, y) {
// summary: Set centre and bbox.
this.containerx = x;
this.containery = y;
this.updateOrigin();
this.centrelon = this.coord2lon(-x + this.mapwidth/2);
this.centrelat = this.coord2lat(-y + this.mapheight/2);
_updateCoords:function(x, y) {
// summary: Set centre and bbox.
this.containerx = x;
this.containery = y;
this.updateOrigin();
this.centrelon = this.coord2lon(-x + this.mapwidth/2);
this.centrelat = this.coord2lat(-y + this.mapheight/2);
this.extent = {
north: this.coord2lat(-y),
south: this.coord2lat(-y + this.mapheight),
west: this.coord2lon(-x),
east: this.coord2lon(-x + this.mapwidth)
};
this.extent = {
north: this.coord2lat(-y),
south: this.coord2lat(-y + this.mapheight),
west: this.coord2lon(-x),
east: this.coord2lon(-x + this.mapwidth)
};
this.loadTiles();
},
this.loadTiles();
},
clickSurface:function(e) {
// summary: Handle a click on an empty area of the map.
if (this.dragged && e.timeStamp==this.dragtime) { return; }
this.controller.entityMouseEvent(e,null);
},
clickSurface:function(e) {
// summary: Handle a click on an empty area of the map.
if (this.dragged && e.timeStamp==this.dragtime) { return; }
this.controller.entityMouseEvent(e,null);
},
// -----------------------
// Co-ordinate conversions
// -----------------------
// Co-ordinate conversions
latp2coord:function(a) { return -(a-this.baselatp)*this.zoomfactor; },
coord2latp:function(a) { return a/-this.zoomfactor+this.baselatp; },
lon2coord:function(a) { return (a-this.baselon)*this.zoomfactor; },
coord2lon:function(a) { return a/this.zoomfactor+this.baselon; },
lon2screen:function(a) { return this.lon2coord(a) + this.marginBox.l + this.containerx; },
latp2coord:function(a) { return -(a-this.baselatp)*this.zoomfactor; },
coord2latp:function(a) { return a/-this.zoomfactor+this.baselatp; },
lon2coord:function(a) { return (a-this.baselon)*this.zoomfactor; },
coord2lon:function(a) { return a/this.zoomfactor+this.baselon; },
lon2screen:function(a) { return this.lon2coord(a) + this.marginBox.l + this.containerx; },
lat2latp:function(a) { return 180/Math.PI * Math.log(Math.tan(Math.PI/4+a*(Math.PI/180)/2)); },
latp2lat:function(a) { return 180/Math.PI * (2 * Math.atan(Math.exp(a*Math.PI/180)) - Math.PI/2); },
lat2coord:function(a) { return -(this.lat2latp(a)-this.baselatp)*this.zoomfactor; },
coord2lat:function(a) { return this.latp2lat(a/-this.zoomfactor+this.baselatp); },
lat2screen:function(a) { return this.lat2coord(a) + this.marginBox.t + this.containery; },
lat2latp:function(a) { return 180/Math.PI * Math.log(Math.tan(Math.PI/4+a*(Math.PI/180)/2)); },
latp2lat:function(a) { return 180/Math.PI * (2 * Math.atan(Math.exp(a*Math.PI/180)) - Math.PI/2); },
lat2coord:function(a) { return -(this.lat2latp(a)-this.baselatp)*this.zoomfactor; },
coord2lat:function(a) { return this.latp2lat(a/-this.zoomfactor+this.baselatp); },
lat2screen:function(a) { return this.lat2coord(a) + this.marginBox.t + this.containery; },
locationCoord: function(ll, z) {
var z2 = Math.pow(2, z), d2r = Math.PI / 180;
return {
z: z,
x: Math.floor((ll.lon + 180) / 360 * z2),
y: Math.floor((1 - Math.log(Math.tan(ll.lat * d2r) +
1 / Math.cos(ll.lat * d2r)) / Math.PI) / 2 * z2)
};
},
lon2tile:function(a) { return (Math.floor((a+180)/360*Math.pow(2,this.zoom))); },
lat2tile:function(a) { return (Math.floor((1-Math.log(Math.tan(a*Math.PI/180) + 1/Math.cos(a*Math.PI/180))/Math.PI)/2 *Math.pow(2,this.zoom))); },
tile2lon:function(a) { return (a/Math.pow(2,this.zoom)*360-180); },
tile2lat:function(a) {
var n=Math.PI-2*Math.PI*a/Math.pow(2,this.zoom);
return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
},
locationCoord: function(ll, z) {
var z2 = Math.pow(2, z), d2r = Math.PI / 180;
return {
z: z,
x: Math.floor((ll.lon + 180) / 360 * z2),
y: Math.floor((1 - Math.log(Math.tan(ll.lat * d2r) +
1 / Math.cos(ll.lat * d2r)) / Math.PI) / 2 * z2)
};
},
lon2tile:function(a) { return (Math.floor((a+180)/360*Math.pow(2,this.zoom))); },
lat2tile:function(a) { return (Math.floor((1-Math.log(Math.tan(a*Math.PI/180) + 1/Math.cos(a*Math.PI/180))/Math.PI)/2 *Math.pow(2,this.zoom))); },
tile2lon:function(a) { return (a/Math.pow(2,this.zoom)*360-180); },
tile2lat:function(a) {
var n=Math.PI-2*Math.PI*a/Math.pow(2,this.zoom);
return (180/Math.PI*Math.atan(0.5*(Math.exp(n)-Math.exp(-n))));
},
// Turn event co-ordinates into map co-ordinates
// Turn event co-ordinates into map co-ordinates
mouseX: function(e) { return e.clientX - this.marginBox.l - this.containerx; },
mouseY: function(e) { return e.clientY - this.marginBox.t - this.containery; }
});
// ----------------------------------------------------------------------
// End of module
mouseX: function(e) { return e.clientX - this.marginBox.l - this.containerx; },
mouseY: function(e) { return e.clientY - this.marginBox.t - this.containery; }
});
// ----------------------------------------------------------------------
// End of module
});
+2 -2
View File
@@ -1,8 +1,8 @@
// iD/renderer/NodeUI.js
// NodeUI classes for iD
define(['dojo/_base/declare','dojo/_base/array','dojox/gfx/_base','iD/renderer/EntityUI'],
function(declare,array,g){
define(['dojo/_base/declare','dojox/gfx/_base','iD/renderer/EntityUI'],
function(declare, g) {
// ----------------------------------------------------------------------
// NodeUI class
+2 -2
View File
@@ -18,7 +18,7 @@ declare("iD.renderer.WayUI", [iD.renderer.EntityUI], {
this.redraw();
},
getEnhancedTags: function() {
var tags=this.inherited(arguments);
var tags = this.inherited(arguments);
if (this.entity.isClosed()) { tags[':area']='yes'; }
return tags;
},
@@ -29,7 +29,7 @@ declare("iD.renderer.WayUI", [iD.renderer.EntityUI], {
redraw: function() {
// summary: Draw the object and add hitzone sprites.
var way = this.entity,
maxwidth=4,
maxwidth = 4,
i;
this.removeSprites();