Working with non-EPSG Coordinate Reference Systems
Overview
Most geospatial workflows use EPSG-defined coordinate reference systems. However, there are cases where we need to work with custom or non-standard projections that are not included in the EPSG registry.
In this example we will be working with the ESRI:53009 - a spherical Mollweide projection, and using WMS, WFS, and WCS services to access data in this projection.
ESRI:53009 can be identified using different formats, as shown in the table below:
| Format | Example |
|---|---|
| Authority code | ESRI:53009 |
| OGC URN | urn:ogc:def:crs:ESRI::53009 |
| OGC HTTP URI | http://www.opengis.net/def/crs/ESRI/0/53009 |
The Mapfile
Projections are referenced in several places in the Mapfile, listed below.
The top-level projection of the Mapfile is set to ESRI:53009:
The source data used for the "raster" and "countries" layers are in the EPSG:4326 projection.
These need to be set explicitly in the Mapfile using the PROJECTION object within the LAYER definition, as follows:
LAYER
NAME "raster"
TYPE RASTER
EXTENT -180 -90 180 90
DATA "data/naturalearth/NE2_50M_SR_SMALL.tif"
PROJECTION
"EPSG:4326"
END
END
The "cities" layer uses a GeoJSON file already in the ESRI:53009 projection. Even though it is not strictly necessary to specify the projection for this layer,
it is good practice to do so to ensure that MapServer can correctly identify the source CRS and reproject the data as needed.
LAYER
NAME "cities"
TYPE POINT
PROJECTION
"ESRI:53009"
END
CONNECTIONTYPE OGR
CONNECTION "data/naturalearth/worldcity_53009.geojson"
Note
GeoJSON traditionally assumes EPSG:4326. Using other CRS relies on client/server agreement and is not part of the official GeoJSON specification.
Finally we want to make the projection available to all the WxS protocols - WMS, WFS, and WCS. This is done using the ows_srs metadata item in the WEB section of the Mapfile, as follows:
Using the ows_ prefix makes the projection available to all OWSs (OGC Web Services), and avoids having to have duplicate entries such as wms_srs, wfs_srs, and wcs_srs
if we were to use the service-specific metadata items.
Note
The order of the layers in the Mapfile is important when requesting a map directly using the MapServer CGI interface (for example http://localhost:7000/?map=/etc/mapserver/other-projections.map&mode=map&layer=countries&layer=cities&layer=raster). The raster layer must be first in the Mapfile, otherwise the raster will be drawn on top of the vector layers and obscure them. The order of the layers in the request itself does not affect the rendering order, only the order in the Mapfile.
OpenLayers
The OpenLayers client for this tutorial is set up to make requests to the WMS, WFS, and WCS services using the ESRI:53009 projection.
The client is configured to request data in this projection and display it on the map.
OpenLayers does not include definitions for all projections by default, so we use the proj4js library
to define the ESRI:53009 projection and register it with OpenLayers.
proj4.defs('ESRI:53009',
'+proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs'
);
register(proj4);
const coverageExtent = [-18019909, -9009954, 18019909, 9009954];
const mollweideProjection = new Projection({
code: 'ESRI:53009',
units: 'm',
extent: coverageExtent,
worldExtent: [-180, -90, 180, 90],
global: true,
});
addProjection(mollweideProjection);
We set the projection on the map object to ESRI:53009 so that all requests sent to MapServer return data in this projection.
const map = new Map({
target: 'map',
layers: [imageLayer, wmsLayer, graticule, wfsLayer],
view: new View({
projection: 'ESRI:53009',
center: [0, 0],
zoom: 1,
}),
});
To help visually confirm the reprojection is working correctly we add a graticule layer to the map, which shows the lines of latitude and longitude.
The graticule uses the ESRI:53009 projection of the map object, and display lines representing degrees of latitude and longitude. We also enable labels to show the coordinates of the graticule lines.
const graticule = new Graticule({
strokeStyle: new Stroke({
color: 'rgba(50,50,50,0.5)',
width: 1,
}),
showLabels: true,
wrapX: false
});
WFS
The "cities" point layer is displayed as a WFS layer in the OpenLayers client.
As w"e construct the URLs to send to MapServer ourselves we need to set the SRSNAME to ESRI:53009 (MapServer interprets the BBOX values in the CRS specified by SRSNAME).
We need to set the dataProjection to ESRI:53009 in the GeoJSON format options to ensure the features are correctly reprojected and displayed on the map.
const wfsSource = new VectorSource({
format: new GeoJSON({
dataProjection: 'ESRI:53009',
}),
url: function (extent) {
const [minx, miny, maxx, maxy] = extent;
return `${url}&SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature` +
`&TYPENAMES=ms:cities` +
`&OUTPUTFORMAT=geojson` +
`&SRSNAME=ESRI:53009` +
`&BBOX=${minx},${miny},${maxx},${maxy}`;
},
strategy: bbox,
});
WMS
The "countries" polygon layer is displayed as a WMS. As the OpenLayers map itself is set to use the ESRI:53009 projection we don't need to
specify any further parameters here.
const wmsLayer = new ImageLayer({
source: new ImageWMS({
url: url,
params: {
LAYERS: 'countries',
FORMAT: 'image/png',
TRANSPARENT: true,
VERSION: '1.3.0',
},
serverType: 'mapserver',
}),
});
WCS
Note
OpenLayers does not natively support WCS. In this example we use the same approach as in the Web Coverage Services (WCS) tutorial for testing the WCS protocol. Images are requested as PNGs using the ImageWMS class and a custom imageLoadFunction, as displaying GeoTIFFs directly in OpenLayers is not supported without additional libraries.
We construct the WCS request parameters manually in the imageLoadFunction and ensure that the SUBSETTINGCRS and OUTPUTCRS parameters are set to http://www.opengis.net/def/crs/ESRI/0/53009
to ensure the data is returned in the correct projection. In this example we use the OGC HTTP URI form of the CRS identifier for the CRS requests.
const wcsSource = new ImageWMS({
url,
params: {
SERVICE: 'WCS',
VERSION: '2.0.1',
REQUEST: 'GetCoverage',
FORMAT: 'image/png',
COVERAGEID: 'raster',
SUBSETTINGCRS: 'http://www.opengis.net/def/crs/ESRI/0/53009',
OUTPUTCRS: 'http://www.opengis.net/def/crs/ESRI/0/53009',
},
imageLoadFunction: (image, src) => {
const srcUrl = new URL(src);
const params = srcUrl.searchParams;
// Get the ImageWMS params
const bbox = params.get('BBOX').split(',');
const width = params.get('WIDTH');
const height = params.get('HEIGHT');
// Replace with WCS 2.0.1 equivalents
params.append('SUBSET', `x(${bbox[0]},${bbox[2]})`);
params.append('SUBSET', `y(${bbox[1]},${bbox[3]})`);
params.set('SCALESIZE', `x(${width}),y(${height})`);
...
Note
SUBSETTINGCRS is not strictly necessary here as MapServer assumes the SUBSET parameters are in the same CRS as the OUTPUTCRS,
but it is good practice to include it.
Code
Example
- MapServer request: http://localhost:7000/?map=/etc/mapserver/other-projections.map&mode=map&layer=countries&layer=cities&layer=raster
- OpenLayers example: http://localhost:7001/other-projections.html
other-projections.js
import '../css/style.css';
import Map from 'ol/Map';
import View from 'ol/View';
import ImageLayer from 'ol/layer/Image';
import proj4 from 'proj4';
import { register } from 'ol/proj/proj4';
import { addProjection } from 'ol/proj';
import Projection from 'ol/proj/Projection';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import GeoJSON from 'ol/format/GeoJSON';
import { bbox } from 'ol/loadingstrategy';
import { Style, Stroke, Fill } from 'ol/style';
import ImageWMS from 'ol/source/ImageWMS';
import { Circle as CircleStyle } from 'ol/style';
import ImageCanvas from 'ol/source/ImageCanvas';
import Graticule from 'ol/layer/Graticule';
const mapserverUrl = import.meta.env.VITE_MAPSERVER_BASE_URL;
const mapfilesPath = import.meta.env.VITE_MAPFILES_PATH;
const url = mapserverUrl + mapfilesPath + 'other-projections.map';
proj4.defs('ESRI:53009',
'+proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs'
);
register(proj4);
const coverageExtent = [-18019909, -9009954, 18019909, 9009954];
const mollweideProjection = new Projection({
code: 'ESRI:53009',
units: 'm',
extent: coverageExtent,
worldExtent: [-180, -90, 180, 90],
global: true,
});
addProjection(mollweideProjection);
const wcsSource = new ImageWMS({
url,
params: {
SERVICE: 'WCS',
VERSION: '2.0.1',
REQUEST: 'GetCoverage',
FORMAT: 'image/png',
COVERAGEID: 'raster',
SUBSETTINGCRS: 'http://www.opengis.net/def/crs/ESRI/0/53009',
OUTPUTCRS: 'http://www.opengis.net/def/crs/ESRI/0/53009',
},
imageLoadFunction: (image, src) => {
const srcUrl = new URL(src);
const params = srcUrl.searchParams;
// Get the ImageWMS params
const bbox = params.get('BBOX').split(',');
const width = params.get('WIDTH');
const height = params.get('HEIGHT');
// Replace with WCS 2.0.1 equivalents
params.append('SUBSET', `x(${bbox[0]},${bbox[2]})`);
params.append('SUBSET', `y(${bbox[1]},${bbox[3]})`);
params.set('SCALESIZE', `x(${width}),y(${height})`);
// Remove the WMS params
params.delete('BBOX');
params.delete('WIDTH');
params.delete('HEIGHT');
params.delete('CRS');
image.getImage().src = srcUrl.toString();
},
ratio: 1,
});
const imageLayer = new ImageLayer({
source: wcsSource
});
const wmsLayer = new ImageLayer({
source: new ImageWMS({
url: url,
params: {
LAYERS: 'countries',
FORMAT: 'image/png',
TRANSPARENT: true,
VERSION: '1.3.0',
},
serverType: 'mapserver',
}),
});
// https://openlayers.org/en/latest/examples/graticule.html
const graticule = new Graticule({
strokeStyle: new Stroke({
color: 'rgba(50,50,50,0.5)',
width: 1,
}),
showLabels: true,
wrapX: false
});
const wfsSource = new VectorSource({
format: new GeoJSON({
dataProjection: 'ESRI:53009',
}),
url: function (extent) {
const [minx, miny, maxx, maxy] = extent;
return `${url}&SERVICE=WFS&VERSION=2.0.0&REQUEST=GetFeature` +
`&TYPENAMES=ms:cities` +
`&OUTPUTFORMAT=geojson` +
`&SRSNAME=ESRI:53009` +
`&BBOX=${minx},${miny},${maxx},${maxy}`;
},
strategy: bbox,
});
const wfsLayer = new VectorLayer({
source: wfsSource,
style: new Style({
image: new CircleStyle({
radius: 5,
fill: new Fill({ color: '#ff6600' }),
stroke: new Stroke({ color: '#ffffff', width: 1 }),
}),
}),
});
const map = new Map({
target: 'map',
layers: [imageLayer, wmsLayer, graticule, wfsLayer],
view: new View({
projection: 'ESRI:53009',
center: [0, 0],
zoom: 1,
}),
});
other-projections.map
MAP
NAME "OTHER_PROJECTIONS"
SIZE 1200 1200
EXTENT -18019909.21 -9009954.61 18019909.21 9009954.61
PROJECTION
"ESRI:53009"
END
IMAGETYPE "png"
MAXSIZE 8096
SYMBOL
NAME "circlef"
TYPE ELLIPSE
FILLED TRUE
POINTS
10 10
END
END
WEB
METADATA
"ows_enable_request" "*"
"ows_onlineresource" "http://localhost/path/to/other-projections?"
"ows_srs" "ESRI:53009 EPSG:4326"
"wfs_getfeature_formatlist" "geojson"
END
END
OUTPUTFORMAT
NAME "geojson"
DRIVER "OGR/GEOJSON"
MIMETYPE "application/geo+json"
FORMATOPTION "FORM=SIMPLE"
END
LAYER
NAME "raster"
TYPE RASTER
EXTENT -180 -90 180 90
DATA "data/naturalearth/NE2_50M_SR_SMALL.tif"
PROJECTION
"EPSG:4326"
END
END
LAYER
NAME "countries"
TYPE POLYGON
CONNECTIONTYPE OGR
CONNECTION "data/naturalearth/ne_110m_admin_0_countries.fgb"
PROJECTION
"EPSG:4326"
END
CLASS
STYLE
OUTLINECOLOR 50 50 50
OUTLINEWIDTH 0.4 # the width of the polygon outline
END
END
END
LAYER
NAME "cities"
TYPE POINT
PROJECTION
"ESRI:53009"
END
METADATA
"gml_include_items" "all"
END
CONNECTIONTYPE OGR
CONNECTION "data/naturalearth/worldcity_53009.geojson"
CLASS
STYLE
SYMBOL "circlef"
COLOR "#ff6600"
SIZE 6
END
END
END
END
Exercises
-
Currently the CRS is specified using the
http://www.opengis.net/def/crs/ESRI/0/53009format, (OGC CRS HTTP URIs) but this can also be specified using other CRS formats.Update the OpenLayers WCS request parameters to use this format, and test that the requests are still working correctly.
-
Update the Mapfile to use another non-EPSG projection, for example
ESRI:54030(the Robinson projection).Test everything is configured correctly by making a direct request to the MapServer CGI interface using http://localhost:7000/?map=/etc/mapserver/other-projections.map&mode=map&layer=countries&layer=cities&layer=raster.