f3c40e41c4cb1b7e90df1a068553e03e286e769f
[potlatch2.git] / net / systemeD / halcyon / connection / XMLConnection.as
1 package net.systemeD.halcyon.connection {
2
3     import flash.events.*;
4         import mx.rpc.http.HTTPService;
5         import mx.rpc.events.*;
6         import flash.system.Security;
7         import flash.net.*;
8     import org.iotashan.oauth.*;
9
10         import net.systemeD.halcyon.AttentionEvent;
11         import net.systemeD.halcyon.MapEvent;
12         import net.systemeD.halcyon.ExtendedURLLoader;
13     import net.systemeD.halcyon.connection.bboxes.*;
14
15     /**
16     * XMLConnection provides all the methods required to connect to a live
17     * OSM server. See OSMConnection for connecting to a read-only .osm file
18     *
19     * @see OSMConnection
20     */
21         public class XMLConnection extends XMLBaseConnection {
22
23                 private const MARGIN:Number=0.05;
24                 private var discardTags:Array=["created_by",
25                         "tiger:upload_uuid", "tiger:tlid", "tiger:source", "tiger:separated",
26                         "geobase:datasetName", "geobase:uuid", "sub_sea:type",
27                         "odbl", "odbl:note",
28                         "yh:LINE_NAME", "yh:LINE_NUM", "yh:STRUCTURE", "yh:TOTYUMONO",
29                         "yh:TYPE", "yh:WIDTH_RANK","SK53_bulk:load"];
30
31         /**
32         * Create a new XML connection
33         * @param name The name of the connection
34         * @param api The url of the OSM API server, e.g. http://api06.dev.openstreetmap.org/api/0.6/
35         * @param policy The url of the flash crossdomain policy to load,
36                         e.g. http://api06.dev.openstreetmap.org/api/crossdomain.xml
37         * @param initparams Any further parameters for the connection, such as the serverName
38         */
39                 public function XMLConnection(name:String,api:String,policy:String,initparams:Object) {
40
41                         super(name,api,policy,initparams);
42                         if (policyURL != "") Security.loadPolicyFile(policyURL);
43
44             var oauthPolicy:String = getParam("oauth_policy", "");
45             if (oauthPolicy != "") Security.loadPolicyFile(oauthPolicy);
46                 }
47                 
48                 override public function loadBbox(left:Number,right:Number,
49                                                                 top:Number,bottom:Number):void {
50             purgeIfFull(left,right,top,bottom);
51                         var requestBox:Box=new Box().fromBbox(left,bottom,right,top);
52                         var boxes:Array;
53                         try {
54                                 boxes=fetchSet.getBoxes(requestBox,MAX_BBOXES);
55                         } catch(err:Error) {
56                                 boxes=[requestBox];
57                         }
58                         for each (var box:Box in boxes) {
59                                 // enlarge bbox by given margin on each edge
60                                 var xmargin:Number=(box.right-box.left)*MARGIN;
61                                 var ymargin:Number=(box.top-box.bottom)*MARGIN;
62                                 left  =box.left  -xmargin; right=box.right+xmargin;
63                                 bottom=box.bottom-ymargin; top  =box.top  +ymargin;
64
65                                 dispatchEvent(new MapEvent(MapEvent.DOWNLOAD, {minlon:left, maxlon:right, maxlat:top, minlat:bottom} ));
66
67                                 // send HTTP request
68                                 var mapVars:URLVariables = new URLVariables();
69                                 mapVars.bbox=left+","+bottom+","+right+","+top;
70                                 var mapRequest:URLRequest = new URLRequest(apiBaseURL+"map");
71                                 mapRequest.data = mapVars;
72                                 sendLoadRequest(mapRequest);
73                         }
74                 }
75
76                 override public function loadEntityByID(type:String, id:Number):void {
77                         var url:String=apiBaseURL + type + "/" + id;
78                         if (type=='way') url+="/full";
79                         sendLoadRequest(new URLRequest(url));
80                 }
81
82                 private function sendLoadRequest(request:URLRequest):void {
83                         var mapLoader:URLLoader = new URLLoader();
84             var errorHandler:Function = function(event:IOErrorEvent):void {
85                 errorOnMapLoad(event, request);
86             }
87                         mapLoader.addEventListener(Event.COMPLETE, loadedMap);
88                         mapLoader.addEventListener(IOErrorEvent.IO_ERROR, errorHandler);
89                         mapLoader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
90             request.requestHeaders.push(new URLRequestHeader("X-Error-Format", "XML"));
91                         mapLoader.load(request);
92                         dispatchEvent(new Event(LOAD_STARTED));
93                 }
94
95         private function errorOnMapLoad(event:Event, request:URLRequest):void {
96             var url:String = request.url + '?' + URLVariables(request.data).toString(); // for get reqeusts, at least
97             dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "There was a problem loading the map data.\nPlease check your internet connection, or try zooming in.\n\n" + url } ));
98             dispatchEvent(new Event(LOAD_COMPLETED));
99         }
100
101         private function mapLoadStatus(event:HTTPStatusEvent):void {
102         }
103
104         protected var appID:OAuthConsumer;
105         protected var authToken:OAuthToken;
106
107             override public function setAuthToken(id:Object):void {
108                 authToken = OAuthToken(id);
109             }
110
111         override public function hasAccessToken():Boolean {
112             return !(getAccessToken() == null);
113         }
114
115         override public function setAccessToken(key:String, secret:String):void {
116             if (key && secret) {
117               authToken = new OAuthToken(key, secret);
118             }
119         }
120
121         /* Get the stored access token, or try setting it up from loader params */
122         private function getAccessToken():OAuthToken {
123             if (authToken == null) {
124               var key:String = getParam("oauth_token", null);
125               var secret:String = getParam("oauth_token_secret", null);
126
127               if ( key != null && secret != null ) {
128                   authToken = new OAuthToken(key, secret);
129               }
130             }
131             return authToken;
132         }
133
134         private function getConsumer():OAuthConsumer {
135             if (appID == null) {
136               var key:String = getParam("oauth_consumer_key", null);
137               var secret:String = getParam("oauth_consumer_secret", null);
138
139               if ( key != null && secret != null ) {
140                   appID = new OAuthConsumer(key, secret);
141               }
142             }
143             return appID;
144         }
145
146         private var httpStatus:int = 0;
147         
148         private function recordStatus(event:HTTPStatusEvent):void {
149             httpStatus = event.status;
150         }
151         
152         private var lastUploadedChangesetTags:Object;
153         
154         override public function createChangeset(tags:Object):void {
155             lastUploadedChangesetTags = tags;
156             
157                 var changesetXML:XML = <osm version="0.6"><changeset /></osm>;
158                 var changeset:XML = <changeset />;
159                 for (var tagKey:Object in tags) {
160               var tagXML:XML = <tag/>;
161               tagXML.@k = tagKey;
162               tagXML.@v = tags[tagKey];
163               changesetXML.changeset.appendChild(tagXML);
164             }        
165
166                         sendOAuthPut(apiBaseURL+"changeset/create",
167                                                  changesetXML,
168                                                  changesetCreateComplete, changesetCreateError, recordStatus);
169             }
170
171         private function changesetCreateComplete(event:Event):void {
172             var result:String = URLLoader(event.target).data;
173
174             if (result.match(/^^\d+$/)) {
175                 // response should be a Number changeset id
176                 var id:Number = Number(URLLoader(event.target).data);
177             
178                 // which means we now have a new changeset!
179                 setActiveChangeset(new Changeset(this, id, lastUploadedChangesetTags));
180             } else {
181                 var results:XML = XML(result);
182
183                 throwServerError(results.message);
184             }
185         }
186
187         private function changesetCreateError(event:IOErrorEvent):void {
188             dispatchEvent(new Event(NEW_CHANGESET_ERROR));
189         }
190
191                 override public function closeChangeset():void {
192             var cs:Changeset = getActiveChangeset();
193                         if (!cs) return;
194                         
195                         sendOAuthPut(apiBaseURL+"changeset/"+cs.id+"/close",
196                                                  null,
197                                                  changesetCloseComplete, changesetCloseError, recordStatus);
198                         closeActiveChangeset();
199                 }
200                 
201                 private function changesetCloseComplete(event:Event):void { 
202                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Changeset closed"));
203                 }
204                 private function changesetCloseError(event:Event):void { 
205                         dispatchEvent(new AttentionEvent(AttentionEvent.ALERT, null, "Couldn't close changeset", 1));
206                 }
207
208         private function signedOAuthURL(url:String, method:String):String {
209             // method should be PUT, GET, POST or DELETE
210             var sig:IOAuthSignatureMethod = new OAuthSignatureMethod_HMAC_SHA1();
211             var oauthRequest:OAuthRequest = new OAuthRequest(method, url, null, getConsumer(), authToken);
212             var urlStr:Object = oauthRequest.buildRequest(sig, OAuthRequest.RESULT_TYPE_URL_STRING);
213             return String(urlStr);
214         }
215
216                 private function sendOAuthPut(url:String, xml:XML, onComplete:Function, onError:Function, onStatus:Function):void {
217             // build the request
218             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "PUT"));
219             urlReq.method = "POST";
220                         if (xml) { urlReq.data = xml.toXMLString(); } else { urlReq.data = true; }
221             urlReq.contentType = "application/xml";
222             urlReq.requestHeaders = [ new URLRequestHeader("X_HTTP_METHOD_OVERRIDE", "PUT"), 
223                                                   new URLRequestHeader("X-Error-Format", "XML") ];
224             var loader:URLLoader = new URLLoader();
225             loader.addEventListener(Event.COMPLETE, onComplete);
226             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
227             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
228                 loader.load(urlReq);
229                 }
230
231         private function sendOAuthGet(url:String, onComplete:Function, onError:Function, onStatus:Function):void {
232             var urlReq:URLRequest = new URLRequest(signedOAuthURL(url, "GET"));
233             urlReq.method = "GET";
234             var loader:URLLoader = new URLLoader();
235             loader.addEventListener(Event.COMPLETE, onComplete);
236             loader.addEventListener(IOErrorEvent.IO_ERROR, onError);
237             loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, onStatus);
238             loader.load(urlReq);
239         }
240
241                 /** Create XML changeset and send it to the server. Returns the XML string for use in the 'Show data' button.
242                     (We don't mind what's returned as long as it implements .toString() ) */
243
244         override public function uploadChanges():* {
245             var changeset:Changeset = getActiveChangeset();
246             var upload:XML = <osmChange version="0.6"/>
247             upload.appendChild(addCreated(changeset, getAllNodeIDs, getNode, serialiseNode));
248             upload.appendChild(addCreated(changeset, getAllWayIDs, getWay, serialiseWay));
249             upload.appendChild(addCreated(changeset, getAllRelationIDs, getRelation, serialiseRelation));
250             upload.appendChild(addModified(changeset, getAllNodeIDs, getNode, serialiseNode));
251             upload.appendChild(addModified(changeset, getAllWayIDs, getWay, serialiseWay));
252             upload.appendChild(addModified(changeset, getAllRelationIDs, getRelation, serialiseRelation));
253             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, false));
254             upload.appendChild(addDeleted(changeset, getAllRelationIDs, getRelation, serialiseEntityRoot, true));
255             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, false));
256             upload.appendChild(addDeleted(changeset, getAllWayIDs, getWay, serialiseEntityRoot, true));
257             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, false));
258             upload.appendChild(addDeleted(changeset, getAllNodeIDs, getNode, serialiseEntityRoot, true));
259
260             // now actually upload them
261             // make an OAuth query
262             var url:String = apiBaseURL+"changeset/" + changeset.id + "/upload";
263
264             // build the actual request
265                         var serv:HTTPService=new HTTPService();
266                         serv.method="POST";
267                         serv.url=signedOAuthURL(url, "POST");
268                         serv.contentType = "text/xml";
269                         serv.headers={'X-Error-Format':'xml'};
270                         serv.request=" ";
271                         serv.resultFormat="e4x";
272                         serv.requestTimeout=0;
273                         serv.addEventListener(ResultEvent.RESULT, diffUploadComplete);
274                         serv.addEventListener(FaultEvent.FAULT, diffUploadIOError);
275                         serv.send(upload);
276                 
277                         dispatchEvent(new Event(SAVE_STARTED));
278                         return upload;
279         }
280
281         private function diffUploadComplete(event:ResultEvent):void {
282                         var results:XML = XML(event.result);
283
284                         // was it an error document?
285                         if (results.name().localName=='osmError') {
286                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
287                                 diffUploadAPIError(results.status, results.message);
288                                 return;
289                         }
290
291             // response should be XML describing the progress
292             
293             for each( var update:XML in results.child("*") ) {
294                 var oldID:Number = Number(update.@old_id);
295                 var newID:Number = Number(update.@new_id);
296                 var version:uint = uint(update.@new_version);
297                 var type:String = update.name();
298
299                                 if (newID==0) {
300                                         // delete
301                         if      (type == "node"    ) { killNode(oldID); }
302                         else if (type == "way"     ) { killWay(oldID); }
303                         else if (type == "relation") { killRelation(oldID); }
304                                         
305                                 } else {
306                                         // create/update
307                         if      (type == "node"    ) { renumberNode(oldID, newID, version); getNode(newID).markClean(); }
308                         else if (type == "way"     ) { renumberWay(oldID, newID, version); getWay(newID).markClean(); }
309                         else if (type == "relation") { renumberRelation(oldID, newID, version); getRelation(newID).markClean(); }
310                                 }
311             }
312
313             dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, true));
314                         freshenActiveChangeset();
315             markClean(); // marks the connection clean. Pressing undo from this point on leads to unexpected results
316             MainUndoStack.getGlobalStack().breakUndo(); // so, for now, break the undo stack
317         }
318
319                 private function diffUploadIOError(event:FaultEvent):void {
320                         trace(event.fault);
321                         dispatchEvent(new MapEvent(MapEvent.ERROR, { message: "Couldn't upload data: "+event.fault.faultString } ));
322                         dispatchEvent(new SaveCompleteEvent(SAVE_COMPLETED, false));
323                 }
324
325                 private function diffUploadAPIError(status:String, message:String):void {
326                         var matches:Array;
327                         switch (status) {
328
329                                 case '409 Conflict':
330                                         if (message.match(/changeset/i)) { throwChangesetError(message); return; }
331                                         matches=message.match(/mismatch.+had: (\d+) of (\w+) (\d+)/i);
332                                         if (matches) { throwConflictError(findEntity(matches[2],matches[3]), Number(matches[1]), message); return; }
333                                         break;
334                                 
335                                 case '410 Gone':
336                                         matches=message.match(/The (\w+) with the id (\d+)/i);
337                                         if (matches) { throwAlreadyDeletedError(findEntity(matches[1],matches[2]), message); return; }
338                                         break;
339                                 
340                                 case '412 Precondition Failed':
341                                         matches=message.match(/Node (\d+) is still used/i);
342                                         if (matches) { throwInUseError(findEntity('Node',matches[1]), message); return; }
343                                         matches=message.match(/relation (\d+) is used/i);
344                                         if (matches) { throwInUseError(findEntity('Relation',matches[1]), message); return; }
345                                         matches=message.match(/Way (\d+) still used/i);
346                                         if (matches) { throwInUseError(findEntity('Way',matches[1]), message); return; }
347                                         matches=message.match(/Cannot update (\w+) (\d+)/i);
348                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
349                                         matches=message.match(/Relation with id (\d+)/i);
350                                         if (matches) { throwEntityError(findEntity('Relation',matches[1]), message); return; }
351                                         matches=message.match(/Way (\d+) requires the nodes/i);
352                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
353                                         throwBugError(message); return;
354                                 
355                                 case '404 Not Found':
356                                         throwBugError(message); return;
357                                         
358                                 case '400 Bad Request':
359                                         matches=message.match(/Element (\w+)\/(\d+)/i);
360                                         if (matches) { throwEntityError(findEntity(matches[1],matches[2]), message); return; }
361                                         matches=message.match(/You tried to add \d+ nodes to way (\d+)/i);
362                                         if (matches) { throwEntityError(findEntity('Way',matches[1]), message); return; }
363                                         throwBugError(message); return;
364                         }
365
366                         // Not caught, so just throw a generic server error
367                         throwServerError(message);
368                 }
369
370         private function addCreated(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
371             var create:XML = <create version="0.6"/>
372             for each( var id:Number in getIDs() ) {
373                 var entity:Entity = get(id);
374                 if ( id >= 0 || entity.deleted )
375                     continue;
376                     
377                 var xml:XML = serialise(entity);
378                 xml.@changeset = changeset.id;
379                 create.appendChild(xml);
380             }
381             return create.hasComplexContent() ? create : <!-- blank create section -->;
382         }
383
384                 private function addDeleted(changeset:Changeset, getIDs:Function, get:Function, serialise:Function, ifUnused:Boolean):XML {
385             var del:XML = <delete version="0.6"/>
386             if (ifUnused) del.@["if-unused"] = "true";
387             for each( var id:Number in getIDs() ) {
388                 var entity:Entity = get(id);
389                 // creates are already included
390                 if ( id < 0 || !entity.deleted || entity.parentsLoaded==ifUnused)
391                     continue;
392                     
393                 var xml:XML = serialise(entity);
394                 xml.@changeset = changeset.id;
395                 del.appendChild(xml);
396             }
397             return del.hasComplexContent() ? del : <!-- blank delete section -->;
398                 }
399
400         private function addModified(changeset:Changeset, getIDs:Function, get:Function, serialise:Function):XML {
401             var modify:XML = <modify version="0.6"/>
402             for each( var id:Number in getIDs() ) {
403                 var entity:Entity = get(id);
404                 // creates and deletes are already included
405                 if ( id < 0 || entity.deleted || !entity.isDirty )
406                     continue;
407                     
408                 var xml:XML = serialise(entity);
409                 xml.@changeset = changeset.id;
410                 modify.appendChild(xml);
411             }
412             return modify.hasComplexContent() ? modify : <!-- blank modify section -->;
413         }
414
415         private function serialiseNode(node:Node):XML {
416             var xml:XML = serialiseEntityRoot(node); //<node/>
417             serialiseEntityTags(node, xml);
418             xml.@lat = node.lat;
419             xml.@lon = node.lon;
420             return xml;
421         }
422
423         private function serialiseWay(way:Way):XML {
424             var xml:XML = serialiseEntityRoot(way); //<node/>
425             serialiseEntityTags(way, xml);
426             for ( var i:uint = 0; i < way.length; i++ ) {
427                 var nd:XML = <nd/>
428                 nd.@ref = way.getNode(i).id;
429                 xml.appendChild(nd);
430             }
431             return xml;
432         }
433
434         private function serialiseRelation(relation:Relation):XML {
435             var xml:XML = serialiseEntityRoot(relation); //<node/>
436             serialiseEntityTags(relation, xml);
437             for ( var i:uint = 0; i < relation.length; i++ ) {
438                 var relMember:RelationMember = relation.getMember(i);
439                 var member:XML = <member/>
440                 member.@ref = relMember.entity.id;
441                 member.@type = relMember.entity.getType();
442                 member.@role = relMember.role;
443                 xml.appendChild(member);
444             }
445             return xml;
446         }
447         
448                 private function serialiseEntityRoot(entity:Object):XML {
449                         var xml:XML;
450                         if      (entity is Way     ) { xml = <way/> }
451                         else if (entity is Node    ) { xml = <node/> }
452                         else if (entity is Relation) { xml = <relation/> }
453                         xml.@id = entity.id;
454                         xml.@version = entity.version;
455                         return xml;
456                 }
457
458         private function serialiseEntityTags(entity:Entity, xml:XML):void {
459             xml.@id = entity.id;
460             xml.@version = entity.version;
461             for each( var tag:Tag in entity.getTagArray() ) {
462               if (discardTags.indexOf(tag.key) > -1) {
463                 entity.setTag(tag.key, null, MainUndoStack.getGlobalStack().addAction);
464                 continue;
465               }
466               var tagXML:XML = <tag/>
467               tagXML.@k = tag.key;
468               tagXML.@v = tag.value;
469               xml.appendChild(tagXML);
470             }
471         }
472
473         override public function fetchUserTraces(refresh:Boolean=false):void {
474             if (traces_loaded && !refresh) {
475               dispatchEvent(new Event(TRACES_LOADED));
476             } else {
477               sendOAuthGet(apiBaseURL+"user/gpx_files", tracesLoadComplete, errorOnMapLoad, mapLoadStatus); //needs error handlers
478               dispatchEvent(new Event(LOAD_STARTED)); //specific to map or reusable?
479             }
480         }
481
482                 private function tracesLoadComplete(event:Event):void {
483                         var files:XML = new XML(URLLoader(event.target).data);
484                         for each(var traceData:XML in files.gpx_file) {
485                                 var t:Trace = findTrace(traceData.@id);
486                                 if (!t) { t=new Trace(this); addTrace(t); }
487                                 t.fromXML(traceData);
488                         }
489                         traces_loaded = true;
490                         dispatchEvent(new Event(LOAD_COMPLETED));
491                         dispatchEvent(new Event(TRACES_LOADED));
492                 }
493
494         override public function fetchTrace(id:Number, callback:Function):void {
495             sendOAuthGet(apiBaseURL+"gpx/"+id+"/data.xml", 
496                                 function(e:Event):void { 
497                         dispatchEvent(new Event(LOAD_COMPLETED));
498                                         callback(e);
499                                 }, errorOnTraceLoad, mapLoadStatus); // needs error handlers
500             dispatchEvent(new Event(LOAD_STARTED)); //specifc to map or reusable?
501         }
502
503         private function errorOnTraceLoad(event:Event):void {
504             trace("Trace load error");
505             dispatchEvent(new Event(LOAD_COMPLETED));
506                 }
507
508         /** Fetch the history for the given entity. The callback function will be given an array of entities of that type, representing the different versions */
509         override public function fetchHistory(entity:Entity, callback:Function):void {
510             if (entity.id >= 0) {
511               var request:URLRequest = new URLRequest(apiBaseURL + entity.getType() + "/" + entity.id + "/history");
512               var loader:ExtendedURLLoader = new ExtendedURLLoader();
513               loader.addEventListener(Event.COMPLETE, loadedHistory);
514               loader.addEventListener(IOErrorEvent.IO_ERROR, errorOnMapLoad); //needs error handlers
515               loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, mapLoadStatus);
516               loader.info['callback'] = callback; //store the callback so we can use it later
517               loader.load(request);
518               dispatchEvent(new Event(LOAD_STARTED));
519             } else {
520               // objects created locally only have one state, their current one
521               callback([entity]);
522             }
523         }
524
525         private function loadedHistory(event:Event):void {
526             var _xml:XML = new XML(ExtendedURLLoader(event.target).data);
527             var results:Array = [];
528             var dummyConn:Connection = new Connection("dummy", null, null);
529
530             dispatchEvent(new Event(LOAD_COMPLETED));
531
532             // only one type of entity should be returned, but this handles any
533
534             for each(var nodeData:XML in _xml.node) {
535                 var newNode:Node = new Node(
536                     dummyConn,
537                     Number(nodeData.@id),
538                     uint(nodeData.@version),
539                     parseTags(nodeData.tag),
540                     true,
541                     Number(nodeData.@lat),
542                     Number(nodeData.@lon),
543                     Number(nodeData.@uid),
544                     nodeData.@timestamp,
545                     nodeData.@user
546                     );
547                 newNode.lastChangeset=nodeData.@changeset;
548                 results.push(newNode);
549             }
550
551             for each(var wayData:XML in _xml.way) {
552                 var nodes:Array = [];
553                 for each(var nd:XML in wayData.nd) {
554                   nodes.push(new Node(dummyConn,Number(nd.@ref), NaN, null, false, NaN, NaN));
555                 }
556                 var newWay:Way = new Way(
557                     dummyConn,
558                     Number(wayData.@id),
559                     uint(wayData.@version),
560                     parseTags(wayData.tag),
561                     true,
562                     nodes,
563                     Number(wayData.@uid),
564                     wayData.@timestamp,
565                     wayData.@user
566                     );
567                 newWay.lastChangeset=wayData.@changeset;
568                 results.push(newWay);
569             }
570
571             for each(var relData:XML in _xml.relation) {
572                 trace("relation history not implemented");
573             }
574
575             // use the callback we stored earlier, and pass it the results
576             ExtendedURLLoader(event.target).info['callback'](results);
577         }
578         }
579 }