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