Friday, July 1, 2011

Simple Maps Using GeoJSON and Tile5

I've been wanting to post about GeoJSON for a while now because I like what the standard provides in terms of simplifiying interoperability.  The thing is, GeoJSON is just a standard structure for geometry and features represented in JSON, so there isn't much for me to talk about in regard to GeoJSON itself.  So what I decided to do is see how I could utilise GeoJSON to create a very simple mapping application by simplifying communication between tiers, and highlight how JavaScript mapping libraries such as Tile5 can help you achieve your vision of simplicity.



GeoJSON


As I said above GeoJSON is a standard for geometries to be represented in JSON. For the uninitiated JSON stands for JavaScript Object Notation, and is basically a way of serialising a JavaScript object in a very simple, human readable form. JSON is very similar to the syntax in c# for initialising an anonymous type, for example


var geoJsonPolygon = { "type": "Polygon",
                        "coordinates": [
                                       [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ]
                                       ]
                     };

Once the object is initialised its properties can be accessed as usual, for example


var geoJsonType = geoJsonPolygon.type;

The GeoJSON standard describes a JSON structure such that a JavaScript object can be created to represent a collection of geometric features, and it is this standard that provides applications with the ability to communicate geometry data in a standard way.


The standard allows for collections of feature objects, of which a feature represents a geometry as well as a list of non-geometric properties. Each geometry object has a property describing its type, i.e. point, line, polygon, etc, as well as a list of coordinates.




GeoJSON Example - The Objective


To illustrate how useful GeoJSON is, I decided to create a simple mapping application utilising an ASP.Net MVC web application, which exposes a Tile5 based map in the browser, with a base image layer supplied by one of the Open Street Map (OSM) data providers that Tile5 supports. In addition to this, as the user pans around the map, the Tile5 functionality will call back to the web server to retrieve cadastral boundaries within the extent of the current view of the map, and display the boundaries as an overlay layer. The cadastral boundary data will be supplied as GeoJSON to Tile5 to render on the browser.


The aim is to see how simple and minimal I can make the code to retrieve/marshall/render spatial data between Tile5 and my SQL Server database.




Will the real GeoJSON.Net please stand up?


Obviously, the ability to communicate between tiers requires that each tier can construct and consume JSON syntax. On a JavaScript client this is fairly simple because JSON relates intimately to the JavaScript, but what do we do on our web server running ASP.Net?


The objective is that we want to interact with an object model both on the client and the server, and just use JSON to communicate between each tier, so we need to have an object model that represents the same structure as GeoJSON that we can use in C#, and then be able to serialise this object model to JSON.


At first I thought I might be able to reuse an open source .Net library to model GeoJSON and take care of JSON serialisation, but a search of GeoJSON.Net yeilds a number of results (CodePlex, Google Code, GitHub, Assembla), that don't all seem to be related.


In addtion to needing a library to represent GeoJSON, I also wanted to use the Net Topology Suite to work with my geometry results from SQL Server for data access, so after reading through this post by Vish Uma, in which there is a discussion of some of the drawbacks of exposing third party library objects to JSON serialisation due to not having the ability to attribute the required classes/properties, I decided to create my own very simple classes to model GeoJSON that are a little tighter bound to Net Topology Suite geometry.




Data Access Utilising Net Topology Suite


As I discussed above, I wanted to use the Net Topology Suite to interact with the geometries I select from SQL Server. The reason for this is that there are no objects in the .Net Framework (other than with SQL Server specific libraries) that represent the geometry and geography types in SQL Server, so the way I access this data is by retrieving the geometry or geography data in Well Known Binary (WKB) or Well Known Text (WKT) format, and then use the NetTopologySuite.IO classes to read from either of those formats, to return a geometry object.


It is then a simple step to populate my GeoJSON model and send the results back to the JavaScript client.


So let's check out the code....


MVC


Controller


The controller has two simple actions

  • Index - for retrieving the initial map view
  • RetrieveCadastre - for retrieving the cadastral boundaries that are within the requested bounds

An interesting thing to point out is that the RetrieveCadastre action takes an argument called bounds, which is an object of type SpatialDataAccess.Bounds. SpatialDataAccess.Bounds is a type I have created as a server side representation of the bounds object that Tile5 uses in the boundsChange event. This illustrates how JSON can be used in both the request and the response in MVC.


using System.Web.Mvc;

namespace GeoJSONMvc.Controllers
{
    public class GeoJSONMapController : Controller
    {
        // Display the map view
        public ActionResult Index()
        {
            return View();
        }

        // Return the requested cadastral information
        [HttpPost]
        public JsonResult RetrieveCadastre(SpatialDataAccess.Bounds bounds)
        {
            return this.Json(SpatialDataAccess.Cadastre.Retrieve(bounds));
        }
    }
}

View


The view contains the HTML and JavaScript to define the Tile5 map and operations that occur as a map is panned.


The map will be rendered using a canvas element called mapCanvas. When the document loads the map is created, referencing the mapCanvas element, and adding an OSM layer to the map.   A function is defined to callback into when the boundsChange event fires, and this in turn calls the requestUpdatedCadastre function, passing the map extents of the map view.


The requestUpdatedCadastre function uses JQuery to execute an ajax call to the RetrieveCadastre MVC action, passing the bounds as part of the request. If the ajax call succeeds we call the parseResponseCadastre function to allow Tile5 to access the GeoJSON features and render them on the canvas.


@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <title>GeoJSONMap</title>
    <script type="text/javascript" src="../../Scripts/jquery-1.5.1.min.js"></script>
    <script type="text/javascript" src="../../Scripts/tile5.js"></script>
    <script type="text/javascript" src="../../Scripts/geo/osm.js"></script>
    <script type="text/javascript">
        var map;

        $(document).ready(function () {
            var startPosition = T5.Geo.Position.parse("-27.43247,153.065654");

            // initialise the map
            map = new T5.Map({
                container: 'mapCanvas'
            });

            map.setLayer('tiles', new T5.ImageLayer('osm.mapquest', {}));

            // goto the specified position
            map.gotoPosition(startPosition, 17, function () {
                map.bind('boundsChange', function (evt, bounds) {
                    requestUpdatedCadastre(bounds);
                });
            });
        });

        function requestUpdatedCadastre(bounds) {
            $.ajax(
            {
                type: "POST",
                url: "/GeoJSONMap/RetrieveCadastre",
                dataType: 'json',
                data: JSON.stringify(bounds),
                contentType: 'application/json; charset=utf-8',
                success: function (result) {
                    parseResponseCadastre(result)
                },
                error: function (req, status, error) {
                    alert("Unable to get cadastral data");
                }
            });
        }

        function parseResponseCadastre(data) {
            T5.GeoJSON.parse(
            data.features,
            function (layers) {
                for (var layerId in layers) {
                    addLayer(layerId, layers[layerId]);
                }
            }, {
                rowPreParse: function (row) {
                    return row.geometry;
                }
            });
        }

        function addLayer(layerId, layer) {
            layer.style = 'area.parkland';
            map.setLayer(layerId, layer);
        }
    </script>
</head>
<body>
    <div id="mapContainer" style="width: 800px; height: 600px;">
        <canvas id="mapCanvas"></canvas>
    </div>
</body>
</html>



Data Access


Cadastre


As you saw above, the RetrieveCadastre looks very simple, calling SpatialDataAccess.Cadastre.Retrieve(bounds), and this is where it accesses the data, and packages it up in an object model that represents GeoJSON.


The data is retrieved using a spatial query in SQL Server, returning the geography values in WKB, which are parsed using the NetTopologySuite.IO.WKBReader to create a GeoAPI.Geometries.IGeometry based object. This object is used as the source for the classes that I have created to model GeoJSON geometries.


namespace GeoJSONMvc.SpatialDataAccess
{
    public class Cadastre
    {
        public static GeoJSON.FeatureCollection Retrieve(Bounds bounds)
        {
            var featureCollection = new GeoJSON.FeatureCollection();

            var connection = new System.Data.SqlClient.SqlConnection("Data Source=localhost;Initial Catalog=Spatial;User ID=spatial;Password=spatial");

            string sql = "select geom.STAsBinary() as wkb, id from state_1 with(index(IX_SP_state_1)) where geom.STIntersects(geography::STPolyFromText('POLYGON((" + bounds.min.lon + " " + bounds.min.lat + "," + bounds.max.lon + " " + bounds.min.lat + ',' + bounds.max.lon + " " + bounds.max.lat + ',' + bounds.min.lon + " " + bounds.max.lat + ',' + bounds.min.lon + " " + bounds.min.lat + "))', 4283)) = 1;";

            using (connection)
            {
                connection.Open();

                var command = new System.Data.SqlClient.SqlCommand(sql, connection);

                using (command)
                {
                    var reader = command.ExecuteReader();

                    using (reader)
                    {
                        while (reader.Read())
                        {
                            var wkbReader = new NetTopologySuite.IO.WKBReader();
                            var sqlGeometry = wkbReader.Read(reader.GetSqlBytes(0).Stream);
                            var geoJsonGeometry = new GeoJSON.Geometry(sqlGeometry);
                            geoJsonGeometry.type = GeoJSON.ObjectType.Polygon.ToString();
                            var geoJsonFeature = new GeoJSON.Feature();
                            geoJsonFeature.geometry = geoJsonGeometry;
                            geoJsonFeature.properties.id = reader.GetInt32(1).ToString();
                            featureCollection.features.Add(geoJsonFeature);
                        }
                    }

                }
            }
            return featureCollection;
        }

    }
}

Bounds


The SpatialDataAccess.Bounds is a fairly simple class that represents two points, min and max, with a lon and lat property representing the coordinates. This type is used as the argument to the RetrieveCadastre action, and is passed through to the Cadastre.Retrieve method to get the spatial features within the bounds.


namespace GeoJSONMvc.SpatialDataAccess
{
    public class Bounds
    {
        public Point min { get; set; }
        public Point max { get; set; }
    }

    public class Point
    {
        public decimal lon { get; set; }
        public decimal lat { get; set; }
    }
}



GeoJSON Classes


GeoJsonObject


The GeoJSONObject class is a base class that all GeoJSON objects derive from.  This class has an implementation for the type property.  The type property has been implemented as a string due to .Net serialising enumerations using the integer value rather than the enumeration value name, so to save mucking around I simply supply the object type name as a string.  I have an enumeration that lists these types and simply pass the required setting as Enumeration.ToString().


namespace GeoJSONMvc.GeoJSON
{
    public abstract class GeoJsonObject
    {
        public string type { get; set; }
    }
    
    public enum ObjectType
    {
        Feature,
        FeatureCollection,
        Point,
        MultiPoint,
        LineString,
        MultiLineString,
        Polygon,
        MultiPolygon,
        GeometryCollection
    }
}

FeatureCollection


The FeatureCollection class represents the collection of features. 


using System.Collections.Generic;

namespace GeoJSONMvc.GeoJSON
{
    public class FeatureCollection : GeoJsonObject
    {
        public FeatureCollection()
        {
            this.features = new List<Feature>();
        }

        public List<Feature> features { get; set; }
    }
}

Feature


The feature represents a geometry object with properties, i.e. a spatial record with non-spatial attributes.



namespace GeoJSONMvc.GeoJSON
{
    public class Feature : GeoJsonObject
    {
        public Feature()
        {
            this.properties = new Properties();
            base.type = ObjectType.Feature.ToString();
        }

        public Geometry geometry { get; set; }

        public Properties properties { get; set; }
    }
}
Geometry


A geometry represents a shape, i.e. Point, MultiPoint, LineString, MultiLineString, Polgyon, MultiPolygon, GeometryCollection.  The coordinates array represents a 1:n array of coordinates.



namespace GeoJSONMvc.GeoJSON
{
    public class Geometry : GeoJsonObject
    {

        public Geometry()
        {
            this.coordinates = new decimal[0][];
        }

        public Geometry(GeoAPI.Geometries.IGeometry shape)
        {
            this.coordinates = new decimal[shape.Coordinates.Length][];
            for(var i = 0; i < shape.Coordinates.Length; i++)
            {
                var coordinate = shape.Coordinates[i];
                this.coordinates[i] = new decimal[2] { System.Convert.ToDecimal(coordinate.X), System.Convert.ToDecimal(coordinate.Y) };
            }
        }

        public decimal[][] coordinates { get; set; }
    }
}

Properties


The Properties class simply represents a container type for all properties of a Feature.  In the case of the data I am using, there is only one property I am interested in including which is feature id.



namespace GeoJSONMvc.GeoJSON
{
    public class Properties
    {
        public string id { get; set; }
    }
}



Results

The image below is a screen shot of the application, which displays the retrieved cadastral boundaries overlaid on the OSM layer. The road names as displayed in OSM are visible in the screen between the blocks of cadastral boundaries. Each time the user pans or zooms, the application retrieves the cadastral boundaries within the map extent and renders them on the map.


While this is a fairly simplistic map application, it shows how easy it is acquire the data in GeoJSON format and render on the map. I put this example together in just a few hours, which was fairly good considering I had not used Tile5 before. The data access logic was very easy to put together.





Conclusion

I like that the spatial community was very quick to identify that the emergent use of JSON would benefit from having standards around the structure of spatial data. JavaScript is now ubiquitous in the world of web mapping, so standards in this area provide developers with opportunities to leverage functionality that interacts and adheres to those standards.


It is also interesting to note that this comparison of serialisation methods in .Net shows JSON serialisation to be one of the better performing methods, resulting in one of the smallest packet sizes.

The reality of this experiment is that all the real work is being done by the Tile5 libraries, which is really cool. All I had to do is provide some simple data access, which was very simple to achieve.

One of the things to note about the example is that there are no constraints around the amount of GeoJSON data being retrieved and rendered on the client. In the real world there would be more logic around scale dependent layers to limit the volume of data being passed around for larger view extents.

1 comment:

  1. Great article. I have been struggling to get it to work because it appears the coordinates need to nested in [[[ 3 levels deep rather than 2. Thanks for the great start.

    ReplyDelete