-
Notifications
You must be signed in to change notification settings - Fork 5
Home
###What the tutorial shows
The tutorial shows how to build a web-based map application with JavaScript to visualize, analyze and manipulate spatial data. Most tutorials on the web show how to work with a limited number of client-side push pins or the 50 US states polygons (which are at a large part just plain rectangles). This tutorial however explains how to work with large amount of complex spatial data, stored in a spatial-relational database. I am using SQL-Server Spatial and/or SpatialLite. The backend for the JavaScript frontend is implemented in ASP.NET. For the map/route/locate services, i'm using PTV xServer internet. The result will be some interactive maps like these.
- Clickable dynamic regions
- Clickable dynamic road segements
- Thematic regions with coverage calculation
- Build custom regions from postal codes (needs Silverlight)
The ambition for this tutorial
- The map must be responsive. I don't have to wait until i get the new map image when scrolling in the map.
- The map content must be interactive. It is not just a WMS overlay image. I can click on a feature to get additional infos or change features.
- The map must be mobile-friendly. It should work well on a smartphone with the according touch-gestures like pinch-zoom or tap.
- The data can be complex and large. It cannot simply be loaded as (Geo)JSON to the client.
- The spatial data is not "special" data. It is stored in the database together with my non-spatial business data. This means i can do SQL queries with joins, etc.
- The data is dynamic. It is directly rendered out of the database. Whenever the spatial or associated business data changes, this is directly reflected when refreshing the map. There is no additional processing neccessary.
- Copy deployment You can download and start the tutorial. That's why i'm using SpatialLite, while this is not my preferred database. I'm using SQL Spatial for real applications, but this would involve some additional steps for deployment and i want to focus on the code. I'll mention the equivalent SQL-Server queries at the tutorial parts.
- Min dependency I'm not using any spatial libraries (NetTopologySuite, SharpMap or System.Data.Spatial). One reason is that i want to explain the basics and not the frameworks. Another reason is speed - Using frameworks involves some overhead and i have to search at what part of the framework the performance bottleneck hides. I'm using Leaflet on the JavaScript side though, because i don't want to code this stuff by myself.
###Prerequisites
You can download the source code at the repository page. The samples use PTV xServer internet. You can get a trial access here. To build the tutorial, you need Microsoft Visual Studio or the free Visual Studio Express 2013 for Web.
###Step 1: Use Leaflet with PTV xServers
I'm using Leaflet as library for my interactive map. Leaflet allows to add an arbitrary "tile provider" as base map. A tile provider is a web service that delivers images with size 256x256 like these http://xmap-eu-n-test.cloud.ptvgroup.com/WMS/GetTile/xmap-ajaxbg/2117/1391/12.png. Leaflet loads the tiles and "stitches" the map from the images. If the user pans the map, only the new tiles have to be loaded, which makes this approach efficent. A tile is the combination of x/y/z, where z is the level and x,y the position of the tile within this level, you can find a good detailed explanation here http://msdn.microsoft.com/en-us/library/bb259689.aspx.
When using PTV xMapServer as tiled map, there's the restriction that only the background-part (terrain, roads) are rendered as tiles. The foreground-part (smybols and labels) has to be rendered as a single image for the whole map viewport. To support this in Leaflet, we've built Leaflet-Plugin https://github.com/ptv-logistics/Leaflet.NonTiledLayer.
A basic boilerplate code for an xMapServer map in Leaflet
The result: http://80.146.239.139/SpatialTutorial/01-XMapLeaflet.html
###Step 2: A dynamic tile provider in ASP.NET
The basic technique to visualize our own data is to implement a "dynamic tile provider". This means we build our own web service that renders images. On the client side, we "inject" these images between the xMapServer back- and foreground layer. A big advantage of the xMapServer rendering engine is that the labels of the basemap are still topmost.
I'm using ASP.NET for my custom imagery service, but the same practice can also be applied to Java or JavaScript (via node). Here is a tutorial for node.
Our first tile provider does nothing else but to render an image with the tile key as text, like http://80.146.239.139/SpatialTutorial/02-DynamicTilesHandler.ashx?x=1&y=2&z=3.
The code-behind for the ASP.NET handler
At leaflet we can add an additional TileLayer using our own handler:
// add dymamic tile layer
var myTileLayer = new L.TileLayer('02-DynamicTilesHandler.ashx?x={x}&y={y}&z={z}', {
maxZoom: 20, minZoom: 0, zIndex: 100}).addTo(map);
The result: http://80.146.239.139/SpatialTutorial/02-DynamicTileProvider.html
###Step 3: Tiles and Geography
For this section, you'll need some understanding of the mercator projection used by nearly all web maps. Most geospatial data sets you'll find today are defined by latitude/longitude values, referring to the World Geodetic Sytstem (WGS84). To display this data on a map (i.e. screen), you'll have to perform a map projection. Even if you paint the latitude/longitude values directly as x/y coordinates on the screen, you implicitly do a special projection, the plate carrée projection.
The projection used by today's web maps (and by the tiling system used here) is the Mercator Projection. (In fact it is some sloppy kind of "Spherical Mercator", but that doesn't matter for us). In contrast to the direct lat/lon-projection, the mercator projection is conformal, which means it preserves shapes and angles, and that makes it a very appropriate projection for aerial imagery or navigation systems. One disadvantage is the distortion of sizes away from the mercator, which makes Greenland appear as big as Africa, while Africa is in fact 5 times larger than Greenland.
In this step we write a tile provider that renders all latitude and longitude lines that are contained within a tile.
http://80.146.239.139/SpatialTutorial/03-TilesAndGeographyHandler.ashx?x=33&y=23&z=6
The usual tile rendering steps are as follows:
- Get the latitude/longitude bounds for the tile key
- Loop through all elements which are within these bounds
- Transform the coordinates of these elements to screen (=tile) coordinates and render them
We need two basic transformations for this:
- To query the elements for a tile, we have to get the mercator bounds for the tile key, and then transform it to WGS (lat/lon).
- To display a geographic (lat/lon) coordinate on a screen/tile, we first have to project the coordinate to the mercator system, and then shift/scale it to the screen/tile viewport.
For these transformations i made a tools class
https://github.com/oliverheilig/SpatialTutorial/blob/master/TransformTools.cs
with the functions
public static Rect TileToWgs(uint x, uint y, uint z)
public static Point WgsToTile(uint x, uint y, uint z, Point wgsPoint)
The rendering handler calculates the wgs bounds and renders the lines with integral latitudes and longiudes that are within these bounds.
https://github.com/oliverheilig/SpatialTutorial/blob/master/03-TilesAndGeographyHandler.ashx.cs
The result is a map with the latitude/longitude grid
http://80.146.239.139/SpatialTutorial/03-TilesAndGeography.html
###Step 4: Symbols, Labels and the Bleeding Problem
Next, we render the value of the latitude and longitude as text at the center of the corresponding line.
The result is an image like http://80.146.239.139/SpatialTutorial/04-TilesAndLabelsHandler.ashx?x=67&y=43&z=7
But this raises a problem when rendering objects where the extension of the renderd object exceeds the geometry. Objects which "bleed" into a neighbouring tile must also be rendered for the neighbouring tile. This occurs typically for symbols or labels. It can also be an issue for line strokes with a size > 1 pixel.
To solve this issue, we can increase our query window, to guarantee that also these objects are rendered, that are located outside our tile, but visually "bleed" into the tile. I've added a new optional parameter bleedingPixels at the TileToWgs method.
public static Rect TileToWgs(uint x, uint y, uint z, int bleedingPixels = 0)
A symbol with size s can bleed s/2 pixel into a neighbouring tile
// the size of our symbol in pixel
int symbolSize = 16;
// calculate geo rect, taking the bleeding into account
var rect = TransformTools.TileToWgs(x, y, z, symbolSize / 2);
Now the symbols are rendered correctly for critical tiles, like
http://80.146.239.139/SpatialTutorial/04-TilesAndLabelsHandler.ashx?x=69&y=44&z=7
and we get a jitter-free overlay layer.
http://80.146.239.139/SpatialTutorial/04-TilesAndLabels.html
###Step 5: Scale the objects
The WgsToTile method gives us coordinates with pixel reference. The DPI number for the tiling scheme is 96, so you can calculate the actual size on the display. Sometimes it is useful to increase the size of object depending on the map zoom. To achieve this, you can take the current map scale (given by the level) into account. One simple method is to explicitly set a size for a range of Levels, for expample we can set the width of our Latitude line depending on the Level.
int strokeSize = (z > 15)? 4 : (z > 10)? 3 : (z > 5)? 2 : 1;
This sets the size of the stroke in Pixels to 3 for zoom Levels between 11 and 15.
a more flexible way is the "power law scaling": For a base size and a adaptive factor, the objects can be scaled in a non-linear way. If the adaptive factor is 0.0, the size is constant (=pixel). If the adaptive factor is 1.0 the size is linear (=meters at equator). If it is something in between, the size grows non-liear.
// calculate map scale
var mapSize = 256 * Math.Pow(2, z); // size of the map in pixel
double earthCircumfence = 2.0 * Math.PI * 6378137.0; // circumfence of earth
var scale = mapSize / earthCircumfence; // pixel per mercator unit
// Calculate the symbol sizes with 3 different scaling modes
// 1 - constant scaling - radius is always 16 pixels
int sz1 = (int)(16 * Math.Pow(scale, 0.0));
// 2 - linear scaling - the symbol has a size of 10000 merctor units (=meter at equator, equals sz = 10000 * scale)
int sz2 = (int)(10000 * Math.Pow(scale, 1.0));
// 3 - logarithmic scaling - the size is adapted with a base size (64) and a scaling factor (0.25)
int sz3 = (int)(64 * Math.Pow(scale, 0.25));
The result: http://80.146.239.139/SpatialTutorial/05-SymbolScaling.html
###Step6: Fill the tiles with our data (...in Progress...)
Now we are going to work with real-world data.
For this tutorial i'm using data from DDS Digital Data Services. They provide both geographic data and the corresponding socio-economic data. But you could also use the geometries and link it to your own data. You can download some test data here
To visualize our data, we have to import it into our database. The de-fact standard for exchanging geographic data are Shape Files. First we have to find a way to import them into our database. In theory, it would be possible to work on shape files directly, but there are many advantages having the geo data in our database.
ToDo: explain http://80.146.239.139/SpatialTutorial/06-SpatialLiteTiles.html
###Step7: Join with business data and create thematic maps
ToDo: explain http://80.146.239.139/SpatialTutorial/06-SpatialLiteTiles.html
###Step7: Add click-interaction
###Step8-xxx: Advanced rendering, interaction, analysis