Tutorial: Rendering a World Map in ActionScript

This tutorial is the third part of a tutorial series about customized map projection in flash/actionscript. It demonstrates how to draw a world map using the as3-proj library and some helper classes. I assume that we read in a shapefile using the SHP library as shown in the previous tutorial.

1 Reading Shapefiles in PHP, 2 Reading Shapefiles in ActionScript, 3 Displaying a Static World Map in ActionScript.

This tutorial assumes that we read in a shapefile using the SHP library as shown in the previous tutorial. At first I present you a very quick and dirty way of rendering a map, which means that I tried to reduce dependencies to other classes or libraries and put almost all code in one function. On the next page you’ll see a solution that are more (re-)usable.

The Quick and Dirty Way

After we read in the shapefiles, we’re now going to project the coordinates. Therefore we must chose and initialize a map projection. I’m using the Wagner V projection of the as3-proj library.

var projection:Projection = new Wagner5Projection();
projection.initialize();

The next thing we need to do is to calculate the range of the projected coordinates in order to map them to our desired map size. Since we don’t know the output range of the projection formulas we initialize the variables for the minimum and maximum values with infinite numbers.

var min_x:Number = Number.POSITIVE_INFINITY, 
    max_x:Number = Number.NEGATIVE_INFINITY,
    min_y:Number = Number.POSITIVE_INFINITY, 
    max_y:Number = Number.NEGATIVE_INFINITY;

Now we iterate over all coordinates, project them with the Projection.project() method and update the min/max values.

for each (var record:ShpRecord in records) {
    if (record.shapeType == ShpType.SHAPE_POLYGON) {
        var poly:ShpPolygon = record.shape as ShpPolygon;
        for each (var coords:Array in poly.rings) {
            for each (var pt:ShpPoint in coords) {
                var o:Point;
                o = projection.project(deg2rad(pt.x), deg2rad(pt.y), new Point());
                min_x = Math.min(min_x, o.x);
                max_x = Math.max(max_x, o.x);
                min_y = Math.min(min_y, o.y);
                max_y = Math.max(max_y, o.y);
            }
        }
    }
}

Once we have the min/max values for x and y we can proceed with the map drawing. We need to scale and translate the projected coordinates to place them inside our desired map. The scale is calculated beforehand. The drawing is done using the moveTo() and lineTo() methods of the flash drawing api.

var w:Number = stage.stageWidth, h:Number = stage.stageHeight;
var s:Number = Math.min(w / (max_x - min_x), h / (max_y - min_y)); // the scale
 
for each (record in records) {
    if (record.shapeType == ShpType.SHAPE_POLYGON) {
        poly = record.shape as ShpPolygon;
        for each (coords in poly.rings) {
            graphics.lineStyle(0, 1);
            for each (pt in coords) {
                o = projection.project(deg2rad(pt.x), deg2rad(pt.y), new Point());
                o.x = (o.x - min_x) * s + (w-(max_x-min_x)*s)/2;
                o.y = h - (o.y - min_y) * s - (h-(max_y-min_y)*s)/2;
                if (pt == coords[0]) graphics.moveTo(o.x, o.y);
                else graphics.lineTo(o.x, o.y);
            }
            graphics.lineStyle();
        }
    }
}

The result is a nicely centered world map (click image to see the swf):

The quick and dirty way has a few drawbacks and a lot of potential for improvements.

Avoiding duplicate projection calculations

Instead of calculating the projection twice for each point it would be better to store the points after the first projection. We could do this using normal arrays but I suggest the usage of my helper classes Polygon and PointSet. We can also replace the calculations of minimum and maximum coordinates using the boundingBox property of the polygons together with the Rectangle.union() method.

var record:ShpRecord, poly:ShpPolygon, coords:Array, pt:ShpPoint;
var polygons:Array = [];
var bounds:Rectangle = new Rectangle();
 
for each (record in records) {
    if (record.shapeType == ShpType.SHAPE_POLYGON) {
        poly = record.shape as ShpPolygon;
        for each (coords in poly.rings) {
            var out:PointSet = new PointSet;
            for each (pt in coords) {
                out.push(projection.project(deg2rad(pt.x), deg2rad(pt.y), new Point()));
            }
            var out_poly:Polygon = new Polygon(out);
            // store polygon and update the bounding box
            polygons.push(out_poly);
            bounds = bounds.union(out_poly.boundingBox);
        }
    }
}

SolidPolygonRenderer and DataView

Because the rendering of polygons and the following view calculations must be done for every map projection I thought it would be a good idea to put this functionality into separate classes. That makes our code shorter and also more readable. The polygon rendering is done by the OutlinePolygonRenderer class, which implements the IPolygonRenderer interface to enable easy exchanges of renderer classes without having to change the rest of the code. The scaling and translation of the projected points is done by the DataView class, which works nicely together with the PolygonRenderer. After all, the rendering code has shrinked to the following few lines:

var renderer:IPolygonRenderer = new OutlinePolygonRenderer(0x000000, 1);
var screen:Rectangle = new Rectangle(0,0, stage.stageWidth, stage.stageHeight);
var view:DataView = new DataView(bounds, screen);
 
for each (out_poly in polygons) renderer.render(out_poly, graphics, view);

Taking care of the projection limitations

Some projections are limited to a specific latitude or longitude range (e.g. the mercator projection, which is not able to display latitudes of 90° because this would lead to infinite y values). This means we have to check each coordinate before we project it. To simplify this check I added the method Projection.testPointDeg() to the Projection api.

In the end, the complete rendering looks like this:

// see the previous tutorial
var records:Array = ShpTools.readRecords(shp);
 
// step1: preprocessing and polygon creation
var projection:Projection = new Wagner5Projection();
projection.initialize();
 
var polygons:Array = [];
var bounds:Rectangle = new Rectangle();
 
for each (var record:ShpRecord in records) {
    if (record.shapeType == ShpType.SHAPE_POLYGON) {
        var poly:ShpPolygon = record.shape as ShpPolygon;
        for each (var coords:Array in poly.rings) {
            var out:PointSet = new PointSet;
            for each (var pt:ShpPoint in coords) {
                if (projection.testPointDeg(pt.y, pt.x)) {
                    var o:Point;
                    o = projection.project(deg2rad(pt.x), deg2rad(pt.y), new Point());
                    o.y *= -1;
                    out.push(o);
                }
            }
            var out_poly:Polygon = new Polygon(out);
            // store polygon for later rendering
            polygons.push(out_poly);
            // update bounding box
            bounds = bounds.union(out_poly.boundingBox);
        }
    }
}
 
// step2: polygon rendering
var renderer:IPolygonRenderer = new OutlinePolygonRenderer(0x000000, 1);
var screen:Rectangle = new Rectangle(0,0, stage.stageWidth, stage.stageHeight);
var view:DataView = new DataView(bounds, screen);
 
for each (out_poly in polygons) renderer.render(out_poly, graphics, view);

Rendering of Sea and Graticule

A great way to improve the visual understanding of a map projection is to integrate the sea boundary and a graticule. If you’re interested in how to draw them in detail, please take a look at the source files.

As usual you can download a zip file containing all files related to this tutorial.
Additionally I uploaded and linked api docs for the classes I used in this tutorial.

Hope this tutorial was useful to you, sorry for any grammar and spelling errors. If you like you can donate an amount of your choice via paypal. Thanks!