Thursday, June 7, 2012

Custom Map Tiles (Part 4 - Programmatically with EF 5)


Hi,

After a pause from mapping related stuff, I couldn't resist going back for some more, especially having just downloaded Visual Studio 2012 RC1.

In this post I'm going to show how to create tiles programatically at server-side using .NET 4.5 and Entity Framework 5 RC 1.

I'll basically create an ASP.NET MVC 4.0 application which handles http tile requests, uses Entity Framework to fetch spatial data from a SQL Server database and produces a bitmap image using GDI+. Then I'll show these tiles on a Leaflet Map. This seems too much trouble for what is worth but sometimes you just need the extra control to do stuff that otherwise would be really difficult to do.

As usual I'm going to show most of the steps to get this thing working, just jumping some hoops on simpler stuff. Anyway, even if some steps seem less clear I'm including a zip file with the whole solution.



Step 1
Download a shapefile

I've downloaded a shapefile of the world from here

Step 2
Insert the shapefile data to a SQL Server database

  • Create an empty database called "SpatialDemo"
  • Use ogr2ogr to load the data.

We can do this in a variety of ways. My suggestion: use a command-line tool from GDAL called ogr2ogr. There are several ways to get this library, including getting the source-code and compiling yourself. Myself, I prefer less fuss, so I downloaded a (not official) installer from this page: http://www.gisinternals.com/sdk/

I've chosen the "MSVC2010 (Win64) -stable" category. Inside it I chose the "gdal-19-1600-x64-core.msi" installer. Direct link to the installer.

After installing you get a nice command prompt with all the environment variables correctly registered and ready to use.




Run the following command:
ogr2ogr -f "MSSQLSpatial" "MSSQL:server=.;database=SpatialDemo;trusted_connection=yes" "TM_WORLD_BORDERS-0.3.shp" -overwrite -nln "Country"

Most of the arguments are pretty much self explanatory. The "-nln" argument was used to specify the table name that will hold the geometry data.

ogr2ogr created the following schema for us:


If we run a query against the country table we can check that our spatial data is there.



Step 3
Create an ASP.NET MVC 4 project with 2 controllers and a custom route
  • HomeController
    • Index action
      • Index.cshtml with a Leaflet map definition
  • TileController   (route  Tile/{z}/{x}/{y}.png)
    • Get action
The route is defined as:
routes.MapRoute(
       name: "Tiles",
       url: "Tile/{z}/{x}/{y}",
       defaults: new { controller = "Tile", action = "Get" }
);

Now, lets add the Entity Framework bits. Since my previous post EF 5 is now in Release Candidate. Get it from NuGet. If you're using a NuGet version previous to 1.7 you won't have the "Include Prerelease" option.


Create a model from the database of Step 2.



Now let's create our action.

The code for the Get action is as follows:
[HttpGet]
public FileResult Get(int z, int x, int y)
{
    int nwX;
    int nwY;

    TileSystem.TileXYToPixelXY(x, y, out nwX, out nwY);

    double nwLatitude, nwLongitude;
    double seLatitude, seLongitude;

    TileSystem.PixelXYToLatLong(nwX, nwY, z, out nwLatitude, out nwLongitude);
    TileSystem.PixelXYToLatLong(nwX+256,nwY+256,z,out seLatitude,out seLongitude);
            
    var boundingBox = GetBoundingBox(nwLatitude,nwLongitude,seLatitude,seLongitude);

    var tile = new Bitmap(256, 256, PixelFormat.Format32bppArgb);
            
    var degreesPerPixel = (nwLatitude - seLatitude) / 256.0;

    using (SpatialDemo db = new SpatialDemo())
    {
        db.country
          .Where(c => c.ogr_geometry.Intersects(boundingBox))
          .Select(s => new 
          { 
             Geometry = SqlSpatialFunctions.Reduce(s.ogr_geometry,degreesPerPixel),
             Country = s.name 
          })
          .ToList()
          .ForEach(g => ProcessCountry(z,nwX,nwY,tile,g.Geometry,g.Country));
    }

    var ms = new MemoryStream();
    tile.Save(ms, ImageFormat.Png);

    byte[] bytes = ms.ToArray();

    return File(bytes, "image/png");
}
I'be borrowed the transformation code from Bing Maps Tile System.

This action is called for each tile and consists of:
  • Determining the coordinates of the top left/bottom right corner of the tile
  • Creating a bounding box for that corners
  • Fetch the database to get the countries which intersect that bounding box
  • While getting the data simplify it for the current zoom level (with the Reduce method)
  • Create a bitmap and return it

The countries are painted in red, ranging from white to vivid red. The color is chosen according to the size of the name of each country. So, the "The former Yugoslav Republic of Macedonia" will have a white color, and  "Cuba" or "Iraq" will have vivid red. Pretty useless, I know, but a fun experiment nevertheless.

The end result is this:



The country names that I've used are not the ones that you can see in the OpenStreetMap. Anyway, don't mind that too much. The idea here is to show that we can programmatically do whatever we please to generate the tile images. For example: draw a watermark on the tile, draw a grid, paint a heatmap, draw a graph inside each country showing some stats, etc. The possibilities are endless.

Anyway, here is the source-code for the solution that I've created. As usual, not production-ready code, but should be enough to get you started.






12 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. I have just switched part of client-displaying to https://sharpmap.codeplex.com/

    ReplyDelete
  7. Hi Pedro...

    I need to apply grid over mapbox map ....can you please help me or provide any demo code so it would be great


    Thanks

    ReplyDelete
  8. Very nice article even though the source code is not available anymore (located on expired domain).

    ReplyDelete