updateEntityUIs(false, false);
download();
}
+
+ public function moveMapFromLatLon(lat:Number,lon:Number):void {
+ updateCoordsFromLatLon(lat,lon);
+ updateEntityUIs(false,false);
+ download();
+ }
// Co-ordinate conversion functions
public static const MOVE:String = "move";
public static const SCALE:String = "scale";
public static const NUDGE_BACKGROUND:String = "nudge_background";
- public static const ERROR:String = "error";
public static const INITIALISED:String = "initialized";
public static const BUMP:String = "bump";
+ public static const ERROR:String = "error"; // ** FIXME - this should be a dedicated ErrorEvent class
+ public static const ATTENTION:String = "attention"; // ** FIXME - this should be a dedicated AttentionEvent class
public var params:Object;
import flash.events.Event;
import net.systemeD.halcyon.Globals;
import net.systemeD.halcyon.connection.actions.*;
+ import net.systemeD.halcyon.MapEvent;
public class Connection extends EventDispatcher {
return relations[id];
}
+ protected function findEntity(type:String, id:*):Entity {
+ var i:Number=Number(id);
+ switch (type.toLowerCase()) {
+ case 'node': return getNode(id);
+ case 'way': return getWay(id);
+ case 'relation': return getRelation(id);
+ default: return null;
+ }
+ }
+
// Remove data from Connection
// These functions are used only internally to stop redundant data hanging around
// (either because it's been deleted on the server, or because we have panned away
killWay(id);
}
-
+ protected function killEntity(entity:Entity):void {
+ if (entity is Way) { killWay(entity.id); }
+ else if (entity is Node) { killNode(entity.id); }
+ else if (entity is Relation) { killRelation(entity.id); }
+ }
public function createNode(tags:Object, lat:Number, lon:Number, performCreate:Function):Node {
var node:Node = new Node(nextNegative, 0, tags, true, lat, lon);
return [];
}
+ // Error-handling
+
+ protected function throwConflictError(entity:Entity,serverVersion:uint,message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "An item you edited has been changed by another mapper. Download their version and try again? (The server said: "+message+")",
+ yes: function():void { revertBeforeUpload(entity) },
+ no: cancelUpload }));
+ // ** FIXME: this should also offer the choice of 'overwrite?'
+ }
+ protected function throwAlreadyDeletedError(entity:Entity,message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "You tried to delete something that's already been deleted. Forget it and try again? (The server said: "+message+")",
+ yes: function():void { deleteBeforeUpload(entity) },
+ no: cancelUpload }));
+ }
+ protected function throwInUseError(entity:Entity,message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "You tried to delete something that's since been used elsewhere. Restore it and try again? (The server said: "+message+")",
+ yes: function():void { revertBeforeUpload(entity) },
+ no: cancelUpload }));
+ }
+ protected function throwEntityError(entity:Entity,message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "There is a problem with your changes which needs to be fixed before you can save: "+message+". Click 'OK' to see the offending item.",
+ ok: function():void { goToEntity(entity) } }));
+ }
+ protected function throwChangesetError(message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "The changeset in which you're saving changes is no longer valid. Start a new one and retry? (The server said: "+message+")",
+ yes: retryUploadWithNewChangeset,
+ no: cancelUpload }));
+ }
+ protected function throwBugError(message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "An unexpected error occurred, probably due to a bug in Potlatch 2. Do you want to retry? (The server said: "+message+")",
+ yes: retryUpload,
+ no: cancelUpload }));
+ }
+ protected function throwServerError(message:String):void {
+ dispatchEvent(new MapEvent(MapEvent.ERROR, {
+ message: "A server error occurred. Do you want to retry? (The server said: "+message+")",
+ yes: retryUpload,
+ no: cancelUpload }));
+ }
+
+ public function retryUpload():void { uploadChanges(); }
+ public function cancelUpload():void { return; }
+ public function retryUploadWithNewChangeset():void {
+ // ** FIXME: we need to move the create-changeset-then-upload logic out of SaveDialog
+ }
+ public function goToEntity(entity:Entity):void {
+ dispatchEvent(new MapEvent(MapEvent.ATTENTION, { entity: entity }));
+ }
+ public function revertBeforeUpload(entity:Entity):void {
+ // ** FIXME: implement a 'revert entity' method, then retry upload on successful download
+ }
+ public function deleteBeforeUpload(entity:Entity):void {
+ var a:CompositeUndoableAction = new CompositeUndoableAction("Delete refs");
+ entity.remove(a.push);
+ a.doAction();
+ killEntity(entity);
+ uploadChanges();
+ }
+
// these are functions that the Connection implementation is expected to
// provide. This class has some generic helpers for the implementation.
public function loadBbox(left:Number, right:Number,
urlReq.method = "POST";
urlReq.data = upload.toXMLString();
urlReq.contentType = "text/xml";
+ // ** FIXME: change this to whatever header we decide upon
+ urlReq.requestHeaders = [new URLRequestHeader("X-Cloak-Errors-As-200","true")];
var loader:URLLoader = new URLLoader();
loader.dataFormat = URLLoaderDataFormat.BINARY;
loader.addEventListener(Event.COMPLETE, diffUploadComplete);
- loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void { trace(urlReq.data); diffUploadError(event); } );
+ loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void { trace(urlReq.data); diffUploadIOError(event); } );
loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
loader.load(urlReq);
}
private function diffUploadComplete(event:Event):void {
+ // check if we've received a cloaked error
+ // ** FIXME: change this if we start returning errors as XML
+ var response:String=URLLoader(event.target).data;
+ var matches:Array=response.match(/^ERROR: (.+?): (.+)/);
+ if (matches) {
+ dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
+ diffUploadAPIError(matches[1],matches[2]);
+ return;
+ }
+
// response should be XML describing the progress
- var results:XML = new XML((URLLoader(event.target).data));
+ var results:XML = new XML(response);
for each( var update:XML in results.child("*") ) {
var oldID:Number = Number(update.@old_id);
MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
}
- private function diffUploadError(event:IOErrorEvent):void {
+ private function diffUploadIOError(event:IOErrorEvent):void {
dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+httpStatus+" "+event.text } ));
dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
}
+ private function diffUploadAPIError(status:String, message:String):void {
+ var matches:Array;
+ switch (status) {
+
+ case 'conflict':
+ if (message.match(/changeset/i)) { throwChangesetError(message); return; }
+ matches=message.match(/mismatch.+had (\d+) of (\w+) (\d+)/i);
+ if (matches) { throwConflictError(findEntity(matches[3],matches[2]), Number(matches[1]), message); return; }
+ break;
+
+ case 'gone':
+ matches=message.match(/The (\w+) with the id (\d+)/i);
+ if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
+ break;
+
+ case 'precondition_failed':
+ matches=message.match(/Node (\d+) is still used/i);
+ if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
+ matches=message.match(/relation (\d+) is used/i);
+ if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
+ matches=message.match(/Way (\d+) still used/i);
+ if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
+ matches=message.match(/Cannot update (\w+) (\d+)/i);
+ if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
+ matches=message.match(/Relation with id (\d+)/i);
+ if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
+ matches=message.match(/Way (\d+) requires the nodes/i);
+ if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
+ throwBugError(message); return;
+
+ case 'not_found':
+ throwBugError(message); return;
+
+ case 'bad_request':
+ matches=message.match(/Element (\w+)\/(\d+)/i);
+ if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
+ matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
+ if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
+ throwBugError(message); return;
+ }
+
+ // Not caught, so just throw a generic server error
+ throwServerError(message);
+ }
+
private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
var create:XML = <create version="0.6"/>
for each( var id:Number in getIDs() ) {
else
fail("Failure when uploading data");
- saveButton.parent.removeChild(saveButton);
+ if (saveButton && saveButton.parent) saveButton.parent.removeChild(saveButton);
cancelButton.label = "Close";
}
import mx.containers.Canvas;
import mx.core.Application;
import mx.events.DragEvent;
+ import mx.events.CloseEvent;
import mx.managers.DragManager;
import mx.core.DragSource;
import mx.controls.Alert;
conn.addEventListener(Connection.DATA_DIRTY, onDataDirty);
conn.addEventListener(Connection.DATA_CLEAN, onDataClean);
conn.addEventListener(MapEvent.ERROR, onMapError);
+ conn.addEventListener(MapEvent.ATTENTION, onAttention);
// set the access token from saved cookie
var tokenObject:SharedObject = SharedObject.getLocal("access_token");
Globals.vars.highlightTiger = obj.data['tiger_highlighted'];
}
- public function onMapError(event:MapEvent):void {
- Alert.show(event.params.message, 'Error', mx.controls.Alert.OK);
+ public function onMapError(mapEvent:MapEvent):void {
+ var buttons:uint=0;
+ if (mapEvent.params.no) { trace("no is set"); }
+ if (mapEvent.params.yes ) buttons|=mx.controls.Alert.YES;
+ if (mapEvent.params.no ) buttons|=mx.controls.Alert.NO;
+ if (mapEvent.params.cancel ) buttons|=mx.controls.Alert.CANCEL;
+ if (mapEvent.params.ok || buttons==0) buttons|=mx.controls.Alert.OK;
+ trace("showing alert with "+buttons);
+ Alert.show(mapEvent.params.message, 'Error', buttons, null, function(closeEvent:CloseEvent):void {
+ switch (closeEvent.detail) {
+ case mx.controls.Alert.CANCEL: mapEvent.params.cancel(); break;
+ case mx.controls.Alert.YES: mapEvent.params.yes(); break;
+ case mx.controls.Alert.NO: mapEvent.params.no(); break;
+ default: if (mapEvent.params.ok) mapEvent.params.ok();
+ }
+ });
+ }
+
+ /* Highlight an entity in response to an 'attention' event */
+
+ public function onAttention(mapEvent:MapEvent):void {
+ var entity:Entity=mapEvent.params.entity;
+ if (entity is Relation) {
+ // If it's a relation, just bring up the editor panel
+ var panel:RelationEditorPanel = RelationEditorPanel(
+ PopUpManager.createPopUp(Application(Application.application), RelationEditorPanel, true));
+ panel.setRelation(entity as Relation);
+ PopUpManager.centerPopUp(panel);
+ return;
+ }
+
+ var lat:Number, lon:Number;
+ var panTo:Boolean=true;
+ if (entity is Way) {
+ // If it's a way, find if it's on-screen
+ for (var i:uint=0; i<Way(entity).length; i++) {
+ var node:Node=Way(entity).getNode(i)
+ if (node.within(theMap.edge_l,theMap.edge_r,theMap.edge_t,theMap.edge_b)) { panTo=false; }
+ lat=node.lat; lon=node.lon;
+ }
+ } else if (entity is Node) {
+ // If it's a node, check if it's on-screen
+ if (entity.within(theMap.edge_l,theMap.edge_r,theMap.edge_t,theMap.edge_b)) { panTo=false; }
+ lat=Node(entity).lat; lon=Node(entity).lon;
+ }
+ // Pan if required, and select the object
+ if (panTo) { theMap.moveMapFromLatLon(lat,lon); }
+ theController.setState(theController.findStateForSelection([entity]));
}
public function onResizeMap():void {