Producing Rotated Maps

MapServer 4.3 (as of May 25th, 2004) includes the experimental ability to render rotated maps implemented by FrankWarmerdam?.

Via .map file

Within a map file use the ANGLE keyword in the MAP section of the .map file to specify the angle of rotation in degrees to apply. The map view rectangle (specified in EXTENTS) will be rotated by the indicated angle in the counter-clockwise direction around the center of the extent. Note that this implies the rendered map will be rotated by the angle in the clockwise direction.

eg.

  NAME TEST
  SIZE 300 400
  EXTENT 360015 4359225 371025 4373355
  ANGLE 30
  IMAGECOLOR 255 255 0
  ...

Via mapscript

In mapscript use the setRotation() method on a map to set the rotation. The semantics are the same as in the mapfile.

eg.

 #!/usr/bin/env python

 import mapscript
 map = mapscript.Map('test1.map')
 for i in range(36):
    angle = i*10
    map.setRotation( angle )
    image = map.draw()
    image.save( 'test-%03d.png' % int(angle) )

Usage Limitations

While the map is rendered rotated, the fact that it is rotated is not preserved in the form variables used in regular MapServer cgi mode. That is, subsequent zoom, recenter, query and other operations will not be aware of the rotation and will not take the rotation into account when mapping screen locations to georeferenced space.

In short, rotation is primarily useful in MapScript where the rotation can be generated and taken into account explicitly.

Internally rotation is hidden internally as if it was part of the reprojection operation. Thus for rotation to work properly it is currently necessary to include a valid projection object on the map and every layer of the map. If no real reprojection is needed the projection objects should contain the same projection definition.

Internal Implementation

geotransform

Internally a six parameter "geotransform" is now used to hold the mapping between map pixel/line space and georeferenced space.

  geo_x = gt[0] + gt[1] * pixel_x + gt[2] * pixel_y
  geo_y = gt[3] + gt[4] * pixel_x + gt[5] * pixel_y

This geotransform[] represents an affine transformation capable of accounting for offset, shear and rotation. The geotransform is the same (primary) means used by GDAL to relate pixel/line and georeferenced coordinates in a raster context.

The geotransform is carried in a geotransformObj structure in MapServer. It looks like this:

 typedef struct {
  int  need_geotransform;

  double rotation_angle;  

  double geotransform[6];    // Pixel/line to georef.
  double invgeotransform[6]; // georef to pixel/line

 } geotransformObj;

The need_geotransform flag will be MS_TRUE if the geotransform is needed - that is if the traditional EXTENT based approach to defining the map transformation is insufficient. This will kick in if the rotation is non-zero for now. In the future it may also kick in if the pixel size is non-square.

The rotation_angle is the angle to apply in decimal degrees. Note that the external EXTENT value is needed along with the rotation_angle to compute the geotransform[].

The geotransform[] is the transformation between pixel/line space and georeferenced space as described above. The invgeotransform[] is the inverse that can be used to easily map between georeferenced space and pixel/line space.

 pixel_x = invgt[0] + invgt[1] * geo_x + invgt[2] * geo_y
 pixel_y = invgt[3] + invgt[4] * geo_x + invgt[5] * geo_y

There is an instance of the geotransformObj carried on the mapObj as well as in the projectionObj. Currently the only projectionObj where the geotransform is actually used would be the one on a map though this in theory might change in the future.

Raster Render

The raster rendering code "understands" the map geotransform and applies it directly. To do so it has to force rendering to go through the general resampling case also used for reprojecting rasters. This results in some slow down, but is necessary to apply rotation.

Note that only the GDAL raster render understands rotation. The other built-in raster renderers (png, tiff, jpeg, erdas, eppl) do not pay any attention to the geotransform at all but then they are fundamentally incapable of rotation at this time.

Vector Render

The vector rendering code currently does not "understand" the geotransform. It consists of quite a substantial amount of rendering code all with the fixed assumption that the mapping can be represented by a simple extent rectangle (and furthermore that the cell size is square).

Rather than change this, the approach taken was to "trick" the vector renderer by applying the rotation within the reprojection operation and resetting the EXTENTS and cellsize information to some arbitrary coordinate system during the render operation.

In practice this is accomplished by altering the extents and cellsize temporarily within the msDrawMap?() function (mapObj->draw() in MapScript). A EXTENT value of "0 0 <width_in_pixels> <height_in_pixels>" is used during rendering and the projectionObj on the map is altered take care of mapping from map georeferenced coordinates to this pseudo-coordinate system during the projection operation.

So when the various vector rendering functions convert the vertice locations between layer georeferenced coordinates and map georeferenced coordinates the extra geotransform transformation is applied. However, in the actual vector rendering code there is no awareness of the geotransform and the fake extents and cellsize are being used. While this seems to work properly, it may cause problems in the future.

Outstanding Issues

The long term plan would be to have all the vector rendering code understand and use the geotransform[] explicitly. However, to do this will be a substantial job, and not one that is funded at this time.

Other issues:

  • valid projectionObj's are required on all layers and the map even though they may well all match exactly. In fact, pointless reprojection operations are going to take place.
  • In MapServer "cgi" mode there is no way to drive the rotation from the URL (Steve plans to fix this).
  • In MapServer "cgi" mode there is no way to substitute the currently render rotation into the returned HTML form for a given render. There needs to be a template variable for rotation.
  • Query operations do not "know" about rotation on the source raster, and so can't properly transform pixel/line coordinates on the last rendered map into georeferenced coordinates for the query.
  • There is no mapscript mechanism to get the rotation angle from a map.

-- FrankWarmerdam?