RFC 66 : OGR random layer read/write capabilities

Author: Even Rouault

Contact: even.rouault at spatialys.com

Status: Implemented

Implementing version: 2.2

Summary

This RFC introduces a new API to be able to iterate over vector features at dataset level, in addition to the existing capability of doing it at the layer level. The existing capability of writing features in layers in random order, that is supported by most drivers with output capabilities, is formalized with a new dataset capability flag.

Rationale

Some vector formats mix features that belong to different layers in an interleaved way, which make the current feature iteration per layer rather inefficient (this requires for each layer to read the whole file). One example of such drivers is the OSM driver. For this driver, a hack had been developed in the past to be able to use the OGRLayer::GetNextFeature() method, but with a really particular semantics. See "Interleaved reading" paragraph of OSM - OpenStreetMap XML and PBF for more details. A similar need arises with the development of a new driver, GMLAS (for GML Application Schemas), that reads GML files with arbitrary element nesting, and thus can return them in a apparent random order, because it works in a streaming way. For example, let's consider the following simplified XML content :

<A>
    ...
    <B>
        ...
    </B>
    ...
</A>

The driver will be first able to complete the building of feature B before emitting feature A. So when reading sequences of this pattern, the driver will emit features in the order B,A,B,A,...

Changes

C++ API

Two new methods are added at the GDALDataset level :

GetNextFeature():

/**
 \brief Fetch the next available feature from this dataset.

 The returned feature becomes the responsibility of the caller to
 delete with OGRFeature::DestroyFeature().

 Depending on the driver, this method may return features from layers in a
 non sequential way. This is what may happen when the
 ODsCRandomLayerRead capability is declared (for example for the
 OSM and GMLAS drivers). When datasets declare this capability, it is strongly
 advised to use GDALDataset::GetNextFeature() instead of
 OGRLayer::GetNextFeature(), as the later might have a slow, incomplete or stub
 implementation.

 The default implementation, used by most drivers, will
 however iterate over each layer, and then over each feature within this
 layer.

 This method takes into account spatial and attribute filters set on layers that
 will be iterated upon.

 The ResetReading() method can be used to start at the beginning again.

 Depending on drivers, this may also have the side effect of calling
 OGRLayer::GetNextFeature() on the layers of this dataset.

 This method is the same as the C function GDALDatasetGetNextFeature().

 @param ppoBelongingLayer a pointer to a OGRLayer* variable to receive the
                          layer to which the object belongs to, or NULL.
                          It is possible that the output of *ppoBelongingLayer
                          to be NULL despite the feature not being NULL.
 @param pdfProgressPct    a pointer to a double variable to receive the
                          percentage progress (in [0,1] range), or NULL.
                          On return, the pointed value might be negative if
                          determining the progress is not possible.
 @param pfnProgress       a progress callback to report progress (for
                          GetNextFeature() calls that might have a long duration)
                          and offer cancellation possibility, or NULL
 @param pProgressData     user data provided to pfnProgress, or NULL
 @return a feature, or NULL if no more features are available.
 @since GDAL 2.2
*/

OGRFeature* GDALDataset::GetNextFeature( OGRLayer** ppoBelongingLayer,
                                         double* pdfProgressPct,
                                         GDALProgressFunc pfnProgress,
                                         void* pProgressData )

and ResetReading():

/**
 \brief Reset feature reading to start on the first feature.

 This affects GetNextFeature().

 Depending on drivers, this may also have the side effect of calling
 OGRLayer::ResetReading() on the layers of this dataset.

 This method is the same as the C function GDALDatasetResetReading().

 @since GDAL 2.2
*/
void        GDALDataset::ResetReading();

New capabilities

The following 2 new dataset capabilities are added :

#define ODsCRandomLayerRead     "RandomLayerRead"   /**< Dataset capability for GetNextFeature() returning features from random layers */
#define ODsCRandomLayerWrite    "RandomLayerWrite " /**< Dataset capability for supporting CreateFeature on layer in random order */

C API

The above 2 new methods are available in the C API with :

OGRFeatureH CPL_DLL GDALDatasetGetNextFeature( GDALDatasetH hDS,
                                               OGRLayerH* phBelongingLayer,
                                               double* pdfProgressPct,
                                               GDALProgressFunc pfnProgress,
                                               void* pProgressData )

void CPL_DLL GDALDatasetResetReading( GDALDatasetH hDS );

Discussion about a few design choices of the new API

Compared to OGRLayer::GetNextFeature(), GDALDataset::GetNextFeature() has a few differences :

  • it returns the layer which the feature belongs to. Indeed, there's no easy way from a feature to know which layer it belongs too (since in the data model, features can exist outside of any layer). One possibility would be to correlate the OGRFeatureDefn* object of the feature with the one of the layer, but that is a bit inconvenient to do (and theoretically, one could imagine several layers sharing the same feature definition object, although this probably never happen in any in-tree driver).

  • even if the feature returned is not NULL, the returned layer might be NULL. This is just a provision for now, since that cannot currently happen. This could be interesting to address schema-less datasources where basically each feature could have a different schema (GeoJSON for example) without really belonging to a clearly identified layer.

  • it returns a progress percentage. When using OGRLayer API, one has to count the number of features returned with the total number returned by GetFeatureCount(). For the use cases we want to address knowing quickly the total number of features of the dataset is not doable. But knowing the position of the file pointer regarding the total size of the size is easy. Hence the decision to make GetNextFeature() return the progress percentage. Regarding the choice of the range [0,1], this is to be consistent with the range accepted by GDAL progress functions.

  • it accepts a progress and cancellation callback. One could wonder why this is needed given that GetNextFeature() is an "elementary" method and that it can already returns the progress percentage. However, in some circumstances, it might take a rather long time to complete a GetNextFeature() call. For example in the case of the OSM driver, as an optimization you can ask the driver to return features of a subset of layers. For example all layers except nodes. But generally the nodes are at the beginning of the file, so before you get the first feature, you have typically to process 70% of the whole file. In the GMLAS driver, the first GetNextFeature() call is also the opportunity to do a preliminary quick scan of the file to determine the SRS of geometry columns, hence having progress feedback is welcome.

The progress percentage output is redundant with the progress callback mechanism, and the latter could be used to get the former, however it may be a bit convoluted. It would require doing things like:

int MyProgress(double pct, const char* msg, void* user_data)
{
    *(double*)user_data = pct;
    return TRUE;
}

myDS->GetNextFeature(&poLayer, MyProgress, &pct)

SWIG bindings (Python / Java / C# / Perl) changes

GDALDatasetGetNextFeature is mapped as gdal::Dataset::GetNextFeature() and GDALDatasetResetReading as gdal::Dataset::ResetReading().

Regarding gdal::Dataset::GetNextFeature(), currently only Python has been modified to return both the feature and its belonging layer. Other bindings just return the feature for now (would need specialized typemaps)

Drivers

The OSM and GMLAS driver are updated to implement the new API.

Existing drivers that support ODsCRandomLayerWrite are updated to advertise it (that is most drivers that have layer creation capabilities, with the exceptions of KML, JML and GeoJSON).

Utilities

ogr2ogr / GDALVectorTranslate() is changed internally to remove the hack that was used for the OSM driver to use the new API, when ODsCRandomLayerRead is advertized. It checks if the output driver advertises ODsCRandomLayerWrite, and if it does not, emit a warning, but still goes on proceeding with the conversion using random layer reading/writing.

ogrinfo is extended to accept a -rl (for random layer) flag that instructs it to use the GDALDataset::GetNextFeature() API. It was considered to use it automatically when ODsCRandomLayerRead was advertized, but the output can be quite... random and thus not very practical for the user.

Documentation

All new methods/functions are documented.

Test Suite

The specialized GetNextFeature() implementation of the OSM and GMLAS driver is tested in their respective tests. The default implementation of GDALDataset::GetNextFeature() is tested in the MEM driver tests.

Compatibility Issues

None for existing users of the C/C++ API.

Since there is a default implementation, the new functions/methods can be safely used on drivers that don't have a specialized implementation.

The addition of the new virtual methods GDALDataset::ResetReading() and GDALDataset::GetNextFeature() may cause issues for out-of-tree drivers that would already use internally such method names, but with different semantics, or signatures. We have encountered such issues with a few in-tree drivers, and fixed them.

Implementation

The implementation will be done by Even Rouault, and is mostly triggered by the needs of the new GMLAS driver (initial development funded by the European Earth observation programme Copernicus).

The proposed implementation is in https://github.com/rouault/gdal2/tree/gmlas_randomreadwrite (commit: https://github.com/rouault/gdal2/commit/8447606d68b9fac571aa4d381181ecfffed6d72c)

Voting history

+1 from TamasS, HowardB, JukkaR, DanielM and EvenR.