Restore compatibility with FP 10.1
[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             // response should be a Number changeset id
166             var id:Number = Number(URLLoader(event.target).data);
167             
168             // which means we now have a new changeset!
169             setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
170         }
171
172         private function changesetCreateError(event:IOErrorEvent):void {
173             dispatchEvent(new Event(NEW_CHANGESET_ERROR));
174         }
175
176                 override public function closeChangeset():void {
177             var cs:Changeset = getActiveChangeset();
178                         if (!cs) return;
179                         
180                         sendOAuthPut(apiBaseURL+"changeset/"+cs.id+"/close",
181                                                  null,
182                                                  changesetCloseComplete, changesetCloseError, recordStatus);
183                         closeActiveChangeset();
184                 }
185                 
186                 private function changesetCloseComplete(event:Event):void { 
187                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
188                 }
189                 private function changesetCloseError(event:Event):void { 
190                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
191                 }
192
193         private function signedOAuthURL(url:String, method:String):String {
194             // method should be PUT, GET, POST or DELETE
195             var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
196             var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
197             var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
198             return String(urlStr);
199         }
200
201                 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
202             // build the request
203             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
204             urlReq.method = "POST";
205                         if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
206             urlReq.contentType = "application/xml";
207             urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"), 
208                                                   new URLRequestHeader("X-Error-Format", "XML") ];
209             var loader:URLLoader = new URLLoader();
210             loader.addEventListener(Event.COMPLETE, onComplete);
211             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
212             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
213                 loader.load(urlReq);
214                 }
215
216         private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
217             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
218             urlReq.method = "GET";
219             var loader:URLLoader = new URLLoader();
220             loader.addEventListener(Event.COMPLETE, onComplete);
221             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
222             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
223             loader.load(urlReq);
224         }
225
226                 /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
227                     (We don't mind what's returned as long as it implements .toString() ) */
228
229         override public function uploadChanges():* {
230             var changeset:Changeset = getActiveChangeset();
231             var upload:XML = <osmChange version="0.6"/>
232             upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
233             upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
234             upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
235             upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
236             upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
237             upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
238             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
239             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
240             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
241             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
242             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
243             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
244
245             // now actually upload them
246             // make an OAuth query
247             var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
248
249             // build the actual request
250                         var serv:HTTPService=new HTTPService();
251                         serv.method="POST";
252                         serv.url=signedOAuthURL(url, "POST");
253                         serv.contentType = "text/xml";
254                         serv.headers={'X-Error-Format':'xml'};
255                         serv.request=" ";
256                         serv.resultFormat="e4x";
257                         serv.requestTimeout=0;
258                         serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
259                         serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
260                         serv.send(upload);
261                 
262                         dispatchEvent(new Event(SAVE_STARTED));
263                         return upload;
264         }
265
266         private function diffUploadComplete(event:ResultEvent):void {
267                         var results:XML = XML(event.result);
268
269                         // was it an error document?
270                         if (results.name().localName=='osmError') {
271                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
272                                 diffUploadAPIError(results.status, results.message);
273                                 return;
274                         }
275
276             // response should be XML describing the progress
277             
278             for each( var update:XML in results.child("*") ) {
279                 var oldID:Number = Number(update.@old_id);
280                 var newID:Number = Number(update.@new_id);
281                 var version:uint = uint(update.@new_version);
282                 var type:String = update.name();
283
284                                 if (newID==0) {
285                                         // delete
286                         if      (type == "node"    ) { killNode(oldID); }
287                         else if (type == "way"     ) { killWay(oldID); }
288                         else if (type == "relation") { killRelation(oldID); }
289                                         
290                                 } else {
291                                         // create/update
292                         if      (type == "node"    ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
293                         else if (type == "way"     ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
294                         else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
295                                 }
296             }
297
298             dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
299                         freshenActiveChangeset();
300             markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
301             MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
302         }
303
304                 private function diffUploadIOError(event:FaultEvent):void {
305                         trace(event.fault);
306                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
307                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
308                 }
309
310                 private function diffUploadAPIError(status:String, message:String):void {
311                         var matches:Array;
312                         switch (status) {
313
314                                 case '409 Conflict':
315                                         if (message.match(/changeset/i)) { throwChangesetError(message); return; }
316                                         matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
317                                         if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
318                                         break;
319                                 
320                                 case '410 Gone':
321                                         matches=message.match(/The (\w+) with the id (\d+)/i);
322                                         if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
323                                         break;
324                                 
325                                 case '412 Precondition Failed':
326                                         matches=message.match(/Node (\d+) is still used/i);
327                                         if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
328                                         matches=message.match(/relation (\d+) is used/i);
329                                         if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
330                                         matches=message.match(/Way (\d+) still used/i);
331                                         if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
332                                         matches=message.match(/Cannot update (\w+) (\d+)/i);
333                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
334                                         matches=message.match(/Relation with id (\d+)/i);
335                                         if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
336                                         matches=message.match(/Way (\d+) requires the nodes/i);
337                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
338                                         throwBugError(message); return;
339                                 
340                                 case '404 Not Found':
341                                         throwBugError(message); return;
342                                         
343                                 case '400 Bad Request':
344                                         matches=message.match(/Element (\w+)\/(\d+)/i);
345                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
346                                         matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
347                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
348                                         throwBugError(message); return;
349                         }
350
351                         // Not caught, so just throw a generic server error
352                         throwServerError(message);
353                 }
354
355         private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
356             var create:XML = <create version="0.6"/>
357             for each( var id:Number in getIDs() ) {
358                 var entity:Entity = get(id);
359                 if ( id >= 0 || entity.deleted )
360                     continue;
361                     
362                 var xml:XML = serialise(entity);
363                 xml.@changeset = changeset.id;
364                 create.appendChild(xml);
365             }
366             return create.hasComplexContent() ? create : <!-- blank create section -->;
367         }
368
369                 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
370             var del:XML = <delete version="0.6"/>
371             if (ifUnused) del.@["if-unused"] = "true";
372             for each( var id:Number in getIDs() ) {
373                 var entity:Entity = get(id);
374                 // creates are already included
375                 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
376                     continue;
377                     
378                 var xml:XML = serialise(entity);
379                 xml.@changeset = changeset.id;
380                 del.appendChild(xml);
381             }
382             return del.hasComplexContent() ? del : <!-- blank delete section -->;
383                 }
384
385         private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
386             var modify:XML = <modify version="0.6"/>
387             for each( var id:Number in getIDs() ) {
388                 var entity:Entity = get(id);
389                 // creates and deletes are already included
390                 if ( id < 0 || entity.deleted || !entity.isDirty )
391                     continue;
392                     
393                 var xml:XML = serialise(entity);
394                 xml.@changeset = changeset.id;
395                 modify.appendChild(xml);
396             }
397             return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
398         }
399
400         private function serialiseNode(node:Node):XML {
401             var xml:XML = serialiseEntityRoot(node); //<node/>
402             serialiseEntityTags(node, xml);
403             xml.@lat = node.lat;
404             xml.@lon = node.lon;
405             return xml;
406         }
407
408         private function serialiseWay(way:Way):XML {
409             var xml:XML = serialiseEntityRoot(way); //<node/>
410             serialiseEntityTags(way, xml);
411             for ( var i:uint = 0; i < way.length; i++ ) {
412                 var nd:XML = <nd/>
413                 nd.@ref = way.getNode(i).id;
414                 xml.appendChild(nd);
415             }
416             return xml;
417         }
418
419         private function serialiseRelation(relation:Relation):XML {
420             var xml:XML = serialiseEntityRoot(relation); //<node/>
421             serialiseEntityTags(relation, xml);
422             for ( var i:uint = 0; i < relation.length; i++ ) {
423                 var relMember:RelationMember = relation.getMember(i);
424                 var member:XML = <member/>
425                 member.@ref = relMember.entity.id;
426                 member.@type = relMember.entity.getType();
427                 member.@role = relMember.role;
428                 xml.appendChild(member);
429             }
430             return xml;
431         }
432         
433                 private function serialiseEntityRoot(entity:Object):XML {
434                         var xml:XML;
435                         if      (entity is Way     ) { xml = <way/> }
436                         else if (entity is Node    ) { xml = <node/> }
437                         else if (entity is Relation) { xml = <relation/> }
438                         xml.@id = entity.id;
439                         xml.@version = entity.version;
440                         return xml;
441                 }
442
443         private function serialiseEntityTags(entity:Entity, xml:XML):void {
444             xml.@id = entity.id;
445             xml.@version = entity.version;
446             for each( var tag:Tag in entity.getTagArray() ) {
447               if (tag.key == 'created_by') {
448                 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
449                 continue;
450               }
451               var tagXML:XML = <tag/>
452               tagXML.@k = tag.key;
453               tagXML.@v = tag.value;
454               xml.appendChild(tagXML);
455             }
456         }
457
458         override public function fetchUserTraces(refresh:Boolean=false):void {
459             if (traces_loaded && !refresh) {
460               dispatchEvent(new Event(TRACES_LOADED));
461             } else {
462               sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
463               dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
464             }
465         }
466
467                 private function tracesLoadComplete(event:Event):void {
468                         var files:XML = new XML(URLLoader(event.target).data);
469                         for each(var traceData:XML in files.gpx_file) {
470                                 var t:Trace = findTrace(traceData.@id);
471                                 if (!t) { t=new Trace(this); addTrace(t); }
472                                 t.fromXML(traceData);
473                         }
474                         traces_loaded = true;
475                         dispatchEvent(new Event(LOAD_COMPLETED));
476                         dispatchEvent(new Event(TRACES_LOADED));
477                 }
478
479         override public function fetchTrace(id:Number, callback:Function):void {
480             sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml", 
481                                 function(e:Event):void { 
482                         dispatchEvent(new Event(LOAD_COMPLETED));
483                                         callback(e);
484                                 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
485             dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
486         }
487         }
488 }