1 package net.systemeD.halcyon.connection {
5 import flash.system.Security;
7 import org.iotashan.oauth.*;
9 import net.systemeD.halcyon.MapEvent;
12 * XMLConnection provides all the methods required to connect to a live
13 * OSM server. See OSMConnection for connecting to a read-only .osm file
15 public class XMLConnection extends XMLBaseConnection {
17 //public var readConnection:NetConnection;
19 public function XMLConnection() {
21 if (Connection.policyURL!='')
22 Security.loadPolicyFile(Connection.policyURL);
23 var oauthPolicy:String = Connection.getParam("oauth_policy", "");
24 if ( oauthPolicy != "" ) {
25 Security.loadPolicyFile(oauthPolicy);
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;
34 var mapRequest:URLRequest = new URLRequest(Connection.apiBaseURL+"map");
35 mapRequest.data = mapVars;
37 var mapLoader:URLLoader = new URLLoader();
38 mapLoader.addEventListener(Event.COMPLETE, loadedMap);
39 mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
40 mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
41 mapLoader.load(mapRequest);
42 dispatchEvent(new Event(LOAD_STARTED));
45 private function errorOnMapLoad(event:Event):void {
46 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
48 private function mapLoadStatus(event:HTTPStatusEvent):void {
49 trace("loading map status = "+event.status);
52 protected var appID:OAuthConsumer;
53 protected var authToken:OAuthToken;
55 override public function setAuthToken(id:Object):void {
56 authToken = OAuthToken(id);
59 override public function hasAccessToken():Boolean {
60 return !(getAccessToken() == null);
63 override public function setAccessToken(key:String, secret:String):void {
65 authToken = new OAuthToken(key, secret);
69 /* Get the stored access token, or try setting it up from loader params */
70 private function getAccessToken():OAuthToken {
71 if (authToken == null) {
72 var key:String = getParam("oauth_token", null);
73 var secret:String = getParam("oauth_token_secret", null);
75 if ( key != null && secret != null ) {
76 authToken = new OAuthToken(key, secret);
82 private function getConsumer():OAuthConsumer {
84 var key:String = getParam("oauth_consumer_key", null);
85 var secret:String = getParam("oauth_consumer_secret", null);
87 if ( key != null && secret != null ) {
88 appID = new OAuthConsumer(key, secret);
94 private var httpStatus:int = 0;
96 private function recordStatus(event:HTTPStatusEvent):void {
97 httpStatus = event.status;
100 private var lastUploadedChangesetTags:Object;
102 override public function createChangeset(tags:Object):void {
103 lastUploadedChangesetTags = tags;
105 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
106 var changeset:XML = <changeset />;
107 for (var tagKey:Object in tags) {
108 var tagXML:XML = <tag/>;
110 tagXML.@v = tags[tagKey];
111 changesetXML.changeset.appendChild(tagXML);
114 sendOAuthPut(Connection.apiBaseURL+"changeset/create",
116 changesetCreateComplete, changesetCreateError, recordStatus);
119 private function changesetCreateComplete(event:Event):void {
120 // response should be a Number changeset id
121 var id:Number = Number(URLLoader(event.target).data);
123 // which means we now have a new changeset!
124 setActiveChangeset(new Changeset(id, lastUploadedChangesetTags));
127 private function changesetCreateError(event:IOErrorEvent):void {
128 dispatchEvent(new Event(NEW_CHANGESET_ERROR));
131 override public function closeChangeset():void {
132 var cs:Changeset = getActiveChangeset();
135 sendOAuthPut(Connection.apiBaseURL+"changeset/"+cs.id+"/close",
137 changesetCloseComplete, changesetCloseError, recordStatus);
138 closeActiveChangeset();
141 private function changesetCloseComplete(event:Event):void { }
142 private function changesetCloseError(event:Event):void { }
143 // ** TODO: when we get little floating warnings, we can send a happy or sad one up
145 private function signedOAuthURL(url:String, method:String):String {
146 // method should be PUT, GET, POST or DELETE
147 var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
148 var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
149 var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
150 return String(urlStr);
153 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
155 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
156 urlReq.method = "POST";
157 if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
158 urlReq.contentType = "application/xml";
159 urlReq.requestHeaders = new Array(new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"));
160 var loader:URLLoader = new URLLoader();
161 loader.addEventListener(Event.COMPLETE, onComplete);
162 loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
163 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
167 private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
168 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
169 urlReq.method = "GET";
170 var loader:URLLoader = new URLLoader();
171 loader.addEventListener(Event.COMPLETE, onComplete);
172 loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
173 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
177 override public function uploadChanges():void {
178 var changeset:Changeset = getActiveChangeset();
179 var upload:XML = <osmChange version="0.6"/>
180 upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
181 upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
182 upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
183 upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
184 upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
185 upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
186 upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot));
187 upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot));
188 upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot));
190 // now actually upload them
191 // make an OAuth query
193 var url:String = Connection.apiBaseURL+"changeset/" + changeset.id + "/upload";
195 // build the actual request
196 var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "POST"));
197 urlReq.method = "POST";
198 urlReq.data = upload.toXMLString();
199 urlReq.contentType = "text/xml";
200 // ** FIXME: change this to whatever header we decide upon
201 urlReq.requestHeaders = [new URLRequestHeader("X-Cloak-Errors-As-200","true")];
202 var loader:URLLoader = new URLLoader();
203 loader.dataFormat = URLLoaderDataFormat.BINARY;
204 loader.addEventListener(Event.COMPLETE, diffUploadComplete);
205 loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void { trace(urlReq.data); diffUploadIOError(event); } );
206 loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
209 dispatchEvent(new Event(SAVE_STARTED));
212 private function diffUploadComplete(event:Event):void {
213 // check if we've received a cloaked error
214 // ** FIXME: change this if we start returning errors as XML
215 var response:String=URLLoader(event.target).data;
216 var matches:Array=response.match(/^ERROR: (.+?): (.+)/);
218 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
219 diffUploadAPIError(matches[1],matches[2]);
223 // response should be XML describing the progress
224 var results:XML = new XML(response);
226 for each( var update:XML in results.child("*") ) {
227 var oldID:Number = Number(update.@old_id);
228 var newID:Number = Number(update.@new_id);
229 var version:uint = uint(update.@new_version);
230 var type:String = update.name();
234 if (type == "node" ) { killNode(oldID); }
235 else if (type == "way" ) { killWay(oldID); }
236 else if (type == "relation") { killRelation(oldID); }
240 if (type == "node" ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
241 else if (type == "way" ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
242 else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
246 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
247 freshenActiveChangeset();
248 markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
249 MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
252 private function diffUploadIOError(event:IOErrorEvent):void {
253 dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+httpStatus+" "+event.text } ));
254 dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
257 private function diffUploadAPIError(status:String, message:String):void {
262 if (message.match(/changeset/i)) { throwChangesetError(message); return; }
263 matches=message.match(/mismatch.+had (\d+) of (\w+) (\d+)/i);
264 if (matches) { throwConflictError(findEntity(matches[3],matches[2]), Number(matches[1]), message); return; }
268 matches=message.match(/The (\w+) with the id (\d+)/i);
269 if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
272 case 'precondition_failed':
273 matches=message.match(/Node (\d+) is still used/i);
274 if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
275 matches=message.match(/relation (\d+) is used/i);
276 if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
277 matches=message.match(/Way (\d+) still used/i);
278 if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
279 matches=message.match(/Cannot update (\w+) (\d+)/i);
280 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
281 matches=message.match(/Relation with id (\d+)/i);
282 if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
283 matches=message.match(/Way (\d+) requires the nodes/i);
284 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
285 throwBugError(message); return;
288 throwBugError(message); return;
291 matches=message.match(/Element (\w+)\/(\d+)/i);
292 if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
293 matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
294 if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
295 throwBugError(message); return;
298 // Not caught, so just throw a generic server error
299 throwServerError(message);
302 private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
303 var create:XML = <create version="0.6"/>
304 for each( var id:Number in getIDs() ) {
305 var entity:Entity = get(id);
306 if ( id >= 0 || entity.deleted )
309 var xml:XML = serialise(entity);
310 xml.@changeset = changeset.id;
311 create.appendChild(xml);
313 return create.hasComplexContent() ? create : <!-- blank create section -->;
316 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
317 var del:XML = <delete version="0.6"/>
318 for each( var id:Number in getIDs() ) {
319 var entity:Entity = get(id);
320 // creates are already included
321 if ( id < 0 || !entity.deleted )
324 var xml:XML = serialise(entity);
325 if (!entity.parentsLoaded) xml.@silent = "true";
326 xml.@changeset = changeset.id;
327 del.appendChild(xml);
329 return del.hasComplexContent() ? del : <!-- blank delete section -->;
332 private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
333 var modify:XML = <modify version="0.6"/>
334 for each( var id:Number in getIDs() ) {
335 var entity:Entity = get(id);
336 // creates and deletes are already included
337 if ( id < 0 || entity.deleted || !entity.isDirty )
340 var xml:XML = serialise(entity);
341 xml.@changeset = changeset.id;
342 modify.appendChild(xml);
344 return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
347 private function serialiseNode(node:Node):XML {
348 var xml:XML = serialiseEntityRoot(node); //<node/>
349 serialiseEntityTags(node, xml);
355 private function serialiseWay(way:Way):XML {
356 var xml:XML = serialiseEntityRoot(way); //<node/>
357 serialiseEntityTags(way, xml);
358 for ( var i:uint = 0; i < way.length; i++ ) {
360 nd.@ref = way.getNode(i).id;
366 private function serialiseRelation(relation:Relation):XML {
367 var xml:XML = serialiseEntityRoot(relation); //<node/>
368 serialiseEntityTags(relation, xml);
369 for ( var i:uint = 0; i < relation.length; i++ ) {
370 var relMember:RelationMember = relation.getMember(i);
371 var member:XML = <member/>
372 member.@ref = relMember.entity.id;
373 member.@type = relMember.entity.getType();
374 member.@role = relMember.role;
375 xml.appendChild(member);
380 private function serialiseEntityRoot(entity:Object):XML {
382 if (entity is Way ) { xml = <way/> }
383 else if (entity is Node ) { xml = <node/> }
384 else if (entity is Relation) { xml = <relation/> }
386 xml.@version = entity.version;
390 private function serialiseEntityTags(entity:Entity, xml:XML):void {
392 xml.@version = entity.version;
393 for each( var tag:Tag in entity.getTagArray() ) {
394 if (tag.key == 'created_by') {
395 entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction);
398 var tagXML:XML = <tag/>
400 tagXML.@v = tag.value;
401 xml.appendChild(tagXML);
405 override public function fetchUserTraces(refresh:Boolean=false):void {
406 if (traces_loaded && !refresh) {
407 dispatchEvent(new Event(TRACES_LOADED));
409 sendOAuthGet(Connection.apiBaseURL+"user/gpx_files",
410 tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
411 dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
415 private function tracesLoadComplete(event:Event):void {
417 var files:XML = new XML(URLLoader(event.target).data);
418 for each(var traceData:XML in files.gpx_file) {
419 var t:Trace = new Trace().fromXML(traceData);
422 traces_loaded = true;
423 dispatchEvent(new Event(LOAD_COMPLETED));
424 dispatchEvent(new Event(TRACES_LOADED));
427 override public function fetchTrace(id:Number, callback:Function):void {
428 sendOAuthGet(Connection.apiBaseURL+"gpx/"+id+"/data.xml",
429 function(e:Event):void {
430 dispatchEvent(new Event(LOAD_COMPLETED));
432 }, errorOnMapLoad, mapLoadStatus); // needs error handlers
433 dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?