Yet Another GIS Blog
GIS, Geography, Programming, and Neogeography

Shapefiles, Actionscript 3.0, and Google Maps

Tuesday, 28 April 2009 23:10 by boxshapedwo

 I'm working an Adobe AIR application and I wanted to be able to have the user select a shapefile, and then parse it to create a KML file.  I didn't want to have the user be responsible for creating a KML file.  I thought I might try and crack the shapefile enigma since it is a well documented format, but that would have taken time and I suddenly realized I'm not actually a developer :).  Instead, I found this set of Actionscript Classes to parse a shapefile in Flash.  Unfortunately, I didn't find a very good tutorial on how to work with the classes.  The example is a little confusing (at least for me) and also uses a far file.  I'd never heard of far compressed files.  So I took the classes and created my own parser.  I thought I would post a tutorial on how to use these shapefile classes in conjunction with AIR and the Google Maps API for flash.  This technique would work with flex as well, I just didn't want to have to write the code to upload a file.  I presume a few things with this.  The shapefile you are using for this should already have a geographic projection (e.g. latitude and longitude Geographic NAD 83).  In order to use the Google Maps API with AIR, you need a URL with a key associated with it.  Below are two zipfiles available for download.  The testfile.zip is the shapefile I was using.  The vanrikom.zip is the downloaded actionscript classes from the Google Code repository.  I had trouble downloading the using an svn so I did it manually.  I'll save you the time by making it available here...unless the original author asks me to remove them.  There are parts that I find confusing with the way the reader was set up.  For some reason polyline inherits from polygon.  Intuitively to me it should be the other way around...but like I said, I'm not a developer.

This was all done using FlashDevelop and the Flex SDK 3.  There are 4 custom classes in addition to the mxml file.  Each are shown here.

This is what the Main.mxml file looks like.  It should be relatively straight forward if you are familiar with mxml.  The map component comes from the Google Maps API swc that you should download from the above link.  I have added comments so hopefully the code is explanatory.  I apologize for how crappy this looks, but the formatting doesn't work to well with .net blog engine.

<?xml version="1.0"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:maps="com.google.maps.*">
    <mx:Script>
        <![CDATA[
  
        import com.google.maps.LatLng;
        import com.google.maps.LatLngBounds;
        import com.google.maps.Map;
        import com.google.maps.MapEvent;
        import com.google.maps.MapType;
        import org.bsw.flex.shapeParse;
        import org.bsw.flex.polylineClass;
        import com.google.maps.overlays.Polyline;
       
       
        private function onMapReady(event:Event):void {

            //This is a custom class that parses the shapefile. The constructor takes three arguments.  The path, the filename, and the name of a field

           //I don't cover attributes in this example but show how to access them

            var parser:shapeParse = new shapeParse("C:\\Workspace", "testfile", "Id");

            if (parser.pc != null)
            {

                //I'll explain this one later

                var bnds:LatLngBounds = parser.pc.getBounds.latlongBounds;
               

                //I use the bounds of the shapefile to set the extents and the zoom

                this.map.setCenter(bnds.getCenter());
                this.map.setZoom(map.getBoundsZoomLevel(bnds));
                this.map.enableScrollWheelZoom();
                this.map.continuousZoomEnabled();
               
                for (var i:int = 0; i < parser.pc.length; i++)
                {
                    var p:polylineClass = parser.pc.getPolylineAtIndex(i);
                    var gp:Polyline = p.gmapPolyline();
                    map.addOverlay(gp);
                }   
            }

        }
        ]]>
    </mx:Script>
    <mx:Canvas width="100%" height="100%">
        <maps:Map id="map" mapevent_mapready="onMapReady(event)"
            width="100%" height="100%" key="your key" url="your url"/>       
    </mx:Canvas>
</mx:WindowedApplication>

shapeParse.as

package org.bsw.flex
{
    import flash.geom.Point;
    import flash.utils.ByteArray;
    import mx.collections.ArrayCollection;
    //be sure to import these
    import org.vanrijkom.shp.*;
    import org.vanrijkom.dbf.*;
    //I think these are only available in AIR
    import flash.filesystem.File;
    import flash.filesystem.FileMode;
    import flash.filesystem.FileStream;
    /**
     * ...
     * @author dsl
     */
    public class shapeParse
    {
        private var filePath:String;
        private var fileName:String;
        private var idFieldName:String;
       
        //Custom classes
        public var pc:polylineCollection = null;
        public var shapeBounds:boundingBox;
       
        public function shapeParse(filePath:String, fileName:String, idFieldName:String)
        {
            this.fileName = fileName;
            this.filePath = filePath;
            this.idFieldName = idFieldName;
            init();
        }
       
        private function init():void
        {
            //get access to the two files *.shp and *.dbf
            var shpfile:File = File.desktopDirectory.resolvePath(filePath + "\\" + fileName + ".shp")
            var dbffile:File = File.desktopDirectory.resolvePath(filePath + "\\" + fileName + ".dbf")

            var shpFS:FileStream = new FileStream;
            var dbfFS:FileStream = new FileStream;
           
            //this is the key to using the shapefile classes.  You need a bytearray to send to the
            //shpheader class
           
            var shpBA:ByteArray = new ByteArray;
            var dbfBA:ByteArray = new ByteArray;
            try {
                shpFS.open(shpfile, FileMode.READ);
                dbfFS.open(dbffile, FileMode.READ);
                shpFS.readBytes(shpBA);
                dbfFS.readBytes(dbfBA);
                dbfFS.close();
                shpFS.close();
            }catch (e:Error)
            {
                trace(e.message);
            }
           
            //Shapeheader has the base information
            var shp:ShpHeader = new ShpHeader(shpBA);
            var dbf:DbfHeader = new DbfHeader(dbfBA);
           
            //Check what type of shapefile it is.  I only worked with polylines.
            if (shp.shapeType == ShpType.SHAPE_POLYLINE);
            {
                //custom class
                pc = new polylineCollection();
               
                //the boundsXY property for the shapeheader class does not seem to function properly
                //shapeBounds = new boundingBox(shp.boundsXY);
               
                //To good all the records in the shapefile you need an array, and then use the ShpTools class
                var polyArray:Array = ShpTools.readRecords(shpBA);
                //loop through the records
                for (var iPoly:int = 0; iPoly < polyArray.length; iPoly++)
                {
                    //custom polylineclass
                    var internalPoly:polylineClass = new polylineClass
                    //from the vanrijkom classes, create a shpPolyline from the array item
                    var poly:ShpPolyline = polyArray[iPoly].shape as ShpPolyline;
                    //the polyline contains an array of "rings"  These are the actual polylines.
                    var ring:Array = poly.rings;
                    //loop through the rings
                    for (var iRing:int = 0; iRing < ring.length; iRing++)
                    {
                        //get the point collection from the rings
                        //(e.g. all the points that make up the line
                        //segments that make up the polyline)
                        var pntArray:Array = ring[iRing];
                       
                        if (pntArray != null)
                        {
                            //Loop through to get all the points
                            for (var j:int = 0; j < pntArray.length; j++)
                            {
                                //Access shpPoint
                                var pnt:ShpPoint = ShpPoint(pntArray[j]);
                                //trace("X " + pnt.x + " Y " + pnt.y);
                                //Add the points to polylineClass
                                internalPoly.addPoint(new Point(pnt.x, pnt.y));
                            }
                        }
                    }
                    pc.addPolyline(internalPoly);
                    //This is how to access the attributes.
                    var dr:DbfRecord = DbfTools.getRecord(dbfBA, dbf, iPoly);
                    var xsID:String = dr.values[idFieldName];
                    trace(xsID);
                }
            }
           
            //place holders to work with points
            if (shp.shapeType == ShpType.SHAPE_POINT)
            {
               
            }
            //place holder to work with polygons
            if (shp.shapeType == ShpType.SHAPE_POLYGON)
            {
               
            }
           
        }
       
    }
   
}

polylineClass.as

package org.bsw.flex
{
    import com.google.maps.LatLng;
    import com.google.maps.overlays.Polyline;
    import com.google.maps.overlays.PolylineOptions;
    import flash.geom.Point
    import flash.geom.Rectangle;
    import flash.sampler.NewObjectSample;
    import mx.collections.ArrayCollection;
   
    /**
     * ...
     * @author ACE
     */
    public class polylineClass
    {
        //collection of points that make the polyline
        private var pointColl:ArrayCollection;
       
        public function polylineClass()
        {
            pointColl = new ArrayCollection;
        }
       
        //add points to the collection
        public function addPoint(pnt:Point):void
        {
            pointColl.addItem(pnt);
        }
       
        //start point of the polyline
        public function get StartPoint():Point
        {
            return Point(pointColl[0]);
        }
       
        //endpoint of the line
        public function get EndPoint():Point
        {
            return Point(pointColl[pointColl.length - 1]);
        }
       
        public function get Length():Number
        {
            var runningTotal:Number = 0;
            for (var i:int; i < pointColl.length; i++)
            {
                var pnt1:Point = pointColl[i];
                var pnt2:Point = pointColl[i + 1];
                var dX:Number = pnt1.x - pnt2.x;
                var dY:Number = pnt1.y - pnt2.y;
                var dist:Number = Math.sqrt((dX * dX) + (dY * dY))
                runningTotal += dist;
            }
            return runningTotal;
        }
       
        //this returns a polyline that works with the google maps api
        public function gmapPolyline(pOpt:PolylineOptions = null):Polyline
        {
            //default polyline style...setup through polylineoptions
            if (pOpt == null)
            {
                pOpt = new PolylineOptions({
                    strokeStyle: {
                        thickness: 2,
                        color: 0x123456,
                        alpha: 1,
                        pixelHinting: true
                    }
                    });
            }

            //a polyline uses an array of LatLng (from google maps api)
            var latlngArray:Array = new Array;
           
            for (var i:int; i < pointColl.length; i++)
            {
                var pnt:Point = Point(pointColl[i]);
                var ll:LatLng = new LatLng(pnt.y, pnt.x);
                latlngArray.push(ll);
            }
            //create and return the polyline (gmaps) type
            var poly:Polyline = new Polyline(latlngArray, pOpt);
            return poly;
        }
       
        //Found the boundxy from vanrijkom classes was inaccurate, so I created my own bounding box
        //this creates the bounding box
        public function get Bounds():boundingBox
        {
            var right:Number = -(Math.pow(10,10));
            var left:Number = Math.pow(10,10);
            var top:Number = -(Math.pow(10,10));
            var bottom:Number = Math.pow(10,10);
            for (var i:int; i < pointColl.length; i++)
            {
                var pnt:Point = Point(pointColl[i]);
                if (pnt.x < left)
                {
                    left = pnt.x;
                }
                if (pnt.y < bottom)
                {
                    bottom = pnt.y;
                }
                if (pnt.y > top)
                {
                    top = pnt.y;
                }
                if (pnt.x > right)
                {
                    right = pnt.x;
                }
            }
           
            var bnds:boundingBox = new boundingBox(left, right, top, bottom)
            return bnds;
        }
    }
   
}

polylinecollection.as

package org.bsw.flex
{
    import mx.collections.ArrayCollection;
    import flash.geom.Rectangle;
    /**
     * ...
     * @author dsl
     */
    public class polylineCollection
    {
        //stores a collection of PolylineClasses
        private var coll:ArrayCollection;
       
        public function polylineCollection()
        {
            coll = new ArrayCollection;
        }
       
        public function addPolyline(poly:polylineClass):void
        {
            coll.addItem(poly);
        }
       
        public function getPolylineAtIndex(i:int):polylineClass
        {
            return polylineClass(coll[i]);
        }
       
        public function get length():Number
        {
            return coll.length;
        }
       
        //calculates the bounds for all the polylines in the collection
        public function get getBounds():boundingBox
        {
            var right:Number = -(Math.pow(10,10));
            var left:Number = Math.pow(10,10);
            var top:Number = -(Math.pow(10,10));
            var bottom:Number = Math.pow(10, 10);
           
            for (var i:int = 0; i < coll.length; i++)
            {
                var cP:polylineClass = polylineClass(coll[i]);
                var bnds:boundingBox = cP.Bounds;
                if (bnds.left < left)
                {
                    left = bnds.left;
                }
                if (bnds.bottom < bottom)
                {
                    bottom = bnds.bottom;
                }
                if (bnds.top > top)
                {
                    top = bnds.top;
                }
                if (bnds.right > right)
                {
                    right = bnds.right;
                }
            }
            //var rect:Rectangle = new Rectangle(left, top, left - right, top - bottom)
            var newbnds:boundingBox = new boundingBox(left, right, top, bottom)
            return newbnds;
        }
       
    }
   
}

boundingbox.as

package org.bsw.flex
{
    import com.google.maps.LatLngBounds;
    import flash.geom.Rectangle;
    import com.google.maps.LatLng;
   
    /**
     * ...
     * @author dsl
     * */
    public class boundingBox
    {
        public var left:Number = 0;
        public var right:Number = 0;
        public var top:Number = 0;
        public var bottom:Number = 0;
       
        //Apparently there is no constructor overloads for AS3
        public function boundingBox(left:Number, right:Number, top:Number, bottom:Number)
        {
            this.left = left;
            this.right = right;
            this.top = top;
            this.bottom = bottom;
        }
       
        //public function boundingBox(rect:Rectangle)
        //{
            //this.left = rect.left
            //this.right = rect.right;
            //this.top = rect.top;
            //this.bottom = rect.bottom;

        //}
       
        //returns a LatLngBounds from the google maps api
        public function get latlongBounds():LatLngBounds
        {
            var bounds:LatLngBounds = new LatLngBounds(
                    new LatLng(bottom, left),
                    new LatLng(top, right));
            return bounds;
        }
    }
   
}

Here is the flashdevelop project:

ShapeReaderAIRTest.zip (936.60 kb)

vanrijkom.zip (17.38 kb)

testfile.zip (1.33 kb)

This is what it should look like.