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