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

SQLite Flare and AIR

Friday, 10 July 2009 18:29 by boxshapedwo

Ok, so displaying data from your database in a table is rather boring don't you think?  I'm working with time series data in this project, and so I'm really interested in being able to quickly display a trend.  This will be particularly useful since data becomes updated once every quarter, so fairly frequently.  I don't have a professional license of Flex Builder which opens up the very simple to use charting features (played around with a demo a while back), so I've had to look for alternatives.  There are two that I came across:  Flare, and Axiis.  The first problem I ran into with both is that my data didn't come in a 'web' format, but as an array from the SQLite question.  The lack of tutorials for both project made it difficult to find out an answer to this question, so I had to play around with both.  The short answer is - yes you can take the SQLResult.Data and apply it directly to both projects.  I ended up using Flare which was actually much much easier to use (for me) compared to Axiis.  I don't find Axiis very intuitive to use.  Which I was really disapointed by, not just because I couldn't figure it out, but because it is based on the excellent Degrafa project.  Which is how it gets such nice looking charts.  It was easy enough, but it took some playing around with, to get the data array to be used by Axiis.  Instead of playing with the dataset class, just set the DataProvider to the array directly.  Here is a tutorial that was useful.  And I was even able to get it to display some bar graphs, which all the tutorials seem to teach.  When I started to try and work with the LineSeriesGroup, I couldn't ever get a series of lines to display.  I found this one really difficult to understand, because I think of data statistically where there is a x and y axis and couldn't grasp how to set this up.  If anyone wants to explain it to me, I would still like to give Axiis a go.

Like I said, I ended up using Flare which I found much easier to work with.  Instead of the mxml base, Flare uses sprites, which is more Flash less Flex, but still useable in Flex.  So after querying and getting my data array, mentioned in a previous post, this is where I start:

A bunch of Imports: 

    import flare.vis.data.Data;
    import flare.vis.data.EdgeSprite;
    import flare.vis.data.NodeSprite;
    import flare.vis.operator.encoder.ColorEncoder;
    import flare.vis.operator.encoder.PropertyEncoder;
    import flare.vis.operator.label.Labeler;
    import flare.vis.operator.layout.AxisLayout;
    import flare.vis.Visualization;
    import flare.scale.ScaleType;
    import flare.vis.controls.AnchorControl;
    import flare.vis.controls.HoverControl;
    import flare.vis.controls.SelectionControl;
    import flare.vis.data.DataSprite;
    import flare.vis.events.SelectionEvent;
    import flare.vis.controls.TooltipControl;
    import flare.vis.events.TooltipEvent;
    import flare.display.TextSprite;
    import flare.util.Strings;
   
    import flash.filters.GlowFilter;
    import flash.display.Sprite;
    import flash.geom.Rectangle;

 

A class that extends Sprite:

 

    public class LineChartExperiment extends Sprite
    {

 

Global variables and the constructor are commented.  The loaddata() function is the key step that takes our data array and puts it in the data format that is used by Flare.  Flare uses a graph data model.  This means everything is stored as a node and an edge.  You see graphs all the time, not line graphs, but real graphs.  Search wikipedia if you want a more in-depth explanation of graph theory.  This makes a lot of sense given what the library is capable of.  If you look at the Flare Demo Page, and select the layout and then force option, this is a visual representation of a graph that is being organized by a force directed method.  In other words it uses physics to place the circles.  The circles are the nodes, and the lines connecting the nodes are the edges.

When you look at the loaddata function, we are actually taking each object in the array and creating a node from this.  Once we exit the loop we can use Flare's createEdges function to generate the edges for us.  It does this based on two inputs, a sort by input and group by input.  So in this example I sort by time, and group by the name.  Another way to think of this is sort is your x axis and group is the series.

 

        //this is the string that will be used for a tooltip that pops up when hovering
        private static const _tipText:String =
            "Venue: {0}<br/>" +
            "Value: {1}<br/>" +
            "Date: {2}";
        //visualization is the component that handles everyting       
        private var vis:Visualization;
        //this holds our data
        private var dataArray:Array;
        //this is passed in when the sprite is created.  I add the sprite to a canvas
        private var _width:Number;
        private var _height:Number;
       
        public function LineChartExperiment(da:Array, width:Number, height:Number)
        {
           
            super();
            this.dataArray = da;
            this._width = width;
            this._height = height;
            //call the load data function
            loadData();
        }
       
        private function loadData():void
        {
           
            var data:Data = new Data();
            for each (var o:Object in dataArray) {
                data.addNode(o);
                //this also works but uses more lines of code
                //data.addNode({
                        //Value: o.Value,
                        //Date: o.Date,
                        //Name: o.Name
                    //});
            }
            //Important Step do not miss this one
            data.createEdges("data.Date", "data.Name");
            createVisualization(data);
        }

 

 

Once the data is loaded we pass it to the createVisualization function.  A lot of this was pulled from the source code of the examples that download with the swc file.

 

        private function createVisualization(data:Data):void
        {

            //This sets up the location and size of the Visualization

            vis = new Visualization(data);
            vis.bounds = new Rectangle(0, 0, _width - 25, _height - 25);
            vis.x = 50;
            vis.y = 0;
            //and is added to the sprite as a child
            addChild(vis);
           

            //I need to look ino these in more depth, but basically they are used to set up the chart, and the colors

           //Date is the xAxis and Value is the y Axis  Note how you are creating symbols for the nodes, and edges.

           //This allows the edges to show as a line in the chart.  You could skip the nodes and just show the lines

            vis.operators.add(new AxisLayout("data.Date", "data.Value"));
            vis.operators.add(new ColorEncoder("data.Name", Data.EDGES, "lineColor", ScaleType.CATEGORIES));
            vis.operators.add(new ColorEncoder("data.Name", Data.NODES, "fillColor", ScaleType.CATEGORIES));
            vis.operators.add(new PropertyEncoder( { lineAlpha: 0, alpha:0.5, buttonMode: false, scaleX: 1, scaleY:1, size:0.5 } ));
            vis.operators.add(new PropertyEncoder( { lineWidth:2 }, Data.EDGES));
           
            vis.data.nodes.setProperties( { fillColor:0, lineWidth:2 } );
           
            // add mouse-over highlight
            vis.controls.add(new HoverControl(NodeSprite, 0,
                // highlight on mouse over
                function(e:SelectionEvent):void {
                    e.item.filters = [new GlowFilter(0xFFFF55, 0.8, 6, 6, 10)];
                },
                // remove higlight on mouse out
                function(e:SelectionEvent):void {
                        e.item.filters = null
                }
            ));
           
            // add tooltip showing data values
            vis.controls.add(new TooltipControl(DataSprite, null,
                function(e:TooltipEvent):void {
                    var data:Object = e.node.data;
                    TextSprite(e.tooltip).htmlText = Strings.format(
                        _tipText, data.Name, data.Value, data.Date);
                }
            ));
           
            vis.update();
        }
       

        //I added an eventlistener to the canvas to when it was resized, it then called this function

        //Which resizes the visualization to the canvas' new size   

        public function resize(w:Number, h:Number):void
        {
            var rect:Rectangle = new Rectangle(0, 0, w - 25, h - 25);
            _width = w;
            _height = h;
            vis.bounds = rect.clone();
            vis.update();
        }

 

 

Because we are working with a sprite, when we add this to our Main.mxml, we should first add it to a UIComponent, then add it to the Canvas or else you get an error.  Can is a canvas that is created in another portion of the code.

 

                exp = new LineChartExperiment(rev.dataArray, can.width, can.height);
                exp.width = can.width;
                exp.height = can.height;
                var ui:UIComponent = new UIComponent();
                can.addChild(ui);
                ui.addChild(exp);

 

 

Hopefully that is helpful to those who are starting out with flare too. 

SQLite and AIR

Wednesday, 8 July 2009 14:28 by boxshapedwo

I've mentioned that I was curious about using AIR in conjunction with Spatialite in a previous post.  Unfortunately this isn't possible, but you can use the overarching SQLite database in AIR to create a compact database for your applications.  There are a number of good examples out there on how to connect to SQLite in AIR.  Two things that took me a little while to figure out:  1)  SQLite is not available in just FLEX, you need AIR, 2)  What is the SQLResult?  Most of the tutorials just stop with telling you how to send a query, parameterized or otherwise, and set up an event listener to catch the result.  But what is the result?

The result comes as part of a class called SQLResult.  The table that you requested from the database is stored in the Data property of the SQLResult class, SQLResult.Data.  This is actually just an Array.  BUT!  That array stores an object, where the properties of that object are the Column names (or aliases) and the values returned by that property is the row value.  So what does that mean?  Here is an example, presuming you've gotten as far as setting all this up.

            var q:SQLStatement = new SQLStatement;

            q.sqlConnection = yourConnection;

            q..text = "SELECT Venue.Venue_Name AS Name, DATE(revenue_data.Rev_Date) AS Date FROM revenue_data INNER JOIN Venue ON revenue_data.Venue_ID = Venue.Venue_ID"

            q.addEventListener(SQLEvent.RESULT, onResult);

            q.addEventListener(SQLErrorEvent.ERROR, onError);

            q.execute();

In this example we are using a SELECT statement and an INNER JOIN to return the name and date from these two tables.  I've also assigned aliases to the columns, which I'll explain why in a minute.

          var result:SQLResult = SQLStatement(e.target).getResult();

          var darray:Array = results.Data;

          trace(String(darray[0].Name));

In this next part I've gottent he results and assigned the data to an Array.  Then with a simple trace I've pulled the first Row (0) with the Column name (Name).  That provides me with direct access to the data.  Now you might actually want to look at this in a Table.  So you can use a DataGrid component for this, and simply set the DataProvider property equal to your Array, darray in the above example.  No problemo.  The problem I had was setting up the columns, and my work around was to change the Column names to an alias in the query, that way I could simplify the process putting more emphasis on building the query.  This really is pretty inelegant in my opinion...it would be nice to be able to retrieve the schema of the query results.  One other problem I have not had a work around for at the moment is the column order in the datagrid is automatically set up in alphabetic order not the returned order of the query.  I'll have to look into that one further.

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.