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