[gdal-dev] Pre-RFC for OGRSpatialReference use in GDAL and changes in how to deal with axis order issues
Even Rouault
even.rouault at spatialys.com
Tue Dec 18 10:04:53 PST 2018
Hi,
I call this pre-RFC as I don't have yet any code implementing the following
ideas (so there might be holes in the analysis), but I wanted to get some
feedback before investing too much time into this substantial work.
Let's start with the easiest topic:
1) Use of OGRSpatialReference in GDAL
Currently GDAL datasets accept and return a WKT 1 string to describe the SRS.
To be more independant of the actual encoding, and for example allowing a
GeoPackage raster dataset to be able to use WKT 2, I propose we switch from a
string representation to the more abstract representation offered by
OGRSpatialReference. In the current state of GDAL, OGRSpatialReference is just
a direct mapping of WKT 1, but in my in-progress gdalbarn branch at
https://github.com/rouault/gdal/tree/gdalbarn , OGRSpatialReference is now
more or less encapsulating a osgeo::proj::crs object (ie a C++ object
representing a CRS as modelled by OGC Topic 2 / ISO-19111), that can have
several representations: OGC WKT1 (GDAL WKT1), WKT2, ESRI WKT1, PROJ strings.
So the proposal would be to change:
GDALDataset:
virtual const char* GetProjectionRef();
to
virtual OGRSpatialReference *GetSpatialRef();
// compatibility layer (conceptually)
const char* GetProjectionRef() {
return GetSpatialRef()->ExportToWkt1(); }
Similarly on the set side,
Change
virtual CPLErr SetProjection(const char* pszWKT);
to
virtual CPLErr SetSpatialRef(OGRSpatialReference*);
// compatibility layer (conceptually)
CPLErr SetProjection(const char* pszWKT) {
return SetSpatialRef(OGRSpatialReference::importFromWkt(pszWKT)); }
Regarding ownership, I guess we would do similarly to the vector side, that is
GetSpatialRef() returns an an object that it owns and doesn't increment the
refcount. And SetSpatialRef() would potentially increment the reference count
on the passed object or clone it (like ICreateLayer() does)
To ease the transition that will require going through all the ~150 raster
drivers, I may add in GDALDataset:
protected:
OGRSpatialReference* GetSpatialRefFromOldGetProjectionRef()
// conceptual implementation
{ return OGRSpatialReference::importFromWkt(OldGetProjectionRef()); }
virtual const char* OldGetProjectionRef() { return ""; }
Drivers would rename their existing GetProjectionRef() to
OldGetProjectionRef() and implement a GetSpatialRef() that would just call
GetSpatialRefFromOldGetProjectionRef(). This is just a technical detail to be
able to complete the transition of API easier in a semi-automatic way. Over
time we may remove the hack and implement natively GetSpatialRef(). And
similarly for the write side.
Now, the harder one:
2) Axis order revamp
Ever recurring headache [1], not sure my below proposal will solve it in a
definitive way
Let's look at the current situation with a common example
EPSG:4326 returns
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.0174532925199433,
AUTHORITY["EPSG","9122"]],
AUTHORITY["EPSG","4326"]]
whereas EPSGA:4326 returns (not the A for Axis)
GEOGCS["WGS 84",
DATUM["WGS_1984",
SPHEROID["WGS 84",6378137,298.257223563,
AUTHORITY["EPSG","7030"]],
AUTHORITY["EPSG","6326"]],
PRIMEM["Greenwich",0,
AUTHORITY["EPSG","8901"]],
UNIT["degree",0.0174532925199433,
AUTHORITY["EPSG","9122"]],
AXIS["Latitude",NORTH],
AXIS["Longitude",EAST],
AUTHORITY["EPSG","4326"]]
In EPSG:4326 expansion, there is no AXIS mention. This is technically
conformant with OGC WKT1 grammar where AXIS is optional. However, GDAL uses
this omission to implicitely mean it doesn't honour the axis order that the
EPSG authority has set for this geographic CRS, which is latitude first,
longitude second, but instead use longitude, latitude. Note also that EPSGA:
4326 is not really honoured by GDAL: if you use it with gdaltransform, it will
still expect/return coordinates in longitude, latitude order.
In WKT2, AXIS is a required element, so we cannot hide ourselves behind the
omission we did in WKT1, and we can't either modify the definition to
advertize longitude, latitude order, as it would be a great source of
confusion (using an authority code but putting a contradictory element into it
is a no-no)
I propose to add a "data axis to SRS axis mapping" concept, which is a bit
similar to what is done in WCS DescribeCoverage response to explain how the
SRS axis map to the grid axis of a coverage
Extract from
https://docs.geoserver.org/stable/en/user/extensions/wcs20eo/index.html
for a coverage that uses EPSG:4326
<gml:coverageFunction>
<gml:GridFunction>
<gml:sequenceRule axisOrder="+2 +1">Linear</gml:sequenceRule>
<gml:startPoint>0 0</gml:startPoint>
</gml:GridFunction>
</gml:coverageFunction>
A similar mapping would be used to define how the 'x' and 'y' components in
the geotransform matrix or in a OGRGeometry map to the axis defined by the CRS
definition.
Such mapping would be given by a new method in OGRSpatialReference
std::vector<int> GetDataAxisToSRSAxisMapping() const
To explain its semantics, imagine that it return 2,-1,3.
That would be interpreted as:
* 2: the first axis of the CRS maps to the second axis of the data
* -1: the second axis of the CRS maps to the first axis of the data, with
values negated
* 3: the third axis of the CRS maps to the third axis of the data
This would be similar to the PROJ axisswap operation:
https://proj4.org/operations/conversions/axisswap.html
By default, GetDataAxisToSRSAxisMapping() would return the identity 1,2[,3],
that is be conformant with the axis order defined by the authority.
As all GDAL and a vast majority of OGR drivers depend on using the "GIS axis
mapping", a method
SetDataAxisToSRSAxisMappingStrategy(
TRADITIONAL_GIS_ORDER or AUTHORITY_COMPLIANT )
would be added to make their job of specifying the axis mapping easier;
TRADITIONAL_GIS_ORDER would mean:
* for geographic 2D CRS,
- for Latitude NORTH, Longitude EAST (such as EPSG:4326),
GetDataAxisToSRSAxisMapping() would return {2,1}, meaning that the data order
is longitude, latitude
- for Longitude EAST, Latitude NORTH (such as OGC:CRS84), would return
{1,2}
* for projected CRS,
- for EAST, NORTH (ie most projected CRS), would return {1,2}
- for NORTH, EAST, would return {2,1}
- for North Pole CRS, with East/SOUTH, North/SOUTH, such as EPSG:5041
("WGS 84 / UPS North (E,N)"), would return {1,2}
- for North Pole CRS, with northing/SOUTH, easting/SOUTH, such as EPSG:
32661 ("WGS 84 / UPS North (N,E)"), would return {2,1}
- similarly for South Pole CRS
- Krovak East North, with EAST, NORTH, such as EPSG:5221 "S-JTSK (Ferro) /
Krovak", would return {1,2}
Cases where I'm less sure about what the 'GIS order' is and suspect we
might not do the right thing currently:
- Transverse Mercator South Oriented, with WEST, SOUTH, would return {1,2}
(I assume that this system is not used for rasters)
- traditional Krovak, with SOUTH, WEST, such as EPSG:2065 "S-JTSK (Ferro)
/ Krovak", would return {1,2} (I assume that this system is not used for
rasters)
- other cases, return {1,2}
OGRCreateCoordinateTransformation(OGRSpatialReference* source,
OGRSpatialReference* target) will honour the data axis to srs axis mapping
That means that gdaltransform -s_srs EPSG:4326 -t_srs EPSG:32631 will expect
latitude, longitude order (so backward incompatible)
(PROJ6 will also be axis order compliant when using EPSG codes)
We may add a -s_axis gis switch (and/or -s_axis long,lat for more explicit) /
-t_axis syntax to have some easier transition.
Raster datasets will all set
SetDataAxisToSRSAxisMappingStrategy(TRADITIONAL_GIS_ORDER) on the
OGRSpatialReference* they return, and will also assume it in SetSpatialRef()
(will probably be just assumed and unchecked for now)
Vector layers will mostly all call
SetDataAxisToSRSAxisMappingStrategy(TRADITIONAL_GIS_ORDER) on the
OGRSpatialReference* returned by GetSpatialRef(). We may decide that some
drivers like GML no longer do coordinate axis swapping for GML3 and then use
the AUTHORITY_COMPLIANT strategy.
ICreateLayer() when receiving a OGRSpatialReference* may decide to change the
axis mapping strategy. That is: if it receives a OGRSpatialReference with
AUTHORITY_COMPLIANT order, it may decide to switch to TRADITIONAL_GIS_ORDER
and GetSpatialRef()::GetDataAxisToSRSAxisMapping() will reflect that. Tools
like ogr2ogr will need to do the geometry axis swapping in that case
I'm midly satisfied with adding the axis mapping in the OGRSpatialReference
class itself, but I feel it would be overkill to have an extra object to
capture both (and can't come with a name for it). The coupling of both will
require careful explanation of what some methods do. For example, I think
OGRSpatialReference::IsSame() would ignore the data axis mapping setting when
comparing 2 OGRSpatialReference object.
I would also lean on making WKT 1 export now always return the AXIS element,
and EPSG:xxxx would then behave like EPSGA:xxxx
So a summary view of this approach is that in the formal SRS definition, we no
longer do derogations regarding axis order, but we add an additional interface
to describe how we actually make our match match with the SRS definition.
In case you reached that point, thoughts... ?
Even
[1] Fresh ticket about such confusion:
https://github.com/OSGeo/gdal/issues/1155
--
Spatialys - Geospatial professional services
http://www.spatialys.com
More information about the gdal-dev
mailing list