error/conflict-handling work-in-progress
authorRichard Fairhurst <richard@systemed.net>
Sun, 12 Dec 2010 23:20:25 +0000 (23:20 +0000)
committerRichard Fairhurst <richard@systemed.net>
Sun, 12 Dec 2010 23:20:25 +0000 (23:20 +0000)
net/systemeD/halcyon/Map.as
net/systemeD/halcyon/MapEvent.as
net/systemeD/halcyon/connection/Connection.as
net/systemeD/halcyon/connection/XMLConnection.as
net/systemeD/potlatch2/save/SaveDialog.mxml
potlatch2.mxml

index 1a6015e..329f45c 100644 (file)
@@ -212,6 +212,12 @@ package net.systemeD.halcyon {
                        updateEntityUIs(false, false);
                        download();
                }
+               
+               public function moveMapFromLatLon(lat:Number,lon:Number):void {
+                       updateCoordsFromLatLon(lat,lon);
+                       updateEntityUIs(false,false);
+                       download();
+               }
 
                // Co-ordinate conversion functions
 
index 75a09b9..1248ad6 100644 (file)
@@ -9,9 +9,10 @@ package net.systemeD.halcyon {
                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;
 
index e8663e7..ac06e07 100644 (file)
@@ -6,6 +6,7 @@ package net.systemeD.halcyon.connection {
     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 {
 
@@ -195,6 +196,16 @@ package net.systemeD.halcyon.connection {
             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
@@ -249,7 +260,11 @@ package net.systemeD.halcyon.connection {
                        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);
@@ -424,6 +439,70 @@ package net.systemeD.halcyon.connection {
             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,
index 1d1415a..be19abd 100644 (file)
@@ -197,10 +197,12 @@ package net.systemeD.halcyon.connection {
             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);
                
@@ -208,8 +210,18 @@ package net.systemeD.halcyon.connection {
         }
 
         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);
@@ -237,11 +249,56 @@ package net.systemeD.halcyon.connection {
             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() ) {
index 03344bf..2dd71e2 100644 (file)
         else
             fail("Failure when uploading data");
 
-        saveButton.parent.removeChild(saveButton);
+        if (saveButton && saveButton.parent) saveButton.parent.removeChild(saveButton);
         cancelButton.label = "Close";
     }
     
index ccca02b..27ede9d 100644 (file)
@@ -91,6 +91,7 @@
         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 {