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