Skip to content

Commit

Permalink
[OAPIF provider] OGC API features 2 / CRS related fixes (fixes #52228)
Browse files Browse the repository at this point in the history
- Takes into account storageCrs in /collection response to get the
  default crs in priority
- Append &crs=... to /items requests when the crs is not OGC:CRS84
- Make sure geometry axis order is flipped to easting/northing or
  long/lat when reading geometries from a CRS with inverted axis order
- Report the list of available CRS in the dialog box where one can
  select one
- Resolve "#/crs"
  • Loading branch information
rouault authored and github-actions[bot] committed Apr 3, 2023
1 parent 54498f0 commit 18a1980
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 53 deletions.
72 changes: 61 additions & 11 deletions src/providers/wfs/oapif/qgsoapifcollection.cpp
Expand Up @@ -25,7 +25,7 @@ using namespace nlohmann;

#include <QTextCodec>

bool QgsOapifCollection::deserialize( const json &j )
bool QgsOapifCollection::deserialize( const json &j, const json &jCollections )
{
if ( !j.is_object() )
return false;
Expand Down Expand Up @@ -121,7 +121,6 @@ bool QgsOapifCollection::deserialize( const json &j )
crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QString::fromStdString( jCrs.get<std::string>() ) );
}
}
mLayerMetadata.setCrs( crs );

const auto jBboxes = spatial["bbox"];
if ( jBboxes.is_array() )
Expand All @@ -148,6 +147,7 @@ bool QgsOapifCollection::deserialize( const json &j )
{
if ( firstBbox )
{
mBboxCrs = crs;
mBbox.set( values[0], values[1], values[2], values[3] );
}
spatialExtent.bounds = QgsBox3d( mBbox );
Expand All @@ -156,6 +156,7 @@ bool QgsOapifCollection::deserialize( const json &j )
{
if ( firstBbox )
{
mBboxCrs = crs;
mBbox.set( values[0], values[1], values[3], values[4] );
}
spatialExtent.bounds = QgsBox3d( values[0], values[1], values[2],
Expand Down Expand Up @@ -298,25 +299,74 @@ bool QgsOapifCollection::deserialize( const json &j )
}
}

// Usage storageCrs from Part 2 in priority
bool layerCrsSet = false;
if ( j.contains( "storageCrs" ) )
{
const auto crsUrl = j["storageCrs"];
if ( crsUrl.is_string() )
{
QString crsStr = QString::fromStdString( crsUrl.get<std::string>() );
QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsStr );

if ( j.contains( "storageCrsCoordinateEpoch" ) )
{
const auto storageCrsCoordinateEpoch = j["storageCrsCoordinateEpoch"];
if ( storageCrsCoordinateEpoch.is_number() )
{
crs.setCoordinateEpoch( storageCrsCoordinateEpoch.get<double>() );
}
}

layerCrsSet = true;
mLayerMetadata.setCrs( crs );
mCrsList.append( crs.authid() );
}
}

if ( j.contains( "crs" ) )
{
const auto crsUrls = j["crs"];
if ( crsUrls.is_array() )
json jCrs = j["crs"];
// Resolve "#/crs" link
if ( jCrs.is_array() && jCrs.size() == 1 &&
jCrs[0].is_string() && jCrs[0].get<std::string>() == "#/crs" &&
jCollections.is_object() && jCollections.contains( "crs" ) )
{
for ( const auto &crsUrl : crsUrls )
jCrs = jCollections["crs"];
}

if ( jCrs.is_array() )
{
for ( const auto &crsUrl : jCrs )
{
if ( crsUrl.is_string() )
{
QString crs = QString::fromStdString( crsUrl.get<std::string>() );
mLayerMetadata.setCrs( QgsCoordinateReferenceSystem::fromOgcWmsCrs( crs ) );
QString crsStr = QString::fromStdString( crsUrl.get<std::string>() );
QgsCoordinateReferenceSystem crs( QgsCoordinateReferenceSystem::fromOgcWmsCrs( crsStr ) );
if ( !layerCrsSet )
{
// Take the first CRS of the list
layerCrsSet = true;
mLayerMetadata.setCrs( crs );
}

// Take the first CRS of the list
break;
if ( !mCrsList.contains( crs.authid() ) )
{
mCrsList.append( crs.authid() );
}
}
}
}
}

if ( mCrsList.isEmpty() )
{
QgsCoordinateReferenceSystem crs = QgsCoordinateReferenceSystem::fromOgcWmsCrs(
QgsOapifProvider::OAPIF_PROVIDER_DEFAULT_CRS );
mLayerMetadata.setCrs( QgsCoordinateReferenceSystem::fromOgcWmsCrs( crs.authid() ) );
mCrsList.append( crs.authid() );
}

return true;
}

Expand Down Expand Up @@ -408,7 +458,7 @@ void QgsOapifCollectionsRequest::processReply()
for ( const auto &jCollection : collections )
{
QgsOapifCollection collection;
if ( collection.deserialize( jCollection ) )
if ( collection.deserialize( jCollection, j ) )
{
if ( collection.mLayerMetadata.licenses().isEmpty() )
{
Expand Down Expand Up @@ -502,7 +552,7 @@ void QgsOapifCollectionRequest::processReply()
try
{
const json j = json::parse( utf8Text.toStdString() );
mCollection.deserialize( j );
mCollection.deserialize( j, json() );
}
catch ( const json::parse_error &ex )
{
Expand Down
10 changes: 8 additions & 2 deletions src/providers/wfs/oapif/qgsoapifcollection.h
Expand Up @@ -39,14 +39,20 @@ struct QgsOapifCollection
//! Description
QString mDescription;

//! Bounding box (in CRS84)
//! Bounding box
QgsRectangle mBbox;

//! Bounding box Crs
QgsCoordinateReferenceSystem mBboxCrs;

//! List of available CRS
QList<QString> mCrsList;

//! Layer metadata
QgsLayerMetadata mLayerMetadata;

//! Fills a collection from its JSON serialization
bool deserialize( const json &j );
bool deserialize( const json &j, const json &jCollections );
};

//! Manages the /collections request
Expand Down
27 changes: 25 additions & 2 deletions src/providers/wfs/oapif/qgsoapifprovider.cpp
Expand Up @@ -151,20 +151,33 @@ bool QgsOapifProvider::init()
return false;
}
}
mShared->mCapabilityExtent = collectionRequest->collection().mBbox;

mLayerMetadata = collectionRequest->collection().mLayerMetadata;

if ( mLayerMetadata.crs().isValid() )
{
// WORKAROUND: Recreate a CRS object with fromOgcWmsCrs because when copying the
// CRS his mPj pointer gets deleted and it is impossible to create a transform
mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( mLayerMetadata.crs().authid() );
mShared->mSourceCrs.setCoordinateEpoch( mLayerMetadata.crs().coordinateEpoch() );
}
else
{
mShared->mSourceCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs(
QgsOapifProvider::OAPIF_PROVIDER_DEFAULT_CRS );
}
mShared->mCapabilityExtent = collectionRequest->collection().mBbox;

// Reproject extent of /collection request to the layer CRS
if ( !mShared->mCapabilityExtent.isNull() &&
collectionRequest->collection().mBboxCrs != mShared->mSourceCrs )
{
QgsCoordinateTransform ct( collectionRequest->collection().mBboxCrs, mShared->mSourceCrs, transformContext() );
ct.setBallparkTransformsAreAppropriate( true );
QgsDebugMsgLevel( "before ext:" + mShared->mCapabilityExtent.toString(), 4 );
mShared->mCapabilityExtent = ct.transformBoundingBox( mShared->mCapabilityExtent );
QgsDebugMsgLevel( "after ext:" + mShared->mCapabilityExtent.toString(), 4 );
}

// Merge contact info from /api
mLayerMetadata.setContacts( apiRequest.metadata().contacts() );
Expand Down Expand Up @@ -788,6 +801,10 @@ void QgsOapifFeatureDownloaderImpl::run( bool serializeFeatures, long long maxFe
}
}

if ( mShared->mSourceCrs
!= QgsCoordinateReferenceSystem::fromOgcWmsCrs( QgsOapifProvider::OAPIF_PROVIDER_DEFAULT_CRS ) )
url += QStringLiteral( "&crs=%1" ).arg( mShared->mSourceCrs.toOgcUri() );

while ( !url.isEmpty() )
{
url = mShared->appendExtraQueryParameters( url );
Expand Down Expand Up @@ -843,7 +860,13 @@ void QgsOapifFeatureDownloaderImpl::run( bool serializeFeatures, long long maxFe
// as the layer, convert them
const QgsFeature &f = pair.first;
QgsFeature dstFeat( dstFields, f.id() );
dstFeat.setGeometry( f.geometry() );
if ( f.hasGeometry() )
{
QgsGeometry g = f.geometry();
if ( mShared->mSourceCrs.hasAxisInverted() )
g.transform( QTransform( 0, 1, 1, 0, 0, 0 ) );
dstFeat.setGeometry( g );
}
const auto srcAttrs = f.attributes();
for ( int j = 0; j < dstFields.size(); j++ )
{
Expand Down
13 changes: 7 additions & 6 deletions src/providers/wfs/qgsbasenetworkrequest.cpp
Expand Up @@ -122,12 +122,6 @@ bool QgsBaseNetworkRequest::sendGET( const QUrl &url, const QString &acceptHeade
}
#endif

// For REST API using URL subpaths, normalize the subpaths
const int afterEndpointStartPos = modifiedUrlString.indexOf( "fake_qgis_http_endpoint" ) + strlen( "fake_qgis_http_endpoint" );
QString afterEndpointStart = modifiedUrlString.mid( afterEndpointStartPos );
afterEndpointStart.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
modifiedUrlString = modifiedUrlString.mid( 0, afterEndpointStartPos ) + afterEndpointStart;

if ( !acceptHeader.isEmpty() )
{
if ( modifiedUrlString.indexOf( '?' ) > 0 )
Expand All @@ -139,6 +133,13 @@ bool QgsBaseNetworkRequest::sendGET( const QUrl &url, const QString &acceptHeade
modifiedUrlString += QStringLiteral( "?Accept=" ) + acceptHeader;
}
}

// For REST API using URL subpaths, normalize the subpaths
const int afterEndpointStartPos = static_cast<int>( modifiedUrlString.indexOf( "fake_qgis_http_endpoint" ) + strlen( "fake_qgis_http_endpoint" ) );
QString afterEndpointStart = modifiedUrlString.mid( afterEndpointStartPos );
afterEndpointStart.replace( QLatin1String( "/" ), QLatin1String( "_" ) );
modifiedUrlString = modifiedUrlString.mid( 0, afterEndpointStartPos ) + afterEndpointStart;

const auto posQuotationMark = modifiedUrlString.indexOf( '?' );
if ( posQuotationMark > 0 )
{
Expand Down
5 changes: 3 additions & 2 deletions src/providers/wfs/qgswfssourceselect.cpp
Expand Up @@ -380,6 +380,7 @@ void QgsWFSSourceSelect::oapifCollectionsReplyFinished()
return;
}

mAvailableCRS.clear();
for ( const auto &collection : mOAPIFCollections->collections() )
{
// insert the typenames, titles and abstracts into the tree view
Expand All @@ -393,8 +394,8 @@ void QgsWFSSourceSelect::oapifCollectionsReplyFinished()
typedef QList< QStandardItem * > StandardItemList;
mModel->appendRow( StandardItemList() << titleItem << nameItem << abstractItem << filterItem );

gbCRS->setEnabled( false );
labelCoordRefSys->setText( collection.mLayerMetadata.crs().authid() );
// insert the available CRS into mAvailableCRS
mAvailableCRS.insert( collection.mId, collection.mCrsList );
}

if ( !mOAPIFCollections->nextUrl().isEmpty() )
Expand Down

0 comments on commit 18a1980

Please sign in to comment.