package net.systemeD.halcyon.connection {
import flash.events.*;
-
+ import mx.rpc.http.HTTPService;
+ import mx.rpc.events.*;
import flash.system.Security;
import flash.net.*;
import org.iotashan.oauth.*;
+ import net.systemeD.halcyon.AttentionEvent;
import net.systemeD.halcyon.MapEvent;
/**
* XMLConnection provides all the methods required to connect to a live
* OSM server. See OSMConnection for connecting to a read-only .osm file
+ *
+ * @see OSMConnection
*/
public class XMLConnection extends XMLBaseConnection {
- //public var readConnection:NetConnection;
-
- public function XMLConnection() {
-
- if (Connection.policyURL!='')
- Security.loadPolicyFile(Connection.policyURL);
- var oauthPolicy:String = Connection.getParam("oauth_policy", "");
- if ( oauthPolicy != "" ) {
- Security.loadPolicyFile(oauthPolicy);
- }
+ /**
+ * Create a new XML connection
+ * @param name The name of the connection
+ * @param api The url of the OSM API server, e.g. http://api06.dev.openstreetmap.org/api/0.6/
+ * @param policy The url of the flash crossdomain policy to load,
+ e.g. http://api06.dev.openstreetmap.org/api/crossdomain.xml
+ * @param initparams Any further parameters for the connection, such as the serverName
+ */
+ public function XMLConnection(name:String,api:String,policy:String,initparams:Object) {
+
+ super(name,api,policy,initparams);
+ if (policyURL != "") Security.loadPolicyFile(policyURL);
+
+ var oauthPolicy:String = getParam("oauth_policy", "");
+ if (oauthPolicy != "") Security.loadPolicyFile(oauthPolicy);
}
override public function loadBbox(left:Number,right:Number,
top:Number,bottom:Number):void {
+ purgeIfFull(left,right,top,bottom);
+ if (isBboxLoaded(left,right,top,bottom)) return;
+
+ // enlarge bbox by 20% on each edge
+ var xmargin:Number=(right-left)/5;
+ var ymargin:Number=(top-bottom)/5;
+ left-=xmargin; right+=xmargin;
+ bottom-=ymargin; top+=ymargin;
+
var mapVars:URLVariables = new URLVariables();
mapVars.bbox= left+","+bottom+","+right+","+top;
- var mapRequest:URLRequest = new URLRequest(Connection.apiBaseURL+"map");
+ var mapRequest:URLRequest = new URLRequest(apiBaseURL+"map");
mapRequest.data = mapVars;
- var mapLoader:URLLoader = new URLLoader();
- mapLoader.addEventListener(Event.COMPLETE, loadedMap);
- mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
- mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
- mapLoader.load(mapRequest);
- dispatchEvent(new Event(LOAD_STARTED));
+ sendLoadRequest(mapRequest);
+ }
+
+ override public function loadEntityByID(type:String, id:Number):void {
+ var url:String=apiBaseURL + type + "/" + id;
+ if (type=='way') url+="/full";
+ sendLoadRequest(new URLRequest(url));
+ }
+
+ private function sendLoadRequest(request:URLRequest):void {
+ var mapLoader:URLLoader = new URLLoader();
+ mapLoader.addEventListener(Event.COMPLETE, loadedMap);
+ mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad);
+ mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
+ request.requestHeaders.push(new URLRequestHeader("X-Error-Format", "XML"));
+ mapLoader.load(request);
+ dispatchEvent(new Event(LOAD_STARTED));
}
private function errorOnMapLoad(event:Event):void {
dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't load the map" } ));
+ dispatchEvent(new Event(LOAD_COMPLETED));
}
private function mapLoadStatus(event:HTTPStatusEvent):void {
trace("loading map status = "+event.status);
changesetXML.changeset.appendChild(tagXML);
}
- sendOAuthPut(Connection.apiBaseURL+"changeset/create",
+ sendOAuthPut(apiBaseURL+"changeset/create",
changesetXML,
changesetCreateComplete, changesetCreateError, recordStatus);
}
var id:Number = Number(URLLoader(event.target).data);
// which means we now have a new changeset!
- setActiveChangeset(new Changeset(id, lastUploadedChangesetTags));
+ setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
}
private function changesetCreateError(event:IOErrorEvent):void {
var cs:Changeset = getActiveChangeset();
if (!cs) return;
- sendOAuthPut(Connection.apiBaseURL+"changeset/"+cs.id+"/close",
+ sendOAuthPut(apiBaseURL+"changeset/"+cs.id+"/close",
null,
changesetCloseComplete, changesetCloseError, recordStatus);
closeActiveChangeset();
}
- private function changesetCloseComplete(event:Event):void { }
- private function changesetCloseError(event:Event):void { }
- // ** TODO: when we get little floating warnings, we can send a happy or sad one up
+ private function changesetCloseComplete(event:Event):void {
+ dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
+ }
+ private function changesetCloseError(event:Event):void {
+ dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
+ }
private function signedOAuthURL(url:String, method:String):String {
// method should be PUT, GET, POST or DELETE
urlReq.method = "POST";
if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
urlReq.contentType = "application/xml";
- urlReq.requestHeaders = new Array(new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"));
+ urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"),
+ new URLRequestHeader("X-Error-Format", "XML") ];
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, onComplete);
loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
loader.load(urlReq);
}
- override public function uploadChanges():void {
+ /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
+ (We don't mind what's returned as long as it implements .toString() ) */
+
+ override public function uploadChanges():* {
var changeset:Changeset = getActiveChangeset();
var upload:XML = <osmChange version="0.6"/>
upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
- upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot));
- upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot));
- upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot));
+ upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
+ upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
+ upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
+ upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
+ upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
+ upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
// now actually upload them
// make an OAuth query
-
- var url:String = Connection.apiBaseURL+"changeset/" + changeset.id + "/upload";
+ var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
// build the actual request
- var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "POST"));
- urlReq.method = "POST";
- urlReq.data = upload.toXMLString();
- urlReq.contentType = "text/xml";
- // ** FIXME: change this to whatever header we decide upon
- urlReq.requestHeaders = [new URLRequestHeader("X-Cloak-Errors-As-200","true")];
- var loader:URLLoader = new URLLoader();
- loader.dataFormat = URLLoaderDataFormat.BINARY;
- loader.addEventListener(Event.COMPLETE, diffUploadComplete);
- loader.addEventListener(IOErrorEvent.IO_ERROR, function(event:IOErrorEvent):void { trace(urlReq.data); diffUploadIOError(event); } );
- loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, recordStatus);
- loader.load(urlReq);
+ var serv:HTTPService=new HTTPService();
+ serv.method="POST";
+ serv.url=signedOAuthURL(url, "POST");
+ serv.contentType = "text/xml";
+ serv.headers={'X-Error-Format':'xml'};
+ serv.request=" ";
+ serv.resultFormat="e4x";
+ serv.requestTimeout=0;
+ serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
+ serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
+ serv.send(upload);
- dispatchEvent(new Event(SAVE_STARTED));
+ dispatchEvent(new Event(SAVE_STARTED));
+ return upload;
}
- private function diffUploadComplete(event:Event):void {
- // check if we've received a cloaked error
- // ** FIXME: change this if we start returning errors as XML
- var response:String=URLLoader(event.target).data;
- var matches:Array=response.match(/^ERROR: (.+?): (.+)/);
- if (matches) {
+ private function diffUploadComplete(event:ResultEvent):void {
+ var results:XML = XML(event.result);
+
+ // was it an error document?
+ if (results.name().localName=='osmError') {
dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
- diffUploadAPIError(matches[1],matches[2]);
+ diffUploadAPIError(results.status, results.message);
return;
}
// response should be XML describing the progress
- var results:XML = new XML(response);
for each( var update:XML in results.child("*") ) {
var oldID:Number = Number(update.@old_id);
MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
}
- private function diffUploadIOError(event:IOErrorEvent):void {
- dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+httpStatus+" "+event.text } ));
- dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
- }
+ private function diffUploadIOError(event:FaultEvent):void {
+ trace(event.fault);
+ dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
+ dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
+ }
private function diffUploadAPIError(status:String, message:String):void {
var matches:Array;
switch (status) {
- case 'conflict':
+ case '409 Conflict':
if (message.match(/changeset/i)) { throwChangesetError(message); return; }
- matches=message.match(/mismatch.+had (\d+) of (\w+) (\d+)/i);
- if (matches) { throwConflictError(findEntity(matches[3],matches[2]), Number(matches[1]), message); return; }
+ matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
+ if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
break;
- case 'gone':
+ case '410 Gone':
matches=message.match(/The (\w+) with the id (\d+)/i);
if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
break;
- case 'precondition_failed':
+ case '412 Precondition Failed':
matches=message.match(/Node (\d+) is still used/i);
if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
matches=message.match(/relation (\d+) is used/i);
if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
throwBugError(message); return;
- case 'not_found':
+ case '404 Not Found':
throwBugError(message); return;
- case 'bad_request':
+ case '400 Bad Request':
matches=message.match(/Element (\w+)\/(\d+)/i);
if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
return create.hasComplexContent() ? create : <!-- blank create section -->;
}
- private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
+ private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
var del:XML = <delete version="0.6"/>
+ if (ifUnused) del.@["if-unused"] = "true";
for each( var id:Number in getIDs() ) {
var entity:Entity = get(id);
// creates are already included
- if ( id < 0 || !entity.deleted )
+ if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
continue;
var xml:XML = serialise(entity);
- if (!entity.parentsLoaded) xml.@silent = "true";
xml.@changeset = changeset.id;
del.appendChild(xml);
}
if (traces_loaded && !refresh) {
dispatchEvent(new Event(TRACES_LOADED));
} else {
- sendOAuthGet(Connection.apiBaseURL+"user/gpx_files",
- tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
+ sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
}
}
- private function tracesLoadComplete(event:Event):void {
- clearTraces();
- var files:XML = new XML(URLLoader(event.target).data);
- for each(var traceData:XML in files.gpx_file) {
- var t:Trace = new Trace().fromXML(traceData);
- addTrace(t);
- }
- traces_loaded = true;
- dispatchEvent(new Event(LOAD_COMPLETED));
- dispatchEvent(new Event(TRACES_LOADED));
- }
+ private function tracesLoadComplete(event:Event):void {
+ var files:XML = new XML(URLLoader(event.target).data);
+ for each(var traceData:XML in files.gpx_file) {
+ var t:Trace = findTrace(traceData.@id);
+ if (!t) { t=new Trace(this); addTrace(t); }
+ t.fromXML(traceData);
+ }
+ traces_loaded = true;
+ dispatchEvent(new Event(LOAD_COMPLETED));
+ dispatchEvent(new Event(TRACES_LOADED));
+ }
override public function fetchTrace(id:Number, callback:Function):void {
- sendOAuthGet(Connection.apiBaseURL+"gpx/"+id+"/data.xml",
+ sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml",
function(e:Event):void {
dispatchEvent(new Event(LOAD_COMPLETED));
callback(e);