Documentation/Dev/proj4js

OpenLayers and Proj4js

Map makers map the real world into a map. That map is a projection of the real world. The projection tries to project something that is on a sphere into a flat image.

There are numerous ways of doing this, and people have used numerous different projections in maps.

If you are a mathematician, you can transform one co-ordinate system into another, using sinuses, cosinuses, curvatures and what not. For those of us who are not, there is the Proj.4 project.

Proj.4

The Proj.4 home page can be found  here. And the javascript implementation is  here. Sample of usage of Proj4s can be found there as well. But OpenLayers integrates with Proj4js if the library is available. This will make using it a bit easier. Having it in the same folder as OpenLayers.js will make it available.

Projection definitions

You have to start out by defining the proj4js definition objects. One will be the source projection, and the other the target. If you are lucky the projection is predefined, and you can use it directly:

var source = Proj4js.Proj('EPSG:4236');    //source coordinates will be in Longitude/Latitude

Or you may need to create the definitions first by specifying all elements explicitly:

Proj4js.defs["EPSG:900913"] = "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs";
Proj4js.defs["EPSG:28993"] = "+proj=sterea +lat_0=52.15616055555555 +lon_0=5.38763888888889 +k=1.0 +x_0=155000 +y_0=463000 +ellps=bessel +towgs84=565.,49.9,465.8,-0.409,0.36,-1.869,4.08 +units=m +no_defs";

Proj4js comes with a lib/defs folder that contains predefined projections, which can be included in your javascript. If your desired projection is not there, go to  http://spatialreference.org/, search for your projection and choose to display the proj4js definition string. This can be pasted into your application.

OpenLayers.Projection

This is the class that handles the transformation. Its api is  here. Projections can be applicable to the whole map and to individual layers. Transformations are mostly performed automatically. To set the projection of the map (given the two definitions above):

var options = {
  projection:"EPSG:900913",
  displayProjection:new OpenLayers.Projection("EPSG:28993"),
  maxExtent(minx, miny, maxx, maxy) // these are google coordinates
};
map = new OpenLayers.Map('map', options);

This gives you a map that uses 900913 internally, and shows 28993 externally. For instance, if you add a MousePosition control, the returned coordinates would be in 28993. However, you can use the function below to handle it yourself:

function onMouseMove(event){
    var ll = map.getLonLatFromPixel(event.xy).transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:28993'));
    var out = ll.lon.toFixed(0) + ", " + ll.lat.toFixed(0); // no more detail than meters.
    OpenLayers.Util.getElement("coords").innerHTML = out;
}

Setting the projection of a layer

If the layer's projection is different from the one used by the map, you need to specify what projection that is:

      var layer = new OpenLayers.Layer.GML(geojsonUrl, {format:OpenLayers.Format.GeoJSON, projection: new OpenLayers.Projection("EPSG:28993")});
      map.addLayer(layer);

With layers from MapServer, the projection can be handled on the server. If you use this in the map file:

MAP
  ...
  PROJECTION
    "init=epsg:900913"
  END
  WEB
    METADATA
      ...
      wms_srs "epsg:900913"
    END
  ...
  LAYER
    ...
    PROJECTION
      "init=epsg:28993"
    END
    ...
  END
END

MapServer (with proj4 support, of course) will do the reprojection for you. In OpenLayers you can then do:

      var layer = new OpenLayers.Layer.WMS(name, url, {layers:layerName, format:'png24'});
      layer.addOptions({isBaseLayer:false, sphericalMercator:true});
      map.addLayer(layer);

You have to make sure that the projections used by MapServer and OpenLayers are identical.

Reading vector data

Sometimes you want to show data on your map that comes from other systems. In my case, the interaction is with the Plone content management system. We allow users to enter search terms, and the features are found in Plone and returned to OpenLayers through OpenLayers.loadURL(). If the data on the map is of the Vector type, you must code the transformation on the feature collection explicitly. This sounds a lot more complex than the actual coding:

function search()
{
   ...
   OpenLayers.loadURL(url_with_search_parameters, "", null, callback);
   ...
}

function callback(response) {
    var g =  new OpenLayers.Format.GeoJSON({internalProjection: new OpenLayers.Projection("EPSG:900913"), externalProjection: new OpenLayers.Projection("EPSG:28993")});
    var feat = g.read(response.responseText);
    vector_layer.addFeatures(feat);
}

The GeoJSON parser in OpenLayers does all the hard work.

Bounding boxes

The same principle applies to bounding boxes. If the other system gives you a bounding box in their coordinate system, you have to transform it into the one your map is using. Again OpenLayers makes this easy:

var bbox = new OpenLayers.Bounds(x[0],x[1],x[2],x[3]);
bbox.transform(new OpenLayers.Projection('EPSG:28993'), new OpenLayers.Projection('EPSG:900913'));

Measure control

If you add a measure control to your map (or both of them: length and area), the results are given in the map's projection, I think. So, if you have a different DisplayProjection that will lead to strange results. You will have to transform the event's geometry and call getArea and getLength on the transformed geometry. Then the units may not match the answers (m vs km, for instance), so you need to modify the values yourself (code adapted from the OpenLayers Measure Example):

  function handleMeasurements(event) {
    var geometry = event.geometry.transform(new OpenLayers.Projection('EPSG:900913'), new OpenLayers.Projection('EPSG:28993'));
    var units = event.units;
    var order = event.order;
    var element = document.getElementById('measurement');
    var out = "";
    // because of the transformation units no longer matches the outcome of getArea and getLength, those are in layer units.
    if(order == 1) {
      if (units=='km'){
        out += "Distance: " + (geometry.getLength()/1000).toFixed(0) + " km";
      }else{
        out += "Distance: " + geometry.getLength().toFixed(0) + " m";
      }
    } else {
      if (units=='km'){
         out += "Area: " + (geometry.getArea()/1000000).toFixed(0) + " km<sup>2</sup>";
      }else{
         out += "Area: " + geometry.getArea().toFixed(0) + " m<sup>2</sup>";
      }
    }
    element.innerHTML = out;
  }

ScaleLine control

If your DisplayProjection is not the same as the map projection and your base maps are Google's, you should not add a ScaleLine control until  this bug is fixed. The ScaleLine is nearly twice too large.