Merge remote-tracking branch 'gravitystorm/mygpx'
[potlatch2.git] / net / systemeD / halcyon / connection / XMLConnection.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.*;
4         import mx.rpc.http.HTTPService;
5         import mx.rpc.events.*;
6         import flash.system.Security;
7         import flash.net.*;
8     import org.iotashan.oauth.*;
9
10         import net.systemeD.halcyon.AttentionEvent;
11         import net.systemeD.halcyon.MapEvent;
12     import net.systemeD.halcyon.connection.bboxes.*;
13
14     /**
15     * XMLConnection provides all the methods required to connect to a live
16     * OSM server. See OSMConnection for connecting to a read-only .osm file
17     *
18     * @see OSMConnection
19     */
20         public class XMLConnection extends XMLBaseConnection {
21
22                 private const MARGIN:Number=0.05;
23
24         /**
25         * Create a new XML connection
26         * @param name The name of the connection
27         * @param api The url of the OSM API server, e.g. http://api06.dev.openstreetmap.org/api/0.6/
28         * @param policy The url of the flash crossdomain policy to load,
29                         e.g. http://api06.dev.openstreetmap.org/api/crossdomain.xml
30         * @param initparams Any further parameters for the connection, such as the serverName
31         */
32                 public function XMLConnection(name:String,api:String,policy:String,initparams:Object) {
33
34                         super(name,api,policy,initparams);
35                         if (policyURL != "") Security.loadPolicyFile(policyURL);
36
37             var oauthPolicy:String = getParam("oauth_policy", "");
38             if (oauthPolicy != "") Security.loadPolicyFile(oauthPolicy);
39                 }
40                 
41                 override public function loadBbox(left:Number,right:Number,
42                                                                 top:Number,bottom:Number):void {
43             purgeIfFull(left,right,top,bottom);
44                         var requestBox:Box=new Box().fromBbox(left,bottom,right,top);
45                         var boxes:Array;
46                         try {
47                                 boxes=fetchSet.getBoxes(requestBox,MAX_BBOXES);
48                         } catch(err:Error) {
49                                 boxes=[requestBox];
50                         }
51                         for each (var box:Box in boxes) {
52                                 // enlarge bbox by given margin on each edge
53                                 var xmargin:Number=(box.right-box.left)*MARGIN;
54                                 var ymargin:Number=(box.top-box.bottom)*MARGIN;
55                                 left  =box.left  -xmargin; right=box.right+xmargin;
56                                 bottom=box.bottom-ymargin; top  =box.top  +ymargin;
57
58                                 dispatchEvent(new MapEvent(MapEvent.DOWNLOAD, {minlon:left, maxlon:right, maxlat:top, minlat:bottom} ));
59
60                                 // send HTTP request
61                                 var mapVars:URLVariables = new URLVariables();
62                                 mapVars.bbox=left+","+bottom+","+right+","+top;
63                                 var mapRequest:URLRequest = new URLRequest(apiBaseURL+"map");
64                                 mapRequest.data = mapVars;
65                                 sendLoadRequest(mapRequest);
66                         }
67                 }
68
69                 override public function loadEntityByID(type:String, id:Number):void {
70                         var url:String=apiBaseURL + type + "/" + id;
71                         if (type=='way') url+="/full";
72                         sendLoadRequest(new URLRequest(url));
73                 }
74
75                 private function sendLoadRequest(request:URLRequest):void {
76                         var mapLoader:URLLoader = new URLLoader();
77             var errorHandler:Function = function(event:IOErrorEvent):void {
78                 errorOnMapLoad(event, request);
79             }
80                         mapLoader.addEventListener(Event.COMPLETE, loadedMap);
81                         mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
82                         mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
83             request.requestHeaders.push(new URLRequestHeader("X-Error-Format", "XML"));
84                         mapLoader.load(request);
85                         dispatchEvent(new Event(LOAD_STARTED));
86                 }
87
88         private function errorOnMapLoad(event:Event, request:URLRequest):void {
89             var url:String = request.url + '?' + URLVariables(request.data).toString(); // for get reqeusts, at least
90             dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "There was a problem loading the map data.\nPlease check your internet connection, or try zooming in.\n\n" + url } ));
91             dispatchEvent(new Event(LOAD_COMPLETED));
92         }
93
94         private function mapLoadStatus(event:HTTPStatusEvent):void {
95         }
96
97         protected var appID:OAuthConsumer;
98         protected var authToken:OAuthToken;
99
100             override public function setAuthToken(id:Object):void {
101                 authToken = OAuthToken(id);
102             }
103
104         override public function hasAccessToken():Boolean {
105             return !(getAccessToken() == null);
106         }
107
108         override public function setAccessToken(key:String, secret:String):void {
109             if (key && secret) {
110               authToken = new OAuthToken(key, secret);
111             }
112         }
113
114         /* Get the stored access token, or try setting it up from loader params */
115         private function getAccessToken():OAuthToken {
116             if (authToken == null) {
117               var key:String = getParam("oauth_token", null);
118               var secret:String = getParam("oauth_token_secret", null);
119
120               if ( key != null && secret != null ) {
121                   authToken = new OAuthToken(key, secret);
122               }
123             }
124             return authToken;
125         }
126
127         private function getConsumer():OAuthConsumer {
128             if (appID == null) {
129               var key:String = getParam("oauth_consumer_key", null);
130               var secret:String = getParam("oauth_consumer_secret", null);
131
132               if ( key != null && secret != null ) {
133                   appID = new OAuthConsumer(key, secret);
134               }
135             }
136             return appID;
137         }
138
139         private var httpStatus:int = 0;
140         
141         private function recordStatus(event:HTTPStatusEvent):void {
142             httpStatus = event.status;
143         }
144         
145         private var lastUploadedChangesetTags:Object;
146         
147         override public function createChangeset(tags:Object):void {
148             lastUploadedChangesetTags = tags;
149             
150                 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
151                 var changeset:XML = <changeset />;
152                 for (var tagKey:Object in tags) {
153               var tagXML:XML = <tag/>;
154               tagXML.@k = tagKey;
155               tagXML.@v = tags[tagKey];
156               changesetXML.changeset.appendChild(tagXML);
157             }        
158
159                         sendOAuthPut(apiBaseURL+"changeset/create",
160                                                  changesetXML,
161                                                  changesetCreateComplete, changesetCreateError, recordStatus);
162             }
163
164         private function changesetCreateComplete(event:Event):void {
165             var result:String = URLLoader(event.target).data;
166
167             if (result.match(/^^\d+$/)) {
168                 // response should be a Number changeset id
169                 var id:Number = Number(URLLoader(event.target).data);
170             
171                 // which means we now have a new changeset!
172                 setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
173             } else {
174                 var results:XML = XML(result);
175
176                 throwServerError(results.message);
177             }
178         }
179
180         private function changesetCreateError(event:IOErrorEvent):void {
181             dispatchEvent(new Event(NEW_CHANGESET_ERROR));
182         }
183
184                 override public function closeChangeset():void {
185             var cs:Changeset = getActiveChangeset();
186                         if (!cs) return;
187                         
188                         sendOAuthPut(apiBaseURL+"changeset/"+cs.id+"/close",
189                                                  null,
190                                                  changesetCloseComplete, changesetCloseError, recordStatus);
191                         closeActiveChangeset();
192                 }
193                 
194                 private function changesetCloseComplete(event:Event):void { 
195                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
196                 }
197                 private function changesetCloseError(event:Event):void { 
198                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
199                 }
200
201         private function signedOAuthURL(url:String, method:String):String {
202             // method should be PUT, GET, POST or DELETE
203             var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
204             var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
205             var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
206             return String(urlStr);
207         }
208
209                 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
210             // build the request
211             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
212             urlReq.method = "POST";
213                         if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
214             urlReq.contentType = "application/xml";
215             urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"), 
216                                                   new URLRequestHeader("X-Error-Format", "XML") ];
217             var loader:URLLoader = new URLLoader();
218             loader.addEventListener(Event.COMPLETE, onComplete);
219             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
220             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
221                 loader.load(urlReq);
222                 }
223
224         private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
225             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
226             urlReq.method = "GET";
227             var loader:URLLoader = new URLLoader();
228             loader.addEventListener(Event.COMPLETE, onComplete);
229             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
230             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
231             loader.load(urlReq);
232         }
233
234                 /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
235                     (We don't mind what's returned as long as it implements .toString() ) */
236
237         override public function uploadChanges():* {
238             var changeset:Changeset = getActiveChangeset();
239             var upload:XML = <osmChange version="0.6"/>
240             upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
241             upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
242             upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
243             upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
244             upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
245             upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
246             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
247             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
248             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
249             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
250             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
251             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
252
253             // now actually upload them
254             // make an OAuth query
255             var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
256
257             // build the actual request
258                         var serv:HTTPService=new HTTPService();
259                         serv.method="POST";
260                         serv.url=signedOAuthURL(url, "POST");
261                         serv.contentType = "text/xml";
262                         serv.headers={'X-Error-Format':'xml'};
263                         serv.request=" ";
264                         serv.resultFormat="e4x";
265                         serv.requestTimeout=0;
266                         serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
267                         serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
268                         serv.send(upload);
269                 
270                         dispatchEvent(new Event(SAVE_STARTED));
271                         return upload;
272         }
273
274         private function diffUploadComplete(event:ResultEvent):void {
275                         var results:XML = XML(event.result);
276
277                         // was it an error document?
278                         if (results.name().localName=='osmError') {
279                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
280                                 diffUploadAPIError(results.status, results.message);
281                                 return;
282                         }
283
284             // response should be XML describing the progress
285             
286             for each( var update:XML in results.child("*") ) {
287                 var oldID:Number = Number(update.@old_id);
288                 var newID:Number = Number(update.@new_id);
289                 var version:uint = uint(update.@new_version);
290                 var type:String = update.name();
291
292                                 if (newID==0) {
293                                         // delete
294                         if      (type == "node"    ) { killNode(oldID); }
295                         else if (type == "way"     ) { killWay(oldID); }
296                         else if (type == "relation") { killRelation(oldID); }
297                                         
298                                 } else {
299                                         // create/update
300                         if      (type == "node"    ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
301                         else if (type == "way"     ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
302                         else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
303                                 }
304             }
305
306             dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
307                         freshenActiveChangeset();
308             markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
309             MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
310         }
311
312                 private function diffUploadIOError(event:FaultEvent):void {
313                         trace(event.fault);
314                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
315                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
316                 }
317
318                 private function diffUploadAPIError(status:String, message:String):void {
319                         var matches:Array;
320                         switch (status) {
321
322                                 case '409 Conflict':
323                                         if (message.match(/changeset/i)) { throwChangesetError(message); return; }
324                                         matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
325                                         if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
326                                         break;
327                                 
328                                 case '410 Gone':
329                                         matches=message.match(/The (\w+) with the id (\d+)/i);
330                                         if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
331                                         break;
332                                 
333                                 case '412 Precondition Failed':
334                                         matches=message.match(/Node (\d+) is still used/i);
335                                         if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
336                                         matches=message.match(/relation (\d+) is used/i);
337                                         if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
338                                         matches=message.match(/Way (\d+) still used/i);
339                                         if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
340                                         matches=message.match(/Cannot update (\w+) (\d+)/i);
341                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
342                                         matches=message.match(/Relation with id (\d+)/i);
343                                         if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
344                                         matches=message.match(/Way (\d+) requires the nodes/i);
345                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
346                                         throwBugError(message); return;
347                                 
348                                 case '404 Not Found':
349                                         throwBugError(message); return;
350                                         
351                                 case '400 Bad Request':
352                                         matches=message.match(/Element (\w+)\/(\d+)/i);
353                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
354                                         matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
355                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
356                                         throwBugError(message); return;
357                         }
358
359                         // Not caught, so just throw a generic server error
360                         throwServerError(message);
361                 }
362
363         private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
364             var create:XML = <create version="0.6"/>
365             for each( var id:Number in getIDs() ) {
366                 var entity:Entity = get(id);
367                 if ( id >= 0 || entity.deleted )
368                     continue;
369                     
370                 var xml:XML = serialise(entity);
371                 xml.@changeset = changeset.id;
372                 create.appendChild(xml);
373             }
374             return create.hasComplexContent() ? create : <!-- blank create section -->;
375         }
376
377                 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
378             var del:XML = <delete version="0.6"/>
379             if (ifUnused) del.@["if-unused"] = "true";
380             for each( var id:Number in getIDs() ) {
381                 var entity:Entity = get(id);
382                 // creates are already included
383                 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
384                     continue;
385                     
386                 var xml:XML = serialise(entity);
387                 xml.@changeset = changeset.id;
388                 del.appendChild(xml);
389             }
390             return del.hasComplexContent() ? del : <!-- blank delete section -->;
391                 }
392
393         private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
394             var modify:XML = <modify version="0.6"/>
395             for each( var id:Number in getIDs() ) {
396                 var entity:Entity = get(id);
397                 // creates and deletes are already included
398                 if ( id < 0 || entity.deleted || !entity.isDirty )
399                     continue;
400                     
401                 var xml:XML = serialise(entity);
402                 xml.@changeset = changeset.id;
403                 modify.appendChild(xml);
404             }
405             return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
406         }
407
408         private function serialiseNode(node:Node):XML {
409             var xml:XML = serialiseEntityRoot(node); //<node/>
410             serialiseEntityTags(node, xml);
411             xml.@lat = node.lat;
412             xml.@lon = node.lon;
413             return xml;
414         }
415
416         private function serialiseWay(way:Way):XML {
417             var xml:XML = serialiseEntityRoot(way); //<node/>
418             serialiseEntityTags(way, xml);
419             for ( var i:uint = 0; i < way.length; i++ ) {
420                 var nd:XML = <nd/>
421                 nd.@ref = way.getNode(i).id;
422                 xml.appendChild(nd);
423             }
424             return xml;
425         }
426
427         private function serialiseRelation(relation:Relation):XML {
428             var xml:XML = serialiseEntityRoot(relation); //<node/>
429             serialiseEntityTags(relation, xml);
430             for ( var i:uint = 0; i < relation.length; i++ ) {
431                 var relMember:RelationMember = relation.getMember(i);
432                 var member:XML = <member/>
433                 member.@ref = relMember.entity.id;
434                 member.@type = relMember.entity.getType();
435                 member.@role = relMember.role;
436                 xml.appendChild(member);
437             }
438             return xml;
439         }
440         
441                 private function serialiseEntityRoot(entity:Object):XML {
442                         var xml:XML;
443                         if      (entity is Way     ) { xml = <way/> }
444                         else if (entity is Node    ) { xml = <node/> }
445                         else if (entity is Relation) { xml = <relation/> }
446                         xml.@id = entity.id;
447                         xml.@version = entity.version;
448                         return xml;
449                 }
450
451         private function serialiseEntityTags(entity:Entity, xml:XML):void {
452             xml.@id = entity.id;
453             xml.@version = entity.version;
454             for each( var tag:Tag in entity.getTagArray() ) {
455               if (tag.key == 'created_by') {
456                 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
457                 continue;
458               }
459               var tagXML:XML = <tag/>
460               tagXML.@k = tag.key;
461               tagXML.@v = tag.value;
462               xml.appendChild(tagXML);
463             }
464         }
465
466         override public function fetchUserTraces(refresh:Boolean=false):void {
467             if (traces_loaded && !refresh) {
468               dispatchEvent(new Event(TRACES_LOADED));
469             } else {
470               sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
471               dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
472             }
473         }
474
475                 private function tracesLoadComplete(event:Event):void {
476                         var files:XML = new XML(URLLoader(event.target).data);
477                         for each(var traceData:XML in files.gpx_file) {
478                                 var t:Trace = findTrace(traceData.@id);
479                                 if (!t) { t=new Trace(this); addTrace(t); }
480                                 t.fromXML(traceData);
481                         }
482                         traces_loaded = true;
483                         dispatchEvent(new Event(LOAD_COMPLETED));
484                         dispatchEvent(new Event(TRACES_LOADED));
485                 }
486
487         override public function fetchTrace(id:Number, callback:Function):void {
488             sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml", 
489                                 function(e:Event):void { 
490                         dispatchEvent(new Event(LOAD_COMPLETED));
491                                         callback(e);
492                                 }, errorOnTraceLoad, mapLoadStatus); // needs error handlers
493             dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
494         }
495
496         private function errorOnTraceLoad(event:Event):void {
497             trace("Trace load error");
498             dispatchEvent(new Event(LOAD_COMPLETED));
499         }
500         }
501 }