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