Peter Robins, his website

Using OpenLayers: Changing the Projection

Those first examples used the default projection, 4326 (which OL uses as shorthand for the Plate Carrée projection, i.e. geographical coordinates used as x/y coordinates, i.e. pretending the earth is a flat rectangle). Now let's look at an example using a 'proper' projection. For this, I'll use the Spanish WMS in one of the UTM projections, EPSG:23030.


    map.projection = "EPSG:23030";
    map.displayProjection = new OpenLayers.Projection("EPSG:4326");
    map.maxExtent = new OpenLayers.Bounds(-100000, 3950000, 1150000, 4900000);
    map.resolutions = [1800,900,450,225,120,50,25,10,4.5,3,2,1,0.5];
    map.tileSize = new OpenLayers.Size(200, 200);
    var ign = new OpenLayers.Layer.WMS( "IGN topo",
        "http://www.idee.es/wms/MTN-Raster/MTN-Raster", {layers: 'mtn_rasterizado'} );
    map.addLayer(ign);
    ign.attribution = 'IGN topo maps from <a target="_blank" href="http://www.idee.es/">IDEE</a>';
    var pnoa = new OpenLayers.Layer.WMS( "PNOA aerial/topo",
        "http://www.idee.es/wms/PNOA/PNOA", {layers: 'pnoa'} );
    map.addLayer(pnoa);
    pnoa.attribution = 'PNOA photos from <a target="_blank" href="http://www.idee.es/">IDEE</a>';

The first few lines show what has to be set up for this. First we define the projection (I'll come back to displayProjection in a moment); the WMS only covers Spain, so we set the maxExtent (based on that given in the WMS getCapabilities). This is a full WMS, so is capable of providing any resolution you like; however, these are rasterised images so you're really limited to whatever resolutions the images were rasterised at. Unfortunately, in this case I've found that trial and error is the only way of establishing which are the most sensible resolutions. This list of resolutions of course corresponds to the zoomlevels, so in this case, there will be 14 zoomlevels. I provide whole numbers, and also set the tilesize to 200x200px so that the tiles provided are easier to relate to m/km on the ground (in principle, it's rather odd to have a tileSize property on the map, as layers not maps have a tileSize, but it does mean that if all your layers are the same you only need to set it once). I add two layers, one for the rasterised topo-maps, the other for orthophotos (only provided at higher zoomlevels). These are thus both baselayers, and both in the same projection. As there are now 2 layers, I add a layerswitcher control so users can change from one to the other:


    new OpenLayers.Control.LayerSwitcher( {title: 'Switch/add/remove layers'} )

You can define the map options after creating the map, as I do above, or you can add these new options to the options object given to Map at instantiation; the latter is probably the better way of doing this.

The way projections are defined in OL is rather confusing - frankly it's a bit of a muddle - so here's some more details. map.projection is a string with a default value of EPSG:4326; it is thus different from layer.projection, which can be a string or a projection object. When a layer is added to a map, if layer.projection is not set, it's taken from map.projection; if the result is a string, it's converted to a projection object. So after being added, map.projection is always a string, layer.projection is always a projection object. Map.js has two methods, getProjection() and getProjectionObject(), but if you read the docs for these you'll find that the logic will change with version 3.

In principle, you can have layers in different projections, but I've not come across any need for this, so to simplify things I always define map.projection, and then let all layers be set to that projection. An advantage of this is that I can avoid getProjection() and getProjectionObject() by using map.baseLayer.projection, which I know is always a projection object.

Adding coordinate conversion

Once you start using different projections, you have to start thinking about the possibility of converting coordinates from one projection to another, either when displaying those coordinates, or when reading in vector data in a different coordinate system from the map's. As stated before, this conversion logic is not provided by OL (apart from the special case of SphericalMercator, as described below), so you have to use Proj4js, which you will have to include in your page:


<script src="http://proj4js.org/lib/proj4js-compressed.js"></script>

You then also need to provide the definition for the particular projection/coordinate system, so that Proj can use it to convert the coordinates. You can get these from a Proj installation or online from spatialreference.org. Here, we use EPSG:23030, for which the proj4js def is here. You then stick this at the top of your script so proj4js can use it.

However, there is a complication in the case of 23030 for Spain, as you have to allow for the datum shift, otherwise conversions in/out of 4326 will be about 100m out. You do this by providing the towgs84 parameter, so the full def at the top of the script looks like:


Proj4js.defs["EPSG:23030"] = "+proj=utm +zone=30 +ellps=intl +towgs84=-131,-100.3,-163.4,-1.244,-0.020,-1.144,9.39 +units=m +no_defs";

Once this is set up, you can then use it for any other coordinate transformation you like. LonLat, Bounds and Geometry objects all have a transform() method which can be used for this. For example, the initial setCenter() coordinates can either be given directly in the map projection, or they can be given as a LonLat and then transformed. For example, for roughly the centre of Spain:


    var lonlat = new OpenLayers.LonLat(-3.57138, 39.8384);
    lonlat.transform(map.displayProjection, map.baseLayer.projection);
    map.setCenter(lonlat, 5);

Note that transform() changes the actual object, so lonlat.lat and lonlat.long will be in the new coordinate system, and no longer the geographical coordinates entered.

SphericalMercator

There is one special case which OL does provide the transform() logic for, namely SphericalMercator, also known as Web Mercator. This is the name given to the projection used by the commercial API providers, such as Google, and also by OpenStreetMap. It has been given the code EPSG:3857, though OL still uses the older, unofficial EPSG:900913 (which looks a bit like 'google'!). For this, there is no need to use Proj4js. The way this is defined changes with 2.12. There is no longer any need to set any layer parameters for Google, Bing or OSM layers, as these are set by default. You can set them on the map as for other projections (as above). There's an example of this, plus the map options needed, in spherical-mercator.html in the OL examples. I also use OpenStreetMap as the baselayer in my edit example later. Again, once this is set up, you can then transform LonLat, Bounds or Geometry objects as above. Besides EPSG:3857 and the fictitious EPSG:900913, OL also supports the equally fictitious EPSG:102100 and EPSG:102113, used by ArcGIS.

displayProjection

There are currently 3 controls which use displayProjection: ArgParser, MousePosition, Permalink. This is to enable converting the coordinates into a projection other than the map's. So when, for example, MousePosition needs to display coordinates, it checks whether displayProjection is set, and if so converts them from the map projection to the displayProjection, as with transform() above. (displayProjection too is always a projection object.)

Look at the docs for Map.displayProjection, and you will see that if it's set it will be used on any control where it's not set. This means you don't have to define it on each control, but of course you have to make sure you set it on the map before adding the controls.

On my maps, I have two MousePositions, for both lat/longs and projected x/y coordinates:


    map.addControl(new OpenLayers.Control.MousePosition( {id: "ll_mouse", formatOutput: formatLonlats} ));
    map.addControl(new OpenLayers.Control.MousePosition( {id: "utm_mouse", prefix: "UTM ", displayProjection: map.baseLayer.projection, numDigits: 0} ));

The first one is set from the map (4326, as we defined at the top of the page); the second is in the UTM metres, using the numDigits option to chop off decimal places. Of course, we also have to define in CSS where we want these to appear:


.olControlMousePosition {
    position: absolute;
    right: 10px;
    top: 0px;
    height: 15px;
    font-size: 8pt;
    background-color: white
}
#utm_mouse {
    top: 15px;
}

I also use the formatOutput option to add a DMS version to the lonlats by adding the following function to our JS:


    function formatLonlats(lonLat) {
        var lat = lonLat.lat;
        var long = lonLat.lon;
        var ns = OpenLayers.Util.getFormattedLonLat(lat);
        var ew = OpenLayers.Util.getFormattedLonLat(long,'lon');
        return ns + ', ' + ew + ' (' + (Math.round(lat * 10000) / 10000) + ', ' + (Math.round(long * 10000) / 10000) + ')';
    }

View/download page with this logic

April 2010, updated January 2012