Integrating Openlayers and HTML5 Canvas (Revisited)

 cartography, html5, openlayers html5 canvas  Comments Off on Integrating Openlayers and HTML5 Canvas (Revisited)
Jan 282014
 

The WordPress stats tell me there is still a lot of interest in our previous post on integrating OpenLayers and HTML5 Canvas from way back in 2010.
Time has passed, technology has moved on and I’ve started buying shoes in bulk like Mr Magorium. So below, I provide an update on how I integrate OL and HTML5 Canvas 3 years on.

Previously my approach was to replace each individual tile image with a corresponding canvas element, the same size as the tile (typically 256*256). Also we used JQuery to capture tile rendering events. The updated approach is to capture tile images as they are rendered by OpenLayers using OL built in event listeners and then draw these onto a single HTML5 canvas element.
Apart from being more efficient and producing cleaner, more robust code, this approach has the advantage that you can use HTML5 to draw shapes, lines and manipulate pixels on a single canvas tile, crossing tile boundaries. This is particularly useful for drawing lines and shapes using paths (e.g. lineTo() , moveTo() functions).

To demonstrate this I’ve set up a simple demo that shows the HTML5 Canvas adjacent to a simple OpenLayers map, where the canvas version (on the right hand side) is manipulated to show a grayscale and inverted version of the original map image (grayscale is triggered by loadend and the invert function by moveend) The source code is available on EDINAs gitHub page. (https://github.com/edina/geomobile) and on JS Fiddle page.
The solution hinges on using the OpenLayers.Layer loadend event to capture the tiles when OpenLayers has finished loading all the tiles for a layer, and also the OpenLayers.Map moveend event, which OpenLayers triggers when it has dealt with the user panning the map. The former is shown in the code snippet below:
// register loadend event for the layer so that once OL has loaded all tiles we can redraw them on the canvas. Triggered by zooming and page refresh.

layer.events.register("loadend", layer, function()
{

// create a canvas if not already created

....

var mapCanvas = document.getElementById("mapcvs" ) ; // get the canvas element
var mapContainer = document.getElementById("OpenLayers.Map_2_OpenLayers_Container") ; // WARNING: Brittle to changes in OL

if(mapCanvas !== null)
{
var ctx = mapCanvas.getContext("2d") ;
var layers = document.getElementsByClassName("olLayerDiv") ; // WARNING: Brittle to changes in OL

// loop through layers starting with base layer
for(var i = 0 ; i < layers.length ; i++)
{

var layertiles = layers[i].getElementsByClassName("olTileImage") ; // WARNING: Brittle to changes on OL

// loop through the tiles loaded for this layer
for(var j = 0 ; j < layertiles.length ; j++ )

{
var tileImg = layertiles[j] ;
// get position of tile relative to map container
var offsetLeft = tileImg.offsetLeft;
var offsetTop = tileImg.offsetTop ;
// get postion of map container
var left = Number(mapContainer.style.left.slice(0, mapContainer.style.left.indexOf("p"))) ; // extract value from style e.g. left: 30px
var top = Number(mapContainer.style.top.slice(0, mapContainer.style.top.indexOf("p"))) ;
// draw the tile on the canvas in same relative postion it appears in OL map
ctx.drawImage(tileImg, offsetLeft + left, offsetTop + top) ;

}

greyscale(mapCanvas, 0, 0, mapCanvas.width, mapCanvas.height) ;
// uncomment below to toggle OL map on /off can only be done after layer has loaded
// mapDiv.style.display = "none" ;

}
}
});


Note that some of the code here comes with a health warning. The DOM functions used to navigate the OpenLayers hierarchy is susceptible to changes in the Open Layers API so you need to use a local copy of OpenLayers (as is case in GitHub sample) rather than point to the OpenLayers URL (as is case in the JS Fiddle version).  Also note that all layers are drawn to the Canvas, not just the one that Open Layers triggered the loadend event for. This is necessary to ensure that the order of layers is maintained. Another issue to be aware of when using Canvas drawing methods on maps  is the likelihood of a CrossOrigin tainting error. This is due to map images being loaded from a different domain to that of the HTML5 code. The error will not get triggered simply by drawing the tiles to canvas using the drawImage() function, but does fail when you attempt pixel manipulation using functions such as putImageData() . OpenLayers handles this using the Cross-Origin-Resource-Sharing protocol which by default is set to ‘anonymous’ as below. So long as the map server you are pointing to is configured to handle CORS requests from anonymous sources you will be fine.

layer.tileOptions = {crossOriginKeyword: ‘anonymous’} ;

Would be interested to hear if others are doing similar or have other solutions to doing Canvasy things with OpenLayers.

OpenLayers Canvas Capture


Oct 242013
 

By Fiona Hemsley-Flint (GIS Engineer)

Whilst developing the background mapping for the Fieldtrip GB App, it became clear that there was going to have to be some cartographic compromises between urban and rural areas at larger scales; Since we were restricted to using OS Open products, we had a choice between Streetview and Vector Map District (VMD) – Streetview works nicely in urban environments, but not so much in rural areas, where VMD works best  (with the addition of some nice EDINA–crafted relief mapping) . This contrast can be seen in images below.

streetview1VectorMapDistrict1

Streetview (L) and Vector Map District (R) maps in an urban area.

streetview2VectorMapDistrict2

Streetview (L) and Vector Map District (R) maps in a rural area.

In an off-the-cuff comment, Ben set me a challenge – “It would be good if we could have the Streetview maps in urban areas, and VMD maps in rural areas “.

I laughed.

Since these products are continuous over the whole of the country, I didn’t see how we could have two different maps showing at the same time.

Then, because I like a challenge, I thought about it some more and found that the newer versions of MapServer (from 6.2) support something called “Mask Layersâ€�  – where one layer is only displayed in places where it intersects another layer.

I realised if I could define something that constitutes an ‘Urban’ area, then I could create a mask layer of these, which could then be used to only display the Streetview mapping in those areas, and all other areas could display a different map – in this case Vector Map District (we used the beta product although are currently updating to the latest version).

I used the Strategi ‘Large Urban Areas’ classification as my means of defining an ‘Urban’ area – with a buffer to take into account suburbia and differences in scale between Strategi and Streetview products.

The resulting set of layers (simplified!) looks a bit like this:

masking example

Using Mask layers in MapServer 6.2 to display only certain parts of a raster image.

Although this doesn’t necessarily look very pretty in the borders between the two products, I feel that the overall result meets the challenge – in urban areas it is now possible to view street names and building details, and in rural areas, contours and other topographic features are more visible. This hopefully provides a flexibility  for users on different types of field trips to successfully implement the background mapping.

Here’s a snippet of the mapfile showing the implementation of the masking, in case you’re really keen…

#VMD_layer(s) defined before mask

LAYER

…..

END

#Streetview mask layer

LAYER

NAME “Streetview_Mask”

METADATA

…..

END

#Data comes from a shapefile (polygons of urban areas only):

DATA “streetview_mask”

TYPE POLYGON

STATUS OFF

END

#Streetview

LAYER

NAME “Streetview”

METADATA

…..

END

#Data is a series of tiff files, location stored in a tileindex

TYPE Raster

STATUS off

TILEINDEX “streetview.shp”

TILEITEM “Location”

#*****The important bit – setting the mask for the layer*****

MASK “Streetview_Mask”

POSTLABELCACHE TRUE

END