helps if you use the right parameters
[potlatch2.git] / net / systemeD / halcyon / connection / XMLConnection.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.*;
4
5         import flash.system.Security;
6         import flash.net.*;
7     import org.iotashan.oauth.*;
8
9         import net.systemeD.halcyon.MapEvent;
10
11     /**
12     * XMLConnection provides all the methods required to connect to a live
13     * OSM server. See OSMConnection for connecting to a read-only .osm file
14     */
15         public class XMLConnection extends XMLBaseConnection {
16
17         //public var readConnection:NetConnection;
18
19                 public function XMLConnection() {
20
21                         if (Connection.policyURL!='')
22                 Security.loadPolicyFile(Connection.policyURL);
23             var oauthPolicy:String = Connection.getParam("oauth_policy", "");
24             if ( oauthPolicy != "" ) {
25                 Security.loadPolicyFile(oauthPolicy);
26             }
27                 }
28                 
29                 override public function loadBbox(left:Number,right:Number,
30                                                                 top:Number,bottom:Number):void {
31             var mapVars:URLVariables = new URLVariables();
32             mapVars.bbox= left+","+bottom+","+right+","+top;
33
34             var mapRequest:URLRequest = new URLRequest(Connection.apiBaseURL+"map");
35             mapRequest.data = mapVars;
36
37             var mapLoader:URLLoader = new URLLoader();
38             mapLoader.addEventListener(Event.COMPLETE, loadedMap);
39             mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
40             mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
41             mapLoader.load(mapRequest);
42             dispatchEvent(new Event(LOAD_STARTED));
43                 }
44
45         private function errorOnMapLoad(event:Event):void {
46                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
47         }
48         private function mapLoadStatus(event:HTTPStatusEvent):void {
49             trace("loading map status = "+event.status);
50         }
51
52         protected var appID:OAuthConsumer;
53         protected var authToken:OAuthToken;
54
55             override public function setAuthToken(id:Object):void {
56                 authToken = OAuthToken(id);
57             }
58
59         override public function hasAccessToken():Boolean {
60             return !(getAccessToken() == null);
61         }
62
63         override public function setAccessToken(key:String, secret:String):void {
64             trace("setAccessToken "+key+" "+secret);
65             if (key && secret) {
66               authToken = new OAuthToken(key, secret);
67             }
68         }
69
70         /* Get the stored access token, or try setting it up from loader params */
71         /* todo: make this private */
72         override public function getAccessToken():OAuthToken {
73             if (authToken == null) {
74               var key:String = getParam("oauth_token", null);
75               var secret:String = getParam("oauth_token_secret", null);
76
77               if ( key != null && secret != null ) {
78                   authToken = new OAuthToken(key, secret);
79               }
80             }
81             return authToken;
82         }
83
84         private function getConsumer():OAuthConsumer {
85             if (appID == null) {
86               var key:String = getParam("oauth_consumer_key", null);
87               var secret:String = getParam("oauth_consumer_secret", null);
88
89               if ( key != null && secret != null ) {
90                   appID = new OAuthConsumer(key, secret);
91               }
92             }
93             return appID;
94         }
95
96         private var httpStatus:int = 0;
97         
98         private function recordStatus(event:HTTPStatusEvent):void {
99             httpStatus = event.status;
100         }
101         
102         private var lastUploadedChangesetTags:Object;
103         
104         override public function createChangeset(tags:Object):void {
105             lastUploadedChangesetTags = tags;
106             
107                 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
108                 var changeset:XML = <changeset />;
109                 for (var tagKey:Object in tags) {
110               var tagXML:XML = <tag/>;
111               tagXML.@k = tagKey;
112               tagXML.@v = tags[tagKey];
113               changesetXML.changeset.appendChild(tagXML);
114             }        
115
116                         sendOAuthPut(Connection.apiBaseURL+"changeset/create",
117                                                  changesetXML,
118                                                  changesetCreateComplete, changesetCreateError, recordStatus);
119             }
120
121         private function changesetCreateComplete(event:Event):void {
122             // response should be a Number changeset id
123             var id:Number = Number(URLLoader(event.target).data);
124             
125             // which means we now have a new changeset!
126             setActiveChangeset(new Changeset(id, lastUploadedChangesetTags));
127         }
128
129         private function changesetCreateError(event:IOErrorEvent):void {
130             dispatchEvent(new Event(NEW_CHANGESET_ERROR));
131         }
132
133                 override public function closeChangeset():void {
134             var cs:Changeset = getActiveChangeset();
135                         if (!cs) return;
136                         
137                         sendOAuthPut(Connection.apiBaseURL+"changeset/"+cs.id+"/close",
138                                                  null,
139                                                  changesetCloseComplete, changesetCloseError, recordStatus);
140                         closeActiveChangeset();
141                 }
142                 
143                 private function changesetCloseComplete(event:Event):void { }
144                 private function changesetCloseError(event:Event):void { }
145                 // ** TODO: when we get little floating warnings, we can send a happy or sad one up
146
147         private function signedOAuthURL(url:String, method:String):String {
148             // method should be PUT, GET, POST or DELETE
149             var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
150             var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
151             var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
152             return String(urlStr);
153         }
154
155                 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
156             // build the request
157             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
158             urlReq.method = "POST";
159                         if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
160             urlReq.contentType = "application/xml";
161             urlReq.requestHeaders = new Array(new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"));
162             var loader:URLLoader = new URLLoader();
163             loader.addEventListener(Event.COMPLETE, onComplete);
164             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
165             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
166                 loader.load(urlReq);
167                 }
168
169         private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
170             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
171             urlReq.method = "GET";
172             var loader:URLLoader = new URLLoader();
173             loader.addEventListener(Event.COMPLETE, onComplete);
174             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
175             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
176             loader.load(urlReq);
177         }
178
179         override public function uploadChanges():void {
180             var changeset:Changeset = getActiveChangeset();
181             var upload:XML = <osmChange version="0.6"/>
182             upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
183             upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
184             upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
185             upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
186             upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
187             upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
188             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot));
189             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot));
190             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot));
191
192             // now actually upload them
193             // make an OAuth query
194
195             var url:String = Connection.apiBaseURL+"changeset/" + changeset.id + "/upload";
196
197             // build the actual request
198             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "POST"));
199             urlReq.method = "POST";
200             urlReq.data = upload.toXMLString();
201             urlReq.contentType = "text/xml";
202             var loader:URLLoader = new URLLoader();
203             loader.addEventListener(Event.COMPLETE, diffUploadComplete);
204             loader.addEventListener(IOErrorEvent.IO_ERROR, diffUploadError);
205             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
206                 loader.load(urlReq);
207                 
208                 dispatchEvent(new Event(SAVE_STARTED));
209         }
210
211         private function diffUploadComplete(event:Event):void {
212             // response should be XML describing the progress
213             var results:XML = new XML((URLLoader(event.target).data));
214             
215             for each( var update:XML in results.child("*") ) {
216                 var oldID:Number = Number(update.@old_id);
217                 var newID:Number = Number(update.@new_id);
218                 var version:uint = uint(update.@new_version);
219                 var type:String = update.name();
220
221                                 if (newID==0) {
222                                         // delete
223                         if ( type == "node" ) killNode(oldID);
224                         else if ( type == "way" ) killWay(oldID);
225                         else if ( type == "relation" ) killRelation(oldID);
226                                         
227                                 } else {
228                                         // create/update
229                         var entity:Entity;
230                         if ( type == "node" ) entity = getNode(oldID);
231                         else if ( type == "way" ) entity = getWay(oldID);
232                         else if ( type == "relation" ) entity = getRelation(oldID);
233                         entity.markClean(newID, version);
234                 
235                         if ( oldID != newID ) {
236                             if ( type == "node" ) renumberNode(oldID, entity as Node, false);
237                             else if ( type == "way" ) renumberWay(oldID, entity as Way, false);
238                             else if ( type == "relation" ) renumberRelation(oldID, entity as Relation, false);
239                         }
240                                 }
241             }
242
243             dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
244                         freshenActiveChangeset();
245             markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
246             MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
247         }
248
249         private function diffUploadError(event:IOErrorEvent):void {
250                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+httpStatus+" "+event.text } ));
251                 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
252         }
253
254         private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
255             var create:XML = <create version="0.6"/>
256             for each( var id:Number in getIDs() ) {
257                 var entity:Entity = get(id);
258                 if ( id >= 0 || entity.deleted )
259                     continue;
260                     
261                 var xml:XML = serialise(entity);
262                 xml.@changeset = changeset.id;
263                 create.appendChild(xml);
264             }
265             return create.hasComplexContent() ? create : <!-- blank create section -->;
266         }
267
268                 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
269             var del:XML = <delete version="0.6"/>
270             for each( var id:Number in getIDs() ) {
271                 var entity:Entity = get(id);
272                 // creates are already included
273                 if ( id < 0 || !entity.deleted )
274                     continue;
275                     
276                 var xml:XML = serialise(entity);
277                 xml.@changeset = changeset.id;
278                 del.appendChild(xml);
279             }
280             return del.hasComplexContent() ? del : <!-- blank delete section -->;
281                 }
282
283         private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
284             var modify:XML = <modify version="0.6"/>
285             for each( var id:Number in getIDs() ) {
286                 var entity:Entity = get(id);
287                 // creates and deletes are already included
288                 if ( id < 0 || entity.deleted || !entity.isDirty )
289                     continue;
290                     
291                 var xml:XML = serialise(entity);
292                 xml.@changeset = changeset.id;
293                 modify.appendChild(xml);
294             }
295             return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
296         }
297
298         private function serialiseNode(node:Node):XML {
299             var xml:XML = serialiseEntityRoot(node); //<node/>
300             serialiseEntityTags(node, xml);
301             xml.@lat = node.lat;
302             xml.@lon = node.lon;
303             return xml;
304         }
305
306         private function serialiseWay(way:Way):XML {
307             var xml:XML = serialiseEntityRoot(way); //<node/>
308             serialiseEntityTags(way, xml);
309             for ( var i:uint = 0; i < way.length; i++ ) {
310                 var nd:XML = <nd/>
311                 nd.@ref = way.getNode(i).id;
312                 xml.appendChild(nd);
313             }
314             return xml;
315         }
316
317         private function serialiseRelation(relation:Relation):XML {
318             var xml:XML = serialiseEntityRoot(relation); //<node/>
319             serialiseEntityTags(relation, xml);
320             for ( var i:uint = 0; i < relation.length; i++ ) {
321                 var relMember:RelationMember = relation.getMember(i);
322                 var member:XML = <member/>
323                 member.@ref = relMember.entity.id;
324                 member.@type = relMember.entity.getType();
325                 member.@role = relMember.role;
326                 xml.appendChild(member);
327             }
328             return xml;
329         }
330         
331                 private function serialiseEntityRoot(entity:Object):XML {
332                         var xml:XML;
333                         if      (entity is Way     ) { xml = <way/> }
334                         else if (entity is Node    ) { xml = <node/> }
335                         else if (entity is Relation) { xml = <relation/> }
336                         xml.@id = entity.id;
337                         xml.@version = entity.version;
338                         return xml;
339                 }
340
341         private function serialiseEntityTags(entity:Entity, xml:XML):void {
342             xml.@id = entity.id;
343             xml.@version = entity.version;
344             for each( var tag:Tag in entity.getTagArray() ) {
345               if (tag.key == 'created_by') {
346                 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
347                 continue;
348               }
349               var tagXML:XML = <tag/>
350               tagXML.@k = tag.key;
351               tagXML.@v = tag.value;
352               xml.appendChild(tagXML);
353             }
354         }
355
356         override public function fetchUserTraces(refresh:Boolean=false):void {
357             if (traces_loaded && !refresh) {
358               dispatchEvent(new Event(TRACES_LOADED));
359             } else {
360               sendOAuthGet(Connection.apiBaseURL+"user/gpx_files",
361                           tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
362               dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
363             }
364         }
365
366         private function tracesLoadComplete(event:Event):void {
367             clearTraces();
368             var files:XML = new XML(URLLoader(event.target).data);
369             for each(var traceData:XML in files.gpx_file) {
370               var t:Trace = new Trace().fromXML(traceData);
371               addTrace(t);
372             }
373             traces_loaded = true;
374             dispatchEvent(new Event(LOAD_COMPLETED));
375             dispatchEvent(new Event(TRACES_LOADED));
376         }
377
378         override public function fetchTrace(id:Number, callback:Function):void {
379             sendOAuthGet(Connection.apiBaseURL+"gpx/"+id+"/data.xml", callback, errorOnMapLoad, mapLoadStatus); // needs error handlers
380             dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
381         }
382         }
383 }