mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-04 00:04:03 -04:00
4912 lines
164 KiB
C++
4912 lines
164 KiB
C++
/***************************************************************************
|
|
qgsogrprovider.cpp Data provider for OGR supported formats
|
|
Formerly known as qgsshapefileprovider.cpp
|
|
begin : Oct 29, 2003
|
|
copyright : (C) 2003 by Gary E.Sherman
|
|
email : sherman at mrcc.com
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "qgsogrprovider.h"
|
|
#include "moc_qgsogrprovider.cpp"
|
|
///@cond PRIVATE
|
|
|
|
#include "qgscplerrorhandler_p.h"
|
|
#include "qgslogger.h"
|
|
#include "qgsvectorfilewriter.h"
|
|
#include "qgsapplication.h"
|
|
#include "qgssettings.h"
|
|
#include "qgsogrconnpool.h"
|
|
#include "qgsogrtransaction.h"
|
|
#include "qgsogrfeatureiterator.h"
|
|
#include "qgsgdalutils.h"
|
|
#include "qgsfeedback.h"
|
|
#include "qgscplhttpfetchoverrider.h"
|
|
#include "qgsmetadatautils.h"
|
|
#include "qgslocalec.h"
|
|
#include "qgssymbol.h"
|
|
#include "qgsembeddedsymbolrenderer.h"
|
|
#include "qgsprovidersublayerdetails.h"
|
|
#include "qgsvectorlayer.h"
|
|
#include "qgsproviderregistry.h"
|
|
#include "qgsvariantutils.h"
|
|
#include "qgsjsonutils.h"
|
|
#include "qgssetrequestinitiator_p.h"
|
|
#include "qgsthreadingutils.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#define CPL_SUPRESS_CPLUSPLUS //#spellok
|
|
#include <gdal.h> // to collect version information
|
|
#include <ogr_api.h>
|
|
#include <ogr_srs_api.h>
|
|
#include <cpl_string.h>
|
|
|
|
#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 2, 1)
|
|
// Temporary solution for gdal < 3.2.1 without GDAL Unique support
|
|
#include "qgssqliteutils.h"
|
|
#include <sqlite3.h>
|
|
// end temporary
|
|
#endif
|
|
|
|
#include <limits>
|
|
#include <memory>
|
|
|
|
#include <QTextCodec>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#include <QRegularExpression>
|
|
|
|
#define TEXT_PROVIDER_KEY QStringLiteral( "ogr" )
|
|
#define TEXT_PROVIDER_DESCRIPTION QStringLiteral( "OGR data provider" )
|
|
|
|
bool QgsOgrProvider::convertField( QgsField &field, const QTextCodec &encoding )
|
|
{
|
|
OGRFieldType ogrType = OFTString; //default to string
|
|
OGRFieldSubType ogrSubType = OFSTNone;
|
|
int ogrWidth = field.length();
|
|
int ogrPrecision = field.precision();
|
|
if ( ogrPrecision > 0 )
|
|
ogrWidth += 1;
|
|
switch ( field.type() )
|
|
{
|
|
case QMetaType::Type::LongLong:
|
|
ogrType = OFTInteger64;
|
|
ogrPrecision = 0;
|
|
ogrWidth = ogrWidth > 0 && ogrWidth <= 21 ? ogrWidth : 21;
|
|
break;
|
|
|
|
case QMetaType::Type::QString:
|
|
ogrType = OFTString;
|
|
if ( ogrWidth < 0 || ogrWidth > 255 )
|
|
ogrWidth = 255;
|
|
break;
|
|
|
|
case QMetaType::Type::Int:
|
|
ogrType = OFTInteger;
|
|
ogrWidth = ogrWidth > 0 && ogrWidth <= 10 ? ogrWidth : 10;
|
|
ogrPrecision = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::Bool:
|
|
ogrType = OFTInteger;
|
|
ogrSubType = OFSTBoolean;
|
|
ogrWidth = 1;
|
|
ogrPrecision = 0;
|
|
break;
|
|
|
|
case QMetaType::Type::Double:
|
|
ogrType = OFTReal;
|
|
break;
|
|
|
|
case QMetaType::Type::QDate:
|
|
ogrType = OFTDate;
|
|
break;
|
|
|
|
case QMetaType::Type::QTime:
|
|
ogrType = OFTTime;
|
|
break;
|
|
|
|
case QMetaType::Type::QDateTime:
|
|
ogrType = OFTDateTime;
|
|
break;
|
|
|
|
case QMetaType::Type::QStringList:
|
|
{
|
|
ogrType = OFTStringList;
|
|
break;
|
|
}
|
|
|
|
case QMetaType::Type::QVariantList:
|
|
if ( field.subType() == QMetaType::Type::QString )
|
|
{
|
|
ogrType = OFTStringList;
|
|
}
|
|
else if ( field.subType() == QMetaType::Type::Int )
|
|
{
|
|
ogrType = OFTIntegerList;
|
|
}
|
|
else if ( field.subType() == QMetaType::Type::LongLong )
|
|
{
|
|
ogrType = OFTInteger64List;
|
|
}
|
|
else if ( field.subType() == QMetaType::Type::Double )
|
|
{
|
|
ogrType = OFTRealList;
|
|
}
|
|
else
|
|
{
|
|
// other lists are not supported at this moment
|
|
return false;
|
|
}
|
|
break;
|
|
|
|
case QMetaType::Type::QVariantMap:
|
|
ogrType = OFTString;
|
|
ogrSubType = OFSTJSON;
|
|
break;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if ( ogrSubType != OFSTNone )
|
|
field.setTypeName( encoding.toUnicode( OGR_GetFieldSubTypeName( ogrSubType ) ) );
|
|
else
|
|
field.setTypeName( encoding.toUnicode( OGR_GetFieldTypeName( ogrType ) ) );
|
|
|
|
field.setLength( ogrWidth );
|
|
field.setPrecision( ogrPrecision );
|
|
return true;
|
|
}
|
|
|
|
void QgsOgrProvider::repack()
|
|
{
|
|
if ( !mValid || mGDALDriverName != QLatin1String( "ESRI Shapefile" ) || !mOgrOrigLayer )
|
|
return;
|
|
|
|
// run REPACK on shape files
|
|
QByteArray sql = QByteArray( "REPACK " ) + mOgrOrigLayer->name(); // don't quote the layer name as it works with spaces in the name and won't work if the name is quoted
|
|
QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 );
|
|
CPLErrorReset();
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( sql );
|
|
if ( CPLGetLastErrorType() != CE_None )
|
|
{
|
|
pushError( tr( "OGR[%1] error %2: %3" ).arg( CPLGetLastErrorType() ).arg( CPLGetLastErrorNo() ).arg( CPLGetLastErrorMsg() ) );
|
|
}
|
|
|
|
if ( mFilePath.endsWith( QLatin1String( ".shp" ), Qt::CaseInsensitive ) || mFilePath.endsWith( QLatin1String( ".dbf" ), Qt::CaseInsensitive ) )
|
|
{
|
|
QString packedDbf( mFilePath.left( mFilePath.size() - 4 ) + "_packed.dbf" );
|
|
if ( QFile::exists( packedDbf ) )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Possible corruption after REPACK detected. %1 still exists. This may point to a permission or locking problem of the original DBF." ).arg( packedDbf ), tr( "OGR" ), Qgis::MessageLevel::Critical );
|
|
|
|
mOgrSqlLayer.reset();
|
|
mOgrOrigLayer.reset();
|
|
|
|
QString errCause;
|
|
if ( mLayerName.isNull() )
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, mOpenOptions, mLayerIndex, errCause, true );
|
|
}
|
|
else
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, mOpenOptions, mLayerName, errCause, true );
|
|
}
|
|
|
|
if ( !mOgrOrigLayer )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Original layer could not be reopened." ) + " " + errCause, tr( "OGR" ), Qgis::MessageLevel::Critical );
|
|
mValid = false;
|
|
}
|
|
|
|
mOgrLayer = mOgrOrigLayer.get();
|
|
}
|
|
|
|
}
|
|
|
|
if ( mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) &&
|
|
mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::UnknownCount ) )
|
|
{
|
|
long long oldcount = mFeaturesCounted;
|
|
recalculateFeatureCount();
|
|
if ( oldcount != mFeaturesCounted )
|
|
emit dataChanged();
|
|
}
|
|
}
|
|
|
|
Qgis::VectorExportResult QgsOgrProvider::createEmptyLayer( const QString &uri,
|
|
const QgsFields &fields,
|
|
Qgis::WkbType wkbType,
|
|
const QgsCoordinateReferenceSystem &srs,
|
|
bool overwrite,
|
|
QMap<int, int> *oldToNewAttrIdxMap,
|
|
QString &createdLayerUri,
|
|
QString *errorMessage,
|
|
const QMap<QString, QVariant> *options )
|
|
{
|
|
QString encoding;
|
|
QString driverName = QStringLiteral( "GPKG" );
|
|
QStringList dsOptions, layerOptions;
|
|
QString layerName;
|
|
|
|
if ( options )
|
|
{
|
|
if ( options->contains( QStringLiteral( "fileEncoding" ) ) )
|
|
encoding = options->value( QStringLiteral( "fileEncoding" ) ).toString();
|
|
|
|
if ( options->contains( QStringLiteral( "driverName" ) ) )
|
|
driverName = options->value( QStringLiteral( "driverName" ) ).toString();
|
|
|
|
if ( options->contains( QStringLiteral( "datasourceOptions" ) ) )
|
|
dsOptions << options->value( QStringLiteral( "datasourceOptions" ) ).toStringList();
|
|
|
|
if ( options->contains( QStringLiteral( "layerOptions" ) ) )
|
|
layerOptions << options->value( QStringLiteral( "layerOptions" ) ).toStringList();
|
|
|
|
if ( options->contains( QStringLiteral( "layerName" ) ) )
|
|
layerName = options->value( QStringLiteral( "layerName" ) ).toString();
|
|
}
|
|
|
|
oldToNewAttrIdxMap->clear();
|
|
if ( errorMessage )
|
|
errorMessage->clear();
|
|
|
|
QgsVectorFileWriter::ActionOnExistingFile action( QgsVectorFileWriter::CreateOrOverwriteFile );
|
|
|
|
bool update = false;
|
|
if ( options && options->contains( QStringLiteral( "update" ) ) )
|
|
{
|
|
update = options->value( QStringLiteral( "update" ) ).toBool();
|
|
if ( update )
|
|
{
|
|
// when updating an existing dataset, always use the original driver
|
|
GDALDriverH hDriver = GDALIdentifyDriverEx( uri.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr );
|
|
if ( hDriver )
|
|
{
|
|
driverName = GDALGetDriverShortName( hDriver );
|
|
}
|
|
|
|
if ( !overwrite && !layerName.isEmpty() )
|
|
{
|
|
gdal::dataset_unique_ptr hDS( GDALOpenEx( uri.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ) );
|
|
if ( hDS )
|
|
{
|
|
if ( GDALDatasetGetLayerByName( hDS.get(), layerName.toUtf8().constData() ) )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage += QObject::tr( "Layer %2 of %1 exists and overwrite flag is false." )
|
|
.arg( uri, layerName );
|
|
return Qgis::VectorExportResult::ErrorCreatingDataSource;
|
|
}
|
|
}
|
|
}
|
|
if ( QFileInfo::exists( uri ) )
|
|
action = QgsVectorFileWriter::CreateOrOverwriteLayer;
|
|
}
|
|
}
|
|
|
|
if ( !overwrite && !update )
|
|
{
|
|
if ( QFileInfo::exists( uri ) )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage += QObject::tr( "Unable to create the datasource. %1 exists and overwrite flag is false." )
|
|
.arg( uri );
|
|
return Qgis::VectorExportResult::ErrorCreatingDataSource;
|
|
}
|
|
}
|
|
|
|
QgsFields cleanedFields = fields;
|
|
#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 6, 3)
|
|
// Temporary solution. Proper fix in https://github.com/OSGeo/gdal/pull/7147
|
|
if ( driverName == QLatin1String( "OpenFileGDB" ) )
|
|
{
|
|
QString fidColumn = QStringLiteral( "OBJECTID" );
|
|
for ( const QString &layerOption : layerOptions )
|
|
{
|
|
if ( layerOption.startsWith( QLatin1String( "FID=" ), Qt::CaseInsensitive ) )
|
|
{
|
|
fidColumn = layerOption.mid( static_cast<int>( strlen( "FID=" ) ) );
|
|
break;
|
|
}
|
|
}
|
|
const int objectIdIndex = cleanedFields.lookupField( fidColumn );
|
|
if ( objectIdIndex >= 0 )
|
|
{
|
|
cleanedFields.remove( objectIdIndex );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
QString newLayerName( layerName );
|
|
|
|
QgsVectorFileWriter::SaveVectorOptions saveOptions;
|
|
saveOptions.layerName = layerName;
|
|
saveOptions.fileEncoding = encoding;
|
|
saveOptions.driverName = driverName;
|
|
saveOptions.datasourceOptions = dsOptions;
|
|
saveOptions.layerOptions = layerOptions;
|
|
saveOptions.actionOnExistingFile = action;
|
|
saveOptions.symbologyExport = Qgis::FeatureSymbologyExport::NoSymbology;
|
|
std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( uri, cleanedFields, wkbType, srs, QgsCoordinateTransformContext(), saveOptions, QgsFeatureSink::SinkFlags(), nullptr, &newLayerName ) );
|
|
layerName = newLayerName;
|
|
|
|
QVariantMap uriParts = QgsOgrProviderMetadata().decodeUri( uri );
|
|
uriParts.insert( QStringLiteral( "layerName" ), newLayerName );
|
|
createdLayerUri = QgsOgrProviderMetadata().encodeUri( uriParts );
|
|
|
|
QgsVectorFileWriter::WriterError error = writer->hasError();
|
|
if ( error )
|
|
{
|
|
if ( errorMessage )
|
|
*errorMessage += writer->errorMessage();
|
|
|
|
return static_cast<Qgis::VectorExportResult>( error );
|
|
}
|
|
|
|
QMap<int, int> attrIdxMap = writer->sourceFieldIndexToWriterFieldIndex();
|
|
writer.reset();
|
|
|
|
{
|
|
bool firstFieldIsFid = false;
|
|
bool fidColumnIsField = false;
|
|
if ( !layerName.isEmpty() )
|
|
{
|
|
gdal::dataset_unique_ptr hDS( GDALOpenEx( uri.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ) );
|
|
if ( hDS )
|
|
{
|
|
OGRLayerH hLayer = GDALDatasetGetLayerByName( hDS.get(), layerName.toUtf8().constData() );
|
|
if ( hLayer )
|
|
{
|
|
// Expose the OGR FID if it comes from a "real" column (typically GPKG)
|
|
// and make sure that this FID column is not exposed as a regular OGR field (shouldn't happen normally)
|
|
const QString ogrFidColumnName { OGR_L_GetFIDColumn( hLayer ) };
|
|
firstFieldIsFid = !( EQUAL( OGR_L_GetFIDColumn( hLayer ), "" ) ) &&
|
|
OGR_FD_GetFieldIndex( OGR_L_GetLayerDefn( hLayer ), ogrFidColumnName.toUtf8() ) < 0 &&
|
|
cleanedFields.indexFromName( ogrFidColumnName.toUtf8() ) < 0;
|
|
// At this point we must check if there is a real FID field in the the fields argument,
|
|
// because in that case we don't want to shift all fields (see issue GH #34333)
|
|
// Check for unique values should be performed in client code.
|
|
for ( const auto &f : std::as_const( fields ) )
|
|
{
|
|
if ( f.name().compare( ogrFidColumnName, Qt::CaseSensitivity::CaseInsensitive ) == 0 )
|
|
{
|
|
fidColumnIsField = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const bool shiftColumnsByOne { firstFieldIsFid &&( ! fidColumnIsField ) };
|
|
|
|
for ( QMap<int, int>::const_iterator attrIt = attrIdxMap.constBegin(); attrIt != attrIdxMap.constEnd(); ++attrIt )
|
|
{
|
|
oldToNewAttrIdxMap->insert( attrIt.key(), *attrIt + ( shiftColumnsByOne ? 1 : 0 ) );
|
|
}
|
|
}
|
|
|
|
QgsOgrProviderUtils::invalidateCachedLastModifiedDate( uri );
|
|
|
|
return Qgis::VectorExportResult::Success;
|
|
}
|
|
|
|
QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &options, Qgis::DataProviderReadFlags flags )
|
|
: QgsVectorDataProvider( uri, options, flags )
|
|
{
|
|
QgsApplication::registerOgrDrivers();
|
|
|
|
QgsSettings settings;
|
|
// we always disable GDAL side shapefile encoding handling, and do it on the QGIS side.
|
|
// why? it's not the ideal choice, but...
|
|
// - if we DON'T disable GDAL side encoding support, then there's NO way to change the encoding used when reading
|
|
// shapefiles. And unfortunately the embedded encoding (which is read by GDAL) is sometimes wrong, so we need
|
|
// to expose support for users to be able to change and correct this
|
|
// - we can't change this setting on-the-fly. If we don't set it upfront, we can't reverse this decision later when
|
|
// a user does want/need to manually specify the encoding
|
|
CPLSetConfigOption( "SHAPE_ENCODING", "" );
|
|
|
|
#ifndef QT_NO_NETWORKPROXY
|
|
QgsGdalUtils::setupProxy();
|
|
#endif
|
|
|
|
// make connection to the data source
|
|
|
|
QgsDebugMsgLevel( "Data source uri is [" + uri + ']', 2 );
|
|
|
|
mFilePath = QgsOgrProviderUtils::analyzeURI( uri,
|
|
mIsSubLayer,
|
|
mLayerIndex,
|
|
mLayerName,
|
|
mSubsetString,
|
|
mOgrGeometryTypeFilter,
|
|
mOpenOptions,
|
|
mCredentialOptions );
|
|
|
|
const QVariantMap parts = QgsOgrProviderMetadata().decodeUri( uri );
|
|
if ( parts.contains( QStringLiteral( "uniqueGeometryType" ) ) )
|
|
{
|
|
mUniqueGeometryType = parts.value( QStringLiteral( "uniqueGeometryType" ) ).toString() == QLatin1String( "yes" );
|
|
}
|
|
|
|
const QString vsiPrefix = parts.value( QStringLiteral( "vsiPrefix" ) ).toString();
|
|
if ( !mCredentialOptions.isEmpty() && !vsiPrefix.isEmpty() )
|
|
{
|
|
const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*)/" ) );
|
|
const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() );
|
|
if ( bucketMatch.hasMatch() )
|
|
{
|
|
QgsGdalUtils::applyVsiCredentialOptions( vsiPrefix, bucketMatch.captured( 1 ), mCredentialOptions );
|
|
}
|
|
}
|
|
|
|
// to be called only after mFilePath has been set
|
|
invalidateNetworkCache();
|
|
|
|
if ( uri.contains( QLatin1String( "authcfg" ) ) )
|
|
{
|
|
const thread_local QRegularExpression authcfgRe( QStringLiteral( " authcfg='([^']+)'" ) );
|
|
QRegularExpressionMatch match;
|
|
if ( uri.contains( authcfgRe, &match ) )
|
|
{
|
|
mAuthCfg = match.captured( 1 );
|
|
// momentarily re-add authcfg since it was stripped off in analyzeURI
|
|
mFilePath = QgsOgrProviderUtils::expandAuthConfig( QStringLiteral( "%1 authcfg='%2'" ).arg( mFilePath, mAuthCfg ) );
|
|
}
|
|
}
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( mReadFlags & Qgis::DataProviderReadFlag::ForceReadOnly )
|
|
{
|
|
open( OpenModeForceReadOnly );
|
|
}
|
|
else
|
|
{
|
|
open( OpenModeInitial );
|
|
}
|
|
|
|
QList<NativeType> nativeTypes;
|
|
if ( mOgrOrigLayer )
|
|
{
|
|
nativeTypes = QgsOgrUtils::nativeFieldTypesForDriver( mOgrOrigLayer->driver() );
|
|
}
|
|
setNativeTypes( nativeTypes );
|
|
|
|
if ( mOgrOrigLayer )
|
|
{
|
|
bool is3D = OGR_GT_HasZ( mOGRGeomType );
|
|
/* We will not analyse the first geometry to compute if we have 3D or 2D data.
|
|
* We will believe in the metadata, even if the metadata is corrupted or misdefined.
|
|
* Below, here is an example of how retrieve the 3D/2D state according to the first geometry:
|
|
*/
|
|
// if ( !is3D )
|
|
// {
|
|
// gdal::ogr_feature_unique_ptr f;
|
|
// mOgrOrigLayer->ResetReading();
|
|
// f.reset( mOgrOrigLayer->GetNextFeature() );
|
|
// if ( f )
|
|
// {
|
|
// OGRGeometryH g = OGR_F_GetGeometryRef( f.get() );
|
|
// if ( g && !OGR_G_IsEmpty( g ) )
|
|
// {
|
|
// is3D = OGR_G_Is3D( g );
|
|
// }
|
|
// }
|
|
// }
|
|
elevationProperties()->setContainsElevationData( is3D );
|
|
mOgrOrigLayer->ResetReading(); // release
|
|
}
|
|
|
|
// layer metadata
|
|
loadMetadata();
|
|
|
|
QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
}
|
|
|
|
QgsOgrProvider::~QgsOgrProvider()
|
|
{
|
|
QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
// We must also make sure to flush unusef cached connections so that
|
|
// the file can be removed (#15137)
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
|
|
// Do that as last step for final cleanup that might be prevented by
|
|
// still opened datasets.
|
|
close();
|
|
}
|
|
|
|
QString QgsOgrProvider::dataSourceUri( bool expandAuthConfig ) const
|
|
{
|
|
if ( expandAuthConfig && QgsDataProvider::dataSourceUri( ).contains( QLatin1String( "authcfg" ) ) )
|
|
{
|
|
return QgsOgrProviderUtils::expandAuthConfig( QgsDataProvider::dataSourceUri( ) );
|
|
}
|
|
else
|
|
{
|
|
return QgsDataProvider::dataSourceUri( );
|
|
}
|
|
}
|
|
|
|
QgsTransaction *QgsOgrProvider::transaction() const
|
|
{
|
|
return static_cast<QgsTransaction *>( mTransaction );
|
|
}
|
|
|
|
void QgsOgrProvider::setTransaction( QgsTransaction *transaction )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "set transaction %1" ).arg( transaction != nullptr ), 2 );
|
|
// static_cast since layers cannot be added to a transaction of a non-matching provider
|
|
mTransaction = static_cast<QgsOgrTransaction *>( transaction );
|
|
if ( transaction )
|
|
{
|
|
connect( mTransaction, &QgsTransaction::afterRollback, this, &QgsOgrProvider::afterRollback );
|
|
connect( mTransaction, &QgsTransaction::afterRollbackToSavepoint, this, &QgsOgrProvider::afterRollbackToSavepoint );
|
|
}
|
|
}
|
|
|
|
QgsAbstractFeatureSource *QgsOgrProvider::featureSource() const
|
|
{
|
|
return new QgsOgrFeatureSource( this );
|
|
}
|
|
|
|
bool QgsOgrProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount )
|
|
{
|
|
return _setSubsetString( theSQL, updateFeatureCount, true );
|
|
}
|
|
|
|
QString QgsOgrProvider::subsetString() const
|
|
{
|
|
return mSubsetString;
|
|
}
|
|
|
|
bool QgsOgrProvider::supportsSubsetString() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
QString QgsOgrProvider::subsetStringDialect() const
|
|
{
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0)
|
|
if ( mOgrLayer )
|
|
{
|
|
if ( const char *pszSqlDialects = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_SUPPORTED_SQL_DIALECTS, nullptr ) )
|
|
{
|
|
const QStringList dialects = QString( pszSqlDialects ).split( ' ' );
|
|
// first dialect is default, which is what QGIS uses
|
|
const QString defaultDialect = !dialects.isEmpty() ? dialects.at( 0 ) : QString();
|
|
if ( defaultDialect == QLatin1String( "NATIVE" ) )
|
|
{
|
|
return tr( "%1 query" ).arg( GDALGetDriverLongName( mOgrLayer->driver() ) );
|
|
}
|
|
else if ( defaultDialect == QLatin1String( "OGRSQL" ) )
|
|
{
|
|
return tr( "OGR SQL query" );
|
|
}
|
|
else if ( defaultDialect == QLatin1String( "SQLITE" ) )
|
|
{
|
|
return tr( "SQLite query" );
|
|
}
|
|
return defaultDialect;
|
|
}
|
|
}
|
|
#endif
|
|
return QString();
|
|
}
|
|
|
|
QString QgsOgrProvider::subsetStringHelpUrl() const
|
|
{
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0)
|
|
if ( mOgrLayer )
|
|
{
|
|
if ( const char *pszSqlDialects = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_SUPPORTED_SQL_DIALECTS, nullptr ) )
|
|
{
|
|
const QStringList dialects = QString( pszSqlDialects ).split( ' ' );
|
|
// first dialect is default, which is what QGIS uses
|
|
const QString defaultDialect = !dialects.isEmpty() ? dialects.at( 0 ) : QString();
|
|
if ( defaultDialect == QLatin1String( "NATIVE" ) )
|
|
{
|
|
return QgsGdalUtils::gdalDocumentationUrlForDriver( mOgrLayer->driver() );
|
|
}
|
|
else if ( defaultDialect == QLatin1String( "OGRSQL" ) )
|
|
{
|
|
return QStringLiteral( "https://gdal.org/user/ogr_sql_dialect.html" );
|
|
}
|
|
else if ( defaultDialect == QLatin1String( "SQLITE" ) )
|
|
{
|
|
return QStringLiteral( "https://gdal.org/user/sql_sqlite_dialect.html" );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
return QString();
|
|
}
|
|
|
|
uint QgsOgrProvider::subLayerCount() const
|
|
{
|
|
uint count = layerCount();
|
|
if ( count == 0 )
|
|
return 0;
|
|
|
|
QString errCause;
|
|
QgsOgrLayerUniquePtr layerStyles = QgsOgrProviderUtils::getLayer( mFilePath, QStringLiteral( "layer_styles" ), errCause );
|
|
if ( layerStyles )
|
|
{
|
|
count--;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
QStringList subLayerDetailsToStringList( const QList< QgsProviderSublayerDetails > &layers )
|
|
{
|
|
QStringList res;
|
|
res.reserve( layers.size() );
|
|
|
|
for ( const QgsProviderSublayerDetails &layer : layers )
|
|
{
|
|
const OGRwkbGeometryType ogrGeomType = QgsOgrProviderUtils::ogrTypeFromQgisType( layer.wkbType() );
|
|
|
|
const QStringList parts { QString::number( layer.layerNumber() ),
|
|
layer.name(),
|
|
QString::number( layer.featureCount() ),
|
|
QgsOgrProviderUtils::ogrWkbGeometryTypeName( ogrGeomType ),
|
|
layer.geometryColumnName(),
|
|
layer.description() };
|
|
res << parts.join( QgsDataProvider::sublayerSeparator() );
|
|
}
|
|
return res;
|
|
}
|
|
|
|
QStringList QgsOgrProvider::subLayers() const
|
|
{
|
|
const bool withFeatureCount = ( mReadFlags & Qgis::DataProviderReadFlag::SkipFeatureCount ) == 0;
|
|
|
|
Qgis::SublayerQueryFlags flags = withFeatureCount
|
|
? ( Qgis::SublayerQueryFlag::CountFeatures | Qgis::SublayerQueryFlag::ResolveGeometryType )
|
|
: Qgis::SublayerQueryFlag::ResolveGeometryType;
|
|
if ( mIsSubLayer )
|
|
flags |= Qgis::SublayerQueryFlag::IncludeSystemTables;
|
|
|
|
return subLayerDetailsToStringList( _subLayers( flags ) );
|
|
}
|
|
|
|
QgsLayerMetadata QgsOgrProvider::layerMetadata() const
|
|
{
|
|
return mLayerMetadata;
|
|
}
|
|
|
|
QList<QgsProviderSublayerDetails> QgsOgrProvider::_subLayers( Qgis::SublayerQueryFlags flags ) const
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !mValid )
|
|
{
|
|
return {};
|
|
}
|
|
|
|
if ( !mSubLayerList.isEmpty() )
|
|
return mSubLayerList;
|
|
|
|
const size_t totalLayerCount = layerCount();
|
|
if ( mOgrLayer && ( mIsSubLayer || totalLayerCount == 1 ) )
|
|
{
|
|
mSubLayerList << QgsOgrProviderUtils::querySubLayerList( mLayerIndex, mOgrLayer, nullptr, mGDALDriverName, flags, dataSourceUri(), totalLayerCount == 1 );
|
|
}
|
|
else
|
|
{
|
|
// In case there is no free opened dataset in the cache, keep the first
|
|
// layer alive while we iterate over the other layers, so that we can
|
|
// reuse the same dataset. Can help in a particular with a FileGDB with
|
|
// the FileGDB driver
|
|
QgsOgrLayerUniquePtr firstLayer;
|
|
for ( size_t i = 0; i < totalLayerCount ; i++ )
|
|
{
|
|
QString errCause;
|
|
QgsOgrLayerUniquePtr layer = QgsOgrProviderUtils::getLayer( mOgrOrigLayer->datasetName(),
|
|
mOgrOrigLayer->updateMode(),
|
|
mOgrOrigLayer->options(),
|
|
i,
|
|
errCause,
|
|
// do not check timestamp beyond the first
|
|
// layer
|
|
!firstLayer );
|
|
if ( !layer )
|
|
continue;
|
|
|
|
mSubLayerList << QgsOgrProviderUtils::querySubLayerList( i, layer.get(), nullptr, mGDALDriverName, flags, dataSourceUri(), totalLayerCount == 1 );
|
|
if ( !firstLayer )
|
|
{
|
|
firstLayer = std::move( layer );
|
|
}
|
|
}
|
|
}
|
|
|
|
return mSubLayerList;
|
|
}
|
|
|
|
void QgsOgrProvider::afterRollbackToSavepoint( const QString &savepointName )
|
|
{
|
|
Q_UNUSED( savepointName );
|
|
mFieldsRequireReload = true;
|
|
}
|
|
|
|
void QgsOgrProvider::afterRollback()
|
|
{
|
|
mFieldsRequireReload = true;
|
|
}
|
|
|
|
void QgsOgrProvider::setEncoding( const QString &e )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
QgsSettings settings;
|
|
|
|
// if the layer has the OLCStringsAsUTF8 capability, we CANNOT override the
|
|
// encoding on the QGIS side!
|
|
if ( mOgrLayer && !mOgrLayer->TestCapability( OLCStringsAsUTF8 ) )
|
|
{
|
|
QgsVectorDataProvider::setEncoding( e );
|
|
}
|
|
else
|
|
{
|
|
QgsVectorDataProvider::setEncoding( QStringLiteral( "UTF-8" ) );
|
|
}
|
|
loadFields();
|
|
}
|
|
|
|
// This is reused by dataItem
|
|
OGRwkbGeometryType QgsOgrProvider::getOgrGeomType( const QString &driverName, OGRLayerH ogrLayer )
|
|
{
|
|
OGRFeatureDefnH fdef = OGR_L_GetLayerDefn( ogrLayer );
|
|
OGRwkbGeometryType geomType = wkbUnknown;
|
|
if ( fdef )
|
|
{
|
|
geomType = OGR_FD_GetGeomType( fdef );
|
|
|
|
// Handle wkbUnknown and its Z/M variants. QGIS has no unknown Z/M variants,
|
|
// so just use flat wkbUnknown
|
|
if ( wkbFlatten( geomType ) == wkbUnknown )
|
|
geomType = wkbUnknown;
|
|
|
|
// Some ogr drivers (e.g. GML) are not able to determine the geometry type of a layer like this.
|
|
// In such cases, we use virtual sublayers for each geometry if the layer contains
|
|
// multiple geometries (see subLayers) otherwise we guess geometry type from the first
|
|
// feature that has a geometry (limit us to a few features, not the whole layer)
|
|
//
|
|
// For ESRI formats with a GeometryCollection25D type we also query features for the geometry type,
|
|
// as they may be ESRI MultiPatch files which we want to report as MultiPolygon25D types
|
|
if ( geomType == wkbUnknown
|
|
|| ( geomType == wkbGeometryCollection25D && ( driverName == QLatin1String( "ESRI Shapefile" ) || driverName == QLatin1String( "OpenFileGDB" ) || driverName == QLatin1String( "FileGDB" ) ) ) )
|
|
{
|
|
geomType = wkbNone;
|
|
OGR_L_ResetReading( ogrLayer );
|
|
for ( int i = 0; i < 10; i++ )
|
|
{
|
|
gdal::ogr_feature_unique_ptr nextFeature( OGR_L_GetNextFeature( ogrLayer ) );
|
|
if ( !nextFeature )
|
|
break;
|
|
|
|
geomType = QgsOgrProviderUtils::resolveGeometryTypeForFeature( nextFeature.get(), driverName );
|
|
if ( geomType != wkbNone )
|
|
break;
|
|
}
|
|
OGR_L_ResetReading( ogrLayer );
|
|
}
|
|
}
|
|
return geomType;
|
|
}
|
|
|
|
void QgsOgrProvider::loadFields()
|
|
{
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
//the attribute fields need to be read again when the encoding changes
|
|
mAttributeFields.clear();
|
|
mDefaultValues.clear();
|
|
mPrimaryKeyAttrs.clear();
|
|
if ( !mOgrLayer )
|
|
return;
|
|
|
|
if ( mOgrGeometryTypeFilter != wkbUnknown )
|
|
{
|
|
mOGRGeomType = mOgrGeometryTypeFilter;
|
|
}
|
|
else
|
|
{
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( mutex );
|
|
QMutexLocker locker( mutex );
|
|
mOGRGeomType = getOgrGeomType( mGDALDriverName, ogrLayer );
|
|
}
|
|
QgsOgrFeatureDefn &fdef = mOgrLayer->GetLayerDefn();
|
|
|
|
// Expose the OGR FID if it comes from a "real" column (typically GPKG)
|
|
// and make sure that this FID column is not exposed as a regular OGR field (shouldn't happen normally)
|
|
QByteArray fidColumn( mOgrLayer->GetFIDColumn() );
|
|
mFirstFieldIsFid = !fidColumn.isEmpty() &&
|
|
fdef.GetFieldIndex( fidColumn ) < 0;
|
|
|
|
#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 2, 1)
|
|
// This is a temporary solution until GDAL Unique support is available
|
|
QSet<QString> uniqueFieldNames;
|
|
|
|
|
|
if ( mGDALDriverName == QLatin1String( "GPKG" ) )
|
|
{
|
|
sqlite3_database_unique_ptr dsPtr;
|
|
if ( dsPtr.open_v2( mFilePath, SQLITE_OPEN_READONLY, nullptr ) == SQLITE_OK )
|
|
{
|
|
QString errMsg;
|
|
uniqueFieldNames = QgsSqliteUtils::uniqueFields( dsPtr.get(), mOgrLayer->name(), errMsg );
|
|
if ( ! errMsg.isEmpty() )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "GPKG error searching for unique constraints on fields for table %1. (%2)" ).arg( QString( mOgrLayer->name() ), errMsg ), tr( "OGR" ) );
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
int createdFields = 0;
|
|
if ( mFirstFieldIsFid )
|
|
{
|
|
QgsField fidField(
|
|
fidColumn,
|
|
QMetaType::Type::LongLong,
|
|
QStringLiteral( "Integer64" )
|
|
);
|
|
// Set constraints for feature id
|
|
QgsFieldConstraints constraints = fidField.constraints();
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
|
|
fidField.setConstraints( constraints );
|
|
mAttributeFields.append(
|
|
fidField
|
|
);
|
|
mDefaultValues.insert( 0, tr( "Autogenerate" ) );
|
|
createdFields++;
|
|
mPrimaryKeyAttrs << 0;
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,3,0)
|
|
// needed for field domain retrieval on GDAL 3.3+
|
|
QRecursiveMutex *datasetMutex = nullptr;
|
|
GDALDatasetH ds = mOgrLayer->getDatasetHandleAndMutex( datasetMutex );
|
|
QMutexLocker locker( datasetMutex );
|
|
#endif
|
|
|
|
for ( int fieldIndex = 0; fieldIndex < fdef.GetFieldCount(); ++fieldIndex )
|
|
{
|
|
OGRFieldDefnH fldDef = fdef.GetFieldDefn( fieldIndex );
|
|
const OGRFieldType ogrType = OGR_Fld_GetType( fldDef );
|
|
const OGRFieldSubType ogrSubType = OGR_Fld_GetSubType( fldDef );
|
|
|
|
QMetaType::Type varType = QMetaType::Type::UnknownType;
|
|
QMetaType::Type varSubType = QMetaType::Type::UnknownType;
|
|
QgsOgrUtils::ogrFieldTypeToQVariantType( ogrType, ogrSubType, varType, varSubType );
|
|
|
|
// Handle special case for OGRFieldType::OFSTJSON which is not necessarily a map.
|
|
// If subtype is JSON try to load a feature and check if it's
|
|
// really an object (rather than something else like an array)
|
|
// fallback to string.
|
|
if ( ( ogrType == OFTString || ogrType == OFTWideString ) && ogrSubType == OFSTJSON )
|
|
{
|
|
QRecursiveMutex *layerMutex = nullptr;
|
|
OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( layerMutex );
|
|
QMutexLocker layerLocker( layerMutex );
|
|
gdal::ogr_feature_unique_ptr f( OGR_L_GetNextFeature( ogrLayer ) );
|
|
if ( f )
|
|
{
|
|
const char *json = OGR_F_GetFieldAsString( f.get(), fieldIndex );
|
|
if ( json && json[0] != '\0' )
|
|
{
|
|
try
|
|
{
|
|
const nlohmann::json json_element = json::parse( json );
|
|
// Check if it's an homogeneous array of numbers or strings
|
|
if ( json_element.is_array() )
|
|
{
|
|
// Check whether the values are all of the same type
|
|
bool allNumbers = true;
|
|
bool allIntegers = true;
|
|
bool allStrings = true;
|
|
for ( auto &value : json_element )
|
|
{
|
|
if ( allStrings && !value.is_string() )
|
|
{
|
|
allStrings = false;
|
|
}
|
|
if ( allNumbers && !value.is_number() )
|
|
{
|
|
allNumbers = false;
|
|
}
|
|
if ( allIntegers && !value.is_number_integer() )
|
|
{
|
|
allIntegers = false;
|
|
}
|
|
}
|
|
if ( allNumbers )
|
|
{
|
|
if ( allIntegers )
|
|
{
|
|
varType = QMetaType::Type::QVariantList;
|
|
varSubType = QMetaType::Type::LongLong;
|
|
}
|
|
else
|
|
{
|
|
varType = QMetaType::Type::QVariantList;
|
|
varSubType = QMetaType::Type::Double;
|
|
}
|
|
}
|
|
else if ( allStrings )
|
|
{
|
|
varType = QMetaType::Type::QStringList;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "JSON array contains mixed types, falling back to string" ), 2 );
|
|
varType = QMetaType::Type::QString;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
}
|
|
else if ( ! json_element.is_object() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "JSON is neither an array nor an object, falling back to string" ), 2 );
|
|
varType = QMetaType::Type::QString;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
else if ( json_element.is_number() )
|
|
{
|
|
if ( json_element.is_number_float() )
|
|
{
|
|
varType = QMetaType::Type::Double;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
else
|
|
{
|
|
varType = QMetaType::Type::LongLong;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
}
|
|
else if ( json_element.is_string() )
|
|
{
|
|
varType = QMetaType::Type::QString;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, falling back to string" ), 2 );
|
|
}
|
|
}
|
|
catch ( const json::parse_error & )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, falling back to string" ), 2 );
|
|
varType = QMetaType::Type::QString;
|
|
varSubType = QMetaType::Type::UnknownType;
|
|
}
|
|
}
|
|
OGR_L_ResetReading( ogrLayer );
|
|
}
|
|
}
|
|
|
|
//TODO: fix this hack
|
|
#ifdef ANDROID
|
|
QString name = OGR_Fld_GetNameRef( fldDef );
|
|
#else
|
|
QString name = textEncoding()->toUnicode( OGR_Fld_GetNameRef( fldDef ) );
|
|
#endif
|
|
|
|
if ( mAttributeFields.indexFromName( name ) != -1 )
|
|
{
|
|
|
|
QString tmpname = name + "_%1";
|
|
int fix = 0;
|
|
|
|
while ( mAttributeFields.indexFromName( name ) != -1 )
|
|
{
|
|
name = tmpname.arg( ++fix );
|
|
}
|
|
}
|
|
|
|
int width = OGR_Fld_GetWidth( fldDef );
|
|
int prec = OGR_Fld_GetPrecision( fldDef );
|
|
if ( prec > 0 )
|
|
width -= 1;
|
|
|
|
QString typeName = OGR_GetFieldTypeName( ogrType );
|
|
if ( ogrSubType != OFSTNone )
|
|
typeName = OGR_GetFieldSubTypeName( ogrSubType );
|
|
|
|
QgsField newField = QgsField(
|
|
name,
|
|
varType,
|
|
#ifdef ANDROID
|
|
typeName,
|
|
#else
|
|
textEncoding()->toUnicode( typeName.toStdString().c_str() ),
|
|
#endif
|
|
width, prec, QString(), varSubType
|
|
);
|
|
|
|
const QString alias = textEncoding()->toUnicode( OGR_Fld_GetAlternativeNameRef( fldDef ) );
|
|
if ( !alias.isEmpty() )
|
|
{
|
|
newField.setAlias( alias );
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
{
|
|
const QString comment = textEncoding()->toUnicode( OGR_Fld_GetComment( fldDef ) );
|
|
if ( !comment.isEmpty() )
|
|
{
|
|
newField.setComment( comment );
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// check if field is nullable
|
|
bool nullable = OGR_Fld_IsNullable( fldDef );
|
|
if ( !nullable )
|
|
{
|
|
QgsFieldConstraints constraints;
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
|
|
newField.setConstraints( constraints );
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 2, 1)
|
|
if ( uniqueFieldNames.contains( OGR_Fld_GetNameRef( fldDef ) ) )
|
|
#else
|
|
if ( OGR_Fld_IsUnique( fldDef ) )
|
|
#endif
|
|
{
|
|
QgsFieldConstraints constraints = newField.constraints();
|
|
constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
|
|
newField.setConstraints( constraints );
|
|
}
|
|
|
|
// check if field has default value
|
|
QString defaultValue = textEncoding()->toUnicode( OGR_Fld_GetDefault( fldDef ) );
|
|
if ( defaultValue == QLatin1String( "FILEGEODATABASE_SHAPE_LENGTH" ) ||
|
|
defaultValue == QLatin1String( "FILEGEODATABASE_SHAPE_AREA" ) )
|
|
{
|
|
// FileGeodatabase may have special fields with autocomputed geometry
|
|
// length and area. Their content is generated by the driver. The
|
|
// user must not fill it (if they do, it will be overridden by the
|
|
// driver)
|
|
newField.setReadOnly( true );
|
|
}
|
|
else if ( !defaultValue.isEmpty() && !OGR_Fld_IsDefaultDriverSpecific( fldDef ) )
|
|
{
|
|
if ( defaultValue.startsWith( '\'' ) )
|
|
{
|
|
defaultValue = defaultValue.remove( 0, 1 );
|
|
defaultValue.chop( 1 );
|
|
defaultValue.replace( QLatin1String( "''" ), QLatin1String( "'" ) );
|
|
}
|
|
mDefaultValues.insert( createdFields, defaultValue );
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,3,0)
|
|
if ( const char *domainName = OGR_Fld_GetDomainName( fldDef ) )
|
|
{
|
|
QgsFieldConstraints constraints = newField.constraints();
|
|
constraints.setDomainName( domainName );
|
|
newField.setConstraints( constraints );
|
|
|
|
// dataset retains ownership of domain!
|
|
if ( OGRFieldDomainH domain = GDALDatasetGetFieldDomain( ds, domainName ) )
|
|
{
|
|
switch ( OGR_FldDomain_GetSplitPolicy( domain ) )
|
|
{
|
|
case OFDSP_DEFAULT_VALUE:
|
|
newField.setSplitPolicy( Qgis::FieldDomainSplitPolicy::DefaultValue );
|
|
break;
|
|
case OFDSP_DUPLICATE:
|
|
newField.setSplitPolicy( Qgis::FieldDomainSplitPolicy::Duplicate );
|
|
break;
|
|
case OFDSP_GEOMETRY_RATIO:
|
|
newField.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio );
|
|
break;
|
|
}
|
|
|
|
switch ( OGR_FldDomain_GetDomainType( domain ) )
|
|
{
|
|
case OFDT_CODED:
|
|
{
|
|
QVariantList valueConfig;
|
|
const OGRCodedValue *codedValue = OGR_CodedFldDomain_GetEnumeration( domain );
|
|
while ( codedValue && codedValue->pszCode )
|
|
{
|
|
const QString code( codedValue->pszCode );
|
|
// if pszValue is null then it indicates we are working with a set of acceptable values which aren't
|
|
// coded. In this case we copy the code as the value so that QGIS exposes the domain as a choice of
|
|
// the valid code values.
|
|
const QString value( codedValue->pszValue ? codedValue->pszValue : codedValue->pszCode );
|
|
|
|
QVariantMap config;
|
|
config[ value ] = code;
|
|
valueConfig.append( config );
|
|
|
|
codedValue++;
|
|
}
|
|
|
|
QVariantMap editorConfig;
|
|
editorConfig.insert( QStringLiteral( "map" ), valueConfig );
|
|
newField.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "ValueMap" ), editorConfig ) );
|
|
break;
|
|
}
|
|
|
|
case OFDT_RANGE:
|
|
if ( newField.isNumeric() )
|
|
{
|
|
// QGIS doesn't support the inclusive option yet!
|
|
bool isInclusive = false;
|
|
|
|
QVariantMap editorConfig;
|
|
editorConfig.insert( QStringLiteral( "Step" ), 1 );
|
|
editorConfig.insert( QStringLiteral( "Style" ), QStringLiteral( "SpinBox" ) );
|
|
editorConfig.insert( QStringLiteral( "AllowNull" ), nullable );
|
|
editorConfig.insert( QStringLiteral( "Precision" ), newField.precision() );
|
|
|
|
OGRFieldType domainFieldType = OGR_FldDomain_GetFieldType( domain );
|
|
bool hasMinOrMax = false;
|
|
if ( const OGRField *min = OGR_RangeFldDomain_GetMin( domain, &isInclusive ) )
|
|
{
|
|
const QVariant minValue = QgsOgrUtils::OGRFieldtoVariant( min, domainFieldType );
|
|
if ( minValue.isValid() )
|
|
{
|
|
editorConfig.insert( QStringLiteral( "Min" ), minValue );
|
|
hasMinOrMax = true;
|
|
}
|
|
}
|
|
if ( const OGRField *max = OGR_RangeFldDomain_GetMax( domain, &isInclusive ) )
|
|
{
|
|
const QVariant maxValue = QgsOgrUtils::OGRFieldtoVariant( max, domainFieldType );
|
|
if ( maxValue.isValid() )
|
|
{
|
|
editorConfig.insert( QStringLiteral( "Max" ), maxValue );
|
|
hasMinOrMax = true;
|
|
}
|
|
}
|
|
|
|
if ( hasMinOrMax )
|
|
newField.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "Range" ), editorConfig ) );
|
|
}
|
|
// GDAL also supports range domains for fields types like date/datetimes, but the QGIS corresponding field
|
|
// config doesn't support this yet!
|
|
break;
|
|
|
|
case OFDT_GLOB:
|
|
// not supported by QGIS yet
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
mAttributeFields.append( newField );
|
|
createdFields++;
|
|
}
|
|
mFieldsRequireReload = false;
|
|
}
|
|
|
|
void QgsOgrProvider::loadMetadata()
|
|
{
|
|
// Set default, may be overridden by stored metadata
|
|
mLayerMetadata.setCrs( crs() );
|
|
|
|
if ( mOgrOrigLayer )
|
|
{
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex );
|
|
QMutexLocker locker( mutex );
|
|
|
|
const QString identifier = GDALGetMetadataItem( layer, "IDENTIFIER", "" );
|
|
if ( !identifier.isEmpty() )
|
|
mLayerMetadata.setTitle( identifier ); // see geopackage specs -- "'identifier' is analogous to 'title'"
|
|
const QString abstract = GDALGetMetadataItem( layer, "DESCRIPTION", "" );
|
|
if ( !abstract.isEmpty() )
|
|
mLayerMetadata.setAbstract( abstract );
|
|
|
|
if ( mGDALDriverName == QLatin1String( "GPKG" ) )
|
|
{
|
|
// first check if metadata tables/extension exists
|
|
QString sql = QStringLiteral( "SELECT name FROM sqlite_master WHERE name='gpkg_metadata' AND type='table'" );
|
|
bool metadataTableExists = false;
|
|
if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toLocal8Bit().constData() ) )
|
|
{
|
|
gdal::ogr_feature_unique_ptr f( l->GetNextFeature() );
|
|
if ( f )
|
|
{
|
|
metadataTableExists = true;
|
|
}
|
|
}
|
|
|
|
if ( metadataTableExists )
|
|
{
|
|
// read Geopackage layer metadata - scan gpkg_metadata table for QGIS metadata
|
|
sql = QStringLiteral( "SELECT metadata from gpkg_metadata LEFT JOIN gpkg_metadata_reference ON "
|
|
"(gpkg_metadata_reference.table_name = %1 AND gpkg_metadata.id = gpkg_metadata_reference.md_file_id) "
|
|
"WHERE md_standard_uri = %2 and reference_scope = %3" ).arg(
|
|
QgsSqliteUtils::quotedString( mOgrOrigLayer->name() ),
|
|
QgsSqliteUtils::quotedString( QStringLiteral( "http://mrcc.com/qgis.dtd" ) ),
|
|
QgsSqliteUtils::quotedString( QStringLiteral( "table" ) ) );
|
|
|
|
if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toUtf8().constData() ) )
|
|
{
|
|
gdal::ogr_feature_unique_ptr f( l->GetNextFeature() );
|
|
if ( f )
|
|
{
|
|
bool ok = false;
|
|
QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), QgsField( QString(), QMetaType::Type::QString ), 0, nullptr, &ok );
|
|
if ( ok )
|
|
{
|
|
QDomDocument doc;
|
|
doc.setContent( res.toString() );
|
|
mLayerMetadata.readMetadataXml( doc.documentElement() );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( mGDALDriverName == QLatin1String( "FileGDB" )
|
|
|| mGDALDriverName == QLatin1String( "OpenFileGDB" )
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,0)
|
|
|| mGDALDriverName == QLatin1String( "PGeo" ) // supported on GDAL 3.4+ only
|
|
#endif
|
|
)
|
|
{
|
|
// read ESRI FileGeodatabase/Personal Geodatabase layer metadata
|
|
|
|
// important -- this ONLY works if the layer name is NOT quoted!!
|
|
QByteArray sql = "GetLayerMetadata " + mOgrOrigLayer->name();
|
|
if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql ) )
|
|
{
|
|
gdal::ogr_feature_unique_ptr f( l->GetNextFeature() );
|
|
if ( f )
|
|
{
|
|
bool ok = false;
|
|
QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), QgsField( QString(), QMetaType::Type::QString ), 0, textEncoding(), &ok );
|
|
if ( ok )
|
|
{
|
|
QDomDocument metadataDoc;
|
|
metadataDoc.setContent( res.toString() );
|
|
mLayerMetadata = QgsMetadataUtils::convertFromEsri( metadataDoc );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if ( ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) )
|
|
{
|
|
// look for .shp.xml sidecar file
|
|
const QString sidecarPath = mFilePath + ".xml";
|
|
if ( QFileInfo::exists( sidecarPath ) )
|
|
{
|
|
QFile file( sidecarPath );
|
|
if ( file.open( QFile::ReadOnly ) )
|
|
{
|
|
QDomDocument doc;
|
|
int line, column;
|
|
QString errorMessage;
|
|
if ( doc.setContent( &file, &errorMessage, &line, &column ) )
|
|
{
|
|
mLayerMetadata = QgsMetadataUtils::convertFromEsri( doc );
|
|
}
|
|
else
|
|
{
|
|
QgsDebugError( QStringLiteral( "Error reading %1: %2 at line %3 column %4" ).arg( sidecarPath, errorMessage ).arg( line ).arg( column ) );
|
|
}
|
|
file.close();
|
|
}
|
|
else
|
|
{
|
|
QgsDebugError( QStringLiteral( "Error reading %1 - could not open file for read" ).arg( sidecarPath ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
mLayerMetadata.setType( QStringLiteral( "dataset" ) );
|
|
}
|
|
|
|
QString QgsOgrProvider::storageType() const
|
|
{
|
|
// Delegate to the driver loaded in by OGR
|
|
return mGDALDriverName;
|
|
}
|
|
|
|
|
|
void QgsOgrProvider::setRelevantFields( bool fetchGeometry, const QgsAttributeList &fetchAttributes ) const
|
|
{
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( mutex );
|
|
QMutexLocker locker( mutex );
|
|
QgsOgrProviderUtils::setRelevantFields( ogrLayer, fields().count(), fetchGeometry, fetchAttributes, mFirstFieldIsFid, QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) );
|
|
}
|
|
|
|
QgsFeatureIterator QgsOgrProvider::getFeatures( const QgsFeatureRequest &request ) const
|
|
{
|
|
return QgsFeatureIterator( new QgsOgrFeatureIterator( static_cast<QgsOgrFeatureSource *>( featureSource() ), true, request, mTransaction ) );
|
|
}
|
|
|
|
unsigned char *QgsOgrProvider::getGeometryPointer( OGRFeatureH fet )
|
|
{
|
|
OGRGeometryH geom = OGR_F_GetGeometryRef( fet );
|
|
unsigned char *gPtr = nullptr;
|
|
|
|
if ( !geom )
|
|
return nullptr;
|
|
|
|
// get the wkb representation
|
|
gPtr = new unsigned char[OGR_G_WkbSize( geom )];
|
|
|
|
OGR_G_ExportToWkb( geom, ( OGRwkbByteOrder ) QgsApplication::endian(), gPtr );
|
|
return gPtr;
|
|
}
|
|
|
|
|
|
QgsRectangle QgsOgrProvider::extent() const
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "QgsOgrProvider::extent()" ), 3 );
|
|
if ( !mExtent2D && !mExtent3D )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
mExtent2D.reset( new OGREnvelope() );
|
|
|
|
// get the extent_ (envelope) of the layer
|
|
QgsDebugMsgLevel( QStringLiteral( "Starting computing extent. subset: '%1'" ).arg( mSubsetString ), 3 );
|
|
|
|
if ( mForceRecomputeExtent && mValid && mWriteAccess &&
|
|
( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) &&
|
|
mOgrOrigLayer )
|
|
{
|
|
// works with unquoted layerName
|
|
QByteArray sql = QByteArray( "RECOMPUTE EXTENT ON " ) + mOgrOrigLayer->name();
|
|
QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 );
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( sql );
|
|
}
|
|
|
|
mExtent2D->MinX = std::numeric_limits<double>::max();
|
|
mExtent2D->MinY = std::numeric_limits<double>::max();
|
|
mExtent2D->MaxX = -std::numeric_limits<double>::max();
|
|
mExtent2D->MaxY = -std::numeric_limits<double>::max();
|
|
|
|
// TODO: This can be expensive, do we really need it!
|
|
if ( mOgrLayer == mOgrOrigLayer.get() && mSubsetString.isEmpty() )
|
|
{
|
|
if ( ( mGDALDriverName == QLatin1String( "OAPIF" ) || mGDALDriverName == QLatin1String( "WFS3" ) ) &&
|
|
!mOgrLayer->TestCapability( OLCFastGetExtent ) )
|
|
{
|
|
// When the extent is not in the metadata, retrieving it would be
|
|
// super slow
|
|
mExtent2D->MinX = -180;
|
|
mExtent2D->MinY = -90;
|
|
mExtent2D->MaxX = 180;
|
|
mExtent2D->MaxY = 90;
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Will call mOgrLayer->GetExtent" ), 3 );
|
|
OGRErr err = mOgrLayer->GetExtent( mExtent2D.get(), true );
|
|
if ( err != OGRERR_NONE )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute extent2D (ogr error: %1)" ).arg( err ), 1 );
|
|
mExtent2D.reset();
|
|
return QgsRectangle();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "will apply slow default 2D extent computing" ), 3 );
|
|
OGREnvelope3D sExtent3D;
|
|
OGRErr err = mOgrLayer->computeExtent3DSlowly( &sExtent3D );
|
|
if ( err == OGRERR_NONE )
|
|
{
|
|
mExtent2D->MinX = sExtent3D.MinX;
|
|
mExtent2D->MinY = sExtent3D.MinY;
|
|
mExtent2D->MaxX = sExtent3D.MaxX;
|
|
mExtent2D->MaxY = sExtent3D.MaxY;
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute slow extent2D (ogr error: %1)" ).arg( err ), 1 );
|
|
mExtent2D.reset();
|
|
return QgsRectangle();
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( mExtent2D )
|
|
{
|
|
mExtentRect = QgsBox3D( mExtent2D->MinX, mExtent2D->MinY, std::numeric_limits<double>::quiet_NaN(),
|
|
mExtent2D->MaxX, mExtent2D->MaxY, std::numeric_limits<double>::quiet_NaN() );
|
|
QgsDebugMsgLevel( QStringLiteral( "Finished get extent from 2D: (%1, %2, : %3, %4)" )
|
|
.arg( mExtentRect.xMinimum() ).arg( mExtentRect.yMinimum() )
|
|
.arg( mExtentRect.xMaximum() ).arg( mExtentRect.yMaximum() ), 3 );
|
|
}
|
|
else
|
|
{
|
|
mExtentRect = QgsBox3D( mExtent3D->MinX, mExtent3D->MinY, mExtent3D->MinZ, mExtent3D->MaxX, mExtent3D->MaxY, mExtent3D->MaxZ );
|
|
QgsDebugMsgLevel( QStringLiteral( "Finished get extent from 3D: (%1, %2, %3 : %4, %5, %6)" )
|
|
.arg( mExtentRect.xMinimum() ).arg( mExtentRect.yMinimum() ).arg( mExtentRect.zMinimum() )
|
|
.arg( mExtentRect.xMaximum() ).arg( mExtentRect.yMaximum() ).arg( mExtentRect.zMaximum() ), 3 );
|
|
}
|
|
|
|
return mExtentRect.toRectangle();
|
|
}
|
|
|
|
QgsBox3D QgsOgrProvider::extent3D() const
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "QgsOgrProvider::extent3D() starting" ), 3 );
|
|
if ( !elevationProperties()->containsElevationData() ) // ie. 2D
|
|
{
|
|
extent();
|
|
mExtentRect = QgsBox3D( mExtent2D->MinX, mExtent2D->MinY, std::numeric_limits<double>::quiet_NaN(),
|
|
mExtent2D->MaxX, mExtent2D->MaxY, std::numeric_limits<double>::quiet_NaN() );
|
|
return mExtentRect;
|
|
}
|
|
|
|
if ( !mExtent3D )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
mExtent3D.reset( new OGREnvelope3D() );
|
|
|
|
// get the extent_ (envelope) of the layer
|
|
QgsDebugMsgLevel( QStringLiteral( "Starting computing extent3D. subset: '%1'" ).arg( mSubsetString ), 3 );
|
|
|
|
if ( mOgrLayer == mOgrOrigLayer.get() )
|
|
{
|
|
bool hasBeenComputed = false;
|
|
|
|
mExtent3D->MinX = std::numeric_limits<double>::max();
|
|
mExtent3D->MinY = std::numeric_limits<double>::max();
|
|
mExtent3D->MinZ = std::numeric_limits<double>::max();
|
|
mExtent3D->MaxX = -std::numeric_limits<double>::max();
|
|
mExtent3D->MaxY = -std::numeric_limits<double>::max();
|
|
mExtent3D->MaxZ = -std::numeric_limits<double>::max();
|
|
|
|
if ( mForceRecomputeExtent && mValid && mWriteAccess &&
|
|
( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) &&
|
|
mOgrOrigLayer )
|
|
{
|
|
// works with unquoted layerName
|
|
QByteArray sql = QByteArray( "RECOMPUTE EXTENT ON " ) + mOgrOrigLayer->name();
|
|
QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 );
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( sql );
|
|
}
|
|
else
|
|
{
|
|
OGREnvelope envelope2D;
|
|
if ( mOgrLayer->GetExtent( &envelope2D, true ) == OGRERR_NONE ) // switch OLCFastGetExtent flag to TRUE
|
|
{
|
|
mExtent3D->MinX = envelope2D.MinX;
|
|
mExtent3D->MinY = envelope2D.MinY;
|
|
mExtent3D->MaxX = envelope2D.MaxX;
|
|
mExtent3D->MaxY = envelope2D.MaxY;
|
|
}
|
|
}
|
|
|
|
if ( !mOgrLayer->TestCapability( OLCFastGetExtent ) )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "WITHOUT OLCFastGetExtent" ), 3 );
|
|
if ( mGDALDriverName == QLatin1String( "OAPIF" ) || mGDALDriverName == QLatin1String( "WFS3" ) )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "WITHOUT OLCFastGetExtent AND is remote" ), 3 );
|
|
// When the extent is not in the metadata, retrieving it would be
|
|
// super slow for remote data sources
|
|
mExtent3D->MinX = -180;
|
|
mExtent3D->MinY = -90;
|
|
mExtent3D->MinZ = std::numeric_limits<double>::quiet_NaN();
|
|
mExtent3D->MaxX = 180;
|
|
mExtent3D->MaxY = 90;
|
|
mExtent3D->MaxZ = std::numeric_limits<double>::quiet_NaN();
|
|
hasBeenComputed = true;
|
|
}
|
|
// else slow computation
|
|
}
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,0)
|
|
else // 3D (with or without subset) we delegate to gdal
|
|
hasBeenComputed = mOgrLayer->GetExtent3D( mExtent3D.get(), true ) == OGRERR_NONE;
|
|
// else only 2D with subset need slow computation
|
|
#else
|
|
else if ( mSubsetString.isEmpty() )
|
|
// mSubsetString is not properly handled by mOgrLayer->GetExtent3D if gdal<3.9.0
|
|
// we need to scan all feature (via QgsOgrLayer::computeExtent3DSlowly) to take ino account the mSubsetString filter
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "WITH OLCFastGetExtent" ), 3 );
|
|
hasBeenComputed = mOgrLayer->GetExtent3D( mExtent3D.get(), true ) == OGRERR_NONE;
|
|
}
|
|
#endif
|
|
// else slow computation
|
|
|
|
if ( !hasBeenComputed )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "will apply slow default 3D extent computing" ), 3 );
|
|
OGRErr err = mOgrLayer->computeExtent3DSlowly( mExtent3D.get() );
|
|
if ( err != OGRERR_NONE )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute slow extent3D (ogr error: %1)" ).arg( err ), 1 );
|
|
mExtent3D.reset();
|
|
return QgsBox3D();
|
|
}
|
|
}
|
|
|
|
if ( mExtent3D->MinZ == std::numeric_limits<double>::max() && mExtent3D->MaxZ == -std::numeric_limits<double>::max() )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "is 2D/flat extent3D" ), 3 );
|
|
// flat extent
|
|
mExtent3D->MinZ = std::numeric_limits<double>::quiet_NaN();
|
|
mExtent3D->MaxZ = std::numeric_limits<double>::quiet_NaN();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute extent3D (mOgrLayer != mOgrOrigLayer)" ), 1 );
|
|
mExtent3D.reset();
|
|
return QgsBox3D();
|
|
}
|
|
} // ends if !mExtent3D
|
|
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "Finished get extent3D: (%1, %2, %3 : %4, %5, %6)" )
|
|
.arg( mExtent3D->MinX ).arg( mExtent3D->MinY ).arg( mExtent3D->MinZ )
|
|
.arg( mExtent3D->MaxX ).arg( mExtent3D->MaxY ).arg( mExtent3D->MaxZ ), 3 );
|
|
|
|
mExtentRect = QgsBox3D( mExtent3D->MinX, mExtent3D->MinY, mExtent3D->MinZ, mExtent3D->MaxX, mExtent3D->MaxY, mExtent3D->MaxZ );
|
|
return mExtentRect;
|
|
}
|
|
|
|
QVariant QgsOgrProvider::defaultValue( int fieldId ) const
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( fieldId < 0 || fieldId >= fields().count() )
|
|
return QVariant();
|
|
|
|
QString defaultVal = mDefaultValues.value( fieldId, QString() );
|
|
if ( defaultVal.isEmpty() )
|
|
return QVariant();
|
|
|
|
QVariant resultVar = defaultVal;
|
|
if ( defaultVal == QLatin1String( "CURRENT_TIMESTAMP" ) )
|
|
resultVar = QDateTime::currentDateTime();
|
|
else if ( defaultVal == QLatin1String( "CURRENT_DATE" ) )
|
|
resultVar = QDate::currentDate();
|
|
else if ( defaultVal == QLatin1String( "CURRENT_TIME" ) )
|
|
resultVar = QTime::currentTime();
|
|
|
|
// Get next sequence value for sqlite in case we are inside a transaction
|
|
if ( mOgrOrigLayer &&
|
|
mTransaction &&
|
|
mDefaultValues.value( fieldId, QString() ) == tr( "Autogenerate" ) &&
|
|
providerProperty( EvaluateDefaultValues, false ).toBool() &&
|
|
( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "SQLite" ) ) &&
|
|
mFirstFieldIsFid &&
|
|
fieldId == 0 )
|
|
{
|
|
QgsOgrLayerUniquePtr resultLayer = mOgrOrigLayer->ExecuteSQL( QByteArray( "SELECT seq FROM sqlite_sequence WHERE name = " ) + QgsSqliteUtils::quotedValue( mOgrOrigLayer->name() ).toUtf8() );
|
|
if ( resultLayer )
|
|
{
|
|
gdal::ogr_feature_unique_ptr f;
|
|
if ( f.reset( resultLayer->GetNextFeature() ), f )
|
|
{
|
|
bool ok { true };
|
|
const QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(),
|
|
fields().at( 0 ),
|
|
0, textEncoding(), &ok );
|
|
if ( ok )
|
|
{
|
|
long long nextVal { res.toLongLong( &ok ) };
|
|
if ( ok )
|
|
{
|
|
// Increment
|
|
resultVar = ++nextVal;
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( QByteArray( "UPDATE sqlite_sequence SET seq = seq + 1 WHERE name = " ) + QgsSqliteUtils::quotedValue( mOgrOrigLayer->name() ).toUtf8() );
|
|
}
|
|
}
|
|
|
|
if ( ! ok )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error retrieving next sequence value for %1" ).arg( QString::fromUtf8( mOgrOrigLayer->name() ) ), tr( "OGR" ) );
|
|
}
|
|
}
|
|
else // no sequence!
|
|
{
|
|
resultVar = 1;
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( QByteArray( "INSERT INTO sqlite_sequence (name, seq) VALUES( " +
|
|
QgsSqliteUtils::quotedValue( mOgrOrigLayer->name() ).toUtf8() ) + ", 1)" );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Error retrieving default value for %1" ).arg( mLayerName ), tr( "OGR" ) );
|
|
}
|
|
}
|
|
|
|
const bool compatible = fields().at( fieldId ).convertCompatible( resultVar );
|
|
return compatible && !QgsVariantUtils::isNull( resultVar ) ? resultVar : QVariant();
|
|
}
|
|
|
|
QString QgsOgrProvider::defaultValueClause( int fieldIndex ) const
|
|
{
|
|
// Return empty clause to force defaultValue calls for sqlite in case we are inside a transaction
|
|
if ( mTransaction &&
|
|
mDefaultValues.value( fieldIndex, QString() ) == tr( "Autogenerate" ) &&
|
|
providerProperty( EvaluateDefaultValues, false ).toBool() &&
|
|
( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "SQLite" ) ) &&
|
|
mFirstFieldIsFid &&
|
|
fieldIndex == 0 )
|
|
return QString();
|
|
else
|
|
return mDefaultValues.value( fieldIndex, QString() );
|
|
}
|
|
|
|
bool QgsOgrProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint constraint, const QVariant &value ) const
|
|
{
|
|
Q_UNUSED( constraint )
|
|
if ( providerProperty( EvaluateDefaultValues, false ).toBool() )
|
|
{
|
|
return ! mDefaultValues.value( fieldIndex ).isEmpty();
|
|
}
|
|
else
|
|
{
|
|
// stricter check
|
|
return mDefaultValues.contains( fieldIndex ) && !QgsVariantUtils::isNull( value ) && (
|
|
mDefaultValues.value( fieldIndex ) == value.toString()
|
|
|| value.userType() == qMetaTypeId<QgsUnsetAttributeValue>() );
|
|
}
|
|
}
|
|
|
|
void QgsOgrProvider::updateExtents()
|
|
{
|
|
invalidateCachedExtent( true );
|
|
}
|
|
|
|
void QgsOgrProvider::invalidateCachedExtent( bool bForceRecomputeExtent )
|
|
{
|
|
mForceRecomputeExtent = bForceRecomputeExtent;
|
|
mExtent2D.reset();
|
|
mExtent3D.reset();
|
|
}
|
|
|
|
size_t QgsOgrProvider::layerCount() const
|
|
{
|
|
if ( !mValid )
|
|
return 0;
|
|
return mOgrLayer->GetLayerCount();
|
|
}
|
|
|
|
/**
|
|
* Returns the feature type
|
|
*/
|
|
Qgis::WkbType QgsOgrProvider::wkbType() const
|
|
{
|
|
Qgis::WkbType wkb = QgsOgrUtils::ogrGeometryTypeToQgsWkbType( mOGRGeomType );
|
|
const Qgis::WkbType wkbFlat = QgsWkbTypes::flatType( wkb );
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) && ( wkbFlat == Qgis::WkbType::LineString || wkbFlat == Qgis::WkbType::Polygon ) )
|
|
{
|
|
wkb = QgsWkbTypes::multiType( wkb );
|
|
}
|
|
return wkb;
|
|
}
|
|
|
|
/**
|
|
* Returns the feature count
|
|
*/
|
|
long long QgsOgrProvider::featureCount() const
|
|
{
|
|
if ( ( mReadFlags & Qgis::DataProviderReadFlag::SkipFeatureCount ) != 0 )
|
|
{
|
|
return static_cast< long long >( Qgis::FeatureCountState::UnknownCount );
|
|
}
|
|
if ( mRefreshFeatureCount )
|
|
{
|
|
mRefreshFeatureCount = false;
|
|
recalculateFeatureCount();
|
|
}
|
|
return mFeaturesCounted;
|
|
}
|
|
|
|
|
|
QgsFields QgsOgrProvider::fields() const
|
|
{
|
|
if ( mFieldsRequireReload )
|
|
{
|
|
const_cast<QgsOgrProvider *>( this )->loadFields();
|
|
}
|
|
return mAttributeFields;
|
|
}
|
|
|
|
|
|
//TODO - add sanity check for shape file layers, to include checking to
|
|
// see if the .shp, .dbf, .shx files are all present and the layer
|
|
// actually has features
|
|
bool QgsOgrProvider::isValid() const
|
|
{
|
|
return mValid;
|
|
}
|
|
|
|
// Drivers may be more tolerant than we really wish (e.g. GeoPackage driver
|
|
// may accept any geometry type)
|
|
OGRGeometryH QgsOgrProvider::ConvertGeometryIfNecessary( OGRGeometryH hGeom )
|
|
{
|
|
if ( !hGeom )
|
|
return hGeom;
|
|
OGRwkbGeometryType layerGeomType = mOgrLayer->GetLayerDefn().GetGeomType();
|
|
OGRwkbGeometryType flattenLayerGeomType = wkbFlatten( layerGeomType );
|
|
OGRwkbGeometryType geomType = OGR_G_GetGeometryType( hGeom );
|
|
OGRwkbGeometryType flattenGeomType = wkbFlatten( geomType );
|
|
|
|
if ( flattenLayerGeomType == wkbUnknown || flattenLayerGeomType == flattenGeomType )
|
|
{
|
|
return hGeom;
|
|
}
|
|
if ( flattenLayerGeomType == wkbMultiPolygon && flattenGeomType == wkbPolygon )
|
|
{
|
|
return OGR_G_ForceToMultiPolygon( hGeom );
|
|
}
|
|
if ( flattenLayerGeomType == wkbMultiLineString && flattenGeomType == wkbLineString )
|
|
{
|
|
return OGR_G_ForceToMultiLineString( hGeom );
|
|
}
|
|
|
|
if ( flattenLayerGeomType == wkbPolygon && flattenGeomType == wkbMultiPolygon &&
|
|
mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
// Do not force multipolygon to polygon for shapefiles, otherwise it will
|
|
// cause issues with GDAL 3.7 that does honour the topological intent of
|
|
// multipolygon
|
|
return hGeom;
|
|
}
|
|
|
|
return OGR_G_ForceTo( hGeom, layerGeomType, nullptr );
|
|
}
|
|
|
|
QString QgsOgrProvider::jsonStringValue( const QVariant &value ) const
|
|
{
|
|
QString stringValue = QString::fromUtf8( QJsonDocument::fromVariant( value ).toJson().constData() );
|
|
if ( stringValue.isEmpty() )
|
|
{
|
|
//store as string, because it's no valid QJson value
|
|
stringValue = value.toString();
|
|
}
|
|
return stringValue;
|
|
}
|
|
|
|
// Stricter version of QVariant::toInt() that addresses the fact that
|
|
// QVariant::toInt() doesn't detect integer truncation if the type
|
|
// is a long long.
|
|
static int strictToInt( const QVariant &v, bool *ok )
|
|
{
|
|
if ( v.userType() == QMetaType::Type::Int )
|
|
{
|
|
*ok = true;
|
|
return v.toInt();
|
|
}
|
|
else
|
|
{
|
|
const qlonglong val = v.toLongLong( ok );
|
|
if ( *ok && val >= std::numeric_limits<int>::min() && val <= std::numeric_limits<int>::max() )
|
|
{
|
|
return static_cast<int>( val );
|
|
}
|
|
else
|
|
{
|
|
*ok = false;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Converts a string with values "0", "false", "1", "true" to 0 / 1
|
|
static int stringToBool( const QString &strVal, bool *ok )
|
|
{
|
|
if ( strVal.compare( QLatin1String( "0" ) ) == 0 || strVal.compare( QLatin1String( "false" ), Qt::CaseInsensitive ) == 0 )
|
|
{
|
|
*ok = true;
|
|
return 0;
|
|
}
|
|
else if ( strVal.compare( QLatin1String( "1" ) ) == 0 || strVal.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 )
|
|
{
|
|
*ok = true;
|
|
return 1;
|
|
}
|
|
*ok = false;
|
|
return 0;
|
|
}
|
|
|
|
|
|
bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId incrementalFeatureId )
|
|
{
|
|
bool returnValue = true;
|
|
QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn();
|
|
gdal::ogr_feature_unique_ptr feature( featureDefinition.CreateFeature() );
|
|
|
|
if ( f.hasGeometry() )
|
|
{
|
|
QByteArray wkb( f.geometry().asWkb() );
|
|
OGRGeometryH geom = nullptr;
|
|
|
|
if ( !wkb.isEmpty() )
|
|
{
|
|
if ( OGR_G_CreateFromWkb( reinterpret_cast<unsigned char *>( const_cast<char *>( wkb.constData() ) ), nullptr, &geom, wkb.length() ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error creating wkb for feature %1: %2" ).arg( f.id() ).arg( CPLGetLastErrorMsg() ) );
|
|
return false;
|
|
}
|
|
|
|
geom = ConvertGeometryIfNecessary( geom );
|
|
|
|
OGR_F_SetGeometryDirectly( feature.get(), geom );
|
|
}
|
|
}
|
|
|
|
QgsAttributes attributes = f.attributes();
|
|
const QgsFields qgisFields { f.fields() };
|
|
|
|
QgsLocaleNumC l;
|
|
|
|
int qgisAttributeId = ( mFirstFieldIsFid ) ? 1 : 0;
|
|
// If the first attribute is the FID and the user has set it, then use it
|
|
if ( mFirstFieldIsFid && attributes.count() > 0 )
|
|
{
|
|
QVariant attrFid = attributes.at( 0 );
|
|
if ( !QgsVariantUtils::isNull( attrFid ) )
|
|
{
|
|
bool ok = false;
|
|
qlonglong id = attrFid.toLongLong( &ok );
|
|
if ( ok )
|
|
{
|
|
OGR_F_SetFID( feature.get(), static_cast<GIntBig>( id ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
//add possible attribute information
|
|
for ( int ogrAttributeId = 0; qgisAttributeId < attributes.count(); ++qgisAttributeId, ++ogrAttributeId )
|
|
{
|
|
// Skip fields that have no provider origin
|
|
if ( qgisFields.exists( qgisAttributeId ) && qgisFields.fieldOrigin( qgisAttributeId ) != Qgis::FieldOrigin::Provider )
|
|
{
|
|
qgisAttributeId++;
|
|
continue;
|
|
}
|
|
|
|
// don't try to set field from attribute map if it's not present in layer
|
|
if ( ogrAttributeId >= featureDefinition.GetFieldCount() )
|
|
{
|
|
pushError( tr( "Feature has too many attributes (expecting %1, received %2)" ).arg( featureDefinition.GetFieldCount() ).arg( f.attributeCount() ) );
|
|
continue;
|
|
}
|
|
|
|
//if(!s.isEmpty())
|
|
// continue;
|
|
//
|
|
OGRFieldDefnH fldDef = featureDefinition.GetFieldDefn( ogrAttributeId );
|
|
const QString ogrFieldName = textEncoding()->toUnicode( OGR_Fld_GetNameRef( fldDef ) );
|
|
const OGRFieldType type = OGR_Fld_GetType( fldDef );
|
|
const OGRFieldSubType subType = OGR_Fld_GetSubType( fldDef );
|
|
|
|
QVariant attrVal = attributes.at( qgisAttributeId );
|
|
const QMetaType::Type qType = static_cast<QMetaType::Type>( attrVal.userType() );
|
|
// The field value is equal to the default (that might be a provider-side expression)
|
|
if ( attributes.isUnsetValue( qgisAttributeId )
|
|
|| ( mDefaultValues.contains( qgisAttributeId ) && attrVal.toString() == mDefaultValues.value( qgisAttributeId ) )
|
|
)
|
|
{
|
|
OGR_F_UnsetField( feature.get(), ogrAttributeId );
|
|
}
|
|
else if ( QgsVariantUtils::isNull( attrVal ) || ( type != OFTString && ( ( qType != QMetaType::Type::QVariantList && attrVal.toString().isEmpty() && qType != QMetaType::Type::QStringList && attrVal.toStringList().isEmpty() ) || ( qType == QMetaType::Type::QVariantList && attrVal.toList().empty() ) ) ) )
|
|
{
|
|
// Starting with GDAL 2.2, there are 2 concepts: unset fields and null fields
|
|
// whereas previously there was only unset fields. For a GeoJSON output,
|
|
// leaving a field unset will cause it to not appear at all in the output
|
|
// feature.
|
|
// When all features of a layer have a field unset, this would cause the
|
|
// field to not be present at all in the output, and thus on reading to
|
|
// have disappeared. #16812
|
|
#ifdef OGRNullMarker
|
|
OGR_F_SetFieldNull( feature.get(), ogrAttributeId );
|
|
#else
|
|
OGR_F_UnsetField( feature.get(), ogrAttId );
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
bool errorEmitted = false;
|
|
bool ok = false;
|
|
switch ( type )
|
|
{
|
|
case OFTInteger:
|
|
{
|
|
if ( subType == OFSTBoolean && qType == QMetaType::Type::QString )
|
|
{
|
|
// compatibility with use case of https://github.com/qgis/QGIS/issues/55517
|
|
const QString strVal = attrVal.toString();
|
|
OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, stringToBool( strVal, &ok ) );
|
|
if ( !ok )
|
|
{
|
|
pushError( tr( "wrong value for attribute %1 of feature %2: %3" )
|
|
.arg( ogrFieldName )
|
|
.arg( f.id() )
|
|
.arg( strVal ) );
|
|
errorEmitted = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, strictToInt( attrVal, &ok ) );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTInteger64:
|
|
OGR_F_SetFieldInteger64( feature.get(), ogrAttributeId, attrVal.toLongLong( &ok ) );
|
|
break;
|
|
|
|
case OFTReal:
|
|
OGR_F_SetFieldDouble( feature.get(), ogrAttributeId, attrVal.toDouble( &ok ) );
|
|
break;
|
|
|
|
case OFTDate:
|
|
{
|
|
const QDate date = attrVal.toDate();
|
|
if ( date.isValid() )
|
|
{
|
|
ok = true;
|
|
OGR_F_SetFieldDateTime( feature.get(), ogrAttributeId,
|
|
date.year(),
|
|
date.month(),
|
|
date.day(),
|
|
0, 0, 0,
|
|
0 );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTTime:
|
|
{
|
|
const QTime time = attrVal.toTime();
|
|
if ( time.isValid() )
|
|
{
|
|
ok = true;
|
|
OGR_F_SetFieldDateTimeEx( feature.get(), ogrAttributeId,
|
|
0, 0, 0,
|
|
time.hour(),
|
|
time.minute(),
|
|
static_cast<float>( time.second() + static_cast< double >( time.msec() ) / 1000 ),
|
|
0 );
|
|
}
|
|
break;
|
|
}
|
|
case OFTDateTime:
|
|
{
|
|
QDateTime dt = attrVal.toDateTime();
|
|
if ( dt.isValid() )
|
|
{
|
|
ok = true;
|
|
if ( mConvertLocalTimeToUTC && dt.timeSpec() == Qt::LocalTime )
|
|
dt = dt.toUTC();
|
|
const QDate date = dt.date();
|
|
const QTime time = dt.time();
|
|
OGR_F_SetFieldDateTimeEx( feature.get(), ogrAttributeId,
|
|
date.year(),
|
|
date.month(),
|
|
date.day(),
|
|
time.hour(),
|
|
time.minute(),
|
|
static_cast<float>( time.second() + static_cast< double >( time.msec() ) / 1000 ),
|
|
QgsOgrUtils::OGRTZFlagFromQt( dt ) );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTString:
|
|
{
|
|
ok = true;
|
|
QString stringValue;
|
|
|
|
if ( subType == OFSTJSON )
|
|
{
|
|
stringValue = QString::fromStdString( QgsJsonUtils::jsonFromVariant( attrVal ).dump() );
|
|
}
|
|
else
|
|
{
|
|
stringValue = attrVal.toString();
|
|
}
|
|
QgsDebugMsgLevel( QStringLiteral( "Writing string attribute %1 with %2, encoding %3" )
|
|
.arg( qgisAttributeId )
|
|
.arg( attrVal.toString(),
|
|
textEncoding()->name().data() ), 3 );
|
|
OGR_F_SetFieldString( feature.get(), ogrAttributeId, textEncoding()->fromUnicode( stringValue ).constData() );
|
|
break;
|
|
}
|
|
case OFTBinary:
|
|
{
|
|
const QByteArray ba = attrVal.toByteArray();
|
|
ok = true;
|
|
OGR_F_SetFieldBinary( feature.get(), ogrAttributeId, ba.size(), const_cast< GByte * >( reinterpret_cast< const GByte * >( ba.data() ) ) );
|
|
|
|
break;
|
|
}
|
|
|
|
case OFTStringList:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
QStringList list = attrVal.toStringList();
|
|
ok = true;
|
|
int count = list.count();
|
|
char **lst = static_cast<char **>( CPLMalloc( ( count + 1 ) * sizeof( char * ) ) );
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QString &string : list )
|
|
{
|
|
lst[pos] = CPLStrdup( textEncoding()->fromUnicode( string ).data() );
|
|
pos++;
|
|
}
|
|
}
|
|
lst[count] = nullptr;
|
|
OGR_F_SetFieldStringList( feature.get(), ogrAttributeId, lst );
|
|
CSLDestroy( lst );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTIntegerList:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
const QVariantList list = attrVal.toList();
|
|
ok = true;
|
|
const int count = list.count();
|
|
int *lst = new int[count];
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QVariant &value : list )
|
|
{
|
|
lst[pos] = strictToInt( value, &ok );
|
|
if ( !ok )
|
|
break;
|
|
pos++;
|
|
}
|
|
}
|
|
if ( ok )
|
|
OGR_F_SetFieldIntegerList( feature.get(), ogrAttributeId, count, lst );
|
|
delete [] lst;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTRealList:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
const QVariantList list = attrVal.toList();
|
|
ok = true;
|
|
const int count = list.count();
|
|
double *lst = new double[count];
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QVariant &value : list )
|
|
{
|
|
lst[pos] = value.toDouble( &ok );
|
|
if ( !ok )
|
|
break;
|
|
pos++;
|
|
}
|
|
}
|
|
if ( ok )
|
|
{
|
|
OGR_F_SetFieldDoubleList( feature.get(), ogrAttributeId, count, lst );
|
|
}
|
|
delete [] lst;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTInteger64List:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
const QVariantList list = attrVal.toList();
|
|
const int count = list.count();
|
|
long long *lst = new long long[count];
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QVariant &value : list )
|
|
{
|
|
lst[pos] = value.toLongLong( &ok );
|
|
if ( !ok )
|
|
break;
|
|
pos++;
|
|
}
|
|
}
|
|
if ( ok )
|
|
{
|
|
OGR_F_SetFieldInteger64List( feature.get(), ogrAttributeId, count, lst );
|
|
}
|
|
delete [] lst;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
QgsMessageLog::logMessage( tr( "type %1 for attribute %2 not found" ).arg( type ).arg( qgisAttributeId ), tr( "OGR" ) );
|
|
break;
|
|
}
|
|
|
|
if ( !ok )
|
|
{
|
|
if ( !errorEmitted )
|
|
{
|
|
QMetaType::Type ogrVariantType = QMetaType::Type::UnknownType;
|
|
QMetaType::Type ogrVariantSubType = QMetaType::Type::UnknownType;
|
|
QgsOgrUtils::ogrFieldTypeToQVariantType( type, subType, ogrVariantType, ogrVariantSubType );
|
|
|
|
pushError( tr( "wrong data type for attribute %1 of feature %2: Got %3, expected %4" )
|
|
.arg( ogrFieldName )
|
|
.arg( f.id() )
|
|
.arg( attrVal.typeName(), QVariant::typeToName( ogrVariantType ) ) );
|
|
}
|
|
returnValue = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( mOgrLayer->CreateFeature( feature.get() ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error creating feature %1: %2" ).arg( f.id() ).arg( CPLGetLastErrorMsg() ) );
|
|
returnValue = false;
|
|
}
|
|
else
|
|
{
|
|
if ( !( flags & QgsFeatureSink::FastInsert ) )
|
|
{
|
|
QgsFeatureId id = static_cast<QgsFeatureId>( OGR_F_GetFID( feature.get() ) );
|
|
if ( id >= 0 )
|
|
{
|
|
f.setId( id );
|
|
|
|
if ( mFirstFieldIsFid && attributes.count() > 0 )
|
|
{
|
|
f.setAttribute( 0, id );
|
|
}
|
|
}
|
|
else if ( incrementalFeatureId >= 0 )
|
|
{
|
|
f.setId( incrementalFeatureId );
|
|
}
|
|
}
|
|
}
|
|
|
|
return returnValue;
|
|
}
|
|
|
|
|
|
bool QgsOgrProvider::addFeatures( QgsFeatureList &flist, Flags flags )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
setRelevantFields( true, attributeIndexes() );
|
|
|
|
const bool inTransaction = startTransaction();
|
|
|
|
QgsFeatureId incrementalFeatureId = -1;
|
|
if ( !( flags & QgsFeatureSink::FastInsert ) &&
|
|
( mGDALDriverName == QLatin1String( "CSV" ) || mGDALDriverName == QLatin1String( "XLSX" ) || mGDALDriverName == QLatin1String( "ODS" ) ) )
|
|
{
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex );
|
|
{
|
|
QMutexLocker locker( mutex );
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
OGR_L_SetAttributeFilter( layer, nullptr );
|
|
|
|
incrementalFeatureId = static_cast< QgsFeatureId >( OGR_L_GetFeatureCount( layer, false ) ) + 1;
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
OGR_L_SetAttributeFilter( layer, textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ).constData() );
|
|
}
|
|
}
|
|
|
|
bool returnvalue = true;
|
|
for ( QgsFeatureList::iterator it = flist.begin(); it != flist.end(); ++it )
|
|
{
|
|
if ( !addFeaturePrivate( *it, flags, incrementalFeatureId ) )
|
|
{
|
|
returnvalue = false;
|
|
}
|
|
if ( incrementalFeatureId >= 0 )
|
|
incrementalFeatureId++;
|
|
}
|
|
|
|
if ( inTransaction )
|
|
{
|
|
if ( returnvalue )
|
|
returnvalue = commitTransaction();
|
|
else
|
|
rollbackTransaction();
|
|
}
|
|
|
|
if ( !syncToDisc() )
|
|
{
|
|
returnvalue = false;
|
|
}
|
|
|
|
if ( mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) &&
|
|
mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::UnknownCount ) )
|
|
{
|
|
if ( returnvalue )
|
|
mFeaturesCounted += flist.size();
|
|
else
|
|
recalculateFeatureCount();
|
|
}
|
|
|
|
if ( returnvalue )
|
|
clearMinMaxCache();
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
return returnvalue;
|
|
}
|
|
|
|
bool QgsOgrProvider::addAttributeOGRLevel( const QgsField &field, bool &ignoreErrorOut )
|
|
{
|
|
ignoreErrorOut = false;
|
|
|
|
OGRFieldType type;
|
|
|
|
switch ( field.type() )
|
|
{
|
|
case QMetaType::Type::Int:
|
|
case QMetaType::Type::Bool:
|
|
type = OFTInteger;
|
|
break;
|
|
case QMetaType::Type::LongLong:
|
|
{
|
|
const char *pszDataTypes = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_CREATIONFIELDDATATYPES, nullptr );
|
|
if ( pszDataTypes && strstr( pszDataTypes, "Integer64" ) )
|
|
type = OFTInteger64;
|
|
else
|
|
{
|
|
type = OFTReal;
|
|
}
|
|
break;
|
|
}
|
|
case QMetaType::Type::Double:
|
|
type = OFTReal;
|
|
break;
|
|
case QMetaType::Type::QDate:
|
|
type = OFTDate;
|
|
break;
|
|
case QMetaType::Type::QTime:
|
|
type = OFTTime;
|
|
break;
|
|
case QMetaType::Type::QDateTime:
|
|
type = OFTDateTime;
|
|
break;
|
|
case QMetaType::Type::QString:
|
|
type = OFTString;
|
|
break;
|
|
case QMetaType::Type::QByteArray:
|
|
type = OFTBinary;
|
|
break;
|
|
case QMetaType::Type::QVariantMap:
|
|
type = OFTString;
|
|
break;
|
|
case QMetaType::Type::QStringList:
|
|
type = OFTStringList;
|
|
break;
|
|
case QMetaType::Type::QVariantList:
|
|
if ( field.subType() == QMetaType::Type::QString )
|
|
{
|
|
type = OFTStringList;
|
|
break;
|
|
}
|
|
else if ( field.subType() == QMetaType::Type::Int )
|
|
{
|
|
type = OFTIntegerList;
|
|
break;
|
|
}
|
|
else if ( field.subType() == QMetaType::Type::LongLong )
|
|
{
|
|
type = OFTInteger64List;
|
|
break;
|
|
}
|
|
else if ( field.subType() == QMetaType::Type::Double )
|
|
{
|
|
type = OFTRealList;
|
|
break;
|
|
}
|
|
// other lists are supported at this moment, fall through to default for other types
|
|
|
|
//intentional fall-through
|
|
[[fallthrough]];
|
|
|
|
default:
|
|
pushError( tr( "type %1 for field %2 not found" ).arg( field.typeName(), field.name() ) );
|
|
ignoreErrorOut = true;
|
|
return false;
|
|
}
|
|
|
|
gdal::ogr_field_def_unique_ptr fielddefn( OGR_Fld_Create( textEncoding()->fromUnicode( field.name() ).constData(), type ) );
|
|
int width = field.length();
|
|
// Increase width by 1 for OFTReal to make room for the decimal point
|
|
if ( type == OFTReal && field.precision() )
|
|
width += 1;
|
|
OGR_Fld_SetWidth( fielddefn.get(), width );
|
|
OGR_Fld_SetPrecision( fielddefn.get(), field.precision() );
|
|
|
|
switch ( field.type() )
|
|
{
|
|
case QMetaType::Type::Bool:
|
|
OGR_Fld_SetSubType( fielddefn.get(), OFSTBoolean );
|
|
break;
|
|
case QMetaType::Type::QVariantMap:
|
|
OGR_Fld_SetSubType( fielddefn.get(), OFSTJSON );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0)
|
|
OGR_Fld_SetAlternativeName( fielddefn.get(), textEncoding()->fromUnicode( field.alias() ).constData() );
|
|
#endif
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
OGR_Fld_SetComment( fielddefn.get(), textEncoding()->fromUnicode( field.comment() ).constData() );
|
|
#endif
|
|
|
|
if ( mOgrLayer->CreateField( fielddefn.get(), true ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error creating field %1: %2" ).arg( field.name(), CPLGetLastErrorMsg() ) );
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool QgsOgrProvider::addAttributes( const QList<QgsField> &attributes )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
if ( mGDALDriverName == QLatin1String( "MapInfo File" ) )
|
|
{
|
|
// adding attributes in mapinfo requires to be able to delete the .dat file
|
|
// so drop any cached connections.
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
}
|
|
|
|
bool returnvalue = true;
|
|
|
|
QMap< QString, QgsField > mapFieldNameToOriginalField;
|
|
|
|
for ( const auto &field : attributes )
|
|
{
|
|
mapFieldNameToOriginalField[ field.name()] = field;
|
|
|
|
bool ignoreErrorOut = false;
|
|
if ( !addAttributeOGRLevel( field, ignoreErrorOut ) )
|
|
{
|
|
returnvalue = false;
|
|
if ( !ignoreErrorOut )
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// We at least want to syncToDisc() for OpenFileGDB, because its AddField
|
|
// implementation doesn't update immediately system tables.
|
|
// We exclude GeoJSON because leaveUpdateMode() has specific behavior for it.
|
|
if ( mGDALDriverName != QLatin1String( "GeoJSON" ) && !syncToDisc() )
|
|
|
|
{
|
|
returnvalue = false;
|
|
}
|
|
|
|
// Backup existing fields. We need them to 'restore' field type, length, precision
|
|
QgsFields oldFields = fields();
|
|
|
|
// Updates mAttributeFields
|
|
loadFields();
|
|
|
|
// The check in QgsVectorLayerEditBuffer::commitChanges() is questionable with
|
|
// real-world drivers that might only be able to satisfy request only partially.
|
|
// So to avoid erroring out, patch field type, width and precision to match
|
|
// what was requested.
|
|
// For example in case of Integer64->Real mapping so that QVariant::LongLong is
|
|
// still returned to the caller
|
|
// Or if a field width was specified but not strictly enforced by the driver (#15614)
|
|
for ( QMap< QString, QgsField >::const_iterator it = mapFieldNameToOriginalField.constBegin();
|
|
it != mapFieldNameToOriginalField.constEnd(); ++it )
|
|
{
|
|
int idx = mAttributeFields.lookupField( it.key() );
|
|
if ( idx >= 0 )
|
|
{
|
|
mAttributeFields[ idx ].setType( it->type() );
|
|
mAttributeFields[ idx ].setLength( it->length() );
|
|
mAttributeFields[ idx ].setPrecision( it->precision() );
|
|
mAttributeFields[ idx ].setEditorWidgetSetup( it->editorWidgetSetup() );
|
|
}
|
|
}
|
|
|
|
// Restore field type, length, precision of existing fields as well
|
|
// We need that in scenarios where the user adds a int field with length != 0
|
|
// in a editing session, and repeat that again in another editing session
|
|
// Without the below hack, the length of the first added field would have
|
|
// been reset to zero, and QgsVectorLayerEditBuffer::commitChanges() would
|
|
// error out because of this.
|
|
// See https://github.com/qgis/QGIS/issues/26840
|
|
for ( const QgsField &field : oldFields )
|
|
{
|
|
int idx = mAttributeFields.lookupField( field.name() );
|
|
if ( idx >= 0 )
|
|
{
|
|
mAttributeFields[ idx ].setType( field.type() );
|
|
mAttributeFields[ idx ].setLength( field.length() );
|
|
mAttributeFields[ idx ].setPrecision( field.precision() );
|
|
}
|
|
}
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
return returnvalue;
|
|
}
|
|
|
|
bool QgsOgrProvider::deleteAttributes( const QgsAttributeIds &attributes )
|
|
{
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
bool res = true;
|
|
QList<int> attrsLst( attributes.begin(), attributes.end() );
|
|
// sort in descending order
|
|
std::sort( attrsLst.begin(), attrsLst.end(), std::greater<int>() );
|
|
const auto constAttrsLst = attrsLst;
|
|
for ( int attr : constAttrsLst )
|
|
{
|
|
if ( mFirstFieldIsFid )
|
|
{
|
|
if ( attr == 0 )
|
|
{
|
|
pushError( tr( "Cannot delete feature id column" ) );
|
|
res = false;
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
--attr;
|
|
}
|
|
}
|
|
if ( mOgrLayer->DeleteField( attr ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error deleting field %1: %2" ).arg( attr ).arg( CPLGetLastErrorMsg() ) );
|
|
res = false;
|
|
}
|
|
}
|
|
|
|
// We at least want to syncToDisc() for OpenFileGDB, because its DeleteField
|
|
// implementation doesn't update immediately system tables.
|
|
// We exclude GeoJSON because leaveUpdateMode() has specific behavior for it.
|
|
if ( mGDALDriverName != QLatin1String( "GeoJSON" ) && !syncToDisc() )
|
|
{
|
|
res = false;
|
|
}
|
|
|
|
loadFields();
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
return res;
|
|
}
|
|
|
|
bool QgsOgrProvider::renameAttributes( const QgsFieldNameMap &renamedAttributes )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
QgsFieldNameMap::const_iterator renameIt = renamedAttributes.constBegin();
|
|
bool result = true;
|
|
for ( ; renameIt != renamedAttributes.constEnd(); ++renameIt )
|
|
{
|
|
int fieldIndex = renameIt.key();
|
|
if ( fieldIndex < 0 || fieldIndex >= fields().count() )
|
|
{
|
|
pushError( tr( "Invalid attribute index" ) );
|
|
result = false;
|
|
continue;
|
|
}
|
|
if ( fields().indexFromName( renameIt.value() ) >= 0 )
|
|
{
|
|
//field name already in use
|
|
pushError( tr( "Error renaming field %1: name '%2' already exists" ).arg( fieldIndex ).arg( renameIt.value() ) );
|
|
result = false;
|
|
continue;
|
|
}
|
|
int ogrFieldIndex = fieldIndex;
|
|
if ( mFirstFieldIsFid )
|
|
{
|
|
ogrFieldIndex -= 1;
|
|
if ( ogrFieldIndex < 0 )
|
|
{
|
|
pushError( tr( "Invalid attribute index" ) );
|
|
result = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
//type does not matter, it will not be used
|
|
gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( textEncoding()->fromUnicode( renameIt.value() ), OFTReal ) );
|
|
if ( mOgrLayer->AlterFieldDefn( ogrFieldIndex, fld.get(), ALTER_NAME_FLAG ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error renaming field %1: %2" ).arg( fieldIndex ).arg( CPLGetLastErrorMsg() ) );
|
|
result = false;
|
|
}
|
|
}
|
|
|
|
// We at least want to syncToDisc() for OpenFileGDB, because its AlterFieldDefn
|
|
// implementation doesn't update immediately system tables.
|
|
// We exclude GeoJSON because leaveUpdateMode() has specific behavior for it.
|
|
if ( mGDALDriverName != QLatin1String( "GeoJSON" ) && !syncToDisc() )
|
|
{
|
|
result = false;
|
|
}
|
|
|
|
loadFields();
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
return result;
|
|
}
|
|
|
|
bool QgsOgrProvider::startTransaction()
|
|
{
|
|
bool inTransaction = false;
|
|
if ( !mTransaction && mOgrLayer->TestCapability( OLCTransactions ) )
|
|
{
|
|
// A transaction might already be active, so be robust on failed
|
|
// StartTransaction.
|
|
CPLPushErrorHandler( CPLQuietErrorHandler );
|
|
inTransaction = ( mOgrLayer->StartTransaction() == OGRERR_NONE );
|
|
CPLPopErrorHandler();
|
|
}
|
|
return inTransaction;
|
|
}
|
|
|
|
|
|
bool QgsOgrProvider::commitTransaction()
|
|
{
|
|
if ( mOgrLayer->CommitTransaction() != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error committing transaction: %1" ).arg( CPLGetLastErrorMsg() ) );
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool QgsOgrProvider::rollbackTransaction()
|
|
{
|
|
if ( mOgrLayer->RollbackTransaction() != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error rolling back transaction: %1" ).arg( CPLGetLastErrorMsg() ) );
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool QgsOgrProvider::_setSubsetString( const QString &theSQL, bool updateFeatureCount, bool updateCapabilities, bool hasExistingRef )
|
|
{
|
|
QgsCPLErrorHandler handler( QObject::tr( "OGR" ) );
|
|
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !mOgrOrigLayer )
|
|
return false;
|
|
|
|
if ( theSQL == mSubsetString && mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) )
|
|
return true;
|
|
|
|
const QString oldSubsetString { mSubsetString };
|
|
|
|
const bool subsetStringHasChanged { theSQL != mSubsetString };
|
|
|
|
const QString cleanSql = QgsOgrProviderUtils::cleanSubsetString( theSQL );
|
|
if ( !cleanSql.isEmpty() )
|
|
{
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex );
|
|
GDALDatasetH ds = mOgrOrigLayer->getDatasetHandleAndMutex( mutex );
|
|
OGRLayerH subsetLayerH;
|
|
{
|
|
QMutexLocker locker( mutex );
|
|
subsetLayerH = QgsOgrProviderUtils::setSubsetString( layer, ds, textEncoding(), cleanSql );
|
|
}
|
|
if ( !subsetLayerH )
|
|
{
|
|
pushError( tr( "OGR[%1] error %2: %3" ).arg( CPLGetLastErrorType() ).arg( CPLGetLastErrorNo() ).arg( CPLGetLastErrorMsg() ) );
|
|
return false;
|
|
}
|
|
if ( layer != subsetLayerH )
|
|
{
|
|
mOgrSqlLayer = QgsOgrProviderUtils::getSqlLayer( mOgrOrigLayer.get(), subsetLayerH, cleanSql );
|
|
Q_ASSERT( mOgrSqlLayer.get() );
|
|
mOgrLayer = mOgrSqlLayer.get();
|
|
|
|
const QStringList tableNames {QgsOgrProviderUtils::tableNamesFromSelectSQL( cleanSql ) };
|
|
if ( ! tableNames.isEmpty() )
|
|
{
|
|
mLayerName.clear();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mOgrSqlLayer.reset();
|
|
mOgrLayer = mOgrOrigLayer.get();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mOgrSqlLayer.reset();
|
|
mOgrLayer = mOgrOrigLayer.get();
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex );
|
|
{
|
|
QMutexLocker locker( mutex );
|
|
OGR_L_SetAttributeFilter( layer, nullptr );
|
|
}
|
|
|
|
// Try to guess the table name from the old subset string or we might
|
|
// end with a layer URI without layername
|
|
if ( !oldSubsetString.isEmpty() )
|
|
{
|
|
const QStringList tableNames { QgsOgrProviderUtils::tableNamesFromSelectSQL( oldSubsetString ) };
|
|
if ( tableNames.size() > 0 )
|
|
{
|
|
mLayerName = tableNames.at( 0 );
|
|
}
|
|
}
|
|
|
|
}
|
|
mSubsetString = theSQL;
|
|
|
|
QVariantMap parts;
|
|
parts.insert( QStringLiteral( "path" ), mFilePath );
|
|
|
|
if ( !mLayerName.isNull() )
|
|
{
|
|
parts.insert( QStringLiteral( "layerName" ), mLayerName );
|
|
}
|
|
else if ( mIsSubLayer && mLayerIndex >= 0 )
|
|
{
|
|
parts.insert( QStringLiteral( "layerId" ), mLayerIndex );
|
|
}
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
{
|
|
parts.insert( QStringLiteral( "subset" ), mSubsetString );
|
|
}
|
|
|
|
if ( mOgrGeometryTypeFilter != wkbUnknown )
|
|
{
|
|
parts.insert( QStringLiteral( "geometryType" ), QgsOgrProviderUtils::ogrWkbGeometryTypeName( mOgrGeometryTypeFilter ) );
|
|
}
|
|
|
|
if ( mUniqueGeometryType )
|
|
{
|
|
parts.insert( QStringLiteral( "uniqueGeometryType" ), QStringLiteral( "yes" ) );
|
|
}
|
|
|
|
if ( !mOpenOptions.isEmpty() )
|
|
{
|
|
parts.insert( QStringLiteral( "openOptions" ), mOpenOptions );
|
|
}
|
|
|
|
if ( !mCredentialOptions.isEmpty() )
|
|
{
|
|
parts.insert( QStringLiteral( "credentialOptions" ), mCredentialOptions );
|
|
}
|
|
|
|
QString uri = QgsOgrProviderMetadata().encodeUri( parts );
|
|
if ( uri != dataSourceUri() )
|
|
{
|
|
if ( hasExistingRef )
|
|
QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
setDataSourceUri( uri );
|
|
if ( hasExistingRef )
|
|
QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
}
|
|
|
|
mOgrLayer->ResetReading();
|
|
|
|
mRefreshFeatureCount = updateFeatureCount;
|
|
|
|
// check the validity of the layer if subset string has changed
|
|
if ( subsetStringHasChanged )
|
|
{
|
|
loadFields();
|
|
}
|
|
|
|
invalidateCachedExtent( false );
|
|
|
|
// Changing the filter may change capabilities
|
|
if ( updateCapabilities )
|
|
computeCapabilities();
|
|
|
|
emit dataChanged();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
if ( attr_map.isEmpty() )
|
|
return true;
|
|
|
|
bool returnValue = true;
|
|
|
|
clearMinMaxCache();
|
|
|
|
setRelevantFields( true, attributeIndexes() );
|
|
|
|
const bool inTransaction = startTransaction();
|
|
|
|
// Some drivers may need to call ResetReading() after GetFeature(), such
|
|
// as GPKG in GDAL < 2.3.0 to avoid letting the database in a locked state.
|
|
// But this is undesirable in general, so don't do this when we know that
|
|
// we don't need to.
|
|
bool mayNeedResetReadingAfterGetFeature = true;
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ||
|
|
mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "CSV" ) )
|
|
{
|
|
mayNeedResetReadingAfterGetFeature = false;
|
|
}
|
|
|
|
QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn();
|
|
|
|
/* Optimization to update a single field of all layer's feature with a
|
|
* constant value */
|
|
bool useUpdate = false;
|
|
// If changing the below value, update it into test_provider_ogr_gpkg.py
|
|
// as well
|
|
constexpr size_t THRESHOLD_UPDATE_OPTIM = 100;
|
|
if ( static_cast<size_t>( attr_map.size() ) >= THRESHOLD_UPDATE_OPTIM &&
|
|
( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "SQLite" ) ) &&
|
|
mOgrLayer->TestCapability( OLCFastFeatureCount ) &&
|
|
attr_map.size() == mOgrLayer->GetFeatureCount() )
|
|
{
|
|
std::set<QgsFeatureId> fids;
|
|
OGRFieldDefnH fd = nullptr;
|
|
int fieldIdx = -1;
|
|
QVariant val;
|
|
OGRFieldType type = OFTMaxType;
|
|
useUpdate = true;
|
|
for ( QgsChangedAttributesMap::const_iterator it = attr_map.begin(); it != attr_map.end(); ++it )
|
|
{
|
|
QgsFeatureId fid = it.key();
|
|
fids.insert( fid );
|
|
const QgsAttributeMap &attr = it.value();
|
|
if ( attr.size() != 1 )
|
|
{
|
|
useUpdate = false;
|
|
break;
|
|
}
|
|
QgsAttributeMap::const_iterator it2 = attr.begin();
|
|
if ( fieldIdx < 0 )
|
|
{
|
|
fieldIdx = it2.key();
|
|
if ( fieldIdx == 0 && mFirstFieldIsFid )
|
|
{
|
|
useUpdate = false;
|
|
break;
|
|
}
|
|
fd = featureDefinition.GetFieldDefn(
|
|
( mFirstFieldIsFid && fieldIdx > 0 ) ? fieldIdx - 1 : fieldIdx );
|
|
if ( !fd )
|
|
{
|
|
useUpdate = false;
|
|
break;
|
|
}
|
|
type = OGR_Fld_GetType( fd );
|
|
if ( type != OFTInteger && type != OFTInteger64 && type != OFTString && type != OFTReal )
|
|
{
|
|
useUpdate = false;
|
|
break;
|
|
}
|
|
val = *it2;
|
|
}
|
|
else if ( fieldIdx != it2.key() || val != *it2 )
|
|
{
|
|
useUpdate = false;
|
|
break;
|
|
}
|
|
}
|
|
if ( useUpdate && fids.size() != static_cast<size_t>( attr_map.size() ) )
|
|
{
|
|
useUpdate = false;
|
|
}
|
|
if ( useUpdate && val.userType() != qMetaTypeId<QgsUnsetAttributeValue >() )
|
|
{
|
|
QString sql = QStringLiteral( "UPDATE %1 SET %2 = %3" )
|
|
.arg( QString::fromUtf8( QgsOgrProviderUtils::quotedIdentifier( mOgrLayer->name(), mGDALDriverName ) ) )
|
|
.arg( QString::fromUtf8( QgsOgrProviderUtils::quotedIdentifier( QByteArray( OGR_Fld_GetNameRef( fd ) ), mGDALDriverName ) ) )
|
|
.arg( QgsOgrProviderUtils::quotedValue( val ) );
|
|
QgsDebugMsgLevel( QStringLiteral( "Using optimized changeAttributeValues(): %1" ).arg( sql ), 3 );
|
|
CPLErrorReset();
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( sql.toUtf8() );
|
|
if ( CPLGetLastErrorType() != CE_None )
|
|
{
|
|
useUpdate = false;
|
|
returnValue = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// General case: let's iterate over all features and attributes to update
|
|
QgsChangedAttributesMap::const_iterator it = attr_map.begin();
|
|
if ( useUpdate )
|
|
it = attr_map.end();
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,3)
|
|
// UpdateFeature is available since 3.7.0, but was broken for GeoJSON up to 3.9.3
|
|
// see https://github.com/OSGeo/gdal/pull/10197
|
|
// and https://github.com/OSGeo/gdal/pull/10794
|
|
const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature );
|
|
#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature )
|
|
&& mGDALDriverName != QLatin1String( "ODS" )
|
|
&& mGDALDriverName != QLatin1String( "XLSX" )
|
|
&& mGDALDriverName != QLatin1String( "GeoJSON" );
|
|
#else
|
|
constexpr bool useUpdateFeature = false;
|
|
#endif
|
|
|
|
for ( ; it != attr_map.end(); ++it )
|
|
{
|
|
QgsFeatureId fid = it.key();
|
|
|
|
const QgsAttributeMap &attr = it.value();
|
|
if ( attr.isEmpty() )
|
|
continue;
|
|
|
|
gdal::ogr_feature_unique_ptr of;
|
|
if ( useUpdateFeature )
|
|
{
|
|
of.reset( featureDefinition.CreateFeature() );
|
|
OGR_F_SetFID( of.get(), FID_TO_NUMBER( fid ) );
|
|
}
|
|
else
|
|
{
|
|
of.reset( mOgrLayer->GetFeature( FID_TO_NUMBER( fid ) ) );
|
|
if ( !of )
|
|
{
|
|
pushError( tr( "Feature %1 for attribute update not found." ).arg( fid ) );
|
|
returnValue = false;
|
|
continue;
|
|
}
|
|
if ( mayNeedResetReadingAfterGetFeature )
|
|
{
|
|
mOgrLayer->ResetReading();
|
|
}
|
|
}
|
|
|
|
QgsLocaleNumC l;
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
QVector< int > changedFieldIndexes;
|
|
changedFieldIndexes.reserve( attr.size() );
|
|
#endif
|
|
|
|
for ( QgsAttributeMap::const_iterator it2 = attr.begin(); it2 != attr.end(); ++it2 )
|
|
{
|
|
int f = it2.key();
|
|
if ( it2->userType() == qMetaTypeId< QgsUnsetAttributeValue >() )
|
|
continue;
|
|
|
|
if ( mFirstFieldIsFid )
|
|
{
|
|
if ( f == 0 )
|
|
{
|
|
if ( it2->toLongLong() != fid )
|
|
{
|
|
pushError( tr( "Changing feature id of feature %1 is not allowed." ).arg( fid ) );
|
|
returnValue = false;
|
|
}
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
--f;
|
|
}
|
|
}
|
|
|
|
OGRFieldDefnH fd = OGR_F_GetFieldDefnRef( of.get(), f );
|
|
if ( !fd )
|
|
{
|
|
pushError( tr( "Field %1 of feature %2 doesn't exist." ).arg( f ).arg( fid ) );
|
|
returnValue = false;
|
|
continue;
|
|
}
|
|
|
|
OGRFieldType type = OGR_Fld_GetType( fd );
|
|
QMetaType::Type qType = static_cast<QMetaType::Type>( it2->userType() );
|
|
if ( QgsVariantUtils::isNull( *it2 ) || ( type != OFTString && ( ( qType != QMetaType::Type::QVariantList && qType != QMetaType::Type::QStringList && it2->toString().isEmpty() ) || ( qType == QMetaType::Type::QVariantList && it2->toList().empty() ) || ( qType == QMetaType::Type::QStringList && it2->toStringList().empty() ) ) ) )
|
|
{
|
|
// Starting with GDAL 2.2, there are 2 concepts: unset fields and null fields
|
|
// whereas previously there was only unset fields. For a GeoJSON output,
|
|
// leaving a field unset will cause it to not appear at all in the output
|
|
// feature.
|
|
// When all features of a layer have a field unset, this would cause the
|
|
// field to not be present at all in the output, and thus on reading to
|
|
// have disappeared. #16812
|
|
#ifdef OGRNullMarker
|
|
OGR_F_SetFieldNull( of.get(), f );
|
|
#else
|
|
OGR_F_UnsetField( of.get(), f );
|
|
#endif
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
bool errorEmitted = false;
|
|
bool ok = false;
|
|
switch ( type )
|
|
{
|
|
case OFTInteger:
|
|
{
|
|
if ( OGR_Fld_GetSubType( fd ) == OFSTBoolean && qType == QMetaType::Type::QString )
|
|
{
|
|
// compatibility with use case of https://github.com/qgis/QGIS/issues/55517
|
|
const QString strVal = it2->toString();
|
|
OGR_F_SetFieldInteger( of.get(), f, stringToBool( strVal, &ok ) );
|
|
if ( !ok )
|
|
{
|
|
pushError( tr( "wrong value for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( strVal ) );
|
|
errorEmitted = true;
|
|
}
|
|
else
|
|
{
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
OGR_F_SetFieldInteger( of.get(), f, strictToInt( *it2, &ok ) );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
case OFTInteger64:
|
|
OGR_F_SetFieldInteger64( of.get(), f, it2->toLongLong( &ok ) );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
break;
|
|
case OFTReal:
|
|
OGR_F_SetFieldDouble( of.get(), f, it2->toDouble( &ok ) );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
break;
|
|
case OFTDate:
|
|
{
|
|
const QDate date = it2->toDate();
|
|
if ( date.isValid() )
|
|
{
|
|
ok = true;
|
|
OGR_F_SetFieldDateTime( of.get(), f,
|
|
date.year(),
|
|
date.month(),
|
|
date.day(),
|
|
0, 0, 0,
|
|
0 );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
case OFTTime:
|
|
{
|
|
const QTime time = it2->toTime();
|
|
if ( time.isValid() )
|
|
{
|
|
ok = true;
|
|
OGR_F_SetFieldDateTimeEx( of.get(), f,
|
|
0, 0, 0,
|
|
time.hour(),
|
|
time.minute(),
|
|
static_cast<float>( time.second() + static_cast< double >( time.msec() ) / 1000 ),
|
|
0 );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
case OFTDateTime:
|
|
{
|
|
QDateTime dt = it2->toDateTime();
|
|
if ( dt.isValid() )
|
|
{
|
|
ok = true;
|
|
if ( mConvertLocalTimeToUTC && dt.timeSpec() == Qt::LocalTime )
|
|
dt = dt.toUTC();
|
|
const QDate date = dt.date();
|
|
const QTime time = dt.time();
|
|
OGR_F_SetFieldDateTimeEx( of.get(), f,
|
|
date.year(),
|
|
date.month(),
|
|
date.day(),
|
|
time.hour(),
|
|
time.minute(),
|
|
static_cast<float>( time.second() + static_cast< double >( time.msec() ) / 1000 ),
|
|
QgsOgrUtils::OGRTZFlagFromQt( dt ) );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
case OFTString:
|
|
{
|
|
ok = true;
|
|
QString stringValue;
|
|
if ( OGR_Fld_GetSubType( fd ) == OFSTJSON )
|
|
{
|
|
stringValue = QString::fromStdString( QgsJsonUtils::jsonFromVariant( it2.value() ).dump() );
|
|
}
|
|
else
|
|
{
|
|
stringValue = jsonStringValue( it2.value() );
|
|
}
|
|
OGR_F_SetFieldString( of.get(), f, textEncoding()->fromUnicode( stringValue ).constData() );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
case OFTBinary:
|
|
{
|
|
ok = true;
|
|
const QByteArray ba = it2->toByteArray();
|
|
OGR_F_SetFieldBinary( of.get(), f, ba.size(), const_cast< GByte * >( reinterpret_cast< const GByte * >( ba.data() ) ) );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
break;
|
|
}
|
|
|
|
case OFTStringList:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
ok = true;
|
|
QStringList list = it2->toStringList();
|
|
int count = list.count();
|
|
char **lst = static_cast<char **>( CPLMalloc( ( count + 1 ) * sizeof( char * ) ) );
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QString &string : list )
|
|
{
|
|
lst[pos] = CPLStrdup( textEncoding()->fromUnicode( string ).data() );
|
|
pos++;
|
|
}
|
|
}
|
|
lst[count] = nullptr;
|
|
OGR_F_SetFieldStringList( of.get(), f, lst );
|
|
CSLDestroy( lst );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTIntegerList:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
ok = true;
|
|
const QVariantList list = it2->toList();
|
|
const int count = list.count();
|
|
int *lst = new int[count];
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QVariant &value : list )
|
|
{
|
|
lst[pos] = strictToInt( value, &ok );
|
|
if ( !ok )
|
|
break;
|
|
pos++;
|
|
}
|
|
}
|
|
if ( ok )
|
|
{
|
|
OGR_F_SetFieldIntegerList( of.get(), f, count, lst );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
delete [] lst;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTRealList:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
ok = true;
|
|
const QVariantList list = it2->toList();
|
|
const int count = list.count();
|
|
double *lst = new double[count];
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QVariant &value : list )
|
|
{
|
|
lst[pos] = value.toDouble( &ok );
|
|
if ( !ok )
|
|
break;
|
|
pos++;
|
|
}
|
|
}
|
|
if ( ok )
|
|
{
|
|
OGR_F_SetFieldDoubleList( of.get(), f, count, lst );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
delete [] lst;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case OFTInteger64List:
|
|
{
|
|
if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList )
|
|
{
|
|
ok = true;
|
|
const QVariantList list = it2->toList();
|
|
const int count = list.count();
|
|
long long *lst = new long long[count];
|
|
if ( count > 0 )
|
|
{
|
|
int pos = 0;
|
|
for ( const QVariant &value : list )
|
|
{
|
|
lst[pos] = value.toLongLong( &ok );
|
|
if ( !ok )
|
|
break;
|
|
pos++;
|
|
}
|
|
}
|
|
if ( ok )
|
|
{
|
|
OGR_F_SetFieldInteger64List( of.get(), f, count, lst );
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
changedFieldIndexes << f;
|
|
#endif
|
|
}
|
|
delete [] lst;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:
|
|
pushError( tr( "Type %1 of attribute %2 of feature %3 unknown." ).arg( type ).arg( it2.key() ).arg( fid ) );
|
|
returnValue = false;
|
|
break;
|
|
}
|
|
|
|
if ( !ok )
|
|
{
|
|
if ( !errorEmitted )
|
|
{
|
|
pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( it2->typeName() ) );
|
|
}
|
|
returnValue = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
const OGRErr updateResult = useUpdateFeature ? mOgrLayer->UpdateFeature( of.get(), changedFieldIndexes.size(), changedFieldIndexes.constData(), 0, nullptr, false )
|
|
: mOgrLayer->SetFeature( of.get() );
|
|
if ( updateResult != OGRERR_NONE )
|
|
#else
|
|
if ( mOgrLayer->SetFeature( of.get() ) != OGRERR_NONE )
|
|
#endif
|
|
{
|
|
pushError( tr( "OGR error setting feature %1: %2" ).arg( fid ).arg( CPLGetLastErrorMsg() ) );
|
|
returnValue = false;
|
|
}
|
|
}
|
|
|
|
if ( inTransaction )
|
|
{
|
|
if ( returnValue )
|
|
returnValue = commitTransaction();
|
|
else
|
|
rollbackTransaction();
|
|
}
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
if ( !syncToDisc() )
|
|
{
|
|
pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) );
|
|
}
|
|
return returnValue;
|
|
}
|
|
|
|
bool QgsOgrProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
setRelevantFields( true, attributeIndexes() );
|
|
|
|
const bool inTransaction = startTransaction();
|
|
|
|
// Some drivers may need to call ResetReading() after GetFeature(), such
|
|
// as GPKG in GDAL < 2.3.0 to avoid letting the database in a locked state.
|
|
// But this is undesirable in general, so don't do this when we know that
|
|
// we don't need to.
|
|
bool mayNeedResetReadingAfterGetFeature = true;
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
mayNeedResetReadingAfterGetFeature = false;
|
|
}
|
|
else if ( mGDALDriverName == QLatin1String( "GPKG" ) )
|
|
{
|
|
mayNeedResetReadingAfterGetFeature = false;
|
|
}
|
|
|
|
QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn();
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,1)
|
|
const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature );
|
|
#elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
// see https://github.com/OSGeo/gdal/pull/10197
|
|
const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature )
|
|
&& mGDALDriverName != QLatin1String( "ODS" )
|
|
&& mGDALDriverName != QLatin1String( "XLSX" )
|
|
&& mGDALDriverName != QLatin1String( "GeoJSON" );
|
|
#else
|
|
constexpr bool useUpdateFeature = false;
|
|
#endif
|
|
|
|
bool returnvalue = true;
|
|
for ( QgsGeometryMap::const_iterator it = geometry_map.constBegin(); it != geometry_map.constEnd(); ++it )
|
|
{
|
|
gdal::ogr_feature_unique_ptr theOGRFeature;
|
|
if ( useUpdateFeature )
|
|
{
|
|
theOGRFeature.reset( featureDefinition.CreateFeature() );
|
|
OGR_F_SetFID( theOGRFeature.get(), FID_TO_NUMBER( it.key() ) );
|
|
}
|
|
else
|
|
{
|
|
theOGRFeature.reset( mOgrLayer->GetFeature( FID_TO_NUMBER( it.key() ) ) );
|
|
if ( !theOGRFeature )
|
|
{
|
|
pushError( tr( "OGR error changing geometry: feature %1 not found" ).arg( it.key() ) );
|
|
returnvalue = false;
|
|
continue;
|
|
}
|
|
|
|
if ( mayNeedResetReadingAfterGetFeature )
|
|
{
|
|
mOgrLayer->ResetReading(); // needed for SQLite-based to clear iterator, which could let the database in a locked state otherwise
|
|
}
|
|
}
|
|
|
|
OGRGeometryH newGeometry = nullptr;
|
|
QByteArray wkb = it->asWkb();
|
|
// We might receive null geometries. It is OK, but don't go through the
|
|
// OGR_G_CreateFromWkb() route then
|
|
if ( !wkb.isEmpty() )
|
|
{
|
|
//create an OGRGeometry
|
|
if ( OGR_G_CreateFromWkb( reinterpret_cast<unsigned char *>( const_cast<char *>( wkb.constData() ) ),
|
|
mOgrLayer->GetSpatialRef(),
|
|
&newGeometry,
|
|
wkb.length() ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error creating geometry for feature %1: %2" ).arg( it.key() ).arg( CPLGetLastErrorMsg() ) );
|
|
OGR_G_DestroyGeometry( newGeometry );
|
|
returnvalue = false;
|
|
continue;
|
|
}
|
|
|
|
if ( !newGeometry )
|
|
{
|
|
pushError( tr( "OGR error in feature %1: geometry is null" ).arg( it.key() ) );
|
|
returnvalue = false;
|
|
continue;
|
|
}
|
|
|
|
newGeometry = ConvertGeometryIfNecessary( newGeometry );
|
|
}
|
|
|
|
//set the new geometry
|
|
if ( OGR_F_SetGeometryDirectly( theOGRFeature.get(), newGeometry ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error setting geometry of feature %1: %2" ).arg( it.key() ).arg( CPLGetLastErrorMsg() ) );
|
|
// Shouldn't happen normally. If it happens, ownership of the geometry
|
|
// may be not really well defined, so better not destroy it, but just
|
|
// the feature.
|
|
returnvalue = false;
|
|
continue;
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
constexpr int firstIndex {0};
|
|
const OGRErr updateResult = useUpdateFeature ? mOgrLayer->UpdateFeature( theOGRFeature.get(), 0, nullptr, 1, &firstIndex, false )
|
|
: mOgrLayer->SetFeature( theOGRFeature.get() );
|
|
if ( updateResult != OGRERR_NONE )
|
|
#else
|
|
if ( mOgrLayer->SetFeature( theOGRFeature.get() ) != OGRERR_NONE )
|
|
#endif
|
|
{
|
|
pushError( tr( "OGR error setting feature %1: %2" ).arg( it.key() ).arg( CPLGetLastErrorMsg() ) );
|
|
returnvalue = false;
|
|
continue;
|
|
}
|
|
mShapefileMayBeCorrupted = true;
|
|
|
|
invalidateCachedExtent( true );
|
|
}
|
|
|
|
if ( inTransaction )
|
|
{
|
|
if ( returnvalue )
|
|
returnvalue = commitTransaction();
|
|
else
|
|
rollbackTransaction();
|
|
}
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
if ( !syncToDisc() )
|
|
{
|
|
pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) );
|
|
}
|
|
return returnvalue;
|
|
}
|
|
|
|
bool QgsOgrProvider::createSpatialIndexImpl()
|
|
{
|
|
QByteArray layerName = mOgrOrigLayer->name();
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
QByteArray sql = QByteArray( "CREATE SPATIAL INDEX ON " ) + quotedIdentifier( layerName ); // quote the layer name so spaces are handled
|
|
QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 );
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( sql );
|
|
|
|
if ( !mFilePath.endsWith( QLatin1String( ".shp" ), Qt::CaseInsensitive ) )
|
|
return true;
|
|
|
|
QFileInfo fi( mFilePath ); // to get the base name
|
|
//find out, if the .qix file is there
|
|
mShapefileHadSpatialIndex = QFileInfo::exists( fi.path().append( '/' ).append( fi.completeBaseName() ).append( ".qix" ) );
|
|
return mShapefileHadSpatialIndex;
|
|
}
|
|
else if ( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "SQLite" ) )
|
|
{
|
|
QRecursiveMutex *mutex = nullptr;
|
|
OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex );
|
|
QByteArray sql = QByteArray( "SELECT CreateSpatialIndex(" + quotedIdentifier( layerName ) + ","
|
|
+ quotedIdentifier( OGR_L_GetGeometryColumn( layer ) ) + ") " ); // quote the layer name so spaces are handled
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( sql );
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
bool QgsOgrProvider::createSpatialIndex()
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !mOgrOrigLayer )
|
|
return false;
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
return createSpatialIndexImpl();
|
|
|
|
}
|
|
|
|
QString QgsOgrProvider::createIndexName( QString tableName, QString field )
|
|
{
|
|
const thread_local QRegularExpression safeExp( QStringLiteral( "[^a-zA-Z0-9]" ) );
|
|
tableName.replace( safeExp, QStringLiteral( "_" ) );
|
|
field.replace( safeExp, QStringLiteral( "_" ) );
|
|
return tableName + "_" + field + "_idx";
|
|
}
|
|
|
|
bool QgsOgrProvider::createAttributeIndex( int field )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( field < 0 || field >= fields().count() )
|
|
return false;
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
QByteArray quotedLayerName = quotedIdentifier( mOgrOrigLayer->name() );
|
|
if ( mGDALDriverName == QLatin1String( "GPKG" ) ||
|
|
mGDALDriverName == QLatin1String( "SQLite" ) )
|
|
{
|
|
if ( field == 0 && mFirstFieldIsFid )
|
|
{
|
|
// already an index on this field, no need to re-created
|
|
return false;
|
|
}
|
|
|
|
QString indexName = createIndexName( mOgrOrigLayer->name(), fields().at( field ).name() );
|
|
QByteArray createSql = "CREATE INDEX IF NOT EXISTS " + textEncoding()->fromUnicode( indexName ) + " ON " + quotedLayerName + " (" + textEncoding()->fromUnicode( fields().at( field ).name() ) + ")";
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( createSql );
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
QByteArray dropSql = "DROP INDEX ON " + quotedLayerName;
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( dropSql );
|
|
QByteArray createSql = "CREATE INDEX ON " + quotedLayerName + " USING " + textEncoding()->fromUnicode( fields().at( field ).name() );
|
|
mOgrOrigLayer->ExecuteSQLNoReturn( createSql );
|
|
|
|
QFileInfo fi( mFilePath ); // to get the base name
|
|
//find out, if the .idm/.ind file is there
|
|
QString idmFile( fi.path().append( '/' ).append( fi.completeBaseName() ).append( ".idm" ) );
|
|
QString indFile( fi.path().append( '/' ).append( fi.completeBaseName() ).append( ".ind" ) );
|
|
return QFile::exists( idmFile ) || QFile::exists( indFile );
|
|
}
|
|
}
|
|
|
|
bool QgsOgrProvider::deleteFeatures( const QgsFeatureIds &id )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
const bool inTransaction = startTransaction();
|
|
|
|
bool returnvalue = true;
|
|
for ( QgsFeatureIds::const_iterator it = id.begin(); it != id.end(); ++it )
|
|
{
|
|
if ( !deleteFeature( *it ) )
|
|
{
|
|
returnvalue = false;
|
|
}
|
|
}
|
|
|
|
if ( inTransaction )
|
|
{
|
|
if ( returnvalue )
|
|
returnvalue = commitTransaction();
|
|
else
|
|
rollbackTransaction();
|
|
}
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
if ( !syncToDisc() )
|
|
{
|
|
returnvalue = false;
|
|
}
|
|
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
// Shapefile behaves in a special way due to possible recompaction
|
|
recalculateFeatureCount();
|
|
}
|
|
else
|
|
{
|
|
if ( mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) &&
|
|
mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::UnknownCount ) )
|
|
{
|
|
if ( returnvalue )
|
|
mFeaturesCounted -= id.size();
|
|
else
|
|
recalculateFeatureCount();
|
|
}
|
|
}
|
|
|
|
clearMinMaxCache();
|
|
|
|
invalidateCachedExtent( true );
|
|
|
|
return returnvalue;
|
|
}
|
|
|
|
bool QgsOgrProvider::deleteFeature( QgsFeatureId id )
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( !doInitialActionsForEdition() )
|
|
return false;
|
|
|
|
if ( mOgrLayer->DeleteFeature( FID_TO_NUMBER( id ) ) != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error deleting feature %1: %2" ).arg( id ).arg( CPLGetLastErrorMsg() ) );
|
|
return false;
|
|
}
|
|
|
|
if ( mTransaction )
|
|
mTransaction->dirtyLastSavePoint();
|
|
|
|
mShapefileMayBeCorrupted = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool QgsOgrProvider::doInitialActionsForEdition()
|
|
{
|
|
if ( !mValid )
|
|
return false;
|
|
|
|
// If mUpdateModeStackDepth > 0, it means that an updateMode is already active and that we have write access
|
|
if ( mUpdateModeStackDepth == 0 )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Enter update mode implicitly" ), 2 );
|
|
if ( !_enterUpdateMode( true ) )
|
|
return false;
|
|
}
|
|
|
|
mShapefileHadSpatialIndex = ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) && ( hasSpatialIndex() == Qgis::SpatialIndexPresence::Present ) );
|
|
|
|
return true;
|
|
}
|
|
|
|
Qgis::VectorProviderCapabilities QgsOgrProvider::capabilities() const
|
|
{
|
|
return mCapabilities;
|
|
}
|
|
|
|
Qgis::VectorDataProviderAttributeEditCapabilities QgsOgrProvider::attributeEditCapabilities() const
|
|
{
|
|
return mAttributeEditCapabilities;
|
|
}
|
|
|
|
QgsAttributeList QgsOgrProvider::pkAttributeIndexes() const
|
|
{
|
|
return mPrimaryKeyAttrs;
|
|
}
|
|
|
|
QString QgsOgrProvider::geometryColumnName() const
|
|
{
|
|
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
|
|
|
|
if ( !mOgrLayer )
|
|
return QString();
|
|
|
|
QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn();
|
|
if ( featureDefinition.GetGeomFieldCount() )
|
|
{
|
|
OGRGeomFieldDefnH geomH = featureDefinition.GetGeomFieldDefn( 0 );
|
|
return QString::fromUtf8( OGR_GFld_GetNameRef( geomH ) );
|
|
}
|
|
return QString();
|
|
}
|
|
|
|
void QgsOgrProvider::computeCapabilities()
|
|
{
|
|
Qgis::VectorProviderCapabilities ability;
|
|
bool updateModeActivated = false;
|
|
|
|
// collect abilities reported by OGR
|
|
if ( mOgrLayer )
|
|
{
|
|
|
|
// We want the layer in rw mode or capabilities will be wrong
|
|
// If mUpdateModeStackDepth > 0, it means that an updateMode is already active and that we have write access
|
|
if ( mUpdateModeStackDepth == 0 )
|
|
{
|
|
updateModeActivated = _enterUpdateMode( true );
|
|
}
|
|
|
|
// Whilst the OGR documentation (e.g. at
|
|
// https://gdal.org/doxygen/classOGRLayer.html#aeedbda1a62f9b89b8e5f24332cf22286) states "The capability
|
|
// codes that can be tested are represented as strings, but #defined
|
|
// constants exists to ensure correct spelling", we always use strings
|
|
// here. This is because older versions of OGR don't always have all
|
|
// the #defines we want to test for here.
|
|
|
|
if ( mOgrLayer->TestCapability( "RandomRead" ) )
|
|
// true if the GetFeature() method works *efficiently* for this layer.
|
|
// TODO: Perhaps influence if QGIS caches into memory
|
|
// (vs read from disk every time) based on this setting.
|
|
{
|
|
// the latter flag is here just for compatibility
|
|
ability |= Qgis::VectorProviderCapability::SelectAtId;
|
|
}
|
|
|
|
if ( mWriteAccessPossible && mOgrLayer->TestCapability( "SequentialWrite" ) )
|
|
// true if the CreateFeature() method works for this layer.
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::AddFeatures;
|
|
}
|
|
|
|
if ( mWriteAccessPossible && mOgrLayer->TestCapability( "DeleteFeature" ) )
|
|
// true if this layer can delete its features
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::DeleteFeatures;
|
|
}
|
|
|
|
if ( mWriteAccessPossible && mOgrLayer->TestCapability( "RandomWrite" ) )
|
|
// true if the SetFeature() method is operational on this layer.
|
|
{
|
|
// TODO According to http://shapelib.maptools.org/ (Shapefile C Library V1.2)
|
|
// TODO "You can't modify the vertices of existing structures".
|
|
// TODO Need to work out versions of shapelib vs versions of GDAL/OGR
|
|
// TODO And test appropriately.
|
|
|
|
ability |= Qgis::VectorProviderCapability::ChangeAttributeValues;
|
|
ability |= Qgis::VectorProviderCapability::ChangeGeometries;
|
|
}
|
|
|
|
#if 0
|
|
if ( mOgrLayer->TestCapability( "FastFeatureCount" ) )
|
|
// true if this layer can return a feature count
|
|
// (via OGRLayer::GetFeatureCount()) efficiently ... ie. without counting
|
|
// the features. In some cases this will return true until a spatial
|
|
// filter is installed after which it will return false.
|
|
{
|
|
// TODO: Perhaps use as a clue by QGIS whether it should spawn a thread to count features.
|
|
}
|
|
|
|
if ( mOgrLayer->TestCapability( "FastSetNextByIndex" ) )
|
|
// true if this layer can perform the SetNextByIndex() call efficiently.
|
|
{
|
|
// No use required for this QGIS release.
|
|
}
|
|
#endif
|
|
|
|
if ( mWriteAccessPossible && mOgrLayer->TestCapability( "CreateField" ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::AddAttributes;
|
|
}
|
|
|
|
if ( mWriteAccessPossible && mOgrLayer->TestCapability( "DeleteField" ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::DeleteAttributes;
|
|
}
|
|
|
|
if ( mWriteAccessPossible && mOgrLayer->TestCapability( "AlterFieldDefn" ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::RenameAttributes;
|
|
}
|
|
|
|
if ( !mOgrLayer->TestCapability( OLCStringsAsUTF8 ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::SelectEncoding;
|
|
}
|
|
|
|
// OGR doesn't handle shapefiles without attributes, ie. missing DBFs well, fixes #803
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::CreateSpatialIndex;
|
|
ability |= Qgis::VectorProviderCapability::CreateAttributeIndex;
|
|
|
|
if ( ( ability & Qgis::VectorProviderCapability::ChangeAttributeValues ) == 0 )
|
|
{
|
|
// on readonly shapes OGR reports that it can delete features although it can't RandomWrite
|
|
ability.setFlag( Qgis::VectorProviderCapability::AddAttributes, false );
|
|
ability.setFlag( Qgis::VectorProviderCapability::DeleteFeatures, false );
|
|
}
|
|
}
|
|
else if ( mGDALDriverName == QLatin1String( "GPKG" ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::CreateSpatialIndex;
|
|
ability |= Qgis::VectorProviderCapability::CreateAttributeIndex;
|
|
}
|
|
else if ( mGDALDriverName == QLatin1String( "SQLite" ) )
|
|
{
|
|
// Spatial index can only be created on Spatialite enabled datasources.
|
|
QString sql = QStringLiteral( "SELECT 1 FROM sqlite_master WHERE name='spatialite_history' AND type='table'" );
|
|
bool isSpatialite = false;
|
|
if ( QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql.toLocal8Bit().constData() ) )
|
|
{
|
|
gdal::ogr_feature_unique_ptr f( l->GetNextFeature() );
|
|
if ( f )
|
|
{
|
|
isSpatialite = true;
|
|
}
|
|
}
|
|
|
|
if ( isSpatialite )
|
|
ability |= Qgis::VectorProviderCapability::CreateSpatialIndex;
|
|
ability |= Qgis::VectorProviderCapability::CreateAttributeIndex;
|
|
}
|
|
|
|
/* Curve geometries are available in some drivers starting with GDAL 2.0 */
|
|
if ( mOgrLayer->TestCapability( "CurveGeometries" ) )
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::CircularGeometries;
|
|
}
|
|
|
|
if ( mGDALDriverName == QLatin1String( "GPKG" ) )
|
|
{
|
|
//supports transactions
|
|
ability |= Qgis::VectorProviderCapability::TransactionSupport;
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
if ( GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DCAP_FEATURE_STYLES_READ, nullptr ) )
|
|
#else
|
|
// GDAL KML driver doesn't support reading feature style, skip metadata check until GDAL can separate reading/writing capability
|
|
if ( mGDALDriverName != QLatin1String( "KML" ) && GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DCAP_FEATURE_STYLES, nullptr ) )
|
|
#endif
|
|
{
|
|
ability |= Qgis::VectorProviderCapability::FeatureSymbology;
|
|
ability |= Qgis::VectorProviderCapability::CreateRenderer;
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0)
|
|
if ( const char *pszAlterFieldDefnFlags = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_CREATION_FIELD_DEFN_FLAGS, nullptr ) )
|
|
{
|
|
char **papszTokens = CSLTokenizeString2( pszAlterFieldDefnFlags, " ", 0 );
|
|
if ( CSLFindString( papszTokens, "AlternativeName" ) >= 0 )
|
|
{
|
|
mAttributeEditCapabilities |= Qgis::VectorDataProviderAttributeEditCapability::EditAlias;
|
|
}
|
|
if ( CSLFindString( papszTokens, "Comment" ) >= 0 )
|
|
{
|
|
mAttributeEditCapabilities |= Qgis::VectorDataProviderAttributeEditCapability::EditComment;
|
|
}
|
|
CSLDestroy( papszTokens );
|
|
}
|
|
#endif
|
|
}
|
|
|
|
ability |= Qgis::VectorProviderCapability::ReadLayerMetadata;
|
|
ability |= Qgis::VectorProviderCapability::ReloadData;
|
|
|
|
if ( updateModeActivated )
|
|
leaveUpdateMode();
|
|
|
|
mCapabilities = ability;
|
|
}
|
|
|
|
|
|
QString QgsOgrProvider::name() const
|
|
{
|
|
return TEXT_PROVIDER_KEY;
|
|
}
|
|
|
|
QString QgsOgrProvider::providerKey()
|
|
{
|
|
return TEXT_PROVIDER_KEY;
|
|
}
|
|
|
|
QString QgsOgrProvider::description() const
|
|
{
|
|
return TEXT_PROVIDER_DESCRIPTION;
|
|
}
|
|
|
|
QgsCoordinateReferenceSystem QgsOgrProvider::crs() const
|
|
{
|
|
QgsCoordinateReferenceSystem srs;
|
|
if ( !mValid || ( mOGRGeomType == wkbNone ) )
|
|
return srs;
|
|
|
|
if ( OGRSpatialReferenceH spatialRefSys = mOgrLayer->GetSpatialRef() )
|
|
{
|
|
srs = QgsOgrUtils::OGRSpatialReferenceToCrs( spatialRefSys );
|
|
}
|
|
else
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "no spatial reference found" ), 2 );
|
|
}
|
|
|
|
return srs;
|
|
}
|
|
|
|
QString QgsOgrProvider::dataComment() const
|
|
{
|
|
if ( !mOgrLayer )
|
|
return QString();
|
|
// Potentially set on File Geodatabase
|
|
return mOgrLayer->GetMetadataItem( QStringLiteral( "ALIAS_NAME" ) );
|
|
}
|
|
|
|
QSet<QVariant> QgsOgrProvider::uniqueValues( int index, int limit ) const
|
|
{
|
|
QSet<QVariant> uniqueValues;
|
|
|
|
if ( !mValid || index < 0 || index >= fields().count() )
|
|
return uniqueValues;
|
|
|
|
const QgsField fld = fields().at( index );
|
|
if ( fld.name().isNull() )
|
|
{
|
|
return uniqueValues; //not a provider field
|
|
}
|
|
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
QByteArray sql = "SELECT DISTINCT " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) );
|
|
|
|
// GPKG/SQLite fid
|
|
// For GPKG and SQLITE drivers PK fields are not exposed as real fields, (and OGR_F_GetFID only
|
|
// works with GPKG), so we are adding an extra column that will become index 0
|
|
// See https://github.com/qgis/QGIS/issues/29129
|
|
if ( ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) )
|
|
&& mFirstFieldIsFid && index == 0 )
|
|
{
|
|
sql += ", " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ) + " AS fid2";
|
|
}
|
|
|
|
sql += " FROM " + quotedIdentifier( mOgrLayer->name() );
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
{
|
|
sql += " WHERE " + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) );
|
|
}
|
|
|
|
sql += " ORDER BY " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ) + " ASC";
|
|
|
|
if ( limit > 0 )
|
|
{
|
|
sql += " LIMIT " + QString::number( limit ).toLocal8Bit();
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ), 2 );
|
|
QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql );
|
|
if ( !l )
|
|
{
|
|
QgsDebugError( QStringLiteral( "Failed to execute SQL" ) );
|
|
return QgsVectorDataProvider::uniqueValues( index, limit );
|
|
}
|
|
|
|
gdal::ogr_feature_unique_ptr f;
|
|
bool ok = false;
|
|
while ( f.reset( l->GetNextFeature() ), f )
|
|
{
|
|
const QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fld, 0, textEncoding(), &ok );
|
|
if ( ok )
|
|
uniqueValues << res;
|
|
|
|
if ( limit >= 0 && uniqueValues.size() >= limit )
|
|
break;
|
|
}
|
|
|
|
return uniqueValues;
|
|
}
|
|
|
|
QStringList QgsOgrProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const
|
|
{
|
|
QStringList results;
|
|
|
|
if ( !mValid || index < 0 || index >= fields().count() )
|
|
return results;
|
|
|
|
QgsField fld = fields().at( index );
|
|
if ( fld.name().isNull() )
|
|
{
|
|
return results; //not a provider field
|
|
}
|
|
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
// uniqueStringsMatching() is supposed to be case insensitive, so use the
|
|
// ILIKE operator when it is available.
|
|
// Prior to GDAL 3.1, with OGR SQL, LIKE behaved like ILIKE
|
|
bool supportsILIKE = false;
|
|
{
|
|
QByteArray sql = "SELECT 1 FROM ";
|
|
sql += quotedIdentifier( mOgrLayer->name() );
|
|
sql += " WHERE 'a' ILIKE 'A' LIMIT 1";
|
|
QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql );
|
|
if ( l )
|
|
{
|
|
gdal::ogr_feature_unique_ptr f;
|
|
f.reset( l->GetNextFeature() );
|
|
supportsILIKE = static_cast< bool>( f );
|
|
}
|
|
}
|
|
|
|
QByteArray sql = "SELECT DISTINCT " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) );
|
|
sql += " FROM " + quotedIdentifier( mOgrLayer->name() );
|
|
|
|
sql += " WHERE " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) );
|
|
if ( supportsILIKE )
|
|
sql += " ILIKE '%";
|
|
else
|
|
sql += " LIKE '%";
|
|
sql += textEncoding()->fromUnicode( substring ) + "%'";
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
{
|
|
sql += " AND (" + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ) + ')';
|
|
}
|
|
|
|
sql += " ORDER BY " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ) + " ASC";
|
|
|
|
if ( limit > 0 )
|
|
{
|
|
sql += " LIMIT " + QString::number( limit ).toLocal8Bit();
|
|
}
|
|
|
|
QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ), 2 );
|
|
QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql );
|
|
if ( !l )
|
|
{
|
|
QgsDebugError( QStringLiteral( "Failed to execute SQL" ) );
|
|
return QgsVectorDataProvider::uniqueStringsMatching( index, substring, limit, feedback );
|
|
}
|
|
|
|
gdal::ogr_feature_unique_ptr f;
|
|
while ( f.reset( l->GetNextFeature() ), f )
|
|
{
|
|
if ( OGR_F_IsFieldSetAndNotNull( f.get(), 0 ) )
|
|
results << textEncoding()->toUnicode( OGR_F_GetFieldAsString( f.get(), 0 ) );
|
|
|
|
if ( ( limit >= 0 && results.size() >= limit ) || ( feedback && feedback->isCanceled() ) )
|
|
break;
|
|
}
|
|
|
|
return results;
|
|
}
|
|
|
|
Qgis::SpatialIndexPresence QgsOgrProvider::hasSpatialIndex() const
|
|
{
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
if ( mOgrLayer && mOgrLayer->TestCapability( OLCFastSpatialFilter ) )
|
|
return Qgis::SpatialIndexPresence::Present;
|
|
else if ( mOgrLayer )
|
|
return Qgis::SpatialIndexPresence::NotPresent;
|
|
else
|
|
return Qgis::SpatialIndexPresence::Unknown;
|
|
}
|
|
|
|
Qgis::VectorLayerTypeFlags QgsOgrProvider::vectorLayerTypeFlags() const
|
|
{
|
|
Qgis::VectorLayerTypeFlags flags;
|
|
if ( mValid && mSubsetString.trimmed().startsWith( QStringLiteral( "SELECT" ), Qt::CaseSensitivity::CaseInsensitive ) )
|
|
{
|
|
flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery );
|
|
}
|
|
return flags;
|
|
}
|
|
|
|
QList<QgsRelation> QgsOgrProvider::discoverRelations( const QgsVectorLayer *target, const QList<QgsVectorLayer *> &layers ) const
|
|
{
|
|
if ( !mOgrLayer )
|
|
return {};
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0)
|
|
QList<QgsRelation> output;
|
|
|
|
QRecursiveMutex *mutex = nullptr;
|
|
GDALDatasetH hDS = mOgrLayer->getDatasetHandleAndMutex( mutex );
|
|
QMutexLocker locker( mutex );
|
|
|
|
char **relationNames = GDALDatasetGetRelationshipNames( hDS, nullptr );
|
|
if ( !relationNames )
|
|
return output;
|
|
|
|
const QStringList names = QgsOgrUtils::cStringListToQStringList( relationNames );
|
|
CSLDestroy( relationNames );
|
|
|
|
QgsProviderMetadata *ogrProviderMetadata = QgsProviderRegistry::instance()->providerMetadata( TEXT_PROVIDER_KEY );
|
|
const QVariantMap thisLayerParts = ogrProviderMetadata->decodeUri( target->source() );
|
|
const QString thisPath = thisLayerParts.value( QStringLiteral( "path" ) ).toString();
|
|
|
|
for ( const QString &name : names )
|
|
{
|
|
GDALRelationshipH relationship = GDALDatasetGetRelationship( hDS, name.toUtf8().constData() );
|
|
if ( !relationship )
|
|
continue;
|
|
|
|
const QString leftTableName( GDALRelationshipGetLeftTableName( relationship ) );
|
|
if ( leftTableName != mLayerName )
|
|
continue;
|
|
|
|
const QString rightTableName( GDALRelationshipGetRightTableName( relationship ) );
|
|
const QString mappingTableName( GDALRelationshipGetMappingTableName( relationship ) );
|
|
|
|
// try to find right and mapping tables in list of provided layers
|
|
QgsVectorLayer *rightTableLayer = nullptr;
|
|
QgsVectorLayer *mappingTableLayer = nullptr;
|
|
for ( QgsVectorLayer *layer : layers )
|
|
{
|
|
if ( layer->providerType() != TEXT_PROVIDER_KEY )
|
|
continue;
|
|
|
|
const QVariantMap parts = ogrProviderMetadata->decodeUri( layer->source() );
|
|
if ( parts.value( QStringLiteral( "path" ) ).toString() != thisPath )
|
|
continue;
|
|
|
|
if ( parts.value( QStringLiteral( "layerName" ) ).toString() == rightTableName )
|
|
rightTableLayer = layer;
|
|
|
|
if ( parts.value( QStringLiteral( "layerName" ) ).toString() == mappingTableName )
|
|
mappingTableLayer = layer;
|
|
}
|
|
|
|
if ( !rightTableLayer )
|
|
continue;
|
|
|
|
if ( !mappingTableName.isEmpty() && !mappingTableLayer )
|
|
continue;
|
|
|
|
const QString relationshipName( GDALRelationshipGetName( relationship ) );
|
|
|
|
char **cslLeftTableFieldNames = GDALRelationshipGetLeftTableFields( relationship );
|
|
const QStringList leftTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslLeftTableFieldNames );
|
|
CSLDestroy( cslLeftTableFieldNames );
|
|
|
|
char **cslRightTableFieldNames = GDALRelationshipGetRightTableFields( relationship );
|
|
const QStringList rightTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslRightTableFieldNames );
|
|
CSLDestroy( cslRightTableFieldNames );
|
|
|
|
char **cslLeftMappingTableFieldNames = GDALRelationshipGetLeftMappingTableFields( relationship );
|
|
const QStringList leftMappingTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslLeftMappingTableFieldNames );
|
|
CSLDestroy( cslLeftMappingTableFieldNames );
|
|
|
|
char **cslRightMappingTableFieldNames = GDALRelationshipGetRightMappingTableFields( relationship );
|
|
const QStringList rightMappingTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslRightMappingTableFieldNames );
|
|
CSLDestroy( cslRightMappingTableFieldNames );
|
|
|
|
const GDALRelationshipType relationshipType = GDALRelationshipGetType( relationship );
|
|
Qgis::RelationshipStrength strength = Qgis::RelationshipStrength::Association;
|
|
switch ( relationshipType )
|
|
{
|
|
case GRT_COMPOSITE:
|
|
strength = Qgis::RelationshipStrength::Composition;
|
|
break;
|
|
|
|
case GRT_ASSOCIATION:
|
|
strength = Qgis::RelationshipStrength::Association;
|
|
break;
|
|
|
|
case GRT_AGGREGATION:
|
|
QgsLogger::warning( "Aggregation relationships are not supported" );
|
|
continue;
|
|
}
|
|
|
|
const GDALRelationshipCardinality cardinality = GDALRelationshipGetCardinality( relationship );
|
|
switch ( cardinality )
|
|
{
|
|
case GRC_ONE_TO_ONE:
|
|
case GRC_ONE_TO_MANY:
|
|
case GRC_MANY_TO_ONE:
|
|
{
|
|
QgsRelation relation;
|
|
relation.setName( relationshipName );
|
|
relation.setId( relationshipName );
|
|
relation.setReferencedLayer( target->id() );
|
|
relation.setReferencingLayer( rightTableLayer->id() );
|
|
relation.setStrength( strength );
|
|
for ( int i = 0; i < std::min( leftTableFieldNames.length(), rightTableFieldNames.length() ); ++i )
|
|
{
|
|
relation.addFieldPair( rightTableFieldNames.at( i ), leftTableFieldNames.at( i ) );
|
|
}
|
|
if ( relation.isValid() )
|
|
{
|
|
output.append( relation );
|
|
}
|
|
else
|
|
{
|
|
QgsLogger::warning( "Invalid relation for " + relationshipName );
|
|
}
|
|
break;
|
|
}
|
|
|
|
case GRC_MANY_TO_MANY:
|
|
{
|
|
if ( !mappingTableLayer )
|
|
{
|
|
QgsLogger::warning( "No mapping table for " + relationshipName );
|
|
continue;
|
|
}
|
|
|
|
QgsRelation relation;
|
|
relation.setName( relationshipName );
|
|
relation.setId( relationshipName + "_forward" );
|
|
relation.setReferencedLayer( target->id() );
|
|
relation.setReferencingLayer( mappingTableLayer->id() );
|
|
relation.setStrength( strength );
|
|
for ( int i = 0; i < std::min( leftTableFieldNames.length(), leftMappingTableFieldNames.length() ); ++i )
|
|
{
|
|
relation.addFieldPair( leftMappingTableFieldNames.at( i ), leftTableFieldNames.at( i ) );
|
|
}
|
|
if ( relation.isValid() )
|
|
{
|
|
output.append( relation );
|
|
}
|
|
else
|
|
{
|
|
QgsLogger::warning( "Invalid relation for " + relationshipName );
|
|
continue;
|
|
}
|
|
|
|
QgsRelation relation2;
|
|
relation2.setName( relationshipName );
|
|
relation2.setId( relationshipName + "_backward" );
|
|
relation2.setReferencedLayer( rightTableLayer->id() );
|
|
relation2.setReferencingLayer( mappingTableLayer->id() );
|
|
relation2.setStrength( strength );
|
|
for ( int i = 0; i < std::min( rightTableFieldNames.length(), rightMappingTableFieldNames.length() ); ++i )
|
|
{
|
|
relation2.addFieldPair( rightMappingTableFieldNames.at( i ), rightTableFieldNames.at( i ) );
|
|
}
|
|
if ( relation2.isValid() )
|
|
{
|
|
output.append( relation2 );
|
|
}
|
|
else
|
|
{
|
|
QgsLogger::warning( "Invalid relation for " + relationshipName );
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return output;
|
|
#else
|
|
Q_UNUSED( target )
|
|
Q_UNUSED( layers )
|
|
return {};
|
|
#endif
|
|
}
|
|
|
|
QVariant QgsOgrProvider::minimumValue( int index ) const
|
|
{
|
|
if ( !mValid || index < 0 || index >= fields().count() )
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
const QgsField originalField = fields().at( index );
|
|
QgsField fld = originalField;
|
|
|
|
// can't use native date/datetime types -- OGR converts these to string in the MAX return value
|
|
if ( fld.type() == QMetaType::Type::QDateTime || fld.type() == QMetaType::Type::QDate )
|
|
{
|
|
fld.setType( QMetaType::Type::QString );
|
|
}
|
|
|
|
// Don't quote column name (see https://trac.osgeo.org/gdal/ticket/5799#comment:9)
|
|
QByteArray sql = "SELECT MIN(" + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) );
|
|
sql += ") FROM " + quotedIdentifier( mOgrLayer->name() );
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
{
|
|
sql += " WHERE " + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) );
|
|
}
|
|
|
|
QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql );
|
|
if ( !l )
|
|
{
|
|
QgsDebugError( QStringLiteral( "Failed to execute SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ) );
|
|
return QgsVectorDataProvider::minimumValue( index );
|
|
}
|
|
|
|
gdal::ogr_feature_unique_ptr f( l->GetNextFeature() );
|
|
if ( !f )
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
bool ok = false;
|
|
QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fld, 0, textEncoding(), &ok );
|
|
if ( !ok )
|
|
return QVariant();
|
|
|
|
if ( res.userType() != originalField.type() )
|
|
res = convertValue( originalField.type(), res.toString() );
|
|
|
|
if ( originalField.type() == QMetaType::Type::QDateTime )
|
|
{
|
|
// ensure that we treat times as local time, to match behavior when iterating features
|
|
QDateTime temp = res.toDateTime();
|
|
temp.setTimeSpec( Qt::LocalTime );
|
|
res = temp;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
QVariant QgsOgrProvider::maximumValue( int index ) const
|
|
{
|
|
if ( !mValid || index < 0 || index >= fields().count() )
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg );
|
|
QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) );
|
|
|
|
const QgsField originalField = fields().at( index );
|
|
QgsField fld = originalField;
|
|
|
|
// can't use native date/datetime types -- OGR converts these to string in the MAX return value
|
|
if ( fld.type() == QMetaType::Type::QDateTime || fld.type() == QMetaType::Type::QDate )
|
|
{
|
|
fld.setType( QMetaType::Type::QString );
|
|
}
|
|
|
|
// Don't quote column name (see https://trac.osgeo.org/gdal/ticket/5799#comment:9)
|
|
QByteArray sql = "SELECT MAX(" + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) );
|
|
sql += ") FROM " + quotedIdentifier( mOgrLayer->name() );
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
{
|
|
sql += " WHERE " + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) );
|
|
}
|
|
|
|
QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql );
|
|
if ( !l )
|
|
{
|
|
QgsDebugError( QStringLiteral( "Failed to execute SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ) );
|
|
return QgsVectorDataProvider::maximumValue( index );
|
|
}
|
|
|
|
gdal::ogr_feature_unique_ptr f( l->GetNextFeature() );
|
|
if ( !f )
|
|
{
|
|
return QVariant();
|
|
}
|
|
|
|
bool ok = false;
|
|
QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fld, 0, textEncoding(), &ok );
|
|
if ( !ok )
|
|
return QVariant();
|
|
|
|
if ( res.userType() != originalField.type() )
|
|
res = convertValue( originalField.type(), res.toString() );
|
|
|
|
if ( originalField.type() == QMetaType::Type::QDateTime )
|
|
{
|
|
// ensure that we treat times as local time, to match behavior when iterating features
|
|
QDateTime temp = res.toDateTime();
|
|
temp.setTimeSpec( Qt::LocalTime );
|
|
res = temp;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
QByteArray QgsOgrProvider::quotedIdentifier( const QByteArray &field ) const
|
|
{
|
|
return QgsOgrProviderUtils::quotedIdentifier( field, mGDALDriverName );
|
|
}
|
|
|
|
bool QgsOgrProvider::syncToDisc()
|
|
{
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
|
|
//for shapefiles, remove spatial index files and create a new index
|
|
QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
bool shapeIndex = false;
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
QString sbnIndexFile;
|
|
QFileInfo fi( mFilePath );
|
|
int suffixLength = fi.suffix().length();
|
|
sbnIndexFile = mFilePath;
|
|
sbnIndexFile.chop( suffixLength );
|
|
sbnIndexFile.append( "sbn" );
|
|
|
|
if ( QFile::exists( sbnIndexFile ) )
|
|
{
|
|
shapeIndex = true;
|
|
close();
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
QFile::remove( sbnIndexFile );
|
|
open( OpenModeSameAsCurrent );
|
|
if ( !mValid )
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if ( mOgrLayer->SyncToDisk() != OGRERR_NONE )
|
|
{
|
|
pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) );
|
|
}
|
|
|
|
QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
if ( shapeIndex )
|
|
{
|
|
return createSpatialIndex();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void QgsOgrProvider::recalculateFeatureCount() const
|
|
{
|
|
if ( !mOgrLayer )
|
|
{
|
|
mFeaturesCounted = static_cast< long long >( Qgis::FeatureCountState::Uncounted );
|
|
return;
|
|
}
|
|
|
|
OGRGeometryH filter = mOgrLayer->GetSpatialFilter();
|
|
if ( filter )
|
|
{
|
|
filter = OGR_G_Clone( filter );
|
|
mOgrLayer->SetSpatialFilter( nullptr );
|
|
}
|
|
|
|
// feature count returns number of features within current spatial filter
|
|
// so we remove it if there's any and then put it back
|
|
if ( mOgrGeometryTypeFilter == wkbUnknown || mUniqueGeometryType )
|
|
{
|
|
mFeaturesCounted = mOgrLayer->GetApproxFeatureCount();
|
|
if ( mFeaturesCounted == -1 )
|
|
{
|
|
mFeaturesCounted = static_cast< long long >( Qgis::FeatureCountState::UnknownCount );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mFeaturesCounted = 0;
|
|
setRelevantFields( true, QgsAttributeList() );
|
|
mOgrLayer->ResetReading();
|
|
gdal::ogr_feature_unique_ptr fet;
|
|
const OGRwkbGeometryType flattenGeomTypeFilter =
|
|
QgsOgrProviderUtils::ogrWkbSingleFlattenAndLinear( mOgrGeometryTypeFilter );
|
|
while ( fet.reset( mOgrLayer->GetNextFeature() ), fet )
|
|
{
|
|
OGRGeometryH geom = OGR_F_GetGeometryRef( fet.get() );
|
|
if ( geom )
|
|
{
|
|
OGRwkbGeometryType gType = OGR_G_GetGeometryType( geom );
|
|
gType = QgsOgrProviderUtils::ogrWkbSingleFlattenAndLinear( gType );
|
|
if ( gType == flattenGeomTypeFilter ) mFeaturesCounted++;
|
|
}
|
|
}
|
|
mOgrLayer->ResetReading();
|
|
setRelevantFields( true, attributeIndexes() );
|
|
}
|
|
|
|
if ( filter )
|
|
{
|
|
mOgrLayer->SetSpatialFilter( filter );
|
|
}
|
|
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
}
|
|
|
|
bool QgsOgrProvider::doesStrictFeatureTypeCheck() const
|
|
{
|
|
// FIXME probably other drivers too...
|
|
return mGDALDriverName != QLatin1String( "ESRI Shapefile" ) || ( mOGRGeomType == wkbPoint || mOGRGeomType == wkbPoint25D );
|
|
}
|
|
|
|
QgsFeatureRenderer *QgsOgrProvider::createRenderer( const QVariantMap & ) const
|
|
{
|
|
if ( !( mCapabilities & Qgis::VectorProviderCapability::FeatureSymbology ) )
|
|
return nullptr;
|
|
|
|
std::unique_ptr< QgsSymbol > defaultSymbol( QgsSymbol::defaultSymbol( QgsWkbTypes::geometryType( wkbType() ) ) );
|
|
return new QgsEmbeddedSymbolRenderer( defaultSymbol.release() );
|
|
}
|
|
|
|
void QgsOgrProvider::open( OpenMode mode )
|
|
{
|
|
bool openReadOnly = false;
|
|
Q_ASSERT( !mOgrSqlLayer );
|
|
Q_ASSERT( !mOgrOrigLayer );
|
|
|
|
// Try to open using VSIFileHandler
|
|
// see http://trac.osgeo.org/gdal/wiki/UserDocs/ReadInZip
|
|
const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( dataSourceUri( true ) );
|
|
if ( ( !vsiPrefix.isEmpty() && vsiPrefix != QLatin1String( "/vsimem/" ) ) || mFilePath.startsWith( QLatin1String( "/vsicurl/" ) ) )
|
|
{
|
|
// GDAL>=1.8.0 has write support for zip, but read and write operations
|
|
// cannot be interleaved, so for now just use read-only.
|
|
openReadOnly = true;
|
|
if ( !mFilePath.startsWith( vsiPrefix ) )
|
|
{
|
|
mFilePath = vsiPrefix + mFilePath;
|
|
setDataSourceUri( mFilePath );
|
|
}
|
|
QgsDebugMsgLevel( QStringLiteral( "Trying %1 syntax, mFilePath= %2" ).arg( vsiPrefix, mFilePath ), 2 );
|
|
}
|
|
|
|
QgsDebugMsgLevel( "mFilePath: " + mFilePath, 3 );
|
|
QgsDebugMsgLevel( "mLayerIndex: " + QString::number( mLayerIndex ), 3 );
|
|
QgsDebugMsgLevel( "mLayerName: " + mLayerName, 3 );
|
|
QgsDebugMsgLevel( "mSubsetString: " + mSubsetString, 3 );
|
|
CPLSetConfigOption( "GPX_ELE_AS_25D", "YES" ); // use GPX elevation as z values
|
|
CPLSetConfigOption( "LIBKML_RESOLVE_STYLE", "YES" ); // resolve kml style urls from style tables to feature style strings
|
|
if ( !CPLGetConfigOption( "OSM_USE_CUSTOM_INDEXING", nullptr ) )
|
|
{
|
|
// Disable custom/fast indexing by default, as it can prevent some .osm.pbf
|
|
// files to be loaded.
|
|
// See https://github.com/qgis/QGIS/issues/31062
|
|
CPLSetConfigOption( "OSM_USE_CUSTOM_INDEXING", "NO" );
|
|
}
|
|
|
|
if ( mFilePath.startsWith( QLatin1String( "MySQL:" ) ) && !mLayerName.isEmpty() && !mFilePath.endsWith( ",tables=" + mLayerName ) )
|
|
{
|
|
mFilePath += ",tables=" + mLayerName;
|
|
}
|
|
|
|
if ( mode == OpenModeForceReadOnly )
|
|
openReadOnly = true;
|
|
else if ( mode == OpenModeSameAsCurrent && !mWriteAccess )
|
|
openReadOnly = true;
|
|
|
|
// Do not try to open FileGeodatabase in update mode if not possible (cf https://github.com/OSGeo/gdal/pull/8075)
|
|
const QString fileSuffix( QFileInfo( mFilePath ).suffix() );
|
|
if ( !openReadOnly && fileSuffix.compare( QLatin1String( "gdb" ), Qt::CaseInsensitive ) == 0 )
|
|
{
|
|
QFileInfo fiCatalogTable( mFilePath, "a00000001.gdbtable" );
|
|
if ( fiCatalogTable.exists() && !fiCatalogTable.isWritable() )
|
|
{
|
|
openReadOnly = true;
|
|
}
|
|
}
|
|
|
|
const bool bIsGpkg = fileSuffix.compare( QLatin1String( "gpkg" ), Qt::CaseInsensitive ) == 0;
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,2)
|
|
bool forceWAL = false;
|
|
#endif
|
|
|
|
// first try to open in update mode (unless specified otherwise)
|
|
QString errCause;
|
|
if ( !openReadOnly )
|
|
{
|
|
QStringList options( mOpenOptions );
|
|
if ( !bIsGpkg && ( mode == OpenModeForceUpdateRepackOff || ( mDeferRepack && OpenModeSameAsCurrent ) ) )
|
|
{
|
|
options << "AUTO_REPACK=OFF";
|
|
}
|
|
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,2)
|
|
if ( bIsGpkg && options.contains( "QGIS_FORCE_WAL=ON" ) )
|
|
{
|
|
// Hack use for qgsofflineediting / https://github.com/qgis/QGIS/issues/48012
|
|
forceWAL = true;
|
|
}
|
|
if ( bIsGpkg && mode == OpenModeInitial && !forceWAL && QgsOgrProviderUtils::IsLocalFile( mFilePath ) )
|
|
{
|
|
// A hint to QgsOgrProviderUtils::GDALOpenWrapper() to not force WAL
|
|
// as in OpenModeInitial we are not going to do anything besides getting capabilities
|
|
// and re-opening in readonly mode.
|
|
options << "DO_NOT_ENABLE_WAL=ON";
|
|
}
|
|
#endif
|
|
|
|
// We get the layer which was requested by the uri. The layername
|
|
// has precedence over the layerid if both are given.
|
|
if ( !mLayerName.isNull() )
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, options, mLayerName, errCause, true );
|
|
}
|
|
else
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, options, mLayerIndex, errCause, true );
|
|
}
|
|
}
|
|
|
|
mValid = false;
|
|
if ( mOgrOrigLayer )
|
|
{
|
|
mWriteAccess = true;
|
|
mWriteAccessPossible = true;
|
|
}
|
|
else
|
|
{
|
|
mWriteAccess = false;
|
|
if ( !openReadOnly )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "OGR failed to opened in update mode, trying in read-only mode" ), 2 );
|
|
}
|
|
|
|
QStringList options( mOpenOptions );
|
|
// assume trusted data to get more speed
|
|
if ( mGDALDriverName == QLatin1String( "FlatGeobuf" ) &&
|
|
!options.contains( QStringLiteral( "VERIFY_BUFFERS=YES" ) ) )
|
|
{
|
|
options << QStringLiteral( "VERIFY_BUFFERS=NO" );
|
|
}
|
|
|
|
// try to open read-only
|
|
if ( !mLayerName.isNull() )
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, options, mLayerName, errCause, true );
|
|
}
|
|
else
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, options, mLayerIndex, errCause, true );
|
|
}
|
|
}
|
|
|
|
if ( mOgrOrigLayer )
|
|
{
|
|
mGDALDriverName = mOgrOrigLayer->driverName();
|
|
mShareSameDatasetAmongLayers = QgsOgrProviderUtils::canDriverShareSameDatasetAmongLayers( mGDALDriverName );
|
|
|
|
// Should we set it to true unconditionally? as OGR doesn't do any time
|
|
// zone conversion for local time. For now, only do that for GeoPackage
|
|
// since it requires UTC.
|
|
mConvertLocalTimeToUTC = ( mGDALDriverName == QLatin1String( "GPKG" ) );
|
|
|
|
QgsDebugMsgLevel( "OGR opened using Driver " + mGDALDriverName, 2 );
|
|
|
|
mOgrLayer = mOgrOrigLayer.get();
|
|
|
|
// check that the initial encoding setting is fit for this layer
|
|
|
|
if ( mode == OpenModeInitial && mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
// determine encoding from shapefile cpg or LDID information, if possible
|
|
QString shpEncoding;
|
|
shpEncoding = mOgrLayer->GetMetadataItem( QStringLiteral( "ENCODING_FROM_CPG" ), QStringLiteral( "SHAPEFILE" ) );
|
|
if ( shpEncoding.isEmpty() )
|
|
shpEncoding = mOgrLayer->GetMetadataItem( QStringLiteral( "ENCODING_FROM_LDID" ), QStringLiteral( "SHAPEFILE" ) );
|
|
|
|
if ( !shpEncoding.isEmpty() )
|
|
setEncoding( shpEncoding );
|
|
else
|
|
setEncoding( encoding() );
|
|
}
|
|
else
|
|
{
|
|
setEncoding( encoding() );
|
|
}
|
|
|
|
// Ensure subset is set (setSubsetString does nothing if the passed sql subset string is equal to mSubsetString, which is the case when reloading the dataset)
|
|
QString origSubsetString = mSubsetString;
|
|
mSubsetString.clear();
|
|
// Block signals to avoid endless recursion reloadData -> emit dataChanged -> reloadData
|
|
blockSignals( true );
|
|
|
|
// Do not update capabilities: it will be done later
|
|
|
|
// WARNING if this is the initial open - we don't already have a connection ref, and will be creating one later. So we *mustn't* grab an extra connection ref
|
|
// while setting the subset string, or we'll be left with an extra reference which is never cleared.
|
|
mValid = _setSubsetString( origSubsetString, false, false, mode != OpenModeInitial );
|
|
|
|
blockSignals( false );
|
|
if ( mValid )
|
|
{
|
|
if ( mode == OpenModeInitial )
|
|
{
|
|
computeCapabilities();
|
|
}
|
|
QgsDebugMsgLevel( QStringLiteral( "Data source is valid" ), 2 );
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Data source is invalid (%1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ), tr( "OGR" ) );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( errCause + "(" + QString::fromUtf8( CPLGetLastErrorMsg() ) + ")", tr( "OGR" ) );
|
|
}
|
|
|
|
// For shapefiles or MapInfo .tab, so as to allow concurrent opening between
|
|
// QGIS and MapInfo, we go back to read-only mode for now.
|
|
// For GPKG too, so to open in read-only nolock mode for GDAL >= 3.4.2
|
|
// We limit to those drivers as re-opening is relatively cheap (other drivers
|
|
// like GeoJSON might do full content ingestion for example)
|
|
if ( mValid && mode == OpenModeInitial && mWriteAccess &&
|
|
( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ||
|
|
mGDALDriverName == QLatin1String( "MapInfo File" )
|
|
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,2)
|
|
|| ( !forceWAL && mGDALDriverName == QLatin1String( "GPKG" ) && QgsOgrProviderUtils::IsLocalFile( mFilePath ) )
|
|
#endif
|
|
) )
|
|
{
|
|
mOgrSqlLayer.reset();
|
|
mOgrOrigLayer.reset();
|
|
mOgrLayer = nullptr;
|
|
mValid = false;
|
|
|
|
// In the case where we deal with a shapefile, it is possible that it has
|
|
// pre-existing holes in the DBF (see #15407), so do a packing at the first edit
|
|
// action.
|
|
if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) )
|
|
{
|
|
mShapefileMayBeCorrupted = true;
|
|
}
|
|
|
|
// try to open read-only
|
|
if ( !mLayerName.isNull() )
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, mOpenOptions, mLayerName, errCause, true );
|
|
}
|
|
else
|
|
{
|
|
mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, mOpenOptions, mLayerIndex, errCause, true );
|
|
}
|
|
|
|
mWriteAccess = false;
|
|
mOgrLayer = mOgrOrigLayer.get();
|
|
if ( mOgrLayer )
|
|
{
|
|
mValid = true;
|
|
mDynamicWriteAccess = true;
|
|
|
|
if ( !mSubsetString.isEmpty() )
|
|
{
|
|
// Do not update capabilities here
|
|
// but ensure subset is set (setSubsetString does nothing if the passed sql subset string is equal to
|
|
// mSubsetString, which is the case when reloading the dataset)
|
|
QString origSubsetString = mSubsetString;
|
|
mSubsetString.clear();
|
|
mValid = _setSubsetString( origSubsetString, false, false );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Cannot reopen %1: %2" ).arg( mFilePath ).arg( errCause ), tr( "OGR" ) );
|
|
}
|
|
}
|
|
|
|
// For debug/testing purposes
|
|
if ( !mValid )
|
|
setProperty( "_debug_open_mode", "invalid" );
|
|
else if ( mWriteAccess )
|
|
setProperty( "_debug_open_mode", "read-write" );
|
|
else
|
|
setProperty( "_debug_open_mode", "read-only" );
|
|
|
|
mRefreshFeatureCount = true;
|
|
}
|
|
|
|
void QgsOgrProvider::close()
|
|
{
|
|
if ( mWriteAccess && mForceRecomputeExtent )
|
|
extent();
|
|
|
|
mOgrSqlLayer.reset();
|
|
mOgrOrigLayer.reset();
|
|
mOgrLayer = nullptr;
|
|
mValid = false;
|
|
setProperty( "_debug_open_mode", "invalid" );
|
|
|
|
invalidateCachedExtent( false );
|
|
}
|
|
|
|
void QgsOgrProvider::invalidateNetworkCache()
|
|
{
|
|
if ( mFilePath.startsWith( QLatin1String( "/vsicurl/" ) ) ||
|
|
mFilePath.startsWith( QLatin1String( "/vsis3/" ) ) ||
|
|
mFilePath.startsWith( QLatin1String( "/vsigs/" ) ) ||
|
|
mFilePath.startsWith( QLatin1String( "/vsiaz/" ) ) ||
|
|
mFilePath.startsWith( QLatin1String( "/vsiadls/" ) ) )
|
|
{
|
|
QgsDebugMsgLevel( QString( "Invalidating cache for %1" ).arg( mFilePath ), 3 );
|
|
VSICurlPartialClearCache( mFilePath.toUtf8().constData() );
|
|
}
|
|
}
|
|
|
|
void QgsOgrProvider::reloadProviderData()
|
|
{
|
|
invalidateNetworkCache();
|
|
mFeaturesCounted = static_cast< long long >( Qgis::FeatureCountState::Uncounted );
|
|
bool wasValid = mValid;
|
|
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) );
|
|
close();
|
|
open( OpenModeSameAsCurrent );
|
|
if ( !mValid && wasValid )
|
|
pushError( tr( "Cannot reopen datasource %1" ).arg( dataSourceUri() ) );
|
|
}
|
|
|
|
bool QgsOgrProvider::_enterUpdateMode( bool implicit )
|
|
{
|
|
if ( !mWriteAccessPossible )
|
|
{
|
|
return false;
|
|
}
|
|
if ( mWriteAccess )
|
|
{
|
|
++mUpdateModeStackDepth;
|
|
return true;
|
|
}
|
|
if ( mUpdateModeStackDepth == 0 )
|
|
{
|
|
Q_ASSERT( mDynamicWriteAccess );
|
|
QgsDebugMsgLevel( QStringLiteral( "Reopening %1 in update mode" ).arg( dataSourceUri() ), 2 );
|
|
close();
|
|
open( implicit ? OpenModeForceUpdate : OpenModeForceUpdateRepackOff );
|
|
if ( !mOgrLayer || !mWriteAccess )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Cannot reopen datasource %1 in update mode" ).arg( dataSourceUri() ), tr( "OGR" ) );
|
|
pushError( tr( "Cannot reopen datasource %1 in update mode" ).arg( dataSourceUri() ) );
|
|
return false;
|
|
}
|
|
}
|
|
++mUpdateModeStackDepth;
|
|
// For implicitly entered updateMode, don't defer repacking
|
|
mDeferRepack = !implicit;
|
|
return true;
|
|
}
|
|
|
|
bool QgsOgrProvider::leaveUpdateMode()
|
|
{
|
|
if ( !mWriteAccessPossible )
|
|
{
|
|
return false;
|
|
}
|
|
--mUpdateModeStackDepth;
|
|
if ( mUpdateModeStackDepth < 0 )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Unbalanced call to leaveUpdateMode() w.r.t. enterUpdateMode()" ), tr( "OGR" ) );
|
|
mUpdateModeStackDepth = 0;
|
|
return false;
|
|
}
|
|
if ( mDeferRepack && mUpdateModeStackDepth == 0 )
|
|
{
|
|
// Only repack once update mode is inactive
|
|
if ( mShapefileMayBeCorrupted )
|
|
{
|
|
repack();
|
|
}
|
|
|
|
if ( mShapefileHadSpatialIndex )
|
|
{
|
|
createSpatialIndexImpl();
|
|
}
|
|
|
|
mShapefileMayBeCorrupted = false;
|
|
mDeferRepack = false;
|
|
}
|
|
if ( !mDynamicWriteAccess )
|
|
{
|
|
// The GeoJSON driver only properly flushes stuff in all situations by
|
|
// closing and re-opening. Starting with GDAL 2.3.1, it should be safe to
|
|
// use GDALDatasetFlush().
|
|
if ( mGDALDriverName == QLatin1String( "GeoJSON" ) )
|
|
{
|
|
// Backup fields since if we created new fields, but didn't populate it
|
|
// with any feature yet, it will disappear.
|
|
const QgsFields oldFields = mAttributeFields;
|
|
|
|
// This will also update mAttributeFields
|
|
reloadData();
|
|
|
|
if ( mValid )
|
|
{
|
|
// Make sure that new fields we added, but didn't populate yet, are
|
|
// recreated at the OGR level, otherwise we won't be able to populate
|
|
// them.
|
|
for ( const auto &field : oldFields )
|
|
{
|
|
int idx = mAttributeFields.lookupField( field.name() );
|
|
if ( idx < 0 )
|
|
{
|
|
bool ignoreErrorOut = false;
|
|
addAttributeOGRLevel( field, ignoreErrorOut );
|
|
mAttributeFields.append( field );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if ( mUpdateModeStackDepth == 0 )
|
|
{
|
|
QgsDebugMsgLevel( QStringLiteral( "Reopening %1 in read-only mode" ).arg( dataSourceUri() ), 2 );
|
|
close();
|
|
open( OpenModeForceReadOnly );
|
|
if ( !mOgrLayer )
|
|
{
|
|
QgsMessageLog::logMessage( tr( "Cannot reopen datasource %1 in read-only mode" ).arg( dataSourceUri() ), tr( "OGR" ) );
|
|
pushError( tr( "Cannot reopen datasource %1 in read-only mode" ).arg( dataSourceUri() ) );
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
Qgis::ProviderStyleStorageCapabilities QgsOgrProvider::styleStorageCapabilities() const
|
|
{
|
|
Qgis::ProviderStyleStorageCapabilities storageCapabilities;
|
|
if ( isValid() && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) )
|
|
{
|
|
storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase;
|
|
storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase;
|
|
storageCapabilities |= Qgis::ProviderStyleStorageCapability::DeleteFromDatabase;
|
|
}
|
|
return storageCapabilities;
|
|
}
|
|
|
|
QString QgsOgrProvider::fileVectorFilters() const
|
|
{
|
|
return QgsOgrProviderUtils::fileVectorFilters();
|
|
}
|
|
|
|
#undef TEXT_PROVIDER_KEY
|
|
#undef TEXT_PROVIDER_DESCRIPTION
|
|
|
|
///@endcond
|