1 package net.systemeD.halcyon.connection {
4 import mx.rpc.http.HTTPService;
5 import mx.rpc.events.*;
6 import flash.system.Security;
8 import org.iotashan.oauth.*;
10 import net.systemeD.halcyon.AttentionEvent;
11 import net.systemeD.halcyon.MapEvent;
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
17 public class XMLConnection extends XMLBaseConnection {
19 public function XMLConnection(name:String,api:String,policy:String,initparams:Object) {
21 super(name,api,policy,initparams);
22 if (policyURL != "") Security.loadPolicyFile(policyURL);
24 var oauthPolicy:String = getParam("oauth_policy", "");
25 if (oauthPolicy != "") Security.loadPolicyFile(oauthPolicy);
28 override public function loadBbox(left:Number,right:Number,
29 top:Number,bottom:Number):void {
30 purgeIfFull(left,right,top,bottom);
31 if (isBboxLoaded(left,right,top,bottom)) return;
33 // enlarge bbox by 20% on each edge
34 var xmargin:Number=(right-left)/5;
35 var ymargin:Number=(top-bottom)/5;
36 left-=xmargin; right+=xmargin;
37 bottom-=ymargin; top+=ymargin;
39 var mapVars:URLVariables = new URLVariables();
40 mapVars.bbox= left+","+bottom+","+right+","+top;
42 var mapRequest:URLRequest = new URLRequest(apiBaseURL+"map");
43 mapRequest.data = mapVars;
45 sendLoadRequest(mapRequest);
48 override public function loadEntityByID(type:String, id:Number):void {
49 var url:String=apiBaseURL + type + "/" + id;
50 if (type=='way') url+="/full";
51 sendLoadRequest(new URLRequest(url));
54 private function sendLoadRequest(request:URLRequest):void {
55 var mapLoader:URLLoader = new URLLoader();
56 mapLoader.addEventListener(Event.COMPLETE, loadedMap);
57 mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
58 mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
59 request.requestHeaders.push(new URLRequestHeader("X-Error-Format", "XML"));
60 mapLoader.load(request);
61 dispatchEvent(new Event(LOAD_STARTED));
64 private function errorOnMapLoad(event:Event):void {
65 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
66 dispatchEvent(new Event(LOAD_COMPLETED));
68 private function mapLoadStatus(event:HTTPStatusEvent):void {
69 trace("loading map status = "+event.status);
72 protected var appID:OAuthConsumer;
73 protected var authToken:OAuthToken;
75 override public function setAuthToken(id:Object):void {
76 authToken = OAuthToken(id);
79 override public function hasAccessToken():Boolean {
80 return !(getAccessToken() == null);
83 override public function setAccessToken(key:String, secret:String):void {
85 authToken = new OAuthToken(key, secret);
89 /* Get the stored access token, or try setting it up from loader params */
90 private function getAccessToken():OAuthToken {
91 if (authToken == null) {
92 var key:String = getParam("oauth_token", null);
93 var secret:String = getParam("oauth_token_secret", null);
95 if ( key != null && secret != null ) {
96 authToken = new OAuthToken(key, secret);
102 private function getConsumer():OAuthConsumer {
104 var key:String = getParam("oauth_consumer_key", null);
105 var secret:String = getParam("oauth_consumer_secret", null);
107 if ( key != null && secret != null ) {
108 appID = new OAuthConsumer(key, secret);
114 private var httpStatus:int = 0;
116 private function recordStatus(event:HTTPStatusEvent):void {
117 httpStatus = event.status;
120 private var lastUploadedChangesetTags:Object;
122 override public function createChangeset(tags:Object):void {
123 lastUploadedChangesetTags = tags;
125 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
126 var changeset:XML = <changeset />;
127 for (var tagKey:Object in tags) {
128 var tagXML:XML = <tag/>;
130 tagXML.@v = tags[tagKey];
131 changesetXML.changeset.appendChild(tagXML);
134 sendOAuthPut(apiBaseURL+"changeset/create",
136 changesetCreateComplete, changesetCreateError, recordStatus);
139 private function changesetCreateComplete(event:Event):void {
140 // response should be a Number changeset id
141 var id:Number = Number(URLLoader(event.target).data);
143 // which means we now have a new changeset!
144 setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
147 private function changesetCreateError(event:IOErrorEvent):void {
148 dispatchEvent(new Event(NEW_CHANGESET_ERROR));
151 override public function closeChangeset():void {
152 var cs:Changeset = getActiveChangeset();
155 sendOAuthPut(apiBaseURL+"changeset/"+cs.id+"/close",
157 changesetCloseComplete, changesetCloseError, recordStatus);
158 closeActiveChangeset();
161 private function changesetCloseComplete(event:Event):void {
162 dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
164 private function changesetCloseError(event:Event):void {
165 dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
168 private function signedOAuthURL(url:String, method:String):String {
169 // method should be PUT, GET, POST or DELETE
170 var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
171 var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
172 var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
173 return String(urlStr);
176 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
178 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
179 urlReq.method = "POST";
180 if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
181 urlReq.contentType = "application/xml";
182 urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"),
183 new URLRequestHeader("X-Error-Format", "XML") ];
184 var loader:URLLoader = new URLLoader();
185 loader.addEventListener(Event.COMPLETE, onComplete);
186 loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
187 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
191 private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
192 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
193 urlReq.method = "GET";
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);
201 /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
202 (We don't mind what's returned as long as it implements .toString() ) */
204 override public function uploadChanges():* {
205 var changeset:Changeset = getActiveChangeset();
206 var upload:XML = <osmChange version="0.6"/>
207 upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
208 upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
209 upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
210 upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
211 upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
212 upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
213 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
214 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
215 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
216 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
217 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
218 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
220 // now actually upload them
221 // make an OAuth query
222 var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
224 // build the actual request
225 var serv:HTTPService=new HTTPService();
227 serv.url=signedOAuthURL(url, "POST");
228 serv.contentType = "text/xml";
229 serv.headers={'X-Error-Format':'xml'};
231 serv.resultFormat="e4x";
232 serv.requestTimeout=0;
233 serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
234 serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
237 dispatchEvent(new Event(SAVE_STARTED));
241 private function diffUploadComplete(event:ResultEvent):void {
242 var results:XML = XML(event.result);
244 // was it an error document?
245 if (results.name().localName=='osmError') {
246 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
247 diffUploadAPIError(results.status, results.message);
251 // response should be XML describing the progress
253 for each( var update:XML in results.child("*") ) {
254 var oldID:Number = Number(update.@old_id);
255 var newID:Number = Number(update.@new_id);
256 var version:uint = uint(update.@new_version);
257 var type:String = update.name();
261 if (type == "node" ) { killNode(oldID); }
262 else if (type == "way" ) { killWay(oldID); }
263 else if (type == "relation") { killRelation(oldID); }
267 if (type == "node" ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
268 else if (type == "way" ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
269 else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
273 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
274 freshenActiveChangeset();
275 markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
276 MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
279 private function diffUploadIOError(event:FaultEvent):void {
281 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
282 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
285 private function diffUploadAPIError(status:String, message:String):void {
290 if (message.match(/changeset/i)) { throwChangesetError(message); return; }
291 matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
292 if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
296 matches=message.match(/The (\w+) with the id (\d+)/i);
297 if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
300 case '412 Precondition Failed':
301 matches=message.match(/Node (\d+) is still used/i);
302 if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
303 matches=message.match(/relation (\d+) is used/i);
304 if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
305 matches=message.match(/Way (\d+) still used/i);
306 if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
307 matches=message.match(/Cannot update (\w+) (\d+)/i);
308 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
309 matches=message.match(/Relation with id (\d+)/i);
310 if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
311 matches=message.match(/Way (\d+) requires the nodes/i);
312 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
313 throwBugError(message); return;
315 case '404 Not Found':
316 throwBugError(message); return;
318 case '400 Bad Request':
319 matches=message.match(/Element (\w+)\/(\d+)/i);
320 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
321 matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
322 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
323 throwBugError(message); return;
326 // Not caught, so just throw a generic server error
327 throwServerError(message);
330 private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
331 var create:XML = <create version="0.6"/>
332 for each( var id:Number in getIDs() ) {
333 var entity:Entity = get(id);
334 if ( id >= 0 || entity.deleted )
337 var xml:XML = serialise(entity);
338 xml.@changeset = changeset.id;
339 create.appendChild(xml);
341 return create.hasComplexContent() ? create : <!-- blank create section -->;
344 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
345 var del:XML = <delete version="0.6"/>
346 if (ifUnused) del.@["if-unused"] = "true";
347 for each( var id:Number in getIDs() ) {
348 var entity:Entity = get(id);
349 // creates are already included
350 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
353 var xml:XML = serialise(entity);
354 xml.@changeset = changeset.id;
355 del.appendChild(xml);
357 return del.hasComplexContent() ? del : <!-- blank delete section -->;
360 private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
361 var modify:XML = <modify version="0.6"/>
362 for each( var id:Number in getIDs() ) {
363 var entity:Entity = get(id);
364 // creates and deletes are already included
365 if ( id < 0 || entity.deleted || !entity.isDirty )
368 var xml:XML = serialise(entity);
369 xml.@changeset = changeset.id;
370 modify.appendChild(xml);
372 return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
375 private function serialiseNode(node:Node):XML {
376 var xml:XML = serialiseEntityRoot(node); //<node/>
377 serialiseEntityTags(node, xml);
383 private function serialiseWay(way:Way):XML {
384 var xml:XML = serialiseEntityRoot(way); //<node/>
385 serialiseEntityTags(way, xml);
386 for ( var i:uint = 0; i < way.length; i++ ) {
388 nd.@ref = way.getNode(i).id;
394 private function serialiseRelation(relation:Relation):XML {
395 var xml:XML = serialiseEntityRoot(relation); //<node/>
396 serialiseEntityTags(relation, xml);
397 for ( var i:uint = 0; i < relation.length; i++ ) {
398 var relMember:RelationMember = relation.getMember(i);
399 var member:XML = <member/>
400 member.@ref = relMember.entity.id;
401 member.@type = relMember.entity.getType();
402 member.@role = relMember.role;
403 xml.appendChild(member);
408 private function serialiseEntityRoot(entity:Object):XML {
410 if (entity is Way ) { xml = <way/> }
411 else if (entity is Node ) { xml = <node/> }
412 else if (entity is Relation) { xml = <relation/> }
414 xml.@version = entity.version;
418 private function serialiseEntityTags(entity:Entity, xml:XML):void {
420 xml.@version = entity.version;
421 for each( var tag:Tag in entity.getTagArray() ) {
422 if (tag.key == 'created_by') {
423 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
426 var tagXML:XML = <tag/>
428 tagXML.@v = tag.value;
429 xml.appendChild(tagXML);
433 override public function fetchUserTraces(refresh:Boolean=false):void {
434 if (traces_loaded && !refresh) {
435 dispatchEvent(new Event(TRACES_LOADED));
437 sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
438 dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
442 private function tracesLoadComplete(event:Event):void {
443 var files:XML = new XML(URLLoader(event.target).data);
444 for each(var traceData:XML in files.gpx_file) {
445 var t:Trace = findTrace(traceData.@id);
446 if (!t) { t=new Trace(this); addTrace(t); }
447 t.fromXML(traceData);
449 traces_loaded = true;
450 dispatchEvent(new Event(LOAD_COMPLETED));
451 dispatchEvent(new Event(TRACES_LOADED));
454 override public function fetchTrace(id:Number, callback:Function):void {
455 sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml",
456 function(e:Event):void {
457 dispatchEvent(new Event(LOAD_COMPLETED));
459 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
460 dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?