Creating a SharpMap WMS Server using .NET MVC4 Razor
Lately I’ve been playing with the idea of creating a lightweight method of displaying geospatial data. I’m currently working on a project using NHibernate Spatial, .NET MVC4 and Sql Server 2012.
As our deployment will initially be without a full server install (no dedicated server) I can’t depend on a GeoServer installation what I would normally do. So I went to my best buddy Google and found a WMS implementation built in SharpMap which is nearing its 1.0 release. (Currently RC2)
I would consider this a success if I could show a GeoAPI IPoint on OpenLayers without using Geoserver/ArcGis or some other server-based installation. Our goal is to create a re-usable and “dummy-proof’ WMS server, usable to easily plug geospatial data onto the web.
Structure
If you look at your typical WMS server, there’s usually a part where you setup a database connection, a part where you tell it which layers (aka tables/views) to use and how it will look like. SharpMap’s WMS Component can do just the same. It has a Map object that contains a list of Layers.
Looking into the Layers I noticed the basic Vector Layer. It looks like your basic layer to use in a thing like this, as I was going to serve only my own data. I think you should never serve your own raster data unless it has some unique feature in it. In my case, I just want a map, I want it to be worldwide and I want it to be free. So I’m using OpenStreetMap.
Back to my data, I’ve figured out I would need 2 layers at first. Normally I would create one map, and add 2 layers to it. However, in my setup I wanted to try a different approach. Normally I would look at my data from a Map perspective, but this time I took a swing at it from the data side of things.
In our project, Draconium, we use NHibernate for storing our data, and use the spatial extension to add spatial data to it. We have implemented the Unit of Work and Repository patterns and I feel that all our data should be accessed through it. As I normally would create a database connection and let the WMS server do all the querying, I wanted to try to do it myself. SharpMap has a WMS example, which is built using MVC2. For Draconium though, we decided on MVC4 using Razor, so I could not ‘steal’ the code. But that example proved a great start for building my generic NHibernate MapServer. First of all, it shows us how to setup basic things like a capabilities description and of course how to handle a request. (But we’ll get to that later)
Back to our structure! Our data is safely within my SQL Server database and I don’t want SharpMap to go query on itself. Why? Because of application rules, authorization, etc. If I define a rule where user A is not allowed to see something, I only want to write that rule ONCE.
So in short: I won’t allow anything to access my database without using my data layer.
Displaying Data
When you create a Vector layer, you have to pass a Provider. Usually (and in my very first SharpMap WMS built) this would be a database provider, such as the SQLServer2008 provider. However, as stated before, I don’t want something circumventing my business rules, so I canned this.
A different, better suited provider is the GeometryProvider. It’s really as simple as the name states: you fill it with some geometries. (GeoAPI IGeometry as of SharpMap 1.0! yeah!)
Then you can create a layer with it:
[sourcecode language=”csharp”]var Geoms = new List
foreach (var feature in features)
{
Geoms.Add(feature.Geometry);
}
Provider = new SharpMap.Data.Providers.GeometryProvider(Geoms);
var layer = new VectorLayer(LayerName, Provider);
layer.Style = new SharpMap.Styles.VectorStyle();
//add some style to it. (could be images and such,
// but let’s keep it simple< for now...)
layer.Style.PointColor = Brushes.Red;
layer.Style.PointSize = 5;
layer.SRID = Srid;
[/sourcecode]
This layer can then be added to the Map object and it will be filled with whatever was in the geoms collection. I don't want to setup a map object every time somebody requests a tile. Also, I want the map to work in the context of one entity, so I can scale my server when needed. So basically, I got a new requirement here: I want to be able to serve my entities from different servers. In order to do this, I would need a Map object for each entity. I also don't want to think about SharpMap whenever I'm using it. (This proved difficult)
I started out with creating a generic MapServer:
[sourcecode language="csharp"]
public class MapServer
[/sourcecode]
Making sure you could only create it with a type that has something geometric. Geography is not used right now so my code doesn’t accept it. (IPersistableGeoEntity states that it should have and ID (used for NH) and a Geometry field)
The MapServer should have some fields to setup with like a private SharpMap Map, perhaps some styling options and a layer name. In my case I want only one layer per map, so a single name will suffice. It really depends on the angle you are getting at. Also, I don’t want the MapServer to go find its own geometries to display, so I gave it methods to add Features to the Map. (through the Add function of Provider.Geometries)
Now this appears to be a costly piece of code. It converts some data, it sets up providers and it will eventually fill up the Map object. When using a popular web mapping toolkit as OpenLayers, you will have 16 tiles per page view when using tiling. This would mean you will be setting up and tearing down your MapServer 16 times per request and per layer. Of course, you can tailor your MapServer to contain the features it needs for that tile, but to me this felt like the wrong way of fixing this. I’d rather limit the number of time we have to rebuild the MapServer object.
Also, on our project, we don’t have that much data changing every minute, but we do want to be in total control of when data should be refreshed.
Now I would normally take a look at something like Web Tile Cache to prepare tiles without data, but this is a server process. I would either need to install it on the server (which I can’t) or have to do manually on my desktop pc once in a while.
The thing is, I’m not sure how long tiling all my data is and I’m not sure how often I will have to refresh it.
To take down both points, I decided to keep my data in memory throughout the application lifecycle by using a Singleton class to store it in. I will set it up on Application_Start() and fill it with everything right then.
This does change our MapServer a bit. As this object will retain in memory as long as IIS keeps my ASP.NET application running, I will not go to the database and get all objects. For now we are not counting on our system to be wildly successful at start, I don’t believe will we get hundreds or even dozens of new entities in our database on a daily basis, but still I don’t want to restart my application every time I want to see fresh data.
So I added a seeding method to the WmsContext singleton. If called, it will throw away the current data and seed the MapServer(s) from database. I borrowed the term seeding from Tile Cache as I felt it was the appropriate terminology.
Using the WMS server
At this point, we have a singleton WmsContext, which holds one or more MapServer
That’s fine and all, but we still have no URI for OpenLayers to use…
First let’s do a quick breakdown of a WMS request:
http://yourserver.com/Wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=….
This is the most basic request. It states it wants to use the WMS specification version 1.3.0 and it wants something from the server. The most used of course is REQUEST=GetMap, but the WMS specification states a lot more. SharpMap uses WMS 1.3.0 by default and only preaches that version. I’ve read some posts about using 1.1.0, but as I’m building both the server as the client side of this project, 1.3.0 is good for me.
Another important request is the GetCapabilities request, which tells us what our server is capable of. It also tells us who is responsible for maintaining the server. First let’s make sure this is filled, we have to create a Capabilities.WmsServiceDescription object for it:
[sourcecode language=”csharp”]
public class MapHelper
{
public static Capabilities.WmsServiceDescription GetDescription(string url)
{
Capabilities.WmsServiceDescription description = new Capabilities.WmsServiceDescription(“WMS NAME”, url);
description.MaxWidth = 256;
description.MaxHeight = 256;
description.Abstract = “Map Server maintained by Acme. Contact: me@myself.com.”;
description.Keywords = new[] { “some keywords” };
description.ContactInformation.PersonPrimary.Person = “you?”;
description.ContactInformation.PersonPrimary.Organisation = “your company?”;
description.ContactInformation.Address.AddressType = “postal”;
description.ContactInformation.Address.Country = “Netherlands”;
description.ContactInformation.VoiceTelephone = “-“;
return description;
}
}[/sourcecode]
I created this as a static function as I don’t have to keep creating some class object to get to this data.
Note the MaxWidth and MaxHeight settings. They are the maximum size of a tile to be requested on your wms server. 256 by 256 is OpenLayers default. If you want to use larger tiles or no tiling at all, you should increase these numbers.
Time to tie it all together! Using MVC4, a URL yourserver.com/Wms?SERVICE=WMS… would imply WmsController, so let’s create just that. As I have a different MapServer for each Layer, it makes sense for me to have a different URL for each as well. Using logical naming, a MapServer
So the controller will get a function Zoo, which should return an image and will look somewhat like this:
[sourcecode language=”csharp”]
public ContentResult Zoo()
{
// first get the URL
HttpRequestBase request = HttpContext.Request;
Uri uri = request.Url;
string url;
string absoluteUri = uri.AbsoluteUri;
if (uri.Query.Length > 0)
{
string s = absoluteUri.Replace(uri.Query, string.Empty);
url = s;
}
else
{
url = absoluteUri;
}
// Process the request on a copy of the map object.
Map map = WmsCont:ext.Instance.ZooServer.GetMap();
var description = MapHelper.GetDescription(url);
// let SharpMap parse the query onto our map (static function)
WmsServer.ParseQueryString(map, description);
var image = map.GetMap();
var result = new ContentResult();
result.ContentType = “image/png”;
result.Content = image.ToString();
return result;
}
[/sourcecode]
A few notes with this piece of code. At first, the ambiguous use of the term GetMap. The ZooServer has a private SharpMap.Map object with the layers. If and when WmsServer parses the querystring, it will in fact change the Map Object itself. It will change the zoom level, the bounding box, etc. This is fine for a single threaded application, but as multiple visitors may ask for a map. (Or in fact, OpenLayers requesting 16 at a time) we want to do the operations on a clone. The Server.GetMap() will therefor lock the Map object, create a clone, release the lock and return the clone.
The map.GetMap() function will actually ask the SharpMap Map Clone object (still with me?) for a picture of the current map. This image would then be the 256×256 pixels tiles OpenLayers requested.
OpenLayers
This will now be a piece of cake as we have no hoops to jump through on client side. Only a few things to look out for:
– Default SharpMap will use WMS 1.3.0, OpenLayers will by default use 1.1.0. You will have to tell OL to use 1.3.0 with the WMS Parameter “version”.
– The projections param for WMS 1.3.0 has been changed to “CRS” instead of the old “SRS”
– SharpMap wants its params uppercase and is case sensitive. OpenLayers will also use its parameters UC, but if you want to test URL’s by hand, remember to UC it all.
– Make sure your OpenLayers Map, OpenLayers Layer, SharpMap Map and SharpMap Layer are all using the same projections. When using OpenLayers with WMS, the overlays should be in the same projection as the base layer. In my case it meant reprojecting and storing all my data in Web Mercator (EPSG:3857) as I want to do as little on the fly reprojecting as possible.
So this wraps up the article. I hope this gives you a guide on how to use SharpMap in different ways and some caveats I stumbled on. Ofcourse I can go in greater detail on this subject, but I have no idea what part I should focus on. So if you are reading this, please tell me what you think of this article and if I should go into more detail on particular parts of it.