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 */ public class XMLConnection extends XMLBaseConnection { public function XMLConnection() { if (Connection.policyURL!='') Security.loadPolicyFile(Connection.policyURL); var oauthPolicy:String = Connection.getParam("oauth_policy", ""); if ( oauthPolicy != "" ) { Security.loadPolicyFile(oauthPolicy); } } override public function loadBbox(left:Number,right:Number, top:Number,bottom:Number):void { var mapVars:URLVariables = new URLVariables(); mapVars.bbox= left+","+bottom+","+right+","+top; var mapRequest:URLRequest = new URLRequest(Connection.apiBaseURL+"map"); mapRequest.data = mapVars; sendLoadRequest(mapRequest); } override public function loadEntity(entity:Entity):void { var url:String=Connection.apiBaseURL + entity.getType() + "/" + entity.id; if (entity is Relation || entity is 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); 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); } protected var appID:OAuthConsumer; protected var authToken:OAuthToken; override public function setAuthToken(id:Object):void { authToken = OAuthToken(id); } override public function hasAccessToken():Boolean { return !(getAccessToken() == null); } override public function setAccessToken(key:String, secret:String):void { if (key && secret) { authToken = new OAuthToken(key, secret); } } /* Get the stored access token, or try setting it up from loader params */ private function getAccessToken():OAuthToken { if (authToken == null) { var key:String = getParam("oauth_token", null); var secret:String = getParam("oauth_token_secret", null); if ( key != null && secret != null ) { authToken = new OAuthToken(key, secret); } } return authToken; } private function getConsumer():OAuthConsumer { if (appID == null) { var key:String = getParam("oauth_consumer_key", null); var secret:String = getParam("oauth_consumer_secret", null); if ( key != null && secret != null ) { appID = new OAuthConsumer(key, secret); } } return appID; } private var httpStatus:int = 0; private function recordStatus(event:HTTPStatusEvent):void { httpStatus = event.status; } private var lastUploadedChangesetTags:Object; override public function createChangeset(tags:Object):void { lastUploadedChangesetTags = tags; var changesetXML:XML = ; var changeset:XML = ; for (var tagKey:Object in tags) { var tagXML:XML = ; tagXML.@k = tagKey; tagXML.@v = tags[tagKey]; changesetXML.changeset.appendChild(tagXML); } sendOAuthPut(Connection.apiBaseURL+"changeset/create", changesetXML, changesetCreateComplete, changesetCreateError, recordStatus); } private function changesetCreateComplete(event:Event):void { // response should be a Number changeset id var id:Number = Number(URLLoader(event.target).data); // which means we now have a new changeset! setActiveChangeset(new Changeset(id, lastUploadedChangesetTags)); } private function changesetCreateError(event:IOErrorEvent):void { dispatchEvent(new Event(NEW_CHANGESET_ERROR)); } override public function closeChangeset():void { var cs:Changeset = getActiveChangeset(); if (!cs) return; sendOAuthPut(Connection.apiBaseURL+"changeset/"+cs.id+"/close", null, changesetCloseComplete, changesetCloseError, recordStatus); closeActiveChangeset(); } 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 var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1(); var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken); var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING); return String(urlStr); } private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void { // build the request var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT")); 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")); var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, onComplete); loader.addEventListener(IOErrorEvent.IO_ERROR, onError); loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus); loader.load(urlReq); } private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void { var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET")); urlReq.method = "GET"; var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, onComplete); loader.addEventListener(IOErrorEvent.IO_ERROR, onError); loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus); loader.load(urlReq); } override public function uploadChanges():void { var changeset:Changeset = getActiveChangeset(); var upload:XML = upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode)); upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay)); upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation)); 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, 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"; // build the actual request 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)); } 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(results.status, results.message); return; } // response should be XML describing the progress for each( var update:XML in results.child("*") ) { var oldID:Number = Number(update.@old_id); var newID:Number = Number(update.@new_id); var version:uint = uint(update.@new_version); var type:String = update.name(); if (newID==0) { // delete if (type == "node" ) { killNode(oldID); } else if (type == "way" ) { killWay(oldID); } else if (type == "relation") { killRelation(oldID); } } else { // create/update if (type == "node" ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); } else if (type == "way" ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); } else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); } } } dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true)); freshenActiveChangeset(); markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack } 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 '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[2],matches[3]), Number(matches[1]), message); return; } break; 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 '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) { throwInUseError(findEntity('Relation',matches[1]), message); return; } matches=message.match(/Way (\d+) still used/i); if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; } matches=message.match(/Cannot update (\w+) (\d+)/i); if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; } matches=message.match(/Relation with id (\d+)/i); if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; } matches=message.match(/Way (\d+) requires the nodes/i); if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; } throwBugError(message); return; case '404 Not Found': throwBugError(message); return; 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); if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; } throwBugError(message); return; } // Not caught, so just throw a generic server error throwServerError(message); } private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML { var create:XML = for each( var id:Number in getIDs() ) { var entity:Entity = get(id); if ( id >= 0 || entity.deleted ) continue; var xml:XML = serialise(entity); xml.@changeset = changeset.id; create.appendChild(xml); } return create.hasComplexContent() ? create : ; } private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML { var del:XML = 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 || entity.parentsLoaded==ifUnused) continue; var xml:XML = serialise(entity); xml.@changeset = changeset.id; del.appendChild(xml); } return del.hasComplexContent() ? del : ; } private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML { var modify:XML = for each( var id:Number in getIDs() ) { var entity:Entity = get(id); // creates and deletes are already included if ( id < 0 || entity.deleted || !entity.isDirty ) continue; var xml:XML = serialise(entity); xml.@changeset = changeset.id; modify.appendChild(xml); } return modify.hasComplexContent() ? modify : ; } private function serialiseNode(node:Node):XML { var xml:XML = serialiseEntityRoot(node); // serialiseEntityTags(node, xml); xml.@lat = node.lat; xml.@lon = node.lon; return xml; } private function serialiseWay(way:Way):XML { var xml:XML = serialiseEntityRoot(way); // serialiseEntityTags(way, xml); for ( var i:uint = 0; i < way.length; i++ ) { var nd:XML = nd.@ref = way.getNode(i).id; xml.appendChild(nd); } return xml; } private function serialiseRelation(relation:Relation):XML { var xml:XML = serialiseEntityRoot(relation); // serialiseEntityTags(relation, xml); for ( var i:uint = 0; i < relation.length; i++ ) { var relMember:RelationMember = relation.getMember(i); var member:XML = member.@ref = relMember.entity.id; member.@type = relMember.entity.getType(); member.@role = relMember.role; xml.appendChild(member); } return xml; } private function serialiseEntityRoot(entity:Object):XML { var xml:XML; if (entity is Way ) { xml = } else if (entity is Node ) { xml = } else if (entity is Relation) { xml = } xml.@id = entity.id; xml.@version = entity.version; return xml; } private function serialiseEntityTags(entity:Entity, xml:XML):void { xml.@id = entity.id; xml.@version = entity.version; for each( var tag:Tag in entity.getTagArray() ) { if (tag.key == 'created_by') { entity.setTag('created_by', null, MainUndoStack.getGlobalStack().addAction); continue; } var tagXML:XML = tagXML.@k = tag.key; tagXML.@v = tag.value; xml.appendChild(tagXML); } } override public function fetchUserTraces(refresh:Boolean=false):void { if (traces_loaded && !refresh) { dispatchEvent(new Event(TRACES_LOADED)); } else { sendOAuthGet(Connection.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)); } override public function fetchTrace(id:Number, callback:Function):void { sendOAuthGet(Connection.apiBaseURL+"gpx/"+id+"/data.xml", function(e:Event):void { dispatchEvent(new Event(LOAD_COMPLETED)); callback(e); }, errorOnMapLoad, mapLoadStatus); // needs error handlers dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable? } } }