October 6, 2011

Maps with HTML5 Canvas

This post is a summary of my first experiments drawing maps with the HTML5 canvas element.  The beautiful map of Romanian counties below was dynamically drawn when you loaded this page.
Your browser is not hip enough to support HTML5 canvas.
First you need some files for JavaScript to munch on. Quantum GIS can help you generate GeoJSON. Just add your shapefiles to your project, right click on the layer in the table of contents and select 'Save As. '  From the format dropdown select GeoJSON.

The code to create the image above is here.  I start off by loading the JSON, getting the bounding box of the data,  setting the scale of the drawing, and finally drawing the shapes.  The canvas element has to be present in your HTML page already

var canvas, ctx, xMin, xMax, yMin, yMax, drawScale;


function loadJson(){
 canvas = document.getElementById('map');
 ctx = canvas.getContext('2d');  
 ctx.strokeStyle = "#000000";  
 ctx.fillStyle = "#FFA500";
 
 var geojson= {"type":"FeatureCollection","features":[{"type":"Feature","properties":{"ROM_DIST_I":1.000000,"ID":"RO-01","NAME":"Alba","AREA":6260.95},"geometry":{"type":"Polygon","coordinates":[[[22.67,46.51],[22.69,46.56],[22.73,46.57],[22.79,46.57],    ///.....   load the rest of your GeoJSON here or better yet, get it asynchronously 


//traverse the features to get the bounding box 
 traverseFeatures(geojson.features,'bbox');
 
 //calculate width and height of data.  divide canvas by data dimensions to get a suitable scale
 xScale = canvas.width/Math.abs(xMax-xMin);
 yScale = canvas.height/Math.abs(yMax-yMin);
 
 //calculate a vertical and horiz scale to fit the data to the canvas.  
 //pick the smaller scale of the two for the rendering
 drawScale = xScale<yScale ? xScale : yScale;
 
 //traverse the features again to draw 
 traverseFeatures(geojson.features,'draw');


 
}

function traverseFeatures(features,action){
 for(var i=0; i<features.length; i++){
  var coords = features[i].geometry.coordinates;
  var geomtype =  features[i].geometry.type; 
  if(geomtype=="Polygon"){
   //Polygons have just one array of coordinates
   traverseCoordinates(coords[0],action);
  }
  else if(geomtype=="MultiPolygon"){
   //Multipolygons have several arrays of coordinates, so loop through those
   for(var k=0; k<coords.length; k++){ 
    traverseCoordinates(coords[k][0],action);
   }
  }
 }
}

function traverseCoordinates(coordinates,action){
 for(var j=0; j<coordinates.length; j++){
  var x = coordinates[j][0];
  var y = coordinates[j][1];
  
  if(action == 'bbox'){
   xMin = xMin<x?xMin:x;
   xMax = xMax>x?xMax:x;
   yMin = yMin<y?yMin:y;
   yMax = yMax>y?yMax:y;
  }else if(action == 'draw'){
   x = (x-xMin)*drawScale;
   y = (yMax-y)*drawScale;
   if(j==0){
    //begin drawing on the first point
    ctx.beginPath();
    ctx.moveTo(x,y);  
   }else{
    //continue drawing
    ctx.lineTo(x,y); 
   }
  }
 }
 
 if(action == 'draw'){
  //close the fill and the stroke
  ctx.stroke();
  ctx.fill();
 }
}

Shortcomings with this approach:

  • the canvas element is static. Once the shapes are drawn there's not much you can do with them.  In Flash any shape could have user interactions associated with it, but with the canvas you have to be really creative if you want to develop something as basic as a mouse-over highlight.  Might want to use SVG for that. Some impressive work in this field has already been done by sites like GIS Cloud using Leaflet vectors.  I'm glad they acknowledge that we were able to do this all with Flash years ago and with very good performance, but alas, everything in software has to be reinvented so we can all have jobs.
  • a 30 MB file GeoJSON will crash the browser