GeoServer production environment on Windows Server with IIS and Apache

In this post Andre explained how to set up a dev environment on win7 so Geoserver can coexist with IIS 7.5.

When it comes to deploying Geoserver to a machine that is exposed to the Internet things may go a bit more difficult. It actually took me a while to figure out how to make all the pieces work together so if you are in a similar situation - trying to run Geoserver on Windows Server - keep on reading.

We have decided to create a subdomain that will take all the traffic targeted at our Geoserver. Apart from looking unusual it certainly makes life easier when setting up rewrite rules for IIS. But let me explain it step by step.

 

1. Installing Apache Tomcat

I had some problems with Tomcat 7.0.6 and Geoserver 2.1RC1 (betas failed too) but luckily Tomcat 6.0.3 was ok. When installing the server it is worth to choose the service startup option so the Tomcat service starts automatically with WIndows. If you are running win x64 make sure you choose x64 JRE as well.

 

2. Making Tomcat pick a proper host name

By default Tomcat binds itself to the port 8080 an I have let it do so in this case as well. The problem though is that geoserver will pick the localhost:8080 for the capabilities documents and also for the example pages generated by the layer preview links. This is not a problem when working with geoserver locally but when accesssing it from another machine, the urls have to be resolved properly.

To make Tomcat know how of the host its pages are requested from I had to edit the tomcat/conf/server.xml file. By default the Connector tag does not have the proxyPort and proxyName properties so I needed to add them. proxyPort is the actual port the resource is requested through (the default is 80 and IIS listens at this port of course) and proxyName specifies the host name that will be used. Adding these two properties to the Connector section makes the geoserver report proxyName:proxyPort as the host name instead of localhost:8080 (in my case geoserver now uses http://geoserver.cartoninjas.net:80).

 

3. Deploying Geoserver

Since I have fixed the server.xml file I can start the Tomcat service and navigate to localhost:8080. After logging in I deployed Geoserver by using the war file available at the download page:

 

4. Setting a rewrite rule in IIS

Having installed Geoserver I was ready to route all the trafic to geoserver.cartoninjas.net to my Tomcat. I had the subdomain already created so there had to be a rewrite rule set appropriately. In my case I needed to have all the incoming traffic routed to localhost:8080:

 

5. Testing the geoserver from a remote computer

The final step was to test if everything worked ok. I have navigated to geoserver.cartoninjas.net/geoserver and tested the tiger ny layer group. It looked like everything was ok now ;-)

 

Why bothering with all the steps above? Initially I had the rewriting set in IIS and I could connect to geoserver through my subdomain. The actual problem though was with the capabilities documents but also with some of the automatically generated preview pages. Of course when knowing the actual resource location it was already possible to connect to the services exposed by geoserver. But clients trying to connect to the geoserver services automatically without knowing there was a problem with the host resolution would obviously fail. A dirty solution was to make the client application replace localhost:8080 with the actual host name and we had it working for quite a while. Luckily there was a bit more elegant way of fixng things and now we have our geoserver instance work as expected. I found that little proxy thing here.

Printing a OpenLayers map in ASP.NET

Printing a map created in OpenLayers or other commercial APIs is still not that easy. There are some problems with transparency of the gif and png images but also with the transparency of the vector canvas used to display vectors. It is possible though to print a road map or layers that do not use transparency without problems. What if we use transparent overlays or vector layers? If our project is utilising geoserver then the problem is solved - there is a mapfish printing module available for geoserver and it does a great job. What if the project does not utilise geoserver but some other custom data sources? Well, we'll need to do some work serverside.

In order to do a serverside tile stitching we will need to collect some data on the clientside so we can then grab all the necessary tiles and assemble them together into one piece:

var tiles = [];
for (var l = 0; l < map.layers.length; l++) {

	//grab the layer
	var layer = map.layers[l];
		
	//skip vector layers	
	if (layer.isVector) continue;

	//now check if it is visible and in range (wms)	
	if (!layer.getVisibility()) continue;
	if (!layer.calculateInRange()) continue;

	// iterate through their grid's tiles, collecting each tile's extent and pixel location at this moment
	for (var r = 0; r < layer.grid.length; r++) { //tile rows (grid is an array of rows)
		for (var c = 0; c < layer.grid[r].length; c++) {//columns

			//grab the tile
			var tile = layer.grid[r][c];

			//when using round there would be some gaps between tiles from time to time so ceil is used instead
			var tilePosX = Math.ceil((tile.bounds.left - mapBounds.left) / resolution);
			var tilePosY = Math.ceil((mapBounds.top - tile.bounds.top) / resolution);                 

			//get the layer opacity
			var opacity = layer.opacity ? parseInt(100 * layer.opacity) : 100;

			//collect data for a tile
			tiles[tiles.length] = {
				url: layer.getURL(tile.bounds),
				x: tilePosX,
				y: tilePosY,
				tileSizeW: layer.tileSize.w,
				tileSizeH: layer.tileSize.h,
				opacity: opacity
			};
		}
	}
}

//data to be sent to the serverside
var printData = {
	mapPixWidth: map.getSize().w,
	mapPixHeight: map.getSize().h,
	tileData: tiles
}

If you searched for some OpenLayers printing examples you may have found examples that use:

tile.position.x,
tile.position.y

instead of:

tilePosX = Math.ceil((tile.bounds.left - mapBounds.left) / resolution);
tilePosY = Math.ceil((mapBounds.top - tile.bounds.top) / resolution);

This is quite weird as I expected both yield the same results but when using OpenLayers within ExtJs layouts I encountered some strange results and found out there were some tile origin positioning shifts. Using the 'manual' tile origin calculation seems to fix the problem so I decided to stay with the adjusted code of course ;)

Since we have the tile data collected already it is the time to move to the serverside. The job to be done here is to download all the necessary tiles (the ones that overlap with the map's viewport), stitch them together and save the final image to jpeg, png, pdf, etc.

I had to do printing to jpeg so the example will use jpeg output.

In order to stitch the images together I had to download them first:

//downloads a remote image from given url
private System.Drawing.Bitmap grabImageFromWeb(string requestUrl, int tileWidth, int tileHeight)
{
    //output image
    System.Drawing.Bitmap outputImage = new System.Drawing.Bitmap(tileWidth, tileHeight);

    //test if the request string was passed and if so request data from the destination server
    if (requestUrl != null)
    {
        //create a new HttpWebRequest
        System.Net.HttpWebRequest webRequest;
        webRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(requestUrl);
        webRequest.Method = "GET";

        System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)webRequest.GetResponse();

        //check if the data was successfully retrieved
        if (response.StatusCode.ToString().ToLower() == "ok")
        {
            System.IO.Stream stream = response.GetResponseStream();
            outputImage = (System.Drawing.Bitmap)System.Drawing.Image.FromStream(stream);
        }
    }

    return outputImage;
}

 Having created a method to grab the images off the web I could now do the actual tile collection and stitching (the output of the code below is an image that maps 1:1 to the map extent visible at the user's display):

//output bitmap
System.Drawing.Bitmap mapBitmap = new System.Drawing.Bitmap(printData.mapPixWidth, printData.mapPixHeight);

//compose the map image
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(mapBitmap))
{
    //stitch all the tiles together 
    for (int t = 0; t < printData.tileData.Length; t++)
    {
        //test if a tile overlaps with the output image and grab it only if so
        //tile origin + tile size must be > 0
        //and tile origin < bitmap size
        if (printData.tileData[t].x + printData.tileData[t].tileSizeW > 0 && printData.tileData[t].x < mapBitmap.Width && printData.tileData[t].y + printData.tileData[t].tileSizeH > 0 && printData.tileData[t].y < printData.mapPixHeight)
        {
            g.DrawImage(
                grabImageFromWeb(printData.tileData[t].url, printData.tileData[t].tileSizeW, printData.tileData[t].tileSizeH), //source image
                new System.Drawing.Rectangle(printData.tileData[t].x, printData.tileData[t].y, printData.tileData[t].tileSizeW, printData.tileData[t].tileSizeH),//destination rect
                new System.Drawing.Rectangle(0, 0, printData.tileData[t].tileSizeW, printData.tileData[t].tileSizeH),//source rect
                System.Drawing.GraphicsUnit.Pixel //drawing unit
            );
        }
    }
}

//output file name
string fileName = "Printout_" + DateTime.Now.Ticks.ToString() + ".jpg";

//save bitmap
pageBitmap.Save(Server.MapPath (System.Configuration.ConfigurationSettings.AppSettings["printedFiles"] + "\\" + fileName), System.Drawing.Imaging.ImageFormat.Jpeg);

There are a few things worth remembering here:

  • google layers will not print as there is no direct access to the google tiles through OpenLayers. OL 3.0 though should have direct access to the Bing Maps tiles, so it should be possible to create a printout off the Bing tiles
  • when collecting the tile data I was testing for a few conditions specific to my set up, you may require some more tests (for example if you have gmaps layers the js example shown here will fail as gmaps layer does not have a grid property
  • there may be some other issues with the code shown but the generic idea should be easy to follow
  • with a bit more work one could collect vector data as well and draw the features on the top of the stitched tiles (I actually did it for my app but it wouldn't make sense to show it here as the code was simply too customised)

ASP.NET xDomainProxy for OpenLayers getInfo requests

After a long time of just talking about using geoserver we have eventually installed it on our server. Making it work behind IIS is a subject for another article and I am hoping to post it soon.

Anyway, we decided to make our geoserver available at geoserver.cartoninjas.net, while the applications we write are likely to be hosted under different subdomains or even under different domains. Since x-domain requests are not allowed in JavaScript due to some security restrictions I needed to create a simple server side proxy that would be exposed to application as a local resource and would take care of pulling the info when a getInfo requests are issued by OpenLayers.

I started with setting up a proxyHost in OpenLayers:

//proxy host
OpenLayers.ProxyHost = 'xDomainProxy.ashx?url=';

Then a server side script for a generic handler:

<%@ WebHandler Language="C#" Class="xDomainProxy" %>

using System;
using System.Web;

public class xDomainProxy : IHttpHandler {
    
    public void ProcessRequest (HttpContext context) {

        //OpenLayers.ProxyHost = 'xdomainProxy.aspx?url=' so the requested url is passed in a url param
        string requestUrl = HttpUtility.UrlDecode(context.Request.QueryString["url"]);

        //test if the request string was passed and of so request data from the destination server
        if (requestUrl != null)
        {
            //create a new HttpWebRequest
            System.Net.HttpWebRequest webRequest;
            webRequest = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(requestUrl);
            webRequest.Method = "GET";

            System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)webRequest.GetResponse();

            //check if the data was successfully retrieved
            if (response.StatusCode.ToString().ToLower() == "ok")
            {
                //set the appropriate response content type
                context.Response.ContentType = response.ContentType;

                //Read the stream associated with the response.
                System.IO.StreamReader reader = new System.IO.StreamReader(response.GetResponseStream());

                //and write it to response
                context.Response.Write(reader.ReadToEnd());
            }
        }
    }
 
    public bool IsReusable {
        get {
            return false;
        }
    }
}

And voila, it's ready to be used ;)

A side note: although in many cases this proxy will work properly this is a mini version of the code and should not be implemented in the production environment - it does not catch any errors and will fail if the connection is not available. Also it was written to work with OpenLayers specifically (OpenLayers.ProxyHost) and needs some extra work before it can act as a bit more flexible proxy allowing one to pull data without having to escape the passed url.

Running GeoServer on Windows 7 (x32 or x64) and IIS 7.5

 

GeoServer is one of the most powerful and rapidly evolving geo-server, capable of serving geographical data in-line with standards developed by Open Geospatial Consortium (OCG).

More information about GeoServer can be found on www.geoserver.org.
What is so attractive in GeoServer?

  • it is Open Source software
  • follows OGC standards
  • functionality hugely improves with every new relase
  • it’s fast and scalable
  • it’s free

GeoServer is written in Java and requires Java Virtual Machine to run on. This can be advantageous as it is operating system independent in sense that it will run on any operating system supporting Java Virtual Machine architecture.

Still, most widely used operating system is Microsoft Windows OS.
And many existing applications and geo systems are required to run from Microsoft IIS (for e.g. because applications require .NET environment, that is exactly what Manifold needs).

So our situation:

Application requires IIS and GeoServer requires Java and Java based Web Server like Apache, Jetty etc.

Possible solutions:
1)Keep them separate.
    Install IIS on one server and Apache or Jetty with GeoServer on different machine (can be virtual).
Probably you will need kind of proxy tool especially if you want to be able to exchange data in different format than images.

2)Make your Apache default server.
Make your Apache default server for e.g. running on port 80 and install IIS on different port for e.g. 8080.
Then you can redirect calls using Apache configuration, this might not be loved by application developers developing .NET web applications

Web Client <-> Apache/Jetty (GeoServer) <-> IIS

3)Install IIS and make it your default Web Server.
Make IIS default Web Server (for e.g.  on port 80) and add Apache/Jetty and GeoServer as secondary web server
   
    Web Client <-> IIS <-> Apache/Jetty (GeoServer)

I think the most popular and easiest to set up is 3rd option.
And here is how this can be done.

The key for this is use of Microsoft “URL Redirection” IIS plug-in.

Here I assume that you have:
Windows 7 Operating System with IIS 7.5 running on port 80
you have downloaded and installed Microsoft URL Redirection plug-in

 

 

Apache with GeoServer or standalone GeoServer distribution running on Jetty using port 8080 so direct call to GeoServer instance can be made as below:
http://localhost:8080/geoserver/web/

Where we want to be:

Client call hits IIS first, and then request is passed to URL Rewriter which checks URL for rules we are going to set up. And if fits in rules request is processed by IIS or is sent to Apache/Jetty

Steps to follow:

1)from IIS Manager select “Default Web Site”
2)start “URL Rewrite” application
3)go to “Actions” and select “Add Rule(s)...”

we will use “Blank rule”

populate rest fields as shown below

Don't worry about “Conditions” - there are from my development environment.
Further down you should set fields like below:


“Action” section is particularly important.
So:
Action type: Rewrite
Action Properties:
    Rewrite URL: http://localhost:8080/{R:1}   <- this could be {R:0} read more from URL Rewrite docs
    Append query string: ticked
    Log rewritten URL: un-ticked

“Rewrite URL” is an “url” pointing to your Web Server hosting GeoServer including port

So we have now IIS redirecting every call to http://localhost:8080/  (which in my case is Apache)

Now we want IIS to process every call having certain string in URL request.
In other words we need to build list of redirection exceptions.
Exceptions will be our web applications.

So if we want to add new application/web site to IIS, simply go to “Default Web Site”, and add application. For e.g. our test application is latest GeoExt library, as we want to check examples
http://localhost/GeoExt-1.0

Using IIS Manager
create new application called GeoExt-1.0
select “Default Web Site” from IIS Manager
open “URL Rewrite” application
select redirection rule “GeoServer Redirect” in my example
right-top – Condition -> Add...
make sure “Check if input string:” is set to “Does Not Match the Pattern”, so every thing that is different to “/geoext-1.0” (ignore case) will be passed to Apache and if it has  “/geoext-1.0” as in url request string will be processed by IIS

populate fields like shown below:

 

So now we can call our application like:

 

Main advantage is that we can call GeoServer installed on same server as IIS
And we can call our application:
http://localhost/myApp
and GeoServer
http://localhost/geoserver/web

So we shouldn’t have “same origin” policy rule violated. This is extremely important when exchange data in  formats like XML, JSON, JavaScript and so on.

We still can use full functionality of both, even having IIS running on port 80 and Apache/Jetty on port 8080.
And we don’t need to provide port for this. As per screenshot below, test GeoExt WMS-Capabilities from examples directory where I have changed url to data from “data/wms.json” to http://localhost/geoserver/ows?service=wms&version=1.1.1&request=Getcapabilities

 

 

Hope this helps

Around the world on a motorbike

For the last 3 years I have been almost exclusively providing cartographic services for Bezdroza, one of the largest tourist guide publishers in Poland. Bezdroza has a distinguished protfolio of books and therefore the maps they publish may vary a lot, although they are usually rather guide-like maps like:

This time I was invited as a cartographer to take a part in a project that appeared to be quite unusual - I was to design maps for a book of travel. Nothing unusual so far, rigth? The unusual part was that the book was not to have any maps inside, the maps were supposed to make the cover of the book.

The book tells a story of two journalists that decided to travel around the world on their motorbikes. When they agreed it would be a great fun, they did not have their motorbike driving licenses nor they could ride the bikes. This made me think that it should be fun for me too to design such a cover for this book.

The data came from the Natural Earth data repository and from the GPS tracks supplied by the authors of the book.
For the front cover map I used the the 1:100M data set and generalised GPS data; for the back cover both source datasets were generalised even further.

When designing the 17 globe maps for the back cover I was tempted to script the data preparation process (prepare the data, center and project it, and finally clip it) though after all I took the quicker path and decided to create them manually. Maybe next time, when we are about to design some globe maps for the folks that visited more than 20 countries ;-)

Designing these maps was an enjoyable experience, hopefully you'll like them too.

Front cover:

Back cover:

Both covers:

Higher res files:

Front cover (3,11 mb)

Back cover (2,13 mb)

Both covers (6,14 mb)