mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-14 00:07:35 -04:00
Merge pull request #5057 from boundlessgeo/gpkg-raster-import2
Geopackage: handle raster drop in browser
This commit is contained in:
commit
daa60d1a0e
2
INSTALL
2
INSTALL
@ -102,7 +102,7 @@ Required build dependencies:
|
||||
- Sqlite3 >= 3.0.0
|
||||
- SpatiaLite
|
||||
- libspatialindex
|
||||
- GDAL/OGR >= 2.0
|
||||
- GDAL/OGR >= 2.1
|
||||
- Qwt >= 5.0 & (< 6.1 with internal QwtPolar)
|
||||
- expat >= 1.95
|
||||
- QScintilla2
|
||||
|
@ -62,8 +62,11 @@ ELSE(WIN32)
|
||||
STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\1" GDAL_VERSION_MAJOR "${GDAL_VERSION}")
|
||||
STRING(REGEX REPLACE "([0-9]+)\\.([0-9]+)\\.([0-9]+)" "\\2" GDAL_VERSION_MINOR "${GDAL_VERSION}")
|
||||
IF (GDAL_VERSION_MAJOR LESS 2)
|
||||
MESSAGE (FATAL_ERROR "GDAL version is too old (${GDAL_VERSION}). Use 2.0 or higher.")
|
||||
MESSAGE (FATAL_ERROR "GDAL version is too old (${GDAL_VERSION}). Use 2.1 or higher.")
|
||||
ENDIF (GDAL_VERSION_MAJOR LESS 2)
|
||||
IF ( (GDAL_VERSION_MAJOR EQUAL 2) AND (GDAL_VERSION_MINOR LESS 1) )
|
||||
MESSAGE (FATAL_ERROR "GDAL version is too old (${GDAL_VERSION}). Use 2.1 or higher.")
|
||||
ENDIF( (GDAL_VERSION_MAJOR EQUAL 2) AND (GDAL_VERSION_MINOR LESS 1) )
|
||||
|
||||
ENDIF (GDAL_LIBRARY)
|
||||
SET (CMAKE_FIND_FRAMEWORK ${CMAKE_FIND_FRAMEWORK_save} CACHE STRING "" FORCE)
|
||||
@ -101,10 +104,13 @@ ELSE(WIN32)
|
||||
|
||||
# check for gdal version
|
||||
# version 1.2.5 is known NOT to be supported (missing CPL_STDCALL macro)
|
||||
# According to INSTALL, 2.0+ is required
|
||||
# According to INSTALL, 2.1+ is required
|
||||
IF (GDAL_VERSION_MAJOR LESS 2)
|
||||
MESSAGE (FATAL_ERROR "GDAL version is too old (${GDAL_VERSION}). Use 2.0 or higher.")
|
||||
MESSAGE (FATAL_ERROR "GDAL version is too old (${GDAL_VERSION}). Use 2.1 or higher.")
|
||||
ENDIF (GDAL_VERSION_MAJOR LESS 2)
|
||||
IF ( (GDAL_VERSION_MAJOR EQUAL 2) AND (GDAL_VERSION_MINOR LESS 1) )
|
||||
MESSAGE (FATAL_ERROR "GDAL version is too old (${GDAL_VERSION}). Use 2.1 or higher.")
|
||||
ENDIF( (GDAL_VERSION_MAJOR EQUAL 2) AND (GDAL_VERSION_MINOR LESS 1) )
|
||||
|
||||
# set INCLUDE_DIR to prefix+include
|
||||
EXEC_PROGRAM(${GDAL_CONFIG}
|
||||
|
@ -48,6 +48,14 @@ Returns encoded representation of the object
|
||||
:rtype: QgsVectorLayer
|
||||
%End
|
||||
|
||||
QgsRasterLayer *rasterLayer( bool &owner, QString &error ) const;
|
||||
%Docstring
|
||||
Get raster layer from uri if possible, otherwise returns 0 and error is set
|
||||
\param owner set to true if caller becomes owner
|
||||
\param error set to error message if cannot get raster
|
||||
:rtype: QgsRasterLayer
|
||||
%End
|
||||
|
||||
QString layerType;
|
||||
%Docstring
|
||||
Type of URI. Recognized types: "vector" / "raster" / "plugin" / "custom"
|
||||
|
@ -100,6 +100,18 @@ QgsVectorLayer *QgsMimeDataUtils::Uri::vectorLayer( bool &owner, QString &error
|
||||
return new QgsVectorLayer( uri, name, providerKey );
|
||||
}
|
||||
|
||||
QgsRasterLayer *QgsMimeDataUtils::Uri::rasterLayer( bool &owner, QString &error ) const
|
||||
{
|
||||
owner = false;
|
||||
if ( layerType != QLatin1String( "raster" ) )
|
||||
{
|
||||
error = QObject::tr( "%1: Not a raster layer." ).arg( name );
|
||||
return nullptr;
|
||||
}
|
||||
owner = true;
|
||||
return new QgsRasterLayer( uri, name, providerKey );
|
||||
}
|
||||
|
||||
// -----
|
||||
|
||||
bool QgsMimeDataUtils::isUriList( const QMimeData *data )
|
||||
|
@ -23,6 +23,7 @@
|
||||
class QgsLayerItem;
|
||||
class QgsLayerTreeNode;
|
||||
class QgsVectorLayer;
|
||||
class QgsRasterLayer;
|
||||
|
||||
/** \ingroup core
|
||||
* \class QgsMimeDataUtils
|
||||
@ -51,6 +52,12 @@ class CORE_EXPORT QgsMimeDataUtils
|
||||
*/
|
||||
QgsVectorLayer *vectorLayer( bool &owner, QString &error ) const;
|
||||
|
||||
/** Get raster layer from uri if possible, otherwise returns 0 and error is set
|
||||
* \param owner set to true if caller becomes owner
|
||||
* \param error set to error message if cannot get raster
|
||||
*/
|
||||
QgsRasterLayer *rasterLayer( bool &owner, QString &error ) const;
|
||||
|
||||
//! Type of URI. Recognized types: "vector" / "raster" / "plugin" / "custom"
|
||||
QString layerType;
|
||||
//! For "vector" / "raster" type: provider id.
|
||||
|
@ -808,15 +808,9 @@ QMap<QString, QgsVectorFileWriter::MetaData> QgsVectorFileWriter::initMetaData()
|
||||
"Can be one of NULL for a simple .dbf file with no .shp file, POINT, "
|
||||
"ARC, POLYGON or MULTIPOINT for 2D, or POINTZ, ARCZ, POLYGONZ or "
|
||||
"MULTIPOINTZ for 3D;" ) +
|
||||
#if defined(GDAL_COMPUTE_VERSION) && GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(2,1,0)
|
||||
QObject::tr( " Shapefiles with measure values are not supported,"
|
||||
" nor are MULTIPATCH files." ) +
|
||||
#endif
|
||||
#if defined(GDAL_COMPUTE_VERSION) && GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,1,0)
|
||||
QObject::tr( " POINTM, ARCM, POLYGONM or MULTIPOINTM for measured geometries"
|
||||
" and POINTZM, ARCZM, POLYGONZM or MULTIPOINTZM for 3D measured"
|
||||
" geometries." ) +
|
||||
#endif
|
||||
#if defined(GDAL_COMPUTE_VERSION) && GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,2,0)
|
||||
QObject::tr( " MULTIPATCH files are supported since GDAL 2.2." ) +
|
||||
#endif
|
||||
@ -831,7 +825,6 @@ QMap<QString, QgsVectorFileWriter::MetaData> QgsVectorFileWriter::initMetaData()
|
||||
<< QStringLiteral( "ARCZ" )
|
||||
<< QStringLiteral( "POLYGONZ" )
|
||||
<< QStringLiteral( "MULTIPOINTZ" )
|
||||
#if defined(GDAL_COMPUTE_VERSION) && GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,1,0)
|
||||
<< QStringLiteral( "POINTM" )
|
||||
<< QStringLiteral( "ARCM" )
|
||||
<< QStringLiteral( "POLYGONM" )
|
||||
@ -840,7 +833,6 @@ QMap<QString, QgsVectorFileWriter::MetaData> QgsVectorFileWriter::initMetaData()
|
||||
<< QStringLiteral( "ARCZM" )
|
||||
<< QStringLiteral( "POLYGONZM" )
|
||||
<< QStringLiteral( "MULTIPOINTZM" )
|
||||
#endif
|
||||
#if defined(GDAL_COMPUTE_VERSION) && GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,2,0)
|
||||
<< QStringLiteral( "MULTIPATCH" )
|
||||
#endif
|
||||
@ -1330,7 +1322,6 @@ QMap<QString, QgsVectorFileWriter::MetaData> QgsVectorFileWriter::initMetaData()
|
||||
true // Allow None
|
||||
) );
|
||||
|
||||
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,0,2)
|
||||
datasetOptions.insert( QStringLiteral( "BLOCK_SIZE" ), new IntOption(
|
||||
QObject::tr( "(multiples of 512): Block size for .map files. Defaults "
|
||||
"to 512. MapInfo 15.2 and above creates .tab files with a "
|
||||
@ -1338,15 +1329,12 @@ QMap<QString, QgsVectorFileWriter::MetaData> QgsVectorFileWriter::initMetaData()
|
||||
"able to handle block sizes from 512 to 32256." ),
|
||||
512
|
||||
) );
|
||||
#endif
|
||||
#if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,0,0)
|
||||
layerOptions.insert( QStringLiteral( "BOUNDS" ), new StringOption(
|
||||
QObject::tr( "xmin,ymin,xmax,ymax: Define custom layer bounds to increase the "
|
||||
"accuracy of the coordinates. Note: the geometry of written "
|
||||
"features must be within the defined box." ),
|
||||
QLatin1String( "" ) // Default value
|
||||
) );
|
||||
#endif
|
||||
|
||||
driverMetadata.insert( QStringLiteral( "MapInfo File" ),
|
||||
MetaData(
|
||||
@ -1894,9 +1882,6 @@ QStringList QgsVectorFileWriter::defaultLayerOptions( const QString &driverName
|
||||
|
||||
OGRwkbGeometryType QgsVectorFileWriter::ogrTypeFromWkbType( QgsWkbTypes::Type type )
|
||||
{
|
||||
#if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(2,1,0)
|
||||
type = QgsWkbTypes::dropM( type );
|
||||
#endif
|
||||
|
||||
OGRwkbGeometryType ogrType = static_cast<OGRwkbGeometryType>( type );
|
||||
|
||||
|
@ -8,6 +8,8 @@ SET (OGR_SRCS
|
||||
qgsogrsourceselect.cpp
|
||||
qgsgeopackagedataitems.cpp
|
||||
qgsgeopackageconnection.cpp
|
||||
qgsgeopackagerasterwriter.cpp
|
||||
qgsgeopackagerasterwritertask.cpp
|
||||
)
|
||||
|
||||
SET(OGR_MOC_HDRS
|
||||
@ -17,6 +19,7 @@ SET(OGR_MOC_HDRS
|
||||
qgsogrsourceselect.h
|
||||
qgsgeopackagedataitems.h
|
||||
qgsgeopackageconnection.h
|
||||
qgsgeopackagerasterwritertask.h
|
||||
)
|
||||
|
||||
########################################################
|
||||
|
48
src/providers/ogr/qgscplerrorhandler.h
Normal file
48
src/providers/ogr/qgscplerrorhandler.h
Normal file
@ -0,0 +1,48 @@
|
||||
/***************************************************************************
|
||||
qgscplerrorhandler.h - QgsCplErrorHandler
|
||||
|
||||
---------------------
|
||||
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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
#ifndef QGSCPLERRORHANDLER_H
|
||||
#define QGSCPLERRORHANDLER_H
|
||||
|
||||
#include "gdal.h"
|
||||
#include "qgsmessagelog.h"
|
||||
|
||||
class QgsCPLErrorHandler
|
||||
{
|
||||
static void CPL_STDCALL showError( CPLErr errClass, int errNo, const char *msg )
|
||||
{
|
||||
if ( errNo != OGRERR_NONE )
|
||||
QgsMessageLog::logMessage( QObject::tr( "OGR[%1] error %2: %3" ).arg( errClass ).arg( errNo ).arg( msg ), QObject::tr( "OGR" ) );
|
||||
}
|
||||
|
||||
public:
|
||||
QgsCPLErrorHandler()
|
||||
{
|
||||
CPLPushErrorHandler( showError );
|
||||
}
|
||||
|
||||
~QgsCPLErrorHandler()
|
||||
{
|
||||
CPLPopErrorHandler();
|
||||
}
|
||||
|
||||
private:
|
||||
QgsCPLErrorHandler( const QgsCPLErrorHandler &other );
|
||||
QgsCPLErrorHandler &operator=( const QgsCPLErrorHandler &other );
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif // QGSCPLERRORHANDLER_H
|
@ -26,6 +26,7 @@
|
||||
#include "qgsnewgeopackagelayerdialog.h"
|
||||
#include "qgsmessageoutput.h"
|
||||
#include "qgsvectorlayerexporter.h"
|
||||
#include "qgsgeopackagerasterwritertask.h"
|
||||
#include "gdal.h"
|
||||
|
||||
#include <QAction>
|
||||
@ -327,57 +328,68 @@ bool QgsGeoPackageConnectionItem::handleDrop( const QMimeData *data, Qt::DropAct
|
||||
bool hasError = false;
|
||||
|
||||
QgsMimeDataUtils::UriList lst = QgsMimeDataUtils::decodeUriList( data );
|
||||
Q_FOREACH ( const QgsMimeDataUtils::Uri &u, lst )
|
||||
Q_FOREACH ( const QgsMimeDataUtils::Uri &dropUri, lst )
|
||||
{
|
||||
if ( u.layerType == QStringLiteral( "vector" ) )
|
||||
// Check that we are not copying over self
|
||||
if ( dropUri.uri.startsWith( mPath ) )
|
||||
{
|
||||
// Check that we are not copying over self
|
||||
if ( u.uri.startsWith( mPath ) )
|
||||
{
|
||||
importResults.append( tr( "You cannot import layer %1 over itself!" ).arg( u.name ) );
|
||||
hasError = true;
|
||||
importResults.append( tr( "You cannot import layer %1 over itself!" ).arg( dropUri.name ) );
|
||||
hasError = true;
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
QgsMapLayer *srcLayer;
|
||||
bool owner;
|
||||
bool isVector = false;
|
||||
QString error;
|
||||
// Common checks for raster and vector
|
||||
// aspatial is treated like vector
|
||||
if ( dropUri.layerType == QStringLiteral( "vector" ) )
|
||||
{
|
||||
// open the source layer
|
||||
srcLayer = dropUri.vectorLayer( owner, error );
|
||||
isVector = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// open the source layer
|
||||
bool owner;
|
||||
QString error;
|
||||
QgsVectorLayer *srcLayer = u.vectorLayer( owner, error );
|
||||
if ( !srcLayer )
|
||||
{
|
||||
importResults.append( tr( "%1: %2" ).arg( u.name ).arg( error ) );
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
srcLayer = dropUri.rasterLayer( owner, error );
|
||||
}
|
||||
if ( !srcLayer )
|
||||
{
|
||||
importResults.append( tr( "%1: %2" ).arg( dropUri.name ).arg( error ) );
|
||||
hasError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( srcLayer->isValid() )
|
||||
{
|
||||
uri = mPath;
|
||||
QgsDebugMsgLevel( "URI " + uri, 3 );
|
||||
if ( srcLayer->isValid() )
|
||||
{
|
||||
uri = mPath;
|
||||
QgsDebugMsgLevel( "URI " + uri, 3 );
|
||||
|
||||
// check if the destination layer already exists
|
||||
bool exists = false;
|
||||
// Q_FOREACH won't detach ...
|
||||
for ( const auto child : children() )
|
||||
// check if the destination layer already exists
|
||||
bool exists = false;
|
||||
// Q_FOREACH won't detach ...
|
||||
for ( const auto child : children() )
|
||||
{
|
||||
if ( child->name() == dropUri.name )
|
||||
{
|
||||
if ( child->name() == u.name )
|
||||
{
|
||||
exists = true;
|
||||
}
|
||||
exists = true;
|
||||
}
|
||||
if ( ! exists || QMessageBox::question( nullptr, tr( "Overwrite Layer" ),
|
||||
tr( "Destination layer <b>%1</b> already exists. Do you want to overwrite it?" ).arg( u.name ), QMessageBox::Yes | QMessageBox::No ) == QMessageBox::Yes )
|
||||
}
|
||||
if ( ! exists || QMessageBox::question( nullptr, tr( "Overwrite Layer" ),
|
||||
tr( "Destination layer <b>%1</b> already exists. Do you want to overwrite it?" ).arg( dropUri.name ), QMessageBox::Yes | QMessageBox::No ) == QMessageBox::Yes )
|
||||
{
|
||||
if ( isVector ) // Import vectors and aspatial
|
||||
{
|
||||
|
||||
QgsVectorLayer *vectorSrcLayer = dynamic_cast < QgsVectorLayer * >( srcLayer );
|
||||
QVariantMap options;
|
||||
options.insert( QStringLiteral( "driverName" ), QStringLiteral( "GPKG" ) );
|
||||
options.insert( QStringLiteral( "update" ), true );
|
||||
options.insert( QStringLiteral( "overwrite" ), true );
|
||||
options.insert( QStringLiteral( "layerName" ), u.name );
|
||||
|
||||
std::unique_ptr< QgsVectorLayerExporterTask > exportTask( new QgsVectorLayerExporterTask( srcLayer, uri, QStringLiteral( "ogr" ), srcLayer->crs(), options, owner ) );
|
||||
options.insert( QStringLiteral( "layerName" ), dropUri.name );
|
||||
|
||||
std::unique_ptr< QgsVectorLayerExporterTask > exportTask( new QgsVectorLayerExporterTask( vectorSrcLayer, uri, QStringLiteral( "ogr" ), vectorSrcLayer->crs(), options, owner ) );
|
||||
// when export is successful:
|
||||
connect( exportTask.get(), &QgsVectorLayerExporterTask::exportComplete, this, [ = ]()
|
||||
{
|
||||
@ -393,31 +405,52 @@ bool QgsGeoPackageConnectionItem::handleDrop( const QMimeData *data, Qt::DropAct
|
||||
{
|
||||
QgsMessageOutput *output = QgsMessageOutput::createMessageOutput();
|
||||
output->setTitle( tr( "Import to GeoPackage database" ) );
|
||||
output->setMessage( tr( "Failed to import some layers!\n\n" ) + errorMessage, QgsMessageOutput::MessageText );
|
||||
output->setMessage( tr( "Failed to import some vector layers!\n\n" ) + errorMessage, QgsMessageOutput::MessageText );
|
||||
output->showMessage();
|
||||
}
|
||||
} );
|
||||
|
||||
QgsApplication::taskManager()->addTask( exportTask.release() );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
importResults.append( tr( "%1: Not a valid layer!" ).arg( u.name ) );
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: implement raster import
|
||||
QgsMessageOutput *output = QgsMessageOutput::createMessageOutput();
|
||||
output->setTitle( tr( "Import to GeoPackage database failed" ) );
|
||||
output->setMessage( tr( "Failed to import some layers!\n\n" ) + QStringLiteral( "Raster import is not yet implemented!\n" ), QgsMessageOutput::MessageText );
|
||||
output->showMessage();
|
||||
}
|
||||
else // Import raster
|
||||
{
|
||||
|
||||
}
|
||||
std::unique_ptr< QgsGeoPackageRasterWriterTask > exportTask( new QgsGeoPackageRasterWriterTask( dropUri, mPath ) );
|
||||
// when export is successful:
|
||||
connect( exportTask.get(), &QgsGeoPackageRasterWriterTask::writeComplete, this, [ = ]()
|
||||
{
|
||||
// this is gross - TODO - find a way to get access to messageBar from data items
|
||||
QMessageBox::information( nullptr, tr( "Import to GeoPackage database" ), tr( "Import was successful." ) );
|
||||
refreshConnections();
|
||||
} );
|
||||
|
||||
// when an error occurs:
|
||||
connect( exportTask.get(), &QgsGeoPackageRasterWriterTask::errorOccurred, this, [ = ]( QgsGeoPackageRasterWriter::WriterError error, const QString & errorMessage )
|
||||
{
|
||||
if ( error != QgsGeoPackageRasterWriter::WriterError::ErrUserCanceled )
|
||||
{
|
||||
QgsMessageOutput *output = QgsMessageOutput::createMessageOutput();
|
||||
output->setTitle( tr( "Import to GeoPackage database" ) );
|
||||
output->setMessage( tr( "Failed to import some raster layers!\n\n" ) + errorMessage, QgsMessageOutput::MessageText );
|
||||
output->showMessage();
|
||||
}
|
||||
// Always try to delete the imported raster, in case the gpkg has been left
|
||||
// in an inconsistent status. Ignore delete errors.
|
||||
QString deleteErr;
|
||||
deleteGeoPackageRasterLayer( QStringLiteral( "GPKG:%1:%2" ).arg( mPath, dropUri.name ), deleteErr );
|
||||
} );
|
||||
|
||||
QgsApplication::taskManager()->addTask( exportTask.release() );
|
||||
}
|
||||
} // do not overwrite
|
||||
}
|
||||
else
|
||||
{
|
||||
importResults.append( tr( "%1: Not a valid layer!" ).arg( dropUri.name ) );
|
||||
hasError = true;
|
||||
}
|
||||
} // check for self copy
|
||||
} // for each
|
||||
|
||||
if ( hasError )
|
||||
{
|
||||
@ -426,7 +459,6 @@ bool QgsGeoPackageConnectionItem::handleDrop( const QMimeData *data, Qt::DropAct
|
||||
output->setMessage( tr( "Failed to import some layers!\n\n" ) + importResults.join( QStringLiteral( "\n" ) ), QgsMessageOutput::MessageText );
|
||||
output->showMessage();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -457,6 +489,124 @@ QgsLayerItem::LayerType QgsGeoPackageConnectionItem::layerTypeFromDb( const QStr
|
||||
return QgsLayerItem::LayerType::TableLayer;
|
||||
}
|
||||
|
||||
bool QgsGeoPackageConnectionItem::deleteGeoPackageRasterLayer( const QString uri, QString &errCause )
|
||||
{
|
||||
bool result = false;
|
||||
// Better safe than sorry
|
||||
if ( ! uri.isEmpty( ) )
|
||||
{
|
||||
QStringList pieces( uri.split( ':' ) );
|
||||
if ( pieces.size() != 3 )
|
||||
{
|
||||
errCause = QStringLiteral( "Layer URI is malformed: layer <b>%1</b> cannot be deleted!" ).arg( uri );
|
||||
}
|
||||
else
|
||||
{
|
||||
QString baseUri = pieces.at( 1 );
|
||||
QString layerName = pieces.at( 2 );
|
||||
sqlite3 *handle;
|
||||
int status = sqlite3_open_v2( baseUri.toUtf8().constData(), &handle, SQLITE_OPEN_READWRITE, NULL );
|
||||
if ( status != SQLITE_OK )
|
||||
{
|
||||
errCause = sqlite3_errmsg( handle );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove table
|
||||
char *errmsg = nullptr;
|
||||
char *sql = sqlite3_mprintf(
|
||||
"DROP table IF EXISTS \"%w\";"
|
||||
"DELETE FROM gpkg_contents WHERE table_name = '%q';"
|
||||
"DELETE FROM gpkg_tile_matrix WHERE table_name = '%q';"
|
||||
"DELETE FROM gpkg_tile_matrix_set WHERE table_name = '%q';",
|
||||
layerName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData() );
|
||||
status = sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
sql, /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
&errmsg /* Error msg written here */
|
||||
);
|
||||
sqlite3_free( sql );
|
||||
// Remove from optional tables, may silently fail
|
||||
QStringList optionalTables;
|
||||
optionalTables << QStringLiteral( "gpkg_extensions" )
|
||||
<< QStringLiteral( "gpkg_metadata_reference" );
|
||||
Q_FOREACH ( const QString &tableName, optionalTables )
|
||||
{
|
||||
char *sql = sqlite3_mprintf( "DELETE FROM %w WHERE table_name = '%q'",
|
||||
tableName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData() );
|
||||
sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
sql, /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
NULL /* Error msg written here */
|
||||
);
|
||||
sqlite3_free( sql );
|
||||
}
|
||||
// Other tables, ignore errors
|
||||
{
|
||||
char *sql = sqlite3_mprintf( "DELETE FROM gpkg_2d_gridded_coverage_ancillary WHERE tile_matrix_set_name = '%q'",
|
||||
layerName.toUtf8().constData() );
|
||||
sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
sql, /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
NULL /* Error msg written here */
|
||||
);
|
||||
sqlite3_free( sql );
|
||||
}
|
||||
{
|
||||
char *sql = sqlite3_mprintf( "DELETE FROM gpkg_2d_gridded_tile_ancillary WHERE tpudt_name = '%q'",
|
||||
layerName.toUtf8().constData() );
|
||||
sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
sql, /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
NULL /* Error msg written here */
|
||||
);
|
||||
sqlite3_free( sql );
|
||||
}
|
||||
// Vacuum
|
||||
{
|
||||
sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
"VACUUM", /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
NULL /* Error msg written here */
|
||||
);
|
||||
}
|
||||
|
||||
if ( status == SQLITE_OK )
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
errCause = tr( "There was an error deleting the layer %1: %2" ).arg( layerName, QString::fromUtf8( errmsg ) );
|
||||
}
|
||||
sqlite3_free( errmsg );
|
||||
}
|
||||
sqlite3_close( handle );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should never happen!
|
||||
errCause = tr( "Layer URI is empty: layer cannot be deleted!" );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
void QgsGeoPackageConnectionItem::deleteConnection()
|
||||
{
|
||||
QgsGeoPackageConnection::deleteConnection( name() );
|
||||
@ -525,7 +675,7 @@ void QgsGeoPackageAbstractLayerItem::deleteLayer()
|
||||
{
|
||||
QMessageBox::information( nullptr, tr( "Delete Layer" ), tr( "Layer <b>%1</b> deleted successfully." ).arg( mName ) );
|
||||
if ( mParent )
|
||||
mParent->refresh();
|
||||
mParent->refreshConnections();
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -564,83 +714,7 @@ QgsGeoPackageRasterLayerItem::QgsGeoPackageRasterLayerItem( QgsDataItem *parent,
|
||||
|
||||
bool QgsGeoPackageRasterLayerItem::executeDeleteLayer( QString &errCause )
|
||||
{
|
||||
bool result = false;
|
||||
// Better safe than sorry
|
||||
if ( ! mUri.isEmpty( ) )
|
||||
{
|
||||
QStringList pieces( mUri.split( ':' ) );
|
||||
if ( pieces.size() != 3 )
|
||||
{
|
||||
errCause = QStringLiteral( "Layer URI is malformed: layer <b>%1</b> cannot be deleted!" ).arg( mName );
|
||||
}
|
||||
else
|
||||
{
|
||||
QString baseUri = pieces.at( 1 );
|
||||
QString layerName = pieces.at( 2 );
|
||||
sqlite3 *handle;
|
||||
int status = sqlite3_open_v2( baseUri.toUtf8().constData(), &handle, SQLITE_OPEN_READWRITE, NULL );
|
||||
if ( status != SQLITE_OK )
|
||||
{
|
||||
errCause = sqlite3_errmsg( handle );
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove table
|
||||
char *errmsg = nullptr;
|
||||
char *sql = sqlite3_mprintf(
|
||||
"DROP table %w;"
|
||||
"DELETE FROM gpkg_contents WHERE table_name = '%q';"
|
||||
"DELETE FROM gpkg_tile_matrix WHERE table_name = '%q';"
|
||||
"DELETE FROM gpkg_tile_matrix_set WHERE table_name = '%q';",
|
||||
layerName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData() );
|
||||
status = sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
sql, /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
&errmsg /* Error msg written here */
|
||||
);
|
||||
sqlite3_free( sql );
|
||||
// Remove from optional tables, may silently fail
|
||||
QStringList optionalTables;
|
||||
optionalTables << QStringLiteral( "gpkg_extensions" )
|
||||
<< QStringLiteral( "gpkg_metadata_reference" );
|
||||
for ( const auto tableName : optionalTables )
|
||||
{
|
||||
char *sql = sqlite3_mprintf( "DELETE FROM table %w WHERE table_name = '%q",
|
||||
tableName.toUtf8().constData(),
|
||||
layerName.toUtf8().constData() );
|
||||
sqlite3_exec(
|
||||
handle, /* An open database */
|
||||
sql, /* SQL to be evaluated */
|
||||
NULL, /* Callback function */
|
||||
NULL, /* 1st argument to callback */
|
||||
NULL /* Error msg written here */
|
||||
);
|
||||
sqlite3_free( sql );
|
||||
}
|
||||
if ( status == SQLITE_OK )
|
||||
{
|
||||
result = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
errCause = tr( "There was an error deleting the layer: %1" ).arg( QString::fromUtf8( errmsg ) );
|
||||
}
|
||||
sqlite3_free( errmsg );
|
||||
}
|
||||
sqlite3_close( handle );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This should never happen!
|
||||
errCause = QStringLiteral( "Layer URI is empty: layer <b>%1</b> cannot be deleted!" ).arg( mName );
|
||||
}
|
||||
return result;
|
||||
return QgsGeoPackageConnectionItem::deleteGeoPackageRasterLayer( mUri, errCause );
|
||||
}
|
||||
|
||||
|
||||
|
@ -82,6 +82,9 @@ class QgsGeoPackageConnectionItem : public QgsDataCollectionItem
|
||||
//! Return the layer type from \a geometryType
|
||||
static QgsLayerItem::LayerType layerTypeFromDb( const QString &geometryType );
|
||||
|
||||
//! Delete a geopackage layer
|
||||
static bool deleteGeoPackageRasterLayer( const QString uri, QString &errCause );
|
||||
|
||||
public slots:
|
||||
#ifdef HAVE_GUI
|
||||
void editConnection();
|
||||
|
74
src/providers/ogr/qgsgeopackagerasterwriter.cpp
Normal file
74
src/providers/ogr/qgsgeopackagerasterwriter.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
/***************************************************************************
|
||||
qgsgeopackagerasterwriter.cpp - QgsGeoPackageRasterWriter
|
||||
|
||||
---------------------
|
||||
begin : 23.8.2017
|
||||
copyright : (C) 2017 by Alessandro Pasotti
|
||||
email : apasotti at boundlessgeo dot 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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
#include "gdal.h"
|
||||
#include "gdal_utils.h"
|
||||
|
||||
#include "qgsgeopackagerasterwriter.h"
|
||||
#include "qgscplerrorhandler.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
QgsGeoPackageRasterWriter::QgsGeoPackageRasterWriter( const QgsMimeDataUtils::Uri sourceUri, const QString outputUrl ):
|
||||
mSourceUri( sourceUri ),
|
||||
mOutputUrl( outputUrl )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
QgsGeoPackageRasterWriter::WriterError QgsGeoPackageRasterWriter::writeRaster( QgsFeedback *feedback, QString *errorMessage )
|
||||
{
|
||||
const char *args[] = { "-of", "gpkg", "-co", QStringLiteral( "RASTER_TABLE=%1" ).arg( mSourceUri.name ).toUtf8().constData(), "-co", "APPEND_SUBDATASET=YES", nullptr };
|
||||
// This sends OGR/GDAL errors to the message log
|
||||
QgsCPLErrorHandler handler;
|
||||
GDALTranslateOptions *psOptions = GDALTranslateOptionsNew( ( char ** )args, nullptr );
|
||||
|
||||
GDALTranslateOptionsSetProgress( psOptions, [ ]( double dfComplete, const char *pszMessage, void *pProgressData ) -> int
|
||||
{
|
||||
Q_UNUSED( pszMessage );
|
||||
QgsFeedback *feedback = static_cast< QgsFeedback * >( pProgressData );
|
||||
feedback->setProgress( dfComplete * 100 );
|
||||
return ! feedback->isCanceled();
|
||||
}, feedback );
|
||||
|
||||
GDALDatasetH hSrcDS = GDALOpen( mSourceUri.uri.toUtf8().constData(), GA_ReadOnly );
|
||||
if ( ! hSrcDS )
|
||||
{
|
||||
*errorMessage = QObject::tr( "Failed to open source layer %1! See the OGR panel in the message logs for details.\n\n" ).arg( mSourceUri.name );
|
||||
mHasError = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
CPLErrorReset();
|
||||
GDALDatasetH hOutDS = GDALTranslate( mOutputUrl.toUtf8().constData(), hSrcDS, psOptions, NULL );
|
||||
if ( ! hOutDS )
|
||||
{
|
||||
*errorMessage = QObject::tr( "Failed to import layer %1! See the OGR panel in the message logs for details.\n\n" ).arg( mSourceUri.name );
|
||||
mHasError = true;
|
||||
}
|
||||
else // All good!
|
||||
{
|
||||
GDALClose( hOutDS );
|
||||
}
|
||||
GDALClose( hSrcDS );
|
||||
}
|
||||
GDALTranslateOptionsFree( psOptions );
|
||||
return ( feedback && feedback->isCanceled() ) ? ErrUserCanceled : ( mHasError ? WriteError : NoError ) ;
|
||||
}
|
||||
|
||||
///@endcond
|
52
src/providers/ogr/qgsgeopackagerasterwriter.h
Normal file
52
src/providers/ogr/qgsgeopackagerasterwriter.h
Normal file
@ -0,0 +1,52 @@
|
||||
/***************************************************************************
|
||||
qgsgeopackagerasterwriter.h - QgsGeoPackageRasterWriter
|
||||
|
||||
---------------------
|
||||
begin : 23.8.2017
|
||||
copyright : (C) 2017 by Alessandro Pasotti
|
||||
email : apasotti at boundlessgeo dot 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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
#ifndef QGSGEOPACKAGERASTERWRITER_H
|
||||
#define QGSGEOPACKAGERASTERWRITER_H
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
#define SIP_NO_FILE
|
||||
|
||||
#include "qgsmimedatautils.h"
|
||||
#include "qgsfeedback.h"
|
||||
|
||||
class QgsGeoPackageRasterWriter
|
||||
{
|
||||
public:
|
||||
|
||||
//! Error codes
|
||||
enum WriterError
|
||||
{
|
||||
NoError = 0, //!< No errors were encountered
|
||||
WriteError, //! Generic GDAL Translate error
|
||||
ErrUserCanceled, //!< User canceled the export
|
||||
};
|
||||
|
||||
QgsGeoPackageRasterWriter( const QgsMimeDataUtils::Uri sourceUri, const QString destinationPath );
|
||||
WriterError writeRaster( QgsFeedback *feedback, QString *errorMessage );
|
||||
const QString outputUrl() const { return mOutputUrl; }
|
||||
|
||||
private:
|
||||
QgsMimeDataUtils::Uri mSourceUri;
|
||||
QString mOutputUrl;
|
||||
bool mHasError = false;
|
||||
};
|
||||
|
||||
|
||||
///@endcond
|
||||
|
||||
#endif // QGSGEOPACKAGERASTERWRITER_H
|
||||
|
55
src/providers/ogr/qgsgeopackagerasterwritertask.cpp
Normal file
55
src/providers/ogr/qgsgeopackagerasterwritertask.cpp
Normal file
@ -0,0 +1,55 @@
|
||||
/***************************************************************************
|
||||
qgsgeopackagerasterwritertask.cpp - QgsGeoPackageRasterWriterTask
|
||||
|
||||
---------------------
|
||||
begin : 23.8.2017
|
||||
copyright : (C) 2017 by Alessandro Pasotti
|
||||
email : apasotti at boundlessgeo dot 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 "qgsgeopackagerasterwritertask.h"
|
||||
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
|
||||
QgsGeoPackageRasterWriterTask::QgsGeoPackageRasterWriterTask( const QgsMimeDataUtils::Uri sourceUri, const QString destinationPath )
|
||||
: QgsTask( tr( "Saving %1" ).arg( destinationPath ), QgsTask::CanCancel )
|
||||
, mWriter( sourceUri, destinationPath )
|
||||
, mFeedback( new QgsFeedback() )
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
void QgsGeoPackageRasterWriterTask::cancel()
|
||||
{
|
||||
mError = QgsGeoPackageRasterWriter::WriterError::ErrUserCanceled;
|
||||
mFeedback.get()->cancel();
|
||||
}
|
||||
|
||||
bool QgsGeoPackageRasterWriterTask::run()
|
||||
{
|
||||
connect( mFeedback.get(), &QgsFeedback::progressChanged, this, &QgsGeoPackageRasterWriterTask::setProgress );
|
||||
mError = mWriter.writeRaster( mFeedback.get(), &mErrorMessage );
|
||||
return mError == QgsGeoPackageRasterWriter::WriterError::NoError;
|
||||
}
|
||||
|
||||
void QgsGeoPackageRasterWriterTask::finished( bool result )
|
||||
{
|
||||
if ( result )
|
||||
{
|
||||
emit writeComplete( mWriter.outputUrl() );
|
||||
}
|
||||
else
|
||||
{
|
||||
emit errorOccurred( mError, mErrorMessage );
|
||||
}
|
||||
}
|
||||
|
||||
///@endcond
|
83
src/providers/ogr/qgsgeopackagerasterwritertask.h
Normal file
83
src/providers/ogr/qgsgeopackagerasterwritertask.h
Normal file
@ -0,0 +1,83 @@
|
||||
/***************************************************************************
|
||||
qgsgeopackagerasterwritertask.h - QgsGeoPackageRasterWriterTask
|
||||
|
||||
---------------------
|
||||
begin : 23.8.2017
|
||||
copyright : (C) 2017 by Alessandro Pasotti
|
||||
email : apasotti at boundlessgeo dot 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. *
|
||||
* *
|
||||
***************************************************************************/
|
||||
#ifndef QGSGEOPACKAGERASTERWRITERTASK_H
|
||||
#define QGSGEOPACKAGERASTERWRITERTASK_H
|
||||
|
||||
|
||||
///@cond PRIVATE
|
||||
|
||||
#define SIP_NO_FILE
|
||||
|
||||
#include "qgis_core.h"
|
||||
#include "qgsgeopackagerasterwriter.h"
|
||||
#include "qgstaskmanager.h"
|
||||
#include "qgsfeedback.h"
|
||||
|
||||
|
||||
/**
|
||||
* \class QgsGeoPackageRasterWriterTask
|
||||
* QgsTask task which performs a QgsGeoPackageRasterWriter layer saving operation as a background
|
||||
* task. This can be used to save a raster layer out to a file without blocking the
|
||||
* QGIS interface.
|
||||
* \since QGIS 3.0
|
||||
* \see QgsGeoPackageRasterWriterTask
|
||||
*/
|
||||
class QgsGeoPackageRasterWriterTask : public QgsTask
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Constructor for QgsVectorFileWriterTask. Takes a source \a layer, destination \a fileName
|
||||
* and save \a options.
|
||||
*/
|
||||
QgsGeoPackageRasterWriterTask( const QgsMimeDataUtils::Uri sourceUri, const QString destinationPath );
|
||||
|
||||
virtual void cancel() override;
|
||||
|
||||
signals:
|
||||
|
||||
/**
|
||||
* Emitted when writing the layer is successfully completed. The \a newFilename
|
||||
* parameter indicates the file path for the written file.
|
||||
*/
|
||||
void writeComplete( const QString &newFilename );
|
||||
|
||||
/**
|
||||
* Emitted when an error occurs which prevented the file being written (or if
|
||||
* the task is canceled). The writing \a error and \a errorMessage will be reported.
|
||||
*/
|
||||
void errorOccurred( QgsGeoPackageRasterWriter::WriterError error, const QString &errorMessage );
|
||||
|
||||
protected:
|
||||
|
||||
virtual bool run() override;
|
||||
virtual void finished( bool result ) override;
|
||||
|
||||
private:
|
||||
|
||||
QgsGeoPackageRasterWriter mWriter;
|
||||
std::unique_ptr< QgsFeedback > mFeedback;
|
||||
QgsGeoPackageRasterWriter::WriterError mError = QgsGeoPackageRasterWriter::WriterError::NoError ;
|
||||
QString mErrorMessage;
|
||||
|
||||
};
|
||||
|
||||
|
||||
///@endcond
|
||||
|
||||
#endif // QGSGEOPACKAGERASTERWRITERTASK_H
|
@ -16,6 +16,7 @@ email : sherman at mrcc.com
|
||||
***************************************************************************/
|
||||
|
||||
#include "qgsogrprovider.h"
|
||||
#include "qgscplerrorhandler.h"
|
||||
#include "qgsogrfeatureiterator.h"
|
||||
#include "qgslogger.h"
|
||||
#include "qgsmessagelog.h"
|
||||
@ -80,31 +81,6 @@ static const QString TEXT_PROVIDER_DESCRIPTION =
|
||||
|
||||
static OGRwkbGeometryType ogrWkbGeometryTypeFromName( const QString &typeName );
|
||||
|
||||
class QgsCPLErrorHandler
|
||||
{
|
||||
static void CPL_STDCALL showError( CPLErr errClass, int errNo, const char *msg )
|
||||
{
|
||||
if ( errNo != OGRERR_NONE )
|
||||
QgsMessageLog::logMessage( QObject::tr( "OGR[%1] error %2: %3" ).arg( errClass ).arg( errNo ).arg( msg ), QObject::tr( "OGR" ) );
|
||||
}
|
||||
|
||||
public:
|
||||
QgsCPLErrorHandler()
|
||||
{
|
||||
CPLPushErrorHandler( showError );
|
||||
}
|
||||
|
||||
~QgsCPLErrorHandler()
|
||||
{
|
||||
CPLPopErrorHandler();
|
||||
}
|
||||
|
||||
private:
|
||||
QgsCPLErrorHandler( const QgsCPLErrorHandler &other );
|
||||
QgsCPLErrorHandler &operator=( const QgsCPLErrorHandler &other );
|
||||
|
||||
};
|
||||
|
||||
|
||||
bool QgsOgrProvider::convertField( QgsField &field, const QTextCodec &encoding )
|
||||
{
|
||||
@ -593,7 +569,6 @@ QString QgsOgrProvider::ogrWkbGeometryTypeName( OGRwkbGeometryType type ) const
|
||||
QString geom;
|
||||
|
||||
// GDAL 2.1 can return M/ZM geometries
|
||||
#if defined(GDAL_COMPUTE_VERSION) && GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,1,0)
|
||||
if ( wkbHasM( type ) )
|
||||
{
|
||||
geom = ogrWkbGeometryTypeName( wkbFlatten( type ) );
|
||||
@ -603,7 +578,6 @@ QString QgsOgrProvider::ogrWkbGeometryTypeName( OGRwkbGeometryType type ) const
|
||||
geom += "M";
|
||||
return geom;
|
||||
}
|
||||
#endif
|
||||
|
||||
switch ( ( long )type )
|
||||
{
|
||||
@ -4365,10 +4339,10 @@ QGISEXTERN bool deleteLayer( const QString &uri, QString &errCause )
|
||||
errCause = QObject::tr( "Success" );
|
||||
break;
|
||||
}
|
||||
errCause = QObject::tr( "GDAL result code: %s" ).arg( errCause );
|
||||
errCause = QObject::tr( "GDAL result code: %1" ).arg( errCause );
|
||||
return error == OGRERR_NONE;
|
||||
}
|
||||
// This should never happen:
|
||||
errCause = QObject::tr( "Layer not found: %s" ).arg( uri );
|
||||
errCause = QObject::tr( "Layer not found: %1" ).arg( uri );
|
||||
return false;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user