Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FlatGeobuf: add support for reading and writing layer title, description and metadata #8860

Merged
merged 1 commit into from
Nov 28, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions autotest/ogr/ogr_flatgeobuf.py
Original file line number Diff line number Diff line change
@@ -1217,3 +1217,43 @@ def test_ogr_flatgeobuf_issue_7401():

ogr.GetDriverByName("FlatGeobuf").DeleteDataSource("/vsimem/test.fgb")
assert not gdal.VSIStatL("/vsimem/test.fgb")


###############################################################################
# Test reading and writing layer title, description and metadata


def test_ogr_flatgeobuf_title_description_metadata(tmp_vsimem):

filename = str(tmp_vsimem / "test.fgb")
ds = ogr.GetDriverByName("FlatGeobuf").CreateDataSource(filename)
lyr = ds.CreateLayer(
"test",
geom_type=ogr.wkbPoint,
options=["SPATIAL_INDEX=NO", "TITLE=title", "DESCRIPTION=description"],
)
lyr.SetMetadata({"foo": "bar", "bar": "baz"})
f = ogr.Feature(lyr.GetLayerDefn())
f.SetGeometry(ogr.CreateGeometryFromWkt("POINT (0 0)"))
lyr.CreateFeature(f)
ds = None

ds = ogr.Open(filename)
lyr = ds.GetLayer(0)
assert lyr.GetMetadata_Dict() == {
"TITLE": "title",
"DESCRIPTION": "description",
"foo": "bar",
"bar": "baz",
}

# Test that on FlatGeoBuf -> FlatGeoBuf TITLE and DESCRIPTION get properly propagated.
filename2 = str(tmp_vsimem / "test2.fgb")
ds = gdal.VectorTranslate(filename2, filename)
lyr = ds.GetLayer(0)
assert lyr.GetMetadata_Dict() == {
"TITLE": "title",
"DESCRIPTION": "description",
"foo": "bar",
"bar": "baz",
}
15 changes: 15 additions & 0 deletions doc/source/drivers/vector/flatgeobuf.rst
Original file line number Diff line number Diff line change
@@ -34,6 +34,9 @@ On creation, passing a filename without a .fgb suffix will instruct the driver
to create a directory of that name, and create layers as .fgb files in that
directory.

Starting with GDAL 3.9, metadata set at the layer level will be written in the
FlatGeobuf header, and retrieved on reading as layer metadata.

Open options
------------

@@ -65,6 +68,18 @@ Layer Creation Options
the :cpp:func:`CPLGenerateTempFilename` function.
"/vsimem/" can be used for in-memory temporary files.

- .. lco:: TITLE
:choices: <string>
:since: 3.9

Dataset title (should be relatively short)

- .. lco:: DESCRIPTION
:choices: <string>
:since: 3.9

Dataset description (intended for free form long text)

Creation Issues
---------------

16 changes: 12 additions & 4 deletions ogr/ogrsf_frmts/flatgeobuf/ogr_flatgeobuf.h
Original file line number Diff line number Diff line change
@@ -103,13 +103,15 @@ class OGRFlatGeobufLayer final : public OGRLayer,
bool m_ignoreAttributeFilter = false;

// creation
GDALDataset *m_poDS = nullptr; // parent dataset to get metadata from it
bool m_create = false;
std::deque<FeatureItem> m_featureItems; // feature item description used to
// create spatial index
bool m_bCreateSpatialIndexAtClose = true;
bool m_bVerifyBuffers = true;
VSILFILE *m_poFpWrite = nullptr;
uint64_t m_writeOffset = 0; // current write offset
CPLStringList m_aosCreationOption{}; // layer creation options
uint64_t m_writeOffset = 0; // current write offset
uint64_t m_offsetAfterHeader =
0; // offset after dummy header writing (when creating a file without
// spatial index)
@@ -142,11 +144,12 @@ class OGRFlatGeobufLayer final : public OGRLayer,
OGRFlatGeobufLayer(const FlatGeobuf::Header *, GByte *headerBuf,
const char *pszFilename, VSILFILE *poFp,
uint64_t offset);
OGRFlatGeobufLayer(const char *pszLayerName, const char *pszFilename,
OGRFlatGeobufLayer(GDALDataset *poDS, const char *pszLayerName,
const char *pszFilename,
const OGRSpatialReference *poSpatialRef,
OGRwkbGeometryType eGType,
bool bCreateSpatialIndexAtClose, VSILFILE *poFpWrite,
std::string &osTempFile);
std::string &osTempFile, CSLConstList papszOptions);

protected:
virtual int GetNextArrowArray(struct ArrowArrayStream *,
@@ -163,7 +166,7 @@ class OGRFlatGeobufLayer final : public OGRLayer,
static OGRFlatGeobufLayer *Open(const char *pszFilename, VSILFILE *fp,
bool bVerifyBuffers);
static OGRFlatGeobufLayer *
Create(const char *pszLayerName, const char *pszFilename,
Create(GDALDataset *poDS, const char *pszLayerName, const char *pszFilename,
const OGRSpatialReference *poSpatialRef, OGRwkbGeometryType eGType,
bool bCreateSpatialIndexAtClose, char **papszOptions);

@@ -192,6 +195,11 @@ class OGRFlatGeobufLayer final : public OGRLayer,
m_bVerifyBuffers = CPL_TO_BOOL(bFlag);
}

GDALDataset *GetDataset() override
{
return m_poDS;
}

const std::string &GetFilename() const override
{
return m_osFilename;
5 changes: 4 additions & 1 deletion ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufdataset.cpp
Original file line number Diff line number Diff line change
@@ -155,6 +155,9 @@ void RegisterOGRFlatGeobuf()
"create a spatial index' default='YES'/>"
" <Option name='TEMPORARY_DIR' type='string' description='Directory "
"where temporary file should be created'/>"
" <Option name='TITLE' type='string' description='Layer title'/>"
" <Option name='DESCRIPTION' type='string' "
"description='Layer description'/>"
"</LayerCreationOptionList>");
poDriver->SetMetadataItem(
GDAL_DMD_OPENOPTIONLIST,
@@ -445,7 +448,7 @@ OGRLayer *OGRFlatGeobufDataset::ICreateLayer(

auto poLayer =
std::unique_ptr<OGRFlatGeobufLayer>(OGRFlatGeobufLayer::Create(
pszLayerName, osFilename, poSpatialRef, eGType,
this, pszLayerName, osFilename, poSpatialRef, eGType,
bCreateSpatialIndexAtClose, papszOptions));
if (poLayer == nullptr)
return nullptr;
5 changes: 3 additions & 2 deletions ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobufeditablelayer.cpp
Original file line number Diff line number Diff line change
@@ -89,8 +89,9 @@ OGRErr OGRFlatGeobufEditableLayerSynchronizer::EditableSyncToDisk(
auto createIndex = m_poFlatGeobufLayer->GetIndexNodeSize() > 0;

OGRFlatGeobufLayer *poFlatGeobufTmpLayer = OGRFlatGeobufLayer::Create(
osLayerName.c_str(), osTmpFilename.c_str(), spatialRef, gType,
createIndex, m_papszOpenOptions);
m_poFlatGeobufLayer->GetDataset(), osLayerName.c_str(),
osTmpFilename.c_str(), spatialRef, gType, createIndex,
m_papszOpenOptions);
if (poFlatGeobufTmpLayer == nullptr)
return OGRERR_FAILURE;

111 changes: 92 additions & 19 deletions ogr/ogrsf_frmts/flatgeobuf/ogrflatgeobuflayer.cpp
Original file line number Diff line number Diff line change
@@ -154,6 +154,31 @@ OGRFlatGeobufLayer::OGRFlatGeobufLayer(const Header *poHeader, GByte *headerBuf,

m_eGType = getOGRwkbGeometryType();

if (const auto title = poHeader->title())
SetMetadataItem("TITLE", title->c_str());

if (const auto description = poHeader->description())
SetMetadataItem("DESCRIPTION", description->c_str());

if (const auto metadata = poHeader->metadata())
{
CPLJSONDocument oDoc;
CPLErrorHandlerPusher oQuietError(CPLQuietErrorHandler);
CPLErrorStateBackuper oErrorStateBackuper;
if (oDoc.LoadMemory(metadata->c_str()) &&
oDoc.GetRoot().GetType() == CPLJSONObject::Type::Object)
{
for (auto oItem : oDoc.GetRoot().GetChildren())
{
if (oItem.GetType() == CPLJSONObject::Type::String)
{
SetMetadataItem(oItem.GetName().c_str(),
oItem.ToString().c_str());
}
}
}
}

const char *pszName =
m_poHeader->name() ? m_poHeader->name()->c_str() : "unknown";
m_poFeatureDefn = new OGRFeatureDefn(pszName);
@@ -168,19 +193,16 @@ OGRFlatGeobufLayer::OGRFlatGeobufLayer(const Header *poHeader, GByte *headerBuf,
m_poFeatureDefn->Reference();
}

OGRFlatGeobufLayer::OGRFlatGeobufLayer(const char *pszLayerName,
const char *pszFilename,
const OGRSpatialReference *poSpatialRef,
OGRwkbGeometryType eGType,
bool bCreateSpatialIndexAtClose,
VSILFILE *poFpWrite,
std::string &osTempFile)
: m_eGType(eGType),
OGRFlatGeobufLayer::OGRFlatGeobufLayer(
GDALDataset *poDS, const char *pszLayerName, const char *pszFilename,
const OGRSpatialReference *poSpatialRef, OGRwkbGeometryType eGType,
bool bCreateSpatialIndexAtClose, VSILFILE *poFpWrite,
std::string &osTempFile, CSLConstList papszOptions)
: m_eGType(eGType), m_poDS(poDS), m_create(true),
m_bCreateSpatialIndexAtClose(bCreateSpatialIndexAtClose),
m_poFpWrite(poFpWrite), m_osTempFile(osTempFile)
m_poFpWrite(poFpWrite), m_aosCreationOption(papszOptions),
m_osTempFile(osTempFile)
{
m_create = true;

if (pszLayerName)
m_osLayerName = pszLayerName;
if (pszFilename)
@@ -457,9 +479,61 @@ void OGRFlatGeobufLayer::writeHeader(VSILFILE *poFp, uint64_t featuresCount,
CPLFree(pszWKT);
}

std::string osTitle(m_aosCreationOption.FetchNameValueDef("TITLE", ""));
std::string osDescription(
m_aosCreationOption.FetchNameValueDef("DESCRIPTION", ""));
std::string osMetadata;
CPLJSONObject oMetadataJSONObj;
bool bEmptyMetadata = true;
for (GDALMajorObject *poContainer :
{static_cast<GDALMajorObject *>(this),
static_cast<GDALMajorObject *>(
m_poDS && m_poDS->GetLayerCount() == 1 ? m_poDS : nullptr)})
{
if (poContainer)
{
if (char **papszMD = poContainer->GetMetadata())
{
for (CSLConstList papszIter = papszMD; *papszIter; ++papszIter)
{
char *pszKey = nullptr;
const char *pszValue =
CPLParseNameValue(*papszIter, &pszKey);
if (pszKey && pszValue && !EQUAL(pszKey, OLMD_FID64))
{
if (EQUAL(pszKey, "TITLE"))
{
if (osTitle.empty())
osTitle = pszValue;
}
else if (EQUAL(pszKey, "DESCRIPTION"))
{
if (osDescription.empty())
osDescription = pszValue;
}
else
{
bEmptyMetadata = false;
oMetadataJSONObj.Add(pszKey, pszValue);
}
}
CPLFree(pszKey);
}
}
}
}
if (!bEmptyMetadata)
{
osMetadata =
oMetadataJSONObj.Format(CPLJSONObject::PrettyFormat::Plain);
}

const auto header = CreateHeaderDirect(
fbb, m_osLayerName.c_str(), extentVector, m_geometryType, m_hasZ,
m_hasM, m_hasT, m_hasTM, &columns, featuresCount, m_indexNodeSize, crs);
m_hasM, m_hasT, m_hasTM, &columns, featuresCount, m_indexNodeSize, crs,
osTitle.empty() ? nullptr : osTitle.c_str(),
osDescription.empty() ? nullptr : osDescription.c_str(),
osMetadata.empty() ? nullptr : osMetadata.c_str());
fbb.FinishSizePrefixed(header);
c = VSIFWriteL(fbb.GetBufferPointer(), 1, fbb.GetSize(), poFp);
CPLDebugOnly("FlatGeobuf", "Wrote header (%lu bytes)",
@@ -2409,20 +2483,19 @@ VSILFILE *OGRFlatGeobufLayer::CreateOutputFile(const CPLString &osFilename,
return poFpWrite;
}

OGRFlatGeobufLayer *
OGRFlatGeobufLayer::Create(const char *pszLayerName, const char *pszFilename,
const OGRSpatialReference *poSpatialRef,
OGRwkbGeometryType eGType,
bool bCreateSpatialIndexAtClose, char **papszOptions)
OGRFlatGeobufLayer *OGRFlatGeobufLayer::Create(
GDALDataset *poDS, const char *pszLayerName, const char *pszFilename,
const OGRSpatialReference *poSpatialRef, OGRwkbGeometryType eGType,
bool bCreateSpatialIndexAtClose, char **papszOptions)
{
std::string osTempFile = GetTempFilePath(pszFilename, papszOptions);
VSILFILE *poFpWrite =
CreateOutputFile(pszFilename, papszOptions, bCreateSpatialIndexAtClose);
if (poFpWrite == nullptr)
return nullptr;
OGRFlatGeobufLayer *layer = new OGRFlatGeobufLayer(
pszLayerName, pszFilename, poSpatialRef, eGType,
bCreateSpatialIndexAtClose, poFpWrite, osTempFile);
poDS, pszLayerName, pszFilename, poSpatialRef, eGType,
bCreateSpatialIndexAtClose, poFpWrite, osTempFile, papszOptions);
return layer;
}