Tuesday, April 14, 2009

Function Pointers to parse XML/KML

So I wrote a XML Parser for KML. I decided to try doing it using function poiters because of the results of earlier testing I had done. In the test the function pointers were faster as both the number of possible xml tags increased and the total number of tags increased. You also end up with generic functions that do most of your work (ie parseInt, parseString, parseHexNumber and so on) which makes the code much more extensible. The way this code works requires some up front work in the constructor. First we have to create the function pointers object.

this._validParseKids.kml = { Document : { func: this.parseXML } ,
                             Folder : { func : this.parseFolder } , 
                             Placemark : { func : this.parsePlacemark } };
this._validParseKids.Document = { Folder : { func : this.parseFolder } , 
                                  Placemark : { func : this.parsePlacemark } ,
                                  Style: { func : this.parseStyle } , 
                                  StyleMap: { func : this.parseStyleMap } };
this._validParseKids.Folder = { Folder : { func : this.parseFolder } ,
                                Placemark : { func : this.parsePlacemark } }; 


As you can see the kml object has three possible children: Document, Folder, and Placemark. Each of these have a func(Function) assigned to them. So if we are in the kml tag and find a Document the this.parseXML function will be executed. This parseXML function looks like this:

protected function parseXML( x:XML ) : Object {
 var returnObject:Object = new Object();
 var validKids:Object = this._validParseKids[x.name().localName];
 
 if( validKids != null ){
  var kids:XMLList = x.children()
  for each( var node:XML in kids  ){
   var validKid:Object = validKids[node.name().localName];
   if( validKid != null && validKid.func is Function ){
    validKid.attrName = ( validKid.attrName == null ) ? node.name().localName : validKid.attrName;
    
    if( validKid.attrType == "Array" ){
     if( returnObject[validKid.attrName] is Array == false ){
      returnObject[validKid.attrName] = new Array();
     }
     returnObject[validKid.attrName].push( validKid.func( node ) );
    }
    else{
     returnObject[validKid.attrName]  = validKid.func( node );
    }
   }  
  }
 }
 else{
  returnObject = "Not Found in Valid Kids Object";
 }
 return returnObject;
}


If you notice there are two other variables in the this._validParseKids object, attrName and attrType. Here is an example of both:

this._validParseKids.Placemark = {
   Point        : { attrName: "_shape"     , func : this.parsePoint} ,
   LineString   : { attrName: "_shape"     , func : this.parseLineString } ,
   LinearRing   : { attrName: "_shape"     , func : this.parseLinearRing } ,
   Polygon      : { attrName: "_shape"     , func : this.parsePolygon    } ,
   name         : { attrName: "name"       , func : this.parseString } ,
   address      : { attrName: "address"    , func : this.parseString } ,
   phoneNumber  : { attrName: "phoneNumber", func : this.parseString } ,
   Snippet      : { attrName: "Snippet"    , func : this.parseString } ,
   description  : { attrName: "description", func : this.parseString } ,
   styleUrl     : { attrName: "_style"     , func : this.parseStyleUrl } ,
   ExtendedData : { attrName: "_data"      , func : this.parseExtendedData } };

this._validParseKids.ExtendedData = { 
   Data : { attrName : "kvPairs" , attrType :  "Array" , func : this.parseData } };


So the attrName is the name of the object that any returned values from the function that was called may return. The attrType is only used to say that there will be multiple objects that return data to the given attrName. So for example the when in the ExtendedData tag you can have an unlimited number of Data tags that are to be stored in an array. As a final example here are a couple of the basic parse functions:

protected function parseString( text:XML ) : String {
 return text.toString();
}

protected function parseInt( i:XML ) : int {
 var s:String = i.toString();
 var n:Number = Number( s );
 
 if( isNaN( n ) ){
  n = this.stringToHex( s );
 }
 return int( n );
}

protected function parseNumber( n:XML ) : Number {
 return Number( n.toString() );
}

protected function parseBoolean( b:XML ) : Boolean {
 var value:String = b.toString();
 if( value.match( /(1|true)/ ) ){
  return true;
 }  
 return false;
}