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 (Connection.policyURL!='')
23 Security.loadPolicyFile(Connection.policyURL);
24 var oauthPolicy:String = Connection.getParam("oauth_policy", "");
25 if ( oauthPolicy != "" ) {
26 Security.loadPolicyFile(oauthPolicy);
30 override public function loadBbox(left:Number,right:Number,
31 top:Number,bottom:Number):void {
32 purgeIfFull(left,right,top,bottom);
33 if (isBboxLoaded(left,right,top,bottom)) return;
35 // enlarge bbox by 20% on each edge
36 var xmargin:Number=(right-left)/5;
37 var ymargin:Number=(top-bottom)/5;
38 left-=xmargin; right+=xmargin;
39 bottom-=ymargin; top+=ymargin;
41 var mapVars:URLVariables = new URLVariables();
42 mapVars.bbox= left+","+bottom+","+right+","+top;
44 var mapRequest:URLRequest = new URLRequest(Connection.apiBaseURL+"map");
45 mapRequest.data = mapVars;
47 sendLoadRequest(mapRequest);
50 override public function loadEntityByID(type:String, id:Number):void {
51 var url:String=Connection.apiBaseURL + type + "/" + id;
52 if (type=='way') url+="/full";
53 sendLoadRequest(new URLRequest(url));
56 private function sendLoadRequest(request:URLRequest):void {
57 var mapLoader:URLLoader = new URLLoader();
58 mapLoader.addEventListener(Event.COMPLETE, loadedMap);
59 mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
60 mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
61 mapLoader.load(request);
62 dispatchEvent(new Event(LOAD_STARTED));
65 private function errorOnMapLoad(event:Event):void {
66 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
67 dispatchEvent(new Event(LOAD_COMPLETED));
69 private function mapLoadStatus(event:HTTPStatusEvent):void {
70 trace("loading map status = "+event.status);
73 protected var appID:OAuthConsumer;
74 protected var authToken:OAuthToken;
76 override public function setAuthToken(id:Object):void {
77 authToken = OAuthToken(id);
80 override public function hasAccessToken():Boolean {
81 return !(getAccessToken() == null);
84 override public function setAccessToken(key:String, secret:String):void {
86 authToken = new OAuthToken(key, secret);
90 /* Get the stored access token, or try setting it up from loader params */
91 private function getAccessToken():OAuthToken {
92 if (authToken == null) {
93 var key:String = getParam("oauth_token", null);
94 var secret:String = getParam("oauth_token_secret", null);
96 if ( key != null && secret != null ) {
97 authToken = new OAuthToken(key, secret);
103 private function getConsumer():OAuthConsumer {
105 var key:String = getParam("oauth_consumer_key", null);
106 var secret:String = getParam("oauth_consumer_secret", null);
108 if ( key != null && secret != null ) {
109 appID = new OAuthConsumer(key, secret);
115 private var httpStatus:int = 0;
117 private function recordStatus(event:HTTPStatusEvent):void {
118 httpStatus = event.status;
121 private var lastUploadedChangesetTags:Object;
123 override public function createChangeset(tags:Object):void {
124 lastUploadedChangesetTags = tags;
126 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
127 var changeset:XML = <changeset />;
128 for (var tagKey:Object in tags) {
129 var tagXML:XML = <tag/>;
131 tagXML.@v = tags[tagKey];
132 changesetXML.changeset.appendChild(tagXML);
135 sendOAuthPut(Connection.apiBaseURL+"changeset/create",
137 changesetCreateComplete, changesetCreateError, recordStatus);
140 private function changesetCreateComplete(event:Event):void {
141 // response should be a Number changeset id
142 var id:Number = Number(URLLoader(event.target).data);
144 // which means we now have a new changeset!
145 setActiveChangeset(new Changeset(id, lastUploadedChangesetTags));
148 private function changesetCreateError(event:IOErrorEvent):void {
149 dispatchEvent(new Event(NEW_CHANGESET_ERROR));
152 override public function closeChangeset():void {
153 var cs:Changeset = getActiveChangeset();
156 sendOAuthPut(Connection.apiBaseURL+"changeset/"+cs.id+"/close",
158 changesetCloseComplete, changesetCloseError, recordStatus);
159 closeActiveChangeset();
162 private function changesetCloseComplete(event:Event):void {
163 dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
165 private function changesetCloseError(event:Event):void {
166 dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
169 private function signedOAuthURL(url:String, method:String):String {
170 // method should be PUT, GET, POST or DELETE
171 var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
172 var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
173 var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
174 return String(urlStr);
177 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
179 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
180 urlReq.method = "POST";
181 if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
182 urlReq.contentType = "application/xml";
183 urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"),
184 new URLRequestHeader("X-Error-Format", "XML") ];
185 var loader:URLLoader = new URLLoader();
186 loader.addEventListener(Event.COMPLETE, onComplete);
187 loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
188 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
192 private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
193 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
194 urlReq.method = "GET";
195 var loader:URLLoader = new URLLoader();
196 loader.addEventListener(Event.COMPLETE, onComplete);
197 loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
198 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
202 override public function uploadChanges():void {
203 var changeset:Changeset = getActiveChangeset();
204 var upload:XML = <osmChange version="0.6"/>
205 upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
206 upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
207 upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
208 upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
209 upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
210 upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
211 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
212 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
213 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
214 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
215 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
216 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
218 // now actually upload them
219 // make an OAuth query
220 var url:String = Connection.apiBaseURL+"changeset/" + changeset.id + "/upload";
222 // build the actual request
223 var serv:HTTPService=new HTTPService();
225 serv.url=signedOAuthURL(url, "POST");
226 serv.contentType = "text/xml";
227 serv.headers={'X-Error-Format':'xml'};
229 serv.resultFormat="e4x";
230 serv.requestTimeout=0;
231 serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
232 serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
235 dispatchEvent(new Event(SAVE_STARTED));
238 private function diffUploadComplete(event:ResultEvent):void {
239 var results:XML = XML(event.result);
241 // was it an error document?
242 if (results.name().localName=='osmError') {
243 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
244 diffUploadAPIError(results.status, results.message);
248 // response should be XML describing the progress
250 for each( var update:XML in results.child("*") ) {
251 var oldID:Number = Number(update.@old_id);
252 var newID:Number = Number(update.@new_id);
253 var version:uint = uint(update.@new_version);
254 var type:String = update.name();
258 if (type == "node" ) { killNode(oldID); }
259 else if (type == "way" ) { killWay(oldID); }
260 else if (type == "relation") { killRelation(oldID); }
264 if (type == "node" ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
265 else if (type == "way" ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
266 else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
270 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
271 freshenActiveChangeset();
272 markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
273 MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
276 private function diffUploadIOError(event:FaultEvent):void {
278 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
279 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
282 private function diffUploadAPIError(status:String, message:String):void {
287 if (message.match(/changeset/i)) { throwChangesetError(message); return; }
288 matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
289 if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
293 matches=message.match(/The (\w+) with the id (\d+)/i);
294 if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
297 case '412 Precondition Failed':
298 matches=message.match(/Node (\d+) is still used/i);
299 if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
300 matches=message.match(/relation (\d+) is used/i);
301 if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
302 matches=message.match(/Way (\d+) still used/i);
303 if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
304 matches=message.match(/Cannot update (\w+) (\d+)/i);
305 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
306 matches=message.match(/Relation with id (\d+)/i);
307 if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
308 matches=message.match(/Way (\d+) requires the nodes/i);
309 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
310 throwBugError(message); return;
312 case '404 Not Found':
313 throwBugError(message); return;
315 case '400 Bad Request':
316 matches=message.match(/Element (\w+)\/(\d+)/i);
317 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
318 matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
319 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
320 throwBugError(message); return;
323 // Not caught, so just throw a generic server error
324 throwServerError(message);
327 private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
328 var create:XML = <create version="0.6"/>
329 for each( var id:Number in getIDs() ) {
330 var entity:Entity = get(id);
331 if ( id >= 0 || entity.deleted )
334 var xml:XML = serialise(entity);
335 xml.@changeset = changeset.id;
336 create.appendChild(xml);
338 return create.hasComplexContent() ? create : <!-- blank create section -->;
341 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
342 var del:XML = <delete version="0.6"/>
343 if (ifUnused) del.@["if-unused"] = "true";
344 for each( var id:Number in getIDs() ) {
345 var entity:Entity = get(id);
346 // creates are already included
347 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
350 var xml:XML = serialise(entity);
351 xml.@changeset = changeset.id;
352 del.appendChild(xml);
354 return del.hasComplexContent() ? del : <!-- blank delete section -->;
357 private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
358 var modify:XML = <modify version="0.6"/>
359 for each( var id:Number in getIDs() ) {
360 var entity:Entity = get(id);
361 // creates and deletes are already included
362 if ( id < 0 || entity.deleted || !entity.isDirty )
365 var xml:XML = serialise(entity);
366 xml.@changeset = changeset.id;
367 modify.appendChild(xml);
369 return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
372 private function serialiseNode(node:Node):XML {
373 var xml:XML = serialiseEntityRoot(node); //<node/>
374 serialiseEntityTags(node, xml);
380 private function serialiseWay(way:Way):XML {
381 var xml:XML = serialiseEntityRoot(way); //<node/>
382 serialiseEntityTags(way, xml);
383 for ( var i:uint = 0; i < way.length; i++ ) {
385 nd.@ref = way.getNode(i).id;
391 private function serialiseRelation(relation:Relation):XML {
392 var xml:XML = serialiseEntityRoot(relation); //<node/>
393 serialiseEntityTags(relation, xml);
394 for ( var i:uint = 0; i < relation.length; i++ ) {
395 var relMember:RelationMember = relation.getMember(i);
396 var member:XML = <member/>
397 member.@ref = relMember.entity.id;
398 member.@type = relMember.entity.getType();
399 member.@role = relMember.role;
400 xml.appendChild(member);
405 private function serialiseEntityRoot(entity:Object):XML {
407 if (entity is Way ) { xml = <way/> }
408 else if (entity is Node ) { xml = <node/> }
409 else if (entity is Relation) { xml = <relation/> }
411 xml.@version = entity.version;
415 private function serialiseEntityTags(entity:Entity, xml:XML):void {
417 xml.@version = entity.version;
418 for each( var tag:Tag in entity.getTagArray() ) {
419 if (tag.key == 'created_by') {
420 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
423 var tagXML:XML = <tag/>
425 tagXML.@v = tag.value;
426 xml.appendChild(tagXML);
430 override public function fetchUserTraces(refresh:Boolean=false):void {
431 if (traces_loaded && !refresh) {
432 dispatchEvent(new Event(TRACES_LOADED));
434 sendOAuthGet(Connection.apiBaseURL+"user/gpx_files",
435 tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
436 dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
440 private function tracesLoadComplete(event:Event):void {
442 var files:XML = new XML(URLLoader(event.target).data);
443 for each(var traceData:XML in files.gpx_file) {
444 var t:Trace = new Trace().fromXML(traceData);
447 traces_loaded = true;
448 dispatchEvent(new Event(LOAD_COMPLETED));
449 dispatchEvent(new Event(TRACES_LOADED));
452 override public function fetchTrace(id:Number, callback:Function):void {
453 sendOAuthGet(Connection.apiBaseURL+"gpx/"+id+"/data.xml",
454 function(e:Event):void {
455 dispatchEvent(new Event(LOAD_COMPLETED));
457 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
458 dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?