diff --git a/autotest/ogr/ogr_gpkg.py b/autotest/ogr/ogr_gpkg.py
index 93d23426290e..67fb786af1ba 100755
--- a/autotest/ogr/ogr_gpkg.py
+++ b/autotest/ogr/ogr_gpkg.py
@@ -10147,8 +10147,13 @@ def test_ogr_gpkg_creation_with_foreign_key_constraint_enabled(tmp_vsimem):
@gdaltest.enable_exceptions()
-@pytest.mark.parametrize("with_metadata", [True, False])
-def test_ogr_gpkg_geom_coord_precision(tmp_vsimem, with_metadata):
+@pytest.mark.parametrize(
+ "with_metadata,DISCARD_COORD_LSB,UNDO_DISCARD_COORD_LSB_ON_READING",
+ [(True, "YES", "YES"), (False, "YES", "NO"), (False, "NO", "NO")],
+)
+def test_ogr_gpkg_geom_coord_precision(
+ tmp_vsimem, with_metadata, DISCARD_COORD_LSB, UNDO_DISCARD_COORD_LSB_ON_READING
+):
filename = str(tmp_vsimem / "test.gpkg")
ds = gdal.GetDriverByName("GPKG").Create(filename, 0, 0, 0, gdal.GDT_Unknown)
@@ -10156,7 +10161,14 @@ def test_ogr_gpkg_geom_coord_precision(tmp_vsimem, with_metadata):
prec = ogr.CreateGeomCoordinatePrecision()
prec.Set(1e-5, 1e-3, 1e-2)
geom_fld.SetCoordinatePrecision(prec)
- lyr = ds.CreateLayerFromGeomFieldDefn("test", geom_fld)
+ lyr = ds.CreateLayerFromGeomFieldDefn(
+ "test",
+ geom_fld,
+ [
+ "DISCARD_COORD_LSB=" + DISCARD_COORD_LSB,
+ "UNDO_DISCARD_COORD_LSB_ON_READING=" + UNDO_DISCARD_COORD_LSB_ON_READING,
+ ],
+ )
geom_fld = lyr.GetLayerDefn().GetGeomFieldDefn(0)
prec = geom_fld.GetCoordinatePrecision()
assert prec.GetXYResolution() == 1e-5
@@ -10183,15 +10195,71 @@ def test_ogr_gpkg_geom_coord_precision(tmp_vsimem, with_metadata):
assert ds.GetMetadata() == {}
assert lyr.GetMetadata() == ({"FOO": "BAR"} if with_metadata else {})
f = lyr.GetNextFeature()
+
+ g = f.GetGeometryRef()
+ assert g.GetX(0) == pytest.approx(1.23456789, abs=1e-5)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetX(0) != pytest.approx(1.23456789, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetX(0) == pytest.approx(1.23457, abs=1e-8)
+ else:
+ assert g.GetX(0) != pytest.approx(1.23457, abs=1e-8)
+
+ assert g.GetY(0) == pytest.approx(2.34567891, abs=1e-5)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetY(0) != pytest.approx(2.34567891, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetY(0) == pytest.approx(2.34568, abs=1e-8)
+
+ assert g.GetZ(0) == pytest.approx(9.87654321, abs=1e-3)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetZ(0) != pytest.approx(9.87654321, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetZ(0) == pytest.approx(9.876, abs=1e-8)
+
+ assert g.GetM(0) == pytest.approx(-1.23456789, abs=1e-2)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetM(0) != pytest.approx(-1.23456789, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetM(0) == pytest.approx(-1.23, abs=1e-8)
+
+ # Test Arrow interface
+ lyr.ResetReading()
+ mem_ds = ogr.GetDriverByName("Memory").CreateDataSource("")
+ mem_lyr = mem_ds.CreateLayer("test", geom_type=ogr.wkbNone)
+ mem_lyr.CreateGeomField(ogr.GeomFieldDefn("my_geom"))
+ mem_lyr.WriteArrow(lyr)
+ f = mem_lyr.GetNextFeature()
+
g = f.GetGeometryRef()
assert g.GetX(0) == pytest.approx(1.23456789, abs=1e-5)
- assert g.GetX(0) != pytest.approx(1.23456789, abs=1e-8)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetX(0) != pytest.approx(1.23456789, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetX(0) == pytest.approx(1.23457, abs=1e-8)
+ else:
+ assert g.GetX(0) != pytest.approx(1.23457, abs=1e-8)
+
assert g.GetY(0) == pytest.approx(2.34567891, abs=1e-5)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetY(0) != pytest.approx(2.34567891, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetY(0) == pytest.approx(2.34568, abs=1e-8)
+
assert g.GetZ(0) == pytest.approx(9.87654321, abs=1e-3)
- assert g.GetZ(0) != pytest.approx(9.87654321, abs=1e-8)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetZ(0) != pytest.approx(9.87654321, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetZ(0) == pytest.approx(9.876, abs=1e-8)
+
assert g.GetM(0) == pytest.approx(-1.23456789, abs=1e-2)
- assert g.GetM(0) != pytest.approx(-1.23456789, abs=1e-8)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetM(0) != pytest.approx(-1.23456789, abs=1e-8)
+ if UNDO_DISCARD_COORD_LSB_ON_READING == "YES":
+ assert g.GetM(0) == pytest.approx(-1.23, abs=1e-8)
+ lyr.ResetReading()
+ f = lyr.GetNextFeature()
# Update geometry to check existing precision settings are used
f.SetGeometry(
ogr.CreateGeometryFromWkt(
@@ -10225,12 +10293,15 @@ def test_ogr_gpkg_geom_coord_precision(tmp_vsimem, with_metadata):
f = lyr.GetNextFeature()
g = f.GetGeometryRef()
assert g.GetX(0) == pytest.approx(-1.23456789, abs=1e-5)
- assert g.GetX(0) != pytest.approx(-1.23456789, abs=1e-8)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetX(0) != pytest.approx(-1.23456789, abs=1e-8)
assert g.GetY(0) == pytest.approx(-2.34567891, abs=1e-5)
assert g.GetZ(0) == pytest.approx(-9.87654321, abs=1e-3)
- assert g.GetZ(0) != pytest.approx(-9.87654321, abs=1e-8)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetZ(0) != pytest.approx(-9.87654321, abs=1e-8)
assert g.GetM(0) == pytest.approx(1.23456789, abs=1e-2)
- assert g.GetM(0) != pytest.approx(1.23456789, abs=1e-8)
+ if DISCARD_COORD_LSB == "YES":
+ assert g.GetM(0) != pytest.approx(1.23456789, abs=1e-8)
ds.DeleteLayer(0)
diff --git a/doc/source/drivers/vector/gpkg.rst b/doc/source/drivers/vector/gpkg.rst
index 49e93ac4f8e7..a6b67133b513 100644
--- a/doc/source/drivers/vector/gpkg.rst
+++ b/doc/source/drivers/vector/gpkg.rst
@@ -351,6 +351,20 @@ The following layer creation options are available:
geometry column can be NULL. Can be set to NO so that geometry is
required.
+- .. lco:: DISCARD_COORD_LSB
+ :choices: YES, NO
+ :default: NO
+ :since: 3.9
+
+ Whether the geometry coordinate precision should be used to set to zero non-significant least-significant bits of geometries. Helps when further compression is used. See :ref:`ogr_gpkg_geometry_coordinate_precision` for more details.
+
+- .. lco:: UNDO_DISCARD_COORD_LSB_ON_READING
+ :choices: YES, NO
+ :default: NO
+ :since: 3.9
+
+ Whether to ask GDAL to take into coordinate precision to undo the effects of DISCARD_COORD_LSB. See :ref:`ogr_gpkg_geometry_coordinate_precision` for more details.
+
- .. lco:: FID
:default: fid
@@ -632,6 +646,8 @@ Update of an existing file is not supported.
Creation involves the creation of a temporary file. Sufficiently large files
will be automatically compressed using the SOZip optimization.
+.. _ogr_gpkg_geometry_coordinate_precision:
+
Geometry coordinate precision
-----------------------------
@@ -639,10 +655,36 @@ Geometry coordinate precision
The GeoPackage driver supports reading and writing the geometry coordinate
precision, using the :cpp:class:`OGRGeomCoordinatePrecision` settings of the
-:cpp:class:`OGRGeomFieldDefn`, as well as setting to zero the less-significant
-bits which are not relevant for the requested precision. This can improve
-further lossless compression stages, for example when putting a GeoPackage in
-an archive.
+:cpp:class:`OGRGeomFieldDefn`. By default, the geometry coordinate precision
+is only noted in metadata, and does not cause geometries that are written to
+be modified to comply with this precision.
+
+Several settings may be combined to apply further processing:
+
+* if the :config:`OGR_APPLY_GEOM_SET_PRECISION` configuration option is set to
+ ``YES``, the :cpp:func:`OGRGeometry::SetPrecision` method will be applied
+ when calling the CreateFeature() and SetFeature() method of the driver, to
+ round X and Y coordinates to the specified precision, and fix potential
+ geometry invalidities resulting from the rounding.
+
+* if the ``DISCARD_COORD_LSB`` layer creation option is set to YES, the
+ less-significant bits of the WKB geometry encoding which are not relevant for
+ the requested precision are set to zero. This can improve further lossless
+ compression stages, for example when putting a GeoPackage in an archive.
+ Note however that when reading back such geometries and displaying them
+ to the maximum precision, they will not "exactly" match the original
+ :cpp:class:`OGRGeomCoordinatePrecision` settings. However, they will round
+ back to it.
+ The value of the ``DISCARD_COORD_LSB`` layer creation option is written in
+ the dataset metadata, and will be re-used for later edition sessions.
+
+* if the ``UNDO_DISCARD_COORD_LSB_ON_READING`` layer creation option is set to
+ YES (only makes sense if the ``DISCARD_COORD_LSB`` layer creation option is
+ also set to YES), when *reading* back geometries from a dataset, the
+ :cpp:func:`OGRGeometry::roundCoordinates` method will be applied so that
+ the geometry coordinates exactly match the original specified coordinate
+ precision. That option will only be honored by GDAL 3.9 or later.
+
Implementation details: the coordinate precision is stored in a record in each
of the ``gpkg_metadata`` and ``gpkg_metadata_reference`` table, with the
@@ -651,7 +693,7 @@ specification:
- gpkg_metadata.md_standard_uri = 'http://gdal.org'
- gpkg_metadata.mime_type = 'text/xml'
-- gpkg_metadata.metadata = ''
+- gpkg_metadata.metadata = ''
- gpkg_metadata_reference.reference_scope = 'column'
- gpkg_metadata_reference.table_name = '{table_name}'
- gpkg_metadata_reference.column_name = '{geometry_column_name}'
diff --git a/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h b/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h
index 4367680810f0..f8c7688201aa 100644
--- a/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h
+++ b/ogr/ogrsf_frmts/gpkg/ogr_geopackage.h
@@ -574,6 +574,9 @@ class OGRGeoPackageLayer CPL_NON_FINAL : public OGRLayer,
int m_iGeomCol = -1;
std::vector m_anFieldOrdinals{};
+ //! Whether to call OGRGeometry::SetPrecision() when reading back geometries from the database
+ bool m_bUndoDiscardCoordLSBOnReading = false;
+
void ClearStatement();
virtual OGRErr ResetStatement() = 0;
@@ -884,13 +887,12 @@ class OGRGeoPackageTableLayer final : public OGRGeoPackageLayer
const char *pszObjectType, bool bIsInGpkgContents,
bool bIsSpatial, const char *pszGeomColName,
const char *pszGeomType, bool bHasZ, bool bHasM);
- void SetCreationParameters(OGRwkbGeometryType eGType,
- const char *pszGeomColumnName, int bGeomNullable,
- OGRSpatialReference *poSRS,
- const OGRGeomCoordinatePrecision &oCoordPrec,
- const char *pszFIDColumnName,
- const char *pszIdentifier,
- const char *pszDescription);
+ void SetCreationParameters(
+ OGRwkbGeometryType eGType, const char *pszGeomColumnName,
+ int bGeomNullable, OGRSpatialReference *poSRS,
+ const OGRGeomCoordinatePrecision &oCoordPrec, bool bDiscardCoordLSB,
+ bool bUndoDiscardCoordLSBOnReading, const char *pszFIDColumnName,
+ const char *pszIdentifier, const char *pszDescription);
void SetDeferredSpatialIndexCreation(bool bFlag);
void SetASpatialVariant(GPKGASpatialVariant eASpatialVariant)
{
diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp
index e6c4d5a92d28..7b122f5e090a 100644
--- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp
+++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedatasource.cpp
@@ -6774,6 +6774,10 @@ GDALGeoPackageDataset::ICreateLayer(const char *pszLayerName,
eGType, pszGeomColumnName, bGeomNullable, poSRS,
poSrcGeomFieldDefn ? poSrcGeomFieldDefn->GetCoordinatePrecision()
: OGRGeomCoordinatePrecision(),
+ CPLTestBool(
+ CSLFetchNameValueDef(papszOptions, "DISCARD_COORD_LSB", "NO")),
+ CPLTestBool(CSLFetchNameValueDef(
+ papszOptions, "UNDO_DISCARD_COORD_LSB_ON_READING", "NO")),
pszFIDColumnName, pszIdentifier,
CSLFetchNameValue(papszOptions, "DESCRIPTION"));
if (poSRS)
diff --git a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedriver.cpp b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedriver.cpp
index e68d6fa37ccc..e5e3043bad72 100644
--- a/ogr/ogrsf_frmts/gpkg/ogrgeopackagedriver.cpp
+++ b/ogr/ogrsf_frmts/gpkg/ogrgeopackagedriver.cpp
@@ -675,6 +675,13 @@ void RegisterOGRGeoPackage()
" "
+ " "
+ " "
" "
"