diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index a61d56461030..0a40502c8805 100644 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -33,11 +33,6 @@ qgis:convertgeometrytype: > See the "Polygonize" or "Lines to polygons" algorithm for alternative options. -qgis:definecurrentprojection: > - This algorithm sets an existing Shapefile's projection to the provided CRS. Contrary to the "Assign projection" algorithm, it will not output a new layer. - - The .prj and .qpj files associated with the Shapefile will be overwritten - or created if missing - to match the provided CRS. - qgis:distancematrix: > This algorithm creates a table containing a distance matrix, with distances between all the points in a points layer. diff --git a/python/plugins/processing/algs/qgis/DefineProjection.py b/python/plugins/processing/algs/qgis/DefineProjection.py deleted file mode 100644 index 5b1db6673b50..000000000000 --- a/python/plugins/processing/algs/qgis/DefineProjection.py +++ /dev/null @@ -1,110 +0,0 @@ -""" -*************************************************************************** - DefineProjection.py - --------------------- - Date : January 2016 - Copyright : (C) 2016 by Alexander Bruy - Email : alexander dot bruy at gmail 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. * -* * -*************************************************************************** -""" - -__author__ = "Alexander Bruy" -__date__ = "January 2016" -__copyright__ = "(C) 2016, Alexander Bruy" - -import os -import re - -from qgis.core import ( - QgsProcessing, - QgsProcessingAlgorithm, - QgsProcessingParameterVectorLayer, - QgsProcessingParameterCrs, - QgsProcessingOutputVectorLayer, - QgsCoordinateReferenceSystem, - QgsProjUtils, -) - -from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm - -pluginPath = os.path.split(os.path.split(os.path.dirname(__file__))[0])[0] - - -class DefineProjection(QgisAlgorithm): - INPUT = "INPUT" - CRS = "CRS" - - def group(self): - return self.tr("Vector general") - - def groupId(self): - return "vectorgeneral" - - def __init__(self): - super().__init__() - - def initAlgorithm(self, config=None): - self.addParameter( - QgsProcessingParameterVectorLayer( - self.INPUT, - self.tr("Input Shapefile"), - types=[QgsProcessing.SourceType.TypeVectorAnyGeometry], - ) - ) - self.addParameter(QgsProcessingParameterCrs(self.CRS, "CRS", "EPSG:4326")) - self.addOutput( - QgsProcessingOutputVectorLayer(self.INPUT, self.tr("Layer with projection")) - ) - - def name(self): - return "definecurrentprojection" - - def displayName(self): - return self.tr("Define Shapefile projection") - - def tags(self): - return self.tr("layer,shp,prj,qpj,change,alter").split(",") - - def shortDescription(self): - return self.tr( - "Changes a Shapefile's projection to a new CRS without reprojecting features" - ) - - def flags(self): - return super().flags() | QgsProcessingAlgorithm.Flag.FlagNoThreading - - def processAlgorithm(self, parameters, context, feedback): - layer = self.parameterAsVectorLayer(parameters, self.INPUT, context) - crs = self.parameterAsCrs(parameters, self.CRS, context) - - provider = layer.dataProvider() - ds = provider.dataSourceUri() - p = re.compile(r"\|.*") - dsPath = p.sub("", ds) - - if dsPath.lower().endswith(".shp"): - dsPath = dsPath[:-4] - - wkt = crs.toWkt(QgsCoordinateReferenceSystem.WktVariant.WKT1_ESRI) - with open(dsPath + ".prj", "w", encoding="utf-8") as f: - f.write(wkt) - - qpjFile = dsPath + ".qpj" - if os.path.exists(qpjFile): - os.remove(qpjFile) - else: - feedback.pushConsoleInfo( - self.tr("Data source isn't a Shapefile, skipping .prj/.qpj creation") - ) - - layer.setCrs(crs) - layer.triggerRepaint() - - return {self.INPUT: layer} diff --git a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py index 6a540340b9cd..da1253351a96 100644 --- a/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QgisAlgorithmProvider.py @@ -29,7 +29,6 @@ from .BoxPlot import BoxPlot from .CheckValidity import CheckValidity from .Climb import Climb -from .DefineProjection import DefineProjection from .EliminateSelection import EliminateSelection from .ExecuteSQL import ExecuteSQL from .ExportGeometryInfo import ExportGeometryInfo @@ -94,7 +93,6 @@ def getAlgs(self): BoxPlot(), CheckValidity(), Climb(), - DefineProjection(), EliminateSelection(), ExecuteSQL(), ExportGeometryInfo(), diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 7c825293b3af..ef271e144e8a 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -80,6 +80,7 @@ set(QGIS_ANALYSIS_SRCS processing/qgsalgorithmcoveragevalidate.cpp processing/qgsalgorithmcreatedirectory.cpp processing/qgsalgorithmdbscanclustering.cpp + processing/qgsalgorithmdefineprojection.cpp processing/qgsalgorithmdelaunaytriangulation.cpp processing/qgsalgorithmdeleteduplicategeometries.cpp processing/qgsalgorithmdensifygeometriesbycount.cpp diff --git a/src/analysis/processing/qgsalgorithmdefineprojection.cpp b/src/analysis/processing/qgsalgorithmdefineprojection.cpp new file mode 100644 index 000000000000..1d733c1b2909 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmdefineprojection.cpp @@ -0,0 +1,137 @@ +/*************************************************************************** + qgsalgorithmdefineprojection.cpp + --------------------- + begin : February 2025 + copyright : (C) 2025 by Alexander Bruy + email : alexander dot bruy at gmail 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 "qgsalgorithmdefineprojection.h" +#include "qgsvectorlayer.h" +#include "qgsproviderregistry.h" + +///@cond PRIVATE + +QString QgsDefineProjectionAlgorithm::name() const +{ + return QStringLiteral( "definecurrentprojection" ); +} + +QString QgsDefineProjectionAlgorithm::displayName() const +{ + return QObject::tr( "Define projection" ); +} + +QStringList QgsDefineProjectionAlgorithm::tags() const +{ + return QObject::tr( "layer,shp,prj,qpj,change,alter" ).split( ',' ); +} + +QString QgsDefineProjectionAlgorithm::group() const +{ + return QObject::tr( "Vector general" ); +} + +QString QgsDefineProjectionAlgorithm::groupId() const +{ + return QStringLiteral( "vectorgeneral" ); +} + +QString QgsDefineProjectionAlgorithm::shortHelpString() const +{ + return QObject::tr( "Sets an existing layer's projection to the provided CRS without reprojecting features. " + "Contrary to the \"Assign projection\" algorithm, it will not output a new layer.\n\n" + "If the input layer is a shapefile, the .prj file will be overwritten — or created if " + "missing — to match the provided CRS." ); +} + +QgsDefineProjectionAlgorithm *QgsDefineProjectionAlgorithm::createInstance() const +{ + return new QgsDefineProjectionAlgorithm(); +} + +void QgsDefineProjectionAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterVectorLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input shapefile" ), QList() << static_cast( Qgis::ProcessingSourceType::VectorAnyGeometry ) ) ); + addParameter( new QgsProcessingParameterCrs( QStringLiteral( "CRS" ), QObject::tr( "CRS" ), QgsCoordinateReferenceSystem( "EPSG:4326" ) ) ); + addOutput( new QgsProcessingOutputVectorLayer( QStringLiteral( "OUTPUT" ), QObject::tr( "Layer with projection" ) ) ); +} + +bool QgsDefineProjectionAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + QgsVectorLayer *layer = parameterAsVectorLayer( parameters, QStringLiteral( "INPUT" ), context ); + const QgsCoordinateReferenceSystem crs = parameterAsCrs( parameters, QStringLiteral( "CRS" ), context ); + + if ( !layer ) + throw QgsProcessingException( QObject::tr( "Invalid input layer" ) ); + + mLayerId = layer->id(); + + if ( layer->providerType().compare( QStringLiteral( "ogr" ), Qt::CaseSensitivity::CaseInsensitive ) == 0 ) + { + const QVariantMap parts = QgsProviderRegistry::instance()->decodeUri( layer->providerType(), layer->dataProvider()->dataSourceUri() ); + QString layerPath; + if ( parts.size() > 0 ) + { + layerPath = parts.value( QStringLiteral( "path" ) ).toString(); + } + + if ( !layerPath.isEmpty() && layerPath.endsWith( QStringLiteral( ".shp" ), Qt::CaseSensitivity::CaseInsensitive ) ) + { + const QString filePath = layerPath.chopped( 4 ); + const QString wkt = crs.toWkt( Qgis::CrsWktVariant::Wkt1Esri ); + + QFile prjFile( filePath + QLatin1String( ".prj" ) ); + if ( prjFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) + { + QTextStream stream( &prjFile ); + stream << wkt << Qt::endl; + } + else + { + feedback->pushWarning( QObject::tr( "Failed to open .prj file for writing." ) ); + } + + QFile qpjFile( filePath + QLatin1String( ".qpj" ) ); + if ( qpjFile.exists() ) + { + qpjFile.remove(); + } + } + else + { + feedback->pushWarning( QObject::tr( "Data source isn't a shapefile, skipping .prj creation" ) ); + } + } + else + { + feedback->pushInfo( QObject::tr( "Data source isn't a shapefile, skipping .prj creation" ) ); + } + + layer->setCrs( crs ); + layer->triggerRepaint(); + + return true; +} + +QVariantMap QgsDefineProjectionAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( parameters ); + Q_UNUSED( context ); + Q_UNUSED( feedback ); + + QVariantMap results; + results.insert( QStringLiteral( "OUTPUT" ), mLayerId ); + return results; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmdefineprojection.h b/src/analysis/processing/qgsalgorithmdefineprojection.h new file mode 100644 index 000000000000..a99aaa111df6 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmdefineprojection.h @@ -0,0 +1,54 @@ +/*************************************************************************** + qgsalgorithmdefineprojection.h + --------------------- + begin : February 2025 + copyright : (C) 2025 by Alexander Bruy + email : alexander dot bruy at gmail 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 QGSALGORITHMDEFINEPROJECTION_H +#define QGSALGORITHMDEFINEPROJECTION_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Native define layer projection algorithm. + */ +class QgsDefineProjectionAlgorithm : public QgsProcessingAlgorithm +{ + public: + QgsDefineProjectionAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsDefineProjectionAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + bool prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + QVariantMap processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + + private: + QString mLayerId; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMDEFINEPROJECTION_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 06e58cc3eb64..0d71703d6e08 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -61,6 +61,7 @@ #include "qgsalgorithmcoveragevalidate.h" #include "qgsalgorithmcreatedirectory.h" #include "qgsalgorithmdbscanclustering.h" +#include "qgsalgorithmdefineprojection.h" #include "qgsalgorithmdelaunaytriangulation.h" #include "qgsalgorithmdeleteduplicategeometries.h" #include "qgsalgorithmdensifygeometriesbycount.h" @@ -350,6 +351,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsCoverageValidateAlgorithm() ); addAlgorithm( new QgsCreateDirectoryAlgorithm() ); addAlgorithm( new QgsDbscanClusteringAlgorithm() ); + addAlgorithm( new QgsDefineProjectionAlgorithm() ); addAlgorithm( new QgsDelaunayTriangulationAlgorithm() ); addAlgorithm( new QgsDeleteDuplicateGeometriesAlgorithm() ); addAlgorithm( new QgsDetectVectorChangesAlgorithm() ); diff --git a/tests/src/analysis/testqgsprocessingalgspt2.cpp b/tests/src/analysis/testqgsprocessingalgspt2.cpp index b0be3b931738..9ffd8e09732d 100644 --- a/tests/src/analysis/testqgsprocessingalgspt2.cpp +++ b/tests/src/analysis/testqgsprocessingalgspt2.cpp @@ -113,6 +113,8 @@ class TestQgsProcessingAlgsPt2 : public QgsTest void nativeAlgsRasterSize(); + void defineProjection(); + private: QString mPointLayerPath; QgsVectorLayer *mPointsLayer = nullptr; @@ -2241,5 +2243,50 @@ void TestQgsProcessingAlgsPt2::nativeAlgsRasterSize() QCOMPARE( densityRasterLayer->width(), gdalRasterLayer->width() ); } +void TestQgsProcessingAlgsPt2::defineProjection() +{ + auto layer = std::make_unique( QStringLiteral( "Point" ), QStringLiteral( "input" ), QStringLiteral( "memory" ) ); + QVERIFY( layer->isValid() ); + + std::unique_ptr alg( QgsApplication::processingRegistry()->createAlgorithmById( QStringLiteral( "native:definecurrentprojection" ) ) ); + QVERIFY( alg != nullptr ); + + QVariantMap parameters; + parameters.insert( QStringLiteral( "INPUT" ), QVariant::fromValue( layer.get() ) ); + parameters.insert( QStringLiteral( "CRS" ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ) ); + + bool ok = false; + auto context = std::make_unique(); + QgsProcessingFeedback feedback; + QVariantMap results; + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + + QCOMPARE( results.value( QStringLiteral( "OUTPUT" ) ), layer->id() ); + QVERIFY( layer->crs().isValid() ); + QCOMPARE( layer->crs().authid(), QStringLiteral( "EPSG:3857" ) ); + + // check that .prj file is create and .qpj file is deleted + const QTemporaryDir tmpPath; + const QString dataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt + QFile::copy( dataDir + "/points.shp", tmpPath.filePath( QStringLiteral( "points.shp" ) ) ); + QFile::copy( dataDir + "/points.shx", tmpPath.filePath( QStringLiteral( "points.shx" ) ) ); + QFile::copy( dataDir + "/points.dbf", tmpPath.filePath( QStringLiteral( "points.dbf" ) ) ); + QFile::copy( dataDir + "/points.qpj", tmpPath.filePath( QStringLiteral( "points.qpj" ) ) ); + + layer.reset( new QgsVectorLayer( tmpPath.filePath( QStringLiteral( "points.shp" ) ), QStringLiteral( "input" ), QStringLiteral( "ogr" ) ) ); + QVERIFY( layer->isValid() ); + QVERIFY( !layer->crs().isValid() ); + + parameters.insert( QStringLiteral( "INPUT" ), QVariant::fromValue( layer.get() ) ); + ok = false; + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + QFile prjFile( tmpPath.filePath( QStringLiteral( "points.prj" ) ) ); + QVERIFY( prjFile.exists() ); + QFile qpjFile( tmpPath.filePath( QStringLiteral( "points.qpj" ) ) ); + QVERIFY( !qpjFile.exists() ); +} + QGSTEST_MAIN( TestQgsProcessingAlgsPt2 ) #include "testqgsprocessingalgspt2.moc"