OAuth support and the basics of diff uploading.
authorDave Stubbs <osm@randomjunk.co.uk>
Sun, 16 Aug 2009 14:28:41 +0000 (14:28 +0000)
committerDave Stubbs <osm@randomjunk.co.uk>
Sun, 16 Aug 2009 14:28:41 +0000 (14:28 +0000)
Issues:
 * doesn't handle deletes at all yet
 * we can't do PUT to start the changeset... I had to modify my rails
 * we can't inspect bodies of conflicts (flash doesn't let us access except on 2xx response)

12 files changed:
embedded/save.svg [new file with mode: 0644]
halcyon.mxml
net/systemeD/halcyon/Map.as
net/systemeD/halcyon/connection/Changeset.as [new file with mode: 0644]
net/systemeD/halcyon/connection/Connection.as
net/systemeD/halcyon/connection/Entity.as
net/systemeD/halcyon/connection/SaveCompleteEvent.as [new file with mode: 0644]
net/systemeD/halcyon/connection/XMLConnection.as
net/systemeD/potlatch2/save/OAuthPanel.mxml [new file with mode: 0644]
net/systemeD/potlatch2/save/SaveDialog.mxml [new file with mode: 0644]
net/systemeD/potlatch2/save/SaveManager.as [new file with mode: 0644]
styles/Application.css [new file with mode: 0644]

diff --git a/embedded/save.svg b/embedded/save.svg
new file mode 100644 (file)
index 0000000..67415d5
--- /dev/null
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="24"
+   height="24"
+   id="svg2383"
+   sodipodi:version="0.32"
+   inkscape:version="0.46"
+   sodipodi:docname="save.svg"
+   inkscape:output_extension="org.inkscape.output.svg.inkscape"
+   version="1.0">
+  <defs
+     id="defs2385">
+    <inkscape:perspective
+       sodipodi:type="inkscape:persp3d"
+       inkscape:vp_x="0 : 16 : 1"
+       inkscape:vp_y="0 : 1000 : 0"
+       inkscape:vp_z="32 : 16 : 1"
+       inkscape:persp3d-origin="16 : 10.666667 : 1"
+       id="perspective2391" />
+  </defs>
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="11.197802"
+     inkscape:cx="17.258093"
+     inkscape:cy="3.7405397"
+     inkscape:current-layer="layer1"
+     showgrid="true"
+     inkscape:grid-bbox="true"
+     inkscape:document-units="px"
+     inkscape:window-width="875"
+     inkscape:window-height="723"
+     inkscape:window-x="123"
+     inkscape:window-y="47" />
+  <metadata
+     id="metadata2388">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="layer1"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer">
+    <g
+       id="g2384"
+       transform="scale(0.75,0.75)">
+      <path
+         id="rect3163"
+         d="M 1.1875,0 C 0.52269997,0 -2.6888214e-17,0.52269997 0,1.1875 L 0,30.8125 C 0,31.4773 0.52269997,32 1.1875,32 L 30.8125,32 C 31.4773,32 32,31.477301 32,30.8125 L 32,28.0625 L 30.375,28.0625 L 30.375,26.09375 L 32,26.09375 L 32,1.1875 C 32,0.52269997 31.477301,-4.3657207e-17 30.8125,0 L 1.1875,0 z M 16,10.71875 C 18.908428,10.71875 21.28125,13.091572 21.28125,16 C 21.28125,18.908428 18.908428,21.28125 16,21.28125 C 13.091572,21.281251 10.71875,18.908428 10.71875,16 C 10.71875,13.091572 13.091572,10.71875 16,10.71875 z"
+         style="fill:#292929;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         transform="matrix(0.9076923,0,0,0.9076923,17.134838,0.9772032)"
+         d="M 4.5544651,16.550539 A 5.8047104,5.8047104 0 1 1 -7.0549557,16.550539 A 5.8047104,5.8047104 0 1 1 4.5544651,16.550539 z"
+         sodipodi:ry="5.8047104"
+         sodipodi:rx="5.8047104"
+         sodipodi:cy="16.550539"
+         sodipodi:cx="-1.2502453"
+         id="path3170"
+         style="fill:none;fill-opacity:1;stroke:#816868;stroke-width:1.89999998;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <rect
+         rx="0.89999998"
+         y="1.9048082"
+         x="15.374877"
+         height="6.3405299"
+         width="1.2502453"
+         id="rect3175"
+         style="fill:none;fill-opacity:1;stroke:#816868;stroke-width:1.89999998;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+      <path
+         transform="matrix(2.6000001,0,0,2.6000001,-10.573504,-16.336016)"
+         d="M 7.054956,10.433268 A 0.22325809,0.22325809 0 1 1 6.6084398,10.433268 A 0.22325809,0.22325809 0 1 1 7.054956,10.433268 z"
+         sodipodi:ry="0.22325809"
+         sodipodi:rx="0.22325809"
+         sodipodi:cy="10.433268"
+         sodipodi:cx="6.8316979"
+         id="path3177"
+         style="fill:#523d3d;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+         sodipodi:type="arc" />
+      <rect
+         rx="0.79342103"
+         y="24.230619"
+         x="3.750736"
+         height="5.9833174"
+         width="13.068825"
+         id="rect3179"
+         style="fill:#eaeded;fill-opacity:1;stroke:none;stroke-width:1.89999998;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+    </g>
+  </g>
+</svg>
index 8e9d2e2..8c78fe2 100755 (executable)
@@ -6,6 +6,8 @@
        layout="vertical"
        horizontalAlign="center" 
        addedToStage="initApp()">
+       
+       <mx:Style source="styles/Application.css"/>
 
     <mx:Glow id="glowImage" duration="100" 
         alphaFrom="0.3" alphaTo="1.0" 
     <mx:WipeLeft id="wipeOut" duration="250"/>
     <mx:WipeRight id="wipeIn" duration="250"/>
 
+    <mx:ApplicationControlBar dock="true">
+        <mx:Spacer width="100%"/>
+        <mx:Button label="Save" icon="@Embed('embedded/save.svg')" click="SaveManager.saveChanges();"/>
+    </mx:ApplicationControlBar>
+    
     <mx:HDividedBox width="100%" height="100%">
 
       <mx:VBox height="100%" width="25%" horizontalAlign="right">
@@ -42,6 +49,7 @@
                import net.systemeD.halcyon.*;
                import net.systemeD.halcyon.connection.*;
                import net.systemeD.potlatch2.*;
+               import net.systemeD.potlatch2.save.SaveManager;
                import flash.system.Security;
                import flash.net.*;
                import flash.events.MouseEvent;
index 239da0c..7a5692c 100755 (executable)
@@ -97,10 +97,10 @@ package net.systemeD.halcyon {
                        s=getPaintSprite(); addChild(s);                        // 12 - shields and POI names
 
                        this.initparams=initparams;
-                       connection = Connection.getConnection(initparams['api'],initparams['policy'],initparams['connection']);
+                       connection = Connection.getConnection(initparams);
             connection.addEventListener(Connection.NEW_WAY, newWayCreated);
             connection.addEventListener(Connection.NEW_POI, newPOICreated);
-                       connection.getEnvironment(new Responder(gotEnvironment,connectionError));
+                       gotEnvironment(null);
         }
 
         private function getPaintSprite():Sprite {
diff --git a/net/systemeD/halcyon/connection/Changeset.as b/net/systemeD/halcyon/connection/Changeset.as
new file mode 100644 (file)
index 0000000..29fc4b9
--- /dev/null
@@ -0,0 +1,24 @@
+package net.systemeD.halcyon.connection {
+
+    public class Changeset extends Entity {
+        private var nodes:Array;
+               public static var entity_type:String = 'changeset';
+
+        public function Changeset(id:Number, tags:Object) {
+            super(id, 0, tags);
+        }
+
+        public override function toString():String {
+            return "Changeset("+id+"): "+getTagList();
+        }
+
+               public function isArea():Boolean {
+                       return (nodes[0].id==nodes[nodes.length-1].id  && nodes.length>2);
+               }
+
+               public override function getType():String {
+                       return 'changeset';
+               }
+    }
+
+}
index 6fad3e0..4675830 100755 (executable)
@@ -7,23 +7,21 @@ package net.systemeD.halcyon.connection {
 
        public class Connection extends EventDispatcher {
 
-        private static var CONNECTION_TYPE:String = "XML";
         private static var connectionInstance:Connection = null;
 
-        protected static var policyURL:String = "http://127.0.0.1:3000/api/crossdomain.xml";
-        protected static var apiBaseURL:String = "http://127.0.0.1:3000/api/0.6/";
-
-        public static function getConnection(api:String,policy:String,conn:String):Connection {
-                       
-                       if ( policy != null )
-                           policyURL=policy;
-                       if ( api != null )
-                           apiBaseURL=api;
-                       if ( conn != null )
-                           CONNECTION_TYPE=conn;
-                       
+        protected static var policyURL:String;
+        protected static var apiBaseURL:String;
+        protected static var params:Object;
+
+        public static function getConnection(initparams:Object=null):Connection {
             if ( connectionInstance == null ) {
-                if ( CONNECTION_TYPE == "XML" )
+            
+                params = initparams == null ? new Object() : initparams;
+                policyURL = getParam("policy", "http://127.0.0.1:3000/api/crossdomain.xml");
+                apiBaseURL = getParam("api", "http://127.0.0.1:3000/api/0.6/");
+                var connectType:String = getParam("connection", "XML");
+                
+                if ( connectType == "XML" )
                     connectionInstance = new XMLConnection();
                 else
                     connectionInstance = new AMFConnection();
@@ -31,6 +29,18 @@ package net.systemeD.halcyon.connection {
             return connectionInstance;
         }
 
+        public static function getParam(name:String, defaultValue:String):String {
+            return params[name] == null ? defaultValue : params[name];
+        }
+
+        public static function get apiBase():String {
+            return apiBaseURL;
+        }
+
+        public static function get serverName():String {
+            return getParam("serverName", "Localhost");
+        }
+                
                public static function getConnectionInstance():Connection {
             return connectionInstance;
                }
@@ -42,6 +52,8 @@ package net.systemeD.halcyon.connection {
         public static var LOAD_COMPLETED:String = "load_completed";
         public static var SAVE_STARTED:String = "save_started";
         public static var SAVE_COMPLETED:String = "save_completed";
+        public static var NEW_CHANGESET:String = "new_changeset";
+        public static var NEW_CHANGESET_ERROR:String = "new_changeset_error";
         public static var NEW_NODE:String = "new_node";
         public static var NEW_WAY:String = "new_way";
         public static var NEW_RELATION:String = "new_relation";
@@ -54,6 +66,7 @@ package net.systemeD.halcyon.connection {
         private var ways:Object = {};
         private var relations:Object = {};
         private var pois:Array = [];
+        private var changeset:Changeset = null;
 
         protected function get nextNegative():Number {
             return negativeID--;
@@ -88,6 +101,11 @@ package net.systemeD.halcyon.connection {
             }
         }
 
+        protected function setActiveChangeset(changeset:Changeset):void {
+            this.changeset = changeset;
+            dispatchEvent(new EntityEvent(NEW_CHANGESET, changeset));
+        }
+        
         public function getNode(id:Number):Node {
             return nodes[id];
         }
@@ -139,11 +157,20 @@ package net.systemeD.halcyon.connection {
             return list;
         }
 
+        public function getActiveChangeset():Changeset {
+            return changeset;
+        }
+        
         // 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,
                                                                top:Number, bottom:Number):void {
            }
+           
+           public function setAppID(id:Object):void {}
+           public function setAuthToken(id:Object):void {}
+           public function createChangeset(tags:Object):void {}
+           public function uploadChanges():void {}
     }
 
 }
index b8204b1..d0e7274 100644 (file)
@@ -77,7 +77,9 @@ package net.systemeD.halcyon.connection {
             return modified;
         }
 
-        public function markClean():void {
+        public function markClean(newID:Number, newVersion:uint):void {
+            this._id = newID;
+            this._version = newVersion;
             modified = false;
         }
 
diff --git a/net/systemeD/halcyon/connection/SaveCompleteEvent.as b/net/systemeD/halcyon/connection/SaveCompleteEvent.as
new file mode 100644 (file)
index 0000000..a283192
--- /dev/null
@@ -0,0 +1,18 @@
+package net.systemeD.halcyon.connection {
+
+    import flash.events.Event;
+
+    public class SaveCompleteEvent extends Event {
+        private var _saveOK:Boolean;
+
+        public function SaveCompleteEvent(type:String, saveOK:Boolean) {
+            super(type);
+            this._saveOK = saveOK;
+        }
+
+        public function get saveOK():Boolean {
+            return _saveOK;
+        }
+    }
+
+}
index 0fa1c6b..3000ce9 100644 (file)
@@ -1,30 +1,20 @@
 package net.systemeD.halcyon.connection {
 
-    import flash.events.Event;
-    import flash.net.URLLoader;
-    import flash.net.URLRequest;
+    import flash.events.*;
 
        import flash.system.Security;
        import flash.net.*;
+    import org.iotashan.oauth.*;
 
 
        public class XMLConnection extends Connection {
 
-        public var readConnection:NetConnection;
+        //public var readConnection:NetConnection;
 
                public function XMLConnection() {
 
                        if (Connection.policyURL!='')
                 Security.loadPolicyFile(Connection.policyURL);
-
-                       readConnection=new NetConnection();
-                       readConnection.objectEncoding = flash.net.ObjectEncoding.AMF0;
-                       readConnection.connect(Connection.apiBaseURL+"amf/read");
-                       
-               }
-
-               override public function getEnvironment(responder:Responder):void {
-                       readConnection.call("getpresets",responder,"en");
                }
                
                override public function loadBbox(left:Number,right:Number,
@@ -84,5 +74,200 @@ package net.systemeD.halcyon.connection {
             }
         }
 
+        protected var appID:OAuthConsumer;
+        protected var authToken:OAuthToken;
+        
+           override public function setAppID(id:Object):void {
+               appID = OAuthConsumer(id);
+           }
+           
+           override public function setAuthToken(id:Object):void {
+               authToken = OAuthToken(id);
+           }
+
+        private var httpStatus:int = 0;
+        
+        private function recordStatus(event:HTTPStatusEvent):void {
+            httpStatus = event.status;
+        }
+        
+        private var lastUploadedChangesetTags:Object;
+        
+        override public function createChangeset(tags:Object):void {
+            lastUploadedChangesetTags = tags;
+            
+               var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
+               var changeset:XML = <changeset />;
+               for (var tagKey:Object in tags) {
+              var tagXML:XML = <tag/>;
+              tagXML.@k = tagKey;
+              tagXML.@v = tags[tagKey];
+              changesetXML.changeset.appendChild(tagXML);
+            }        
+
+            // make an OAuth query
+            var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
+            var url:String = Connection.apiBaseURL+"changeset/create";
+            var params:Object = { _method: "PUT" };
+            var oauthRequest:OAuthRequest = new OAuthRequest("POST", url, params, appID, authToken);
+            var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING)
+
+            // build the actual request
+            var urlReq:URLRequest = new URLRequest(String(urlStr));
+            urlReq.method = "POST";
+            urlReq.data = changesetXML.toXMLString();
+            urlReq.contentType = "application/xml";
+            var loader:URLLoader = new URLLoader();
+            loader.addEventListener(Event.COMPLETE, changesetCreateComplete);
+            loader.addEventListener(IOErrorEvent.IO_ERROR, changesetCreateError);
+            loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
+               loader.load(urlReq);
+           }
+
+        private function changesetCreateComplete(event:Event):void {
+            // response should be a Number changeset id
+            var id:Number = Number(URLLoader(event.target).data);
+            
+            // which means we now have a new changeset!
+            setActiveChangeset(new Changeset(id, lastUploadedChangesetTags));
+        }
+
+        private function changesetCreateError(event:IOErrorEvent):void {
+            dispatchEvent(new Event(NEW_CHANGESET_ERROR));
+        }
+        
+        override public function uploadChanges():void {
+            var changeset:Changeset = getActiveChangeset();
+            var upload:XML = <osmChange version="0.6"/>
+            upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
+            upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
+            upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
+            upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
+            upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
+            upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
+            
+            // *** TODO *** deleting items
+            
+            // now actually upload them
+            // make an OAuth query
+            var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
+            var url:String = Connection.apiBaseURL+"changeset/" + changeset.id + "/upload";
+            var oauthRequest:OAuthRequest = new OAuthRequest("POST", url, null, appID, authToken);
+            var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING)
+
+            // build the actual request
+            var urlReq:URLRequest = new URLRequest(String(urlStr));
+            urlReq.method = "POST";
+            urlReq.data = upload.toXMLString();
+            urlReq.contentType = "text/xml";
+            var loader:URLLoader = new URLLoader();
+            loader.addEventListener(Event.COMPLETE, diffUploadComplete);
+            loader.addEventListener(IOErrorEvent.IO_ERROR, diffUploadError);
+            loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
+               loader.load(urlReq);
+               
+               dispatchEvent(new Event(SAVE_STARTED));
+        }
+
+        private function diffUploadComplete(event:Event):void {
+            // response should be XML describing the progress
+            var results:XML = new XML((URLLoader(event.target).data));
+            
+            for each( var update:XML in results.child("*") ) {
+                var oldID:Number = Number(update.@old_id);
+                var newID:Number = Number(update.@new_id);
+                var version:uint = uint(update.@new_version);
+                var type:String = update.name();
+                
+                var entity:Entity;
+                if ( type == "node" ) entity = getNode(oldID);
+                else if ( type == "way" ) entity = getWay(oldID);
+                else if ( type == "relation" ) entity = getRelation(oldID);
+                entity.markClean(newID, version);
+                
+                // *** TODO *** handle renumbering of creates, and deleting
+            }
+
+               dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
+        }
+
+        private function diffUploadError(event:IOErrorEvent):void {
+            trace("error "+URLLoader(event.target).data + " "+httpStatus+ " " + event.text);
+
+               dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
+        }
+
+        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() ) {
+                if ( id >= 0 )
+                    continue;
+                    
+                var entity:Object = get(id);
+                var xml:XML = serialise(entity);
+                xml.@changeset = changeset.id;
+                create.appendChild(xml);
+            }
+            return create.hasComplexContent() ? create : <!-- blank create section -->;
+        }
+
+        private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
+            var modify:XML = <modify version="0.6"/>
+            for each( var id:Number in getIDs() ) {
+                var entity:Entity = get(id);
+                // creates are already included
+                if ( id < 0 || !entity.isDirty )
+                    continue;
+                    
+                var xml:XML = serialise(entity);
+                xml.@changeset = changeset.id;
+                modify.appendChild(xml);
+            }
+            return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
+        }
+
+        private function serialiseNode(node:Node):XML {
+            var xml:XML = <node/>
+            serialiseEntity(node, xml);
+            xml.@lat = node.lat;
+            xml.@lon = node.lon;
+            return xml;
+        }
+
+        private function serialiseWay(way:Way):XML {
+            var xml:XML = <way/>
+            serialiseEntity(way, xml);
+            for ( var i:uint = 0; i < way.length; i++ ) {
+                var nd:XML = <nd/>
+                nd.@ref = way.getNode(i).id;
+                xml.appendChild(nd);
+            }
+            return xml;
+        }
+
+        private function serialiseRelation(relation:Relation):XML {
+            var xml:XML = <relation/>
+            serialiseEntity(relation, xml);
+            for ( var i:uint = 0; i < relation.length; i++ ) {
+                var relMember:RelationMember = relation.getMember(i);
+                var member:XML = <member/>
+                member.@ref = relMember.entity.id;
+                member.@type = relMember.entity.getType();
+                member.@role = relMember.role;
+                xml.appendChild(member);
+            }
+            return xml;
+        }
+        
+        private function serialiseEntity(entity:Entity, xml:XML):void {
+            xml.@id = entity.id;
+            xml.@version = entity.version;
+            for each( var tag:Tag in entity.getTagArray() ) {
+              var tagXML:XML = <tag/>
+              tagXML.@k = tag.key;
+              tagXML.@v = tag.value;
+              xml.appendChild(tagXML);
+            }
+        }
        }
 }
diff --git a/net/systemeD/potlatch2/save/OAuthPanel.mxml b/net/systemeD/potlatch2/save/OAuthPanel.mxml
new file mode 100644 (file)
index 0000000..ea8d1e1
--- /dev/null
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="utf-8"?>
+<mx:TitleWindow
+       xmlns:mx="http://www.adobe.com/2006/mxml" 
+       layout="vertical"
+       horizontalAlign="center" title="Authorisation Required"
+       creationComplete="getRequestToken()"
+       height="250">
+       
+       <mx:ViewStack id="contentStack" width="100%" height="100%">
+       
+       <mx:VBox id="okPanel" width="100%" height="100%">
+         <mx:Text width="100%" text="{getAuthText()}"/>
+         <mx:VBox width="100%" id="gotLinkBox" visible="false">
+           <mx:Text width="100%">
+             <mx:text>
+               Click the link below to open a web page where
+               you will be asked to authorise access to this app.
+             </mx:text>
+           </mx:Text>
+           <mx:LinkButton id="link"
+               label="http://oauth.dev.openstreetmap.org/oauth/authorize?somekey"
+               click="openURL(authoriseURL); tryAccessButton.enabled=true;"/>
+           <mx:Text width="100%">
+             <mx:text>Once you've authorised the access click the 'Try Access' button below</mx:text>
+           </mx:Text>
+           <mx:Text styleName="failText" visible="false" id="deniedLabel">
+             <mx:text><![CDATA[<b>Access was denied, please check, and try again</b>]]></mx:text>
+           </mx:Text>
+         </mx:VBox>
+       </mx:VBox>
+       
+       <mx:VBox id="permFailPanel" width="100%" height="100%">
+         <mx:Text styleName="failText" width="100%" condenseWhite="true">
+           <mx:htmlText><![CDATA[
+             <p>The server refused this application's credentials -- an authorisation link
+             could not be obtained.
+             </p><p>
+             <b>OAuth access will not be possible.</b>
+             </p><p>
+             Please contact application vendor to find out what's going on.
+           ]]></mx:htmlText>
+         </mx:Text>
+       </mx:VBox>
+       
+       <mx:VBox id="tempFailPanel" width="100%" height="100%">
+         <mx:Text width="100%">
+           <mx:text>
+             There was a problem contacting the server to get authorisation.
+             This may be a temporary error, try again later.
+           </mx:text>
+         </mx:Text>
+       </mx:VBox>
+       
+       </mx:ViewStack>
+       
+       <mx:ControlBar horizontalAlign="right">
+       
+           <mx:ProgressBar id="progress" label="Contacting server..." labelPlacement="top"
+               indeterminate="true"/>
+        <mx:Spacer width="100%"/>
+
+           <mx:Button label="Cancel" click="PopUpManager.removePopUp(this);"/>
+           <mx:Button id="tryAccessButton" label="Try Access" click="getAccessToken()" enabled="false"/>
+       </mx:ControlBar>
+       
+       <mx:Script><![CDATA[
+        import flash.events.Event;
+        import flash.net.*;
+        import mx.managers.PopUpManager;
+        import net.systemeD.halcyon.connection.*;
+        import org.iotashan.oauth.*;
+
+        private var connection:Connection;
+        private var requestToken:OAuthToken;
+        private var _accessToken:OAuthToken;
+        private var authoriseURL:String;
+        private var lastHTTPStatus:int = 0;
+        
+        public static var ACCESS_TOKEN_EVENT:String = "gotAccessToken";
+        
+        private function getAuthText():String {
+            return "To save data you must authorise this application to edit "+
+                    Connection.serverName + " on your behalf.";
+        }
+        
+        private function openURL(url:String):void {
+            var urlRequest:URLRequest = new URLRequest(url);
+            navigateToURL(urlRequest, "_blank");
+        }
+        
+        private function getRequestToken():void {
+            connection = Connection.getConnectionInstance();
+            
+            var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
+            var consumer:OAuthConsumer = getConsumer();
+            var url:String = Connection.getParam("oauth_request_url", "http://127.0.0.1:3000/oauth/request_token");
+            
+            var params:Object = new Object();
+            var oauthRequest:OAuthRequest = new OAuthRequest("GET", url, params, consumer, null);
+            var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING)
+            
+            // build the actual request
+            var urlReq:URLRequest = new URLRequest(String(urlStr));
+            var loader:URLLoader = new URLLoader();
+            loader.addEventListener(Event.COMPLETE, loadedRequestToken);
+            loader.addEventListener(IOErrorEvent.IO_ERROR, requestTokenError);
+            loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
+            loader.load(urlReq);
+        }
+        
+        private function recordStatus(event:HTTPStatusEvent):void {
+            lastHTTPStatus = event.status;
+        }
+        
+        private function requestTokenError(event:IOErrorEvent):void {
+            trace("error occured... last status was: "+lastHTTPStatus);
+            
+            if ( lastHTTPStatus == 401 ) {
+                // this means authorisation was refused -- refused at this stage
+                // means our consumer token is broken
+                contentStack.selectedChild = permFailPanel;
+            } else {
+                contentStack.selectedChild = tempFailPanel;
+            }
+            progress.visible = false;
+        }
+        
+        private function loadedRequestToken(event:Event):void {
+            trace("Yay! response: "+URLLoader(event.target).data);
+            requestToken = getResponseToken(URLLoader(event.target));
+            
+            var url:String = Connection.getParam("oauth_auth_url", "http://127.0.0.1:3000/oauth/authorize");            
+            link.label = url;
+            authoriseURL = url + "?oauth_token="+requestToken.key;
+            progress.visible = false;
+            gotLinkBox.visible = true;
+        }
+
+        private function getResponseToken(loader:URLLoader):OAuthToken {
+            var vars:URLVariables = new URLVariables(loader.data);
+            
+            // build out request token
+            var token:OAuthToken = new OAuthToken(
+                String(vars["oauth_token"]),
+                String(vars["oauth_token_secret"]));
+            return token;
+        }
+        
+        private function getAccessToken():void {
+            var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
+            var consumer:OAuthConsumer = getConsumer();
+            var url:String = Connection.getParam("oauth_access_url", "http://127.0.0.1:3000/oauth/access_token");
+
+            var oauthRequest:OAuthRequest = new OAuthRequest("GET", url, null, consumer, requestToken);
+            var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING)
+
+            var urlReq:URLRequest = new URLRequest(String(urlStr));
+            var loader:URLLoader = new URLLoader();
+            loader.addEventListener(Event.COMPLETE, loadedAccessToken);
+            loader.addEventListener(IOErrorEvent.IO_ERROR, accessTokenError);
+            loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
+            loader.load(urlReq);
+            
+            progress.label = "Checking access";
+            progress.visible = true;  
+        }
+        
+        private function loadedAccessToken(event:Event):void {
+            trace("Yay! response: "+URLLoader(event.target).data);
+            progress.label = "Received Access";
+            progress.indeterminate = false;
+            progress.setProgress(100,100);
+            PopUpManager.removePopUp(this);
+            
+            _accessToken = getResponseToken(URLLoader(event.target));
+            dispatchEvent(new Event(ACCESS_TOKEN_EVENT));
+        }
+        
+        public function get accessToken():OAuthToken {
+            return _accessToken;
+        }
+        
+        private function accessTokenError(event:IOErrorEvent):void {
+            if ( lastHTTPStatus == 401 ) {
+                deniedLabel.htmlText = "<b>Access was denied, please check, and try again</b>";
+            } else {
+                deniedLabel.htmlText = "<b>Error occurred</b> ("+lastHTTPStatus+"): please try again";
+            }
+            deniedLabel.visible = true;
+        }
+        
+        private function getConsumer():OAuthConsumer {
+            var key:String = Connection.getParam("oauth_consumer_key", "");
+            var secret:String = Connection.getParam("oauth_consumer_secret", "");
+            return new OAuthConsumer(key, secret);
+        }
+        
+       ]]></mx:Script>
+</mx:TitleWindow>
+
diff --git a/net/systemeD/potlatch2/save/SaveDialog.mxml b/net/systemeD/potlatch2/save/SaveDialog.mxml
new file mode 100644 (file)
index 0000000..de4145e
--- /dev/null
@@ -0,0 +1,141 @@
+<?xml version="1.0" encoding="utf-8"?>
+<mx:TitleWindow
+       xmlns:mx="http://www.adobe.com/2006/mxml" 
+       layout="vertical"
+       horizontalAlign="center" title="Save Changes"
+       width="350" height="250" verticalGap="0">
+
+  <mx:ArrayCollection id="changesetTags">
+    <mx:Object k="created_by" v="Potlatch 2"/>
+    <mx:Object k="version" v="x.xx.x"/>
+    <mx:Object k="comment" v=""/>
+  </mx:ArrayCollection>
+  
+  <mx:ViewStack id="processSequence" width="100%" height="100%">
+  
+    <!-- section for entering tags -->
+    <mx:VBox width="100%" height="100%" verticalGap="0">
+      <mx:ViewStack id="tagStack" width="100%" height="100%">
+        <mx:VBox width="100%" height="100%" label="Simple">
+          <mx:Text width="100%">
+            <mx:text>
+               Please enter a description of your edits. This will be used to give other
+               mappers an idea of what changes you are making.
+            </mx:text>
+          </mx:Text>
+          <mx:Label text="Comment:"/>
+          <mx:TextArea id="comment" width="100%" height="100%"/>
+        </mx:VBox>
+        
+        <mx:VBox width="100%" height="100%" label="Advanced">
+          <mx:Label text="Changeset tags:"/>
+          <mx:DataGrid editable="true" width="100%" height="100%" id="advancedTagGrid"
+              dataProvider="{changesetTags}">
+            <mx:columns>
+                <mx:DataGridColumn editable="true" dataField="k" headerText="Key"/>
+                <mx:DataGridColumn editable="true" dataField="v" headerText="Value"/>
+            </mx:columns>
+          </mx:DataGrid>        
+        </mx:VBox>
+      </mx:ViewStack>
+      <mx:LinkBar dataProvider="{tagStack}"/>
+    </mx:VBox>
+    
+    <mx:VBox width="100%" height="100%" id="createChangesetTab">
+      <mx:VBox width="100%" height="100%" id="infoBox"/>
+      <mx:Spacer height="100%"/>
+      <mx:ProgressBar label="Creating changeset" labelPlacement="bottom" width="100%"
+          indeterminate="true" id="saveProgress"/>
+    </mx:VBox>
+    
+    <mx:VBox width="100%" height="100%" id="failureTab">
+      <mx:Text width="100%" styleName="failText" text="{failureText}"/>
+    </mx:VBox>
+
+    <mx:VBox width="100%" height="100%" id="successTab">
+      <mx:Text width="100%">
+        <mx:htmlText><![CDATA[<b>All data uploaded!</b>]]></mx:htmlText>
+      </mx:Text>
+    </mx:VBox>
+  </mx:ViewStack>
+
+  <mx:ControlBar>
+    <mx:Spacer width="100%"/>
+    <mx:Button label="Cancel" click="close();"/>
+    <mx:Button id="saveButton" label="Save >" click="startSave();"/>
+  </mx:ControlBar>
+  
+  <mx:Script><![CDATA[
+  
+    import mx.controls.*;
+    import mx.managers.PopUpManager;
+    
+    import net.systemeD.halcyon.connection.*;
+    
+    private var conn:Connection = Connection.getConnectionInstance();
+    
+    [Bindable]
+    private var failureText:String = "";
+    
+    private function startSave():void {
+    
+        // move to next sequence
+        processSequence.selectedChild = createChangesetTab;
+        saveButton.enabled = false;
+        
+        var tags:Object = new Object();
+        for each (var tag:Object in changesetTags) {
+           tags[tag['k']] = tag['v'];
+        }
+        
+        // add the listeners
+        conn.addEventListener(Connection.NEW_CHANGESET, changesetCreated);
+        conn.addEventListener(Connection.NEW_CHANGESET_ERROR, changesetError);
+        conn.createChangeset(tags);
+    }
+    
+    private function changesetCreated(event:EntityEvent):void {
+        var changeset:Changeset = conn.getActiveChangeset();
+        addStatus("Changeset created (id: "+changeset.id+")");
+        
+        saveProgress.label = "Uploading changes";
+        conn.addEventListener(Connection.SAVE_COMPLETED, saveCompleted);
+        conn.uploadChanges();
+    }
+    
+    private function changesetError(event:Event):void {
+        fail("Error creating changeset");
+    }
+    
+    private function saveCompleted(event:SaveCompleteEvent):void {
+        if ( event.saveOK )
+            succeed("All Data Saved!");
+        else
+            fail("Failure when uploading data");
+    }
+    
+    private function addStatus(text:String):void {
+        var label:Text = new Text();
+        label.text = text;
+        
+        infoBox.addChild(label);
+    }
+    
+    private function succeed(text:String):void {
+        processSequence.selectedChild = successTab;
+    }
+    
+    private function fail(text:String):void {
+        processSequence.selectedChild = failureTab;
+        failureText = text;
+    }
+    
+    private function close():void {
+        conn.removeEventListener(Connection.NEW_CHANGESET, changesetCreated);
+        conn.removeEventListener(Connection.NEW_CHANGESET_ERROR, changesetError);
+        conn.removeEventListener(Connection.SAVE_COMPLETED, saveCompleted);
+        PopUpManager.removePopUp(this);
+    }
+  ]]></mx:Script>
+</mx:TitleWindow>
+
diff --git a/net/systemeD/potlatch2/save/SaveManager.as b/net/systemeD/potlatch2/save/SaveManager.as
new file mode 100644 (file)
index 0000000..18d97cd
--- /dev/null
@@ -0,0 +1,75 @@
+package net.systemeD.potlatch2.save {
+
+    import flash.events.*;
+    import mx.managers.PopUpManager;
+    import mx.core.Application;
+    import net.systemeD.halcyon.connection.*;
+    import org.iotashan.oauth.*;
+
+    public class SaveManager {
+    
+        private static var instance:SaveManager = new SaveManager();
+        
+        private var accessToken:OAuthToken;
+        private var consumer:OAuthConsumer;
+
+        public static function saveChanges():void {
+            instance.save();
+        }
+        
+        private function save():void {
+            if ( consumer == null )
+                consumer = getConsumer();
+            if ( accessToken == null )
+                accessToken = getAccessToken();
+        
+            if ( accessToken == null )
+                getNewToken(saveData);
+            else
+                saveData();
+        }
+    
+        private function getAccessToken():OAuthToken {
+            var key:String = Connection.getParam("oauth_token", null);
+            var secret:String = Connection.getParam("oauth_token_secret", null);
+            
+            if ( key == null || secret == null )
+                return null;
+            else    
+                return new OAuthToken(key, secret);
+        }
+
+        private function getConsumer():OAuthConsumer {
+            var key:String = Connection.getParam("oauth_consumer_key", null);
+            var secret:String = Connection.getParam("oauth_consumer_secret", null);
+            
+            if ( key == null || secret == null )
+                return null;
+            else    
+                return new OAuthConsumer(key, secret);
+        }
+        
+        private function getNewToken(onCompletion:Function):void {
+            var oauthPanel:OAuthPanel = OAuthPanel(
+                PopUpManager.createPopUp(Application(Application.application), OAuthPanel, true));
+            PopUpManager.centerPopUp(oauthPanel);
+            
+            var listener:Function = function(event:Event):void {
+                accessToken = oauthPanel.accessToken;
+                onCompletion();
+            }
+            oauthPanel.addEventListener(OAuthPanel.ACCESS_TOKEN_EVENT, listener);
+        }
+        
+        private function saveData():void {
+            Connection.getConnectionInstance().setAppID(consumer);
+            Connection.getConnectionInstance().setAuthToken(accessToken);
+            
+            var saveDialog:SaveDialog = SaveDialog(
+                PopUpManager.createPopUp(Application(Application.application), SaveDialog, true));
+            PopUpManager.centerPopUp(saveDialog);
+        }
+    }
+    
+}
+
diff --git a/styles/Application.css b/styles/Application.css
new file mode 100644 (file)
index 0000000..ad61b38
--- /dev/null
@@ -0,0 +1,17 @@
+
+Application {
+  padding-left: 5;
+  padding-right: 5;
+  padding-top: 5;
+  padding-bottom: 5;
+}
+
+ApplicationControlBar {
+  padding-top: 2;
+  padding-bottom: 2;
+}
+
+.failText {
+  color: red;
+}
+