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)
blog comments powered by Disqus