diff --git a/autotest/ogr/ogr_geojson.py b/autotest/ogr/ogr_geojson.py index 1fe232c98fa5..8fad3db57da7 100755 --- a/autotest/ogr/ogr_geojson.py +++ b/autotest/ogr/ogr_geojson.py @@ -4734,8 +4734,6 @@ def test_ogr_geojson_foreign_members_feature(tmp_vsimem): ############################################################################### - - def test_ogr_json_getextent3d(tmp_vsimem): jdata = r"""{ @@ -4743,7 +4741,7 @@ def test_ogr_json_getextent3d(tmp_vsimem): "features": [ { "type": "Feature", - "properties": {}, + "properties": {"foo": "bar"}, "geometry": { "type": "%s", "coordinates": %s @@ -4751,7 +4749,7 @@ def test_ogr_json_getextent3d(tmp_vsimem): }, { "type": "Feature", - "properties": {}, + "properties": {"foo": "baz"}, "geometry": { "type": "%s", "coordinates": %s @@ -4792,7 +4790,8 @@ def test_ogr_json_getextent3d(tmp_vsimem): assert gdal.GetLastErrorMsg() == "" lyr = ds.GetLayer(0) - assert not lyr.TestCapability(ogr.OLCFastGetExtent3D) + assert lyr.TestCapability(ogr.OLCFastGetExtent3D) + assert lyr.TestCapability(ogr.OLCFastGetExtent) dfn = lyr.GetLayerDefn() assert dfn.GetGeomFieldCount() == 1 ext2d = lyr.GetExtent() @@ -4800,6 +4799,32 @@ def test_ogr_json_getextent3d(tmp_vsimem): ext3d = lyr.GetExtent3D() assert ext3d == (1.0, 2.0, 1.0, 2.0, float("inf"), float("-inf")) + # Test capabilities and extent with filters and round trip + lyr.SetAttributeFilter("foo = 'baz'") + assert not lyr.TestCapability(ogr.OLCFastGetExtent3D) + assert not lyr.TestCapability(ogr.OLCFastGetExtent) + ext2d = lyr.GetExtent() + assert ext2d == (2.0, 2.0, 2.0, 2.0) + ext3d = lyr.GetExtent3D() + assert ext3d == (2.0, 2.0, 2.0, 2.0, float("inf"), float("-inf")) + + lyr.SetAttributeFilter(None) + assert lyr.TestCapability(ogr.OLCFastGetExtent3D) + assert lyr.TestCapability(ogr.OLCFastGetExtent) + ext2d = lyr.GetExtent() + assert ext2d == (1.0, 2.0, 1.0, 2.0) + ext3d = lyr.GetExtent3D() + assert ext3d == (1.0, 2.0, 1.0, 2.0, float("inf"), float("-inf")) + + # Test capability with geometry filter + lyr.SetSpatialFilterRect(1.5, 1.5, 2.5, 2.5) + assert not lyr.TestCapability(ogr.OLCFastGetExtent3D) + assert not lyr.TestCapability(ogr.OLCFastGetExtent) + ext2d = lyr.GetExtent() + assert ext2d == (2.0, 2.0, 2.0, 2.0) + ext3d = lyr.GetExtent3D() + assert ext3d == (2.0, 2.0, 2.0, 2.0, float("inf"), float("-inf")) + # Test mixed 2D gdal.FileFromMemBuffer( tmp_vsimem / "test.json", @@ -4819,3 +4844,215 @@ def test_ogr_json_getextent3d(tmp_vsimem): assert ext2d == (1.0, 2.0, 1.0, 2.0) ext3d = lyr.GetExtent3D() assert ext3d == (1.0, 2.0, 1.0, 2.0, 1.0, 1.0) + + # Text mixed geometry types + gdal.FileFromMemBuffer( + tmp_vsimem / "test.json", + jdata % ("Point", "[1, 1, 1]", "LineString", "[[2, 2, 2], [3, 3, 3]]"), + ) + + ds = gdal.OpenEx(tmp_vsimem / "test.json", gdal.OF_VECTOR) + + assert gdal.GetLastErrorMsg() == "" + + lyr = ds.GetLayer(0) + dfn = lyr.GetLayerDefn() + assert dfn.GetGeomFieldCount() == 1 + # Check geometry type is unknown + assert dfn.GetGeomFieldDefn(0).GetType() == ogr.wkbUnknown + + # Test a polygon + gdal.FileFromMemBuffer( + tmp_vsimem / "test.json", + """{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]]] + } + } + ] + }""", + ) + + ds = gdal.OpenEx(tmp_vsimem / "test.json", gdal.OF_VECTOR) + + assert gdal.GetLastErrorMsg() == "" + + lyr = ds.GetLayer(0) + dfn = lyr.GetLayerDefn() + assert dfn.GetGeomFieldCount() == 1 + ext2d = lyr.GetExtent() + assert ext2d == (1.0, 2.0, 1.0, 2.0) + ext3d = lyr.GetExtent3D() + assert ext3d == (1.0, 2.0, 1.0, 2.0, float("inf"), float("-inf")) + + # Test a polygon with a hole + gdal.FileFromMemBuffer( + tmp_vsimem / "test.json", + """{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[1, 1], [2, 1], [2, 2], [1, 2], [1, 1]], [[1.5, 1.5], [1.5, 1.6], [1.6, 1.6], [1.6, 1.5], [1.5, 1.5]]] + } + } + ] + }""", + ) + + ds = gdal.OpenEx(tmp_vsimem / "test.json", gdal.OF_VECTOR) + + assert gdal.GetLastErrorMsg() == "" + + lyr = ds.GetLayer(0) + dfn = lyr.GetLayerDefn() + assert dfn.GetGeomFieldCount() == 1 + ext2d = lyr.GetExtent() + assert ext2d == (1.0, 2.0, 1.0, 2.0) + ext3d = lyr.GetExtent3D() + assert ext3d == (1.0, 2.0, 1.0, 2.0, float("inf"), float("-inf")) + + # Test a series of different 2D geometries including polygons with holes + gdal.FileFromMemBuffer( + tmp_vsimem / "test.json", + """{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [1, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [[2, 2], [3, 3]] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[4, 4], [5, 4], [5, 5], [4, 5], [4, 4]]] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[6, 6], [7, 6], [7, 7], [6, 7], [6, 6]], [[6.5, 6.5], [6.5, 6.6], [6.6, 6.6], [6.6, 6.5], [6.5, 6.5]]] + } + } + ] + }""", + ) + + ds = gdal.OpenEx(tmp_vsimem / "test.json", gdal.OF_VECTOR) + + assert gdal.GetLastErrorMsg() == "" + + lyr = ds.GetLayer(0) + + assert lyr.GetExtent() == (1.0, 7.0, 1.0, 7.0) + assert lyr.GetExtent3D() == (1.0, 7.0, 1.0, 7.0, float("inf"), float("-inf")) + + # Test a series of different 3D geometries including polygons with holes + + gdal.FileFromMemBuffer( + tmp_vsimem / "test.json", + """{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Point", + "coordinates": [1, 1, 1] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "LineString", + "coordinates": [[2, 2, 2], [3, 3, 3]] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[4, 4, 4], [5, 4, 4], [5, 5, 5], [4, 5, 5], [4, 4, 4]]] + } + }, + { + "type": "Feature", + "properties": {}, + "geometry": { + "type": "Polygon", + "coordinates": [[[6, 6, 6], [7, 6, 6], [7, 7, 7], [6, 7, 7], [6, 6, 6]], [[6.5, 6.5, 6.5], [6.5, 6.6, 6.5], [6.6, 6.6, 6.5], [6.6, 6.5, 6.5], [6.5, 6.5, 6.5]]] + } + } + ] + }""", + ) + + ds = gdal.OpenEx(tmp_vsimem / "test.json", gdal.OF_VECTOR) + + assert gdal.GetLastErrorMsg() == "" + + lyr = ds.GetLayer(0) + + assert lyr.GetExtent() == (1.0, 7.0, 1.0, 7.0) + assert lyr.GetExtent3D() == (1.0, 7.0, 1.0, 7.0, 1.0, 7.0) + + # Test geometrycollection + gdal.FileFromMemBuffer( + tmp_vsimem / "test.json", + r""" + { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "properties": {}, + "geometry": { + "type": "GeometryCollection", + "geometries": [{ + "type": "Point", + "coordinates": [6, 7] + }, { + "type": "Polygon", + "coordinates": [[[3, 4, 2], [5, 4, 4], [5, 5, 5], [4, 5, 5], [3, 4, 2]]] + }] + } + }] + } + """, + ) + + ds = gdal.OpenEx(tmp_vsimem / "test.json", gdal.OF_VECTOR) + + assert gdal.GetLastErrorMsg() == "" + + lyr = ds.GetLayer(0) + + assert lyr.GetExtent() == (3.0, 6.0, 4.0, 7.0) + assert lyr.GetExtent3D() == (3.0, 6.0, 4.0, 7.0, 2.0, 5.0) diff --git a/ogr/ogrsf_frmts/geojson/ogr_geojson.h b/ogr/ogrsf_frmts/geojson/ogr_geojson.h index 38399e473789..910f1e500abc 100644 --- a/ogr/ogrsf_frmts/geojson/ogr_geojson.h +++ b/ogr/ogrsf_frmts/geojson/ogr_geojson.h @@ -91,6 +91,11 @@ class OGRGeoJSONLayer final : public OGRMemLayer int nFlags) override; virtual OGRErr CreateGeomField(const OGRGeomFieldDefn *poGeomField, int bApproxOK = TRUE) override; + virtual OGRErr GetExtent(OGREnvelope *psExtent, int bForce = TRUE) override; + virtual OGRErr GetExtent(int iGeomField, OGREnvelope *psExtent, + int bForce = TRUE) override; + virtual OGRErr GetExtent3D(int iGeomField, OGREnvelope3D *psExtent3D, + int bForce = TRUE) override; // // OGRGeoJSONLayer Interface diff --git a/ogr/ogrsf_frmts/geojson/ogrgeojsonlayer.cpp b/ogr/ogrsf_frmts/geojson/ogrgeojsonlayer.cpp index fb27a926607b..d16f1d3795c6 100644 --- a/ogr/ogrsf_frmts/geojson/ogrgeojsonlayer.cpp +++ b/ogr/ogrsf_frmts/geojson/ogrgeojsonlayer.cpp @@ -438,6 +438,52 @@ OGRErr OGRGeoJSONLayer::CreateGeomField(const OGRGeomFieldDefn *poGeomField, return OGRMemLayer::CreateGeomField(poGeomField, bApproxOK); } +OGRErr OGRGeoJSONLayer::GetExtent(OGREnvelope *psExtent, int bForce) +{ + return GetExtent(0, psExtent, bForce); +} + +OGRErr OGRGeoJSONLayer::GetExtent(int iGeomField, OGREnvelope *psExtent, + int bForce) +{ + if (iGeomField != 0) + { + return OGRERR_FAILURE; + } + + if (poReader_ && poReader_->ExtentRead() && + TestCapability(OLCFastGetExtent)) + { + *psExtent = poReader_->GetExtent3D(); + return OGRERR_NONE; + } + else + { + return OGRMemLayer::GetExtentInternal(iGeomField, psExtent, bForce); + } +} + +OGRErr OGRGeoJSONLayer::GetExtent3D(int iGeomField, OGREnvelope3D *psExtent3D, + int bForce) +{ + + if (iGeomField != 0) + { + return OGRERR_FAILURE; + } + + if (poReader_ && poReader_->ExtentRead() && + TestCapability(OLCFastGetExtent3D)) + { + *psExtent3D = poReader_->GetExtent3D(); + return OGRERR_NONE; + } + else + { + return OGRMemLayer::GetExtent3D(iGeomField, psExtent3D, bForce); + } +} + /************************************************************************/ /* TestCapability() */ /************************************************************************/ @@ -451,6 +497,9 @@ int OGRGeoJSONLayer::TestCapability(const char *pszCap) return TRUE; else if (EQUAL(pszCap, OLCStringsAsUTF8)) return TRUE; + else if (EQUAL(pszCap, OLCFastGetExtent) || + EQUAL(pszCap, OLCFastGetExtent3D)) + return m_poFilterGeom == nullptr && m_poAttrQuery == nullptr; return OGRMemLayer::TestCapability(pszCap); } diff --git a/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.cpp b/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.cpp index 2aa5cbaf6888..fdf58480c32d 100644 --- a/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.cpp +++ b/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.cpp @@ -35,6 +35,7 @@ #include #include +#include /************************************************************************/ /* OGRGeoJSONReaderStreamingParser */ @@ -1722,14 +1723,19 @@ bool OGRGeoJSONBaseReader::GenerateFeatureDefn( poObj, poObjProps, nPrevFieldIdx, oMapFieldNameToIdx, apoFieldDefn, dag, bFeatureLevelIdAsFID_, bFeatureLevelIdAsAttribute_, m_bNeedFID64); - if (m_bDetectLayerGeomType) + json_object *poGeomObj = CPL_json_object_object_get(poObj, "geometry"); + if (poGeomObj && json_object_get_type(poGeomObj) == json_type_object) { - json_object *poGeomObj = CPL_json_object_object_get(poObj, "geometry"); - if (poGeomObj && json_object_get_type(poGeomObj) == json_type_object) + const auto eType = OGRGeoJSONGetOGRGeometryType(poGeomObj); + + OGRGeoJSONUpdateLayerGeomType(m_bFirstGeometry, eType, + m_eLayerGeomType); + + if (eType != wkbNone && eType != wkbUnknown) { - const auto eType = OGRGeoJSONGetOGRGeometryType(poGeomObj); - m_bDetectLayerGeomType = OGRGeoJSONUpdateLayerGeomType( - m_bFirstGeometry, eType, m_eLayerGeomType); + // This is maybe too optimistic: it assumes that the geometry + // coordinates array is in the correct format + m_bExtentRead |= OGRGeoJSONGetExtent3D(poGeomObj, &m_oEnvelope3D); } } @@ -2318,6 +2324,20 @@ OGRFeature *OGRGeoJSONBaseReader::ReadFeature(OGRLayer *poLayer, return poFeature; } +/************************************************************************/ +/* Extent getters */ +/************************************************************************/ + +bool OGRGeoJSONBaseReader::ExtentRead() const +{ + return m_bExtentRead; +} + +OGREnvelope3D OGRGeoJSONBaseReader::GetExtent3D() const +{ + return m_oEnvelope3D; +} + /************************************************************************/ /* ReadFeatureCollection() */ /************************************************************************/ @@ -3157,3 +3177,146 @@ json_object *CPL_json_object_object_get(struct json_object *obj, json_object_object_get_ex(obj, key, &poRet); return poRet; } + +bool OGRGeoJSONGetExtent3D(json_object *poObj, OGREnvelope3D *poEnvelope) +{ + if (!poEnvelope || !poObj) + { + return false; + } + + // poObjCoords can be an array of arrays, this lambda function will + // recursively parse the array + std::function fParseCoords; + fParseCoords = [&fParseCoords](json_object *poObjCoordsIn, + OGREnvelope3D *poEnvelopeIn) -> bool + { + if (json_type_array == json_object_get_type(poObjCoordsIn)) + { + const auto nItems = json_object_array_length(poObjCoordsIn); + + double dXVal = std::numeric_limits::quiet_NaN(); + double dYVal = std::numeric_limits::quiet_NaN(); + double dZVal = std::numeric_limits::quiet_NaN(); + + for (auto i = decltype(nItems){0}; i < nItems; ++i) + { + + // Get the i element + json_object *poObjCoordsElement = + json_object_array_get_idx(poObjCoordsIn, i); + + const json_type eType{json_object_get_type(poObjCoordsElement)}; + + // if it is an array, recurse + if (json_type_array == eType) + { + if (!fParseCoords(poObjCoordsElement, poEnvelopeIn)) + { + return false; + } + } + else if (json_type_double == eType || json_type_int == eType) + { + switch (i) + { + case 0: + { + dXVal = json_object_get_double(poObjCoordsElement); + break; + } + case 1: + { + dYVal = json_object_get_double(poObjCoordsElement); + break; + } + case 2: + { + dZVal = json_object_get_double(poObjCoordsElement); + break; + } + default: + return false; + } + } + else + { + return false; + } + } + + if (!std::isnan(dXVal) && !std::isnan(dYVal)) + { + if (std::isnan(dZVal)) + { + static_cast(poEnvelopeIn) + ->Merge(dXVal, dYVal); + } + else + { + poEnvelopeIn->Merge(dXVal, dYVal, dZVal); + } + } + + return true; + } + else + { + return false; + } + }; + + // This function looks for "coordinates" and for "geometries" to handle + // geometry collections. It will recurse on itself to handle nested geometry. + std::function fParseGeometry; + fParseGeometry = [&fParseGeometry, + &fParseCoords](json_object *poObjIn, + OGREnvelope3D *poEnvelopeIn) -> bool + { + // Get the "coordinates" array from the JSON object + json_object *poObjCoords = + OGRGeoJSONFindMemberByName(poObjIn, "coordinates"); + + // Return if found and not an array + if (poObjCoords && json_object_get_type(poObjCoords) != json_type_array) + { + return false; + } + else if (poObjCoords) + { + return fParseCoords(poObjCoords, poEnvelopeIn); + } + + // Try "geometries" + if (!poObjCoords) + { + poObjCoords = OGRGeoJSONFindMemberByName(poObjIn, "geometries"); + } + + // Return if not found or not an array + if (!poObjCoords || + json_object_get_type(poObjCoords) != json_type_array) + { + return false; + } + else + { + // Loop thgrough the geometries + const auto nItems = json_object_array_length(poObjCoords); + for (auto i = decltype(nItems){0}; i < nItems; ++i) + { + json_object *poObjGeometry = + json_object_array_get_idx(poObjCoords, i); + + // Recurse + if (!fParseGeometry(poObjGeometry, poEnvelopeIn)) + { + return false; + } + } + return true; + } + }; + + return fParseGeometry(poObj, poEnvelope); +} diff --git a/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.h b/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.h index dc9bc4e6e730..9c592c783695 100644 --- a/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.h +++ b/ogr/ogrsf_frmts/geojson/ogrgeojsonreader.h @@ -116,6 +116,10 @@ class OGRGeoJSONBaseReader OGRFeature *ReadFeature(OGRLayer *poLayer, json_object *poObj, const char *pszSerializedObj); + bool ExtentRead() const; + + OGREnvelope3D GetExtent3D() const; + protected: bool bGeometryPreserve_ = true; bool bAttributesSkip_ = false; @@ -139,8 +143,10 @@ class OGRGeoJSONBaseReader bool bFeatureLevelIdAsFID_ = false; bool m_bNeedFID64 = false; - bool m_bDetectLayerGeomType = true; bool m_bFirstGeometry = true; + OGREnvelope3D m_oEnvelope3D; + // Becomes true when extent has been read from data + bool m_bExtentRead = false; OGRwkbGeometryType m_eLayerGeomType = wkbUnknown; CPL_DISALLOW_COPY_ASSIGN(OGRGeoJSONBaseReader) @@ -269,6 +275,9 @@ bool OGRGeoJSONUpdateLayerGeomType(bool &bFirstGeom, OGRwkbGeometryType eGeomType, OGRwkbGeometryType &eLayerGeomType); +// Get the 3D extent from the geometry coordinates of a feature +bool OGRGeoJSONGetExtent3D(json_object *poObj, OGREnvelope3D *poEnvelope); + /************************************************************************/ /* GeoJSON Geometry Translators */ /************************************************************************/