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
19 public class XMLConnection extends XMLBaseConnection {
22 * Create a new XML connection
23 * @param name The name of the connection
24 * @param api The url of the OSM API server, e.g. http://api06.dev.openstreetmap.org/api/0.6/
25 * @param policy The url of the flash crossdomain policy to load,
26 e.g. http://api06.dev.openstreetmap.org/api/crossdomain.xml
27 * @param initparams Any further parameters for the connection, such as the serverName
29 public function XMLConnection(name:String,api:String,policy:String,initparams:Object) {
31 super(name,api,policy,initparams);
32 if (policyURL != "") Security.loadPolicyFile(policyURL);
34 var oauthPolicy:String = getParam("oauth_policy", "");
35 if (oauthPolicy != "") Security.loadPolicyFile(oauthPolicy);
38 override public function loadBbox(left:Number,right:Number,
39 top:Number,bottom:Number):void {
40 purgeIfFull(left,right,top,bottom);
41 if (isBboxLoaded(left,right,top,bottom)) return;
43 // enlarge bbox by 20% on each edge
44 var xmargin:Number=(right-left)/5;
45 var ymargin:Number=(top-bottom)/5;
46 left-=xmargin; right+=xmargin;
47 bottom-=ymargin; top+=ymargin;
49 var mapVars:URLVariables = new URLVariables();
50 mapVars.bbox= left+","+bottom+","+right+","+top;
52 var mapRequest:URLRequest = new URLRequest(apiBaseURL+"map");
53 mapRequest.data = mapVars;
55 sendLoadRequest(mapRequest);
58 override public function loadEntityByID(type:String, id:Number):void {
59 var url:String=apiBaseURL + type + "/" + id;
60 if (type=='way') url+="/full";
61 sendLoadRequest(new URLRequest(url));
64 private function sendLoadRequest(request:URLRequest):void {
65 var mapLoader:URLLoader = new URLLoader();
66 mapLoader.addEventListener(Event.COMPLETE, loadedMap);
67 mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
68 mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
69 request.requestHeaders.push(new URLRequestHeader("X-Error-Format", "XML"));
70 mapLoader.load(request);
71 dispatchEvent(new Event(LOAD_STARTED));
74 private function errorOnMapLoad(event:Event):void {
75 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
76 dispatchEvent(new Event(LOAD_COMPLETED));
78 private function mapLoadStatus(event:HTTPStatusEvent):void {
79 trace("loading map status = "+event.status);
82 protected var appID:OAuthConsumer;
83 protected var authToken:OAuthToken;
85 override public function setAuthToken(id:Object):void {
86 authToken = OAuthToken(id);
89 override public function hasAccessToken():Boolean {
90 return !(getAccessToken() == null);
93 override public function setAccessToken(key:String, secret:String):void {
95 authToken = new OAuthToken(key, secret);
99 /* Get the stored access token, or try setting it up from loader params */
100 private function getAccessToken():OAuthToken {
101 if (authToken == null) {
102 var key:String = getParam("oauth_token", null);
103 var secret:String = getParam("oauth_token_secret", null);
105 if ( key != null && secret != null ) {
106 authToken = new OAuthToken(key, secret);
112 private function getConsumer():OAuthConsumer {
114 var key:String = getParam("oauth_consumer_key", null);
115 var secret:String = getParam("oauth_consumer_secret", null);
117 if ( key != null && secret != null ) {
118 appID = new OAuthConsumer(key, secret);
124 private var httpStatus:int = 0;
126 private function recordStatus(event:HTTPStatusEvent):void {
127 httpStatus = event.status;
130 private var lastUploadedChangesetTags:Object;
132 override public function createChangeset(tags:Object):void {
133 lastUploadedChangesetTags = tags;
135 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
136 var changeset:XML = <changeset />;
137 for (var tagKey:Object in tags) {
138 var tagXML:XML = <tag/>;
140 tagXML.@v = tags[tagKey];
141 changesetXML.changeset.appendChild(tagXML);
144 sendOAuthPut(apiBaseURL+"changeset/create",
146 changesetCreateComplete, changesetCreateError, recordStatus);
149 private function changesetCreateComplete(event:Event):void {
150 // response should be a Number changeset id
151 var id:Number = Number(URLLoader(event.target).data);
153 // which means we now have a new changeset!
154 setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
157 private function changesetCreateError(event:IOErrorEvent):void {
158 dispatchEvent(new Event(NEW_CHANGESET_ERROR));
161 override public function closeChangeset():void {
162 var cs:Changeset = getActiveChangeset();
165 sendOAuthPut(apiBaseURL+"changeset/"+cs.id+"/close",
167 changesetCloseComplete, changesetCloseError, recordStatus);
168 closeActiveChangeset();
171 private function changesetCloseComplete(event:Event):void {
172 dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
174 private function changesetCloseError(event:Event):void {
175 dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
178 private function signedOAuthURL(url:String, method:String):String {
179 // method should be PUT, GET, POST or DELETE
180 var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
181 var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
182 var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
183 return String(urlStr);
186 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
188 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
189 urlReq.method = "POST";
190 if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
191 urlReq.contentType = "application/xml";
192 urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"),
193 new URLRequestHeader("X-Error-Format", "XML") ];
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 private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
202 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
203 urlReq.method = "GET";
204 var loader:URLLoader = new URLLoader();
205 loader.addEventListener(Event.COMPLETE, onComplete);
206 loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
207 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
211 /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
212 (We don't mind what's returned as long as it implements .toString() ) */
214 override public function uploadChanges():* {
215 var changeset:Changeset = getActiveChangeset();
216 var upload:XML = <osmChange version="0.6"/>
217 upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
218 upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
219 upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
220 upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
221 upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
222 upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
223 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
224 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
225 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
226 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
227 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
228 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
230 // now actually upload them
231 // make an OAuth query
232 var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
234 // build the actual request
235 var serv:HTTPService=new HTTPService();
237 serv.url=signedOAuthURL(url, "POST");
238 serv.contentType = "text/xml";
239 serv.headers={'X-Error-Format':'xml'};
241 serv.resultFormat="e4x";
242 serv.requestTimeout=0;
243 serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
244 serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
247 dispatchEvent(new Event(SAVE_STARTED));
251 private function diffUploadComplete(event:ResultEvent):void {
252 var results:XML = XML(event.result);
254 // was it an error document?
255 if (results.name().localName=='osmError') {
256 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
257 diffUploadAPIError(results.status, results.message);
261 // response should be XML describing the progress
263 for each( var update:XML in results.child("*") ) {
264 var oldID:Number = Number(update.@old_id);
265 var newID:Number = Number(update.@new_id);
266 var version:uint = uint(update.@new_version);
267 var type:String = update.name();
271 if (type == "node" ) { killNode(oldID); }
272 else if (type == "way" ) { killWay(oldID); }
273 else if (type == "relation") { killRelation(oldID); }
277 if (type == "node" ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
278 else if (type == "way" ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
279 else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
283 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
284 freshenActiveChangeset();
285 markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
286 MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
289 private function diffUploadIOError(event:FaultEvent):void {
291 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
292 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
295 private function diffUploadAPIError(status:String, message:String):void {
300 if (message.match(/changeset/i)) { throwChangesetError(message); return; }
301 matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
302 if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
306 matches=message.match(/The (\w+) with the id (\d+)/i);
307 if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
310 case '412 Precondition Failed':
311 matches=message.match(/Node (\d+) is still used/i);
312 if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
313 matches=message.match(/relation (\d+) is used/i);
314 if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
315 matches=message.match(/Way (\d+) still used/i);
316 if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
317 matches=message.match(/Cannot update (\w+) (\d+)/i);
318 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
319 matches=message.match(/Relation with id (\d+)/i);
320 if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
321 matches=message.match(/Way (\d+) requires the nodes/i);
322 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
323 throwBugError(message); return;
325 case '404 Not Found':
326 throwBugError(message); return;
328 case '400 Bad Request':
329 matches=message.match(/Element (\w+)\/(\d+)/i);
330 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
331 matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
332 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
333 throwBugError(message); return;
336 // Not caught, so just throw a generic server error
337 throwServerError(message);
340 private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
341 var create:XML = <create version="0.6"/>
342 for each( var id:Number in getIDs() ) {
343 var entity:Entity = get(id);
344 if ( id >= 0 || entity.deleted )
347 var xml:XML = serialise(entity);
348 xml.@changeset = changeset.id;
349 create.appendChild(xml);
351 return create.hasComplexContent() ? create : <!-- blank create section -->;
354 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
355 var del:XML = <delete version="0.6"/>
356 if (ifUnused) del.@["if-unused"] = "true";
357 for each( var id:Number in getIDs() ) {
358 var entity:Entity = get(id);
359 // creates are already included
360 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
363 var xml:XML = serialise(entity);
364 xml.@changeset = changeset.id;
365 del.appendChild(xml);
367 return del.hasComplexContent() ? del : <!-- blank delete section -->;
370 private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
371 var modify:XML = <modify version="0.6"/>
372 for each( var id:Number in getIDs() ) {
373 var entity:Entity = get(id);
374 // creates and deletes are already included
375 if ( id < 0 || entity.deleted || !entity.isDirty )
378 var xml:XML = serialise(entity);
379 xml.@changeset = changeset.id;
380 modify.appendChild(xml);
382 return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
385 private function serialiseNode(node:Node):XML {
386 var xml:XML = serialiseEntityRoot(node); //<node/>
387 serialiseEntityTags(node, xml);
393 private function serialiseWay(way:Way):XML {
394 var xml:XML = serialiseEntityRoot(way); //<node/>
395 serialiseEntityTags(way, xml);
396 for ( var i:uint = 0; i < way.length; i++ ) {
398 nd.@ref = way.getNode(i).id;
404 private function serialiseRelation(relation:Relation):XML {
405 var xml:XML = serialiseEntityRoot(relation); //<node/>
406 serialiseEntityTags(relation, xml);
407 for ( var i:uint = 0; i < relation.length; i++ ) {
408 var relMember:RelationMember = relation.getMember(i);
409 var member:XML = <member/>
410 member.@ref = relMember.entity.id;
411 member.@type = relMember.entity.getType();
412 member.@role = relMember.role;
413 xml.appendChild(member);
418 private function serialiseEntityRoot(entity:Object):XML {
420 if (entity is Way ) { xml = <way/> }
421 else if (entity is Node ) { xml = <node/> }
422 else if (entity is Relation) { xml = <relation/> }
424 xml.@version = entity.version;
428 private function serialiseEntityTags(entity:Entity, xml:XML):void {
430 xml.@version = entity.version;
431 for each( var tag:Tag in entity.getTagArray() ) {
432 if (tag.key == 'created_by') {
433 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
436 var tagXML:XML = <tag/>
438 tagXML.@v = tag.value;
439 xml.appendChild(tagXML);
443 override public function fetchUserTraces(refresh:Boolean=false):void {
444 if (traces_loaded && !refresh) {
445 dispatchEvent(new Event(TRACES_LOADED));
447 sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
448 dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
452 private function tracesLoadComplete(event:Event):void {
453 var files:XML = new XML(URLLoader(event.target).data);
454 for each(var traceData:XML in files.gpx_file) {
455 var t:Trace = findTrace(traceData.@id);
456 if (!t) { t=new Trace(this); addTrace(t); }
457 t.fromXML(traceData);
459 traces_loaded = true;
460 dispatchEvent(new Event(LOAD_COMPLETED));
461 dispatchEvent(new Event(TRACES_LOADED));
464 override public function fetchTrace(id:Number, callback:Function):void {
465 sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml",
466 function(e:Event):void {
467 dispatchEvent(new Event(LOAD_COMPLETED));
469 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
470 dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?