diff --git a/.ci/azure-pipelines/azure-pipelines.yml b/.ci/azure-pipelines/azure-pipelines.yml index 95746bc21e29..4ea567b391e4 100644 --- a/.ci/azure-pipelines/azure-pipelines.yml +++ b/.ci/azure-pipelines/azure-pipelines.yml @@ -1,7 +1,7 @@ variables: - LR: release-3_10 - LTR: release-3_4 - CTEST_CUSTOM_TESTS_IGNORE: "ProcessingGdalAlgorithmsRasterTest;ProcessingGdalAlgorithmsVectorTest;ProcessingGrass7AlgorithmsImageryTest;ProcessingGrass7AlgorithmsRasterTest;ProcessingGrass7AlgorithmsVectorTest;ProcessingGuiTest;ProcessingOtbAlgorithmsTest;ProcessingQgisAlgorithmsTestPt1;ProcessingQgisAlgorithmsTestPt2;ProcessingQgisAlgorithmsTestPt3;ProcessingQgisAlgorithmsTestPt4;ProcessingScriptUtilsTest;PyQgsAnnotation;PyQgsAppStartup;PyQgsAuthManagerOAuth2OWSTest;PyQgsAuthManagerPasswordOWSTest;PyQgsAuthManagerPKIOWSTest;PyQgsAuthManagerProxy;PyQgsAuthSettingsWidget;PyQgsAuxiliaryStorage;PyQgsBlockingNetworkRequest;PyQgsExifTools;PyQgsFileDownloader;PyQgsFileUtils;PyQgsGeometryTest;PyQgsImageCache;PyQgsImportIntoPostGIS;PyQgsLayoutAtlas;PyQgsLayoutLegend;PyQgsLayoutMap;PyQgsLayoutMapGrid;PyQgsMapLayer;PyQgsOfflineEditingWFS;PyQgsOGRProvider;PyQgsOGRProviderGpkg;PyQgsOGRProviderSqlite;PyQgsPalLabelingCanvas;PyQgsPalLabelingLayout;PyQgsPalLabelingPlacement;PyQgsPointDisplacementRenderer;PyQgsProject;PyQgsProviderConnectionGpkg;PyQgsProviderConnectionPostgres;PyQgsPythonProvider;PyQgsRasterFileWriter;PyQgsRasterLayer;PyQgsSelectiveMasking;PyQgsServerAccessControlWMSGetlegendgraphic;PyQgsServerApi;PyQgsServerCacheManager;PyQgsServerLocaleOverride;PyQgsServerSecurity;PyQgsServerSettings;PyQgsServerWMS;PyQgsServerWMSDimension;PyQgsServerWMSGetFeatureInfo;PyQgsServerWMSGetLegendGraphic;PyQgsServerWMSGetMap;PyQgsServerWMSGetPrint;PyQgsServerWMTS;PyQgsSettings;PyQgsShapefileProvider;PyQgsSpatialiteProvider;PyQgsSvgCache;PyQgsSymbolLayer;PyQgsTaskManager;PyQgsTextRenderer;PyQgsVectorFileWriter;PyQgsVectorLayer;PyQgsVectorLayerUtils;PyQgsVirtualLayerProvider;PyQgsWFSProviderGUI;PyQgsZipUtils;qgis_3drenderingtest;qgis_alignrastertest;qgis_arcgisrestutilstest;qgis_banned_keywords;qgis_browsermodeltest;qgis_callouttest;qgis_compositionconvertertest;qgis_coordinatereferencesystemtest;qgis_datadefinedsizelegendtest;qgis_datumtransformdialog;qgis_diagramtest;qgis_doxygen_order;qgis_dxfexporttest;qgis_expressiontest;qgis_filedownloader;qgis_geometrycheckstest;qgis_geometrytest;qgis_geonodeconnectiontest;qgis_grassprovidertest7;qgis_imagecachetest;qgis_invertedpolygonrenderertest;qgis_labelingenginetest;qgis_layerdefinitiontest;qgis_layout3dmaptest;qgis_layouthtmltest;qgis_layoutlabeltest;qgis_layoutmapgridtest;qgis_layoutmaptest;qgis_layoutpicturetest;qgis_layoutscalebartest;qgis_layouttabletest;qgis_legendrenderertest;qgis_licenses;qgis_maprendererjobtest;qgis_maprotationtest;qgis_mapsettingsutilstest;qgis_maptooladdfeatureline;qgis_mimedatautilstest;qgis_networkaccessmanagertest;qgis_openclutilstest;qgis_painteffecttest;qgis_pallabelingtest;qgis_processingtest;qgis_projecttest;qgis_qgisappclipboard;qgis_rasterlayersaveasdialog;qgis_shellcheck;qgis_sipify;qgis_sip_include;qgis_sip_uptodate;qgis_spelling;qgis_styletest;qgis_svgcachetest;qgis_taskmanagertest;qgis_transformdialog;qgis_vectorfilewritertest;qgis_wcsprovidertest;qgis_ziplayertest;qgis_meshcalculator;qgis_pointlocatortest;PyQgsExpressionBuilderWidget;PyQgsDatumTransform" + LR: release-3_12 + LTR: release-3_10 + CTEST_CUSTOM_TESTS_IGNORE: "ProcessingGdalAlgorithmsRasterTest;ProcessingGdalAlgorithmsVectorTest;ProcessingGrass7AlgorithmsImageryTest;ProcessingGrass7AlgorithmsRasterTest;ProcessingGrass7AlgorithmsVectorTest;ProcessingGuiTest;ProcessingOtbAlgorithmsTest;ProcessingQgisAlgorithmsTestPt1;ProcessingQgisAlgorithmsTestPt2;ProcessingQgisAlgorithmsTestPt3;ProcessingQgisAlgorithmsTestPt4;ProcessingScriptUtilsTest;PyQgsAnnotation;PyQgsAppStartup;PyQgsAuthManagerOAuth2OWSTest;PyQgsAuthManagerPasswordOWSTest;PyQgsAuthManagerPKIOWSTest;PyQgsAuthManagerProxy;PyQgsAuthSettingsWidget;PyQgsAuxiliaryStorage;PyQgsBlockingNetworkRequest;PyQgsExifTools;PyQgsFileDownloader;PyQgsFileUtils;PyQgsGeometryTest;PyQgsImageCache;PyQgsImportIntoPostGIS;PyQgsLayoutAtlas;PyQgsLayoutLegend;PyQgsLayoutMap;PyQgsLayoutMapGrid;PyQgsMapLayer;PyQgsOfflineEditingWFS;PyQgsOGRProvider;PyQgsOGRProviderGpkg;PyQgsOGRProviderSqlite;PyQgsPalLabelingCanvas;PyQgsPalLabelingLayout;PyQgsPalLabelingPlacement;PyQgsPointDisplacementRenderer;PyQgsProject;PyQgsProviderConnectionGpkg;PyQgsProviderConnectionPostgres;PyQgsPythonProvider;PyQgsRasterFileWriter;PyQgsRasterLayer;PyQgsSelectiveMasking;PyQgsServerAccessControlWMSGetlegendgraphic;PyQgsServerApi;PyQgsServerCacheManager;PyQgsServerLocaleOverride;PyQgsServerSecurity;PyQgsServerSettings;PyQgsServerWMS;PyQgsServerWMSDimension;PyQgsServerWMSGetFeatureInfo;PyQgsServerWMSGetLegendGraphic;PyQgsServerWMSGetMap;PyQgsServerWMSGetPrint;PyQgsServerWMTS;PyQgsSettings;PyQgsShapefileProvider;PyQgsSpatialiteProvider;PyQgsSvgCache;PyQgsSymbolLayer;PyQgsTaskManager;PyQgsTextRenderer;PyQgsVectorFileWriter;PyQgsVectorLayer;PyQgsVectorLayerUtils;PyQgsVirtualLayerProvider;PyQgsWFSProviderGUI;PyQgsZipUtils;qgis_3drenderingtest;qgis_alignrastertest;qgis_arcgisrestutilstest;qgis_banned_keywords;qgis_browsermodeltest;qgis_callouttest;qgis_compositionconvertertest;qgis_coordinatereferencesystemtest;qgis_datadefinedsizelegendtest;qgis_datumtransformdialog;qgis_diagramtest;qgis_doxygen_order;qgis_dxfexporttest;qgis_expressiontest;qgis_filedownloader;qgis_geometrycheckstest;qgis_geometrytest;qgis_geonodeconnectiontest;qgis_grassprovidertest7;qgis_imagecachetest;qgis_invertedpolygonrenderertest;qgis_labelingenginetest;qgis_layerdefinitiontest;qgis_layout3dmaptest;qgis_layouthtmltest;qgis_layoutlabeltest;qgis_layoutmapgridtest;qgis_layoutmaptest;qgis_layoutpicturetest;qgis_layoutscalebartest;qgis_layouttabletest;qgis_legendrenderertest;qgis_licenses;qgis_maprendererjobtest;qgis_maprotationtest;qgis_mapsettingsutilstest;qgis_maptooladdfeatureline;qgis_mimedatautilstest;qgis_networkaccessmanagertest;qgis_openclutilstest;qgis_painteffecttest;qgis_pallabelingtest;qgis_processingtest;qgis_projecttest;qgis_qgisappclipboard;qgis_rasterlayersaveasdialog;qgis_shellcheck;qgis_sipify;qgis_sip_include;qgis_sip_uptodate;qgis_spelling;qgis_styletest;qgis_svgcachetest;qgis_taskmanagertest;qgis_transformdialog;qgis_vectorfilewritertest;qgis_wcsprovidertest;qgis_ziplayertest;qgis_meshcalculator;qgis_pointlocatortest;PyQgsExpressionBuilderWidget;PyQgsDatumTransform;qgis_vertextool;PyQgsCoordinateOperationWidget;PyQgsProviderConnectionSpatialite;qgis_maptoolsplitpartstest;qgis_vectortilelayertest" Agent.Source.Git.ShallowFetchDepth: 120 trigger: @@ -90,7 +90,7 @@ jobs: url=${url//(/%28} url=${url//)/%29} url=${url// /+} - url="https://cdash.orfeo-toolbox.org/index.php?project=QGIS&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercount=2&showfilters=1&filtercombine=and&field1=buildname&compare1=61&value1=$url&field2=site&compare2=61&value2=azure-pipelines" + url="https://cdash.orfeo-toolbox.org/index.php?project=QGIS&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercombine=and&filtercount=4&showfilters=0&filtercombine=and&field1=buildname&compare1=61&value1=$url&field2=site&compare2=65&value2=azure-pipelines&field3=buildstarttime&compare3=83&value3=$(date +%Y-%m-%d --date=yesterday)&field4=buildstarttime&compare4=84&value4=$(date +%Y-%m-%d --date=tomorrow)" echo "##vso[task.setvariable variable=TARGET]$target" echo "##vso[task.setvariable variable=OSGEO4W_PKG]$OSGEO4W_PKG" @@ -101,7 +101,7 @@ jobs: echo "##vso[task.setvariable variable=BINARY]$binary" echo "##vso[task.setvariable variable=VERSION]$version" echo "##vso[task.setvariable variable=BUILDNAME]$buildname" - echo "##vso[task.setvariable variable=DASHURL]$url" + echo "##vso[task.setvariable variable=DASHURL]${url//&/^&}" displayName: 'Setup build variables' @@ -183,7 +183,7 @@ jobs: cd ms-windows\osgeo4w touch skippackage set OSGEO4W_CXXFLAGS=/MD /MP /Od /D NDEBUG - @echo "##[section] %OSGEO4W_ARCH% results available at %DASHURL%" + @echo ##[section]%OSGEO4W_ARCH% results available at %DASHURL% package-nightly.cmd %VERSION% %BINARY% %OSGEO4W_PKG% %OSGEO4W_ARCH% %BUILD_SOURCEVERSION:~0,10% azure-pipelines displayName: 'Building QGIS' diff --git a/.ci/travis/linux/scripts/test_blacklist.txt b/.ci/travis/linux/scripts/test_blacklist.txt index 95bb03fe69da..866579f9fb09 100644 --- a/.ci/travis/linux/scripts/test_blacklist.txt +++ b/.ci/travis/linux/scripts/test_blacklist.txt @@ -21,3 +21,5 @@ qgis_openclutilstest # Relies on a broken/unreliable 3rd party service qgis_layerdefinition +# MSSQL requires the MSSQL docker +PyQgsProviderConnectionMssql diff --git a/.ci/travis/linux/scripts/test_flaky.txt b/.ci/travis/linux/scripts/test_flaky.txt index 62c69816b39f..404e5e68ed6d 100644 --- a/.ci/travis/linux/scripts/test_flaky.txt +++ b/.ci/travis/linux/scripts/test_flaky.txt @@ -4,9 +4,6 @@ qgis_wcsprovidertest PyQgsWFSProviderGUI qgis_ziplayertest -# Flaky, see https://dash.orfeo-toolbox.org/testDetails.php?test=63061783&build=297405 -PyQgsSpatialiteProvider - # Flaky, the ms odbc driver crashes a lot on the ubuntu docker image. Retest when # the docker base image is upgraded PyQgsMssqlProvider diff --git a/.docker/qgis.dockerfile b/.docker/qgis.dockerfile index f7f42322cb95..15b8853002af 100644 --- a/.docker/qgis.dockerfile +++ b/.docker/qgis.dockerfile @@ -54,6 +54,9 @@ RUN cmake \ && SUCCESS=OK \ && timeout ${BUILD_TIMEOUT}s ninja install || SUCCESS=TIMEOUT \ && echo "$SUCCESS" > /QGIS/build_exit_value + +# Additional run-time dependencies +RUN pip3 install jinja2 pygments ################################################################################ ARG DELETE_CACHE=FALSE @@ -77,7 +80,6 @@ COPY .docker/qgis_resources/supervisor/ /etc/supervisor # needed to find PyQt wrapper provided by QGIS ENV PYTHONPATH=/usr/share/qgis/python/:/usr/share/qgis/python/plugins:/usr/lib/python3/dist-packages/qgis:/usr/share/qgis/python/qgis - WORKDIR / # Run supervisor diff --git a/.docker/qgis3-build-deps.dockerfile b/.docker/qgis3-build-deps.dockerfile index 511ff683e289..9e7cc2113ef8 100644 --- a/.docker/qgis3-build-deps.dockerfile +++ b/.docker/qgis3-build-deps.dockerfile @@ -35,6 +35,7 @@ RUN apt-get update \ libgsl-dev \ libpq-dev \ libproj-dev \ + libprotobuf-dev \ libqca-qt5-2-dev \ libqca-qt5-2-plugins \ libqt53drender5 \ @@ -63,6 +64,7 @@ RUN apt-get update \ pkg-config \ poppler-utils \ postgresql-client \ + protobuf-compiler \ pyqt5-dev \ pyqt5-dev-tools \ pyqt5.qsci-dev \ @@ -157,7 +159,7 @@ RUN locale-gen RUN echo "alias python=python3" >> ~/.bash_aliases # OTB: download and install otb packages for QGIS tests -RUN curl -k https://www.orfeo-toolbox.org/packages/OTB-7.0.0-Linux64.run -o /tmp/OTB-Linux64.run && sh /tmp/OTB-Linux64.run --target /opt/otb +RUN curl -k https://www.orfeo-toolbox.org/packages/OTB-7.1.0-Linux64.run -o /tmp/OTB-Linux64.run && sh /tmp/OTB-Linux64.run --target /opt/otb ENV OTB_INSTALL_DIR=/opt/otb # Clazy diff --git a/.docker/qgis_resources/test_runner/qgis_setup.sh b/.docker/qgis_resources/test_runner/qgis_setup.sh index 12abaa83b55c..7be7a862c706 100644 --- a/.docker/qgis_resources/test_runner/qgis_setup.sh +++ b/.docker/qgis_resources/test_runner/qgis_setup.sh @@ -44,7 +44,12 @@ if [[ -n "$PLUGIN_NAME" ]]; then printf "%s=true\n\n" "$PLUGIN_NAME" >> $CONF_MASTER_FILE # Install the plugin if [ ! -d "${PLUGIN_MASTER_FOLDER}/${PLUGIN_NAME}" ]; then - ln -s "/tests_directory/${PLUGIN_NAME}" "${PLUGIN_MASTER_FOLDER}" + plugin_dir="/tests_directory/${PLUGIN_NAME}" + if [ ! -d "${plugin_dir}" ]; then + echo "ERROR: ${plugin_dir} does not exist" >&2 + exit 1 + fi + ln -s "${plugin_dir}" "${PLUGIN_MASTER_FOLDER}" echo "Plugin master folder linked in ${PLUGIN_MASTER_FOLDER}/${PLUGIN_NAME}" fi fi diff --git a/.github/ISSUE_TEMPLATE/00_question.md b/.github/ISSUE_TEMPLATE/00_question.md new file mode 100644 index 000000000000..c9ee8b85cda1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/00_question.md @@ -0,0 +1,15 @@ +--- +name: ❓ Question about QGIS +about: ❗️ You should raise your question on another channel. +labels: "Won't fix" + +--- + +IMPORTANT: + +Do NOT use GitHub to post any questions or support requests! +They will be closed immediately and ignored. +Questions should go to the qgis-user mailing list at https://lists.osgeo.org/mailman/listinfo/qgis-user +or other support forums such as https://gis.stackexchange.com/. + +GitHub issues are for bug reports and suggestions for new features. diff --git a/.github/ISSUE_TEMPLATE/05_plugin.md b/.github/ISSUE_TEMPLATE/05_plugin.md new file mode 100644 index 000000000000..bd9ecdb76ec3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/05_plugin.md @@ -0,0 +1,12 @@ +--- +name: 🐞 Bug using a plugin +about: ❗️ Report to the plugin author instead +labels: "Won't fix" +--- + +IMPORTANT: + +If the issue concerns a third party plugin (downloaded with the plugin manager) then it can't be fixed by the QGIS team. +Please raise your issue in the dedicated bug tracker for that specific plugin (as listed in the plugin's description) + +Tickets created here will be closed. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/10_bug_report.md similarity index 79% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/10_bug_report.md index 4beb1f6b4182..a21af193701d 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/10_bug_report.md @@ -15,8 +15,7 @@ Checklist before submitting - [ ] Search through existing issue reports and gis.stackexchange.com to check whether the issue already exists - [ ] Test with a [clean new user profile](https://docs.qgis.org/testing/en/docs/user_manual/introduction/qgis_configuration.html?highlight=profile#working-with-user-profiles). - [ ] Create a light and self-contained sample dataset and project file which demonstrates the issue - -If the issue concerns a **third party plugin**, then it **cannot** be fixed by the QGIS team. Please raise your issue in the dedicated bug tracker for that specific plugin (as listed in the plugin's description). --> +--> **Describe the bug** @@ -32,7 +31,7 @@ If the issue concerns a **third party plugin**, then it **cannot** be fixed by t **QGIS and OS versions** - + **Additional context** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/15_feature_request.md similarity index 100% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to .github/ISSUE_TEMPLATE/15_feature_request.md diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d465c09292c4..928924e6ed65 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Backport Bot if: github.event.pull_request.merged && ( ( github.event.action == 'closed' && contains( join( github.event.pull_request.labels.*.name ), 'backport') ) || contains( github.event.label.name, 'backport' ) ) - uses: Gaurav0/backport@v1.0.24 + uses: Gaurav0/backport@v1.0.26 with: bot_username: qgis-bot bot_token: ddbdec32940df79f1adf2369b4b10f10b5a66f65 diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml new file mode 100644 index 000000000000..2b4fb991c071 --- /dev/null +++ b/.github/workflows/macos-build.yml @@ -0,0 +1,41 @@ +name: Mac OS build +on: [push, pull_request] +env: + QT_VERSION: 5.14.1 + QGIS_DEPS_VERSION: 0.2.2 + +jobs: + mac_os_build: + if: github.repository == 'qgis/QGIS' + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + + - name: download qgis-deps + run: | + wget https://qgis.org/downloads/macos/deps/qt-${QT_VERSION}.tar.gz + wget https://qgis.org/downloads/macos/deps/qgis-deps-${QGIS_DEPS_VERSION}.tar.gz + wget https://qgis.org/downloads/macos/deps/install_qgis_deps-${QGIS_DEPS_VERSION}.bash + + - name: install qgis-deps + run: | + chmod +x ./install_qgis_deps-${QGIS_DEPS_VERSION}.bash + sudo ./install_qgis_deps-${QGIS_DEPS_VERSION}.bash + + - name: create build system + run: | + QGIS_DIR=`pwd` + mkdir -p ../build-QGIS + cd ../build-QGIS + + PATH=/opt/QGIS/qgis-deps-${QGIS_DEPS_VERSION}/stage/bin:$PATH \ + cmake -DQGIS_MAC_DEPS_DIR=/opt/QGIS/qgis-deps-${QGIS_DEPS_VERSION}/stage \ + -DCMAKE_PREFIX_PATH=/opt/Qt/${QT_VERSION}/clang_64 \ + -DWITH_BINDINGS=TRUE \ + -DWITH_3D=TRUE \ + $QGIS_DIR + + - name: build QGIS + run: | + cd ../build-QGIS + make -j $(sysctl -n hw.ncpu) diff --git a/.github/workflows/pr-auto-milestone.yml b/.github/workflows/pr-auto-milestone.yml index e87fea1ff679..4904b50a6bc2 100644 --- a/.github/workflows/pr-auto-milestone.yml +++ b/.github/workflows/pr-auto-milestone.yml @@ -76,7 +76,7 @@ jobs: echo "RE_RUN_JOB: ${RE_RUN_JOB}" # Get the base branch - BASE_BRANCH=$(echo "${JSON_DATA}" | jq ".repository.pullRequests.edges[] | select( .node.number == ${PR_NUMBER} ) | .node.baseRef.name") + BASE_BRANCH=$(echo "${JSON_DATA}" | jq -r ".repository.pullRequests.edges[] | select( .node.number == ${PR_NUMBER} ) | .node.baseRef.name") echo "BASE_BRANCH: ${BASE_BRANCH}" # master => NOTHING, release_3-10 => _10 @@ -124,7 +124,7 @@ jobs: MILESTONE_NUMBER_EXISTING: ${{ steps.extract_data.outputs.milestone_number }} MILESTONE_NUMBER_CREATED_JSON: ${{ steps.create_milestone.outputs.data }} run: | - FINAL_MILESTONE_NUMBER=$([[ -n ${MILESTONE_NUMBER_EXISTING} ]] && echo "${MILESTONE_NUMBER_EXISTING}" || $(echo "${MILESTONE_NUMBER_CREATED_JSON}" | jq '.number' )) + FINAL_MILESTONE_NUMBER=$([[ -n ${MILESTONE_NUMBER_EXISTING} ]] && echo "${MILESTONE_NUMBER_EXISTING}" || echo $(echo "${MILESTONE_NUMBER_CREATED_JSON}" | jq .number )) echo "FINAL_MILESTONE_NUMBER: ${FINAL_MILESTONE_NUMBER}" echo "::set-output name=milestone_number::${FINAL_MILESTONE_NUMBER}" diff --git a/.github/workflows/pr_to_doc_issue.yml b/.github/workflows/pr_to_doc_issue.yml index 5f0e7761c086..27d36ec82ba9 100644 --- a/.github/workflows/pr_to_doc_issue.yml +++ b/.github/workflows/pr_to_doc_issue.yml @@ -84,7 +84,7 @@ jobs: token: ${{ steps.token.outputs.token }} owner: qgis repo: QGIS-Documentation - title: ${{ format('Request in QGIS ({0})', github.event.pull_request.title) }} + title: ${{ format('{0} (Request in QGIS)', github.event.pull_request.title) }} # do not modify the QGIS version, an action automatically creates a label in the doc repo # this is not possible to set labels directly due to security reasons # the token is in clear, so no rights are given to qgis-bot diff --git a/.gitignore b/.gitignore index 1af4ce54c2ab..226c965b9c95 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ *.sortinc *.tex *.toc +*.tmp *~ *-stamp .*.swp diff --git a/CMakeLists.txt b/CMakeLists.txt index c7f145d52578..49249b8ed279 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,20 @@ ENDIF (APPLE) MATH(EXPR QGIS_VERSION_INT "${CPACK_PACKAGE_VERSION_MAJOR}*10000+${CPACK_PACKAGE_VERSION_MINOR}*100+${CPACK_PACKAGE_VERSION_PATCH}") MESSAGE(STATUS "QGIS version: ${COMPLETE_VERSION} ${RELEASE_NAME} (${QGIS_VERSION_INT})") +############################################################# +IF (APPLE) + # QGIS custom dependencies package from qgis/QGIS-Mac-Packager + # they can be downloaded from https://qgis.org/downloads/macos/qgis-deps + # and extracted to /opt/QGIS/qgis-deps-/stage + SET (QGIS_MAC_DEPS_DIR "" CACHE PATH "Path to QGIS Mac custom dependencies directory") + + # Setup LIB_DIR and CMAKE_PREFIX_PATH to help CMake's + # find_packages to look for these libraries instead of system libraries + IF ( QGIS_MAC_DEPS_DIR ) + SET(ENV{LIB_DIR} ${QGIS_MAC_DEPS_DIR}) + LIST(APPEND CMAKE_PREFIX_PATH ${QGIS_MAC_DEPS_DIR}) + ENDIF ( QGIS_MAC_DEPS_DIR ) +ENDIF (APPLE) ############################################################# # Configure OpenCL if available @@ -331,6 +345,14 @@ IF(WITH_CORE) MESSAGE (SEND_ERROR "sqlite3 dependency was not found!") ENDIF (NOT SQLITE3_FOUND) + FIND_PACKAGE(Protobuf REQUIRED) # for decoding of vector tiles in MVT format + MESSAGE(STATUS "Found Protobuf: ${Protobuf_LIBRARIES}") + IF (NOT Protobuf_PROTOC_EXECUTABLE) + MESSAGE (SEND_ERROR "Protobuf library's 'protoc' tool was not found!") + ENDIF () + FIND_PACKAGE(ZLIB REQUIRED) # for decompression of vector tiles in MBTiles file + MESSAGE(STATUS "Found zlib: ${ZLIB_LIBRARIES}") + # optional IF (WITH_POSTGRESQL) FIND_PACKAGE(Postgres) # PostgreSQL provider @@ -675,8 +697,9 @@ IF (WITH_CORE) SET (QGIS_MACAPP_PREFIX ${CMAKE_INSTALL_PREFIX}/${QGIS_APP_NAME}.app/Contents) # common prefix for components, let cmake handle it SET (CMAKE_INSTALL_PREFIX ${QGIS_MACAPP_PREFIX}/MacOS) - # 4 bundling levels, each includes previous - # 0 nothing + # 5 bundling levels, each includes previous + # -1 nothing + # 0 fixup the library paths for all QGIS libraries with @loader_path # 1 Qt frameworks # 2 non-system libraries, "standard" # 3 non-system frameworks, "standalone" @@ -838,7 +861,7 @@ ENDIF (DISABLE_DEPRECATED) ############################################################# # Python build dependency -FIND_PACKAGE(PythonInterp 3 REQUIRED) +FIND_PACKAGE(PythonLibrary REQUIRED) ############################################################# # Python bindings diff --git a/INSTALL b/INSTALL index df69959991d6..e0e01faa6176 100644 --- a/INSTALL +++ b/INSTALL @@ -1,6 +1,6 @@ QGIS Building QGIS from source - step by step -2020-02-12 +2020-04-05 ------------------------------------------------------------------------ @@ -16,8 +16,10 @@ Building QGIS from source - step by step 3.5. Prepare your development environment 3.6. Check out the QGIS Source Code 3.7. Starting the compile - 3.8. Building Debian packages - 3.9. On Fedora Linux + 3.8. Compiling with 3D + 3.9. Building different branches + 3.10. Building Debian packages + 3.11. On Fedora Linux 4. Building on Windows 4.1. Building with Microsoft Visual Studio 4.2. Building using MinGW @@ -47,8 +49,8 @@ Building QGIS from source - step by step ------------------------------------------------------------------------ -Last Updated: 2020-02-12 -Last Change : 2020-02-12 +Last Updated: 2020-04-05 +Last Change : 2020-04-05 1. Introduction @@ -187,11 +189,11 @@ Now update your local sources database: =============================== || Distribution | install command for packages | - | buster | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | - | bionic | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | - | eoan | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | - | focal | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | - | sid | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | + | buster | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | + | bionic | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | + | eoan | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | + | focal | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | + | sid | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | (extracted from the control.in file in debian/) @@ -265,7 +267,7 @@ When you run ccmake (note the .. is required!), a menu will appear where you can configure various aspects of the build. If you want QGIS to have debugging capabilities then set CMAKE_BUILD_TYPE to Debug. If you do not have root access or do not want to overwrite existing QGIS installs (by your -packagemanager for example), set the CMAKE_INSTALL_PREFIX to somewhere you +package manager for example), set the CMAKE_INSTALL_PREFIX to somewhere you have write access to (I usually use ${HOME}/apps). Now press 'c' to configure, 'e' to dismiss any error messages that may appear. and 'g' to generate the make files. Note that sometimes 'c' needs to @@ -274,7 +276,7 @@ After the 'g' generation is complete, press 'q' to exit the ccmake interactive dialog. /!\ Warning: Make sure that your build directory is completely empty when you -enter the command. Do never try to "re-use" an existing Qt4 build directory. +enter the command. Do never try to "re-use" an existing Qt5 build directory. If you want to use `ccmake` or other interactive tools, run the command in the empty build directory once before starting to use the interactive tools. @@ -327,14 +329,53 @@ Build and install with ninja: ninja (uses all cores by default; also supports the above described -jX option) ninja install -You can build just the targets you need using, for example: +To build even faster, you can build just the targets you need using, for example: ninja qgis ninja pycore + # if it's on desktop related code only: + ninja qgis_desktop - 3.8. Building Debian packages - ============================= + 3.8. Compiling with 3D + ====================== + +In the cmake, you need to enable: + + WITH_3D=True + + + 3.8.1. Compiling with 3D on Debian based distributions + ====================================================== + +QGIS 3D requires Qt53DExtras. These headers have been removed +from Qt upstream on Debian based distributions. A copy has been made in the +QGIS repository in external/qt3dextra-headers. +To compile with 3D enabled, you need to add some cmake options: + + CMAKE_PREFIX_PATH={path to QGIS Git repo}/external/qt3dextra-headers/cmake + QT5_3DEXTRA_INCLUDE_DIR={path to QGIS Git repo}/external/qt3dextra-headers + QT5_3DEXTRA_LIBRARY=/usr/lib/x86_64-linux-gnu/libQt53DExtras.so + + + 3.9. Building different branches + ================================ + +By using git worktree, you can switch between different branches to use +several sources in parallel, based on the same Git configuration. +We recommand you to read the documentation about this Git command: + + git commit + git worktree add ../my_new_functionality + cd ../my_new_functionality + git fetch qgis/master + git rebase -i qgis/master + # only keep the commits to be pushed + git push -u my_own_repo my_new_functionality + + + 3.10. Building Debian packages + ============================== Instead of creating a personal installation as in the previous step you can also create debian package. This is done from the QGIS root directory, where @@ -373,15 +414,15 @@ Install them using dpkg. E.g.: sudo debi - 3.9. On Fedora Linux - ==================== + 3.11. On Fedora Linux + ===================== We assume that you have the source code of QGIS ready and created a new subdirectory called `build` or `build-qt5` in it. - 3.9.1. Install build dependencies - ================================= + 3.11.1. Install build dependencies + ================================== dnf install qt5-qtbase-private-devel qt5-qtwebkit-devel qt5-qtlocation-devel qt5-qttools-static qca-qt5-devel qca-qt5-ossl qt5-qt3d-devel python3-qt5-devel python3-qscintilla-qt5-devel qscintilla-qt5-devel python3-qscintilla-devel python3-qscintilla-qt5 clang flex bison geos-devel gdal-devel sqlite-devel libspatialite-devel qt5-qtsvg-devel spatialindex-devel expat-devel proj-devel qwt-qt5-devel gsl-devel postgresql-devel cmake python3-future gdal-python3 python3-psycopg2 python3-PyYAML python3-pygments python3-jinja2 python3-OWSLib qca-qt5-ossl qwt-qt5-devel qtkeychain-qt5-devel qwt-devel sip-devel libzip-devel exiv2-devel @@ -390,7 +431,7 @@ To build QGIS server additional dependencies are required: dnf install fcgi-devel Make sure that your build directory is completely empty when you enter the -following command. Do never try to "re-use" an existing Qt4 build directory. +following command. Do never try to "re-use" an existing Qt5 build directory. If you want to use `ccmake` or other interactive tools, run the following command in the empty build directory once before starting to use the interactive tools. @@ -412,8 +453,8 @@ Or install to your system make install - 3.9.2. Suggested system tweaks - ============================== + 3.11.2. Suggested system tweaks + =============================== By default Fedora disables debugging calls from Qt applications. This prevents the useful debug output which is normally printed when running the unit tests. diff --git a/cmake/Bison.cmake b/cmake/Bison.cmake index 0a848dfa24e0..6b2f6cd110d9 100644 --- a/cmake/Bison.cmake +++ b/cmake/Bison.cmake @@ -13,10 +13,13 @@ MACRO(FIND_BISON) FIND_PROGRAM(BISON_EXECUTABLE PATHS NAMES bison.exe PATHS $ENV{LIB_DIR}/bin c:/cygwin/bin $ENV{PROGRAMFILES}/GnuWin32/bin - ) + ) + ELSEIF(APPLE AND QGIS_MAC_DEPS_DIR) + FIND_PROGRAM(BISON_EXECUTABLE bison PATHS $ENV{LIB_DIR}/bin NO_DEFAULT_PATH) ELSE (MSVC) FIND_PROGRAM(BISON_EXECUTABLE bison) ENDIF (MSVC) + IF (NOT BISON_EXECUTABLE) MESSAGE(FATAL_ERROR "Bison not found - aborting") diff --git a/cmake/FindEXIV2.cmake b/cmake/FindEXIV2.cmake index 78aebd73ab5d..5eb877f036ce 100644 --- a/cmake/FindEXIV2.cmake +++ b/cmake/FindEXIV2.cmake @@ -9,8 +9,8 @@ # -FIND_PATH(EXIV2_INCLUDE_DIR exiv2/exiv2.hpp /usr/local/include /usr/include) -FIND_LIBRARY(EXIV2_LIBRARY NAMES exiv2 PATHS /usr/local/lib /usr/lib) +FIND_PATH(EXIV2_INCLUDE_DIR exiv2/exiv2.hpp $ENV{LIB_DIR}/include /usr/local/include /usr/include) +FIND_LIBRARY(EXIV2_LIBRARY NAMES exiv2 PATHS $ENV{LIB_DIR}/lib /usr/local/lib /usr/lib) IF (EXIV2_INCLUDE_DIR AND EXIV2_LIBRARY) SET(EXIV2_FOUND TRUE) diff --git a/cmake/FindGDAL.cmake b/cmake/FindGDAL.cmake index b722a55c0b53..79412a9c0232 100644 --- a/cmake/FindGDAL.cmake +++ b/cmake/FindGDAL.cmake @@ -32,9 +32,14 @@ IF(WIN32) CACHE STRING INTERNAL) ENDIF (GDAL_LIBRARY) ENDIF (MSVC) - - + +ELSEIF(APPLE AND QGIS_MAC_DEPS_DIR) + + FIND_PATH(GDAL_INCLUDE_DIR gdal.h "$ENV{LIB_DIR}/include") + FIND_LIBRARY(GDAL_LIBRARY NAMES gdal PATHS "$ENV{LIB_DIR}/lib") + ELSE(WIN32) + IF(UNIX) # try to use framework on mac @@ -81,6 +86,7 @@ ELSE(WIN32) FIND_PROGRAM(GDAL_CONFIG gdal-config ${GDAL_CONFIG_PREFER_PATH} ${GDAL_CONFIG_PREFER_FWTOOLS_PATH} + $ENV{LIB_DIR}/bin /usr/local/bin/ /usr/bin/ ) diff --git a/cmake/FindGEOS.cmake b/cmake/FindGEOS.cmake index ee6aeb4bd403..bf02b8182f3e 100644 --- a/cmake/FindGEOS.cmake +++ b/cmake/FindGEOS.cmake @@ -18,8 +18,8 @@ INCLUDE (${CMAKE_SOURCE_DIR}/cmake/MacPlistMacros.cmake) IF(WIN32) IF (MINGW) - FIND_PATH(GEOS_INCLUDE_DIR geos_c.h /usr/local/include /usr/include c:/msys/local/include) - FIND_LIBRARY(GEOS_LIBRARY NAMES geos_c PATHS /usr/local/lib /usr/lib c:/msys/local/lib) + FIND_PATH(GEOS_INCLUDE_DIR geos_c.h "$ENV{LIB_DIR}/include" /usr/local/include /usr/include c:/msys/local/include) + FIND_LIBRARY(GEOS_LIBRARY NAMES geos_c PATHS "$ENV{LIB_DIR}/lib" /usr/local/lib /usr/lib c:/msys/local/lib) ENDIF (MINGW) IF (MSVC) @@ -30,6 +30,11 @@ IF(WIN32) ) ENDIF (MSVC) +ELSEIF(APPLE AND QGIS_MAC_DEPS_DIR) + + FIND_PATH(GEOS_INCLUDE_DIR geos_c.h "$ENV{LIB_DIR}/include" ) + FIND_LIBRARY(GEOS_LIBRARY NAMES geos_c PATHS "$ENV{LIB_DIR}/lib" ) + ELSE(WIN32) IF(UNIX) @@ -71,6 +76,7 @@ ELSE(WIN32) SET(GEOS_CONFIG_PREFER_PATH "$ENV{GEOS_HOME}/bin" CACHE STRING "preferred path to GEOS (geos-config)") FIND_PROGRAM(GEOS_CONFIG geos-config ${GEOS_CONFIG_PREFER_PATH} + $ENV{LIB_DIR}/bin /usr/local/bin/ /usr/bin/ ) diff --git a/cmake/FindGRASS.cmake b/cmake/FindGRASS.cmake index 80e16011673d..c5d76f7caf19 100644 --- a/cmake/FindGRASS.cmake +++ b/cmake/FindGRASS.cmake @@ -174,15 +174,21 @@ IF (UNIX) ENDIF (UNIX) IF (APPLE) - IF (GRASS_FIND_VERSION EQUAL 7) - LIST(APPEND GRASS_PATHS - /Applications/GRASS-7.6.app/Contents/MacOS - /Applications/GRASS-7.4.app/Contents/MacOS - /Applications/GRASS-7.2.app/Contents/MacOS - /Applications/GRASS-7.0.app/Contents/MacOS - ) - ENDIF () - LIST(APPEND GRASS_PATHS /Applications/GRASS.app/Contents/Resources) + IF (QGIS_MAC_DEPS_DIR) + FOREACH (VERSION_MINOR 9 8 7 6 5 4 3 2 1 0) + LIST (APPEND GRASS_PATHS $ENV{LIB_DIR}/grass${GRASS_FIND_VERSION}${VERSION_MINOR}) + ENDFOREACH(VERSION_MINOR) + ELSE (QGIS_MAC_DEPS_DIR) + IF (GRASS_FIND_VERSION EQUAL 7) + LIST(APPEND GRASS_PATHS + /Applications/GRASS-7.6.app/Contents/MacOS + /Applications/GRASS-7.4.app/Contents/MacOS + /Applications/GRASS-7.2.app/Contents/MacOS + /Applications/GRASS-7.0.app/Contents/MacOS + ) + ENDIF () + LIST(APPEND GRASS_PATHS /Applications/GRASS.app/Contents/Resources) + ENDIF (QGIS_MAC_DEPS_DIR) ENDIF (APPLE) IF (WITH_GRASS${GRASS_CACHE_VERSION}) diff --git a/cmake/FindGSL.cmake b/cmake/FindGSL.cmake index c9c9de047da0..28d359f780c5 100644 --- a/cmake/FindGSL.cmake +++ b/cmake/FindGSL.cmake @@ -65,6 +65,7 @@ ELSE(WIN32) SET(GSL_CONFIG_PREFER_PATH "$ENV{GSL_HOME}/bin" CACHE STRING "preferred path to GSL (gsl-config)") FIND_PROGRAM(GSL_CONFIG gsl-config ${GSL_CONFIG_PREFER_PATH} + $ENV{LIB_DIR}/bin /usr/local/bin/ /usr/bin/ ) diff --git a/cmake/FindLibZip.cmake b/cmake/FindLibZip.cmake index 8dd41472faf2..1b3efc554d96 100644 --- a/cmake/FindLibZip.cmake +++ b/cmake/FindLibZip.cmake @@ -15,39 +15,32 @@ FIND_PATH(LIBZIP_INCLUDE_DIR zip.h - PATHS - /usr/local/include - /usr/include "$ENV{LIB_DIR}/include" "$ENV{INCLUDE}" - ) + /usr/local/include + /usr/include +) FIND_PATH(LIBZIP_CONF_INCLUDE_DIR zipconf.h - PATHS - /usr/local/lib/libzip/include - /usr/lib/libzip/include + "$ENV{LIB_DIR}/include" "$ENV{LIB_DIR}/lib/libzip/include" "$ENV{LIB}/lib/libzip/include" + /usr/local/lib/libzip/include + /usr/lib/libzip/include /usr/local/include /usr/include - "$ENV{LIB_DIR}/include" "$ENV{INCLUDE}" - ) +) + +FIND_LIBRARY(LIBZIP_LIBRARY NAMES zip PATHS "$ENV{LIB_DIR}/lib" "$ENV{LIB}" /usr/local/lib /usr/lib ) -FIND_LIBRARY(LIBZIP_LIBRARY - NAMES zip - PATHS - /usr/local/lib - /usr/lib - "$ENV{LIB_DIR}/lib" - "$ENV{LIB}" - ) +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(LibZip DEFAULT_MSG + LIBZIP_LIBRARY LIBZIP_INCLUDE_DIR LIBZIP_CONF_INCLUDE_DIR) -IF (LIBZIP_LIBRARY AND LIBZIP_INCLUDE_DIR AND LIBZIP_CONF_INCLUDE_DIR) - SET(LIBZIP_FOUND TRUE) - SET(LIBZIP_INCLUDE_DIRS ${LIBZIP_INCLUDE_DIR} ${LIBZIP_CONF_INCLUDE_DIR}) -ENDIF (LIBZIP_LIBRARY AND LIBZIP_INCLUDE_DIR AND LIBZIP_CONF_INCLUDE_DIR) +SET(LIBZIP_INCLUDE_DIRS ${LIBZIP_INCLUDE_DIR} ${LIBZIP_CONF_INCLUDE_DIR}) +MARK_AS_ADVANCED(LIBZIP_LIBRARY LIBZIP_INCLUDE_DIR LIBZIP_CONF_INCLUDE_DIR LIBZIP_INCLUDE_DIRS) IF (LIBZIP_FOUND) MESSAGE(STATUS "Found libzip: ${LIBZIP_LIBRARY}") diff --git a/cmake/FindLibtasn1.cmake b/cmake/FindLibtasn1.cmake index c416e8747603..3f3f3fa1c73f 100644 --- a/cmake/FindLibtasn1.cmake +++ b/cmake/FindLibtasn1.cmake @@ -29,6 +29,7 @@ find_library(LIBTASN1_LIBRARY NAMES tasn1 PATHS ${LIB_DIR} + $ENV{LIB_DIR}/lib "$ENV{LIB_DIR}" $ENV{LIB} /usr/local/lib diff --git a/cmake/FindMDAL.cmake b/cmake/FindMDAL.cmake index 72d8ecbfbe0f..88b7772234a6 100644 --- a/cmake/FindMDAL.cmake +++ b/cmake/FindMDAL.cmake @@ -16,11 +16,11 @@ PKG_CHECK_MODULES(PC_MDAL QUIET libmdal) SET(MDAL_DEFINITIONS ${PC_MDAL_CFLAGS_OTHER}) FIND_PATH(MDAL_INCLUDE_DIR mdal.h - HINTS ${PC_MDAL_INCLUDEDIR} ${PC_MDAL_INCLUDE_DIRS} ${MDAL_PREFIX}/include + HINTS $ENV{LIB_DIR}/include ${PC_MDAL_INCLUDEDIR} ${PC_MDAL_INCLUDE_DIRS} ${MDAL_PREFIX}/include PATH_SUFFIXES libmdal ) FIND_LIBRARY(MDAL_LIBRARY NAMES mdal libmdal - HINTS ${PC_MDAL_LIBDIR} ${PC_MDAL_LIBRARY_DIRS} ${MDAL_PREFIX}/lib) + HINTS $ENV{LIB_DIR}/lib ${PC_MDAL_LIBDIR} ${PC_MDAL_LIBRARY_DIRS} ${MDAL_PREFIX}/lib) INCLUDE(FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS(MDAL DEFAULT_MSG diff --git a/cmake/FindOSGEARTH.cmake b/cmake/FindOSGEARTH.cmake index fc1a6144d729..d134c71a0995 100644 --- a/cmake/FindOSGEARTH.cmake +++ b/cmake/FindOSGEARTH.cmake @@ -27,6 +27,7 @@ FIND_PATH( ${THIS_OSGEARTH_INCLUDE_DIR} ${THIS_OSGEARTH_INCLUDE_FILE} $ENV{OSGEARTHDIR} $ENV{OSGEARTH_DIR} $ENV{OSGEO4W_ROOT} + $ENV{LIB_DIR} /usr/local/ /usr/ /sw/ # Fink @@ -62,6 +63,7 @@ FIND_LIBRARY(${MYLIBRARY} $ENV{OSGEO4W_ROOT} ~/Library/Frameworks /Library/Frameworks + $ENV{LIB_DIR} /usr/local /usr /sw diff --git a/cmake/FindPythonLibrary.cmake b/cmake/FindPythonLibrary.cmake index beb0d899bcfc..a7ca9a39e0f4 100644 --- a/cmake/FindPythonLibrary.cmake +++ b/cmake/FindPythonLibrary.cmake @@ -35,8 +35,8 @@ else(EXISTS "${PYTHON_INCLUDE_PATH}" AND EXISTS "${PYTHON_LIBRARY}" AND EXISTS " if(PYTHONINTERP_FOUND) FIND_FILE(_find_lib_python_py FindLibPython.py PATHS ${CMAKE_MODULE_PATH} NO_CMAKE_FIND_ROOT_PATH) - EXECUTE_PROCESS(COMMAND ${PYTHON_EXECUTABLE} ${_find_lib_python_py} OUTPUT_VARIABLE python_config) + if(python_config) STRING(REGEX REPLACE ".*exec_prefix:([^\n]+).*$" "\\1" PYTHON_PREFIX ${python_config}) STRING(REGEX REPLACE ".*\nshort_version:([^\n]+).*$" "\\1" PYTHON_SHORT_VERSION ${python_config}) @@ -54,8 +54,11 @@ else(EXISTS "${PYTHON_INCLUDE_PATH}" AND EXISTS "${PYTHON_LIBRARY}" AND EXISTS " if(WIN32) STRING(REPLACE "\\" "/" PYTHON_SITE_PACKAGES_DIR ${PYTHON_SITE_PACKAGES_DIR}) FIND_LIBRARY(PYTHON_LIBRARY NAMES ${PYTHON_LIBRARY_NAMES} PATHS ${PYTHON_PREFIX}/lib ${PYTHON_PREFIX}/libs) + elseif(APPLE AND QGIS_MAC_DEPS_DIR) + FIND_LIBRARY(PYTHON_LIBRARY python${PYTHON_SHORT_VERSION}m PATHS $ENV{LIB_DIR}/lib) + else(WIN32) + FIND_LIBRARY(PYTHON_LIBRARY NAMES ${PYTHON_LIBRARY_NAMES}) endif(WIN32) - FIND_LIBRARY(PYTHON_LIBRARY NAMES ${PYTHON_LIBRARY_NAMES}) set(PYTHON_INCLUDE_PATH ${PYTHON_INCLUDE_PATH} CACHE FILEPATH "Directory holding the python.h include file" FORCE) set(PYTHONLIBRARY_FOUND TRUE) endif(python_config) diff --git a/cmake/FindQCA.cmake b/cmake/FindQCA.cmake index 5b9c9f533d97..4c6a7a7b9a1a 100644 --- a/cmake/FindQCA.cmake +++ b/cmake/FindQCA.cmake @@ -28,6 +28,7 @@ else(QCA_INCLUDE_DIR AND QCA_LIBRARY) ${LIB_DIR} $ENV{LIB} "$ENV{LIB_DIR}" + $ENV{LIB_DIR}/lib /usr/local/lib ) diff --git a/cmake/FindQScintilla.cmake b/cmake/FindQScintilla.cmake index b26abf47fb2a..1a6ab3d43798 100644 --- a/cmake/FindQScintilla.cmake +++ b/cmake/FindQScintilla.cmake @@ -38,6 +38,7 @@ ELSE(QSCINTILLA_VERSION_STR) NAMES ${QSCINTILLA_LIBRARY_NAMES} PATHS "${QT_LIBRARY_DIR}" + $ENV{LIB_DIR}/lib /usr/local/lib /usr/local/lib/qt5 /usr/lib @@ -54,6 +55,7 @@ ELSE(QSCINTILLA_VERSION_STR) "${_qsci_fw}/Headers" ${Qt5Core_INCLUDE_DIRS} "${QT_INCLUDE_DIR}" + $ENV{LIB_DIR}/include /usr/local/include /usr/include PATH_SUFFIXES qt diff --git a/cmake/FindQtKeychain.cmake b/cmake/FindQtKeychain.cmake index c2b55fc4a696..7c9279ca90c3 100644 --- a/cmake/FindQtKeychain.cmake +++ b/cmake/FindQtKeychain.cmake @@ -28,6 +28,7 @@ FIND_LIBRARY(QTKEYCHAIN_LIBRARY NAMES qt5keychain qtkeychain PATHS ${LIB_DIR} "$ENV{LIB_DIR}" + $ENV{LIB_DIR}/lib $ENV{LIB} /usr/local/lib /usr/lib diff --git a/cmake/FindSqlite3.cmake b/cmake/FindSqlite3.cmake index 4ac079c46c50..4b3c34b53cca 100644 --- a/cmake/FindSqlite3.cmake +++ b/cmake/FindSqlite3.cmake @@ -21,7 +21,7 @@ # try to use framework on mac # want clean framework path, not unix compatibility path -IF (APPLE) +IF (APPLE AND NOT QGIS_MAC_DEPS_DIR) IF (CMAKE_FIND_FRAMEWORK MATCHES "FIRST" OR CMAKE_FRAMEWORK_PATH MATCHES "ONLY" OR NOT CMAKE_FIND_FRAMEWORK) @@ -35,18 +35,28 @@ IF (APPLE) ENDIF (SQLITE3_LIBRARY) SET (CMAKE_FIND_FRAMEWORK ${CMAKE_FIND_FRAMEWORK_save} CACHE STRING "" FORCE) ENDIF () -ENDIF (APPLE) +ENDIF (APPLE AND NOT QGIS_MAC_DEPS_DIR) +# FIND_PATH and FIND_LIBRARY normally search standard locations +# before the specified paths. To search non-standard paths first, +# FIND_* is invoked first with specified paths and NO_DEFAULT_PATH +# and then again with no specified paths to search the default +# locations. When an earlier FIND_* succeeds, subsequent FIND_*s +# searching for the same item do nothing. FIND_PATH(SQLITE3_INCLUDE_DIR sqlite3.h "$ENV{LIB_DIR}/include" "$ENV{LIB_DIR}/include/sqlite" "$ENV{INCLUDE}" + NO_DEFAULT_PATH ) +FIND_PATH(SQLITE3_INCLUDE_DIR sqlite3.h) FIND_LIBRARY(SQLITE3_LIBRARY NAMES sqlite3_i sqlite3 PATHS "$ENV{LIB_DIR}/lib" "$ENV{LIB}/lib" - ) + NO_DEFAULT_PATH +) +FIND_LIBRARY(SQLITE3_LIBRARY NAMES sqlite3_i sqlite3) IF (SQLITE3_INCLUDE_DIR AND SQLITE3_LIBRARY) SET(SQLITE3_FOUND TRUE) diff --git a/cmake/Flex.cmake b/cmake/Flex.cmake index b2926fb43d2a..2232b0b421b9 100644 --- a/cmake/Flex.cmake +++ b/cmake/Flex.cmake @@ -14,12 +14,15 @@ MACRO(FIND_FLEX) NAMES flex.exe PATHS $ENV{LIB_DIR}/bin c:/cygwin/bin $ENV{PROGRAMFILES}/GnuWin32/bin ) + ELSEIF(APPLE AND QGIS_MAC_DEPS_DIR) + FIND_PROGRAM(FLEX_EXECUTABLE flex PATHS $ENV{LIB_DIR}/bin NO_DEFAULT_PATH) ELSE(MSVC) FIND_PROGRAM(FLEX_EXECUTABLE flex) ENDIF (MSVC) - IF (NOT FLEX_EXECUTABLE) - MESSAGE(FATAL_ERROR "flex not found - aborting") - ENDIF (NOT FLEX_EXECUTABLE) + + IF (NOT FLEX_EXECUTABLE) + MESSAGE(FATAL_ERROR "flex not found - aborting") + ENDIF (NOT FLEX_EXECUTABLE) ENDIF(NOT FLEX_EXECUTABLE) ENDMACRO(FIND_FLEX) diff --git a/cmake/MacBundleMacros.cmake b/cmake/MacBundleMacros.cmake index 3012e460ba68..6e9d943aec78 100644 --- a/cmake/MacBundleMacros.cmake +++ b/cmake/MacBundleMacros.cmake @@ -166,6 +166,11 @@ FUNCTION (UPDATEQGISPATHS LIBFROM LIBTO) FOREACH (QP ${QGPLUGLIST}) INSTALLNAMETOOL_CHANGE ("${LIBFROM}" "${LIB_CHG_TO}" "${QP}") ENDFOREACH (QP) + # quick plugin + IF (${OSX_HAVE_LOADERPATH}) + SET (LIB_CHG_TO "${ATLOADER}/../../${LIBMID}/${LIBPOST}") + ENDIF () + INSTALLNAMETOOL_CHANGE ("${LIBFROM}" "${LIB_CHG_TO}" "${QAPPDIR}/qml/QgsQuick/libqgis_quick_plugin.dylib") # qgis python IF (${OSX_HAVE_LOADERPATH}) SET (LIB_CHG_TO "${ATLOADER}/../../${QGIS_DATA_SUBDIR_REV}/${LIBMID}/${LIBPOST}") diff --git a/cmake/PyQtMacros.cmake b/cmake/PyQtMacros.cmake index 607960c90938..918d5c137a2b 100644 --- a/cmake/PyQtMacros.cmake +++ b/cmake/PyQtMacros.cmake @@ -15,7 +15,7 @@ IF(NOT PYUIC_PROGRAM) PATHS $ENV{LIB_DIR}/bin ) ELSE(MSVC) - FIND_PROGRAM(PYUIC_PROGRAM NAMES ${PYUIC_PROG_NAMES}) + FIND_PROGRAM(PYUIC_PROGRAM NAMES ${PYUIC_PROG_NAMES} PATHS $ENV{LIB_DIR}/bin) ENDIF (MSVC) IF (NOT PYUIC_PROGRAM) @@ -59,7 +59,7 @@ IF(NOT PYRCC_PROGRAM) PATHS $ENV{LIB_DIR}/bin ) ELSE(MSVC) - FIND_PROGRAM(PYRCC_PROGRAM ${PYRCC_PROG_NAME}) + FIND_PROGRAM(PYRCC_PROGRAM ${PYRCC_PROG_NAME} PATHS $ENV{LIB_DIR}/bin) ENDIF (MSVC) IF (NOT PYRCC_PROGRAM) diff --git a/debian/control.in b/debian/control.in index c3f30d5c02e5..b9830c2b1c1b 100644 --- a/debian/control.in +++ b/debian/control.in @@ -71,6 +71,8 @@ Build-Depends: qtpositioning5-dev, qttools5-dev-tools, qttools5-dev, + libprotobuf-dev, + protobuf-compiler, saga, git, doxygen, diff --git a/debian/qgis.xml b/debian/qgis.xml index aac0d762ee8b..e718a93ef9cf 100644 --- a/debian/qgis.xml +++ b/debian/qgis.xml @@ -13,6 +13,7 @@ + diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 78ab8d1d2be1..d1366a9dcc8c 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -99,10 +99,12 @@ IF(WITH_APIDOC) ${CMAKE_SOURCE_DIR}/src/core/scalebar ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/auth ${CMAKE_SOURCE_DIR}/src/gui/attributetable ${CMAKE_SOURCE_DIR}/src/gui/callouts + ${CMAKE_SOURCE_DIR}/src/gui/devtools ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets/core ${CMAKE_SOURCE_DIR}/src/gui/effects @@ -116,6 +118,7 @@ IF(WITH_APIDOC) ${CMAKE_SOURCE_DIR}/src/gui/processing/models ${CMAKE_SOURCE_DIR}/src/gui/raster ${CMAKE_SOURCE_DIR}/src/gui/symbology + ${CMAKE_SOURCE_DIR}/src/gui/vectortile ${CMAKE_SOURCE_DIR}/src/analysis ${CMAKE_SOURCE_DIR}/src/analysis/mesh ${CMAKE_SOURCE_DIR}/src/analysis/interpolation diff --git a/doc/INSTALL.html b/doc/INSTALL.html index a9fefdfc088d..57d10b81e252 100644 --- a/doc/INSTALL.html +++ b/doc/INSTALL.html @@ -27,7 +27,7 @@

QGIS

Building QGIS from source - step by step

-

2020-02-12

+

2020-04-05

@@ -56,65 +56,69 @@

2020-02-12

  • 3.7. Starting the compile
  • -
  • 3.8. Building Debian packages +
  • 3.8. Compiling with 3D
  • -
  • 3.9. On Fedora Linux +
  • 3.9. Building different branches +
  • +
  • 3.10. Building Debian packages +
  • +
  • 3.11. On Fedora Linux
  • -
  • Building on Windows +
  • Building on Windows
  • -
  • Building on MacOS X +
  • Building on MacOS X
  • -
  • Setting up the WCS test server on GNU/Linux +
  • Setting up the WCS test server on GNU/Linux
  • -
  • Setting up a Jenkins Build Server +
  • Setting up a Jenkins Build Server
  • -
  • Debug output and running tests +
  • Debug output and running tests
  • -
  • Authors and Acknowledgments +
  • Authors and Acknowledgments
  • @@ -122,8 +126,8 @@

    2020-02-12

    -Last Updated: 2020-02-12 -Last Change : 2020-02-12 +Last Updated: 2020-04-05 +Last Change : 2020-04-05

    @@ -333,23 +337,23 @@

    3.3. Install build dependencies

    buster -apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb +apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb bionic -apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb +apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb eoan -apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb +apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb focal -apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb +apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb sid -apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb +apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb @@ -458,7 +462,7 @@

    3.7. Starting the compile

    you can configure various aspects of the build. If you want QGIS to have debugging capabilities then set CMAKE_BUILD_TYPE to Debug. If you do not have root access or do not want to overwrite existing QGIS installs (by your -packagemanager for example), set the CMAKE_INSTALL_PREFIX to somewhere you +package manager for example), set the CMAKE_INSTALL_PREFIX to somewhere you have write access to (I usually use ${HOME}/apps). Now press 'c' to configure, 'e' to dismiss any error messages that may appear. and 'g' to generate the make files. Note that sometimes 'c' needs to @@ -468,7 +472,7 @@

    3.7. Starting the compile

    /!\ Warning: Make sure that your build directory is completely empty when you -enter the command. Do never try to "re-use" an existing Qt4 build directory. +enter the command. Do never try to "re-use" an existing Qt5 build directory. If you want to use `ccmake` or other interactive tools, run the command in the empty build directory once before starting to use the interactive tools.

    @@ -562,17 +566,68 @@

    3.7. Starting the compile

    -You can build just the targets you need using, for example: +To build even faster, you can build just the targets you need using, for example:

     ninja qgis
     ninja pycore
    +# if it's on desktop related code only:
    +ninja qgis_desktop
     
    -

    3.8. Building Debian packages

    +

    3.8. Compiling with 3D

    + +

    +In the cmake, you need to enable: +

    + +
    +WITH_3D=True
    +
    + +
    +

    3.8.1. Compiling with 3D on Debian based distributions

    + +

    +QGIS 3D requires Qt53DExtras. These headers have been removed +from Qt upstream on Debian based distributions. A copy has been made in the +QGIS repository in external/qt3dextra-headers. +To compile with 3D enabled, you need to add some cmake options: +

    + +
    +CMAKE_PREFIX_PATH={path to QGIS Git repo}/external/qt3dextra-headers/cmake
    +QT5_3DEXTRA_INCLUDE_DIR={path to QGIS Git repo}/external/qt3dextra-headers
    +QT5_3DEXTRA_LIBRARY=/usr/lib/x86_64-linux-gnu/libQt53DExtras.so
    +
    + +
    +
    +
    +

    3.9. Building different branches

    + +

    +By using git worktree, you can switch between different branches to use +several sources in parallel, based on the same Git configuration. +We recommand you to read the documentation about this Git command: +

    + +
    +git commit
    +git worktree add ../my_new_functionality
    +cd ../my_new_functionality
    +git fetch qgis/master
    +git rebase -i qgis/master
    +# only keep the commits to be pushed
    +git push -u my_own_repo my_new_functionality
    +
    + +
    +
    +

    3.10. Building Debian packages

    Instead of creating a personal installation as in the previous step you can @@ -632,8 +687,8 @@

    3.8. Building Debian packages

    -
    -

    3.9. On Fedora Linux

    +
    +

    3.11. On Fedora Linux

    We assume that you have the source code of QGIS ready and created a @@ -641,7 +696,7 @@

    3.9. On Fedora Linux

    -

    3.9.1. Install build dependencies

    +

    3.11.1. Install build dependencies

     dnf install qt5-qtbase-private-devel qt5-qtwebkit-devel qt5-qtlocation-devel qt5-qttools-static qca-qt5-devel qca-qt5-ossl qt5-qt3d-devel python3-qt5-devel python3-qscintilla-qt5-devel qscintilla-qt5-devel python3-qscintilla-devel python3-qscintilla-qt5 clang flex bison geos-devel gdal-devel sqlite-devel libspatialite-devel qt5-qtsvg-devel spatialindex-devel expat-devel proj-devel qwt-qt5-devel gsl-devel postgresql-devel cmake python3-future gdal-python3 python3-psycopg2 python3-PyYAML python3-pygments python3-jinja2 python3-OWSLib qca-qt5-ossl qwt-qt5-devel qtkeychain-qt5-devel qwt-devel sip-devel libzip-devel exiv2-devel
    @@ -657,7 +712,7 @@ 

    3.9.1. Install build dependencies

    Make sure that your build directory is completely empty when you enter the -following command. Do never try to "re-use" an existing Qt4 build directory. +following command. Do never try to "re-use" an existing Qt5 build directory. If you want to use `ccmake` or other interactive tools, run the following command in the empty build directory once before starting to use the interactive tools. @@ -695,7 +750,7 @@

    3.9.1. Install build dependencies

    -

    3.9.2. Suggested system tweaks

    +

    3.11.2. Suggested system tweaks

    By default Fedora disables debugging calls from Qt applications. This prevents @@ -715,10 +770,10 @@

    3.9.2. Suggested system tweaks

    -
    +

    4. Building on Windows

    -
    +

    4.1. Building with Microsoft Visual Studio

    @@ -1006,7 +1061,7 @@

    4.1.6. Osgeo4w packaging

    -
    +

    4.2. Building using MinGW

    @@ -1314,7 +1369,7 @@

    4.2.12. Create the installation package: (optional)

    -
    +

    4.3. Creation of MSYS environment for compilation of QGIS

    @@ -1608,7 +1663,7 @@

    4.3.3. Cleanup

    -
    +

    4.4. Building on Linux with mxe

    @@ -1677,7 +1732,7 @@

    4.4.2. Testing QGIS

    -
    +

    5. Building on MacOS X

    @@ -1748,7 +1803,7 @@

    5. Building on MacOS X

    many threads.

    -
    +

    5.1. Install Developer Tools

    @@ -1806,7 +1861,7 @@

    5.1. Install Developer Tools

    -
    +

    5.2. Install Qt4 from disk image

    @@ -1867,7 +1922,7 @@

    5.2. Install Qt4 from disk image

    -
    +

    5.3. Install CMake for OSX

    @@ -1952,7 +2007,7 @@

    5.3.1. Optional setup: ccache

    -
    +

    5.4. Install development frameworks for QGIS dependencies

    @@ -2654,7 +2709,7 @@

    5.4.13. Optional dependencies: OSG & osgEarth

    -
    +

    5.5. API documentation

    @@ -2691,7 +2746,7 @@

    5.5. API documentation

    -
    +

    5.6. QGIS source

    @@ -2727,7 +2782,7 @@

    5.6. QGIS source

    -
    +

    5.7. Configure the build

    @@ -2868,7 +2923,7 @@

    5.7. Configure the build

    -
    +

    5.8. Building

    @@ -2897,7 +2952,7 @@

    5.8. Building

    -
    +

    5.9. Post-Install

    @@ -2935,7 +2990,7 @@

    5.9. Post-Install

    -
    +

    6. Setting up the WCS test server on GNU/Linux

    @@ -2946,7 +3001,7 @@

    6. Setting up the WCS test server on GNU/Linux

    require slight variations in package names.

    -
    +

    6.1. Preparation

    @@ -2967,7 +3022,7 @@

    6.1. Preparation

    -
    +

    6.2. Setup mapserver

    @@ -3034,7 +3089,7 @@

    6.2. Setup mapserver

    -
    +

    6.3. Create a home page

    @@ -3053,7 +3108,7 @@ 

    6.3. Create a home page

    -
    +

    6.4. Now deploy it

    @@ -3067,7 +3122,7 @@ 

    6.4. Now deploy it

    -
    +

    6.5. Debugging

    @@ -3076,7 +3131,7 @@ 

    6.5. Debugging

    -
    +

    7. Setting up a Jenkins Build Server

    @@ -3223,7 +3278,7 @@

    7. Setting up a Jenkins Build Server

    -
    +

    8. Debug output and running tests

    @@ -3290,7 +3345,7 @@

    8. Debug output and running tests

    -
    +

    9. Authors and Acknowledgments

    diff --git a/doc/index.dox b/doc/index.dox index 5ac09c9dfc6e..cba11bb42fe9 100644 --- a/doc/index.dox +++ b/doc/index.dox @@ -32,6 +32,8 @@ See \ref api_break for information about incompatible changes to API between rel Earlier versions of the documentation are also available on the QGIS website: +3.12, +3.10 (LTR), 3.8, 3.6, 3.4 (LTR), diff --git a/doc/linux.t2t b/doc/linux.t2t index 9bd04e70350e..3788e9e5c2db 100644 --- a/doc/linux.t2t +++ b/doc/linux.t2t @@ -44,11 +44,11 @@ sudo apt-get update == Install build dependencies == || Distribution | install command for packages | -| buster | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | -| bionic | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | -| eoan | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | -| focal | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | -| sid | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | +| buster | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | +| bionic | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | +| eoan | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | +| focal | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | +| sid | ``apt-get install bison ca-certificates ccache cmake cmake-curses-gui dh-python doxygen expect flex flip gdal-bin git graphviz grass-dev libexiv2-dev libexpat1-dev libfcgi-dev libgdal-dev libgeos-dev libgsl-dev libpq-dev libproj-dev libprotobuf-dev libqca-qt5-2-dev libqca-qt5-2-plugins libqscintilla2-qt5-dev libqt5opengl5-dev libqt5serialport5-dev libqt5sql5-sqlite libqt5svg5-dev libqt5webkit5-dev libqt5xmlpatterns5-dev libqwt-qt5-dev libspatialindex-dev libspatialite-dev libsqlite3-dev libsqlite3-mod-spatialite libyaml-tiny-perl libzip-dev lighttpd locales ninja-build ocl-icd-opencl-dev opencl-headers pkg-config poppler-utils protobuf-compiler pyqt5-dev pyqt5-dev-tools pyqt5.qsci-dev python3-all-dev python3-autopep8 python3-dateutil python3-dev python3-future python3-gdal python3-httplib2 python3-jinja2 python3-lxml python3-markupsafe python3-mock python3-nose2 python3-owslib python3-plotly python3-psycopg2 python3-pygments python3-pyproj python3-pyqt5 python3-pyqt5.qsci python3-pyqt5.qtsql python3-pyqt5.qtsvg python3-pyqt5.qtwebkit python3-requests python3-sip python3-sip-dev python3-six python3-termcolor python3-tz python3-yaml qt3d-assimpsceneimport-plugin qt3d-defaultgeometryloader-plugin qt3d-gltfsceneio-plugin qt3d-scene2d-plugin qt3d5-dev qt5-default qt5keychain-dev qtbase5-dev qtbase5-private-dev qtpositioning5-dev qttools5-dev qttools5-dev-tools saga spawn-fcgi txt2tags xauth xfonts-100dpi xfonts-75dpi xfonts-base xfonts-scalable xvfb`` | (extracted from the control.in file in ``debian/``) @@ -128,7 +128,7 @@ When you run ccmake (note the .. is required!), a menu will appear where you can configure various aspects of the build. If you want QGIS to have debugging capabilities then set CMAKE_BUILD_TYPE to Debug. If you do not have root access or do not want to overwrite existing QGIS installs (by your -packagemanager for example), set the CMAKE_INSTALL_PREFIX to somewhere you +package manager for example), set the CMAKE_INSTALL_PREFIX to somewhere you have write access to (I usually use ${HOME}/apps). Now press 'c' to configure, 'e' to dismiss any error messages that may appear. and 'g' to generate the make files. Note that sometimes 'c' needs to @@ -137,7 +137,7 @@ After the 'g' generation is complete, press 'q' to exit the ccmake interactive dialog. /!\ **Warning:** Make sure that your build directory is completely empty when you -enter the command. Do never try to "re-use" an existing **Qt4** build directory. +enter the command. Do never try to "re-use" an existing **Qt5** build directory. If you want to use `ccmake` or other interactive tools, run the command in the empty build directory once before starting to use the interactive tools. @@ -210,10 +210,46 @@ Build and install with ninja: ninja (uses all cores by default; also supports the above described -jX option) ninja install ``` -You can build just the targets you need using, for example: + +To build even faster, you can build just the targets you need using, for example: ``` ninja qgis ninja pycore +# if it's on desktop related code only: +ninja qgis_desktop +``` + +== Compiling with 3D == + +In the cmake, you need to enable: +``` +WITH_3D=True +``` +=== Compiling with 3D on Debian based distributions === + +QGIS 3D requires Qt53DExtras. These headers have been removed +from Qt upstream on Debian based distributions. A copy has been made in the +QGIS repository in ``external/qt3dextra-headers``. +To compile with 3D enabled, you need to add some cmake options: +``` +CMAKE_PREFIX_PATH={path to QGIS Git repo}/external/qt3dextra-headers/cmake +QT5_3DEXTRA_INCLUDE_DIR={path to QGIS Git repo}/external/qt3dextra-headers +QT5_3DEXTRA_LIBRARY=/usr/lib/x86_64-linux-gnu/libQt53DExtras.so +``` + +== Building different branches == + +By using ``git worktree``, you can switch between different branches to use +several sources in parallel, based on the same Git configuration. +We recommand you to read the documentation about this Git command: +``` +git commit +git worktree add ../my_new_functionality +cd ../my_new_functionality +git fetch qgis/master +git rebase -i qgis/master +# only keep the commits to be pushed +git push -u my_own_repo my_new_functionality ``` == Building Debian packages == @@ -280,7 +316,7 @@ dnf install fcgi-devel ``` Make sure that your build directory is completely empty when you enter the -following command. Do never try to "re-use" an existing Qt4 build directory. +following command. Do never try to "re-use" an existing Qt5 build directory. If you want to use `ccmake` or other interactive tools, run the following command in the empty build directory once before starting to use the interactive tools. diff --git a/external/mdal/cmake/FindNetCDF.cmake b/external/mdal/cmake/FindNetCDF.cmake index 49ac69c450c7..75be47ba1f49 100644 --- a/external/mdal/cmake/FindNetCDF.cmake +++ b/external/mdal/cmake/FindNetCDF.cmake @@ -18,12 +18,12 @@ PKG_CHECK_MODULES(PC_NETCDF QUIET netcdf) SET(NETCDF_DEFINITIONS ${PC_NETCDF_CFLAGS_OTHER}) FIND_PATH (NETCDF_INCLUDE_DIR netcdf.h - HINTS ${PC_NETCDF_INCLUDEDIR} ${PC_NETCDF_INCLUDE_DIRS} ${NETCDF_PREFIX}/include + HINTS $ENV{LIB_DIR}/include ${PC_NETCDF_INCLUDEDIR} ${PC_NETCDF_INCLUDE_DIRS} ${NETCDF_PREFIX}/include PATH_SUFFIXES libnetcdf ) - -FIND_LIBRARY (NETCDF_LIBRARY - NAMES netcdf libnetcdf - HINTS HINTS ${PC_NETCDF_LIBDIR} ${PC_NETCDF_LIBRARY_DIRS} ${NETCDF_PREFIX}/lib) + +FIND_LIBRARY (NETCDF_LIBRARY + NAMES netcdf libnetcdf + HINTS $ENV{LIB_DIR}/lib ${PC_NETCDF_LIBDIR} ${PC_NETCDF_LIBRARY_DIRS} ${NETCDF_PREFIX}/lib) INCLUDE (FindPackageHandleStandardArgs) FIND_PACKAGE_HANDLE_STANDARD_ARGS (NetCDF diff --git a/images/images.qrc b/images/images.qrc index 55b9b61d4a98..59f4edffacd5 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -112,9 +112,11 @@ themes/default/algorithms/mAlgorithmNearestNeighbour.svg themes/default/algorithms/mAlgorithmNetworkAnalysis.svg themes/default/algorithms/mAlgorithmPolygonToLine.svg + themes/default/algorithms/mAlgorithmRandomPointsOnLines.svg themes/default/algorithms/mAlgorithmRandomPointsWithinPolygon.svg themes/default/algorithms/mAlgorithmRandomPointsWithinExtent.svg themes/default/algorithms/mAlgorithmRegularPoints.svg + themes/default/algorithms/mAlgorithmRoundRastervalues.svg themes/default/algorithms/mAlgorithmSelectLocation.svg themes/default/algorithms/mAlgorithmSelectRandom.svg themes/default/algorithms/mAlgorithmSimplify.svg @@ -197,6 +199,9 @@ themes/default/mActionAddArrow.svg themes/default/mActionAddBasicShape.svg themes/default/mActionAddBasicCircle.svg + themes/default/mActionAllowIntersections.svg + themes/default/mActionAvoidIntersectionsCurrentLayer.svg + themes/default/mActionAvoidIntersectionsLayers.svg themes/default/mActionEditNodesItem.svg themes/default/mActionEditHelpContent.svg themes/default/mActionIconView.svg @@ -268,6 +273,7 @@ themes/default/mActionDeleteSelected.svg themes/default/mActionDeleteTable.svg themes/default/mActionDeselectAll.svg + themes/default/mActionDeselectActiveLayer.svg themes/default/mActionDuplicateLayer.svg themes/default/mActionDuplicateComposer.svg themes/default/mActionEditCopy.svg @@ -297,6 +303,7 @@ themes/default/mActionHelpSponsors.png themes/default/mActionHideAllLayers.svg themes/default/mActionToggleAllLayers.svg + themes/default/mActionToggleSelectedLayers.svg themes/default/mActionHideSelectedLayers.svg themes/default/mActionHideDeselectedLayers.svg themes/default/mActionHistory.svg @@ -512,6 +519,7 @@ themes/default/mIconTimerPause.svg themes/default/mIconTreeView.svg themes/default/mIconVector.svg + themes/default/mIconVectorTileLayer.svg themes/default/mIconVirtualLayer.svg themes/default/mIconWcs.svg themes/default/mIconWfs.svg @@ -680,6 +688,7 @@ themes/default/mIconSnappingArea.svg themes/default/mIconSnappingCentroid.svg themes/default/mIconSnappingMiddle.svg + themes/default/mIconSnappingOnScale.svg themes/default/mIconSnappingVertex.svg themes/default/mIconSnappingSegment.svg themes/default/mIconTopologicalEditing.svg @@ -832,6 +841,10 @@ themes/default/temporal_navigation/rewindToStart.svg themes/default/temporal_navigation/skipToEnd.svg themes/default/temporal_navigation/pause.svg + themes/default/mIconIterate.svg + themes/default/mIconNetworkLogger.svg + themes/default/mActionAddMarker.svg + themes/default/mLayoutItemMarker.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/algorithms/mAlgorithmRandomPointsOnLines.svg b/images/themes/default/algorithms/mAlgorithmRandomPointsOnLines.svg new file mode 100644 index 000000000000..ebb78fed061b --- /dev/null +++ b/images/themes/default/algorithms/mAlgorithmRandomPointsOnLines.svg @@ -0,0 +1,120 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/images/themes/default/algorithms/mAlgorithmRoundRastervalues.svg b/images/themes/default/algorithms/mAlgorithmRoundRastervalues.svg new file mode 100644 index 000000000000..86a2c5148d0a --- /dev/null +++ b/images/themes/default/algorithms/mAlgorithmRoundRastervalues.svg @@ -0,0 +1 @@ + diff --git a/images/themes/default/mActionAddMarker.svg b/images/themes/default/mActionAddMarker.svg new file mode 100644 index 000000000000..4f9ae01457b4 --- /dev/null +++ b/images/themes/default/mActionAddMarker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/themes/default/mActionAllowIntersections.svg b/images/themes/default/mActionAllowIntersections.svg new file mode 100644 index 000000000000..c6e23ea46967 --- /dev/null +++ b/images/themes/default/mActionAllowIntersections.svg @@ -0,0 +1 @@ + diff --git a/images/themes/default/mActionAvoidIntersectionsCurrentLayer.svg b/images/themes/default/mActionAvoidIntersectionsCurrentLayer.svg new file mode 100644 index 000000000000..2d03088f0334 --- /dev/null +++ b/images/themes/default/mActionAvoidIntersectionsCurrentLayer.svg @@ -0,0 +1 @@ + diff --git a/images/themes/default/mActionAvoidIntersectionsLayers.svg b/images/themes/default/mActionAvoidIntersectionsLayers.svg new file mode 100644 index 000000000000..36b6dc0596ca --- /dev/null +++ b/images/themes/default/mActionAvoidIntersectionsLayers.svg @@ -0,0 +1 @@ + diff --git a/images/themes/default/mActionDeselectActiveLayer.svg b/images/themes/default/mActionDeselectActiveLayer.svg new file mode 100644 index 000000000000..7a6ef4ed19cf --- /dev/null +++ b/images/themes/default/mActionDeselectActiveLayer.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/images/themes/default/mActionDeselectAll.svg b/images/themes/default/mActionDeselectAll.svg index 7a6ef4ed19cf..229848dd6b45 100644 --- a/images/themes/default/mActionDeselectAll.svg +++ b/images/themes/default/mActionDeselectAll.svg @@ -1,6 +1,7 @@ + diff --git a/images/themes/default/mActionToggleSelectedLayers.svg b/images/themes/default/mActionToggleSelectedLayers.svg new file mode 100644 index 000000000000..e71939b9da76 --- /dev/null +++ b/images/themes/default/mActionToggleSelectedLayers.svg @@ -0,0 +1,126 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/themes/default/mIconIterate.svg b/images/themes/default/mIconIterate.svg new file mode 100644 index 000000000000..5ad34d826e61 --- /dev/null +++ b/images/themes/default/mIconIterate.svg @@ -0,0 +1,61 @@ + + + + + + image/svg+xml + + + + + + + + + + diff --git a/images/themes/default/mIconNetworkLogger.svg b/images/themes/default/mIconNetworkLogger.svg new file mode 100644 index 000000000000..32d1e04035c8 --- /dev/null +++ b/images/themes/default/mIconNetworkLogger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/images/themes/default/mIconSnappingOnScale.svg b/images/themes/default/mIconSnappingOnScale.svg new file mode 100644 index 000000000000..f2869a29e397 --- /dev/null +++ b/images/themes/default/mIconSnappingOnScale.svg @@ -0,0 +1,158 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/themes/default/mIconVectorTileLayer.svg b/images/themes/default/mIconVectorTileLayer.svg new file mode 100644 index 000000000000..fa10e503b7d5 --- /dev/null +++ b/images/themes/default/mIconVectorTileLayer.svg @@ -0,0 +1,195 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/themes/default/mIndicatorTemporal.svg b/images/themes/default/mIndicatorTemporal.svg index 14ecd0617e60..2407e400515b 100644 --- a/images/themes/default/mIndicatorTemporal.svg +++ b/images/themes/default/mIndicatorTemporal.svg @@ -1,62 +1 @@ - - - - - - image/svg+xml - - - - - - - - - - + \ No newline at end of file diff --git a/images/themes/default/mLayoutItemMarker.svg b/images/themes/default/mLayoutItemMarker.svg new file mode 100644 index 000000000000..00e286c67574 --- /dev/null +++ b/images/themes/default/mLayoutItemMarker.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/mac/CMakeLists.txt b/mac/CMakeLists.txt index 3a9d7a85f1b6..645c9295af36 100644 --- a/mac/CMakeLists.txt +++ b/mac/CMakeLists.txt @@ -1,6 +1,6 @@ # mac bundling must happen at end, so all binaries installed # and install_names can be adjusted -IF (APPLE) +IF (APPLE AND QGIS_MACAPP_BUNDLE GREATER -1) # for included scripts that set policies (ie OS X bundling) INSTALL (CODE "cmake_policy(SET CMP0011 NEW)") CONFIGURE_FILE (cmake/0vars.cmake.in 0vars.cmake @ONLY) @@ -30,4 +30,4 @@ IF (APPLE) ENDIF (QGIS_MACAPP_BUNDLE_USER) # tickle app bundle INSTALL (CODE "EXECUTE_PROCESS(COMMAND touch \"$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/../../.\")") -ENDIF (APPLE) +ENDIF (APPLE AND QGIS_MACAPP_BUNDLE GREATER -1) diff --git a/mac/cmake/1qt.cmake.in b/mac/cmake/1qt.cmake.in index 27981a8d44e3..5aa8feca618c 100644 --- a/mac/cmake/1qt.cmake.in +++ b/mac/cmake/1qt.cmake.in @@ -56,7 +56,7 @@ IF (NOT @OSX_HAVE_LOADERPATH@) EXECUTE_PROCESS (COMMAND ln -sfn @QGIS_CGIBIN_SUBDIR_REV@/@QGIS_LIB_SUBDIR@ "${QCGIDIR}/lib") ENDIF (NOT @OSX_HAVE_LOADERPATH@) -### copy files +### copy files & strip qt rpath # Qt frameworks # Qt5 cmake does not create overall qt prefix var, only individual lib prefixes. @@ -67,6 +67,7 @@ EXECUTE_PROCESS (COMMAND mkdir -p "${QFWDIR}") FOREACH (QFW ${QTLISTQG}) IF (NOT IS_DIRECTORY "${QFWDIR}/${QFW}.framework") COPY_FRAMEWORK("${QT_LIBRARY_DIR}" ${QFW} "${QFWDIR}") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QFWDIR}/${QFW}.framework/${QFW}") ENDIF () ENDFOREACH (QFW) @@ -76,12 +77,14 @@ EXECUTE_PROCESS (COMMAND mkdir -p "${QPLUGDIR}/../imageformats") FOREACH (QI qgif;qico;qjpeg;qsvg;qtiff) IF (NOT EXISTS "${QPLUGDIR}/../imageformats/lib${QI}.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QT_PLUGINS_DIR@/imageformats/lib${QI}.dylib" "${QPLUGDIR}/../imageformats/lib${QI}.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../imageformats/lib${QI}.dylib") ENDIF () ENDFOREACH (QI) EXECUTE_PROCESS (COMMAND mkdir -p "${QPLUGDIR}/../platforms") FOREACH (QTP cocoa;minimal;offscreen) IF (NOT EXISTS "${QPLUGDIR}/../platforms/libq${QTP}.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QT_PLUGINS_DIR@/platforms/libq${QTP}.dylib" "${QPLUGDIR}/../platforms/libq${QTP}.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../platforms/libq${QTP}.dylib") ENDIF () ENDFOREACH (QTP) EXECUTE_PROCESS (COMMAND mkdir -p "${QPLUGDIR}/../sqldrivers") @@ -91,11 +94,13 @@ IF (@WITH_QSPATIALITE@) LIST(APPEND QTLISTSQL spatialite) IF (EXISTS "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") EXECUTE_PROCESS (COMMAND ${CMAKE_COMMAND} -E remove "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") ENDIF () ENDIF () FOREACH (QSL ${QTLISTSQL}) IF (NOT EXISTS "${QPLUGDIR}/../sqldrivers/libqsql${QSL}.dylib" AND EXISTS "@QT_PLUGINS_DIR@/sqldrivers/libqsql${QSL}.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QT_PLUGINS_DIR@/sqldrivers/libqsql${QSL}.dylib" "${QPLUGDIR}/../sqldrivers/libqsql${QSL}.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../sqldrivers/libqsql${QSL}.dylib") ENDIF () ENDFOREACH (QSL) IF (NOT @WITH_QSPATIALITE@ AND EXISTS "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") @@ -104,10 +109,12 @@ ENDIF () EXECUTE_PROCESS (COMMAND mkdir -p "${QPLUGDIR}/../iconengines") IF (NOT EXISTS "${QPLUGDIR}/../iconengines/libqsvgicon.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QT_PLUGINS_DIR@/iconengines/libqsvgicon.dylib" "${QPLUGDIR}/../iconengines/libqsvgicon.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../iconengines/libqsvgicon.dylib") ENDIF () EXECUTE_PROCESS (COMMAND mkdir -p "${QPLUGDIR}/../styles") IF (NOT EXISTS "${QPLUGDIR}/../styles/libqmacstyle.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QT_PLUGINS_DIR@/styles/libqmacstyle.dylib" "${QPLUGDIR}/../styles/libqmacstyle.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../styles/libqmacstyle.dylib") ENDIF () # Qwt @@ -119,6 +126,7 @@ IF (QWT_ISLIB) MESSAGE (STATUS "Copying Qwt and updating library paths...") IF (NOT EXISTS "${QLIBDIR}/libqwt.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QWT_LIBRARY@" "${QLIBDIR}/libqwt.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QLIBDIR}/libqwt.dylib") ENDIF () GET_INSTALL_NAME ("@QWT_LIBRARY@" "libqwt" QWT_CHG) IF (QWT_CHG) @@ -130,6 +138,7 @@ ELSEIF (QWT_ISFW AND EXISTS "@QWT_LIBRARY@") STRING(REGEX REPLACE "/qwt.framework.*" "" QWT_PARENT "@QWT_LIBRARY@") IF (NOT IS_DIRECTORY "${QFWDIR}/qwt.framework") COPY_FRAMEWORK("${QWT_PARENT}" "qwt" "${QFWDIR}") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QFWDIR}/qwt.framework/qwt") ENDIF () GET_INSTALL_NAME ("${QWT_PARENT}/qwt.framework/qwt" "qwt.framework" QWT_CHG) IF (QWT_CHG) @@ -149,6 +158,7 @@ IF (@WITH_DESKTOP@) MESSAGE (STATUS "Copying QwtPolar and updating library paths...") IF (NOT EXISTS "${QLIBDIR}/libqwtpolar.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QWT_LIBRARY@" "${QLIBDIR}/libqwtpolar.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QLIBDIR}/libqwtpolar.dylib") ENDIF () GET_INSTALL_NAME ("@QWTPOLAR_LIBRARY@" "libqwtpolar" QWTP_CHG) IF (QWTP_CHG) @@ -169,6 +179,7 @@ IF (@WITH_DESKTOP@) STRING(REGEX REPLACE "/qwtpolar.framework.*" "" QWTP_PARENT "@QWTPOLAR_LIBRARY@") IF (NOT IS_DIRECTORY "${QFWDIR}/qwtpolar.framework") COPY_FRAMEWORK("${QWTP_PARENT}" "qwtpolar" "${QFWDIR}") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QFWDIR}/qwtpolar.framework/qwtpolar") ENDIF () GET_INSTALL_NAME ("${QWTP_PARENT}/qwtpolar.framework/qwtpolar" "qwtpolar.framework" QWTP_CHG) IF (QWTP_CHG) @@ -197,6 +208,7 @@ IF (ISLIB) SET (QCA_CHG_TO "${ATLOADER}/@QGIS_PLUGIN_SUBDIR_REV@/${QGIS_LIB_SUBDIR}/libqca.dylib") IF (NOT EXISTS "${QLIBDIR}/libqca.dylib") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QCA_LIBRARY@" "${QLIBDIR}/libqca.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QLIBDIR}/libqca.dylib") ENDIF () GET_INSTALL_NAME ("@QCA_LIBRARY@" "libqca" QCA_CHG) IF (QCA_CHG) @@ -220,6 +232,7 @@ ELSEIF (ISFW AND EXISTS "@QCA_LIBRARY@") IF (NOT IS_DIRECTORY "${QFWDIR}/${_qca_libname}.framework") STRING(REGEX REPLACE "/${_qca_libname}.framework.*" "" QCA_PARENT "@QCA_LIBRARY@") COPY_FRAMEWORK("${QCA_PARENT}" "${_qca_libname}" "${QFWDIR}") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QFWDIR}/${_qca_libname}.framework/${_qca_libname}") ENDIF () GET_INSTALL_NAME ("${QCA_LIBRARY}" "${_qca_libname}" QCA_CHG) IF (QCA_CHG) @@ -234,6 +247,7 @@ MESSAGE (STATUS "Updating QCA plugins with QCA library path in ${QCA_PLUGIN_DIR} SET(QCA_PLUGINS logger ossl softstore) FOREACH (qca_plugin ${QCA_PLUGINS}) EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "${QCA_PLUGIN_DIR}/crypto/libqca-${qca_plugin}.dylib" "${QPLUGDIR}/../crypto/") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QCA_PLUGIN_DIR}/crypto/libqca-${qca_plugin}.dylib") IF (QCA_CHG) INSTALLNAMETOOL_CHANGE ("${QCA_CHG}" "${QCA_CHG_TO}" "${QPLUGDIR}/../crypto/libqca-${qca_plugin}.dylib") ENDIF () @@ -243,6 +257,7 @@ ENDFOREACH () # linked to qca and qgis_core frameworks (see also 2lib.cmake.in) IF (@WITH_QSPATIALITE@ AND EXISTS "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") # qca.framework + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") INSTALLNAMETOOL_CHANGE ("${QCA_CHG}" "${QCA_CHG_TO}" "${QPLUGDIR}/../sqldrivers/libqsqlspatialite.dylib") # qgis_core.framework GET_INSTALL_NAME ("@QGIS_OUTPUT_DIRECTORY@/lib/qgis_core.framework/qgis_core" qgis_core.framework QGCORE) @@ -260,6 +275,7 @@ ENDIF () IF (QSCI_LIB) MESSAGE (STATUS "Copying QScintilla2 library and updating library paths...") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QSCINTILLA_LIBRARY@" "${QLIBDIR}/${QSCI_LIB}.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QLIBDIR}/${QSCI_LIB}.dylib") GET_INSTALL_NAME ("@QSCINTILLA_LIBRARY@" "${QSCI_LIB}" QSCI_CHG) IF (QSCI_CHG) UPDATEQGISPATHS (${QSCI_CHG} ${QSCI_LIB}.dylib) @@ -271,6 +287,7 @@ ENDIF () IF (@QTKEYCHAIN_LIBRARY@ MATCHES ".*libqt5keychain.dylib") MESSAGE (STATUS "Copying QtKeychain library and updating library paths...") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "@QTKEYCHAIN_LIBRARY@" "${QLIBDIR}/libqt5keychain.dylib") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QLIBDIR}/libqt5keychain.dylib") GET_INSTALL_NAME ("@QTKEYCHAIN_LIBRARY@" "libqt5keychain" QTKEY_CHG) IF (QTKEY_CHG) UPDATEQGISPATHS (${QTKEY_CHG} libqt5keychain.dylib) @@ -290,12 +307,14 @@ ENDFOREACH (PYPATH) MESSAGE (STATUS "Copying sip...") PYTHONMODULEDIR("sip.so" SIPMODDIR) IF (SIPMODDIR) - IF (NOT EXISTS "${QGISPYDIR}/sip.so") + IF (NOT EXISTS "${QGISPYDIR}/sip.so" AND NOT EXISTS "${QGISPYDIR}/PyQt5/sip.so") # MYMESSAGE ("ditto ${QARCHS} \"${SIPMODDIR}/sip.so\" \"${QGISPYDIR}/\"") IF (${SIPMODDIR} MATCHES ".*PyQt5.*") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "${SIPMODDIR}/sip.so" "${QGISPYDIR}/PyQt5/") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QGISPYDIR}/PyQt5/sip.so") ELSE () EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "${SIPMODDIR}/sip.so" "${QGISPYDIR}/") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QGISPYDIR}/sip.so") ENDIF () EXECUTE_PROCESS (COMMAND cp -fp "${SIPMODDIR}/sipconfig.py" "${QGISPYDIR}/") ENDIF () @@ -316,6 +335,7 @@ FOREACH (PQ ${PYQTLIST}) IF (NOT EXISTS "${QGISPYDIR}/${MODSUBPATH}") # MESSAGE (STATUS "ditto ${QARCHS} \"${PYQT5MOD}\" \"${QGISPYDIR}/${MODSUBPATH}\"") EXECUTE_PROCESS (COMMAND ditto ${QARCHS} "${MODDIR}/${MODSUBPATH}" "${QGISPYDIR}/${MODSUBPATH}") + EXECUTE_PROCESS (COMMAND install_name_tool -delete_rpath "@QT_LIBRARY_DIR@" "${QGISPYDIR}/${MODSUBPATH}") IF (EXISTS "${MODDIR}/${MODPYI}") EXECUTE_PROCESS (COMMAND ditto "${MODDIR}/${MODPYI}" "${QGISPYDIR}/${MODPYI}") ENDIF () @@ -505,6 +525,11 @@ FOREACH (QFW ${QTLISTQG}) INSTALLNAMETOOL_CHANGE ("${QFW_CHG}" "${QFW_CHG_TO}" "${QPLUGDIR}/../iconengines/libqsvgicon.dylib") INSTALLNAMETOOL_CHANGE ("${QFW_CHG}" "${QFW_CHG_TO}" "${QPLUGDIR}/../phonon_backend/libphonon_qt7.dylib") INSTALLNAMETOOL_CHANGE ("${QFW_CHG}" "${QFW_CHG_TO}" "${QPLUGDIR}/../styles/libqmacstyle.dylib") + # quick plugin + IF (@OSX_HAVE_LOADERPATH@) + SET (QFW_CHG_TO "${ATLOADER}/../../../${LIBPOST}") + ENDIF () + INSTALLNAMETOOL_CHANGE ("${QFW_CHG}" "${QFW_CHG_TO}" "${QAPPDIR}/qml/QgsQuick/libqgis_quick_plugin.dylib") # qt fw IF (@OSX_HAVE_LOADERPATH@) SET (QFW_CHG_TO "${ATLOADER}/../../../${LIBPOST}") diff --git a/mac/readme.txt b/mac/readme.txt index 4b8ef24daaef..d1885a64c1b1 100644 --- a/mac/readme.txt +++ b/mac/readme.txt @@ -5,6 +5,7 @@ application package and fixing up the library paths. It is automatic during installation. There are 2 levels currently, specified with the cmake config option QGIS_MACAPP_BUNDLE, and one that always occurs: +-1 = do not run bundle install scrips 0 = (default) fixup the library paths for all QGIS libraries if @loader_path is available in the system (OS X 10.5+) 1 = bundle Qt, PyQt, PyQwt and OSG/osgEarth diff --git a/ms-windows/mxe/README.md b/ms-windows/mxe/README.md index 4460a5d78c6a..78c3835d8fbb 100644 --- a/ms-windows/mxe/README.md +++ b/ms-windows/mxe/README.md @@ -44,7 +44,10 @@ make MXE_TARGETS=i686-w64-mingw32.shared.posix -j 16 \ qtwinextras \ libzip \ gsl \ - libspatialindex + libspatialindex \ + exiv2 \ + protobuf + ``` When done, you can check and edit the `build-mxe.sh` script and set the `MXE` path to your mxe installation directory, `MXE` can also be passed as an environment variable. diff --git a/ms-windows/mxe/build-mxe.sh b/ms-windows/mxe/build-mxe.sh index 22b372538d0f..1166f04c3164 100755 --- a/ms-windows/mxe/build-mxe.sh +++ b/ms-windows/mxe/build-mxe.sh @@ -105,13 +105,17 @@ cat <<__TXT__ > ${RELEASE_DIR}/qt.conf Plugins = qt5plugins __TXT__ +# First cleanup +rm -rf ${BUILD_DIR} +rm -rf ${CCACHE_DIR} + # Make the zip cd ${RELEASE_DIR}/.. ZIP_NAME=qgis-mxe-release-$(date +%Y-%m-%d-%H-%I-%S).zip -zip -r ${ZIP_NAME} $(basename ${RELEASE_DIR}) +zip -r -m ${ZIP_NAME} $(basename ${RELEASE_DIR}) -# Cleanup +# Second cleanup rm -rf ${RELEASE_DIR} popd diff --git a/ms-windows/mxe/mxe.Dockerfile b/ms-windows/mxe/mxe.Dockerfile index 8d2f9cdb1e34..9cd266259302 100644 --- a/ms-windows/mxe/mxe.Dockerfile +++ b/ms-windows/mxe/mxe.Dockerfile @@ -20,7 +20,8 @@ RUN make MXE_TARGETS=x86_64-w64-mingw32.shared.posix -j 16 \ libzip \ gsl \ libspatialindex \ - exiv2 + exiv2 \ + protobuf RUN chmod -R a+rw /mxe/usr/x86_64-w64-mingw32.shared.posix diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index e87ac9aad67a..566dd66c0630 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -123,6 +123,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/classification ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/plugins ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/external/nlohmann @@ -141,6 +142,7 @@ IF (WITH_GUI) ${CMAKE_SOURCE_DIR}/src/gui/raster ${CMAKE_SOURCE_DIR}/src/gui/attributetable ${CMAKE_SOURCE_DIR}/src/gui/auth + ${CMAKE_SOURCE_DIR}/src/gui/devtools ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets/core ${CMAKE_SOURCE_DIR}/src/gui/effects diff --git a/python/analysis/auto_generated/mesh/qgsmeshcontours.sip.in b/python/analysis/auto_generated/mesh/qgsmeshcontours.sip.in index 3cc3abeb7d5d..988ab8dc0798 100644 --- a/python/analysis/auto_generated/mesh/qgsmeshcontours.sip.in +++ b/python/analysis/auto_generated/mesh/qgsmeshcontours.sip.in @@ -36,7 +36,7 @@ Caches the native and triangular mesh from data provider QgsGeometry exportLines( const QgsMeshDatasetIndex &index, double value, - QgsMeshRendererScalarSettings::DataInterpolationMethod method, + QgsMeshRendererScalarSettings::DataResamplingMethod method, QgsFeedback *feedback = 0 ); %Docstring Exports multi line string containing the contour line for particular dataset and value @@ -52,7 +52,7 @@ Exports multi line string containing the contour line for particular dataset and QgsGeometry exportPolygons( const QgsMeshDatasetIndex &index, double min_value, double max_value, - QgsMeshRendererScalarSettings::DataInterpolationMethod method, + QgsMeshRendererScalarSettings::DataResamplingMethod method, QgsFeedback *feedback = 0 ); %Docstring Exports multi polygons representing the areas with values in range for particular dataset diff --git a/python/core/additions/processing.py b/python/core/additions/processing.py index 95545e674baa..40e8f621faec 100644 --- a/python/core/additions/processing.py +++ b/python/core/additions/processing.py @@ -20,8 +20,12 @@ # add some __repr__ methods to processing classes def processing_source_repr(self): - return "".format( - self.source.staticValue(), self.selectedFeaturesOnly) + if self.featureLimit != -1: + return "".format( + self.source.staticValue(), self.selectedFeaturesOnly, self.featureLimit) + else: + return "".format( + self.source.staticValue(), self.selectedFeaturesOnly) def processing_output_layer_repr(self): diff --git a/python/core/auto_additions/qgsmaplayer.py b/python/core/auto_additions/qgsmaplayer.py index 4535f58d3d3d..fde3f16868da 100644 --- a/python/core/auto_additions/qgsmaplayer.py +++ b/python/core/auto_additions/qgsmaplayer.py @@ -9,7 +9,9 @@ QgsMapLayer.PluginLayer.__doc__ = "" QgsMapLayer.MeshLayer = QgsMapLayerType.MeshLayer QgsMapLayer.MeshLayer.__doc__ = "Added in 3.2" -QgsMapLayerType.__doc__ = 'Types of layers that can be added to a map\n\n.. versionadded:: 3.8\n\n' + '* ``VectorLayer``: ' + QgsMapLayerType.VectorLayer.__doc__ + '\n' + '* ``RasterLayer``: ' + QgsMapLayerType.RasterLayer.__doc__ + '\n' + '* ``PluginLayer``: ' + QgsMapLayerType.PluginLayer.__doc__ + '\n' + '* ``MeshLayer``: ' + QgsMapLayerType.MeshLayer.__doc__ +QgsMapLayer.VectorTileLayer = QgsMapLayerType.VectorTileLayer +QgsMapLayer.VectorTileLayer.__doc__ = "Added in 3.14" +QgsMapLayerType.__doc__ = 'Types of layers that can be added to a map\n\n.. versionadded:: 3.8\n\n' + '* ``VectorLayer``: ' + QgsMapLayerType.VectorLayer.__doc__ + '\n' + '* ``RasterLayer``: ' + QgsMapLayerType.RasterLayer.__doc__ + '\n' + '* ``PluginLayer``: ' + QgsMapLayerType.PluginLayer.__doc__ + '\n' + '* ``MeshLayer``: ' + QgsMapLayerType.MeshLayer.__doc__ + '\n' + '* ``VectorTileLayer``: ' + QgsMapLayerType.VectorTileLayer.__doc__ # -- QgsMapLayer.LayerFlag.baseClass = QgsMapLayer QgsMapLayer.LayerFlags.baseClass = QgsMapLayer diff --git a/python/core/auto_additions/qgsproject.py b/python/core/auto_additions/qgsproject.py index 8f7747e4d2ad..43f6e31a0bac 100644 --- a/python/core/auto_additions/qgsproject.py +++ b/python/core/auto_additions/qgsproject.py @@ -12,3 +12,10 @@ QgsProject.FileFormat.__doc__ = 'Flags which control project read behavior.\n\n.. versionadded:: 3.12\n\n' + '* ``Qgz``: ' + QgsProject.FileFormat.Qgz.__doc__ + '\n' + '* ``Qgs``: ' + QgsProject.FileFormat.Qgs.__doc__ # -- QgsProject.FileFormat.baseClass = QgsProject +# monkey patching scoped based enum +QgsProject.AvoidIntersectionsMode.AllowIntersections.__doc__ = "Overlap with any feature allowed when digitizing new features" +QgsProject.AvoidIntersectionsMode.AvoidIntersectionsCurrentLayer.__doc__ = "Overlap with features from the active layer when digitizing new features not allowed" +QgsProject.AvoidIntersectionsMode.AvoidIntersectionsLayers.__doc__ = "Overlap with features from a specified list of layers when digitizing new features not allowed" +QgsProject.AvoidIntersectionsMode.__doc__ = 'Flags which control how intersections of pre-existing feature are handled when digitizing new features.\n\n.. versionadded:: 3.14\n\n' + '* ``AllowIntersections``: ' + QgsProject.AvoidIntersectionsMode.AllowIntersections.__doc__ + '\n' + '* ``AvoidIntersectionsCurrentLayer``: ' + QgsProject.AvoidIntersectionsMode.AvoidIntersectionsCurrentLayer.__doc__ + '\n' + '* ``AvoidIntersectionsLayers``: ' + QgsProject.AvoidIntersectionsMode.AvoidIntersectionsLayers.__doc__ +# -- +QgsProject.AvoidIntersectionsMode.baseClass = QgsProject diff --git a/python/core/auto_additions/qgsproviderconnectionmodel.py b/python/core/auto_additions/qgsproviderconnectionmodel.py new file mode 100644 index 000000000000..0d1db9650bed --- /dev/null +++ b/python/core/auto_additions/qgsproviderconnectionmodel.py @@ -0,0 +1,2 @@ +# The following has been generated automatically from src/core/qgsproviderconnectionmodel.h +QgsProviderConnectionModel.Role.baseClass = QgsProviderConnectionModel diff --git a/python/core/auto_additions/qgsscalebarrenderer.py b/python/core/auto_additions/qgsscalebarrenderer.py new file mode 100644 index 000000000000..2406a9e901de --- /dev/null +++ b/python/core/auto_additions/qgsscalebarrenderer.py @@ -0,0 +1,15 @@ +# The following has been generated automatically from src/core/scalebar/qgsscalebarrenderer.h +# monkey patching scoped based enum +QgsScaleBarRenderer.Flag.FlagUsesLineSymbol.__doc__ = "Renderer utilizes the scalebar line symbol (see QgsScaleBarSettings::lineSymbol() )" +QgsScaleBarRenderer.Flag.FlagUsesFillSymbol.__doc__ = "Renderer utilizes the scalebar fill symbol (see QgsScaleBarSettings::fillSymbol() )" +QgsScaleBarRenderer.Flag.FlagUsesAlternateFillSymbol.__doc__ = "Renderer utilizes the alternate scalebar fill symbol (see QgsScaleBarSettings::alternateFillSymbol() )" +QgsScaleBarRenderer.Flag.FlagRespectsUnits.__doc__ = "Renderer respects the QgsScaleBarSettings::units() setting" +QgsScaleBarRenderer.Flag.FlagRespectsMapUnitsPerScaleBarUnit.__doc__ = "Renderer respects the QgsScaleBarSettings::mapUnitsPerScaleBarUnit() setting" +QgsScaleBarRenderer.Flag.FlagUsesUnitLabel.__doc__ = "Renderer uses the QgsScaleBarSettings::unitLabel() setting" +QgsScaleBarRenderer.Flag.FlagUsesSegments.__doc__ = "Renderer uses the scalebar segments" +QgsScaleBarRenderer.Flag.FlagUsesLabelBarSpace.__doc__ = "Renderer uses the QgsScaleBarSettings::labelBarSpace() setting" +QgsScaleBarRenderer.Flag.FlagUsesLabelVerticalPlacement.__doc__ = "Renderer uses the QgsScaleBarSettings::labelVerticalPlacement() setting" +QgsScaleBarRenderer.Flag.FlagUsesLabelHorizontalPlacement.__doc__ = "Renderer uses the QgsScaleBarSettings::labelHorizontalPlacement() setting" +QgsScaleBarRenderer.Flag.FlagUsesAlignment.__doc__ = "Renderer uses the QgsScaleBarSettings::alignment() setting" +QgsScaleBarRenderer.Flag.__doc__ = 'Flags which control scalebar renderer behavior.\n\n.. versionadded:: 3.14\n\n' + '* ``FlagUsesLineSymbol``: ' + QgsScaleBarRenderer.Flag.FlagUsesLineSymbol.__doc__ + '\n' + '* ``FlagUsesFillSymbol``: ' + QgsScaleBarRenderer.Flag.FlagUsesFillSymbol.__doc__ + '\n' + '* ``FlagUsesAlternateFillSymbol``: ' + QgsScaleBarRenderer.Flag.FlagUsesAlternateFillSymbol.__doc__ + '\n' + '* ``FlagRespectsUnits``: ' + QgsScaleBarRenderer.Flag.FlagRespectsUnits.__doc__ + '\n' + '* ``FlagRespectsMapUnitsPerScaleBarUnit``: ' + QgsScaleBarRenderer.Flag.FlagRespectsMapUnitsPerScaleBarUnit.__doc__ + '\n' + '* ``FlagUsesUnitLabel``: ' + QgsScaleBarRenderer.Flag.FlagUsesUnitLabel.__doc__ + '\n' + '* ``FlagUsesSegments``: ' + QgsScaleBarRenderer.Flag.FlagUsesSegments.__doc__ + '\n' + '* ``FlagUsesLabelBarSpace``: ' + QgsScaleBarRenderer.Flag.FlagUsesLabelBarSpace.__doc__ + '\n' + '* ``FlagUsesLabelVerticalPlacement``: ' + QgsScaleBarRenderer.Flag.FlagUsesLabelVerticalPlacement.__doc__ + '\n' + '* ``FlagUsesLabelHorizontalPlacement``: ' + QgsScaleBarRenderer.Flag.FlagUsesLabelHorizontalPlacement.__doc__ + '\n' + '* ``FlagUsesAlignment``: ' + QgsScaleBarRenderer.Flag.FlagUsesAlignment.__doc__ +# -- diff --git a/python/core/auto_additions/qgssnappingconfig.py b/python/core/auto_additions/qgssnappingconfig.py index 46280cd94f95..2a877dbf1042 100644 --- a/python/core/auto_additions/qgssnappingconfig.py +++ b/python/core/auto_additions/qgssnappingconfig.py @@ -3,3 +3,4 @@ QgsSnappingConfig.SnappingTypes.baseClass = QgsSnappingConfig QgsSnappingConfig.SnappingTypeFlag.baseClass = QgsSnappingConfig SnappingTypeFlag = QgsSnappingConfig # dirty hack since SIP seems to introduce the flags in module +QgsSnappingConfig.ScaleDependencyMode.baseClass = QgsSnappingConfig diff --git a/python/core/auto_additions/qgstaskmanager.py b/python/core/auto_additions/qgstaskmanager.py new file mode 100644 index 000000000000..e6ca414d0485 --- /dev/null +++ b/python/core/auto_additions/qgstaskmanager.py @@ -0,0 +1,2 @@ +# The following has been generated automatically from src/core/qgstaskmanager.h +QgsTask.TaskStatus.baseClass = QgsTask diff --git a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in index c363b0894437..543c5b86b997 100644 --- a/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in +++ b/python/core/auto_generated/fieldformatter/qgsvaluerelationfieldformatter.sip.in @@ -25,7 +25,7 @@ features on another layer. public: struct ValueRelationItem { - ValueRelationItem( const QVariant &key, const QString &value ); + ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString() ); %Docstring Constructor for ValueRelationItem %End @@ -37,6 +37,7 @@ Constructor for ValueRelationItem QVariant key; QString value; + QString description; }; typedef QVector < QgsValueRelationFieldFormatter::ValueRelationItem > ValueRelationCache; diff --git a/python/core/auto_generated/geometry/qgsgeometry.sip.in b/python/core/auto_generated/geometry/qgsgeometry.sip.in index 4bfddd3ab202..1d9ac9f3ccb4 100644 --- a/python/core/auto_generated/geometry/qgsgeometry.sip.in +++ b/python/core/auto_generated/geometry/qgsgeometry.sip.in @@ -839,7 +839,7 @@ Rotate this geometry around the Z axis :return: OperationResult a result code: success or reason of failure %End - OperationResult splitGeometry( const QVector &splitLine, QVector &newGeometries /Out/, bool topological, QVector &topologyTestPoints /Out/ ) /Deprecated/; + OperationResult splitGeometry( const QVector &splitLine, QVector &newGeometries /Out/, bool topological, QVector &topologyTestPoints /Out/, bool splitFeature = true ) /Deprecated/; %Docstring Splits this geometry according to a given line. @@ -847,6 +847,7 @@ Splits this geometry according to a given line. \param[out] newGeometries list of new geometries that have been created with the split :param topological: ``True`` if topological editing is enabled \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset +:param splitFeature: Set to True if you want to split a feature, otherwise set to False to split parts :return: OperationResult a result code: success or reason of failure @@ -854,7 +855,7 @@ Splits this geometry according to a given line. - will be removed in QGIS 4.0. Use the variant which accepts QgsPoint objects instead of QgsPointXY. %End - OperationResult splitGeometry( const QgsPointSequence &splitLine, QVector &newGeometries /Out/, bool topological, QgsPointSequence &topologyTestPoints /Out/ ); + OperationResult splitGeometry( const QgsPointSequence &splitLine, QVector &newGeometries /Out/, bool topological, QgsPointSequence &topologyTestPoints /Out/, bool splitFeature = true ); %Docstring Splits this geometry according to a given line. @@ -862,6 +863,8 @@ Splits this geometry according to a given line. \param[out] newGeometries list of new geometries that have been created with the split :param topological: ``True`` if topological editing is enabled \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset +:param splitFeature: Set to True if you want to split a feature, otherwise set to False to split parts + fix this bug? :return: OperationResult a result code: success or reason of failure %End @@ -1539,7 +1542,32 @@ Exports the geometry to a GeoJSON string. %End - QgsGeometry convertToType( QgsWkbTypes::GeometryType destType, bool destMultipart = false ) const /Factory/; + QVector< QgsGeometry > coerceToType( QgsWkbTypes::Type type ) const; +%Docstring +Attempts to coerce this geometry into the specified destination ``type``. + +This method will do anything possible to force the current geometry into the specified type. E.g. +- lines or polygons will be converted to points by return either a single multipoint geometry or multiple +single point geometries. +- polygons will be converted to lines by extracting their exterior and interior rings, returning +either a multilinestring or multiple single line strings as dictated by ``type``. +- lines will be converted to polygon rings if ``type`` is a polygon type +- curved geometries will be segmented if ``type`` is non-curved. +- multi geometries will be converted to a list of single geometries +- single geometries will be upgraded to multi geometries +- z or m values will be added or dropped as required. + +.. note:: + + This method is much stricter than convertToType(), as it considers the exact WKB type + of geometries instead of the geometry family (point/line/polygon), and tries more exhaustively + to coerce geometries to the desired ``type``. It also correctly maintains curves and z/m values + wherever appropriate. + +.. versionadded:: 3.14 +%End + + QgsGeometry convertToType( QgsWkbTypes::GeometryType destType, bool destMultipart = false ) const; %Docstring Try to convert the geometry to the requested type @@ -1548,6 +1576,12 @@ Try to convert the geometry to the requested type :return: the converted geometry or ``None`` if the conversion fails. +.. note:: + + The coerceToType() method applies much stricter and more exhaustive attempts to convert + between geometry types, and is recommended instead of this method. This method force drops + curves and any z or m values present in the geometry. + .. versionadded:: 2.2 %End diff --git a/python/core/auto_generated/layertree/qgslayertreelayer.sip.in b/python/core/auto_generated/layertree/qgslayertreelayer.sip.in index 589f5da50681..863625fb5146 100644 --- a/python/core/auto_generated/layertree/qgslayertreelayer.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreelayer.sip.in @@ -136,6 +136,24 @@ set the expression to evaluate Returns the expression member of the LayerTreeNode .. versionadded:: 3.10 +%End + + QgsLegendPatchShape patchShape() const; +%Docstring +Returns the symbol patch shape to use when rendering the legend node symbol. + +.. seealso:: :py:func:`setPatchShape` + +.. versionadded:: 3.14 +%End + + void setPatchShape( const QgsLegendPatchShape &shape ); +%Docstring +Sets the symbol patch ``shape`` to use when rendering the legend node symbol. + +.. seealso:: :py:func:`patchShape` + +.. versionadded:: 3.14 %End signals: diff --git a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in index 0a54f85db4f1..ca0ccc71f447 100644 --- a/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreemodellegendnode.sip.in @@ -99,6 +99,7 @@ Default implementation does nothing. * double maxSiblingSymbolWidth; + QgsLegendPatchShape patchShape; }; struct ItemMetrics @@ -322,6 +323,24 @@ Sets format of text to be shown on top of the symbol. Label of the symbol, user defined label will be used, otherwise will default to the label made by QGIS. .. versionadded:: 3.10 +%End + + QgsLegendPatchShape patchShape() const; +%Docstring +Returns the symbol patch shape to use when rendering the legend node symbol. + +.. seealso:: :py:func:`setPatchShape` + +.. versionadded:: 3.14 +%End + + void setPatchShape( const QgsLegendPatchShape &shape ); +%Docstring +Sets the symbol patch ``shape`` to use when rendering the legend node symbol. + +.. seealso:: :py:func:`patchShape` + +.. versionadded:: 3.14 %End QString evaluateLabel( const QgsExpressionContext &context = QgsExpressionContext(), const QString &label = QString() ); diff --git a/python/core/auto_generated/layertree/qgslayertreenode.sip.in b/python/core/auto_generated/layertree/qgslayertreenode.sip.in index 3c35af387864..f15c41925f9d 100644 --- a/python/core/auto_generated/layertree/qgslayertreenode.sip.in +++ b/python/core/auto_generated/layertree/qgslayertreenode.sip.in @@ -208,6 +208,13 @@ Returns a list of any checked layers which belong to this node or its children. .. versionadded:: 3.0 +%End + + int depth() const; +%Docstring +Returns the depth of this node, i.e. the number of its ancestors + +.. versionadded:: 3.14 %End bool isExpanded() const; diff --git a/python/core/auto_generated/layertree/qgslegendpatchshape.sip.in b/python/core/auto_generated/layertree/qgslegendpatchshape.sip.in new file mode 100644 index 000000000000..029348438bf0 --- /dev/null +++ b/python/core/auto_generated/layertree/qgslegendpatchshape.sip.in @@ -0,0 +1,148 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layertree/qgslegendpatchshape.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsLegendPatchShape +{ +%Docstring +Represents a patch shape for use in map legends. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgslegendpatchshape.h" +%End + public: + + QgsLegendPatchShape(); +%Docstring +Constructor for a null QgsLegendPatchShape. + +A null QgsLegendPatchShape indicates that the default legend patch shape +should be used instead. +%End + + QgsLegendPatchShape( QgsSymbol::SymbolType type, + const QgsGeometry &geometry, + bool preserveAspectRatio = true ); +%Docstring +Constructor for QgsLegendPatchShape. + +The ``type`` argument specifies the symbol type associated with this patch. + +The ``geometry`` argument gives the shape of the patch to render. See setGeometry() +for further details on the geometry requirements. + +If ``preserveAspectRatio`` is ``True``, then the patch shape should preserve its aspect ratio when +it is resized to fit a desired legend patch size. +%End + + bool isNull() const; +%Docstring +Returns ``True`` if the patch shape is a null QgsLegendPatchShape, +which indicates that the default legend patch shape should be used instead. +%End + + QgsSymbol::SymbolType symbolType() const; +%Docstring +Returns the symbol type associated with this patch. + +.. seealso:: :py:func:`setSymbolType` +%End + + void setSymbolType( QgsSymbol::SymbolType type ); +%Docstring +Sets the symbol ``type`` associated with this patch. + +.. seealso:: :py:func:`symbolType` +%End + + QgsGeometry geometry() const; +%Docstring +Returns the geometry for the patch shape. + +.. seealso:: :py:func:`setGeometry` +%End + + void setGeometry( const QgsGeometry &geometry ); +%Docstring +Sets the ``geometry`` for the patch shape. + +The origin and size of the ``geometry`` is not important, as the legend +renderer will automatically scale and transform the geometry to match +the desired overall patch bounds. + +Geometries for legend patches are rendered respecting the traditional +"y values increase toward the top of the map" convention, as opposed +to the standard computer graphics convention of "y values increase toward +the bottom of the display". + +.. warning:: + + The geometry type should match the patch shape's symbolType(), + e.g. a fill symbol type should only have Polygon or MultiPolygon geometries + set, while a line symbol type must have LineString or MultiLineString geometries. + +.. seealso:: :py:func:`geometry` +%End + + bool preserveAspectRatio() const; +%Docstring +Returns ``True`` if the patch shape should preserve its aspect ratio when +it is resized to fit a desired legend patch size. + +.. seealso:: :py:func:`setPreserveAspectRatio` +%End + + void setPreserveAspectRatio( bool preserve ); +%Docstring +Sets whether the patch shape should ``preserve`` its aspect ratio when +it is resized to fit a desired legend patch size. + +The default behavior is to respect the geometry()'s aspect ratio. + +.. seealso:: :py:func:`setPreserveAspectRatio` +%End + + QList< QList< QPolygonF > > toQPolygonF( QgsSymbol::SymbolType type, QSizeF size ) const; +%Docstring +Converts the patch shape to a set of QPolygonF objects representing +how the patch should be drawn for a symbol of the given ``type`` at the specified ``size`` (as +geometry parts and rings). +%End + + static QList< QList< QPolygonF > > defaultPatch( QgsSymbol::SymbolType type, QSizeF size ); +%Docstring +Returns the default patch geometry for the given symbol ``type`` and ``size`` as a set of QPolygonF objects (parts and rings). +%End + + void readXml( const QDomElement &element, const QgsReadWriteContext &context ); +%Docstring +Read settings from a DOM ``element``. + +.. seealso:: :py:func:`writeXml` +%End + + void writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const; +%Docstring +Write settings into a DOM ``element``. + +.. seealso:: :py:func:`readXml` +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layertree/qgslegendpatchshape.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in index c4309900beb6..c5b9b6fe2368 100644 --- a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in @@ -502,6 +502,15 @@ Sets the ``map`` to associate with the legend. Returns the associated map. .. seealso:: :py:func:`setLinkedMap` +%End + + QString themeName() const; +%Docstring +Returns the name of the theme currently linked to the legend. + +This usually equates to the theme rendered in the linkedMap(). + +.. versionadded:: 3.14 %End void updateLegend(); diff --git a/python/core/auto_generated/layout/qgslayoutitemmap.sip.in b/python/core/auto_generated/layout/qgslayoutitemmap.sip.in index 7854144cdb57..d36d56f3de5e 100644 --- a/python/core/auto_generated/layout/qgslayoutitemmap.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemmap.sip.in @@ -296,6 +296,8 @@ Preset name that decides which layers and layer styles are used for map renderin used when followVisibilityPreset() returns ``True``. .. seealso:: :py:func:`setFollowVisibilityPresetName` + +.. seealso:: :py:func:`themeChanged` %End void setFollowVisibilityPresetName( const QString &name ); @@ -303,6 +305,8 @@ used when followVisibilityPreset() returns ``True``. Sets preset name for map rendering. See followVisibilityPresetName() for more details. .. seealso:: :py:func:`followVisibilityPresetName` + +.. seealso:: :py:func:`themeChanged` %End virtual void moveContent( double dx, double dy ); @@ -680,6 +684,18 @@ Emitted when the map has been prepared for atlas rendering, just before actual r %Docstring Emitted when layer style overrides are changed... a means to let associated legend items know they should update +%End + + void themeChanged( const QString &theme ); +%Docstring +Emitted when the map's associated ``theme`` is changed. + +.. note:: + + This signal is not emitted when the definition of the theme changes, only the map + is linked to a different theme then it previously was. + +.. versionadded:: 3.14 %End public slots: diff --git a/python/core/auto_generated/layout/qgslayoutitemmarker.sip.in b/python/core/auto_generated/layout/qgslayoutitemmarker.sip.in new file mode 100644 index 000000000000..8e195ff72a74 --- /dev/null +++ b/python/core/auto_generated/layout/qgslayoutitemmarker.sip.in @@ -0,0 +1,152 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutitemmarker.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsLayoutItemMarker : QgsLayoutItem +{ +%Docstring +A layout item for showing marker symbols. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgslayoutitemmarker.h" +%End + public: + + explicit QgsLayoutItemMarker( QgsLayout *layout ); +%Docstring +Constructor for QgsLayoutItemMarker, with the specified parent ``layout``. +%End + ~QgsLayoutItemMarker(); + + static QgsLayoutItemMarker *create( QgsLayout *layout ) /Factory/; +%Docstring +Returns a new marker item for the specified ``layout``. + +The caller takes responsibility for deleting the returned object. +%End + + + virtual int type() const; + + virtual QIcon icon() const; + + + void setSymbol( QgsMarkerSymbol *symbol /Transfer/ ); +%Docstring +Sets the marker ``symbol`` used to draw the shape. Ownership is transferred. + +.. seealso:: :py:func:`symbol` +%End + + QgsMarkerSymbol *symbol(); +%Docstring +Returns the marker symbol used to draw the shape. + +.. seealso:: :py:func:`setSymbol` +%End + + void setLinkedMap( QgsLayoutItemMap *map ); +%Docstring +Sets the ``map`` object for rotation. + +If this is set then the marker will be rotated by the same +amount as the specified map object. This is useful especially for +syncing north arrows with a map item. + +.. seealso:: :py:func:`linkedMap` +%End + + QgsLayoutItemMap *linkedMap() const; +%Docstring +Returns the linked rotation map, if set. An ``None`` means map rotation is +disabled. If this is set then the marker is rotated by the same amount +as the specified map object. + +.. seealso:: :py:func:`setLinkedMap` +%End + + double northArrowRotation() const; +%Docstring +When the marker is linked to a map in north arrow rotation mode, +returns the current north arrow rotation for the marker. + +.. seealso:: :py:func:`setLinkedMap` +%End + + QgsLayoutNorthArrowHandler::NorthMode northMode() const; +%Docstring +Returns the mode used to align the marker to a map's North. + +.. seealso:: :py:func:`setNorthMode` + +.. seealso:: :py:func:`northOffset` +%End + + void setNorthMode( QgsLayoutNorthArrowHandler::NorthMode mode ); +%Docstring +Sets the ``mode`` used to align the marker to a map's North. + +.. seealso:: :py:func:`northMode` + +.. seealso:: :py:func:`setNorthOffset` +%End + + double northOffset() const; +%Docstring +Returns the offset added to the marker's rotation from a map's North. + +.. seealso:: :py:func:`setNorthOffset` + +.. seealso:: :py:func:`northMode` +%End + + void setNorthOffset( double offset ); +%Docstring +Sets the ``offset`` added to the marker's rotation from a map's North. + +.. seealso:: :py:func:`northOffset` + +.. seealso:: :py:func:`setNorthMode` +%End + + virtual QRectF boundingRect() const; + + + virtual QgsLayoutSize fixedSize() const; + + + virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; + + + protected: + + virtual void draw( QgsLayoutItemRenderContext &context ); + + + virtual bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + + + virtual void finalizeRestoreFromXml(); + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutitemmarker.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/layout/qgslayoutitempicture.sip.in b/python/core/auto_generated/layout/qgslayoutitempicture.sip.in index 09b824e4c16c..c477a27b23d8 100644 --- a/python/core/auto_generated/layout/qgslayoutitempicture.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitempicture.sip.in @@ -62,12 +62,14 @@ The caller takes responsibility for deleting the returned object. %End - void setPicturePath( const QString &path ); + void setPicturePath( const QString &path, Format format = FormatUnknown ); %Docstring Sets the source ``path`` of the image (may be svg or a raster format). Data defined picture source may override this value. The path can either be a local path or a remote (http) path. +Ideally, the ``format`` argument should specify the image format. + .. seealso:: :py:func:`picturePath` %End @@ -249,6 +251,17 @@ Sets the stroke ``width`` (in layout units) used for parametrized SVG files. Format mode() const; %Docstring Returns the current picture mode (image format). + +.. seealso:: :py:func:`setMode` +%End + + void setMode( Format mode ); +%Docstring +Sets the current picture ``mode`` (image format). + +.. seealso:: :py:func:`mode` + +.. versionadded:: 3.14 %End virtual void finalizeRestoreFromXml(); diff --git a/python/core/auto_generated/layout/qgslayoutitemregistry.sip.in b/python/core/auto_generated/layout/qgslayoutitemregistry.sip.in index 076cd516167d..23c8e74b4437 100644 --- a/python/core/auto_generated/layout/qgslayoutitemregistry.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemregistry.sip.in @@ -192,6 +192,7 @@ of layout items. Layout3DMap, LayoutManualTable, + LayoutMarker, // item types provided by plugins PluginItem, diff --git a/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in b/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in index 98b512b78d03..4a00c6aeef19 100644 --- a/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in @@ -208,6 +208,77 @@ Sets the text ``format`` used for drawing text in the scalebar. .. seealso:: :py:func:`textFormat` .. versionadded:: 3.2 +%End + + QgsLineSymbol *lineSymbol() const; +%Docstring +Returns the line symbol used to render the scalebar (only used for some scalebar types). + +Ownership is not transferred. + +.. seealso:: :py:func:`setLineSymbol` + +.. versionadded:: 3.14 +%End + + void setLineSymbol( QgsLineSymbol *symbol /Transfer/ ); +%Docstring +Sets the line ``symbol`` used to render the scalebar (only used for some scalebar types). Ownership of ``symbol`` is +transferred to the scalebar. + +.. seealso:: :py:func:`lineSymbol` + +.. versionadded:: 3.14 +%End + + QgsFillSymbol *fillSymbol() const; +%Docstring +Returns the primary fill symbol used to render the scalebar (only used for some scalebar types). + +Ownership is not transferred. + +.. seealso:: :py:func:`setFillSymbol` + +.. seealso:: :py:func:`alternateFillSymbol` + +.. versionadded:: 3.14 +%End + + void setFillSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the primary fill ``symbol`` used to render the scalebar (only used for some scalebar types). Ownership of ``symbol`` is +transferred to the scalebar. + +.. seealso:: :py:func:`fillSymbol` + +.. seealso:: :py:func:`setAlternateFillSymbol` + +.. versionadded:: 3.14 +%End + + QgsFillSymbol *alternateFillSymbol() const; +%Docstring +Returns the secondary fill symbol used to render the scalebar (only used for some scalebar types). + +Ownership is not transferred. + +.. seealso:: :py:func:`setAlternateFillSymbol` + +.. seealso:: :py:func:`fillSymbol` + +.. versionadded:: 3.14 +%End + + void setAlternateFillSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the secondary fill ``symbol`` used to render the scalebar (only used for some scalebar types). Ownership of ``symbol`` is +transferred to the scalebar. + +.. seealso:: :py:func:`alternateFillSymbol` + +.. seealso:: :py:func:`setFillSymbol` + +.. versionadded:: 3.14 %End QFont font() const /Deprecated/; @@ -254,78 +325,105 @@ Sets the ``color`` used for drawing text in the scalebar. use setTextFormat() instead %End - QColor fillColor() const; + QColor fillColor() const /Deprecated/; %Docstring Returns the color used for fills in the scalebar. .. seealso:: :py:func:`setFillColor` .. seealso:: :py:func:`fillColor2` + +.. deprecated:: + use fillSymbol() instead %End - void setFillColor( const QColor &color ); + void setFillColor( const QColor &color ) /Deprecated/; %Docstring Sets the ``color`` used for fills in the scalebar. .. seealso:: :py:func:`fillColor` .. seealso:: :py:func:`setFillColor2` + +.. deprecated:: + use setFillSymbol() instead %End - QColor fillColor2() const; + QColor fillColor2() const /Deprecated/; %Docstring Returns the secondary color used for fills in the scalebar. .. seealso:: :py:func:`setFillColor2` .. seealso:: :py:func:`fillColor` + +.. deprecated:: + use alternateFillSymbol() instead %End - void setFillColor2( const QColor &color ); + void setFillColor2( const QColor &color ) /Deprecated/; %Docstring Sets the secondary ``color`` used for fills in the scalebar. .. seealso:: :py:func:`fillColor2` .. seealso:: :py:func:`setFillColor2` + +.. deprecated:: + use setAlternateFillSymbol() instead %End - QColor lineColor() const; + QColor lineColor() const /Deprecated/; %Docstring Returns the color used for lines in the scalebar. .. seealso:: :py:func:`setLineColor` + +.. deprecated:: + use lineSymbol() instead %End - void setLineColor( const QColor &color ); + void setLineColor( const QColor &color ) /Deprecated/; %Docstring Sets the ``color`` used for lines in the scalebar. .. seealso:: :py:func:`lineColor` + +.. deprecated:: + use setLineSymbol() instead %End - double lineWidth() const; + double lineWidth() const /Deprecated/; %Docstring Returns the line width in millimeters for lines in the scalebar. .. seealso:: :py:func:`setLineWidth` + +.. deprecated:: + use lineSymbol() instead %End - void setLineWidth( double width ); + void setLineWidth( double width ) /Deprecated/; %Docstring Sets the line ``width`` in millimeters for lines in the scalebar. .. seealso:: :py:func:`lineWidth` + +.. deprecated:: + use setLineSymbol() instead %End - QPen pen() const; + QPen pen() const /Deprecated/; %Docstring Returns the pen used for drawing outlines in the scalebar. .. seealso:: :py:func:`brush` + +.. deprecated:: + use lineSymbol() instead %End - QBrush brush() const; + QBrush brush() const /Deprecated/; %Docstring Returns the primary brush for the scalebar. @@ -334,9 +432,12 @@ Returns the primary brush for the scalebar. .. seealso:: :py:func:`brush2` .. seealso:: :py:func:`pen` + +.. deprecated:: + use fillSymbol() instead %End - QBrush brush2() const; + QBrush brush2() const /Deprecated/; %Docstring Returns the secondary brush for the scalebar. This is used for alternating color style scalebars, such as single and double box styles. @@ -344,6 +445,9 @@ as single and double box styles. :return: QBrush used for secondary color areas .. seealso:: :py:func:`brush` + +.. deprecated:: + use alternateFillSymbol() instead %End double height() const; @@ -466,32 +570,44 @@ Sets the distance ``units`` used by the scalebar. .. seealso:: :py:func:`units` %End - Qt::PenJoinStyle lineJoinStyle() const; + Qt::PenJoinStyle lineJoinStyle() const /Deprecated/; %Docstring Returns the join style used for drawing lines in the scalebar. .. seealso:: :py:func:`setLineJoinStyle` + +.. deprecated:: + use lineSymbol() instead %End - void setLineJoinStyle( Qt::PenJoinStyle style ); + void setLineJoinStyle( Qt::PenJoinStyle style ) /Deprecated/; %Docstring Sets the join ``style`` used when drawing the lines in the scalebar .. seealso:: :py:func:`lineJoinStyle` + +.. deprecated:: + use setLineSymbol() instead %End - Qt::PenCapStyle lineCapStyle() const; + Qt::PenCapStyle lineCapStyle() const /Deprecated/; %Docstring Returns the cap style used for drawing lines in the scalebar. .. seealso:: :py:func:`setLineCapStyle` + +.. deprecated:: + use lineSymbol() instead %End - void setLineCapStyle( Qt::PenCapStyle style ); + void setLineCapStyle( Qt::PenCapStyle style ) /Deprecated/; %Docstring Sets the cap ``style`` used when drawing the lines in the scalebar. .. seealso:: :py:func:`lineCapStyle` + +.. deprecated:: + use setLineSymbol() instead %End void applyDefaultSettings(); @@ -499,6 +615,15 @@ Sets the cap ``style`` used when drawing the lines in the scalebar. Applies the default scalebar settings to the scale bar. .. seealso:: :py:func:`applyDefaultSize` +%End + + bool applyDefaultRendererSettings( QgsScaleBarRenderer *renderer ); +%Docstring +Applies any default settings relating to the specified ``renderer`` to the item. + +Returns ``True`` if settings were applied. + +.. versionadded:: 3.14 %End QgsUnitTypes::DistanceUnit guessUnits() const; diff --git a/python/core/auto_generated/layout/qgslayoutnortharrowhandler.sip.in b/python/core/auto_generated/layout/qgslayoutnortharrowhandler.sip.in new file mode 100644 index 000000000000..374eee17d35b --- /dev/null +++ b/python/core/auto_generated/layout/qgslayoutnortharrowhandler.sip.in @@ -0,0 +1,106 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutnortharrowhandler.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsLayoutNorthArrowHandler: QObject +{ +%Docstring +An object which handles north-arrow type behavior for layout items. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgslayoutnortharrowhandler.h" +%End + public: + + enum NorthMode + { + GridNorth, + TrueNorth, + }; + + QgsLayoutNorthArrowHandler( QObject *parent /TransferThis/ ); +%Docstring +Constructor for QgsLayoutNorthArrowHandler, with the specified parent ``object``. +%End + + double arrowRotation() const; +%Docstring +Returns the rotation to be used for the arrow, in degrees clockwise. +%End + + void setLinkedMap( QgsLayoutItemMap *map ); +%Docstring +Sets the linked ``map`` item. + +.. seealso:: :py:func:`linkedMap` +%End + + QgsLayoutItemMap *linkedMap() const; +%Docstring +Returns the linked rotation map, if set. An ``None`` means arrow calculation is +disabled. + +.. seealso:: :py:func:`setLinkedMap` +%End + + NorthMode northMode() const; +%Docstring +Returns the mode used to calculate the arrow rotation. + +.. seealso:: :py:func:`setNorthMode` + +.. seealso:: :py:func:`northOffset` +%End + + void setNorthMode( NorthMode mode ); +%Docstring +Sets the ``mode`` used to calculate the arrow rotation. + +.. seealso:: :py:func:`northMode` + +.. seealso:: :py:func:`setNorthOffset` +%End + + double northOffset() const; +%Docstring +Returns the offset added to the arrows's rotation from a map's North. + +.. seealso:: :py:func:`setNorthOffset` + +.. seealso:: :py:func:`northMode` +%End + + void setNorthOffset( double offset ); +%Docstring +Sets the ``offset`` added to the arrows's rotation from a map's North. + +.. seealso:: :py:func:`northOffset` + +.. seealso:: :py:func:`setNorthMode` +%End + + signals: + void arrowRotationChanged( double newRotation ); +%Docstring +Emitted on arrow rotation change +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/layout/qgslayoutnortharrowhandler.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/locator/qgslocatorfilter.sip.in b/python/core/auto_generated/locator/qgslocatorfilter.sip.in index 88112361fea8..e6d22dd86c39 100644 --- a/python/core/auto_generated/locator/qgslocatorfilter.sip.in +++ b/python/core/auto_generated/locator/qgslocatorfilter.sip.in @@ -252,6 +252,14 @@ Tests a ``candidate`` string to see if it should be considered a match for a specified ``search`` string. Filter subclasses should use this method when comparing strings instead of directly using QString.contains() or Python 'in' checks. +%End + + static double fuzzyScore( const QString &candidate, const QString &search ); +%Docstring +Tests a ``candidate`` string to see how likely it is a match for +a specified ``search`` string. + +.. versionadded:: 3.14 %End bool enabled() const; diff --git a/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in b/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in index a749ac506e60..380da3cc81be 100644 --- a/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshdataprovider.sip.in @@ -12,43 +12,6 @@ -class QgsMeshDatasetIndex -{ -%Docstring - -QgsMeshDatasetIndex is index that identifies the dataset group (e.g. wind speed) -and a dataset in this group (e.g. magnitude of wind speed in particular time) - -.. note:: - - The API is considered EXPERIMENTAL and can be changed without a notice - -.. versionadded:: 3.4 -%End - -%TypeHeaderCode -#include "qgsmeshdataprovider.h" -%End - public: - QgsMeshDatasetIndex( int group = -1, int dataset = -1 ); -%Docstring -Creates an index. -1 represents invalid group/dataset -%End - int group() const; -%Docstring -Returns a group index -%End - int dataset() const; -%Docstring -Returns a dataset index within group() -%End - bool isValid() const; -%Docstring -Returns whether index is valid, ie at least groups is set -%End - bool operator == ( QgsMeshDatasetIndex other ) const; - bool operator != ( QgsMeshDatasetIndex other ) const; -}; typedef QgsPoint QgsMeshVertex; @@ -114,495 +77,6 @@ Remove all vertices, edges and faces }; -class QgsMeshDatasetValue -{ -%Docstring - -QgsMeshDatasetValue represents single dataset value - -could be scalar or vector. Nodata values are represented by NaNs. - -.. note:: - - The API is considered EXPERIMENTAL and can be changed without a notice - -.. versionadded:: 3.2 -%End - -%TypeHeaderCode -#include "qgsmeshdataprovider.h" -%End - public: - QgsMeshDatasetValue( double x, - double y ); -%Docstring -Constructor for vector value -%End - - QgsMeshDatasetValue( double scalar ); -%Docstring -Constructor for scalar value -%End - - QgsMeshDatasetValue(); -%Docstring -Default Ctor, initialize to NaN -%End - - ~QgsMeshDatasetValue(); - - void set( double scalar ); -%Docstring -Sets scalar value -%End - - void setX( double x ); -%Docstring -Sets X value -%End - - void setY( double y ); -%Docstring -Sets Y value -%End - - double scalar() const; -%Docstring -Returns magnitude of vector for vector data or scalar value for scalar data -%End - - double x() const; -%Docstring -Returns x value -%End - - double y() const; -%Docstring -Returns y value -%End - - bool operator==( QgsMeshDatasetValue other ) const; - -}; - -class QgsMeshDataBlock -{ -%Docstring - -QgsMeshDataBlock is a block of integers/doubles that can be used -to retrieve: -active flags (e.g. face's active integer flag) -scalars (e.g. scalar dataset double values) -vectors (e.g. vector dataset doubles x,y values) - -data are implicitly shared, so the class can be quickly copied -std.numeric_limits.quiet_NaN() represents NODATA value - -Data can be accessed all at once with values() (faster) or -value by value (slower) with active() or value() - -.. versionadded:: 3.6 -%End - -%TypeHeaderCode -#include "qgsmeshdataprovider.h" -%End - public: - enum DataType - { - ActiveFlagInteger, - ScalarDouble, - Vector2DDouble, - }; - - QgsMeshDataBlock(); -%Docstring -Constructs an invalid block -%End - - QgsMeshDataBlock( DataType type, int count ); -%Docstring -Constructs a new block -%End - - DataType type() const; -%Docstring -Type of data stored in the block -%End - - int count() const; -%Docstring -Number of items stored in the block -%End - - bool isValid() const; -%Docstring -Whether the block is valid -%End - - QgsMeshDatasetValue value( int index ) const; -%Docstring -Returns a value represented by the index -For active flag the behavior is undefined -%End - - bool active( int index ) const; -%Docstring -Returns a value for active flag by the index -For scalar and vector 2d the behavior is undefined -%End - - void setActive( const QVector &vals ); -%Docstring -Sets active flag values. - -If the data provider/datasets does not have active -flag capability (== all values are valid), just -set block validity by setValid( true ) - -:param vals: value vector with size count() - -For scalar and vector 2d the behavior is undefined - -.. versionadded:: 3.12 -%End - - QVector active() const; -%Docstring -Returns active flag array - -Even for active flag valid dataset, the returned array could be empty. -This means that the data provider/dataset does not support active flag -capability, so all faces are active by default. - -For scalar and vector 2d the behavior is undefined - -.. versionadded:: 3.12 -%End - - QVector values() const; -%Docstring -Returns buffer to the array with values -For vector it is pairs (x1, y1, x2, y2, ... ) - -.. versionadded:: 3.12 -%End - - void setValues( const QVector &vals ); -%Docstring -Sets values - -For scalar datasets, it must have size count() -For vector datasets, it must have size 2 * count() -For active flag the behavior is undefined - -.. versionadded:: 3.12 -%End - - void setValid( bool valid ); -%Docstring -Sets block validity -%End - -}; - -class QgsMesh3dDataBlock -{ -%Docstring - -QgsMesh3dDataBlock is a block of 3d stacked mesh data related N -faces defined on base mesh frame. - -data are implicitly shared, so the class can be quickly copied -std.numeric_limits.quiet_NaN() represents NODATA value - -.. note:: - - The API is considered EXPERIMENTAL and can be changed without a notice - -.. versionadded:: 3.12 -%End - -%TypeHeaderCode -#include "qgsmeshdataprovider.h" -%End - public: - QgsMesh3dDataBlock(); -%Docstring -Constructs an invalid block -%End - - ~QgsMesh3dDataBlock(); - - QgsMesh3dDataBlock( int count, bool isVector ); -%Docstring -Constructs a new block for count faces -%End - - void setValid( bool valid ); -%Docstring -Sets block validity -%End - - bool isValid() const; -%Docstring -Whether the block is valid -%End - - bool isVector() const; -%Docstring -Whether we store vector values -%End - - int count() const; -%Docstring -Number of 2d faces for which the volume data is stored in the block -%End - - int firstVolumeIndex() const; -%Docstring -Index of the first volume stored in the buffer (absolute) -%End - - int lastVolumeIndex() const; -%Docstring -Index of the last volume stored in the buffer (absolute) -%End - - int volumesCount() const; -%Docstring -Returns number of volumes stored in the buffer -%End - - QVector verticalLevelsCount() const; -%Docstring -Returns number of vertical level above 2d faces -%End - - void setVerticalLevelsCount( const QVector &verticalLevelsCount ); -%Docstring -Sets the vertical level counts -%End - - QVector verticalLevels() const; -%Docstring -Returns the vertical levels height -%End - - void setVerticalLevels( const QVector &verticalLevels ); -%Docstring -Sets the vertical levels height -%End - - QVector faceToVolumeIndex() const; -%Docstring -Returns the indexing between faces and volumes -%End - - void setFaceToVolumeIndex( const QVector &faceToVolumeIndex ); -%Docstring -Sets the indexing between faces and volumes -%End - - QVector values() const; -%Docstring -Returns the values at volume centers - -For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) -%End - - QgsMeshDatasetValue value( int volumeIndex ) const; -%Docstring -Returns the value at volume centers - -:param volumeIndex: volume index relative to firstVolumeIndex() - -:return: value (scalar or vector) -%End - - void setValues( const QVector &doubleBuffer ); -%Docstring -Sets the values at volume centers - -For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) -%End - -}; - -class QgsMeshDatasetGroupMetadata -{ -%Docstring - -QgsMeshDatasetGroupMetadata is a collection of dataset group metadata -such as whether the data is vector or scalar, name - -.. note:: - - The API is considered EXPERIMENTAL and can be changed without a notice - -.. versionadded:: 3.4 -%End - -%TypeHeaderCode -#include "qgsmeshdataprovider.h" -%End - public: - - enum DataType - { - DataOnFaces, - DataOnVertices, - DataOnVolumes, - DataOnEdges - }; - - QgsMeshDatasetGroupMetadata(); -%Docstring -Constructs an empty metadata object -%End - - QgsMeshDatasetGroupMetadata( const QString &name, - bool isScalar, - DataType dataType, - double minimum, - double maximum, - int maximumVerticalLevels, - const QDateTime &referenceTime, - const QMap &extraOptions ); -%Docstring -Constructs a valid metadata object - -:param name: name of the dataset group -:param isScalar: dataset contains scalar data, specifically the y-value of QgsMeshDatasetValue is NaN -:param dataType: where the data are defined on (vertices, faces or volumes) -:param minimum: minimum value (magnitude for vectors) present among all group's dataset values -:param maximum: maximum value (magnitude for vectors) present among all group's dataset values -:param maximumVerticalLevels: maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes -:param referenceTime: reference time of the dataset group -:param extraOptions: dataset's extra options stored by the provider. Usually contains the name, time value, time units, data file vendor, ... -%End - - QString name() const; -%Docstring -Returns name of the dataset group -%End - - QMap extraOptions() const; -%Docstring -Returns extra metadata options, for example description -%End - - bool isVector() const; -%Docstring -Returns whether dataset group has vector data -%End - - bool isScalar() const; -%Docstring -Returns whether dataset group has scalar data -%End - - DataType dataType() const; -%Docstring -Returns whether dataset group data is defined on vertices or faces or volumes - -.. versionadded:: 3.12 -%End - - double minimum() const; -%Docstring -Returns minimum scalar value/vector magnitude present for whole dataset group -%End - - double maximum() const; -%Docstring -Returns maximum scalar value/vector magnitude present for whole dataset group -%End - - int maximumVerticalLevelsCount() const; -%Docstring -Returns maximum number of vertical levels for 3d stacked meshes - -.. versionadded:: 3.12 -%End - - QDateTime referenceTime() const; -%Docstring -Returns the reference time - -.. versionadded:: 3.12 -%End - -}; - -class QgsMeshDatasetMetadata -{ -%Docstring - -QgsMeshDatasetMetadata is a collection of mesh dataset metadata such -as whether the data is valid or associated time for the dataset - -.. note:: - - The API is considered EXPERIMENTAL and can be changed without a notice - -.. versionadded:: 3.2 -%End - -%TypeHeaderCode -#include "qgsmeshdataprovider.h" -%End - public: - QgsMeshDatasetMetadata(); -%Docstring -Constructs an empty metadata object -%End - - QgsMeshDatasetMetadata( double time, - bool isValid, - double minimum, - double maximum, - int maximumVerticalLevels - ); -%Docstring -Constructs a valid metadata object - -:param time: a time which this dataset represents in the dataset group -:param isValid: dataset is loadad and valid for fetching the data -:param minimum: minimum value (magnitude for vectors) present among dataset values -:param maximum: maximum value (magnitude for vectors) present among dataset values -:param maximumVerticalLevels: maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes -%End - - double time() const; -%Docstring -Returns the time value for this dataset -%End - - bool isValid() const; -%Docstring -Returns whether dataset is valid -%End - - double minimum() const; -%Docstring -Returns minimum scalar value/vector magnitude present for the dataset -%End - - double maximum() const; -%Docstring -Returns maximum scalar value/vector magnitude present for the dataset -%End - - int maximumVerticalLevelsCount() const; -%Docstring -Returns maximum number of vertical levels for 3d stacked meshes - -.. versionadded:: 3.12 -%End - -}; - class QgsMeshDataSourceInterface /Abstract/ { %Docstring @@ -840,6 +314,18 @@ Responsible for reading native mesh data QgsMeshDataProvider( const QString &uri, const QgsDataProvider::ProviderOptions &providerOptions ); %Docstring Ctor +%End + + virtual QgsMeshDataProviderTemporalCapabilities *temporalCapabilities(); + + + void setTemporalUnit( QgsUnitTypes::TemporalUnit unit ); +%Docstring +Sets the temporal unit of the provider and reload data if it changes. + +:param unit: the temporal unit + +.. versionadded:: 3.14 %End signals: @@ -847,6 +333,7 @@ Ctor %Docstring Emitted when some new dataset groups have been added %End + }; /************************************************************************ diff --git a/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in b/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in new file mode 100644 index 000000000000..cf5b5f97b8b1 --- /dev/null +++ b/python/core/auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip.in @@ -0,0 +1,99 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsMeshDataProviderTemporalCapabilities: QgsDataProviderTemporalCapabilities +{ +%Docstring +Class for handling properties relating to a mesh data provider's temporal capabilities. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsmeshdataprovidertemporalcapabilities.h" +%End + public: + + QgsMeshDataProviderTemporalCapabilities(); +%Docstring +Constructor for QgsMeshDataProviderTemporalCapabilities +%End + + QgsMeshDatasetIndex datasetIndexFromRelativeTimeRange( int group, qint64 startTimeSinceGlobalReference, qint64 endTimeSinceGlobalReference ) const; +%Docstring +Returns the first dataset that are include in the range [``startTimeSinceGlobalReference``,``endTimeSinceGlobalReference``[ (in milliseconds) +from the dataset ``group``. If no dataset is present in this range return the last dataset before this range if it not the last one +of whole the dataset group + +Returns invalid dataset index if there is no data set in the range + +.. note:: + + for non temporal dataset group, the range is not used and the unique dataset is returned +%End + + + + + bool hasReferenceTime() const; +%Docstring +Returns whether the reference time is set +%End + + QDateTime referenceTime() const; +%Docstring +Returns the reference time +%End + + QgsDateTimeRange timeExtent() const; +%Docstring +Returns the time extent using the internal reference time +and the first and last times available from the all the dataset +%End + + QgsDateTimeRange timeExtent( const QDateTime &reference ) const; +%Docstring +Returns the time extent using an external ``reference`` date time +and the first and last times available from the all the dataset +%End + + void setTemporalUnit( QgsUnitTypes::TemporalUnit temporalUnit ); +%Docstring +Sets the temporal unit (``temporalUnit``) used to read data by the data provider + +Temporal units supported are milliseconds, seconds, minutes, hors, days and weeks +%End + + QgsUnitTypes::TemporalUnit temporalUnit() const; +%Docstring +Returns the temporal unit used to read data by the data provider +%End + + qint64 datasetTime( const QgsMeshDatasetIndex &index ) const; +%Docstring +Returns the relative time in milliseconds of the dataset +%End + + void clear(); +%Docstring +Clears alls stored reference times and dataset times +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/mesh/qgsmeshdataset.sip.in b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in new file mode 100644 index 000000000000..ac51f0b06724 --- /dev/null +++ b/python/core/auto_generated/mesh/qgsmeshdataset.sip.in @@ -0,0 +1,558 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/mesh/qgsmeshdataset.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + + +class QgsMeshDatasetIndex +{ +%Docstring + +QgsMeshDatasetIndex is index that identifies the dataset group (e.g. wind speed) +and a dataset in this group (e.g. magnitude of wind speed in particular time) + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.4 +%End + +%TypeHeaderCode +#include "qgsmeshdataset.h" +%End + public: + QgsMeshDatasetIndex( int group = -1, int dataset = -1 ); +%Docstring +Creates an index. -1 represents invalid group/dataset +%End + int group() const; +%Docstring +Returns a group index +%End + int dataset() const; +%Docstring +Returns a dataset index within group() +%End + bool isValid() const; +%Docstring +Returns whether index is valid, ie at least groups is set +%End + bool operator == ( QgsMeshDatasetIndex other ) const; + bool operator != ( QgsMeshDatasetIndex other ) const; +}; + +class QgsMeshDatasetValue +{ +%Docstring + +QgsMeshDatasetValue represents single dataset value + +could be scalar or vector. Nodata values are represented by NaNs. + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.2 +%End + +%TypeHeaderCode +#include "qgsmeshdataset.h" +%End + public: + QgsMeshDatasetValue( double x, + double y ); +%Docstring +Constructor for vector value +%End + + QgsMeshDatasetValue( double scalar ); +%Docstring +Constructor for scalar value +%End + + QgsMeshDatasetValue(); +%Docstring +Default Ctor, initialize to NaN +%End + + ~QgsMeshDatasetValue(); + + void set( double scalar ); +%Docstring +Sets scalar value +%End + + void setX( double x ); +%Docstring +Sets X value +%End + + void setY( double y ); +%Docstring +Sets Y value +%End + + double scalar() const; +%Docstring +Returns magnitude of vector for vector data or scalar value for scalar data +%End + + double x() const; +%Docstring +Returns x value +%End + + double y() const; +%Docstring +Returns y value +%End + + bool operator==( QgsMeshDatasetValue other ) const; + +}; + +class QgsMeshDataBlock +{ +%Docstring + +QgsMeshDataBlock is a block of integers/doubles that can be used +to retrieve: +active flags (e.g. face's active integer flag) +scalars (e.g. scalar dataset double values) +vectors (e.g. vector dataset doubles x,y values) + +data are implicitly shared, so the class can be quickly copied +std.numeric_limits.quiet_NaN() represents NODATA value + +Data can be accessed all at once with values() (faster) or +value by value (slower) with active() or value() + +.. versionadded:: 3.6 +%End + +%TypeHeaderCode +#include "qgsmeshdataset.h" +%End + public: + enum DataType + { + ActiveFlagInteger, + ScalarDouble, + Vector2DDouble, + }; + + QgsMeshDataBlock(); +%Docstring +Constructs an invalid block +%End + + QgsMeshDataBlock( DataType type, int count ); +%Docstring +Constructs a new block +%End + + DataType type() const; +%Docstring +Type of data stored in the block +%End + + int count() const; +%Docstring +Number of items stored in the block +%End + + bool isValid() const; +%Docstring +Whether the block is valid +%End + + QgsMeshDatasetValue value( int index ) const; +%Docstring +Returns a value represented by the index +For active flag the behavior is undefined +%End + + bool active( int index ) const; +%Docstring +Returns a value for active flag by the index +For scalar and vector 2d the behavior is undefined +%End + + void setActive( const QVector &vals ); +%Docstring +Sets active flag values. + +If the data provider/datasets does not have active +flag capability (== all values are valid), just +set block validity by setValid( true ) + +:param vals: value vector with size count() + +For scalar and vector 2d the behavior is undefined + +.. versionadded:: 3.12 +%End + + QVector active() const; +%Docstring +Returns active flag array + +Even for active flag valid dataset, the returned array could be empty. +This means that the data provider/dataset does not support active flag +capability, so all faces are active by default. + +For scalar and vector 2d the behavior is undefined + +.. versionadded:: 3.12 +%End + + QVector values() const; +%Docstring +Returns buffer to the array with values +For vector it is pairs (x1, y1, x2, y2, ... ) + +.. versionadded:: 3.12 +%End + + void setValues( const QVector &vals ); +%Docstring +Sets values + +For scalar datasets, it must have size count() +For vector datasets, it must have size 2 * count() +For active flag the behavior is undefined + +.. versionadded:: 3.12 +%End + + void setValid( bool valid ); +%Docstring +Sets block validity +%End + +}; + +class QgsMesh3dDataBlock +{ +%Docstring + +QgsMesh3dDataBlock is a block of 3d stacked mesh data related N +faces defined on base mesh frame. + +data are implicitly shared, so the class can be quickly copied +std.numeric_limits.quiet_NaN() represents NODATA value + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.12 +%End + +%TypeHeaderCode +#include "qgsmeshdataset.h" +%End + public: + QgsMesh3dDataBlock(); +%Docstring +Constructs an invalid block +%End + + ~QgsMesh3dDataBlock(); + + QgsMesh3dDataBlock( int count, bool isVector ); +%Docstring +Constructs a new block for count faces +%End + + void setValid( bool valid ); +%Docstring +Sets block validity +%End + + bool isValid() const; +%Docstring +Whether the block is valid +%End + + bool isVector() const; +%Docstring +Whether we store vector values +%End + + int count() const; +%Docstring +Number of 2d faces for which the volume data is stored in the block +%End + + int firstVolumeIndex() const; +%Docstring +Index of the first volume stored in the buffer (absolute) +%End + + int lastVolumeIndex() const; +%Docstring +Index of the last volume stored in the buffer (absolute) +%End + + int volumesCount() const; +%Docstring +Returns number of volumes stored in the buffer +%End + + QVector verticalLevelsCount() const; +%Docstring +Returns number of vertical level above 2d faces +%End + + void setVerticalLevelsCount( const QVector &verticalLevelsCount ); +%Docstring +Sets the vertical level counts +%End + + QVector verticalLevels() const; +%Docstring +Returns the vertical levels height +%End + + void setVerticalLevels( const QVector &verticalLevels ); +%Docstring +Sets the vertical levels height +%End + + QVector faceToVolumeIndex() const; +%Docstring +Returns the indexing between faces and volumes +%End + + void setFaceToVolumeIndex( const QVector &faceToVolumeIndex ); +%Docstring +Sets the indexing between faces and volumes +%End + + QVector values() const; +%Docstring +Returns the values at volume centers + +For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) +%End + + QgsMeshDatasetValue value( int volumeIndex ) const; +%Docstring +Returns the value at volume centers + +:param volumeIndex: volume index relative to firstVolumeIndex() + +:return: value (scalar or vector) +%End + + void setValues( const QVector &doubleBuffer ); +%Docstring +Sets the values at volume centers + +For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) +%End + +}; + +class QgsMeshDatasetGroupMetadata +{ +%Docstring + +QgsMeshDatasetGroupMetadata is a collection of dataset group metadata +such as whether the data is vector or scalar, name + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.4 +%End + +%TypeHeaderCode +#include "qgsmeshdataset.h" +%End + public: + + enum DataType + { + DataOnFaces, + DataOnVertices, + DataOnVolumes, + DataOnEdges + }; + + QgsMeshDatasetGroupMetadata(); +%Docstring +Constructs an empty metadata object +%End + + QgsMeshDatasetGroupMetadata( const QString &name, + bool isScalar, + DataType dataType, + double minimum, + double maximum, + int maximumVerticalLevels, + const QDateTime &referenceTime, + bool isTemporal, + const QMap &extraOptions ); +%Docstring +Constructs a valid metadata object + +:param name: name of the dataset group +:param isScalar: dataset contains scalar data, specifically the y-value of QgsMeshDatasetValue is NaN +:param dataType: where the data are defined on (vertices, faces or volumes) +:param minimum: minimum value (magnitude for vectors) present among all group's dataset values +:param maximum: maximum value (magnitude for vectors) present among all group's dataset values +:param maximumVerticalLevels: maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes +:param referenceTime: reference time of the dataset group +:param isTemporal: weither the dataset group is temporal (contains time-related dataset) +:param extraOptions: dataset's extra options stored by the provider. Usually contains the name, time value, time units, data file vendor, ... +%End + + QString name() const; +%Docstring +Returns name of the dataset group +%End + + QMap extraOptions() const; +%Docstring +Returns extra metadata options, for example description +%End + + bool isVector() const; +%Docstring +Returns whether dataset group has vector data +%End + + bool isScalar() const; +%Docstring +Returns whether dataset group has scalar data +%End + + + bool isTemporal() const; +%Docstring +Returns whether the dataset group is temporal (contains time-related dataset) +%End + + + DataType dataType() const; +%Docstring +Returns whether dataset group data is defined on vertices or faces or volumes + +.. versionadded:: 3.12 +%End + + double minimum() const; +%Docstring +Returns minimum scalar value/vector magnitude present for whole dataset group +%End + + double maximum() const; +%Docstring +Returns maximum scalar value/vector magnitude present for whole dataset group +%End + + int maximumVerticalLevelsCount() const; +%Docstring +Returns maximum number of vertical levels for 3d stacked meshes + +.. versionadded:: 3.12 +%End + + QDateTime referenceTime() const; +%Docstring +Returns the reference time + +.. versionadded:: 3.12 +%End + +}; + +class QgsMeshDatasetMetadata +{ +%Docstring + +QgsMeshDatasetMetadata is a collection of mesh dataset metadata such +as whether the data is valid or associated time for the dataset + +.. note:: + + The API is considered EXPERIMENTAL and can be changed without a notice + +.. versionadded:: 3.2 +%End + +%TypeHeaderCode +#include "qgsmeshdataset.h" +%End + public: + QgsMeshDatasetMetadata(); +%Docstring +Constructs an empty metadata object +%End + + QgsMeshDatasetMetadata( double time, + bool isValid, + double minimum, + double maximum, + int maximumVerticalLevels + ); +%Docstring +Constructs a valid metadata object + +:param time: a time which this dataset represents in the dataset group +:param isValid: dataset is loadad and valid for fetching the data +:param minimum: minimum value (magnitude for vectors) present among dataset values +:param maximum: maximum value (magnitude for vectors) present among dataset values +:param maximumVerticalLevels: maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes +%End + + double time() const; +%Docstring +Returns the time value for this dataset +%End + + bool isValid() const; +%Docstring +Returns whether dataset is valid +%End + + double minimum() const; +%Docstring +Returns minimum scalar value/vector magnitude present for the dataset +%End + + double maximum() const; +%Docstring +Returns maximum scalar value/vector magnitude present for the dataset +%End + + int maximumVerticalLevelsCount() const; +%Docstring +Returns maximum number of vertical levels for 3d stacked meshes + +.. versionadded:: 3.12 +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/mesh/qgsmeshdataset.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in index 0dbb1b54cacd..22f0b94f61d8 100644 --- a/python/core/auto_generated/mesh/qgsmeshlayer.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshlayer.sip.in @@ -46,11 +46,11 @@ E.g. to create mesh with one quad and one triangle "3.0, 2.0 \n" \ "2.0, 3.0 \n" \ "1.0, 3.0 \n" \ - "---" + "---" \ "0, 1, 3, 4 \n" \ "1, 2, 3 \n" ); - QgsMeshLayer *scratchLayer = new QgsMeshLayer(uri, "My Scratch layer", "memory_mesh"); + QgsMeshLayer *scratchLayer = new QgsMeshLayer(uri, "My Scratch layer", "mesh_memory"); \subsection mdal MDAL data provider (mdal) @@ -126,6 +126,10 @@ QgsMeshLayer cannot be copied. QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories = QgsMapLayer::AllStyleCategories ); virtual bool writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories = QgsMapLayer::AllStyleCategories ) const; + virtual bool writeStyle( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ) const; + + virtual bool readStyle( const QDomNode &node, QString &errorMessage, QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ); + virtual QString encodedSource( const QString &source, const QgsReadWriteContext &context ) const; virtual QString decodedSource( const QString &source, const QString &provider, const QgsReadWriteContext &context ) const; @@ -134,6 +138,8 @@ QgsMeshLayer cannot be copied. virtual bool writeXml( QDomNode &layer_node, QDomDocument &doc, const QgsReadWriteContext &context ) const; + virtual QgsMeshLayerTemporalProperties *temporalProperties(); + virtual void reload(); @@ -230,6 +236,55 @@ Returns the 3d values of stacked 3d mesh defined by the given point .. versionadded:: 3.12 +%End + + QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const; +%Docstring +Returns dataset index from active scalar group depending on the time range. +If the temporal properties is not active, return the static dataset + +:param timeRange: the time range + +:return: dataset index + +.. versionadded:: 3.14 +%End + + QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const; +%Docstring +Returns dataset index from active vector group depending on the time range +If the temporal properties is not active, return the static dataset + +:param timeRange: the time range + +:return: dataset index + +.. versionadded:: 3.14 +%End + + + + QgsMeshDatasetIndex staticScalarDatasetIndex() const; +%Docstring +Returns the static scalar dataset index that is rendered if the temporal properties is not active + +.. versionadded:: 3.14 +%End + + QgsMeshDatasetIndex staticVectorDatasetIndex() const; +%Docstring +Returns the static vector dataset index that is rendered if the temporal properties is not active + +.. versionadded:: 3.14 +%End + + void setReferenceTime( const QDateTime &referenceTime ); +%Docstring +Sets the reference time of the layer + +:param referenceTime: the reference time + +.. versionadded:: 3.14 %End public slots: @@ -244,18 +299,18 @@ Sets the coordinate transform context to ``transformContext``. signals: - void activeScalarDatasetChanged( const QgsMeshDatasetIndex &index ); + void activeScalarDatasetGroupChanged( int index ); %Docstring -Emitted when active scalar dataset is changed +Emitted when active scalar group dataset is changed -.. versionadded:: 3.4 +.. versionadded:: 3.14 %End - void activeVectorDatasetChanged( const QgsMeshDatasetIndex &index ); + void activeVectorDatasetGroupChanged( int index ); %Docstring -Emitted when active vector dataset is changed +Emitted when active vector group dataset is changed -.. versionadded:: 3.4 +.. versionadded:: 3.14 %End void timeSettingsChanged( ); diff --git a/python/core/auto_generated/mesh/qgsmeshlayertemporalproperties.sip.in b/python/core/auto_generated/mesh/qgsmeshlayertemporalproperties.sip.in new file mode 100644 index 000000000000..4c1738b27d67 --- /dev/null +++ b/python/core/auto_generated/mesh/qgsmeshlayertemporalproperties.sip.in @@ -0,0 +1,84 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/mesh/qgsmeshlayertemporalproperties.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsMeshLayerTemporalProperties : QgsMapLayerTemporalProperties +{ +%Docstring +Implementation of map layer temporal properties for mesh layers. + +The time in a mesh layer is defined by : +- a reference time provided by the data, the project or the user +- each dataset is associated with a relative times +- time extent is defined by the first time and the last time of all dataset + +Reference time : AT +Dataset 1 time o-----RT------RT-----RT-----------RT +Dataset 2 time o---------RT------RT--------RT +Dataset 3 time o------------------------------RT-------RT----------RT +Time extent of layer o-----<---------------------------------------------> + +AT : absolute time (QDateTime) +RT : relative time (qint64) + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsmeshlayertemporalproperties.h" +%End + public: + + QgsMeshLayerTemporalProperties( QObject *parent /TransferThis/ = 0, bool enabled = true ); +%Docstring +Constructor for QgsMeshLayerTemporalProperties + +:param parent: pointer to the parent object +:param enabled: argument specifies whether the temporal properties are initially enabled or not (see isActive()). +%End + + public: + virtual QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ); + + virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void setDefaultsFromDataProviderTemporalCapabilities( const QgsDataProviderTemporalCapabilities *capabilities ); + + + QgsDateTimeRange timeExtent() const; +%Docstring +Returns the time extent +%End + + QDateTime referenceTime() const; +%Docstring +Returns the reference time +%End + + void setReferenceTime( const QDateTime &referenceTime, const QgsDataProviderTemporalCapabilities *capabilities ); +%Docstring +Sets the reference time and update the time extent from the temporal capabilities, +if the temporal capabilities is null, set a void time extent (reference time to reference time) + +:param referenceTime: the reference time +:param capabilities: the temporal capabilities of the data provider +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/mesh/qgsmeshlayertemporalproperties.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in b/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in index 8ec828b11754..3abbc26ef64e 100644 --- a/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshrenderersettings.sip.in @@ -96,7 +96,8 @@ Represents a mesh renderer settings for scalar datasets #include "qgsmeshrenderersettings.h" %End public: - enum DataInterpolationMethod + + enum DataResamplingMethod { None, @@ -135,7 +136,7 @@ Returns opacity Sets opacity %End - DataInterpolationMethod dataInterpolationMethod() const; + DataResamplingMethod dataResamplingMethod() const; %Docstring Returns the type of interpolation to use to convert face defined datasets to @@ -144,7 +145,7 @@ values on vertices .. versionadded:: 3.12 %End - void setDataInterpolationMethod( const DataInterpolationMethod &dataInterpolationMethod ); + void setDataResamplingMethod( const DataResamplingMethod &dataResamplingMethod ); %Docstring Sets data interpolation method @@ -334,7 +335,7 @@ Represents a streamline renderer settings for vector datasets displayed by strea MeshGridded, - Random, + Random }; SeedingStartPointsMethod seedingMethod() const; @@ -448,6 +449,14 @@ Represents a renderer settings for vector datasets Traces }; + enum ColoringMethod + { + //! Render the vector with a single color + SingleColor, + //! Render the vector with a color ramp + ColorRamp + }; + double lineWidth() const; %Docstring @@ -534,6 +543,34 @@ Returns the displaying method used to render vector datasets Sets the displaying method used to render vector datasets .. versionadded:: 3.12 +%End + + ColoringMethod coloringMethod() const; +%Docstring +Returns the coloring method used to render vector datasets + +.. versionadded:: 3.14 +%End + + void setColoringMethod( const ColoringMethod &coloringMethod ); +%Docstring +Sets the coloring method used to render vector datasets + +.. versionadded:: 3.14 +%End + + QgsColorRampShader colorRampShader() const; +%Docstring +Sets the color ramp shader used to render vector datasets + +.. versionadded:: 3.14 +%End + + void setColorRampShader( const QgsColorRampShader &colorRampShader ); +%Docstring +Returns the color ramp shader used to render vector datasets + +.. versionadded:: 3.14 %End QgsMeshRendererVectorArrowSettings arrowSettings() const; @@ -587,7 +624,6 @@ Writes configuration to a new DOM element Reads configuration from the given DOM element %End - }; class QgsMeshRendererSettings @@ -678,31 +714,41 @@ Sets averaging method for conversion of 3d stacked mesh data to 2d data Ownership of the method is not transferred. %End - QgsMeshDatasetIndex activeScalarDataset() const; + QDomElement writeXml( QDomDocument &doc ) const; %Docstring -Returns active scalar dataset +Writes configuration to a new DOM element %End - void setActiveScalarDataset( QgsMeshDatasetIndex index = QgsMeshDatasetIndex() ); + void readXml( const QDomElement &elem ); %Docstring -Sets active scalar dataset for rendering +Reads configuration from the given DOM element %End - QgsMeshDatasetIndex activeVectorDataset() const; + int activeScalarDatasetGroup() const; %Docstring -Returns active vector dataset +Returns the active scalar dataset group + +.. versionadded:: 3.14 %End - void setActiveVectorDataset( QgsMeshDatasetIndex index = QgsMeshDatasetIndex() ); + + void setActiveScalarDatasetGroup( int activeScalarDatasetGroup ); %Docstring -Sets active vector dataset for rendering. +Sets the active scalar dataset group + +.. versionadded:: 3.14 %End - QDomElement writeXml( QDomDocument &doc ) const; + int activeVectorDatasetGroup() const; %Docstring -Writes configuration to a new DOM element +Returns the active vector dataset group + +.. versionadded:: 3.14 %End - void readXml( const QDomElement &elem ); + + void setActiveVectorDatasetGroup( int activeVectorDatasetGroup ); %Docstring -Reads configuration from the given DOM element +Sets the active vector dataset group + +.. versionadded:: 3.14 %End }; diff --git a/python/core/auto_generated/mesh/qgsmeshtimesettings.sip.in b/python/core/auto_generated/mesh/qgsmeshtimesettings.sip.in index 90061ffa0f66..2ef65eb5dd73 100644 --- a/python/core/auto_generated/mesh/qgsmeshtimesettings.sip.in +++ b/python/core/auto_generated/mesh/qgsmeshtimesettings.sip.in @@ -37,14 +37,6 @@ Represents a mesh time settings for mesh datasets }; QgsMeshTimeSettings(); - QgsMeshTimeSettings( double relativeTimeOffsetHours, const QString &relativeTimeFormat ); -%Docstring -Constructs relative time format settings with defined offset in hours -%End - QgsMeshTimeSettings( const QDateTime &absoluteTimeReferenceTime, const QString &absoluteTimeFormat ); -%Docstring -Constructs absolute time format settings with defined reference time -%End QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const; %Docstring @@ -53,24 +45,6 @@ Writes configuration to a new DOM element void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); %Docstring Reads configuration from the given DOM element -%End - - bool useAbsoluteTime() const; -%Docstring -Returns whether to use absolute time format -%End - void setUseAbsoluteTime( bool useAbsoluteTime ); -%Docstring -Sets use absolute time flag -%End - - double relativeTimeOffsetHours() const; -%Docstring -Returns number of offset hours for relative time formatting -%End - void setRelativeTimeOffsetHours( double relativeTimeOffsetHours ); -%Docstring -Sets number of offset hours for relative time formatting %End QString relativeTimeFormat() const; @@ -80,15 +54,6 @@ Returns format used for relative time void setRelativeTimeFormat( const QString &relativeTimeFormat ); %Docstring Sets format used for relative time -%End - - QDateTime absoluteTimeReferenceTime() const; -%Docstring -Returns reference time used for absolute time format -%End - void setAbsoluteTimeReferenceTime( const QDateTime &absoluteTimeReferenceTime ); -%Docstring -Sets reference time used for absolute time format %End QString absoluteTimeFormat() const; @@ -98,20 +63,6 @@ Returns format used for absolute time void setAbsoluteTimeFormat( const QString &absoluteTimeFormat ); %Docstring Sets format used for absolute time -%End - - TimeUnit providerTimeUnit() const; -%Docstring -Returns the provider time unit - -.. versionadded:: 3.12 -%End - - void setProviderTimeUnit( const TimeUnit &providerTimeUnit ); -%Docstring -Sets the provider time unit - -.. versionadded:: 3.12 %End }; diff --git a/python/core/auto_generated/numericformats/qgsfractionnumericformat.sip.in b/python/core/auto_generated/numericformats/qgsfractionnumericformat.sip.in new file mode 100644 index 000000000000..7c9b6ed4d923 --- /dev/null +++ b/python/core/auto_generated/numericformats/qgsfractionnumericformat.sip.in @@ -0,0 +1,172 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/numericformats/qgsfractionnumericformat.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + +class QgsFractionNumericFormat : QgsNumericFormat +{ +%Docstring +A numeric formatter which returns a vulgar fractional representation of a decimal value (e.g. "1/2" instead of 0.5). + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsfractionnumericformat.h" +%End + public: + + QgsFractionNumericFormat(); +%Docstring +Default constructor +%End + + virtual QString id() const; + + virtual QString visibleName() const; + + virtual int sortKey(); + + virtual QString formatDouble( double value, const QgsNumericFormatContext &context ) const; + + virtual QgsNumericFormat *clone() const /Factory/; + + virtual QgsNumericFormat *create( const QVariantMap &configuration, const QgsReadWriteContext &context ) const /Factory/; + + virtual QVariantMap configuration( const QgsReadWriteContext &context ) const; + + virtual double suggestSampleValue() const; + + + bool useDedicatedUnicodeCharacters() const; +%Docstring +Returns ``True`` if dedicated unicode characters should be used, when the are available for the +particular fraction (e.g. ½, ¼). + +.. seealso:: :py:func:`setUseDedicatedUnicodeCharacters` + +.. seealso:: :py:func:`useUnicodeSuperSubscript` +%End + + void setUseDedicatedUnicodeCharacters( bool enabled ); +%Docstring +Sets whether dedicated unicode characters should be used, when the are available for the +particular fraction (e.g. ½, ¼). + +.. seealso:: :py:func:`useDedicatedUnicodeCharacters` + +.. seealso:: :py:func:`setUseUnicodeSuperSubscript` +%End + + bool useUnicodeSuperSubscript() const; +%Docstring +Returns ``True`` if unicode superscript and subscript characters should be used, (e.g. "⁶/₇"). + +.. seealso:: :py:func:`setUseUnicodeSuperSubscript` + +.. seealso:: :py:func:`useDedicatedUnicodeCharacters` +%End + + void setUseUnicodeSuperSubscript( bool enabled ); +%Docstring +Sets whether unicode superscript and subscript characters should be used, (e.g. "⁶/₇"). + +.. seealso:: :py:func:`useUnicodeSuperSubscript` + +.. seealso:: :py:func:`setUseDedicatedUnicodeCharacters` +%End + + bool showThousandsSeparator() const; +%Docstring +Returns ``True`` if the thousands grouping separator will be shown. + +.. seealso:: :py:func:`setShowThousandsSeparator` +%End + + void setShowThousandsSeparator( bool show ); +%Docstring +Sets whether the thousands grouping separator will be shown. + +.. seealso:: :py:func:`showThousandsSeparator` +%End + + bool showPlusSign() const; +%Docstring +Returns ``True`` if a leading plus sign will be shown for positive values. + +.. seealso:: :py:func:`setShowPlusSign` +%End + + void setShowPlusSign( bool show ); +%Docstring +Sets whether a leading plus sign will be shown for positive values. + +.. seealso:: :py:func:`showPlusSign` +%End + + QChar thousandsSeparator() const; +%Docstring +Returns any override for the thousands separator character. If an invalid QChar is returned, +then the QGIS locale separator is used instead. + +.. seealso:: :py:func:`setThousandsSeparator` +%End + + void setThousandsSeparator( QChar character ); +%Docstring +Sets an override ``character`` for the thousands separator character. If an invalid QChar is set, +then the QGIS locale separator is used instead. + +.. seealso:: :py:func:`thousandsSeparator` +%End + + static bool doubleToVulgarFraction( const double value, unsigned long long &numerator /Out/, unsigned long long &denominator /Out/, int &sign /Out/, const double tolerance = 1e-10 ); +%Docstring +Converts a double ``value`` to a vulgar fraction (e.g. ⅓, ¼, etc) by attempting to calculate +the corresponding ``numerator`` and ``denominator``, within the specified ``tolerance``. + +This method is based of Richard's algorithm (1981) from "Continued Fractions without Tears" (University of Minnesota). + +:param value: input value to convert +:param denominator: will be set to the calculated fraction denominator +:param sign: will be set to the sign of the result (as -1 or +1 values) +:param tolerance: acceptable tolerance. Larger values will give "nicer" fractions. + +:return: - ``True`` if ``value`` was successfully converted to a fraction + - numerator: will be set to calculated fraction numerator +%End + + static QString toUnicodeSuperscript( const QString &input ); +%Docstring +Converts numbers in an ``input`` string to unicode superscript equivalents. + +.. seealso:: :py:func:`toUnicodeSubscript` +%End + + static QString toUnicodeSubscript( const QString &input ); +%Docstring +Converts numbers in an ``input`` string to unicode subscript equivalents. + +.. seealso:: :py:func:`toUnicodeSuperscript` +%End + + protected: + + virtual void setConfiguration( const QVariantMap &configuration, const QgsReadWriteContext &context ); +%Docstring +Sets the format's ``configuration``. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/numericformats/qgsfractionnumericformat.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/numericformats/qgsnumericformat.sip.in b/python/core/auto_generated/numericformats/qgsnumericformat.sip.in index b1792b5fd79e..fc1528b6ac9c 100644 --- a/python/core/auto_generated/numericformats/qgsnumericformat.sip.in +++ b/python/core/auto_generated/numericformats/qgsnumericformat.sip.in @@ -168,6 +168,8 @@ This is an abstract base class and will always need to be subclassed. sipType = sipType_QgsCurrencyNumericFormat; else if ( dynamic_cast< QgsBasicNumericFormat * >( sipCpp ) ) sipType = sipType_QgsBasicNumericFormat; + else if ( dynamic_cast< QgsFractionNumericFormat * >( sipCpp ) ) + sipType = sipType_QgsFractionNumericFormat; else sipType = NULL; %End diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelalgorithm.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelalgorithm.sip.in index fead741a03cc..d34891dc92eb 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelalgorithm.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelalgorithm.sip.in @@ -281,6 +281,37 @@ this name a new component will be added to the model and returned. Updates the model's parameter definitions to include all relevant destination parameters as required by child algorithm ModelOutputs. Must be called whenever child algorithm ModelOutputs are altered. +%End + + void addGroupBox( const QgsProcessingModelGroupBox &groupBox ); +%Docstring +Adds a new group ``box`` to the model. + +If an existing group box with the same uuid already exists then its definition will be replaced. + +.. seealso:: :py:func:`groupBoxes` + +.. versionadded:: 3.14 +%End + + QList< QgsProcessingModelGroupBox > groupBoxes() const; +%Docstring +Returns a list of the group boxes within the model. + +.. seealso:: :py:func:`addGroupBox` + +.. versionadded:: 3.14 +%End + + void removeGroupBox( const QString &uuid ); +%Docstring +Removes the group box with matching ``uuid`` from the model. + +.. seealso:: :py:func:`addGroupBox` + +.. seealso:: :py:func:`groupBoxes` + +.. versionadded:: 3.14 %End bool toFile( const QString &path ) const; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip.in index dc01d7dc98cf..13d030136118 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip.in @@ -30,6 +30,7 @@ Source for the value of a parameter for a child algorithm within a model. StaticValue, Expression, ExpressionText, + ModelOutput, }; QgsProcessingModelChildParameterSource(); @@ -120,6 +121,13 @@ algorithms already executed by the model. Source source() const; %Docstring Returns the parameter value's source. +%End + + void setSource( Source source ); +%Docstring +Sets the parameter's source. + +.. versionadded:: 3.14 %End QVariant staticValue() const; @@ -245,6 +253,13 @@ Loads this source from a QVariantMap. Attempts to convert the source to executable Python code. The ``friendlyChildNames`` argument gives a map of child id to a friendly algorithm name, to be used in the code to identify that algorithm instead of the raw child id. +%End + + QString friendlyIdentifier( QgsProcessingModelAlgorithm *model ) const; +%Docstring +Returns a user-friendly identifier for this source, given the context of the specified ``model``. + +.. versionadded:: 3.14 %End }; diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in index d395352fe3c6..0de7ed00de7a 100644 --- a/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelcomponent.sip.in @@ -69,6 +69,27 @@ Sets the ``size`` of the model component within the graphical modeler. .. seealso:: :py:func:`size` +.. versionadded:: 3.14 +%End + + QColor color() const; +%Docstring +Returns the color of the model component within the graphical modeler. + +An invalid color indicates that the default color for the component should be used. + +.. seealso:: :py:func:`setColor` + +.. versionadded:: 3.14 +%End + + void setColor( const QColor &color ); +%Docstring +Sets the ``color`` of the model component within the graphical modeler. An invalid ``color`` +indicates that the default color for the component should be used. + +.. seealso:: :py:func:`color` + .. versionadded:: 3.14 %End diff --git a/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in b/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in new file mode 100644 index 000000000000..fe9449389cad --- /dev/null +++ b/python/core/auto_generated/processing/models/qgsprocessingmodelgroupbox.sip.in @@ -0,0 +1,62 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelgroupbox.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsProcessingModelGroupBox : QgsProcessingModelComponent +{ +%Docstring +Represents a group box in a model. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingmodelgroupbox.h" +%End + public: + + QgsProcessingModelGroupBox( const QString &description = QString() ); +%Docstring +Constructor for QgsProcessingModelGroupBox with the specified ``description``. +%End + + virtual QgsProcessingModelGroupBox *clone() const /Factory/; + + + QVariant toVariant() const; +%Docstring +Saves this group box to a QVariant. + +.. seealso:: :py:func:`loadVariant` +%End + + bool loadVariant( const QVariantMap &map ); +%Docstring +Loads this group box from a QVariantMap. + +.. seealso:: :py:func:`toVariant` +%End + + QString uuid() const; +%Docstring +Returns the unique ID associated with this group box. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/processing/models/qgsprocessingmodelgroupbox.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in b/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in index a5223e116689..482e2692f661 100644 --- a/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingalgorithm.sip.in @@ -47,6 +47,8 @@ Abstract base class for processing algorithms. FlagSupportsInPlaceEdits, FlagKnownIssues, FlagCustomException, + FlagPruneModelBranchesBasedOnAlgorithmResults, + FlagSkipGenericModelLogging, FlagDeprecated, }; typedef QFlags Flags; @@ -294,6 +296,43 @@ manner. bool hasHtmlOutputs() const; %Docstring Returns ``True`` if this algorithm generates HTML outputs. +%End + + enum PropertyAvailability + { + NotAvailable, + Available, + }; + + struct VectorProperties + { + QgsFields fields; + + QgsWkbTypes::Type wkbType; + + QgsCoordinateReferenceSystem crs; + + QgsProcessingAlgorithm::PropertyAvailability availability; + }; + + virtual QgsProcessingAlgorithm::VectorProperties sinkProperties( const QString &sink, + const QVariantMap ¶meters, + QgsProcessingContext &context, + const QMap< QString, QgsProcessingAlgorithm::VectorProperties > &sourceProperties ) const; +%Docstring +Returns the vector properties which will be used for the ``sink`` with matching name. + +The ``parameters`` argument specifies the values of all parameters which would be used to generate +the sink. These can be used alongside the provided ``context`` in order to pre-evaluate inputs +when required in order to determine the sink's properties. + +The ``sourceProperties`` map will contain the vector properties of the various sources used +as inputs to the algorithm. These will only be available in certain circumstances (e.g. when the +algorithm is used within a model), so implementations will need to be adaptable to circumstances +when either ``sourceParameters`` is empty or ``parameters`` is empty, and use whatever information +is passed in order to make a best guess determination of the output properties. + +.. versionadded:: 3.14 %End QVariantMap run( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool *ok /Out/ = 0, const QVariantMap &configuration = QVariantMap() ) const; @@ -623,7 +662,7 @@ Evaluates the parameter with matching ``name`` to a static boolean value. %End QgsFeatureSink *parameterAsSink( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context, QString &destinationIdentifier /Out/, - const QgsFields &fields, QgsWkbTypes::Type geometryType = QgsWkbTypes::NoGeometry, const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem(), QgsFeatureSink::SinkFlags sinkFlags = 0 ) const /Factory/; + const QgsFields &fields, QgsWkbTypes::Type geometryType = QgsWkbTypes::NoGeometry, const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem(), QgsFeatureSink::SinkFlags sinkFlags = 0 ) const throw( QgsProcessingException ) /Factory/; %Docstring Evaluates the parameter with matching ``name`` to a feature sink. @@ -637,6 +676,8 @@ The ``destinationIdentifier`` argument will be set to a string which can be used to the sink, e.g. via calling :py:func:`QgsProcessingUtils.mapLayerFromString()` This function creates a new object and the caller takes responsibility for deleting the returned object. + +:raises :: py:class:`QgsProcessingException` %End QgsProcessingFeatureSource *parameterAsSource( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ) const /Factory/; @@ -872,6 +913,20 @@ Evaluates the parameter with matching ``name`` to a color, or returns an invalid %Docstring Evaluates the parameter with matching ``name`` to a connection name string. +.. versionadded:: 3.14 +%End + + QString parameterAsSchema( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ); +%Docstring +Evaluates the parameter with matching ``name`` to a database schema name string. + +.. versionadded:: 3.14 +%End + + QString parameterAsDatabaseTableName( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ); +%Docstring +Evaluates the parameter with matching ``name`` to a database table name string. + .. versionadded:: 3.14 %End @@ -1136,6 +1191,11 @@ Read the source from ``parameters`` and ``context`` and set it .. versionadded:: 3.4 %End + virtual QgsProcessingAlgorithm::VectorProperties sinkProperties( const QString &sink, + const QVariantMap ¶meters, + QgsProcessingContext &context, + const QMap< QString, QgsProcessingAlgorithm::VectorProperties > &sourceProperties ) const; + }; diff --git a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in index 34e0be8fa1b4..154f0598cb04 100644 --- a/python/core/auto_generated/processing/qgsprocessingcontext.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingcontext.sip.in @@ -290,6 +290,7 @@ called using the feature with invalid geometry as a parameter. %End + void setTransformErrorCallback( SIP_PYCALLABLE / AllowNone / ); %Docstring Sets a callback function to use when encountering a transform error when iterating diff --git a/python/core/auto_generated/processing/qgsprocessingfeedback.sip.in b/python/core/auto_generated/processing/qgsprocessingfeedback.sip.in index 1482de49d6a4..4da88e0d2db1 100644 --- a/python/core/auto_generated/processing/qgsprocessingfeedback.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingfeedback.sip.in @@ -27,6 +27,14 @@ it to users via the GUI. %End public: + QgsProcessingFeedback( bool logFeedback = true ); +%Docstring +Constructor for QgsProcessingFeedback. + +If ``logFeedback`` is ``True``, then all feedback received will be directed +to :py:class:`QgsMessageLog`. +%End + virtual void setProgressText( const QString &text ); %Docstring Sets a progress report text string. This can be used in conjunction with @@ -98,6 +106,24 @@ report the output from executing an external command or subprocess. Pushes a summary of the QGIS (and underlying library) version information to the log. .. versionadded:: 3.4.7 +%End + + virtual QString htmlLog() const; +%Docstring +Returns the HTML formatted contents of the log, which contains all messages pushed to the feedback object. + +.. seealso:: :py:func:`textLog` + +.. versionadded:: 3.14 +%End + + virtual QString textLog() const; +%Docstring +Returns the plain text contents of the log, which contains all messages pushed to the feedback object. + +.. seealso:: :py:func:`htmlLog` + +.. versionadded:: 3.14 %End }; @@ -146,6 +172,9 @@ to scale the current progress to account for progress through the overall proces virtual void pushConsoleInfo( const QString &info ); + virtual QString htmlLog() const; + + virtual QString textLog() const; }; diff --git a/python/core/auto_generated/processing/qgsprocessingparameters.sip.in b/python/core/auto_generated/processing/qgsprocessingparameters.sip.in index ad680468e00e..6cdf1f0463f5 100644 --- a/python/core/auto_generated/processing/qgsprocessingparameters.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingparameters.sip.in @@ -25,20 +25,76 @@ Encapsulates settings relating to a feature source input to a processing algorit %End public: - QgsProcessingFeatureSourceDefinition( const QString &source = QString(), bool selectedFeaturesOnly = false ); + enum Flag + { + FlagOverrideDefaultGeometryCheck, + FlagCreateIndividualOutputPerInputFeature, + }; + typedef QFlags Flags; + + + QgsProcessingFeatureSourceDefinition( const QString &source = QString(), bool selectedFeaturesOnly = false, long long featureLimit = -1, + QgsProcessingFeatureSourceDefinition::Flags flags = 0, QgsFeatureRequest::InvalidGeometryCheck geometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid ); %Docstring -Constructor for QgsProcessingFeatureSourceDefinition, accepting a static string source. +Constructor for QgsProcessingFeatureSourceDefinition, accepting a static string ``source``. + +If ``selectedFeaturesOnly`` is ``True``, then only selected features from the source will be used. + +The optional ``featureLimit`` can be set to a value > 0 to place a hard limit on the maximum number +of features which will be read from the source. + +The ``flags`` argument can be used to specify flags which dictate the source behavior. + +If the QgsProcessingFeatureSourceDefinition.Flag.FlagOverrideDefaultGeometryCheck is set in ``flags``, then the value of ``geometryCheck`` will override +the default geometry check method (as dictated by :py:class:`QgsProcessingContext`) for this source. %End - QgsProcessingFeatureSourceDefinition( const QgsProperty &source, bool selectedFeaturesOnly = false ); + QgsProcessingFeatureSourceDefinition( const QgsProperty &source, bool selectedFeaturesOnly = false, long long featureLimit = -1, + QgsProcessingFeatureSourceDefinition::Flags flags = 0, QgsFeatureRequest::InvalidGeometryCheck geometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid ); %Docstring Constructor for QgsProcessingFeatureSourceDefinition, accepting a QgsProperty source. + +If ``selectedFeaturesOnly`` is ``True``, then only selected features from the source will be used. + +The optional ``featureLimit`` can be set to a value > 0 to place a hard limit on the maximum number +of features which will be read from the source. + +The ``flags`` argument can be used to specify flags which dictate the source behavior. + +If the QgsProcessingFeatureSourceDefinition.Flag.FlagOverrideDefaultGeometryCheck is set in ``flags``, then the value of ``geometryCheck`` will override +the default geometry check method (as dictated by :py:class:`QgsProcessingContext`) for this source. %End QgsProperty source; bool selectedFeaturesOnly; + long long featureLimit; + + Flags flags; + + QgsFeatureRequest::InvalidGeometryCheck geometryCheck; + + QVariant toVariant() const; +%Docstring +Saves this source definition to a QVariantMap, wrapped in a QVariant. +You can use QgsXmlUtils.writeVariant to save it to an XML document. + +.. seealso:: :py:func:`loadVariant` + +.. versionadded:: 3.14 +%End + + bool loadVariant( const QVariantMap &map ); +%Docstring +Loads this source definition from a QVariantMap, wrapped in a QVariant. +You can use QgsXmlUtils.readVariant to load it from an XML document. + +.. seealso:: :py:func:`toVariant` + +.. versionadded:: 3.14 +%End + bool operator==( const QgsProcessingFeatureSourceDefinition &other ); bool operator!=( const QgsProcessingFeatureSourceDefinition &other ); @@ -47,6 +103,8 @@ Constructor for QgsProcessingFeatureSourceDefinition, accepting a QgsProperty so }; +QFlags operator|(QgsProcessingFeatureSourceDefinition::Flag f1, QFlags f2); + class QgsProcessingOutputLayerDefinition @@ -85,6 +143,39 @@ to automatically load the resulting sink/layer after completing processing. QVariantMap createOptions; + bool useRemapping() const; +%Docstring +Returns ``True`` if the output uses a remapping definition. + +.. seealso:: :py:func:`remappingDefinition` + +.. versionadded:: 3.14 +%End + + QgsRemappingSinkDefinition remappingDefinition() const; +%Docstring +Returns the output remapping definition, if useRemapping() is ``True``. + +.. seealso:: :py:func:`useRemapping` + +.. seealso:: :py:func:`setRemappingDefinition` + +.. versionadded:: 3.14 +%End + + void setRemappingDefinition( const QgsRemappingSinkDefinition &definition ); +%Docstring +Sets the remapping ``definition`` to use when adding features to the output layer. + +Calling this method will set useRemapping() to ``True``. + +.. seealso:: :py:func:`remappingDefinition` + +.. seealso:: :py:func:`useRemapping` + +.. versionadded:: 3.14 +%End + QVariant toVariant() const; %Docstring Saves this output layer definition to a QVariantMap, wrapped in a QVariant. @@ -107,6 +198,9 @@ You can use QgsXmlUtils.readVariant to load it from an XML document. operator QVariant() const; + bool operator==( const QgsProcessingOutputLayerDefinition &other ) const; + bool operator!=( const QgsProcessingOutputLayerDefinition &other ) const; + }; @@ -199,6 +293,10 @@ their acceptable ranges, defaults, etc. sipType = sipType_QgsProcessingParameterDateTime; else if ( sipCpp->type() == QgsProcessingParameterProviderConnection::typeName() ) sipType = sipType_QgsProcessingParameterProviderConnection; + else if ( sipCpp->type() == QgsProcessingParameterDatabaseSchema::typeName() ) + sipType = sipType_QgsProcessingParameterDatabaseSchema; + else if ( sipCpp->type() == QgsProcessingParameterDatabaseTable::typeName() ) + sipType = sipType_QgsProcessingParameterDatabaseTable; else sipType = nullptr; %End @@ -754,7 +852,7 @@ This function creates a new object and the caller takes responsibility for delet static QgsFeatureSink *parameterAsSink( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsFields &fields, QgsWkbTypes::Type geometryType, const QgsCoordinateReferenceSystem &crs, - QgsProcessingContext &context, QString &destinationIdentifier /Out/, QgsFeatureSink::SinkFlags sinkFlags = 0 ) /Factory/; + QgsProcessingContext &context, QString &destinationIdentifier /Out/, QgsFeatureSink::SinkFlags sinkFlags = 0 ) throw( QgsProcessingException ) /Factory/; %Docstring Evaluates the parameter with matching ``definition`` and ``value`` to a feature sink. @@ -768,6 +866,8 @@ to the sink, e.g. via calling :py:func:`QgsProcessingUtils.mapLayerFromString()` This function creates a new object and the caller takes responsibility for deleting the returned object. +:raises :: py:class:`QgsProcessingException` + .. versionadded:: 3.4 %End @@ -845,7 +945,7 @@ a conversion in this case and will return the target layer name in the ``layerNa .. versionadded:: 3.10 %End - static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ); + static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingUtils::LayerHint layerHint = QgsProcessingUtils::LayerHint::UnknownType ); %Docstring Evaluates the parameter with matching ``definition`` to a map layer. @@ -854,7 +954,7 @@ sources and stored temporarily in the ``context``. In either case, callers do no need to handle deletion of the returned layer. %End - static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ); + static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context, QgsProcessingUtils::LayerHint layerHint = QgsProcessingUtils::LayerHint::UnknownType ); %Docstring Evaluates the parameter with matching ``definition`` and ``value`` to a map layer. @@ -1014,6 +1114,14 @@ Returns the coordinate reference system associated with an extent parameter valu .. seealso:: :py:func:`parameterAsExtent` %End + static QgsCoordinateReferenceSystem parameterAsExtentCrs( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ); +%Docstring +Returns the coordinate reference system associated with an extent parameter value. + +.. seealso:: :py:func:`parameterAsExtent` +%End + + static QgsPointXY parameterAsPoint( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context, const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem() ); %Docstring @@ -1201,6 +1309,34 @@ Evaluates the parameter with matching ``definition`` to a connection name string %Docstring Evaluates the parameter with matching ``definition`` and ``value`` to a connection name string. +.. versionadded:: 3.14 +%End + + static QString parameterAsSchema( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, const QgsProcessingContext &context ); +%Docstring +Evaluates the parameter with matching ``definition`` to a database schema name. + +.. versionadded:: 3.14 +%End + + static QString parameterAsSchema( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ); +%Docstring +Evaluates the parameter with matching ``definition`` and ``value`` to a database schema name. + +.. versionadded:: 3.14 +%End + + static QString parameterAsDatabaseTableName( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, const QgsProcessingContext &context ); +%Docstring +Evaluates the parameter with matching ``definition`` to a database table name. + +.. versionadded:: 3.14 +%End + + static QString parameterAsDatabaseTableName( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ); +%Docstring +Evaluates the parameter with matching ``definition`` and ``value`` to a database table name. + .. versionadded:: 3.14 %End @@ -1302,44 +1438,6 @@ Creates a new parameter using the definition from a script code. }; -class QgsProcessingParameterMapLayer : QgsProcessingParameterDefinition -{ -%Docstring -A map layer parameter for processing algorithms. - -.. versionadded:: 3.0 -%End - -%TypeHeaderCode -#include "qgsprocessingparameters.h" -%End - public: - - QgsProcessingParameterMapLayer( const QString &name, const QString &description = QString(), const QVariant &defaultValue = QVariant(), - bool optional = false ); -%Docstring -Constructor for QgsProcessingParameterMapLayer. -%End - - static QString typeName(); -%Docstring -Returns the type name for the parameter class. -%End - virtual QgsProcessingParameterDefinition *clone() const /Factory/; - - virtual QString type() const; - virtual bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = 0 ) const; - - virtual QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const; - - - static QgsProcessingParameterMapLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) /Factory/; -%Docstring -Creates a new parameter using the definition from a script code. -%End - -}; - class QgsProcessingParameterExtent : QgsProcessingParameterDefinition { %Docstring @@ -1633,7 +1731,7 @@ Creates a new parameter using the definition from a script code. }; -class QgsProcessingParameterMultipleLayers : QgsProcessingParameterDefinition +class QgsProcessingParameterMultipleLayers : QgsProcessingParameterDefinition, QgsFileFilterGenerator { %Docstring A parameter for processing algorithms which accepts multiple map layers. @@ -1668,6 +1766,8 @@ Returns the type name for the parameter class. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + QgsProcessing::SourceType layerType() const; %Docstring @@ -2015,7 +2115,7 @@ Creates a new parameter using the definition from a script code. }; -class QgsProcessingParameterRasterLayer : QgsProcessingParameterDefinition +class QgsProcessingParameterRasterLayer : QgsProcessingParameterDefinition, QgsFileFilterGenerator { %Docstring A raster layer parameter for processing algorithms. @@ -2045,6 +2145,8 @@ Returns the type name for the parameter class. virtual QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const; + virtual QString createFileFilter() const; + static QgsProcessingParameterRasterLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) /Factory/; %Docstring @@ -2331,7 +2433,7 @@ Sets the geometry ``types`` for sources acceptable by the parameter. }; -class QgsProcessingParameterVectorLayer : QgsProcessingParameterDefinition, QgsProcessingParameterLimitedDataTypes +class QgsProcessingParameterVectorLayer : QgsProcessingParameterDefinition, QgsProcessingParameterLimitedDataTypes, QgsFileFilterGenerator { %Docstring A vector layer (with or without geometry) parameter for processing algorithms. Consider using @@ -2367,6 +2469,8 @@ Returns the type name for the parameter class. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + virtual QVariantMap toVariantMap() const; @@ -2380,7 +2484,7 @@ Creates a new parameter using the definition from a script code. }; -class QgsProcessingParameterMeshLayer : QgsProcessingParameterDefinition +class QgsProcessingParameterMeshLayer : QgsProcessingParameterDefinition, QgsFileFilterGenerator { %Docstring A mesh layer parameter for processing algorithms. @@ -2412,6 +2516,8 @@ Returns the type name for the parameter class. virtual QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const; + virtual QString createFileFilter() const; + static QgsProcessingParameterMeshLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) /Factory/; %Docstring @@ -2419,6 +2525,56 @@ Creates a new parameter using the definition from a script code. %End }; +class QgsProcessingParameterMapLayer : QgsProcessingParameterDefinition, QgsProcessingParameterLimitedDataTypes, QgsFileFilterGenerator +{ +%Docstring +A map layer parameter for processing algorithms. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsprocessingparameters.h" +%End + public: + + QgsProcessingParameterMapLayer( const QString &name, const QString &description = QString(), const QVariant &defaultValue = QVariant(), + bool optional = false, + const QList< int > &types = QList< int >() ); +%Docstring +Constructor for QgsProcessingParameterMapLayer. +%End + + static QString typeName(); +%Docstring +Returns the type name for the parameter class. +%End + virtual QgsProcessingParameterDefinition *clone() const /Factory/; + + virtual QString type() const; + virtual bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = 0 ) const; + + virtual QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const; + + virtual QString asScriptCode() const; + + virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + + virtual QString createFileFilter() const; + + + virtual QVariantMap toVariantMap() const; + + virtual bool fromVariantMap( const QVariantMap &map ); + + + static QgsProcessingParameterMapLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) /Factory/; +%Docstring +Creates a new parameter using the definition from a script code. +%End + +}; + class QgsProcessingParameterField : QgsProcessingParameterDefinition { %Docstring @@ -2547,7 +2703,7 @@ Creates a new parameter using the definition from a script code. }; -class QgsProcessingParameterFeatureSource : QgsProcessingParameterDefinition, QgsProcessingParameterLimitedDataTypes +class QgsProcessingParameterFeatureSource : QgsProcessingParameterDefinition, QgsProcessingParameterLimitedDataTypes, QgsFileFilterGenerator { %Docstring An input feature source (such as vector layers) parameter for processing algorithms. @@ -2582,6 +2738,8 @@ Returns the type name for the parameter class. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + virtual QVariantMap toVariantMap() const; @@ -2595,7 +2753,7 @@ Creates a new parameter using the definition from a script code. }; -class QgsProcessingDestinationParameter : QgsProcessingParameterDefinition +class QgsProcessingDestinationParameter : QgsProcessingParameterDefinition, QgsFileFilterGenerator { %Docstring Base class for all parameter definitions which represent file or layer destinations, e.g. parameters @@ -2625,6 +2783,8 @@ output will not be created by default. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + virtual QgsProcessingOutputDefinition *toOutputDefinition() const = 0 /Factory/; %Docstring @@ -2709,7 +2869,7 @@ A parameter which represents the destination feature sink for features created b public: QgsProcessingParameterFeatureSink( const QString &name, const QString &description = QString(), QgsProcessing::SourceType type = QgsProcessing::TypeVectorAnyGeometry, const QVariant &defaultValue = QVariant(), - bool optional = false, bool createByDefault = true ); + bool optional = false, bool createByDefault = true, bool supportsAppend = false ); %Docstring Constructor for QgsProcessingParameterFeatureSink. @@ -2736,6 +2896,8 @@ Returns the type name for the parameter class. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + virtual QStringList supportedOutputVectorLayerExtensions() const; %Docstring @@ -2764,6 +2926,30 @@ cannot be reliably determined in advance, this method will default to returning Sets the layer ``type`` for the sinks associated with the parameter. .. seealso:: :py:func:`dataType` +%End + + bool supportsAppend() const; +%Docstring +Returns ``True`` if the sink supports appending features to an existing table. + +A sink only supports appending if the algorithm implements QgsProcessingAlgorithm.sinkProperties for the sink parameter. + +.. seealso:: :py:func:`setSupportsAppend` + +.. versionadded:: 3.14 +%End + + void setSupportsAppend( bool supportsAppend ); +%Docstring +Sets whether the sink supports appending features to an existing table. + +.. warning:: + + A sink only supports appending if the algorithm implements QgsProcessingAlgorithm.sinkProperties for the sink parameter. + +.. seealso:: :py:func:`supportsAppend` + +.. versionadded:: 3.14 %End virtual QVariantMap toVariantMap() const; @@ -2828,6 +3014,8 @@ Returns the type name for the parameter class. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + virtual QStringList supportedOutputVectorLayerExtensions() const; %Docstring @@ -2911,6 +3099,8 @@ Returns the type name for the parameter class. virtual QString defaultFileExtension() const; + virtual QString createFileFilter() const; + virtual QStringList supportedOutputRasterLayerExtensions() const; %Docstring @@ -2970,6 +3160,8 @@ Returns the type name for the parameter class. virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + virtual QString createFileFilter() const; + QString fileFilter() const; %Docstring @@ -3687,6 +3879,187 @@ Creates a new parameter using the definition from a script code. }; +class QgsProcessingParameterDatabaseSchema : QgsProcessingParameterDefinition +{ +%Docstring +A database schema parameter for processing algorithms, allowing users to select from existing schemas +on a registered database connection. + +QgsProcessingParameterDatabaseSchema should be evaluated by calling :py:func:`QgsProcessingAlgorithm.parameterAsSchema()` + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingparameters.h" +%End + public: + + QgsProcessingParameterDatabaseSchema( const QString &name, const QString &description, const QString &connectionParameterName = QString(), const QVariant &defaultValue = QVariant(), + bool optional = false ); +%Docstring +Constructor for QgsProcessingParameterDatabaseSchema. + +The ``connectionParameterName`` specifies the name of the parent QgsProcessingParameterProviderConnection parameter. + +.. warning:: + + The provider must support the connection API methods in its QgsProviderMetadata implementation + in order for the model to work correctly. This is only implemented for a subset of current data providers. +%End + + static QString typeName(); +%Docstring +Returns the type name for the parameter class. +%End + virtual QgsProcessingParameterDefinition *clone() const /Factory/; + + virtual QString type() const; + virtual bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = 0 ) const; + + virtual QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const; + + virtual QString asScriptCode() const; + + virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + + virtual QVariantMap toVariantMap() const; + + virtual bool fromVariantMap( const QVariantMap &map ); + + virtual QStringList dependsOnOtherParameters() const; + + + QString parentConnectionParameterName() const; +%Docstring +Returns the name of the parent connection parameter, or an empty string if this is not set. + +.. seealso:: :py:func:`setParentConnectionParameterName` +%End + + void setParentConnectionParameterName( const QString &name ); +%Docstring +Sets the ``name`` of the parent connection parameter. Use an empty string if this is not required. + +.. seealso:: :py:func:`parentConnectionParameterName` +%End + + static QgsProcessingParameterDatabaseSchema *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) /Factory/; +%Docstring +Creates a new parameter using the definition from a script code. +%End + +}; + + +class QgsProcessingParameterDatabaseTable : QgsProcessingParameterDefinition +{ +%Docstring +A database table name parameter for processing algorithms, allowing users to select from existing database tables +on a registered database connection (or optionally to enter a new table name). + +QgsProcessingParameterDatabaseTable should be evaluated by calling :py:func:`QgsProcessingAlgorithm.parameterAsDatabaseTableName()` + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingparameters.h" +%End + public: + + QgsProcessingParameterDatabaseTable( const QString &name, const QString &description, + const QString &connectionParameterName = QString(), + const QString &schemaParameterName = QString(), + const QVariant &defaultValue = QVariant(), + bool optional = false, + bool allowNewTableNames = false ); +%Docstring +Constructor for QgsProcessingParameterDatabaseTable. + +The ``connectionParameterName`` specifies the name of the parent QgsProcessingParameterProviderConnection parameter. +The ``schemaParameterName`` specifies the name of the parent QgsProcessingParameterDatabaseSchema parameter. + +.. warning:: + + The provider must support the connection API methods in its QgsProviderMetadata implementation + in order for the model to work correctly. This is only implemented for a subset of current data providers. +%End + + static QString typeName(); +%Docstring +Returns the type name for the parameter class. +%End + virtual QgsProcessingParameterDefinition *clone() const /Factory/; + + virtual QString type() const; + virtual bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = 0 ) const; + + virtual QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const; + + virtual QString asScriptCode() const; + + virtual QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const; + + virtual QVariantMap toVariantMap() const; + + virtual bool fromVariantMap( const QVariantMap &map ); + + virtual QStringList dependsOnOtherParameters() const; + + + QString parentConnectionParameterName() const; +%Docstring +Returns the name of the parent connection parameter, or an empty string if this is not set. + +.. seealso:: :py:func:`setParentConnectionParameterName` +%End + + void setParentConnectionParameterName( const QString &name ); +%Docstring +Sets the ``name`` of the parent connection parameter. Use an empty string if this is not required. + +.. seealso:: :py:func:`parentConnectionParameterName` +%End + + QString parentSchemaParameterName() const; +%Docstring +Returns the name of the parent schema parameter, or an empty string if this is not set. + +.. seealso:: :py:func:`setParentSchemaParameterName` +%End + + void setParentSchemaParameterName( const QString &name ); +%Docstring +Sets the ``name`` of the parent schema parameter. Use an empty string if this is not required. + +.. seealso:: :py:func:`parentSchemaParameterName` +%End + + static QgsProcessingParameterDatabaseTable *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) /Factory/; +%Docstring +Creates a new parameter using the definition from a script code. +%End + + bool allowNewTableNames() const; +%Docstring +Returns ``True`` if the parameter allows users to enter names for +a new (non-existing) tables. + +.. seealso:: :py:func:`setAllowNewTableNames` +%End + + void setAllowNewTableNames( bool allowed ); +%Docstring +Sets whether the parameter allows users to enter names for +a new (non-existing) tables. + +.. seealso:: :py:func:`allowNewTableNames` +%End + +}; + + diff --git a/python/core/auto_generated/processing/qgsprocessingprovider.sip.in b/python/core/auto_generated/processing/qgsprocessingprovider.sip.in index 888830ed9074..1486af167fb6 100644 --- a/python/core/auto_generated/processing/qgsprocessingprovider.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingprovider.sip.in @@ -24,6 +24,13 @@ to a common area of analysis. %End public: + enum Flag + { + FlagDeemphasiseSearchResults, + }; + typedef QFlags Flags; + + QgsProcessingProvider( QObject *parent /TransferThis/ = 0 ); %Docstring Constructor for QgsProcessingProvider. @@ -44,6 +51,14 @@ Returns an icon for the provider. Returns a path to an SVG version of the provider's icon. .. seealso:: :py:func:`icon` +%End + + virtual Flags flags() const; +%Docstring +Returns the flags indicating how and when the provider operates and should be exposed to users. +Default is no flags. + +.. versionadded:: 3.14 %End virtual QString id() const = 0; @@ -284,6 +299,9 @@ Adds an ``algorithm`` to the provider. Ownership of the algorithm is transferred QgsProcessingProvider( const QgsProcessingProvider &other ); }; +QFlags operator|(QgsProcessingProvider::Flag f1, QFlags f2); + + /************************************************************************ diff --git a/python/core/auto_generated/processing/qgsprocessingutils.sip.in b/python/core/auto_generated/processing/qgsprocessingutils.sip.in index 08377dcc75fe..944b88ec602e 100644 --- a/python/core/auto_generated/processing/qgsprocessingutils.sip.in +++ b/python/core/auto_generated/processing/qgsprocessingutils.sip.in @@ -90,6 +90,28 @@ value. .. seealso:: :py:func:`compatibleRasterLayers` .. seealso:: :py:func:`compatibleVectorLayers` +%End + + static QString encodeProviderKeyAndUri( const QString &providerKey, const QString &uri ); +%Docstring +Encodes a provider key and layer ``uri`` to a single string, for use with +decodeProviderKeyAndUri() + +.. versionadded:: 3.14 +%End + + static bool decodeProviderKeyAndUri( const QString &string, QString &providerKey /Out/, QString &uri /Out/ ); +%Docstring +Decodes a provider key and layer ``uri`` from an encoded string, for use with +encodeProviderKeyAndUri() + +:param string: encoded string, as returned by encodeProviderKeyAndUri() +:param uri: decoded layer uri + +:return: - ``True`` if ``string`` was successfully decoded + - providerKey: ID key for corresponding data provider + +.. versionadded:: 3.14 %End enum class LayerHint @@ -241,7 +263,7 @@ a specified ``algorithm``. const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, - QgsProcessingFeedback *feedback ); + QgsProcessingFeedback *feedback, long long featureLimit = -1 ); %Docstring Converts a source vector ``layer`` to a file path of a vector layer of compatible format. @@ -254,6 +276,8 @@ in a temporary location using ``baseName``. The function will then return the pa The ``preferredFormat`` argument is used to specify to desired file extension to use when a temporary layer export is required. This defaults to shapefiles. +The ``featureLimit`` argument can be used to specify a limit on the number of features read from the layer. + When an algorithm is capable of handling multi-layer input files (such as Geopackage), it is preferable to use convertToCompatibleFormatAndLayerName() which may avoid conversion in more situations. @@ -267,7 +291,7 @@ to use convertToCompatibleFormatAndLayerName() which may avoid conversion in mor const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, - QString &layerName /Out/ ); + QString &layerName /Out/, long long featureLimit = -1 ); %Docstring Converts a source vector ``layer`` to a file path and layer name of a vector layer of compatible format. @@ -277,6 +301,8 @@ in a temporary location using ``baseName``. The function will then return the pa ``compatibleFormats`` should consist entirely of lowercase file extensions, e.g. 'shp'. +The ``featureLimit`` argument can be used to specify a limit on the number of features read from the layer. + The ``preferredFormat`` argument is used to specify to desired file extension to use when a temporary layer export is required. This defaults to shapefiles. @@ -291,6 +317,7 @@ a conversion in this case and will return the target layer name in the ``layerNa :param preferredFormat: preferred format extension to use if conversion if required :param context: processing context :param feedback: feedback object +:param featureLimit: can be used to place a limit on the maximum number of features read from the layer :return: - path to source layer, or nearly converted compatible layer - layerName: will be set to the target layer name for multi-layer sources (e.g. Geopackage) @@ -379,13 +406,17 @@ results according to the settings in a :py:class:`QgsProcessingContext`. typedef QFlags Flags; - QgsProcessingFeatureSource( QgsFeatureSource *originalSource, const QgsProcessingContext &context, bool ownsOriginalSource = false ); + QgsProcessingFeatureSource( QgsFeatureSource *originalSource, const QgsProcessingContext &context, bool ownsOriginalSource = false, + long long featureLimit = -1 ); %Docstring Constructor for QgsProcessingFeatureSource, accepting an original feature source ``originalSource`` and processing ``context``. Ownership of ``originalSource`` is dictated by ``ownsOriginalSource``. If ``ownsOriginalSource`` is ``False``, ownership is not transferred, and callers must ensure that ``originalSource`` exists for the lifetime of this object. If ``ownsOriginalSource`` is ``True``, then this object will take ownership of ``originalSource``. + +If ``featureLimit`` is set to a value > 0, then a limit is placed on the maximum number of features which will be +read from the source. %End ~QgsProcessingFeatureSource(); @@ -428,6 +459,13 @@ iterator, eg by restricting the returned attributes or geometry. QgsExpressionContextScope *createExpressionContextScope() const /Factory/; %Docstring Returns an expression context scope suitable for this source. +%End + + void setInvalidGeometryCheck( QgsFeatureRequest::InvalidGeometryCheck method ); +%Docstring +Overrides the default geometry check method for the source. + +.. versionadded:: 3.14 %End }; diff --git a/python/core/auto_generated/qgsabstractdatabaseproviderconnection.sip.in b/python/core/auto_generated/qgsabstractdatabaseproviderconnection.sip.in index 438e7b05323f..1cfa07de3115 100644 --- a/python/core/auto_generated/qgsabstractdatabaseproviderconnection.sip.in +++ b/python/core/auto_generated/qgsabstractdatabaseproviderconnection.sip.in @@ -231,6 +231,9 @@ This information is calculated from the geometry columns types. SqlLayers, TableExists, Spatial, + CreateSpatialIndex, + SpatialIndexExists, + DeleteSpatialIndex, }; typedef QFlags Capabilities; @@ -270,7 +273,7 @@ Raises a QgsProviderConnectionException if any errors are encountered. virtual void createVectorTable( const QString &schema, const QString &name, const QgsFields &fields, QgsWkbTypes::Type wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, const QMap *options ) const throw( QgsProviderConnectionException ); %Docstring -Creates an empty table with ``name`` in the given ``schema`` (schema is ignored if not supported by the backend). +Creates an empty table with ``name`` in the given ``schema`` (schema is ignored if not supported by the backend). Raises a QgsProviderConnectionException if any errors are encountered. :raises :: py:class:`QgsProviderConnectionException` @@ -298,7 +301,7 @@ Raises a QgsProviderConnectionException if any errors are encountered. virtual void dropRasterTable( const QString &schema, const QString &name ) const throw( QgsProviderConnectionException ); %Docstring -Drops a raster table with given ``schema`` (schema is ignored if not supported by the backend) and ``name``. +Drops a raster table with given ``schema`` (schema is ignored if not supported by the backend) and ``name``. Raises a QgsProviderConnectionException if any errors are encountered. .. note:: @@ -310,7 +313,7 @@ Raises a QgsProviderConnectionException if any errors are encountered. virtual void renameVectorTable( const QString &schema, const QString &name, const QString &newName ) const throw( QgsProviderConnectionException ); %Docstring -Renames a vector or aspatial table with given ``schema`` (schema is ignored if not supported by the backend) and ``name``. +Renames a vector or aspatial table with given ``schema`` (schema is ignored if not supported by the backend) and ``name``. Raises a QgsProviderConnectionException if any errors are encountered. .. note:: @@ -322,7 +325,7 @@ Raises a QgsProviderConnectionException if any errors are encountered. virtual void renameRasterTable( const QString &schema, const QString &name, const QString &newName ) const throw( QgsProviderConnectionException ); %Docstring -Renames a raster table with given ``schema`` (schema is ignored if not supported by the backend) and ``name``. +Renames a raster table with given ``schema`` (schema is ignored if not supported by the backend) and ``name``. Raises a QgsProviderConnectionException if any errors are encountered. .. note:: @@ -376,12 +379,54 @@ Raises a QgsProviderConnectionException if any errors are encountered. virtual void vacuum( const QString &schema, const QString &name ) const throw( QgsProviderConnectionException ); %Docstring -Vacuum the database table with given ``schema`` and ``name`` (schema is ignored if not supported by the backend). +Vacuum the database table with given ``schema`` and ``name`` (schema is ignored if not supported by the backend). Raises a QgsProviderConnectionException if any errors are encountered. :raises :: py:class:`QgsProviderConnectionException` %End + struct SpatialIndexOptions + { + QString geometryColumnName; + }; + + virtual void createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options = QgsAbstractDatabaseProviderConnection::SpatialIndexOptions() ) const throw( QgsProviderConnectionException ); +%Docstring +Creates a spatial index for the database table with given ``schema`` and ``name`` (schema is ignored if not supported by the backend). + +The ``options`` argument can be used to provide extra options controlling the spatial index creation. + +Raises a QgsProviderConnectionException if any errors are encountered. + +:raises :: py:class:`QgsProviderConnectionException` + +.. versionadded:: 3.14 +%End + + virtual bool spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const throw( QgsProviderConnectionException ); +%Docstring +Determines whether a spatial index exists for the database table with given ``schema``, ``name`` and ``geometryColumn`` (``schema`` and ``geometryColumn`` are +ignored if not supported by the backend). + +Raises a QgsProviderConnectionException if any errors are encountered. + +:raises :: py:class:`QgsProviderConnectionException` + +.. versionadded:: 3.14 +%End + + virtual void deleteSpatialIndex( const QString &schema, const QString &name, const QString &geometryColumn ) const throw( QgsProviderConnectionException ); +%Docstring +Deletes the existing spatial index for the database table with given ``schema``, ``name`` and ``geometryColumn`` (``schema`` and ``geometryColumn`` are +ignored if not supported by the backend). + +Raises a QgsProviderConnectionException if any errors are encountered. + +:raises :: py:class:`QgsProviderConnectionException` + +.. versionadded:: 3.14 +%End + virtual QgsAbstractDatabaseProviderConnection::TableProperty table( const QString &schema, const QString &table ) const; %Docstring diff --git a/python/core/auto_generated/qgsactionscope.sip.in b/python/core/auto_generated/qgsactionscope.sip.in index 1c4a6439bdcd..32984bb89466 100644 --- a/python/core/auto_generated/qgsactionscope.sip.in +++ b/python/core/auto_generated/qgsactionscope.sip.in @@ -36,6 +36,9 @@ form. #include "qgsactionscope.h" %End public: +%TypeCode +#include +%End explicit QgsActionScope(); %Docstring @@ -109,6 +112,10 @@ in here, they are extracted automatically from the expressionContextScope(). Returns if this scope is valid. .. versionadded:: 3.0 +%End + long __hash__(); +%MethodCode + sipRes = qHash( *sipCpp ); %End }; diff --git a/python/core/auto_generated/qgsapplication.sip.in b/python/core/auto_generated/qgsapplication.sip.in index b45b54d93549..c83163800373 100644 --- a/python/core/auto_generated/qgsapplication.sip.in +++ b/python/core/auto_generated/qgsapplication.sip.in @@ -810,6 +810,13 @@ Returns the application's page size registry, used for managing layout page size Returns the action scope registry. .. versionadded:: 3.0 +%End + + static QgsConnectionRegistry *connectionRegistry(); +%Docstring +Returns the application's connection registry, used for managing saved data provider connections. + +.. versionadded:: 3.14 %End static QgsRuntimeProfiler *profiler(); @@ -836,6 +843,13 @@ Gets the registry of available field formatters. Returns registry of available 3D renderers. .. versionadded:: 3.0 +%End + + static QgsScaleBarRendererRegistry *scaleBarRendererRegistry() /KeepReference/; +%Docstring +Gets the registry of available scalebar renderers. + +.. versionadded:: 3.14 %End static QgsProjectStorageRegistry *projectStorageRegistry() /KeepReference/; diff --git a/python/core/auto_generated/qgsconnectionregistry.sip.in b/python/core/auto_generated/qgsconnectionregistry.sip.in new file mode 100644 index 000000000000..099550c6e74f --- /dev/null +++ b/python/core/auto_generated/qgsconnectionregistry.sip.in @@ -0,0 +1,61 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsconnectionregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsConnectionRegistry : QObject +{ +%Docstring +A registry for saved data provider connections, allowing retrieval of +saved connections by name and provider type. + +QgsConnectionRegistry is not usually directly created, but rather accessed through +:py:func:`QgsApplication.connectionRegistry()` + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsconnectionregistry.h" +%End + public: + + QgsConnectionRegistry( QObject *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsConnectionRegistry. +%End + + + QgsAbstractProviderConnection *createConnection( const QString &name ) throw( QgsProviderConnectionException ) /Factory/; +%Docstring +Creates a new connection by loading the connection with the given ``id`` from the settings. + +The ``id`` string must be of the format "provider://connection_name", e.g. "postgres://my_connection" for +the PostgreSQL connection saved as "my_connection". + +Ownership is transferred to the caller. + +:raises :: py:class:`QgsProviderConnectionException` +%End + + private: + QgsConnectionRegistry( const QgsConnectionRegistry &other ); +}; + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsconnectionregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgsdatabaseschemamodel.sip.in b/python/core/auto_generated/qgsdatabaseschemamodel.sip.in index 63395862a8ad..46199d17c399 100644 --- a/python/core/auto_generated/qgsdatabaseschemamodel.sip.in +++ b/python/core/auto_generated/qgsdatabaseschemamodel.sip.in @@ -28,6 +28,11 @@ called. %End public: + enum Role + { + RoleEmpty, + }; + explicit QgsDatabaseSchemaModel( const QString &provider, const QString &connection, QObject *parent /TransferThis/ = 0 ); %Docstring Constructor for QgsDatabaseSchemaModel, for the specified ``provider`` and ``connection`` name. @@ -56,6 +61,20 @@ Ownership of ``connection`` is transferred to the model. virtual QModelIndex index( int row, int column, const QModelIndex &parent ) const; + void setAllowEmptySchema( bool allowEmpty ); +%Docstring +Sets whether an optional empty schema ("not set") option is present in the model. + +.. seealso:: :py:func:`allowEmptySchema` +%End + + bool allowEmptySchema() const; +%Docstring +Returns ``True`` if the model allows the empty schema ("not set") choice. + +.. seealso:: :py:func:`setAllowEmptySchema` +%End + public slots: void refresh(); diff --git a/python/core/auto_generated/qgsdatabasetablemodel.sip.in b/python/core/auto_generated/qgsdatabasetablemodel.sip.in index c62e1f060455..3c9ae3974b3b 100644 --- a/python/core/auto_generated/qgsdatabasetablemodel.sip.in +++ b/python/core/auto_generated/qgsdatabasetablemodel.sip.in @@ -36,6 +36,7 @@ called. RoleCustomInfo, RoleWkbType, RoleCrs, + RoleEmpty, }; explicit QgsDatabaseTableModel( const QString &provider, const QString &connection, const QString &schema = QString(), QObject *parent /TransferThis/ = 0 ); @@ -70,6 +71,20 @@ Ownership of ``connection`` is transferred to the model. virtual QModelIndex index( int row, int column, const QModelIndex &parent ) const; + void setAllowEmptyTable( bool allowEmpty ); +%Docstring +Sets whether an optional empty table ("not set") option is present in the model. + +.. seealso:: :py:func:`allowEmptyTable` +%End + + bool allowEmptyTable() const; +%Docstring +Returns ``True`` if the model allows the empty table ("not set") choice. + +.. seealso:: :py:func:`setAllowEmptyTable` +%End + public slots: void refresh(); diff --git a/python/core/auto_generated/qgsdataitem.sip.in b/python/core/auto_generated/qgsdataitem.sip.in index 4edac4ac530a..c44ca229b69e 100644 --- a/python/core/auto_generated/qgsdataitem.sip.in +++ b/python/core/auto_generated/qgsdataitem.sip.in @@ -479,7 +479,8 @@ Item that represents a layer that can be opened with one of the providers Database, Table, Plugin, - Mesh + Mesh, + VectorTile }; @@ -573,6 +574,10 @@ Use QgsDataItemGuiProvider.deleteLayer instead static QIcon iconMesh(); %Docstring Returns icon for mesh layer type +%End + static QIcon iconVectorTile(); +%Docstring +Returns icon for vector tile layer %End virtual QString layerName() const; diff --git a/python/core/auto_generated/qgsdataprovider.sip.in b/python/core/auto_generated/qgsdataprovider.sip.in index fe1cbd10788f..c72c2a33ce3a 100644 --- a/python/core/auto_generated/qgsdataprovider.sip.in +++ b/python/core/auto_generated/qgsdataprovider.sip.in @@ -102,6 +102,18 @@ connection string only when needed within a provider %End + virtual QString dataComment() const; +%Docstring +Returns a short comment for the data that this provider is +providing access to (e.g. the comment for postgres table). + +.. note:: + + The default implementation returns an empty string. + +.. versionadded:: 3.14 +%End + void setUri( const QgsDataSourceUri &uri ); %Docstring Set the data source specification. @@ -125,6 +137,7 @@ This may be ``None``, depending on the data provider. .. versionadded:: 3.14 %End + virtual QgsRectangle extent() const = 0; %Docstring Returns the extent of the layer diff --git a/python/core/auto_generated/qgsdataprovidertemporalcapabilities.sip.in b/python/core/auto_generated/qgsdataprovidertemporalcapabilities.sip.in index 22f18321130b..1b1cd1c3e7d2 100644 --- a/python/core/auto_generated/qgsdataprovidertemporalcapabilities.sip.in +++ b/python/core/auto_generated/qgsdataprovidertemporalcapabilities.sip.in @@ -30,6 +30,10 @@ Base class for handling properties relating to a data provider's temporal capabi { sipType = sipType_QgsVectorDataProviderTemporalCapabilities; } + else if ( dynamic_cast < QgsMeshDataProviderTemporalCapabilities * >( sipCpp ) ) + { + sipType = sipType_QgsMeshDataProviderTemporalCapabilities; + } else { sipType = 0; diff --git a/python/core/auto_generated/qgsfeaturesink.sip.in b/python/core/auto_generated/qgsfeaturesink.sip.in index 596a37c8ba2e..b958e7ece934 100644 --- a/python/core/auto_generated/qgsfeaturesink.sip.in +++ b/python/core/auto_generated/qgsfeaturesink.sip.in @@ -77,41 +77,6 @@ QFlags operator|(QgsFeatureSink::Flag f1, QFlags" ).arg( sipCpp->name(), sipCpp->dataProvider()->name() ); + QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider() ? sipCpp->dataProvider()->name() : QStringLiteral( "Invalid" ) ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); %End diff --git a/python/core/auto_generated/qgsmaplayerlegend.sip.in b/python/core/auto_generated/qgsmaplayerlegend.sip.in index b5f7e3aa20c6..5bd98d90b215 100644 --- a/python/core/auto_generated/qgsmaplayerlegend.sip.in +++ b/python/core/auto_generated/qgsmaplayerlegend.sip.in @@ -96,6 +96,24 @@ Miscellaneous utility functions for handling of map layer legend static QString legendNodeUserLabel( QgsLayerTreeLayer *nodeLayer, int originalIndex ); static bool hasLegendNodeUserLabel( QgsLayerTreeLayer *nodeLayer, int originalIndex ); + static void setLegendNodePatchShape( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsLegendPatchShape &shape ); +%Docstring +Sets the legend patch ``shape`` for the legend node belonging to ``nodeLayer`` at the specified ``originalIndex``. + +.. seealso:: :py:func:`legendNodePatchShape` + +.. versionadded:: 3.14 +%End + + static QgsLegendPatchShape legendNodePatchShape( QgsLayerTreeLayer *nodeLayer, int originalIndex ); +%Docstring +Returns the legend patch shape for the legend node belonging to ``nodeLayer`` at the specified ``originalIndex``. + +.. seealso:: :py:func:`setLegendNodePatchShape` + +.. versionadded:: 3.14 +%End + static void applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList &nodes ); %Docstring update according to layer node's custom properties (order of items, user labels for items) diff --git a/python/core/auto_generated/qgsmaplayerproxymodel.sip.in b/python/core/auto_generated/qgsmaplayerproxymodel.sip.in index 6f07901d899a..cd78381587d9 100644 --- a/python/core/auto_generated/qgsmaplayerproxymodel.sip.in +++ b/python/core/auto_generated/qgsmaplayerproxymodel.sip.in @@ -34,6 +34,7 @@ The QgsMapLayerProxyModel class provides an easy to use model to display the lis PluginLayer, WritableLayer, MeshLayer, + VectorTileLayer, All }; typedef QFlags Filters; diff --git a/python/core/auto_generated/qgsmaplayertemporalproperties.sip.in b/python/core/auto_generated/qgsmaplayertemporalproperties.sip.in index 2c76a37159f5..088296a52f81 100644 --- a/python/core/auto_generated/qgsmaplayertemporalproperties.sip.in +++ b/python/core/auto_generated/qgsmaplayertemporalproperties.sip.in @@ -31,6 +31,10 @@ how an individual QgsMapLayer behaves in a temporal context, e.g. while animatin { sipType = sipType_QgsRasterLayerTemporalProperties; } + else if ( qobject_cast( sipCpp ) ) + { + sipType = sipType_QgsMeshLayerTemporalProperties; + } else { sipType = 0; @@ -57,26 +61,6 @@ Writes the properties to a DOM ``element``, to be used later with readXml(). Reads temporal properties from a DOM ``element`` previously written by writeXml(). .. seealso:: :py:func:`writeXml` -%End - - enum TemporalSource - { - Layer, - Project - }; - - TemporalSource temporalSource() const; -%Docstring -Returns the temporal properties temporal range source, can be layer or project. - -.. seealso:: :py:func:`setTemporalSource` -%End - - void setTemporalSource( TemporalSource source ); -%Docstring -Sets the temporal properties temporal range ``source``. - -.. seealso:: :py:func:`temporalSource` %End virtual bool isVisibleInTemporalRange( const QgsDateTimeRange &range ) const; diff --git a/python/core/auto_generated/qgsnetworkcontentfetchertask.sip.in b/python/core/auto_generated/qgsnetworkcontentfetchertask.sip.in index c81dce840140..86910db4869a 100644 --- a/python/core/auto_generated/qgsnetworkcontentfetchertask.sip.in +++ b/python/core/auto_generated/qgsnetworkcontentfetchertask.sip.in @@ -34,7 +34,7 @@ without danger of the task being first removed by the QgsTaskManager. %End public: - QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg = QString() ); + QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg = QString(), QgsTask::Flags flags = QgsTask::CanCancel ); %Docstring Constructor for a QgsNetworkContentFetcherTask which fetches the specified ``url``. @@ -42,7 +42,7 @@ the specified ``url``. Optionally, authentication configuration can be set via the ``authcfg`` argument. %End - QgsNetworkContentFetcherTask( const QNetworkRequest &request, const QString &authcfg = QString() ); + QgsNetworkContentFetcherTask( const QNetworkRequest &request, const QString &authcfg = QString(), QgsTask::Flags flags = QgsTask::CanCancel ); %Docstring Constructor for a QgsNetworkContentFetcherTask which fetches the specified network ``request``. diff --git a/python/core/auto_generated/qgsproject.sip.in b/python/core/auto_generated/qgsproject.sip.in index b8c790abbfbf..38b01d97ea06 100644 --- a/python/core/auto_generated/qgsproject.sip.in +++ b/python/core/auto_generated/qgsproject.sip.in @@ -48,6 +48,13 @@ open within the main QGIS application. Qgs, }; + enum class AvoidIntersectionsMode + { + AllowIntersections, + AvoidIntersectionsCurrentLayer, + AvoidIntersectionsLayers, + }; + static QgsProject *instance(); %Docstring Returns the QgsProject singleton instance @@ -740,9 +747,24 @@ A list of layers with which intersections should be avoided. void setAvoidIntersectionsLayers( const QList &layers ); %Docstring -A list of layers with which intersections should be avoided. +Sets the list of layers with which intersections should be avoided. +Only used if the avoid intersection mode is set to advanced. .. versionadded:: 3.0 +%End + + void setAvoidIntersectionsMode( const AvoidIntersectionsMode mode ); +%Docstring +Sets the avoid intersections mode. + +.. versionadded:: 3.14 +%End + + AvoidIntersectionsMode avoidIntersectionsMode() const; +%Docstring +Returns the current avoid intersections mode. + +.. versionadded:: 3.14 %End QVariantMap customVariables() const; @@ -1369,6 +1391,13 @@ Emitted when the home path of the project changes. void snappingConfigChanged( const QgsSnappingConfig &config ); %Docstring Emitted whenever the configuration for snapping has changed. +%End + + void avoidIntersectionsModeChanged(); +%Docstring +Emitted whenever the avoid intersections mode has changed. + +.. versionadded:: 3.14 %End void customVariablesChanged(); diff --git a/python/core/auto_generated/qgsproperty.sip.in b/python/core/auto_generated/qgsproperty.sip.in index 7d73e02e2cab..07a716599b30 100644 --- a/python/core/auto_generated/qgsproperty.sip.in +++ b/python/core/auto_generated/qgsproperty.sip.in @@ -491,6 +491,39 @@ be called in non-performance critical code. operator QVariant() const; + + SIP_PYOBJECT __repr__(); +%MethodCode + QString typeString; + QString definitionString; + switch ( sipCpp->propertyType() ) + { + case QgsProperty::StaticProperty: + typeString = QStringLiteral( "static" ); + definitionString = sipCpp->staticValue().toString(); + break; + + case QgsProperty::FieldBasedProperty: + typeString = QStringLiteral( "field" ); + definitionString = sipCpp->field(); + break; + + case QgsProperty::ExpressionBasedProperty: + typeString = QStringLiteral( "expression" ); + definitionString = sipCpp->expressionString(); + break; + + case QgsProperty::InvalidProperty: + typeString = QStringLiteral( "invalid" ); + break; + } + + QString str = QStringLiteral( "" ).arg( !sipCpp->isActive() && sipCpp->propertyType() != QgsProperty::InvalidProperty ? QStringLiteral( "INACTIVE " ) : QString(), + typeString, + definitionString.isEmpty() ? QString() : QStringLiteral( " (%1)" ).arg( definitionString ) ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); +%End + }; diff --git a/python/core/auto_generated/qgsproviderregistry.sip.in b/python/core/auto_generated/qgsproviderregistry.sip.in index 34ded27e6e3b..5c06732dd813 100644 --- a/python/core/auto_generated/qgsproviderregistry.sip.in +++ b/python/core/auto_generated/qgsproviderregistry.sip.in @@ -170,7 +170,7 @@ Either the ``parent`` widget must be set or the caller becomes responsible for deleting the returned widget. .. deprecated:: - QGIS 3.10 - use QgsGui.providerGuiRegistry()->createDataSourceWidget() instead + QGIS 3.10 - use QgsGui.sourceSelectProviderRegistry()->createDataSourceWidget() instead %End QList< QgsDataItemProvider * > dataItemProviders( const QString &providerKey ) const /Factory/; diff --git a/python/core/auto_generated/qgsproxyfeaturesink.sip.in b/python/core/auto_generated/qgsproxyfeaturesink.sip.in new file mode 100644 index 000000000000..af5b16d34cb3 --- /dev/null +++ b/python/core/auto_generated/qgsproxyfeaturesink.sip.in @@ -0,0 +1,58 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsproxyfeaturesink.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsProxyFeatureSink : QgsFeatureSink +{ +%Docstring +A simple feature sink which proxies feature addition on to another feature sink. + +This class is designed to allow factory methods which always return new QgsFeatureSink +objects. Since it is not always possible to create an entirely new QgsFeatureSink +(e.g. if the feature sink is a layer's data provider), a new QgsProxyFeatureSink +can instead be returned which forwards features on to the destination sink. The +proxy sink can be safely deleted without affecting the destination sink. + +.. versionadded:: 3.0 +%End + +%TypeHeaderCode +#include "qgsproxyfeaturesink.h" +%End + public: + + QgsProxyFeatureSink( QgsFeatureSink *sink ); +%Docstring +Constructs a new QgsProxyFeatureSink which forwards features onto a destination ``sink``. +%End + virtual bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = 0 ); + virtual bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = 0 ); + virtual bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = 0 ); + + QgsFeatureSink *destinationSink(); +%Docstring +Returns the destination QgsFeatureSink which the proxy will forward features to. +%End + +}; + + + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsproxyfeaturesink.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgsremappingproxyfeaturesink.sip.in b/python/core/auto_generated/qgsremappingproxyfeaturesink.sip.in new file mode 100644 index 000000000000..706e34464ef0 --- /dev/null +++ b/python/core/auto_generated/qgsremappingproxyfeaturesink.sip.in @@ -0,0 +1,213 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsremappingproxyfeaturesink.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsRemappingSinkDefinition +{ +%Docstring +Defines the parameters used to remap features when creating a QgsRemappingProxyFeatureSink. + +The definition includes parameters required to correctly map incoming features to the structure +of the destination sink, e.g. information about how to create output field values and how to transform +geometries to match the destination CRS. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsremappingproxyfeaturesink.h" +%End + public: + + QMap< QString, QgsProperty > fieldMap() const; +%Docstring +Returns the field mapping, which defines how to map the values from incoming features to destination +field values. + +Field values are mapped using a QgsProperty source object, which allows either direct field value to field value +mapping or use of QgsExpression expressions to transform values to the destination field. + +.. seealso:: :py:func:`setFieldMap` + +.. seealso:: :py:func:`addMappedField` +%End + + void setFieldMap( const QMap< QString, QgsProperty > &map ); +%Docstring +Sets the field mapping, which defines how to map the values from incoming features to destination +field values. + +Field values are mapped using a QgsProperty source object, which allows either direct field value to field value +mapping or use of QgsExpression expressions to transform values to the destination field. + +.. seealso:: :py:func:`fieldMap` + +.. seealso:: :py:func:`addMappedField` +%End + + void addMappedField( const QString &destinationField, const QgsProperty &property ); +%Docstring +Adds a mapping for a destination field. + +Field values are mapped using a QgsProperty source object, which allows either direct field value to field value +mapping or use of QgsExpression expressions to transform values to the destination field. + +.. seealso:: :py:func:`setFieldMap` + +.. seealso:: :py:func:`fieldMap` +%End + + QgsCoordinateReferenceSystem sourceCrs() const; +%Docstring +Returns the source CRS used for reprojecting incoming features to the sink's destination CRS. + +.. seealso:: :py:func:`setSourceCrs` +%End + + void setSourceCrs( const QgsCoordinateReferenceSystem &source ); +%Docstring +Sets the ``source`` crs used for reprojecting incoming features to the sink's destination CRS. + +.. seealso:: :py:func:`sourceCrs` +%End + + QgsCoordinateReferenceSystem destinationCrs() const; +%Docstring +Returns the destination CRS used for reprojecting incoming features to the sink's destination CRS. + +.. seealso:: :py:func:`setDestinationCrs` +%End + + void setDestinationCrs( const QgsCoordinateReferenceSystem &destination ); +%Docstring +Sets the ``destination`` crs used for reprojecting incoming features to the sink's destination CRS. + +.. seealso:: :py:func:`destinationCrs` +%End + + QgsWkbTypes::Type destinationWkbType() const; +%Docstring +Returns the WKB geometry type for the destination. + +.. seealso:: :py:func:`setDestinationWkbType` +%End + + void setDestinationWkbType( QgsWkbTypes::Type type ); +%Docstring +Sets the WKB geometry ``type`` for the destination. + +.. seealso:: :py:func:`setDestinationWkbType` +%End + + QgsFields destinationFields() const; +%Docstring +Returns the fields for the destination sink. + +.. seealso:: :py:func:`setDestinationFields` +%End + + void setDestinationFields( const QgsFields &fields ); +%Docstring +Sets the ``fields`` for the destination sink. + +.. seealso:: :py:func:`destinationFields` +%End + + QVariant toVariant() const; +%Docstring +Saves this remapping definition to a QVariantMap, wrapped in a QVariant. +You can use QgsXmlUtils.writeVariant to save it to an XML document. + +.. seealso:: :py:func:`loadVariant` +%End + + bool loadVariant( const QVariantMap &map ); +%Docstring +Loads this remapping definition from a QVariantMap, wrapped in a QVariant. +You can use QgsXmlUtils.readVariant to load it from an XML document. + +.. seealso:: :py:func:`toVariant` +%End + + bool operator==( const QgsRemappingSinkDefinition &other ) const; + bool operator!=( const QgsRemappingSinkDefinition &other ) const; + +}; + + + + +class QgsRemappingProxyFeatureSink : QgsFeatureSink +{ +%Docstring +A QgsFeatureSink which proxies incoming features to a destination feature sink, after applying +transformations and field value mappings. + +This sink allows for transformation of incoming features to match the requirements of storing +in an existing destination layer, e.g. by reprojecting the features to the destination's CRS +and by coercing geometries to the format required by the destination sink. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsremappingproxyfeaturesink.h" +%End + public: + + + QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink ); +%Docstring +Constructor for QgsRemappingProxyFeatureSink, using the specified ``mappingDefinition`` +to manipulate features before sending them to the destination ``sink``. +%End + + ~QgsRemappingProxyFeatureSink(); + + void setExpressionContext( const QgsExpressionContext &context ); +%Docstring +Sets the expression ``context`` to use when evaluating mapped field values. +%End + + void setTransformContext( const QgsCoordinateTransformContext &context ); +%Docstring +Sets the transform ``context`` to use when reprojecting features. +%End + + QgsFeatureList remapFeature( const QgsFeature &feature ) const; +%Docstring +Remaps a ``feature`` to a set of features compatible with the destination sink. +%End + + virtual bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = 0 ); + + virtual bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = 0 ); + + virtual bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = 0 ); + + + QgsFeatureSink *destinationSink(); +%Docstring +Returns the destination QgsFeatureSink which the proxy will forward features to. +%End + +}; + + + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgsremappingproxyfeaturesink.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgssnappingconfig.sip.in b/python/core/auto_generated/qgssnappingconfig.sip.in index 4563c10d41b5..c2839faeffd9 100644 --- a/python/core/auto_generated/qgssnappingconfig.sip.in +++ b/python/core/auto_generated/qgssnappingconfig.sip.in @@ -51,6 +51,13 @@ This is a container for configuration of the snapping of the project typedef QFlags SnappingTypeFlag; + enum ScaleDependencyMode + { + Disabled, + Global, + PerLayer + }; + static const QString snappingTypeFlagToString( SnappingTypeFlag type ); %Docstring Convenient method to returns the translated name of the enum type @@ -86,7 +93,7 @@ IndividualLayerSettings use the method with SnappingTypeFlag instead. %End - IndividualLayerSettings( bool enabled, SnappingTypeFlag type, double tolerance, QgsTolerance::UnitType units ); + IndividualLayerSettings( bool enabled, SnappingTypeFlag type, double tolerance, QgsTolerance::UnitType units, double minScale = 0.0, double maxScale = 0.0 ); %Docstring IndividualLayerSettings @@ -94,6 +101,8 @@ IndividualLayerSettings :param type: :param tolerance: :param units: +:param minScale: 0.0 disable scale limit +:param maxScale: 0.0 disable scale limit .. versionadded:: 3.12 %End @@ -166,6 +175,34 @@ Returns the type of units void setUnits( QgsTolerance::UnitType units ); %Docstring Sets the type of units +%End + + double minimumScale() const; +%Docstring +Returns minimum scale on which snapping is limited + +.. versionadded:: 3.14 +%End + + void setMinimumScale( double minScale ); +%Docstring +Sets the min scale value on which snapping is used, 0.0 disable scale limit + +.. versionadded:: 3.14 +%End + + double maximumScale() const; +%Docstring +Returns max scale on which snapping is limited + +.. versionadded:: 3.14 +%End + + void setMaximumScale( double maxScale ); +%Docstring +Sets the max scale value on which snapping is used, 0.0 disable scale limit + +.. versionadded:: 3.14 %End bool operator!= ( const QgsSnappingConfig::IndividualLayerSettings &other ) const; @@ -244,6 +281,48 @@ Returns the tolerance void setTolerance( double tolerance ); %Docstring Sets the tolerance +%End + + double minimumScale() const; +%Docstring +Returns the min scale + +.. versionadded:: 3.14 +%End + + void setMinimumScale( double minScale ); +%Docstring +Sets the min scale on which snapping is enabled, 0.0 disable scale limit + +.. versionadded:: 3.14 +%End + + double maximumScale() const; +%Docstring +Returns the max scale + +.. versionadded:: 3.14 +%End + + void setMaximumScale( double maxScale ); +%Docstring +Set the max scale on which snapping is enabled, 0.0 disable scale limit + +.. versionadded:: 3.14 +%End + + void setScaleDependencyMode( ScaleDependencyMode mode ); +%Docstring +Set the scale dependency mode + +.. versionadded:: 3.14 +%End + + ScaleDependencyMode scaleDependencyMode() const; +%Docstring +Returns the scale dependency mode + +.. versionadded:: 3.14 %End QgsTolerance::UnitType units() const; diff --git a/python/core/auto_generated/qgsstatisticalsummary.sip.in b/python/core/auto_generated/qgsstatisticalsummary.sip.in index ab16d31d90bc..afcbe9263368 100644 --- a/python/core/auto_generated/qgsstatisticalsummary.sip.in +++ b/python/core/auto_generated/qgsstatisticalsummary.sip.in @@ -263,20 +263,20 @@ or via setStatistics. double minority() const; %Docstring -Returns minority of values. The minority is the value with least occurrences in the list +Returns minority of values. The minority is the value with least occurrences in the list. This is only calculated if Statistic.Minority has been specified in the constructor -or via setStatistics. A NaN value may be returned if the minority cannot -be calculated. +or via setStatistics. If multiple values match, return the first value relative to the +initial values order. A NaN value may be returned if the minority cannot be calculated. .. seealso:: :py:func:`majority` %End double majority() const; %Docstring -Returns majority of values. The majority is the value with most occurrences in the list +Returns majority of values. The majority is the value with most occurrences in the list. This is only calculated if Statistic.Majority has been specified in the constructor -or via setStatistics. A NaN value may be returned if the majority cannot -be calculated. +or via setStatistics. If multiple values match, return the first value relative to the +initial values order. A NaN value may be returned if the minority cannot be calculated. .. seealso:: :py:func:`minority` %End diff --git a/python/core/auto_generated/qgsstringstatisticalsummary.sip.in b/python/core/auto_generated/qgsstringstatisticalsummary.sip.in index 382de1623c34..de9e7101fb2c 100644 --- a/python/core/auto_generated/qgsstringstatisticalsummary.sip.in +++ b/python/core/auto_generated/qgsstringstatisticalsummary.sip.in @@ -39,6 +39,8 @@ specifying the statistic in the constructor or via setStatistics(). MinimumLength, MaximumLength, MeanLength, + Minority, + Majority, All, }; typedef QFlags Statistics; @@ -213,6 +215,30 @@ Returns the maximum length of strings. Returns the mean length of strings. .. versionadded:: 3.0 +%End + + QString minority() const; +%Docstring +Returns the least common string. The minority is the value with least occurrences in the list +This is only calculated if Statistic.Minority has been specified in the constructor +or via setStatistics. If multiple values match, return the first value relative to the +initial values order. + +.. seealso:: :py:func:`majority` + +.. versionadded:: 3.14 +%End + + QString majority() const; +%Docstring +Returns the most common string. The majority is the value with most occurrences in the list +This is only calculated if Statistic.Majority has been specified in the constructor +or via setStatistics. If multiple values match, return the first value relative to the +initial values order. + +.. seealso:: :py:func:`minority` + +.. versionadded:: 3.14 %End static QString displayName( QgsStringStatisticalSummary::Statistic statistic ); diff --git a/python/core/auto_generated/qgsstringutils.sip.in b/python/core/auto_generated/qgsstringutils.sip.in index cf9358f75ac0..478f679e3095 100644 --- a/python/core/auto_generated/qgsstringutils.sip.in +++ b/python/core/auto_generated/qgsstringutils.sip.in @@ -248,6 +248,23 @@ so strings with similar sounds should be represented by the same Soundex code. :param string: input string :return: 4 letter Soundex code +%End + + static double fuzzyScore( const QString &candidate, const QString &search ); +%Docstring +Tests a ``candidate`` string to see how likely it is a match for +a specified ``search`` string. Values are normalized between 0 and 1. + +:param candidate: candidate string +:param search: search term string + +:return: Normalized value of how likely is the ``search`` to be in the ``candidate`` + +.. note:: + + Use this function only to calculate the fuzzy score between two strings and later compare these values, but do not depend on the actual numbers. They are implementation detail that may change in a future release. + +.. versionadded:: 3.14 %End static QString insertLinks( const QString &string, bool *foundLinks = 0 ); diff --git a/python/core/auto_generated/qgstaskmanager.sip.in b/python/core/auto_generated/qgstaskmanager.sip.in index 231e6ad48e41..f55dc361700f 100644 --- a/python/core/auto_generated/qgstaskmanager.sip.in +++ b/python/core/auto_generated/qgstaskmanager.sip.in @@ -47,6 +47,7 @@ clean up and terminate at the earliest possible convenience. enum Flag { CanCancel, + CancelWithoutPrompt, AllFlags, }; typedef QFlags Flags; diff --git a/python/core/auto_generated/qgstemporalutils.sip.in b/python/core/auto_generated/qgstemporalutils.sip.in new file mode 100644 index 000000000000..375bfdc309bc --- /dev/null +++ b/python/core/auto_generated/qgstemporalutils.sip.in @@ -0,0 +1,43 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgstemporalutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsTemporalUtils +{ +%Docstring +Contains utility methods for working with temporal layers and projects. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstemporalutils.h" +%End + public: + + static QgsDateTimeRange calculateTemporalRangeForProject( QgsProject *project ); +%Docstring +Calculates the temporal range for a ``project``. + +This method considers the temporal range available from layers contained within the project and +returns the maximal combined temporal extent of these layers. +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgstemporalutils.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgstiles.sip.in b/python/core/auto_generated/qgstiles.sip.in new file mode 100644 index 000000000000..befad4c77ee3 --- /dev/null +++ b/python/core/auto_generated/qgstiles.sip.in @@ -0,0 +1,144 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgstiles.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsTileXYZ +{ +%Docstring +Stores coordinates of a tile in a tile matrix set. Tile matrix is identified +by the zoomLevel(), and the position within tile matrix is given by column() +and row(). + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstiles.h" +%End + public: + QgsTileXYZ( int tc = -1, int tr = -1, int tz = -1 ); +%Docstring +Constructs a tile identifier from given column, row and zoom level indices +%End + + int column() const; +%Docstring +Returns tile's column index (X) +%End + int row() const; +%Docstring +Returns tile's row index (Y) +%End + int zoomLevel() const; +%Docstring +Returns tile's zoom level (Z) +%End + + QString toString() const; +%Docstring +Returns tile coordinates in a formatted string +%End + +}; + + +class QgsTileRange +{ +%Docstring +Range of tiles in a tile matrix to be rendered. The selection is rectangular, +given by start/end row and column numbers. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstiles.h" +%End + public: + QgsTileRange( int c1 = -1, int c2 = -1, int r1 = -1, int r2 = -1 ); +%Docstring +Constructs a range of tiles from given span of columns and rows +%End + bool isValid() const; +%Docstring +Returns whether the range is valid (when all row/column numbers are not negative) +%End + + int startColumn() const; +%Docstring +Returns index of the first column in the range +%End + int endColumn() const; +%Docstring +Returns index of the last column in the range +%End + int startRow() const; +%Docstring +Returns index of the first row in the range +%End + int endRow() const; +%Docstring +Returns index of the last row in the range +%End + +}; + + +class QgsTileMatrix +{ +%Docstring +Defines a matrix of tiles for a single zoom level: it is defined by its size (width * height) +and map extent that it covers. + +Please note that we follow the XYZ convention of X/Y axes, i.e. top-left tile has [0,0] coordinate +(which is different from TMS convention where bottom-left tile has [0,0] coordinate). + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstiles.h" +%End + public: + + static QgsTileMatrix fromWebMercator( int mZoomLevel ); +%Docstring +Returns a tile matrix for the usual web mercator +%End + + QgsRectangle tileExtent( QgsTileXYZ id ) const; +%Docstring +Returns extent of the given tile in this matrix +%End + + QgsPointXY tileCenter( QgsTileXYZ id ) const; +%Docstring +Returns center of the given tile in this matrix +%End + + QgsTileRange tileRangeFromExtent( const QgsRectangle &mExtent ); +%Docstring +Returns tile range that fully covers the given extent +%End + + QPointF mapToTileCoordinates( const QgsPointXY &mapPoint ) const; +%Docstring +Returns row/column coordinates (floating point number) from the given point in map coordinates +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgstiles.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgsvectorlayerjoininfo.sip.in b/python/core/auto_generated/qgsvectorlayerjoininfo.sip.in index c19b53d08650..ef842fc66018 100644 --- a/python/core/auto_generated/qgsvectorlayerjoininfo.sip.in +++ b/python/core/auto_generated/qgsvectorlayerjoininfo.sip.in @@ -77,9 +77,11 @@ Returns prefix of fields from the joined layer. If ``None``, joined layer's name %Docstring Sets whether values from the joined layer should be cached in memory to speed up lookups %End + bool isUsingMemoryCache() const; %Docstring -Returns whether values from the joined layer should be cached in memory to speed up lookups +Returns whether values from the joined layer should be cached in memory to speed up lookups. +Will return false if upsertOnEdit is enabled. %End bool isDynamicFormEnabled() const; diff --git a/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in new file mode 100644 index 000000000000..a7a0e0eb0cf0 --- /dev/null +++ b/python/core/auto_generated/raster/qgsrastercontourrenderer.sip.in @@ -0,0 +1,125 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastercontourrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsRasterContourRenderer : QgsRasterRenderer +{ +%Docstring +Raster renderer that generates contours on the fly for a source raster band. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsrastercontourrenderer.h" +%End + public: + explicit QgsRasterContourRenderer( QgsRasterInterface *input ); +%Docstring +Creates a contour renderer +%End + ~QgsRasterContourRenderer(); + + + virtual QgsRasterContourRenderer *clone() const /Factory/; + +%Docstring +QgsRasterContourRenderer cannot be copied. Use clone() instead. +%End + + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) /Factory/; +%Docstring +Creates an instance of the renderer based on definition from XML (used by renderer registry) +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &parentElem ) const; + + + virtual QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = 0 ) /Factory/; + + + virtual QList usesBands() const; + + + + int inputBand() const; +%Docstring +Returns the number of the input raster band +%End + void setInputBand( int band ); +%Docstring +Sets the number of the input raster band +%End + + double contourInterval() const; +%Docstring +Returns the interval of contour lines generation +%End + void setContourInterval( double interval ); +%Docstring +Sets the interval of contour lines generation +%End + + QgsLineSymbol *contourSymbol() const; +%Docstring +Returns the symbol used for contour lines +%End + void setContourSymbol( QgsLineSymbol *symbol /Transfer/ ); +%Docstring +Sets the symbol used for contour lines. Takes ownership of the passed symbol +%End + + double contourIndexInterval() const; +%Docstring +Returns the interval of index contour lines (index contour lines are typical further apart and with a wider line symbol) +%End + void setContourIndexInterval( double interval ); +%Docstring +Sets the interval of index contour lines (index contour lines are typical further apart and with a wider line symbol) +%End + + QgsLineSymbol *contourIndexSymbol() const; +%Docstring +Returns the symbol of index contour lines +%End + void setContourIndexSymbol( QgsLineSymbol *symbol /Transfer/ ); +%Docstring +Sets the symbol of index contour lines +%End + + double downscale() const; +%Docstring +Returns by how much the renderer will scale down the request to the data provider. +For example, for a raster block 1000x500 with downscale 10, the renderer will request raster 100x50 from provider. +Higher downscale makes contour lines more simplified (at the expense of losing some detail). +The value of one means there will be no downscaling. +%End + + void setDownscale( double scale ); +%Docstring +Sets by how much the renderer will scale down the request to the data provider. + +.. seealso:: :py:func:`downscale` +%End + + private: + QgsRasterContourRenderer( const QgsRasterContourRenderer & ); + const QgsRasterContourRenderer &operator=( const QgsRasterContourRenderer & ); +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/raster/qgsrastercontourrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/raster/qgsrasterdataprovider.sip.in b/python/core/auto_generated/raster/qgsrasterdataprovider.sip.in index 77e769019f92..0d32757918c7 100644 --- a/python/core/auto_generated/raster/qgsrasterdataprovider.sip.in +++ b/python/core/auto_generated/raster/qgsrasterdataprovider.sip.in @@ -117,6 +117,13 @@ It makes no sense to set input on provider */ Returns data type for the band specified by number %End + virtual QgsFields fields() const; +%Docstring +Returns the fields of the raster layer for data providers that expose them, +the default implementation returns an empty list. + +.. versionadded:: 3.14 +%End virtual Qgis::DataType sourceDataType( int bandNo ) const = 0; %Docstring diff --git a/python/core/auto_generated/raster/qgsrasterdataprovidertemporalcapabilities.sip.in b/python/core/auto_generated/raster/qgsrasterdataprovidertemporalcapabilities.sip.in index f2993c5d2804..1d05bc0ca1d8 100644 --- a/python/core/auto_generated/raster/qgsrasterdataprovidertemporalcapabilities.sip.in +++ b/python/core/auto_generated/raster/qgsrasterdataprovidertemporalcapabilities.sip.in @@ -38,6 +38,8 @@ The ``enabled`` argument specifies whether the data provider has temporal capabi MatchUsingWholeRange, MatchExactUsingStartOfRange, MatchExactUsingEndOfRange, + FindClosestMatchToStartOfRange, + FindClosestMatchToEndOfRange }; IntervalHandlingMethod intervalHandlingMethod() const; @@ -90,60 +92,6 @@ extent of datetime values available for reference temporal ranges from the provi %Docstring Returns the requested temporal range. Intended to be used by the provider in fetching data. -%End - - void setRequestedReferenceTemporalRange( const QgsDateTimeRange &range ); -%Docstring -Sets the requested reference temporal ``range`` to retrieve when -returning data from the associated data provider. - -.. note:: - - this is not normally manually set, and is intended for use by - QgsRasterLayerRenderer to automatically set the requested temporal range - on a clone of the data provider during a render job. - -.. seealso:: :py:func:`requestedReferenceTemporalRange` -%End - - const QgsDateTimeRange &requestedReferenceTemporalRange() const; -%Docstring -Returns the requested reference temporal range. -Intended to be used by the provider in fetching data. - -.. seealso:: :py:func:`setRequestedReferenceTemporalRange` -%End - - void setEnableTime( bool enabled ); -%Docstring -Sets the time enabled status. -This enables whether time part in the temporal range should be -used when updated the temporal range of these capabilities. - -This is useful in some temporal layers who use dates only. - -.. seealso:: :py:func:`isTimeEnabled` -%End - - bool isTimeEnabled() const; -%Docstring -Returns the temporal property status. - -.. seealso:: :py:func:`setEnableTime` -%End - - void setReferenceEnable( bool enabled ); -%Docstring -Sets the usage status of the reference range. - -.. seealso:: :py:func:`isReferenceEnable` -%End - - bool isReferenceEnable() const; -%Docstring -Returns the enabled status of the reference range. - -.. seealso:: :py:func:`setReferenceEnable` %End }; diff --git a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in index d29f3dbcd08d..66f2032ce92e 100644 --- a/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in +++ b/python/core/auto_generated/raster/qgsrasterlayertemporalproperties.sip.in @@ -93,70 +93,6 @@ Returns the fixed temporal range for the layer. QgsRasterLayerTemporalProperties.ModeFixedTemporalRange .. seealso:: :py:func:`setFixedTemporalRange` -%End - - void setFixedReferenceTemporalRange( const QgsDateTimeRange &range ); -%Docstring -Sets a fixed reference temporal ``range`` to apply to the whole layer. All bands from -the raster layer will be rendered whenever the current datetime range of -a render context intersects the specified ``range``. - -.. warning:: - - This setting is only effective when mode() is - QgsRasterLayerTemporalProperties.ModeFixedTemporalRange - -.. seealso:: :py:func:`fixedReferenceTemporalRange` -%End - - const QgsDateTimeRange &fixedReferenceTemporalRange() const; -%Docstring -Returns the fixed reference temporal range for the layer. - -.. warning:: - - To be used only when mode() is - QgsRasterLayerTemporalProperties.ModeFixedTemporalRange - -.. seealso:: :py:func:`setFixedReferenceTemporalRange` -%End - - void setTemporalRange( const QgsDateTimeRange &dateTimeRange ); -%Docstring -Sets the current active datetime range for the temporal properties. - -.. note:: - - This can be set by user, through raster layer properties widget. - -.. seealso:: :py:func:`temporalRange` -%End - - const QgsDateTimeRange &temporalRange() const; -%Docstring -Returns the current active datetime range for these temporal properties. - -.. seealso:: :py:func:`setTemporalRange` -%End - - void setReferenceTemporalRange( const QgsDateTimeRange &dateTimeRange ); -%Docstring -Sets the current active reference datetime range for the temporal properties. - -This will be used by bi-temporal data. - -.. note:: - - This can be set by user, through raster layer properties widget. - -.. seealso:: :py:func:`referenceTemporalRange` -%End - - const QgsDateTimeRange &referenceTemporalRange() const; -%Docstring -Returns the current active reference datetime range for these temporal properties. - -.. seealso:: :py:func:`setReferenceTemporalRange` %End virtual QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ); diff --git a/python/core/auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip.in index 916917e0739a..6801cc345f79 100644 --- a/python/core/auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip.in +++ b/python/core/auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip.in @@ -26,7 +26,16 @@ Double box with alternating colors. Constructor for QgsDoubleBoxScaleBarRenderer. %End - virtual QString name() const; + virtual QString id() const; + + virtual QString visibleName() const; + + virtual Flags flags() const; + + virtual int sortKey() const; + + virtual QgsDoubleBoxScaleBarRenderer *clone() const /Factory/; + virtual void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, diff --git a/python/core/auto_generated/scalebar/qgshollowscalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgshollowscalebarrenderer.sip.in new file mode 100644 index 000000000000..0e5972af0f87 --- /dev/null +++ b/python/core/auto_generated/scalebar/qgshollowscalebarrenderer.sip.in @@ -0,0 +1,55 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/scalebar/qgshollowscalebarrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsHollowScaleBarRenderer: QgsScaleBarRenderer +{ +%Docstring +Scalebar style that draws a single box with alternating color for the segments, with horizontal lines through +alternating segments. AKA "South African" style. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgshollowscalebarrenderer.h" +%End + public: + + QgsHollowScaleBarRenderer(); +%Docstring +Constructor for QgsHollowScaleBarRenderer. +%End + + virtual QString id() const; + + virtual QString visibleName() const; + + virtual Flags flags() const; + + virtual int sortKey() const; + + virtual QgsHollowScaleBarRenderer *clone() const /Factory/; + + + virtual void draw( QgsRenderContext &context, + const QgsScaleBarSettings &settings, + const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const; + virtual bool applyDefaultSettings( QgsScaleBarSettings &settings ) const; + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/scalebar/qgshollowscalebarrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/scalebar/qgsnumericscalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgsnumericscalebarrenderer.sip.in index fea56cbca065..b46f996c3149 100644 --- a/python/core/auto_generated/scalebar/qgsnumericscalebarrenderer.sip.in +++ b/python/core/auto_generated/scalebar/qgsnumericscalebarrenderer.sip.in @@ -26,15 +26,28 @@ A scale bar style that draws text in the form of '1:XXXXX'. Constructor for QgsNumericScaleBarRenderer. %End - virtual QString name() const; + virtual QString id() const; + + virtual QString visibleName() const; + + virtual int sortKey() const; + + virtual Flags flags() const; + + virtual QgsNumericScaleBarRenderer *clone() const /Factory/; + virtual void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const; - virtual QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, + virtual QSizeF calculateBoxSize( QgsRenderContext &context, + const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const; + virtual QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const /Deprecated/; + + }; /************************************************************************ diff --git a/python/core/auto_generated/scalebar/qgsscalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgsscalebarrenderer.sip.in index 34fb04002dd0..33de3a36d53e 100644 --- a/python/core/auto_generated/scalebar/qgsscalebarrenderer.sip.in +++ b/python/core/auto_generated/scalebar/qgsscalebarrenderer.sip.in @@ -36,15 +36,68 @@ custom labeling. }; + enum class Flag + { + FlagUsesLineSymbol, + FlagUsesFillSymbol, + FlagUsesAlternateFillSymbol, + FlagRespectsUnits, + FlagRespectsMapUnitsPerScaleBarUnit, + FlagUsesUnitLabel, + FlagUsesSegments, + FlagUsesLabelBarSpace, + FlagUsesLabelVerticalPlacement, + FlagUsesLabelHorizontalPlacement, + FlagUsesAlignment, + }; + typedef QFlags Flags; + + QgsScaleBarRenderer(); %Docstring Constructor for QgsScaleBarRenderer. %End virtual ~QgsScaleBarRenderer(); - virtual QString name() const = 0; + QString name() const /Deprecated/; %Docstring Returns the unique name for this style. + +.. deprecated:: + use id() instead +%End + + virtual QString id() const = 0; +%Docstring +Returns the unique ID for this renderer. + +.. versionadded:: 3.14 +%End + + virtual QString visibleName() const = 0; +%Docstring +Returns the user friendly, translated name for the renderer. + +.. versionadded:: 3.14 +%End + + virtual Flags flags() const; +%Docstring +Returns the scalebar rendering flags, which dictates the renderer's behavior. + +.. versionadded:: 3.14 +%End + + virtual int sortKey() const; +%Docstring +Returns a sorting key value, where renderers with a lower sort key will be shown earlier in lists. + +Generally, subclasses should return QgsScaleBarRenderer.sortKey() as their sorting key. +%End + + virtual QgsScaleBarRenderer *clone() const = 0 /Factory/; +%Docstring +Returns a clone of the renderer. The caller takes ownership of the returned value. %End virtual void draw( QgsRenderContext &context, @@ -54,10 +107,31 @@ Returns the unique name for this style. Draws the scalebar using the specified ``settings`` and ``scaleContext`` to a destination render ``context``. %End - virtual QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, + virtual QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, + const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const /Deprecated/; +%Docstring +Calculates the required box size (in millimeters) for a scalebar using the specified ``settings`` and ``scaleContext``. + +.. deprecated:: + Use the version with a QgsRenderContext instead. +%End + + virtual QSizeF calculateBoxSize( QgsRenderContext &context, + const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const; %Docstring Calculates the required box size (in millimeters) for a scalebar using the specified ``settings`` and ``scaleContext``. + +.. versionadded:: 3.14 +%End + + virtual bool applyDefaultSettings( QgsScaleBarSettings &settings ) const; +%Docstring +Applies any default settings relating to the scalebar to the passed ``settings`` object. + +Returns ``True`` if settings were applied. + +.. versionadded:: 3.14 %End protected: @@ -89,9 +163,19 @@ Returns the x-offset (in render context painter units) used for the first label .. versionadded:: 3.2 %End - QList segmentPositions( const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const; + QList segmentPositions( const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const /Deprecated/; %Docstring Returns a list of positions for each segment within the scalebar. + +.. deprecated:: + use the version with a QgsRenderContext instead +%End + + QList segmentPositions( QgsRenderContext &context, const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const; +%Docstring +Returns a list of positions for each segment within the scalebar. + +.. versionadded:: 3.14 %End QList segmentWidths( const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const; @@ -101,6 +185,9 @@ Returns a list of widths of each segment of the scalebar. }; +QFlags operator|(QgsScaleBarRenderer::Flag f1, QFlags f2); + + /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/core/auto_generated/scalebar/qgsscalebarrendererregistry.sip.in b/python/core/auto_generated/scalebar/qgsscalebarrendererregistry.sip.in new file mode 100644 index 000000000000..545e7b3fb187 --- /dev/null +++ b/python/core/auto_generated/scalebar/qgsscalebarrendererregistry.sip.in @@ -0,0 +1,84 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/scalebar/qgsscalebarrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsScaleBarRendererRegistry +{ +%Docstring +The QgsScaleBarRendererRegistry manages registered scalebar renderers. + +A reference to the QgsScaleBarRendererRegistry can be obtained from +:py:func:`QgsApplication.scalebarRendererRegistry()` + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsscalebarrendererregistry.h" +%End + public: + + explicit QgsScaleBarRendererRegistry(); +%Docstring +You should not normally need to create your own scalebar renderer registry. + +Use the one provided by `QgsApplication.scalebarRendererRegistry()` instead. +%End + ~QgsScaleBarRendererRegistry(); + + QStringList renderers() const; +%Docstring +Returns a list of the renderer ids currently contained in the registry. +%End + + QStringList sortedRendererList() const; +%Docstring +Returns a list of the renderer ids currently contained in the registry, +sorted in an order respecting the renderer's sort keys and display strings. +%End + + void addRenderer( QgsScaleBarRenderer *renderer /Transfer/ ); +%Docstring +Adds a new ``renderer`` to the registry. + +Ownership is transferred to the registry. +%End + + void removeRenderer( const QString &id ); +%Docstring +Removes the renderer with matching ``id`` from the registry. +%End + + QgsScaleBarRenderer *renderer( const QString &id ) const /TransferBack/; +%Docstring +Creates a new scalebar renderer by ``id``. If there is no such ``id`` registered, +``None`` will be returned instead. + +The caller takes ownership of the returned object. +%End + + QString visibleName( const QString &id ) const; +%Docstring +Returns the translated, user-visible name for the renderer with matching ``id``. +%End + + int sortKey( const QString &id ) const; +%Docstring +Returns the sorting key for the renderer with matching ``id``. +%End + +}; +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/scalebar/qgsscalebarrendererregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/scalebar/qgsscalebarsettings.sip.in b/python/core/auto_generated/scalebar/qgsscalebarsettings.sip.in index 6bf986f48548..5724292010b7 100644 --- a/python/core/auto_generated/scalebar/qgsscalebarsettings.sip.in +++ b/python/core/auto_generated/scalebar/qgsscalebarsettings.sip.in @@ -9,7 +9,6 @@ - class QgsScaleBarSettings { %Docstring @@ -291,87 +290,189 @@ Sets the ``color`` used for drawing text in the scalebar. use textFormat() instead %End - QColor fillColor() const; + QColor fillColor() const /Deprecated/; %Docstring Returns the color used for fills in the scalebar. .. seealso:: :py:func:`setFillColor` .. seealso:: :py:func:`fillColor2` + +.. deprecated:: + use fillSymbol() instead. %End - void setFillColor( const QColor &color ); + void setFillColor( const QColor &color ) /Deprecated/; %Docstring Sets the ``color`` used for fills in the scalebar. .. seealso:: :py:func:`fillColor` .. seealso:: :py:func:`setFillColor2` + +.. deprecated:: + use setFillSymbol() instead. %End - QColor fillColor2() const; + QColor fillColor2() const /Deprecated/; %Docstring Returns the secondary color used for fills in the scalebar. .. seealso:: :py:func:`setFillColor2` .. seealso:: :py:func:`fillColor` + +.. deprecated:: + use alternateFillSymbol() instead %End - void setFillColor2( const QColor &color ); + void setFillColor2( const QColor &color ) /Deprecated/; %Docstring Sets the secondary ``color`` used for fills in the scalebar. .. seealso:: :py:func:`fillColor2` .. seealso:: :py:func:`setFillColor2` + +.. deprecated:: + use setAlternateFillSymbol() instead. %End - QColor lineColor() const; + QColor lineColor() const /Deprecated/; %Docstring Returns the color used for lines in the scalebar. .. seealso:: :py:func:`setLineColor` + +.. deprecated:: + use lineSymbol() instead. %End - void setLineColor( const QColor &color ); + void setLineColor( const QColor &color ) /Deprecated/; %Docstring Sets the ``color`` used for lines in the scalebar. .. seealso:: :py:func:`lineColor` + +.. deprecated:: + use setLineSymbol() instead. %End - double lineWidth() const; + double lineWidth() const /Deprecated/; %Docstring Returns the line width in millimeters for lines in the scalebar. .. seealso:: :py:func:`setLineWidth` + +.. deprecated:: + use lineSymbol() instead. %End - void setLineWidth( double width ); + void setLineWidth( double width ) /Deprecated/; %Docstring Sets the line ``width`` in millimeters for lines in the scalebar. .. seealso:: :py:func:`lineWidth` + +.. deprecated:: + use setLineSymbol() instead. %End - QPen pen() const; + QPen pen() const /Deprecated/; %Docstring Returns the pen used for drawing outlines in the scalebar. .. seealso:: :py:func:`setPen` .. seealso:: :py:func:`brush` + +.. deprecated:: + use lineSymbol() instead. %End - void setPen( const QPen &pen ); + void setPen( const QPen &pen ) /Deprecated/; %Docstring Sets the pen used for drawing outlines in the scalebar. .. seealso:: :py:func:`pen` + +.. deprecated:: + use setLineSymbol() instead. +%End + + QgsLineSymbol *lineSymbol() const; +%Docstring +Returns the line symbol used to render the scalebar (only used for some scalebar types). + +Ownership is not transferred. + +.. seealso:: :py:func:`setLineSymbol` + +.. versionadded:: 3.14 %End - QBrush brush() const; + void setLineSymbol( QgsLineSymbol *symbol /Transfer/ ); +%Docstring +Sets the line ``symbol`` used to render the scalebar (only used for some scalebar types). Ownership of ``symbol`` is +transferred to the scalebar. + +.. seealso:: :py:func:`lineSymbol` + +.. versionadded:: 3.14 +%End + + QgsFillSymbol *fillSymbol() const; +%Docstring +Returns the primary fill symbol used to render the scalebar (only used for some scalebar types). + +Ownership is not transferred. + +.. seealso:: :py:func:`setFillSymbol` + +.. seealso:: :py:func:`alternateFillSymbol` + +.. versionadded:: 3.14 +%End + + void setFillSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the primary fill ``symbol`` used to render the scalebar (only used for some scalebar types). Ownership of ``symbol`` is +transferred to the scalebar. + +.. seealso:: :py:func:`fillSymbol` + +.. seealso:: :py:func:`setAlternateFillSymbol` + +.. versionadded:: 3.14 +%End + + + QgsFillSymbol *alternateFillSymbol() const; +%Docstring +Returns the secondary fill symbol used to render the scalebar (only used for some scalebar types). + +Ownership is not transferred. + +.. seealso:: :py:func:`setAlternateFillSymbol` + +.. seealso:: :py:func:`fillSymbol` + +.. versionadded:: 3.14 +%End + + void setAlternateFillSymbol( QgsFillSymbol *symbol /Transfer/ ); +%Docstring +Sets the secondary fill ``symbol`` used to render the scalebar (only used for some scalebar types). Ownership of ``symbol`` is +transferred to the scalebar. + +.. seealso:: :py:func:`alternateFillSymbol` + +.. seealso:: :py:func:`setFillSymbol` + +.. versionadded:: 3.14 +%End + + QBrush brush() const /Deprecated/; %Docstring Returns the primary brush used for filling the scalebar. @@ -380,16 +481,22 @@ Returns the primary brush used for filling the scalebar. .. seealso:: :py:func:`brush2` .. seealso:: :py:func:`pen` + +.. deprecated:: + use fillSymbol() instead %End - void setBrush( const QBrush &brush ); + void setBrush( const QBrush &brush ) /Deprecated/; %Docstring Sets the primary brush used for filling the scalebar. .. seealso:: :py:func:`brush` + +.. deprecated:: + use setFillSymbol() instead %End - QBrush brush2() const; + QBrush brush2() const /Deprecated/; %Docstring Returns the secondary brush for the scalebar. This is used for alternating color style scalebars, such as single and double box styles. @@ -397,13 +504,19 @@ as single and double box styles. .. seealso:: :py:func:`setBrush2` .. seealso:: :py:func:`brush` + +.. deprecated:: + use alternateFillSymbol() instead %End - void setBrush2( const QBrush &brush ); + void setBrush2( const QBrush &brush ) /Deprecated/; %Docstring Sets the secondary brush used for filling the scalebar. .. seealso:: :py:func:`brush` + +.. deprecated:: + use setAlternateFillSymbol() instead %End double height() const; @@ -498,32 +611,44 @@ Sets the scalebar ``alignment``. .. seealso:: :py:func:`alignment` %End - Qt::PenJoinStyle lineJoinStyle() const; + Qt::PenJoinStyle lineJoinStyle() const /Deprecated/; %Docstring Returns the join style used for drawing lines in the scalebar. .. seealso:: :py:func:`setLineJoinStyle` + +.. deprecated:: + use lineSymbol() instead %End - void setLineJoinStyle( Qt::PenJoinStyle style ); + void setLineJoinStyle( Qt::PenJoinStyle style ) /Deprecated/; %Docstring Sets the join ``style`` used when drawing the lines in the scalebar .. seealso:: :py:func:`lineJoinStyle` + +.. deprecated:: + use setLineSymbol() instead %End - Qt::PenCapStyle lineCapStyle() const; + Qt::PenCapStyle lineCapStyle() const /Deprecated/; %Docstring Returns the cap style used for drawing lines in the scalebar. .. seealso:: :py:func:`setLineCapStyle` + +.. deprecated:: + use lineSymbol() instead %End - void setLineCapStyle( Qt::PenCapStyle style ); + void setLineCapStyle( Qt::PenCapStyle style ) /Deprecated/; %Docstring Sets the cap ``style`` used when drawing the lines in the scalebar. .. seealso:: :py:func:`lineCapStyle` + +.. deprecated:: + use setLineSymbol() instead %End const QgsNumericFormat *numericFormat() const; diff --git a/python/core/auto_generated/scalebar/qgssingleboxscalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgssingleboxscalebarrenderer.sip.in index 886ecc3dd737..33168ab3ee95 100644 --- a/python/core/auto_generated/scalebar/qgssingleboxscalebarrenderer.sip.in +++ b/python/core/auto_generated/scalebar/qgssingleboxscalebarrenderer.sip.in @@ -27,7 +27,16 @@ color for the segments. Constructor for QgsSingleBoxScaleBarRenderer. %End - virtual QString name() const; + virtual QString id() const; + + virtual QString visibleName() const; + + virtual int sortKey() const; + + virtual Flags flags() const; + + virtual QgsSingleBoxScaleBarRenderer *clone() const /Factory/; + virtual void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, diff --git a/python/core/auto_generated/scalebar/qgssteppedlinescalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgssteppedlinescalebarrenderer.sip.in new file mode 100644 index 000000000000..37e4dde8fdc5 --- /dev/null +++ b/python/core/auto_generated/scalebar/qgssteppedlinescalebarrenderer.sip.in @@ -0,0 +1,52 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/scalebar/qgssteppedlinescalebarrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsSteppedLineScaleBarRenderer: QgsScaleBarRenderer +{ +%Docstring +Scalebar style that draws a stepped line representation of a scalebar. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgssteppedlinescalebarrenderer.h" +%End + public: + + QgsSteppedLineScaleBarRenderer(); +%Docstring +Constructor for QgsSteppedLineScaleBarRenderer. +%End + + virtual QString id() const; + + virtual QString visibleName() const; + + virtual int sortKey() const; + + virtual Flags flags() const; + + virtual QgsSteppedLineScaleBarRenderer *clone() const /Factory/; + + + virtual void draw( QgsRenderContext &context, + const QgsScaleBarSettings &settings, + const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const; + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/scalebar/qgssteppedlinescalebarrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/scalebar/qgsticksscalebarrenderer.sip.in b/python/core/auto_generated/scalebar/qgsticksscalebarrenderer.sip.in index 30e6f95c2a24..25801851f446 100644 --- a/python/core/auto_generated/scalebar/qgsticksscalebarrenderer.sip.in +++ b/python/core/auto_generated/scalebar/qgsticksscalebarrenderer.sip.in @@ -28,12 +28,20 @@ A scale bar that draws segments using short ticks. TicksMiddle, }; - QgsTicksScaleBarRenderer(); + QgsTicksScaleBarRenderer( TickPosition position = TicksMiddle ); %Docstring Constructor for QgsTicksScaleBarRenderer. %End - virtual QString name() const; + virtual QString id() const; + + virtual QString visibleName() const; + + virtual int sortKey() const; + + virtual Flags flags() const; + + virtual QgsTicksScaleBarRenderer *clone() const /Factory/; virtual void draw( QgsRenderContext &context, diff --git a/python/core/auto_generated/symbology/qgsmarkersymbollayer.sip.in b/python/core/auto_generated/symbology/qgsmarkersymbollayer.sip.in index da85da0591e5..6188fd48dcde 100644 --- a/python/core/auto_generated/symbology/qgsmarkersymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgsmarkersymbollayer.sip.in @@ -877,8 +877,6 @@ class QgsFontMarkerSymbolLayer : QgsMarkerSymbolLayer Constructs a font marker symbol layer. %End - ~QgsFontMarkerSymbolLayer(); - static QgsSymbolLayer *create( const QgsStringMap &properties = QgsStringMap() ) /Factory/; %Docstring @@ -925,6 +923,24 @@ Returns the font family name for the associated font which will be used to rende Sets the font ``family`` for the font which will be used to render the point. .. seealso:: :py:func:`fontFamily` +%End + + QString fontStyle() const; +%Docstring +Returns the font style for the associated font which will be used to render the point. + +.. seealso:: :py:func:`setFontStyle` + +.. versionadded:: 3.14 +%End + + void setFontStyle( const QString &style ); +%Docstring +Sets the font ``style`` for the font which will be used to render the point. + +.. seealso:: :py:func:`fontStyle` + +.. versionadded:: 3.14 %End QString character() const; @@ -1043,10 +1059,6 @@ Sets the stroke join ``style``. virtual QRectF bounds( QPointF point, QgsSymbolRenderContext &context ); - protected: - - - }; diff --git a/python/core/auto_generated/symbology/qgsstyle.sip.in b/python/core/auto_generated/symbology/qgsstyle.sip.in index 97223e0dea57..26dea77bc1c7 100644 --- a/python/core/auto_generated/symbology/qgsstyle.sip.in +++ b/python/core/auto_generated/symbology/qgsstyle.sip.in @@ -353,6 +353,15 @@ Removes all tags for the specified symbol or colorramp bool removeSymbol( const QString &name ); %Docstring Removes symbol from style (and delete it) +%End + + bool renameEntity( StyleEntity type, const QString &oldName, const QString &newName ); +%Docstring +Renames an entity of the specified ``type`` from ``oldName`` to ``newName``. + +Returns ``True`` if the entity was successfully renamed. + +.. versionadded:: 3.14 %End bool renameSymbol( const QString &oldName, const QString &newName ); @@ -744,6 +753,36 @@ Emitted whenever an ``entity``'s tags are changed. Emitted whenever an ``entity`` is either favorited or un-favorited. .. versionadded:: 3.4 +%End + + void entityAdded( QgsStyle::StyleEntity entity, const QString &name ); +%Docstring +Emitted every time a new entity has been added to the database. + +.. versionadded:: 3.14 +%End + + void entityRemoved( QgsStyle::StyleEntity entity, const QString &name ); +%Docstring +Emitted whenever an entity of the specified type is removed from the style and the database +has been updated as a result. + +.. versionadded:: 3.14 +%End + + void entityRenamed( QgsStyle::StyleEntity entity, const QString &oldName, const QString &newName ); +%Docstring +Emitted whenever a entity of the specified type has been renamed from ``oldName`` to ``newName`` + +.. versionadded:: 3.14 +%End + + void entityChanged( QgsStyle::StyleEntity entity, const QString &name ); +%Docstring +Emitted whenever an entity's definition is changed. This does not include +name or tag changes. + +.. versionadded:: 3.14 %End void symbolRemoved( const QString &name ); diff --git a/python/core/auto_generated/symbology/qgssymbol.sip.in b/python/core/auto_generated/symbology/qgssymbol.sip.in index 5831f506cd29..9d52954af1c4 100644 --- a/python/core/auto_generated/symbology/qgssymbol.sip.in +++ b/python/core/auto_generated/symbology/qgssymbol.sip.in @@ -272,7 +272,8 @@ layer. .. seealso:: :py:func:`setColor` %End - void drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext *customContext = 0, bool selected = false, const QgsExpressionContext *expressionContext = 0 ); + void drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext *customContext = 0, bool selected = false, const QgsExpressionContext *expressionContext = 0, + const QgsLegendPatchShape *patchShape = 0 ); %Docstring Draws an icon of the symbol that occupies an area given by ``size`` using the specified ``painter``. @@ -284,6 +285,7 @@ matches the settings from that context. :param customContext: the context in which the rendering happens :param selected: set to ``True`` to render the symbol in a selected state :param expressionContext: optional custom expression context +:param patchShape: optional patch shape to use for symbol preview. If not specified a default shape will be used instead. .. seealso:: :py:func:`exportImage` @@ -613,6 +615,8 @@ Constructor for QgsSymbolRenderContext :param mapUnitScale: %End + ~QgsSymbolRenderContext(); + QgsRenderContext &renderContext(); %Docstring @@ -800,6 +804,24 @@ Set an expression scope for this symbol. Will take ownership. :param contextScope: An expression scope for details about this symbol +%End + + const QgsLegendPatchShape *patchShape() const; +%Docstring +Returns the symbol patch shape, to use if rendering symbol preview icons. + +.. seealso:: :py:func:`setPatchShape` + +.. versionadded:: 3.14 +%End + + void setPatchShape( const QgsLegendPatchShape &shape ); +%Docstring +Sets the symbol patch ``shape``, to use if rendering symbol preview icons. + +.. seealso:: :py:func:`patchShape` + +.. versionadded:: 3.14 %End private: diff --git a/python/core/auto_generated/symbology/qgssymbollayer.sip.in b/python/core/auto_generated/symbology/qgssymbollayer.sip.in index 59673017e5bc..7cb68f80d9ed 100644 --- a/python/core/auto_generated/symbology/qgssymbollayer.sip.in +++ b/python/core/auto_generated/symbology/qgssymbollayer.sip.in @@ -143,6 +143,8 @@ class QgsSymbolLayer PropertyRandomSeed, PropertyClipPoints, PropertyDensityArea, + PropertyFontFamily, + PropertyFontStyle, }; static const QgsPropertiesDefinition &propertyDefinitions(); diff --git a/python/core/auto_generated/vectortile/qgsvectortilebasicrenderer.sip.in b/python/core/auto_generated/vectortile/qgsvectortilebasicrenderer.sip.in new file mode 100644 index 000000000000..18f1fa3442d3 --- /dev/null +++ b/python/core/auto_generated/vectortile/qgsvectortilebasicrenderer.sip.in @@ -0,0 +1,206 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilebasicrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsVectorTileBasicRendererStyle +{ +%Docstring +Definition of map rendering of a subset of vector tile data. The subset of data is defined by: +1. sub-layer name +2. geometry type (a single sub-layer may have multiple geometry types) +3. filter expression + +Renering is determined by the associated symbol (QgsSymbol). Symbol has to be of the same +type as the chosen geometryType() - i.e. QgsMarkerSymbol for points, QgsLineSymbol for linestrings +and QgsFillSymbol for polygons. + +It is possible to further constrain when this style is applied by setting a range of allowed +zoom levels, or by disabling it. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilebasicrenderer.h" +%End + public: + QgsVectorTileBasicRendererStyle( const QString &stName = QString(), const QString &laName = QString(), QgsWkbTypes::GeometryType geomType = QgsWkbTypes::UnknownGeometry ); +%Docstring +Constructs a style object +%End + QgsVectorTileBasicRendererStyle( const QgsVectorTileBasicRendererStyle &other ); +%Docstring +Constructs a style object as a copy of another style +%End + ~QgsVectorTileBasicRendererStyle(); + + void setStyleName( const QString &name ); +%Docstring +Sets human readable name of this style +%End + QString styleName() const; +%Docstring +Returns human readable name of this style +%End + + void setLayerName( const QString &name ); +%Docstring +Sets name of the sub-layer to render (empty layer means that all layers match) +%End + QString layerName() const; +%Docstring +Returns name of the sub-layer to render (empty layer means that all layers match) +%End + + void setGeometryType( QgsWkbTypes::GeometryType geomType ); +%Docstring +Sets type of the geometry that will be used (point / line / polygon) +%End + QgsWkbTypes::GeometryType geometryType() const; +%Docstring +Returns type of the geometry that will be used (point / line / polygon) +%End + + void setFilterExpression( const QString &expr ); +%Docstring +Sets filter expression (empty filter means that all features match) +%End + QString filterExpression() const; +%Docstring +Returns filter expression (empty filter means that all features match) +%End + + void setSymbol( QgsSymbol *sym /Transfer/ ); +%Docstring +Sets symbol for rendering. Takes ownership of the symbol. +%End + QgsSymbol *symbol() const; +%Docstring +Returns symbol for rendering +%End + + void setEnabled( bool enabled ); +%Docstring +Sets whether this style is enabled (used for rendering) +%End + bool isEnabled() const; +%Docstring +Returns whether this style is enabled (used for rendering) +%End + + void setMinZoomLevel( int minZoom ); +%Docstring +Sets minimum zoom level index (negative number means no limit) +%End + int minZoomLevel() const; +%Docstring +Returns minimum zoom level index (negative number means no limit) +%End + + void setMaxZoomLevel( int maxZoom ); +%Docstring +Sets maximum zoom level index (negative number means no limit) +%End + int maxZoomLevel() const; +%Docstring +Returns maxnimum zoom level index (negative number means no limit) +%End + + bool isActive( int zoomLevel ) const; +%Docstring +Returns whether the style is active at given zoom level (also checks "enabled" flag) +%End + + void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const; +%Docstring +Writes object content to given DOM element +%End + void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); +%Docstring +Reads object content from given DOM element +%End + +}; + + +class QgsVectorTileBasicRenderer : QgsVectorTileRenderer +{ +%Docstring +The default vector tile renderer implementation. It has an ordered list of "styles", +each defines a rendering rule. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilebasicrenderer.h" +%End + public: + QgsVectorTileBasicRenderer(); +%Docstring +Constructs renderer with no styles +%End + + virtual QString type() const; + + virtual QgsVectorTileBasicRenderer *clone() const /Factory/; + + virtual void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ); + + virtual void stopRender( QgsRenderContext &context ); + + virtual void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ); + + virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); + + + void setStyles( const QList &styles ); +%Docstring +Sets list of styles of the renderer +%End + QList styles() const; +%Docstring +Returns list of styles of the renderer +%End + void setStyle( int index, const QgsVectorTileBasicRendererStyle &style ); +%Docstring +Updates style definition at the paricular index of the list (the index must be in interval [0,N-1] otherwise this function does nothing) +%End + QgsVectorTileBasicRendererStyle style( int index ) const; +%Docstring +Returns style definition at the particular index +%End + + static QList simpleStyle( + const QColor &polygonFillColor, const QColor &polygonStrokeColor, double polygonStrokeWidth, + const QColor &lineStrokeColor, double lineStrokeWidth, + const QColor &pointFillColor, const QColor &pointStrokeColor, double pointSize ); +%Docstring +Returns a list of styles to render all layers with the given fill/stroke colors, stroke widths and marker sizes +%End + + static QList simpleStyleWithRandomColors(); +%Docstring +Returns a list of styles to render all layers, using random colors +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilebasicrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/vectortile/qgsvectortilelayer.sip.in b/python/core/auto_generated/vectortile/qgsvectortilelayer.sip.in new file mode 100644 index 000000000000..196d732ff912 --- /dev/null +++ b/python/core/auto_generated/vectortile/qgsvectortilelayer.sip.in @@ -0,0 +1,146 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilelayer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsVectorTileLayer : QgsMapLayer +{ +%Docstring +Implements a map layer that is dedicated to rendering of vector tiles. +Vector tiles compared to "ordinary" vector layers are pre-processed data +optimized for fast rendering. A dataset is provided with a series of zoom levels +for different map scales. Each zoom level has a matrix of tiles that contain +actual data. A single vector tile may be a a file stored on a local drive, +requested over HTTP request or retrieved from a database. + +Content of a vector tile is divided into one or more named sub-layers. Each such +sub-layer may contain many features which consist of geometry and attributes. +Contrary to traditional vector layers, these sub-layers do not need to have a rigid +schema where geometry type and attributes are the same for all features. A single +sub-layer may have multiple geometry types in a single tile or have some attributes +defined only at particular zoom levels. + +Vector tile layer currently does not use the concept of data providers that other +layer types use. The process of rendering of vector tiles looks like this: + ++--------+ +------+ +---------+ +| DATA | | RAW | | DECODED | +| | --> LOADER --> | | --> DECODER --> | | --> RENDERER +| SOURCE | | TILE | | TILE | ++--------+ +------+ +---------+ + +Data source is a place from where tiles are fetched from (URL for HTTP access, local +files, MBTiles file, GeoPackage file or others. Loader (QgsVectorTileLoader) class +takes care of loading data from the data source. The "raw tile" data is just a blob +(QByteArray) that is encoded in some way. There are multiple ways how vector tiles +are encoded just like there are different formats how to store images. For example, +tiles can be encoded using Mapbox Vector Tiles (MVT) format or in GeoJSON. Decoder +(QgsVectorTileDecoder) takes care of decoding raw tile data into QgsFeature objects. +A decoded tile is essentially an array of vector features for each sub-layer found +in the tile - this is what vector tile renderer (QgsVectorTileRenderer) expects +and does the map rendering. + +To construct a vector tile layer, it is best to use QgsDataSourceUri class and set +the following parameters to get a valid encoded URI: +- "type" - what kind of data source will be used +- "url" - URL or path of the data source (specific to each data source type, see below) + +Currently supported data source types: +- "xyz" - the "url" should be a template like http://example.com/{z}/{x}/{y}.pbf where +{x},{y},{z} will be replaced by tile coordinates +- "mbtiles" - tiles read from a MBTiles file (a SQLite database) + +Currently supported decoders: +- MVT - following Mapbox Vector Tiles specification + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilelayer.h" +%End + public: + explicit QgsVectorTileLayer( const QString &path = QString(), const QString &baseName = QString() ); +%Docstring +Constructs a new vector tile layer +%End + ~QgsVectorTileLayer(); + + + virtual QgsVectorTileLayer *clone() const /Factory/; + + + virtual QgsMapLayerRenderer *createMapRenderer( QgsRenderContext &rendererContext ) /Factory/; + + virtual bool readXml( const QDomNode &layerNode, QgsReadWriteContext &context ); + + virtual bool writeXml( QDomNode &layerNode, QDomDocument &doc, const QgsReadWriteContext &context ) const; + + virtual bool readSymbology( const QDomNode &node, QString &errorMessage, + QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ); + + virtual bool writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, + StyleCategories categories = AllStyleCategories ) const; + + virtual void setTransformContext( const QgsCoordinateTransformContext &transformContext ); + + + QString sourceType() const; +%Docstring +Returns type of the data source +%End + QString sourcePath() const; +%Docstring +Returns URL/path of the data source (syntax different to each data source type) +%End + + int sourceMinZoom() const; +%Docstring +Returns minimum zoom level at which source has any valid tiles (negative = unconstrained) +%End + int sourceMaxZoom() const; +%Docstring +Returns maximum zoom level at which source has any valid tiles (negative = unconstrained) +%End + + + void setRenderer( QgsVectorTileRenderer *r /Transfer/ ); +%Docstring +Sets renderer for the map layer. + +.. note:: + + Takes ownership of the passed renderer +%End + QgsVectorTileRenderer *renderer() const; +%Docstring +Returns currently assigned renderer +%End + + void setTileBorderRenderingEnabled( bool enabled ); +%Docstring +Sets whether to render also borders of tiles (useful for debugging) +%End + bool isTileBorderRenderingEnabled() const; +%Docstring +Returns whether to render also borders of tiles (useful for debugging) +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilelayer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/vectortile/qgsvectortilerenderer.sip.in b/python/core/auto_generated/vectortile/qgsvectortilerenderer.sip.in new file mode 100644 index 000000000000..de0b552eca55 --- /dev/null +++ b/python/core/auto_generated/vectortile/qgsvectortilerenderer.sip.in @@ -0,0 +1,133 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilerenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsVectorTileRendererData +{ +%Docstring +Contains decoded features of a single vector tile and any other data necessary +for rendering of it. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilerenderer.h" +%End + public: + explicit QgsVectorTileRendererData( QgsTileXYZ id ); +%Docstring +Constructs the object +%End + + QgsTileXYZ id() const; +%Docstring +Returns coordinates of the tile +%End + + void setTilePolygon( QPolygon polygon ); +%Docstring +Sets polygon of the tile +%End + QPolygon tilePolygon() const; +%Docstring +Returns polygon (made out of four corners of the tile) in screen coordinates calculated from render context +%End + + QStringList layers() const; +%Docstring +Returns list of layer names present in the tile +%End + QVector layerFeatures( const QString &layerName ) const; +%Docstring +Returns list of all features within a single sub-layer +%End + +}; + +class QgsVectorTileRenderer +{ +%Docstring +Abstract base class for all vector tile renderer implementations. + +For rendering it is expected that client code calls: +1. startRender() to prepare renderer +2. renderTile() for each tile +3. stopRender() to clean up renderer and free resources + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilerenderer.h" +%End +%ConvertToSubClassCode + + const QString type = sipCpp->type(); + + if ( type == QStringLiteral( "basic" ) ) + sipType = sipType_QgsVectorTileBasicRenderer; + else + sipType = 0; +%End + public: + virtual ~QgsVectorTileRenderer(); + + virtual QString type() const = 0; +%Docstring +Returns unique type name of the renderer implementation +%End + + virtual QgsVectorTileRenderer *clone() const = 0 /Factory/; +%Docstring +Returns a clone of the renderer +%End + + virtual void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) = 0; +%Docstring +Initializes rendering. It should be paired with a stopRender() call. +%End + + + virtual void stopRender( QgsRenderContext &context ) = 0; +%Docstring +Finishes rendering and cleans up any resources +%End + + virtual void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) = 0; +%Docstring +Renders given vector tile. Must be called between startRender/stopRender. +%End + + virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const = 0; +%Docstring +Writes renderer's properties to given XML element +%End + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) = 0; +%Docstring +Reads renderer's properties from given XML element +%End + virtual void resolveReferences( const QgsProject &project ); +%Docstring +Resolves references to other objects - second phase of loading - after readXml() +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilerenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index ccaf34747479..0ad2cdb021c7 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -30,6 +30,7 @@ %Include auto_generated/qgscolorscheme.sip %Include auto_generated/qgscolorschemeregistry.sip %Include auto_generated/qgsconditionalstyle.sip +%Include auto_generated/qgsconnectionregistry.sip %Include auto_generated/qgscoordinateformatter.sip %Include auto_generated/qgscoordinatereferencesystem.sip %Include auto_generated/qgscoordinatetransform.sip @@ -77,6 +78,7 @@ %Include auto_generated/qgsfieldproxymodel.sip %Include auto_generated/qgsfields.sip %Include auto_generated/qgsfiledownloader.sip +%Include auto_generated/qgsfilefiltergenerator.sip %Include auto_generated/qgsfileutils.sip %Include auto_generated/qgsfontutils.sip %Include auto_generated/qgsgeometryoptions.sip @@ -165,6 +167,7 @@ %Include auto_generated/qgsproviderconnectionmodel.sip %Include auto_generated/qgsprovidermetadata.sip %Include auto_generated/qgsproviderregistry.sip +%Include auto_generated/qgsproxyfeaturesink.sip %Include auto_generated/qgsproxyprogresstask.sip %Include auto_generated/qgspythonrunner.sip %Include auto_generated/qgsrange.sip @@ -172,6 +175,7 @@ %Include auto_generated/qgsreadwritelocker.sip %Include auto_generated/qgsrelation.sip %Include auto_generated/qgsrelationcontext.sip +%Include auto_generated/qgsremappingproxyfeaturesink.sip %Include auto_generated/qgsrelationmanager.sip %Include auto_generated/qgsrenderchecker.sip %Include auto_generated/qgsrendercontext.sip @@ -199,9 +203,11 @@ %Include auto_generated/qgstemporalnavigationobject.sip %Include auto_generated/qgstemporalproperty.sip %Include auto_generated/qgstemporalrangeobject.sip +%Include auto_generated/qgstemporalutils.sip %Include auto_generated/qgstessellator.sip %Include auto_generated/qgstestutils.sip %Include auto_generated/qgstextrenderer.sip +%Include auto_generated/qgstiles.sip %Include auto_generated/qgstolerance.sip %Include auto_generated/qgstracer.sip %Include auto_generated/qgstrackedvectorlayertools.sip @@ -339,6 +345,7 @@ %Include auto_generated/layertree/qgslayertreenode.sip %Include auto_generated/layertree/qgslayertreeregistrybridge.sip %Include auto_generated/layertree/qgslayertreeutils.sip +%Include auto_generated/layertree/qgslegendpatchshape.sip %Include auto_generated/layout/qgsabstractlayoutiterator.sip %Include auto_generated/layout/qgsabstractreportsection.sip %Include auto_generated/layout/qgslayout.sip @@ -360,6 +367,7 @@ %Include auto_generated/layout/qgslayoutitemmapgrid.sip %Include auto_generated/layout/qgslayoutitemmapitem.sip %Include auto_generated/layout/qgslayoutitemmapoverview.sip +%Include auto_generated/layout/qgslayoutitemmarker.sip %Include auto_generated/layout/qgslayoutitemnodeitem.sip %Include auto_generated/layout/qgslayoutitempage.sip %Include auto_generated/layout/qgslayoutitempicture.sip @@ -374,6 +382,7 @@ %Include auto_generated/layout/qgslayoutmeasurementconverter.sip %Include auto_generated/layout/qgslayoutmodel.sip %Include auto_generated/layout/qgslayoutmultiframe.sip +%Include auto_generated/layout/qgslayoutnortharrowhandler.sip %Include auto_generated/layout/qgslayoutobject.sip %Include auto_generated/layout/qgslayoutpagecollection.sip %Include auto_generated/layout/qgslayoutpoint.sip @@ -400,12 +409,15 @@ %Include auto_generated/locator/qgslocatormodelbridge.sip %Include auto_generated/mesh/qgsmesh3daveraging.sip %Include auto_generated/mesh/qgsmeshdataprovider.sip +%Include auto_generated/mesh/qgsmeshdataset.sip %Include auto_generated/mesh/qgsmeshlayer.sip %Include auto_generated/mesh/qgsmeshlayerinterpolator.sip %Include auto_generated/mesh/qgsmeshrenderersettings.sip %Include auto_generated/mesh/qgsmeshspatialindex.sip %Include auto_generated/mesh/qgsmeshtimesettings.sip %Include auto_generated/mesh/qgsmeshtracerenderer.sip +%Include auto_generated/mesh/qgsmeshlayertemporalproperties.sip +%Include auto_generated/mesh/qgsmeshdataprovidertemporalcapabilities.sip %Include auto_generated/metadata/qgsabstractmetadatabase.sip %Include auto_generated/metadata/qgslayermetadata.sip %Include auto_generated/metadata/qgslayermetadataformatter.sip @@ -415,6 +427,7 @@ %Include auto_generated/numericformats/qgsbearingnumericformat.sip %Include auto_generated/numericformats/qgscurrencynumericformat.sip %Include auto_generated/numericformats/qgsfallbacknumericformat.sip +%Include auto_generated/numericformats/qgsfractionnumericformat.sip %Include auto_generated/numericformats/qgsnumericformat.sip %Include auto_generated/numericformats/qgsnumericformatregistry.sip %Include auto_generated/numericformats/qgspercentagenumericformat.sip @@ -424,6 +437,7 @@ %Include auto_generated/processing/models/qgsprocessingmodelchildparametersource.sip %Include auto_generated/processing/models/qgsprocessingmodelcomment.sip %Include auto_generated/processing/models/qgsprocessingmodelcomponent.sip +%Include auto_generated/processing/models/qgsprocessingmodelgroupbox.sip %Include auto_generated/processing/models/qgsprocessingmodeloutput.sip %Include auto_generated/processing/models/qgsprocessingmodelparameter.sip %Include auto_generated/processing/qgsprocessing.sip @@ -455,6 +469,7 @@ %Include auto_generated/raster/qgsrasterbandstats.sip %Include auto_generated/raster/qgsrasterblock.sip %Include auto_generated/raster/qgsrasterchecker.sip +%Include auto_generated/raster/qgsrastercontourrenderer.sip %Include auto_generated/raster/qgsrasterdataprovider.sip %Include auto_generated/raster/qgsrasterdataprovidertemporalcapabilities.sip %Include auto_generated/raster/qgsrasterdrawer.sip @@ -483,10 +498,13 @@ %Include auto_generated/raster/qgssinglebandgrayrenderer.sip %Include auto_generated/raster/qgssinglebandpseudocolorrenderer.sip %Include auto_generated/scalebar/qgsdoubleboxscalebarrenderer.sip +%Include auto_generated/scalebar/qgshollowscalebarrenderer.sip %Include auto_generated/scalebar/qgsnumericscalebarrenderer.sip %Include auto_generated/scalebar/qgsscalebarrenderer.sip +%Include auto_generated/scalebar/qgsscalebarrendererregistry.sip %Include auto_generated/scalebar/qgsscalebarsettings.sip %Include auto_generated/scalebar/qgssingleboxscalebarrenderer.sip +%Include auto_generated/scalebar/qgssteppedlinescalebarrenderer.sip %Include auto_generated/scalebar/qgsticksscalebarrenderer.sip %Include auto_generated/symbology/qgs25drenderer.sip %Include auto_generated/symbology/qgsarrowsymbollayer.sip @@ -524,6 +542,9 @@ %Include auto_generated/validity/qgsabstractvaliditycheck.sip %Include auto_generated/validity/qgsvaliditycheckcontext.sip %Include auto_generated/validity/qgsvaliditycheckregistry.sip +%Include auto_generated/vectortile/qgsvectortilebasicrenderer.sip +%Include auto_generated/vectortile/qgsvectortilelayer.sip +%Include auto_generated/vectortile/qgsvectortilerenderer.sip %Include auto_generated/gps/qgsqtlocationconnection.sip %Include auto_generated/gps/qgsgpsconnectionregistry.sip %Include auto_generated/symbology/qgsmasksymbollayer.sip diff --git a/python/gui/auto_additions/qgsexpressionbuilderwidget.py b/python/gui/auto_additions/qgsexpressionbuilderwidget.py new file mode 100644 index 000000000000..80927bf17a8f --- /dev/null +++ b/python/gui/auto_additions/qgsexpressionbuilderwidget.py @@ -0,0 +1,3 @@ +# The following has been generated automatically from src/gui/qgsexpressionbuilderwidget.h +QgsExpressionBuilderWidget.Flag.baseClass = QgsExpressionBuilderWidget +Flag = QgsExpressionBuilderWidget # dirty hack since SIP seems to introduce the flags in module diff --git a/python/gui/auto_additions/qgsfieldmappingmodel.py b/python/gui/auto_additions/qgsfieldmappingmodel.py new file mode 100644 index 000000000000..8267c8ef2ef7 --- /dev/null +++ b/python/gui/auto_additions/qgsfieldmappingmodel.py @@ -0,0 +1,11 @@ +# The following has been generated automatically from src/gui/qgsfieldmappingmodel.h +# monkey patching scoped based enum +QgsFieldMappingModel.ColumnDataIndex.SourceExpression.__doc__ = "Expression" +QgsFieldMappingModel.ColumnDataIndex.DestinationName.__doc__ = "Destination field name" +QgsFieldMappingModel.ColumnDataIndex.DestinationType.__doc__ = "Destination field QVariant::Type casted to (int)" +QgsFieldMappingModel.ColumnDataIndex.DestinationLength.__doc__ = "Destination field length" +QgsFieldMappingModel.ColumnDataIndex.DestinationPrecision.__doc__ = "Destination field precision" +QgsFieldMappingModel.ColumnDataIndex.DestinationConstraints.__doc__ = "Destination field constraints" +QgsFieldMappingModel.ColumnDataIndex.__doc__ = 'The ColumnDataIndex enum represents the column index for the view\n\n' + '* ``SourceExpression``: ' + QgsFieldMappingModel.ColumnDataIndex.SourceExpression.__doc__ + '\n' + '* ``DestinationName``: ' + QgsFieldMappingModel.ColumnDataIndex.DestinationName.__doc__ + '\n' + '* ``DestinationType``: ' + QgsFieldMappingModel.ColumnDataIndex.DestinationType.__doc__ + '\n' + '* ``DestinationLength``: ' + QgsFieldMappingModel.ColumnDataIndex.DestinationLength.__doc__ + '\n' + '* ``DestinationPrecision``: ' + QgsFieldMappingModel.ColumnDataIndex.DestinationPrecision.__doc__ + '\n' + '* ``DestinationConstraints``: ' + QgsFieldMappingModel.ColumnDataIndex.DestinationConstraints.__doc__ +# -- +QgsFieldMappingModel.ColumnDataIndex.baseClass = QgsFieldMappingModel diff --git a/python/gui/auto_generated/attributetable/qgsattributetablefiltermodel.sip.in b/python/gui/auto_generated/attributetable/qgsattributetablefiltermodel.sip.in index e3960387ef0b..348da96c73a9 100644 --- a/python/gui/auto_generated/attributetable/qgsattributetablefiltermodel.sip.in +++ b/python/gui/auto_generated/attributetable/qgsattributetablefiltermodel.sip.in @@ -196,6 +196,14 @@ Returns -1 if none is defined. Set the attribute table configuration to control which fields are shown, in which order they are shown as well as if and where an action column is shown. +%End + + void setFilterExpression( const QgsExpression &expression, const QgsExpressionContext &context ); +%Docstring +Set the ``expression`` and the ``context`` to be stored in case of the features +need to be filtered again (like on filter or on main model data change). + +.. versionadded:: 3.10.3 %End signals: @@ -234,10 +242,21 @@ selection state of the feature in case selected features are to be shown on top. public slots: - void extentsChanged(); + void extentsChanged(); %Docstring Is called upon every change of the visible extents on the map canvas. When a change is signalled, the filter is updated and invalidated if needed. + +.. deprecated:: QGIS 3.10.3 + - made private as reloadVisible() +%End + + void filterFeatures(); +%Docstring +Updates the filtered features in the filter model. It is called when the data of the +main table or the filter expression changed. + +.. versionadded:: 3.10.3 %End }; diff --git a/python/gui/auto_generated/attributetable/qgsdualview.sip.in b/python/gui/auto_generated/attributetable/qgsdualview.sip.in index 958356388ec4..401d434d7c9b 100644 --- a/python/gui/auto_generated/attributetable/qgsdualview.sip.in +++ b/python/gui/auto_generated/attributetable/qgsdualview.sip.in @@ -122,11 +122,22 @@ filter restrictions :return: Number of features %End - void setFilteredFeatures( const QgsFeatureIds &filteredFeatures ); + void setFilteredFeatures( const QgsFeatureIds &filteredFeatures ); %Docstring Set a list of currently visible features :param filteredFeatures: A list of feature ids + +.. deprecated:: + since filterFeatures is handled in the attribute filter model itself +%End + + void filterFeatures( const QgsExpression &filterExpression, const QgsExpressionContext &context ); +%Docstring +Sets the expression and Updates the filtered features in the filter model. +It is called when the filter expression changed. + +.. versionadded:: 3.10.3 %End QgsFeatureIds filteredFeatures(); diff --git a/python/gui/auto_generated/qgstemporalvcrdockwidget.sip.in b/python/gui/auto_generated/devtools/qgsdevtoolwidget.sip.in similarity index 71% rename from python/gui/auto_generated/qgstemporalvcrdockwidget.sip.in rename to python/gui/auto_generated/devtools/qgsdevtoolwidget.sip.in index 9834319ed182..4c8806f91686 100644 --- a/python/gui/auto_generated/qgstemporalvcrdockwidget.sip.in +++ b/python/gui/auto_generated/devtools/qgsdevtoolwidget.sip.in @@ -1,42 +1,36 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/gui/qgstemporalvcrdockwidget.h * + * src/gui/devtools/qgsdevtoolwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ - - - - -class QgsTemporalVcrDockWidget : QgsDockWidget +class QgsDevToolWidget : QgsPanelWidget { %Docstring -The QgsTemporalVcrDockWidget class +A panel widget that can be shown in the developer tools panel. .. versionadded:: 3.14 %End %TypeHeaderCode -#include "qgstemporalvcrdockwidget.h" +#include "qgsdevtoolwidget.h" %End public: - QgsTemporalVcrDockWidget( const QString &name, QWidget *parent = 0 ); + QgsDevToolWidget( QWidget *parent /TransferThis/ = 0 ); %Docstring -Constructor for QgsTemporalVcrDockWidget +Constructor for QgsDevToolWidget, with the specified ``parent`` widget. %End - ~QgsTemporalVcrDockWidget(); - }; /************************************************************************ * This file has been generated automatically from * * * - * src/gui/qgstemporalvcrdockwidget.h * + * src/gui/devtools/qgsdevtoolwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ diff --git a/python/gui/auto_generated/devtools/qgsdevtoolwidgetfactory.sip.in b/python/gui/auto_generated/devtools/qgsdevtoolwidgetfactory.sip.in new file mode 100644 index 000000000000..5f1fee84f3f8 --- /dev/null +++ b/python/gui/auto_generated/devtools/qgsdevtoolwidgetfactory.sip.in @@ -0,0 +1,73 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/devtools/qgsdevtoolwidgetfactory.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsDevToolWidgetFactory +{ +%Docstring +Factory class for creating custom developer/debugging tool pages + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsdevtoolwidgetfactory.h" +%End + public: + + QgsDevToolWidgetFactory( const QString &title = QString(), const QIcon &icon = QIcon() ); +%Docstring +Constructor for a QgsDevToolWidgetFactory with the specified ``title`` and ``icon``. +%End + + virtual ~QgsDevToolWidgetFactory(); + + virtual QIcon icon() const; +%Docstring +Returns the icon that will be shown in the tool in the panel. + +.. seealso:: :py:func:`setIcon` +%End + + void setIcon( const QIcon &icon ); +%Docstring +Sets the ``icon`` for the factory object, which will be shown for the tool in the panel. + +.. seealso:: :py:func:`icon` +%End + + virtual QString title() const; +%Docstring +Returns the (translated) title of the tool. + +.. seealso:: :py:func:`setTitle` +%End + + void setTitle( const QString &title ); +%Docstring +Set the translated ``title`` for the tool. +%End + + virtual QgsDevToolWidget *createWidget( QWidget *parent = 0 ) const = 0 /Factory/; +%Docstring +Factory function to create the widget on demand as needed by the dock. + +The ``parent`` argument gives the correct parent for the newly created widget. +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/devtools/qgsdevtoolwidgetfactory.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/editorwidgets/qgsrelationreferencewidget.sip.in b/python/gui/auto_generated/editorwidgets/qgsrelationreferencewidget.sip.in index b62a2e075ea9..adbdf46c02e8 100644 --- a/python/gui/auto_generated/editorwidgets/qgsrelationreferencewidget.sip.in +++ b/python/gui/auto_generated/editorwidgets/qgsrelationreferencewidget.sip.in @@ -77,6 +77,14 @@ Returns the related feature foreign keys %End void setEditorContext( const QgsAttributeEditorContext &context, QgsMapCanvas *canvas, QgsMessageBar *messageBar ); +%Docstring +Sets the editor ``context`` + +.. note:: + + if context cadDockWidget is null, it won't be possible to digitize + the geometry of a referenced feature from this widget +%End bool embedForm(); %Docstring diff --git a/python/gui/auto_generated/layertree/qgslayertreeviewdefaultactions.sip.in b/python/gui/auto_generated/layertree/qgslayertreeviewdefaultactions.sip.in index 4a951e3baedb..53074355552c 100644 --- a/python/gui/auto_generated/layertree/qgslayertreeviewdefaultactions.sip.in +++ b/python/gui/auto_generated/layertree/qgslayertreeviewdefaultactions.sip.in @@ -80,6 +80,14 @@ Action to zoom to selected features of a vector layer .. seealso:: :py:func:`moveToTop` .. versionadded:: 3.2 +%End + + QAction *actionMoveToBottom( QObject *parent = 0 ) /Factory/; +%Docstring + +.. seealso:: :py:func:`moveToBottom` + +.. versionadded:: 3.14 %End QAction *actionGroupSelected( QObject *parent = 0 ) /Factory/; @@ -139,6 +147,14 @@ Moves selected layer(s) and/or group(s) to the top of the layer panel or the top of the group if the layer/group is placed within a group. .. versionadded:: 3.2 +%End + + void moveToBottom(); +%Docstring +Moves selected layer(s) and/or group(s) to the bottom of the layer panel +or the bottom of the group if the layer/group is placed within a group. + +.. versionadded:: 3.14 %End void groupSelected(); diff --git a/python/gui/auto_generated/numericformats/qgsnumericformatwidget.sip.in b/python/gui/auto_generated/numericformats/qgsnumericformatwidget.sip.in index ad4afd249a91..22f2a7d78232 100644 --- a/python/gui/auto_generated/numericformats/qgsnumericformatwidget.sip.in +++ b/python/gui/auto_generated/numericformats/qgsnumericformatwidget.sip.in @@ -7,6 +7,7 @@ ************************************************************************/ + class QgsNumericFormatWidget : QgsPanelWidget { %Docstring @@ -231,6 +232,33 @@ Constructor for QgsScientificNumericFormatWidget, initially showing the specifie }; + +class QgsFractionNumericFormatWidget : QgsNumericFormatWidget +{ +%Docstring +A widget which allow control over the properties of a :py:class:`QgsFractionNumericFormat`. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsnumericformatwidget.h" +%End + public: + + QgsFractionNumericFormatWidget( const QgsNumericFormat *format, QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsFractionNumericFormatWidget, initially showing the specified ``format``. +%End + ~QgsFractionNumericFormatWidget(); + + virtual void setFormat( QgsNumericFormat *format ); + + + virtual QgsNumericFormat *format() /Factory/; + + +}; /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/gui/auto_generated/processing/models/qgsmodelarrowitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelarrowitem.sip.in index 086354f8705c..dc91803fb423 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelarrowitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelarrowitem.sip.in @@ -28,8 +28,8 @@ A link arrow item for use in the model designer. %End public: - QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, - QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex ); + QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, bool startIsOutgoing, + QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex, bool endIsIncoming ); %Docstring Constructor for QgsModelArrowItem, with the specified ``parent`` item. diff --git a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in index 06faf9a94756..cc2d319119d3 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelcomponentgraphicitem.sip.in @@ -106,11 +106,6 @@ Shows a preview of moving the item from its stored position by ``dx``, ``dy``. void setItemRect( QRectF rect ); %Docstring Sets a new scene ``rect`` for the item. -%End - - void previewItemRectChange( QRectF rect ); -%Docstring -Shows a preview of setting a new ``rect`` for the item. %End virtual void mouseDoubleClickEvent( QGraphicsSceneMouseEvent *event ); @@ -125,6 +120,8 @@ Shows a preview of setting a new ``rect`` for the item. virtual QRectF boundingRect() const; + virtual bool contains( const QPointF &point ) const; + virtual void paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = 0 ); @@ -162,7 +159,7 @@ Returns the number of link points associated with the component on the specified Returns the text to use for the link point with the specified ``index`` on the specified ``edge``. %End - QPointF linkPoint( Qt::Edge edge, int index ) const; + QPointF linkPoint( Qt::Edge edge, int index, bool incoming ) const; %Docstring Returns the location of the link point with the specified ``index`` on the specified ``edge``. %End @@ -279,6 +276,11 @@ Returns the label text color for the item for the specified ``state``. virtual Qt::PenStyle strokeStyle( State state ) const; %Docstring Returns the stroke style to use while rendering the outline of the item. +%End + + virtual Qt::Alignment titleAlignment() const; +%Docstring +Returns the title alignment %End virtual QPicture iconPicture() const; @@ -392,6 +394,16 @@ Ownership of ``child`` is transferred to the item. virtual bool canDeleteComponent(); + void setResults( const QVariantMap &results ); +%Docstring +Sets the results obtained for this child algorithm for the last model execution through the dialog. +%End + + void setInputs( const QVariantMap &inputs ); +%Docstring +Sets the inputs used for this child algorithm for the last model execution through the dialog. +%End + protected: virtual QColor fillColor( State state ) const; @@ -529,6 +541,64 @@ Ownership of ``output`` is transferred to the item. }; + +class QgsModelGroupBoxGraphicItem : QgsModelComponentGraphicItem +{ +%Docstring +A graphic item representing a group box in the model designer. + +.. warning:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsmodelcomponentgraphicitem.h" +%End + public: + + QgsModelGroupBoxGraphicItem( QgsProcessingModelGroupBox *box /Transfer/, + QgsProcessingModelAlgorithm *model, + QGraphicsItem *parent /TransferThis/ ); +%Docstring +Constructor for QgsModelGroupBoxGraphicItem for the specified group ``box``, with the specified ``parent`` item. + +The ``model`` argument specifies the associated processing model. Ownership of ``model`` is not transferred, and +it must exist for the lifetime of this object. + +Ownership of ``output`` is transferred to the item. +%End + ~QgsModelGroupBoxGraphicItem(); + virtual void contextMenuEvent( QGraphicsSceneContextMenuEvent *event ); + + virtual bool canDeleteComponent(); + + protected: + + virtual QColor fillColor( State state ) const; + + virtual QColor strokeColor( State state ) const; + + virtual QColor textColor( State state ) const; + + virtual Qt::PenStyle strokeStyle( State state ) const; + + virtual Qt::Alignment titleAlignment() const; + + virtual void updateStoredComponentPosition( const QPointF &pos, const QSizeF &size ); + + + protected slots: + + virtual void deleteComponent(); + + virtual void editComponent(); + +}; + + /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in index 09e8a502733e..b14695e69ffd 100644 --- a/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodeldesignerdialog.sip.in @@ -95,6 +95,16 @@ Checks if the model can current be saved, and returns ``True`` if it can. Checks if there are unsaved changes in the model, and if so, prompts the user to save them. Returns ``False`` if the cancel option was selected +%End + + void setLastRunChildAlgorithmResults( const QVariantMap &results ); +%Docstring +Sets the results of child algorithms for the last run of the model through the designer window. +%End + + void setLastRunChildAlgorithmInputs( const QVariantMap &inputs ); +%Docstring +Sets the inputs for child algorithms for the last run of the model through the designer window. %End }; diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in index cdd7a07bbc70..701e1e2d5999 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsscene.sip.in @@ -29,6 +29,7 @@ QGraphicsScene subclass representing the model designer. enum ZValues { + GroupBox, ArrowLink, ModelComponent, MouseHandles, @@ -94,6 +95,16 @@ Returns list of selected component items. QgsModelComponentGraphicItem *componentItemAt( QPointF position ) const; %Docstring Returns the topmost component item at a specified ``position``. +%End + + QgsModelComponentGraphicItem *groupBoxItem( const QString &uuid ); +%Docstring +Returns the graphic item corresponding to the specified group box ``uuid``. +%End + + void selectAll(); +%Docstring +Selects all the components in the scene. %End void deselectAll(); @@ -107,6 +118,16 @@ not correctly emit signals to allow the scene's model to update. void setSelectedItem( QgsModelComponentGraphicItem *item ); %Docstring Clears any selected items and sets ``item`` as the current selection. +%End + + void setChildAlgorithmResults( const QVariantMap &results ); +%Docstring +Sets the results for child algorithms for the last model execution. +%End + + void setChildAlgorithmInputs( const QVariantMap &inputs ); +%Docstring +Sets the inputs for child algorithms for the last model execution. %End signals: @@ -142,7 +163,7 @@ If ``None``, no item is selected. Creates a new graphic item for a model parameter. %End - virtual QgsModelComponentGraphicItem *createChildAlgGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelChildAlgorithm *child ) const /Factory/; + virtual QgsModelChildAlgorithmGraphicItem *createChildAlgGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelChildAlgorithm *child ) const /Factory/; %Docstring Creates a new graphic item for a model child algorithm. %End @@ -158,6 +179,10 @@ Creates a new graphic item for a model output. Creates a new graphic item for a model comment. %End + QgsModelComponentGraphicItem *createGroupBoxGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelGroupBox *box ) const /Factory/; +%Docstring +Creates a new graphic item for a model group box. +%End }; diff --git a/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in b/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in index 757be4b4c6b6..5b3846320e3a 100644 --- a/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in +++ b/python/gui/auto_generated/processing/models/qgsmodelgraphicsview.sip.in @@ -78,6 +78,13 @@ Starts a macro command, containing a group of interactions in the view. void endMacroCommand(); %Docstring Ends a macro command, containing a group of interactions in the view. +%End + + public slots: + + void snapSelected(); +%Docstring +Snaps the selected items to the grid. %End signals: diff --git a/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in b/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in index 08a57654acf6..9a0e6379241e 100644 --- a/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingalgorithmdialogbase.sip.in @@ -11,7 +11,7 @@ -class QgsProcessingAlgorithmDialogBase : QDialog +class QgsProcessingAlgorithmDialogBase : QDialog, QgsProcessingParametersGenerator { %Docstring Base class for processing algorithm dialogs. @@ -56,7 +56,7 @@ Returns the algorithm running in the dialog. .. seealso:: :py:func:`setAlgorithm` %End - void setMainWidget( QWidget *widget /Transfer/ ); + void setMainWidget( QgsPanelWidget *widget /Transfer/ ); %Docstring Sets the main ``widget`` for the dialog, usually a panel for configuring algorithm parameters. @@ -97,11 +97,6 @@ Returns the results returned by the algorithm executed. %Docstring Creates a new processing feedback object, automatically connected to the appropriate slots in this dialog. -%End - - virtual QVariantMap getParameterValues() const; -%Docstring -Returns the parameter values for the algorithm to run in the dialog. %End void saveLogToFile( const QString &path, LogFormat format = FormatPlainText ); @@ -180,6 +175,11 @@ Opens a dialog allowing users to save the current log contents. Copies the current log contents to the clipboard. .. versionadded:: 3.2 +%End + + void showParameters(); +%Docstring +Switches the dialog to the parameters page. %End protected: @@ -195,6 +195,11 @@ Returns the dialog's run button. QPushButton *cancelButton(); %Docstring Returns the dialog's cancel button. +%End + + QPushButton *changeParametersButton(); +%Docstring +Returns the dialog's change parameters button. %End QDialogButtonBox *buttonBox(); @@ -219,6 +224,11 @@ Sets whether the algorithm was executed through the dialog. .. seealso:: :py:func:`wasExecuted` .. seealso:: :py:func:`setResults` +%End + + void setExecutedAnyResult( bool executedAnyResult ); +%Docstring +Sets whether the algorithm was executed through the dialog (no matter the result). %End void setResults( const QVariantMap &results ); @@ -238,6 +248,29 @@ Displays an info ``message`` in the dialog's log. void resetGui(); %Docstring Resets the dialog's gui, ready for another algorithm execution. +%End + + virtual void resetAdditionalGui(); +%Docstring +For subclasses to register their own GUI controls to be reset, ready +for another algorithm execution. +%End + + void updateRunButtonVisibility(); +%Docstring +Sets visibility for mutually exclusive buttons Run and Change Parameters. +%End + + void blockControlsWhileRunning(); +%Docstring +Blocks run and changeParameters buttons and parameters tab while the +algorithm is running. +%End + + virtual void blockAdditionalControlsWhileRunning(); +%Docstring +For subclasses to register their own GUI controls to be blocked while +the algorithm is running. %End QgsMessageBar *messageBar(); @@ -261,6 +294,15 @@ by the dialog. Ownership of ``task`` is transferred to the dialog. Formats an input ``string`` for display in the log tab. .. versionadded:: 3.0.1 +%End + + signals: + + void algorithmFinished( bool successful, const QVariantMap &result ); +%Docstring +Emitted whenever an algorithm has finished executing in the dialog. + +.. versionadded:: 3.14 %End protected slots: diff --git a/python/gui/auto_generated/processing/qgsprocessingmaplayercombobox.sip.in b/python/gui/auto_generated/processing/qgsprocessingmaplayercombobox.sip.in index f9eac231a335..a7e5b2fcd587 100644 --- a/python/gui/auto_generated/processing/qgsprocessingmaplayercombobox.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingmaplayercombobox.sip.in @@ -10,7 +10,6 @@ - class QgsProcessingMapLayerComboBox : QWidget { %Docstring @@ -28,7 +27,7 @@ Processing map layer combo box. %End public: - QgsProcessingMapLayerComboBox( const QgsProcessingParameterDefinition *parameter, QWidget *parent = 0 ); + QgsProcessingMapLayerComboBox( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = 0 ); %Docstring Constructor for QgsProcessingMapLayerComboBox, with the specified ``parameter`` definition. %End @@ -80,17 +79,36 @@ Returns the current value of the widget. .. seealso:: :py:func:`setValue` %End - signals: + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ); +%Docstring +Sets the ``context`` in which the widget is shown. - void valueChanged(); +.. versionadded:: 3.14 +%End + + void setEditable( bool editable ); %Docstring -Emitted whenever the value is changed in the widget. +Sets whether the combo box value can be freely edited. + +.. seealso:: :py:func:`isEditable` + +.. versionadded:: 3.14 %End - void triggerFileSelection(); + bool isEditable() const; %Docstring -Emitted when the widget has triggered a file selection operation (to be -handled in Python for now). +Returns whether the combo box value can be freely edited. + +.. seealso:: :py:func:`setEditable` + +.. versionadded:: 3.14 +%End + + signals: + + void valueChanged(); +%Docstring +Emitted whenever the value is changed in the widget. %End protected: diff --git a/python/gui/auto_generated/processing/qgsprocessingmodelerparameterwidget.sip.in b/python/gui/auto_generated/processing/qgsprocessingmodelerparameterwidget.sip.in index e16ea67b3c4b..ed8f030e0753 100644 --- a/python/gui/auto_generated/processing/qgsprocessingmodelerparameterwidget.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingmodelerparameterwidget.sip.in @@ -114,7 +114,49 @@ Sets the current ``value`` for the parameter. .. seealso:: :py:func:`value` %End - virtual QgsProcessingModelChildParameterSource value() const; + void setWidgetValue( const QList< QgsProcessingModelChildParameterSource > &values ); +%Docstring +Sets the current ``values`` for the parameter. + +.. seealso:: :py:func:`value` + +.. versionadded:: 3.14 +%End + + void setToModelOutput( const QString &value ); +%Docstring +Sets the widget to a model output, for destination parameters only. + +.. seealso:: :py:func:`isModelOutput` + +.. seealso:: :py:func:`modelOutputName` + +.. versionadded:: 3.14 +%End + + bool isModelOutput() const; +%Docstring +Returns ``True`` if the widget is set to the model output mode. + +.. seealso:: :py:func:`setToModelOutput` + +.. seealso:: :py:func:`modelOutputName` + +.. versionadded:: 3.14 +%End + + QString modelOutputName() const; +%Docstring +Returns the model output name, if isModelOutput() is ``True``. + +.. seealso:: :py:func:`setToModelOutput` + +.. seealso:: :py:func:`isModelOutput` + +.. versionadded:: 3.14 +%End + + virtual QVariant value() const; %Docstring Returns the current value of the parameter. diff --git a/python/gui/auto_generated/processing/qgsprocessingmultipleselectiondialog.sip.in b/python/gui/auto_generated/processing/qgsprocessingmultipleselectiondialog.sip.in index e8d57d7cfc81..a8e1563595a8 100644 --- a/python/gui/auto_generated/processing/qgsprocessingmultipleselectiondialog.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingmultipleselectiondialog.sip.in @@ -10,11 +10,101 @@ +class QgsProcessingMultipleSelectionPanelWidget : QgsPanelWidget +{ +%Docstring +A panel widget for selection of multiple options from a fixed list of options. + +.. note:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingmultipleselectiondialog.h" +%End + public: + + QgsProcessingMultipleSelectionPanelWidget( const QVariantList &availableOptions = QVariantList(), + const QVariantList &selectedOptions = QVariantList(), + QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsProcessingMultipleSelectionPanelWidget. + +The ``availableOptions`` list specifies the list of standard known options for the parameter, +whilst the ``selectedOptions`` list specifies which options should be initially selected. + +The ``selectedOptions`` list may contain extra options which are not present in ``availableOptions``, +in which case they will be also added as existing options within the dialog. +%End + + void setValueFormatter( SIP_PYCALLABLE ); +%Docstring +Sets a callback function to use when encountering an invalid geometry and +%End +%MethodCode + + Py_BEGIN_ALLOW_THREADS + + sipCpp->setValueFormatter( [a0]( const QVariant &v )->QString + { + QString res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a0, "D", &v, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QString *t1 = reinterpret_cast( sipConvertToType( s, sipType_QString, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QString( *t1 ); + } + sipReleaseType( t1, sipType_QString, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS +%End + + + QVariantList selectedOptions() const; +%Docstring +Returns the ordered list of selected options. +%End + + QDialogButtonBox *buttonBox(); +%Docstring +Returns the widget's button box. +%End + + signals: + + void acceptClicked(); +%Docstring +Emitted when the accept button is clicked. +%End + + void selectionChanged(); +%Docstring +Emitted when the selection changes in the widget. +%End + + protected: + + void addOption( const QVariant &value, const QString &title, bool selected, bool updateExistingTitle = false ); +%Docstring +Adds a new option to the widget. +%End + +}; + class QgsProcessingMultipleSelectionDialog : QDialog { %Docstring -Dialog for configuration of a matrix (fixed table) parameter. +A dialog for selection of multiple options from a fixed list of options. .. note:: @@ -32,7 +122,7 @@ Dialog for configuration of a matrix (fixed table) parameter. const QVariantList &selectedOptions = QVariantList(), QWidget *parent /TransferThis/ = 0, Qt::WindowFlags flags = 0 ); %Docstring -Constructor for QgsProcessingMultipleSelectionDialog. +Constructor for :py:class:`QgsProcessingMultipleSelectionPanelWidget`. The ``availableOptions`` list specifies the list of standard known options for the parameter, whilst the ``selectedOptions`` list specifies which options should be initially selected. @@ -79,6 +169,83 @@ Returns the ordered list of selected options. }; +class QgsProcessingMultipleInputPanelWidget : QgsProcessingMultipleSelectionPanelWidget +{ +%Docstring +A panel widget for selection of multiple inputs from a fixed list of options. + +.. note:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingmultipleselectiondialog.h" +%End + public: + + QgsProcessingMultipleInputPanelWidget( const QgsProcessingParameterMultipleLayers *parameter, + const QVariantList &selectedOptions, + const QList< QgsProcessingModelChildParameterSource > &modelSources, + QgsProcessingModelAlgorithm *model = 0, + QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsProcessingMultipleInputPanelWidget. +%End + + void setProject( QgsProject *project ); +%Docstring +Sets the project associated with the widget. +%End + +}; + + +class QgsProcessingMultipleInputDialog : QDialog +{ +%Docstring +A dialog for selection of multiple layer inputs. + +.. note:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingmultipleselectiondialog.h" +%End + public: + + QgsProcessingMultipleInputDialog( const QgsProcessingParameterMultipleLayers *parameter, + const QVariantList &selectedOptions, + const QList< QgsProcessingModelChildParameterSource > &modelSources, + QgsProcessingModelAlgorithm *model = 0, + QWidget *parent /TransferThis/ = 0, Qt::WindowFlags flags = 0 ); +%Docstring +Constructor for QgsProcessingMultipleInputDialog. + +The ``selectedOptions`` list may contain extra options which are not present in ``availableOptions``, +in which case they will be also added as existing options within the dialog. +%End + + QVariantList selectedOptions() const; +%Docstring +Returns the ordered list of selected options. +%End + + void setProject( QgsProject *project ); +%Docstring +Sets the project associated with the dialog. +%End + +}; + + + /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/gui/auto_generated/processing/qgsprocessingoutputdestinationwidget.sip.in b/python/gui/auto_generated/processing/qgsprocessingoutputdestinationwidget.sip.in new file mode 100644 index 000000000000..121855ee78da --- /dev/null +++ b/python/gui/auto_generated/processing/qgsprocessingoutputdestinationwidget.sip.in @@ -0,0 +1,108 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/processing/qgsprocessingoutputdestinationwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsProcessingLayerOutputDestinationWidget : QWidget +{ +%Docstring +A widget which allows users to select the destination path for an output style Processing parameter. + +.. note:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingoutputdestinationwidget.h" +%End + public: + + QgsProcessingLayerOutputDestinationWidget( const QgsProcessingDestinationParameter *parameter, bool defaultSelection, QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsProcessingLayerOutputDestinationWidget, associated with the specified ``parameter``. +%End + + bool outputIsSkipped() const; +%Docstring +Returns ``True`` if the output is set to be skipped. +%End + + void setValue( const QVariant &value ); +%Docstring +Sets the ``value`` to show in the widget. +%End + + QVariant value() const; +%Docstring +Returns the widgets current value. +%End + + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ); +%Docstring +Sets the ``context`` in which the widget is shown, e.g., the +parent model algorithm, a linked map canvas, and other relevant information which allows the widget +to fine-tune its behavior. +%End + + void setContext( QgsProcessingContext *context ); +%Docstring +Sets the processing ``context`` in which this widget is being shown. +%End + + void registerProcessingParametersGenerator( QgsProcessingParametersGenerator *generator ); +%Docstring +Registers a Processing parameters ``generator`` class that will be used to retrieve +algorithm parameters for the widget when required. + +.. versionadded:: 3.14 +%End + + void addOpenAfterRunningOption(); +%Docstring +Adds the "Open output file after running" option to the widget. +%End + + bool openAfterRunning() const; +%Docstring +Returns ``True`` if the widget has the "Open output file after running" option checked. +%End + + signals: + + void skipOutputChanged( bool skipped ); +%Docstring +Emitted whenever the "skip output" option is toggled in the widget. +%End + + void destinationChanged(); +%Docstring +Emitted whenever the destination value is changed in the widget. +%End + protected: + + virtual void dragEnterEvent( QDragEnterEvent *event ); + + virtual void dragLeaveEvent( QDragLeaveEvent *event ); + + virtual void dropEvent( QDropEvent *event ); + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/processing/qgsprocessingoutputdestinationwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/processing/qgsprocessingparameterdefinitionwidget.sip.in b/python/gui/auto_generated/processing/qgsprocessingparameterdefinitionwidget.sip.in index db3d6457d0e5..8fb9dad414a8 100644 --- a/python/gui/auto_generated/processing/qgsprocessingparameterdefinitionwidget.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingparameterdefinitionwidget.sip.in @@ -157,6 +157,24 @@ Returns the comments for the parameter. .. seealso:: :py:func:`setComments` +.. versionadded:: 3.14 +%End + + void setCommentColor( const QColor &color ); +%Docstring +Sets the color for the comments for the parameter. + +.. seealso:: :py:func:`commentColor` + +.. versionadded:: 3.14 +%End + + QColor commentColor() const; +%Docstring +Returns the color for the comments for the parameter. + +.. seealso:: :py:func:`setCommentColor` + .. versionadded:: 3.14 %End diff --git a/python/gui/auto_generated/processing/qgsprocessingparameterswidget.sip.in b/python/gui/auto_generated/processing/qgsprocessingparameterswidget.sip.in new file mode 100644 index 000000000000..2baa2b1e02a1 --- /dev/null +++ b/python/gui/auto_generated/processing/qgsprocessingparameterswidget.sip.in @@ -0,0 +1,58 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/processing/qgsprocessingparameterswidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsProcessingParametersWidget : QgsPanelWidget, QgsProcessingParametersGenerator +{ +%Docstring +A widget which allows users to select the value for the parameters for an algorithm. + +.. note:: + + Not stable API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingparameterswidget.h" +%End + public: + + QgsProcessingParametersWidget( const QgsProcessingAlgorithm *algorithm, QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsProcessingParametersWidget, for the specified ``algorithm``. +%End + + const QgsProcessingAlgorithm *algorithm() const; + + protected: + + virtual void initWidgets(); + + void addParameterWidget( const QgsProcessingParameterDefinition *parameter, QWidget *widget /Transfer/ ); + void addParameterLabel( const QgsProcessingParameterDefinition *parameter, QWidget *label /Transfer/ ); + + void addOutputLabel( QWidget *label /Transfer/ ); + void addOutputWidget( QWidget *widget /Transfer/ ); + + void addExtraWidget( QWidget *widget /Transfer/ ); + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/processing/qgsprocessingparameterswidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/processing/qgsprocessingtoolboxmodel.sip.in b/python/gui/auto_generated/processing/qgsprocessingtoolboxmodel.sip.in index 0fea68b611a7..d287b837df93 100644 --- a/python/gui/auto_generated/processing/qgsprocessingtoolboxmodel.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingtoolboxmodel.sip.in @@ -256,6 +256,7 @@ of this model. RoleAlgorithmName, RoleAlgorithmShortDescription, RoleAlgorithmTags, + RoleProviderFlags, }; QgsProcessingToolboxModel( QObject *parent /TransferThis/ = 0, QgsProcessingRegistry *registry = 0, @@ -447,6 +448,8 @@ Returns the current filter string, if set. virtual bool lessThan( const QModelIndex &left, const QModelIndex &right ) const; + virtual QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const; + }; diff --git a/python/gui/auto_generated/processing/qgsprocessingwidgetwrapper.sip.in b/python/gui/auto_generated/processing/qgsprocessingwidgetwrapper.sip.in index 44f778c2aff3..823e562a2994 100644 --- a/python/gui/auto_generated/processing/qgsprocessingwidgetwrapper.sip.in +++ b/python/gui/auto_generated/processing/qgsprocessingwidgetwrapper.sip.in @@ -36,6 +36,30 @@ return a pointer to a context which they have already created and own. virtual ~QgsProcessingContextGenerator(); }; +class QgsProcessingParametersGenerator +{ +%Docstring + +An interface for objects which can create sets of parameter values for processing algorithms. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsprocessingwidgetwrapper.h" +%End + public: + + virtual QVariantMap createProcessingParameters() = 0; +%Docstring +This method needs to be reimplemented in all classes which implement this interface +and return a algorithm parameters. +%End + + virtual ~QgsProcessingParametersGenerator(); +}; + + class QgsProcessingParameterWidgetContext { %Docstring @@ -88,6 +112,24 @@ to the user. .. seealso:: :py:func:`setMessageBar` +.. versionadded:: 3.12 +%End + + void setBrowserModel( QgsBrowserGuiModel *model ); +%Docstring +Sets the browser ``model`` associated with the widget. This will usually be the shared app instance of the browser model + +.. seealso:: :py:func:`browserModel` + +.. versionadded:: 3.14 +%End + + QgsBrowserGuiModel *browserModel() const; +%Docstring +Returns the browser model associated with the widget. + +.. seealso:: :py:func:`setBrowserModel` + .. versionadded:: 3.12 %End @@ -142,6 +184,24 @@ Sets the child algorithm ``id`` within the model which the parameter widget is a .. seealso:: :py:func:`modelChildAlgorithmId` .. seealso:: :py:func:`setModel` +%End + + QgsMapLayer *activeLayer() const; +%Docstring +Returns the current active layer. + +.. seealso:: :py:func:`setActiveLayer` + +.. versionadded:: 3.14 +%End + + void setActiveLayer( QgsMapLayer *layer ); +%Docstring +Sets the current active ``layer``. + +.. seealso:: :py:func:`activeLayer` + +.. versionadded:: 3.14 %End }; @@ -266,12 +326,26 @@ current value is associated with. Returns the current value of the parameter. .. seealso:: :py:func:`setParameterValue` +%End + + virtual QVariantMap customProperties() const; +%Docstring +Returns any custom properties set by the wrapper. %End void registerProcessingContextGenerator( QgsProcessingContextGenerator *generator ); %Docstring Registers a Processing context ``generator`` class that will be used to retrieve a Processing context for the wrapper when required. +%End + + void registerProcessingParametersGenerator( QgsProcessingParametersGenerator *generator ); +%Docstring +Registers a Processing parameters ``generator`` class that will be used to retrieve +algorithm parameters for the wrapper when required (e.g. when a wrapper needs access +to other parameter's values). + +.. versionadded:: 3.14 %End virtual void postInitialize( const QList< QgsAbstractProcessingParameterWidgetWrapper * > &wrappers ); @@ -477,10 +551,10 @@ values which can be used as values for the parameter. .. seealso:: :py:func:`compatibleDataTypes` %End - virtual QList< int > compatibleDataTypes() const = 0; + virtual QList< int > compatibleDataTypes( const QgsProcessingParameterDefinition *parameter ) const; %Docstring Returns a list of compatible Processing data types for inputs -for this parameter. +for this widget for the specified ``parameter``. In order to determine the available sources for the parameter in a model the types returned by this method are checked. The returned list corresponds diff --git a/python/gui/auto_generated/qgisinterface.sip.in b/python/gui/auto_generated/qgisinterface.sip.in index 06c7c2dbffb5..e48ba3753358 100644 --- a/python/gui/auto_generated/qgisinterface.sip.in +++ b/python/gui/auto_generated/qgisinterface.sip.in @@ -578,6 +578,20 @@ Statistical summary action. virtual QAction *actionShowAllLayers() = 0; virtual QAction *actionHideSelectedLayers() = 0; + virtual QAction *actionToggleSelectedLayers() = 0; +%Docstring +Returns the Toggle Selected Layers action. + +.. versionadded:: 3.14 +%End + + virtual QAction *actionToggleSelectedLayersIndependently() = 0; +%Docstring +Returns the Toggle Selected Layers Independently action. + +.. versionadded:: 3.14 +%End + virtual QAction *actionHideDeselectedLayers() = 0; %Docstring Returns the Hide Deselected Layers action. @@ -1118,15 +1132,38 @@ Unregister a previously registered tab in the options dialog. .. versionadded:: 3.0 %End - virtual void registerCustomDropHandler( QgsCustomDropHandler *handler ) = 0; + virtual void registerDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) = 0; %Docstring -Register a new custom drop handler. +Register a new tool in the development/debugging tools dock. .. note:: Ownership of the factory is not transferred, and the factory must be unregistered when plugin is unloaded. +.. seealso:: :py:func:`unregisterDevToolWidgetFactory` + +.. versionadded:: 3.14 +%End + + virtual void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) = 0; +%Docstring +Unregister a previously registered tool factory from the development/debugging tools dock. + +.. seealso:: :py:func:`registerDevToolWidgetFactory` + +.. versionadded:: 3.14 +%End + + virtual void registerCustomDropHandler( QgsCustomDropHandler *handler ) = 0; +%Docstring +Register a new custom drop ``handler``. + +.. note:: + + Ownership of ``handler`` is not transferred, and the handler must + be unregistered when plugin is unloaded. + .. seealso:: :py:class:`QgsCustomDropHandler` .. seealso:: :py:func:`unregisterCustomDropHandler` @@ -1136,13 +1173,40 @@ Register a new custom drop handler. virtual void unregisterCustomDropHandler( QgsCustomDropHandler *handler ) = 0; %Docstring -Unregister a previously registered custom drop handler. +Unregister a previously registered custom drop ``handler``. .. seealso:: :py:class:`QgsCustomDropHandler` .. seealso:: :py:func:`registerCustomDropHandler` .. versionadded:: 3.0 +%End + + virtual void registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) = 0; +%Docstring +Register a new custom project open ``handler``. + +.. note:: + + Ownership of ``handler`` is not transferred, and the handler must + be unregistered when plugin is unloaded. + +.. seealso:: :py:class:`QgsCustomProjectOpenHandler` + +.. seealso:: :py:func:`unregisterCustomProjectOpenHandler` + +.. versionadded:: 3.14 +%End + + virtual void unregisterCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) = 0; +%Docstring +Unregister a previously registered custom project open ``handler``. + +.. seealso:: :py:class:`QgsCustomDropHandler` + +.. seealso:: :py:func:`registerCustomProjectOpenHandler` + +.. versionadded:: 3.14 %End virtual void registerCustomLayoutDropHandler( QgsLayoutCustomDropHandler *handler ) = 0; @@ -1151,7 +1215,7 @@ Register a new custom drop ``handler`` for layout windows. .. note:: - Ownership of the factory is not transferred, and the factory must + Ownership of ``handler`` is not transferred, and the handler must be unregistered when plugin is unloaded. .. seealso:: :py:class:`QgsLayoutCustomDropHandler` diff --git a/python/gui/auto_generated/qgscheckablecombobox.sip.in b/python/gui/auto_generated/qgscheckablecombobox.sip.in index 1374b8b80abc..fbdb1a28caf4 100644 --- a/python/gui/auto_generated/qgscheckablecombobox.sip.in +++ b/python/gui/auto_generated/qgscheckablecombobox.sip.in @@ -69,6 +69,14 @@ no items selected. Returns currently checked items. .. seealso:: :py:func:`setCheckedItems` +%End + + QVariantList checkedItemsData() const; +%Docstring +Returns userData (stored in the Qt.UserRole) associated with +currently checked items. + +.. seealso:: :py:func:`checkedItems` %End Qt::CheckState itemCheckState( int index ) const; diff --git a/python/gui/auto_generated/qgscolorbutton.sip.in b/python/gui/auto_generated/qgscolorbutton.sip.in index 13f086f34bf4..2f048047cf0a 100644 --- a/python/gui/auto_generated/qgscolorbutton.sip.in +++ b/python/gui/auto_generated/qgscolorbutton.sip.in @@ -210,11 +210,12 @@ Sets the string to use for the "no color" option in the button's drop-down menu. dialog %End - void setShowNull( bool showNull ); + void setShowNull( bool showNull, const QString &nullString = QString() ); %Docstring Sets whether a set to null (clear) option is shown in the button's drop-down menu. :param showNull: set to ``True`` to show a null option +:param nullString: translated string to use for the null option. If not set, a default "Clear Color" string will be used. .. seealso:: :py:func:`showNull` diff --git a/python/gui/auto_generated/qgscustomprojectopenhandler.sip.in b/python/gui/auto_generated/qgscustomprojectopenhandler.sip.in new file mode 100644 index 000000000000..4ffcb7948925 --- /dev/null +++ b/python/gui/auto_generated/qgscustomprojectopenhandler.sip.in @@ -0,0 +1,70 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgscustomprojectopenhandler.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsCustomProjectOpenHandler : QObject +{ +%Docstring +Abstract base class that may be implemented to handle new project file types within +the QGIS application. + +This interface allows extending the QGIS interface by adding support for opening additional +(non QGS/QGZ) project files, e.g. allowing plugins to add support for opening other +vendor project formats (such as ArcGIS MXD documents or MapInfo WOR workspaces). + +Handler implementations should indicate the file types they support via their filters() +implementation, and then implement handleProjectOpen() to open the associated files. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgscustomprojectopenhandler.h" +%End + public: + + virtual bool handleProjectOpen( const QString &file ) = 0; +%Docstring +Called when the specified project ``file`` has been opened within QGIS. If ``True`` +is returned, then the handler has accepted this file and it should not +be further processed (e.g. by other QgsCustomProjectOpenHandler). + +It it is the subclasses' responsibility to ignore file types it cannot handle +by returning ``False`` for these. + +The base class implementation does nothing. +%End + + virtual QStringList filters() const = 0; +%Docstring +Returns file filters associated with this handler, e.g. "MXD Documents (*.mxd)", "MapInfo Workspaces (*.wor)". + +Each individual filter should be reflected as one entry in the returned list. +%End + + virtual bool createDocumentThumbnailAfterOpen() const; +%Docstring +Returns ``True`` if a document thumbnail should automatically be created after opening the project. + +The default behavior is to return ``False``. +%End + + virtual QIcon icon() const; +%Docstring +Returns a custom icon used to represent this handler. +%End +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgscustomprojectopenhandler.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgsdatabaseschemacombobox.sip.in b/python/gui/auto_generated/qgsdatabaseschemacombobox.sip.in index df658c5a338d..bbd752c2a901 100644 --- a/python/gui/auto_generated/qgsdatabaseschemacombobox.sip.in +++ b/python/gui/auto_generated/qgsdatabaseschemacombobox.sip.in @@ -10,6 +10,7 @@ + class QgsDatabaseSchemaComboBox : QWidget { %Docstring @@ -43,6 +44,20 @@ Constructor for QgsDatabaseSchemaComboBox, for the specified ``provider`` and `` Constructor for QgsDatabaseSchemaComboBox, for the specified ``connection``. Ownership of ``connection`` is transferred to the combobox. +%End + + void setAllowEmptySchema( bool allowEmpty ); +%Docstring +Sets whether an optional empty schema ("not set") option is present in the combobox. + +.. seealso:: :py:func:`allowEmptySchema` +%End + + bool allowEmptySchema() const; +%Docstring +Returns ``True`` if the combobox allows the empty schema ("not set") choice. + +.. seealso:: :py:func:`setAllowEmptySchema` %End QString currentSchema() const; diff --git a/python/gui/auto_generated/qgsdatabasetablecombobox.sip.in b/python/gui/auto_generated/qgsdatabasetablecombobox.sip.in index b875b73cdfa8..2770d33a0308 100644 --- a/python/gui/auto_generated/qgsdatabasetablecombobox.sip.in +++ b/python/gui/auto_generated/qgsdatabasetablecombobox.sip.in @@ -10,6 +10,7 @@ + class QgsDatabaseTableComboBox : QWidget { %Docstring @@ -47,6 +48,20 @@ Constructor for QgsDatabaseTableComboBox, for the specified ``connection``. The optional ``schema`` argument can be used to restrict the listed tables to a specific schema. Ownership of ``connection`` is transferred to the combobox. +%End + + void setAllowEmptyTable( bool allowEmpty ); +%Docstring +Sets whether an optional empty table ("not set") option is present in the combobox. + +.. seealso:: :py:func:`allowEmptyTable` +%End + + bool allowEmptyTable() const; +%Docstring +Returns ``True`` if the combobox allows the empty table ("not set") choice. + +.. seealso:: :py:func:`setAllowEmptyTable` %End QString currentTable() const; diff --git a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in index c232885fcad2..24772d5dc177 100644 --- a/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in +++ b/python/gui/auto_generated/qgsdatasourceselectdialog.sip.in @@ -9,10 +9,10 @@ -class QgsDataSourceSelectDialog: QDialog +class QgsDataSourceSelectWidget: QgsPanelWidget { %Docstring -The QgsDataSourceSelectDialog class embeds the browser view to +The QgsDataSourceSelectWidget class embeds the browser view to select an existing data source. By default any layer type can be chosen, the valid layer @@ -23,7 +23,7 @@ directly from the constructor. To retrieve the selected data source, uri() can be called and it will return a (possibly invalid) QgsMimeDataUtils.Uri. -.. versionadded:: 3.6 +.. versionadded:: 3.14 %End %TypeHeaderCode @@ -31,12 +31,12 @@ will return a (possibly invalid) QgsMimeDataUtils.Uri. %End public: - QgsDataSourceSelectDialog( QgsBrowserGuiModel *browserModel = 0, + QgsDataSourceSelectWidget( QgsBrowserGuiModel *browserModel = 0, bool setFilterByLayerType = false, QgsMapLayerType layerType = QgsMapLayerType::VectorLayer, QWidget *parent = 0 ); %Docstring -Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type +Constructs a QgsDataSourceSelectWidget, optionally filtering by layer type :param browserModel: an existing browser model (typically from app), if ``None`` an instance will be created :param setFilterByLayerType: activates filtering by layer type @@ -45,7 +45,7 @@ Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type %End - ~QgsDataSourceSelectDialog(); + ~QgsDataSourceSelectWidget(); void setLayerTypeFilter( QgsMapLayerType layerType ); %Docstring @@ -90,6 +90,103 @@ Apply filter to the model %Docstring Scroll to last selected index and expand it's children +%End + + signals: + + void validationChanged( bool isValid ); +%Docstring +This signal is emitted whenever the validation status of the widget changes. + +:param isValid: ``True`` if the current status of the widget is valid +%End + + void selectionChanged(); +%Docstring +Emitted when the current selection changes in the widget. +%End + + void itemTriggered( const QgsMimeDataUtils::Uri &uri ); +%Docstring +Emitted when an item is triggered, e.g. via a double-click. +%End + +}; + + +class QgsDataSourceSelectDialog: QDialog +{ +%Docstring +The QgsDataSourceSelectDialog class embeds the browser view to +select an existing data source. + +By default any layer type can be chosen, the valid layer +type can be restricted by setting a layer type filter with +setLayerTypeFilter(layerType) or by activating the filter +directly from the constructor. + +To retrieve the selected data source, uri() can be called and it +will return a (possibly invalid) QgsMimeDataUtils.Uri. + +.. versionadded:: 3.6 +%End + +%TypeHeaderCode +#include "qgsdatasourceselectdialog.h" +%End + public: + + QgsDataSourceSelectDialog( QgsBrowserGuiModel *browserModel = 0, + bool setFilterByLayerType = false, + QgsMapLayerType layerType = QgsMapLayerType::VectorLayer, + QWidget *parent = 0 ); +%Docstring +Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type + +:param browserModel: an existing browser model (typically from app), if ``None`` an instance will be created +:param setFilterByLayerType: activates filtering by layer type +:param layerType: sets the layer type filter, this is in effect only if filtering by layer type is also active +:param parent: the object +%End + + void setLayerTypeFilter( QgsMapLayerType layerType ); +%Docstring +Sets layer type filter to ``layerType`` and activates the filtering +%End + + void setDescription( const QString &description ); +%Docstring +Sets a description label + +:param description: a description string + +.. note:: + + the description will be displayed at the bottom of the dialog + +.. versionadded:: 3.8 +%End + + QgsMimeDataUtils::Uri uri() const; +%Docstring +Returns the (possibly invalid) uri of the selected data source +%End + + void showFilterWidget( bool visible ); +%Docstring +Show/hide filter widget +%End + void setFilterSyntax( QAction * ); +%Docstring +Sets filter syntax +%End + void setCaseSensitive( bool caseSensitive ); +%Docstring +Sets filter case sensitivity +%End + void setFilter(); +%Docstring +Apply filter to the model %End }; diff --git a/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in b/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in index 3e1d5052dce1..ee186e536341 100644 --- a/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in +++ b/python/gui/auto_generated/qgsexpressionbuilderwidget.sip.in @@ -10,104 +10,58 @@ -class QgsExpressionItem : QStandardItem + + + +class QgsExpressionBuilderWidget : QWidget { %Docstring -An expression item that can be used in the QgsExpressionBuilderWidget tree. +A reusable widget that can be used to build a expression string. +See QgsExpressionBuilderDialog for example of usage. %End %TypeHeaderCode #include "qgsexpressionbuilderwidget.h" %End public: - enum ItemType + + enum Flag { - Header, - Field, - ExpressionNode + LoadNothing, + LoadRecent, + LoadUserExpressions, + LoadAll, }; + typedef QFlags Flags; - QgsExpressionItem( const QString &label, - const QString &expressionText, - const QString &helpText, - QgsExpressionItem::ItemType itemType = ExpressionNode ); - - QgsExpressionItem( const QString &label, - const QString &expressionText, - QgsExpressionItem::ItemType itemType = ExpressionNode ); - QString getExpressionText() const; - QString getHelpText() const; + QgsExpressionBuilderWidget( QWidget *parent /TransferThis/ = 0 ); %Docstring -Gets the help text that is associated with this expression item. - -:return: The help text. +Create a new expression builder widget with an optional parent. %End + ~QgsExpressionBuilderWidget(); - void setHelpText( const QString &helpText ); + void init( const QgsExpressionContext &context = QgsExpressionContext(), const QString &recentCollection = QStringLiteral( "generic" ), const Flags &flags = LoadAll ); %Docstring -Set the help text for the current item +Initialize without any layer -.. note:: - - The help text can be set as a html string. +.. versionadded:: 3.14 %End - QgsExpressionItem::ItemType getItemType() const; + void initWithLayer( QgsVectorLayer *layer, const QgsExpressionContext &context = QgsExpressionContext(), const QString &recentCollection = QStringLiteral( "generic" ), const Flags &flags = LoadAll ); %Docstring -Gets the type of expression item, e.g., header, field, ExpressionNode. +Initialize with a layer -:return: The QgsExpressionItem.ItemType +.. versionadded:: 3.14 %End - static const int CUSTOM_SORT_ROLE; - static const int ITEM_TYPE_ROLE; - static const int SEARCH_TAGS_ROLE; - -}; - -class QgsExpressionItemSearchProxy : QSortFilterProxyModel -{ + void initWithFields( const QgsFields &fields, const QgsExpressionContext &context = QgsExpressionContext(), const QString &recentCollection = QStringLiteral( "generic" ), const Flags &flags = LoadAll ); %Docstring -Search proxy used to filter the QgsExpressionBuilderWidget tree. -The default search for a tree model only searches top level this will handle one -level down -%End - -%TypeHeaderCode -#include "qgsexpressionbuilderwidget.h" -%End - public: - QgsExpressionItemSearchProxy(); - - virtual bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const; - - - protected: - - virtual bool lessThan( const QModelIndex &left, const QModelIndex &right ) const; - -}; - - -class QgsExpressionBuilderWidget : QWidget -{ -%Docstring -A reusable widget that can be used to build a expression string. -See QgsExpressionBuilderDialog for example of usage. -%End +Initialize with given fields without any layer -%TypeHeaderCode -#include "qgsexpressionbuilderwidget.h" -%End - public: - - QgsExpressionBuilderWidget( QWidget *parent /TransferThis/ = 0 ); -%Docstring -Create a new expression builder widget with an optional parent. +.. versionadded:: 3.14 %End - ~QgsExpressionBuilderWidget(); void setLayer( QgsVectorLayer *layer ); %Docstring @@ -118,23 +72,33 @@ Sets layer in order to get the fields and values this needs to be called before calling loadFieldNames(). %End - void loadFieldNames(); + QgsVectorLayer *layer() const; %Docstring -Loads all the field names from the layer. -@remarks Should this really be public couldn't we just do this for the user? +Returns the current layer or a None. %End - void loadFieldNames( const QgsFields &fields ); + void loadFieldNames(); +%Docstring + +.. deprecated:: QGIS 3.14 + this is now done automatically +%End - void loadFieldsAndValues( const QMap &fieldValues ); + void loadFieldNames( const QgsFields &fields ); %Docstring -Loads field names and values from the specified map. -.. note:: +.. deprecated:: QGIS 3.14 + use expressionTree()->loadFieldNames() instead +%End - The field values must be quoted appropriately if they are strings. + void loadFieldsAndValues( const QMap &fieldValues ) /Deprecated/; +%Docstring +Loads field names and values from the specified map. .. versionadded:: 2.12 + +.. deprecated:: QGIS 3.14 + this will not do anything, use setLayer() instead %End void setGeomCalculator( const QgsDistanceArea &da ); @@ -187,7 +151,7 @@ preview result and for populating the list of available functions and variables. void setExpressionContext( const QgsExpressionContext &context ); %Docstring Sets the expression context for the widget. The context is used for the expression -preview result and for populating the list of available functions and variables. +preview result and to populate the list of available functions and variables. :param context: expression context @@ -196,58 +160,63 @@ preview result and for populating the list of available functions and variables. .. versionadded:: 2.12 %End - void registerItem( const QString &group, const QString &label, const QString &expressionText, - const QString &helpText = QString(), - QgsExpressionItem::ItemType type = QgsExpressionItem::ExpressionNode, - bool highlightedItem = false, int sortOrder = 1, - QIcon icon = QIcon(), - const QStringList &tags = QStringList() ); + bool isExpressionValid(); %Docstring -Registers a node item for the expression builder. - -:param group: The group the item will be show in the tree view. If the group doesn't exist it will be created. -:param label: The label that is show to the user for the item in the tree. -:param expressionText: The text that is inserted into the expression area when the user double clicks on the item. -:param helpText: The help text that the user will see when item is selected. -:param type: The type of the expression item. -:param highlightedItem: set to ``True`` to make the item highlighted, which inserts a bold copy of the item at the top level -:param sortOrder: sort ranking for item -:param icon: custom icon to show for item -:param tags: tags to find function +Returns if the expression is valid %End - bool isExpressionValid(); - - void saveToRecent( const QString &collection = "generic" ); + void saveToRecent( const QString &collection = "generic" ) /Deprecated/; %Docstring Adds the current expression to the given ``collection``. By default it is saved to the collection "generic". + +.. deprecated:: QGIS 3.14 + use expressionTree()->saveRecent() instead %End - void loadRecent( const QString &collection = QStringLiteral( "generic" ) ); + void loadRecent( const QString &collection = QStringLiteral( "generic" ) )/Deprecated/; %Docstring Loads the recent expressions from the given ``collection``. By default it is loaded from the collection "generic". + +.. deprecated:: QGIS 3.14 + use expressionTree()->loadRecent() instead %End - void loadUserExpressions( ); + QgsExpressionTreeView *expressionTree() const; +%Docstring +Returns the expression tree + +.. versionadded:: 3.14 +%End + + void loadUserExpressions() /Deprecated/; %Docstring Loads the user expressions. +.. deprecated:: QGIS 3.14 + use expressionTree()->loadUserExpressions() instead + .. versionadded:: 3.12 %End - void saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ); + void saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ) /Deprecated/; %Docstring Stores the user ``expression`` with given ``label`` and ``helpText``. +.. deprecated:: QGIS 3.14 + use expressionTree()->saveToUserExpressions() instead + .. versionadded:: 3.12 %End - void removeFromUserExpressions( const QString &label ); + void removeFromUserExpressions( const QString &label ) /Deprecated/; %Docstring Removes the expression ``label`` from the user stored expressions. +.. deprecated:: QGIS 3.14 + use expressionTree()->removeFromUserExpressions() instead + .. versionadded:: 3.12 %End @@ -276,12 +245,14 @@ Loads code into the function editor Updates the list of function files found at the given path %End - QStandardItemModel *model(); + QStandardItemModel *model() /Deprecated/; %Docstring Returns a pointer to the dialog's function item model. This method is exposed for testing purposes only - it should not be used to modify the model. .. versionadded:: 3.0 + +.. deprecated:: QGIS 3.14 %End QgsProject *project(); @@ -379,6 +350,14 @@ Removes the selected expression from the stored user expressions, the selected expression must be a user stored expression. .. versionadded:: 3.12 +%End + + void editSelectedUserExpression(); +%Docstring +Edits the selected expression from the stored user expressions, +the selected expression must be a user stored expression. + +.. versionadded:: 3.14 %End const QList findExpressions( const QString &label ); @@ -386,6 +365,9 @@ the selected expression must be a user stored expression. Returns the list of expression items matching a ``label``. .. versionadded:: 3.12 + +.. deprecated:: QGIS 3.14 + use expressionTree()->findExpressions instead %End @@ -420,6 +402,7 @@ with the context. virtual void showEvent( QShowEvent *e ); + public: }; diff --git a/python/gui/auto_generated/qgsexpressionstoredialog.sip.in b/python/gui/auto_generated/qgsexpressionstoredialog.sip.in index 0a2112dc8b62..1ca660d6a2f8 100644 --- a/python/gui/auto_generated/qgsexpressionstoredialog.sip.in +++ b/python/gui/auto_generated/qgsexpressionstoredialog.sip.in @@ -26,7 +26,7 @@ A generic dialog for editing expression text, label and help text. QgsExpressionStoreDialog( const QString &label, const QString &expression, const QString &helpText, - const QStringList &existingLabels, + const QStringList &existingLabels = QStringList(), QWidget *parent = 0 ); %Docstring Creates a QgsExpressionStoreDialog with given ``label``, ``expression`` and ``helpText``. diff --git a/python/gui/auto_generated/qgsexpressiontreeview.sip.in b/python/gui/auto_generated/qgsexpressiontreeview.sip.in new file mode 100644 index 000000000000..4155558c97e2 --- /dev/null +++ b/python/gui/auto_generated/qgsexpressiontreeview.sip.in @@ -0,0 +1,277 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsexpressiontreeview.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + + +class QgsExpressionItem : QStandardItem +{ +%Docstring +An expression item that can be used in the QgsExpressionBuilderWidget tree. +%End + +%TypeHeaderCode +#include "qgsexpressiontreeview.h" +%End + public: + enum ItemType + { + Header, + Field, + ExpressionNode + }; + + QgsExpressionItem( const QString &label, + const QString &expressionText, + const QString &helpText, + QgsExpressionItem::ItemType itemType = ExpressionNode ); + + QgsExpressionItem( const QString &label, + const QString &expressionText, + QgsExpressionItem::ItemType itemType = ExpressionNode ); + + QString getExpressionText() const; + + QString getHelpText() const; +%Docstring +Gets the help text that is associated with this expression item. + +:return: The help text. +%End + + void setHelpText( const QString &helpText ); +%Docstring +Set the help text for the current item + +.. note:: + + The help text can be set as a html string. +%End + + QgsExpressionItem::ItemType getItemType() const; +%Docstring +Gets the type of expression item, e.g., header, field, ExpressionNode. + +:return: The QgsExpressionItem.ItemType +%End + + static const int CUSTOM_SORT_ROLE; + static const int ITEM_TYPE_ROLE; + static const int SEARCH_TAGS_ROLE; + +}; + + +class QgsExpressionItemSearchProxy : QSortFilterProxyModel +{ +%Docstring +Search proxy used to filter the QgsExpressionBuilderWidget tree. +The default search for a tree model only searches top level this will handle one +level down +%End + +%TypeHeaderCode +#include "qgsexpressiontreeview.h" +%End + public: + QgsExpressionItemSearchProxy(); + + virtual bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const; + + + protected: + + virtual bool lessThan( const QModelIndex &left, const QModelIndex &right ) const; + +}; + +class QgsExpressionTreeView : QTreeView +{ +%Docstring +QgsExpressionTreeView is a tree view to list all expressions +functions, variables and fields that can be used in an expression. + +.. seealso:: :py:class:`QgsExpressionBuilderWidget` + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsexpressiontreeview.h" +%End + public: + + class MenuProvider +{ +%Docstring +Implementation of this interface can be implemented to allow QgsExpressionTreeView +instance to provide custom context menus (opened upon right-click). + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsexpressiontreeview.h" +%End + public: + explicit MenuProvider(); +%Docstring +Constructor +%End + virtual ~MenuProvider(); + + virtual QMenu *createContextMenu( QgsExpressionItem *item ) /Factory/; +%Docstring +Returns a newly created menu instance +%End + }; + + QgsExpressionTreeView( QWidget *parent = 0 ); +%Docstring +Constructor +%End + + void setLayer( QgsVectorLayer *layer ); +%Docstring +Sets layer in order to get the fields and values +%End + + void loadFieldNames( const QgsFields &fields ); +%Docstring +This allows loading fields without specifying a layer +%End + + void setExpressionContext( const QgsExpressionContext &context ); +%Docstring +Sets the expression context for the tree view. The context is used +to populate the list of available functions and variables. + +:param context: expression context + +.. seealso:: :py:func:`expressionContext` +%End + + QgsExpressionContext expressionContext() const; +%Docstring +Returns the expression context for the widget. The context is used for the expression +preview result and for populating the list of available functions and variables. + +.. seealso:: :py:func:`setExpressionContext` +%End + + QgsProject *project(); +%Docstring +Returns the project currently associated with the widget. + +.. seealso:: :py:func:`setProject` +%End + + void setProject( QgsProject *project ); +%Docstring +Sets the ``project`` currently associated with the widget. This +controls which layers and relations and other project-specific items are shown in the widget. + +.. seealso:: :py:func:`project` +%End + + void setMenuProvider( MenuProvider *provider ); +%Docstring +Sets the menu provider. +This does not take ownership of the provider +%End + + void refresh(); +%Docstring +Refreshes the content of the tree +%End + + QgsExpressionItem *currentItem() const; +%Docstring +Returns the current item or a None +%End + + + void loadRecent( const QString &collection = QStringLiteral( "generic" ) ); +%Docstring +Loads the recent expressions from the given ``collection``. +By default it is loaded from the collection "generic". +%End + + void saveToRecent( const QString &expressionText, const QString &collection = "generic" ); +%Docstring +Adds the current expression to the given ``collection``. +By default it is saved to the collection "generic". +%End + + void saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ); +%Docstring +Stores the user ``expression`` with given ``label`` and ``helpText``. +%End + + void removeFromUserExpressions( const QString &label ); +%Docstring +Removes the expression ``label`` from the user stored expressions. +%End + + void loadUserExpressions( ); +%Docstring +Loads the user expressions. +This is done on request since it can be very slow if there are thousands of user expressions +%End + + const QList findExpressions( const QString &label ); +%Docstring +Returns the list of expression items matching a ``label``. +%End + + + QJsonDocument exportUserExpressions(); +%Docstring +Create the expressions JSON document storing all the user expressions to be exported. + +:return: the created expressions JSON file +%End + + void loadExpressionsFromJson( const QJsonDocument &expressionsDocument ); +%Docstring +Load and permanently store the expressions from the expressions JSON document. + +:param expressionsDocument: the parsed expressions JSON file +%End + + signals: + void expressionItemDoubleClicked( const QString &text ); +%Docstring +Emitted when a expression item is double clicked +%End + + void currentExpressionItemChanged( QgsExpressionItem *item ); +%Docstring +Emitter when the current expression item changed +%End + + public slots: + void setSearchText( const QString &text ); +%Docstring +Sets the text to filter the expression tree +%End + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsexpressiontreeview.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgsextentgroupbox.sip.in b/python/gui/auto_generated/qgsextentgroupbox.sip.in index 2426cb489e98..6184944c0a17 100644 --- a/python/gui/auto_generated/qgsextentgroupbox.sip.in +++ b/python/gui/auto_generated/qgsextentgroupbox.sip.in @@ -11,7 +11,6 @@ - class QgsExtentGroupBox : QgsCollapsibleGroupBox { %Docstring @@ -20,7 +19,9 @@ Collapsible group box for configuration of extent, typically for a save operatio Besides allowing the user to enter the extent manually, it comes with options to use original extent or extent defined by the current view in map canvas. -When using the widget, make sure to call setOriginalExtent(), setCurrentExtent() and setOutputCrs() during initialization. +When using the group box, make sure to call setOriginalExtent(), setCurrentExtent() and setOutputCrs() during initialization. + +.. seealso:: :py:class:`QgsExtentWidget` .. versionadded:: 2.4 %End @@ -30,6 +31,7 @@ When using the widget, make sure to call setOriginalExtent(), setCurrentExtent() %End public: + enum ExtentState { OriginalExtent, diff --git a/python/gui/auto_generated/qgsextentwidget.sip.in b/python/gui/auto_generated/qgsextentwidget.sip.in new file mode 100644 index 000000000000..eba2cbd29ee3 --- /dev/null +++ b/python/gui/auto_generated/qgsextentwidget.sip.in @@ -0,0 +1,247 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsextentwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsExtentWidget : QWidget +{ +%Docstring +A widget for configuration of a map extent. + +Besides allowing the user to enter the extent manually, it comes with options to use +original extent or extent defined by the current view in map canvas. + +When using the widget, make sure to call setOriginalExtent(), setCurrentExtent() and setOutputCrs() during initialization. + +.. seealso:: :py:class:`QgsExtentGroupBox` + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsextentwidget.h" +%End + public: + + enum ExtentState + { + OriginalExtent, + CurrentExtent, + UserExtent, + ProjectLayerExtent, + DrawOnCanvas, + }; + + enum WidgetStyle + { + CondensedStyle, + ExpandedStyle, + }; + + explicit QgsExtentWidget( QWidget *parent /TransferThis/ = 0, WidgetStyle style = CondensedStyle ); +%Docstring +Constructor for QgsExtentWidget. +%End + + void setOriginalExtent( const QgsRectangle &originalExtent, const QgsCoordinateReferenceSystem &originalCrs ); +%Docstring +Sets the original extent and coordinate reference system for the widget. This should be called as part of initialization. + +.. seealso:: :py:func:`originalExtent` + +.. seealso:: :py:func:`originalCrs` +%End + + QgsRectangle originalExtent() const; +%Docstring +Returns the original extent set for the widget. + +.. seealso:: :py:func:`setOriginalExtent` + +.. seealso:: :py:func:`originalCrs` +%End + + QgsCoordinateReferenceSystem originalCrs() const; +%Docstring +Returns the original coordinate reference system set for the widget. + +.. seealso:: :py:func:`originalExtent` + +.. seealso:: :py:func:`setOriginalExtent` +%End + + void setCurrentExtent( const QgsRectangle ¤tExtent, const QgsCoordinateReferenceSystem ¤tCrs ); +%Docstring +Sets the current extent to show in the widget - should be called as part of initialization (or whenever current extent changes). +The current extent is usually set to match the current map canvas extent. + +.. seealso:: :py:func:`currentExtent` + +.. seealso:: :py:func:`currentCrs` +%End + + QgsRectangle currentExtent() const; +%Docstring +Returns the current extent set for the widget. The current extent is usually set to match the +current map canvas extent. + +.. seealso:: :py:func:`setCurrentExtent` + +.. seealso:: :py:func:`currentCrs` +%End + + QgsCoordinateReferenceSystem currentCrs() const; +%Docstring +Returns the coordinate reference system for the current extent set for the widget. The current +extent and CRS usually reflects the map canvas extent and CRS. + +.. seealso:: :py:func:`setCurrentExtent` + +.. seealso:: :py:func:`currentExtent` +%End + + void setOutputCrs( const QgsCoordinateReferenceSystem &outputCrs ); +%Docstring +Sets the output CRS - may need to be used for transformation from original/current extent. +Should be called as part of initialization and whenever the the output CRS is changed. +The current extent will be reprojected into the new output CRS. +%End + + QgsRectangle outputExtent() const; +%Docstring +Returns the extent shown in the widget - in output CRS coordinates. + +.. seealso:: :py:func:`outputCrs` +%End + + QgsCoordinateReferenceSystem outputCrs() const; +%Docstring +Returns the current output CRS, used in the display. + +.. seealso:: :py:func:`outputExtent` +%End + + QgsExtentWidget::ExtentState extentState() const; +%Docstring +Returns the currently selected state for the widget's extent. +%End + + void setMapCanvas( QgsMapCanvas *canvas ); +%Docstring +Sets the map canvas to enable dragging of extent on a canvas. + +:param canvas: the map canvas +%End + + QSize ratio() const; +%Docstring +Returns the current fixed aspect ratio to be used when dragging extent onto the canvas. +If the aspect ratio isn't fixed, the width and height will be set to zero. +%End + + QString extentLayerName() const; +%Docstring +Returns the name of the extent layer. +%End + + bool isValid() const; +%Docstring +Returns ``True`` if the widget is in a valid state, i.e. has an extent set. +%End + + void setNullValueAllowed( bool allowed, const QString ¬SetText = QString() ); +%Docstring +Sets whether the widget can be set to a "not set" (null) state. + +The specified ``notSetText`` will be used for showing null values. + +.. note:: + + This mode only applies to widgets in the condensed state! +%End + + public slots: + + void setOutputExtentFromOriginal(); +%Docstring +Sets the output extent to be the same as original extent (may be transformed to output CRS). +%End + + void setOutputExtentFromCurrent(); +%Docstring +Sets the output extent to be the same as current extent (may be transformed to output CRS). +%End + + void setOutputExtentFromUser( const QgsRectangle &extent, const QgsCoordinateReferenceSystem &crs ); +%Docstring +Sets the output extent to a custom extent (may be transformed to output CRS). +%End + + void setOutputExtentFromLayer( const QgsMapLayer *layer ); +%Docstring +Sets the output extent to match a ``layer``'s extent (may be transformed to output CRS). +%End + + void setOutputExtentFromDrawOnCanvas(); +%Docstring +Sets the output extent by dragging on the canvas. +%End + + void setRatio( QSize ratio ); +%Docstring +Sets a fixed aspect ratio to be used when dragging extent onto the canvas. +To unset a fixed aspect ratio, set the width and height to zero. + +:param ratio: aspect ratio's width and height +%End + + void clear(); +%Docstring +Clears the widget, setting it to a null value. +%End + + signals: + + void extentChanged( const QgsRectangle &r ); +%Docstring +Emitted when the widget's extent is changed. +%End + + void validationChanged( bool valid ); +%Docstring +Emitted when the widget's validation state changes. +%End + + void toggleDialogVisibility( bool visible ); +%Docstring +Emitted when the parent dialog visibility must be changed (e.g. +to permit access to the map canvas) +%End + + protected: + + virtual void dragEnterEvent( QDragEnterEvent *event ); + + virtual void dragLeaveEvent( QDragLeaveEvent *event ); + + virtual void dropEvent( QDropEvent *event ); + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsextentwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in b/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in index c26cb0c5a409..6dbaabf72b1e 100644 --- a/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in +++ b/python/gui/auto_generated/qgsfieldexpressionwidget.sip.in @@ -16,7 +16,7 @@ class QgsFieldExpressionWidget : QWidget { %Docstring The QgsFieldExpressionWidget class reates a widget to choose fields and edit expressions -It contains a combo boxto display the fields and expression and a button to open the expression dialog. +It contains a combo box to display the fields and expression and a button to open the expression dialog. The combo box is editable, allowing expressions to be edited inline. The validity of the expression is checked live on key press, invalid expressions are displayed in red. The expression will be added to the model (and the fieldChanged signals emitted) @@ -200,6 +200,13 @@ sets the current row in the widget void setField( const QString &fieldName ); %Docstring sets the current field or expression in the widget +%End + + void setFields( const QgsFields &fields ); +%Docstring +Sets the fields used in the widget to ``fields``, this allows the widget to work without a layer. + +.. versionadded:: 3.14 %End void setExpression( const QString &expression ); diff --git a/python/gui/auto_generated/qgsfieldmappingmodel.sip.in b/python/gui/auto_generated/qgsfieldmappingmodel.sip.in new file mode 100644 index 000000000000..5a3d72db2d5d --- /dev/null +++ b/python/gui/auto_generated/qgsfieldmappingmodel.sip.in @@ -0,0 +1,161 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsfieldmappingmodel.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsFieldMappingModel: QAbstractTableModel +{ +%Docstring +The QgsFieldMappingModel holds mapping information for mapping from one set of QgsFields to another, +for each set of "destination" fields an expression defines how to obtain the values of the +"destination" fields. +The model can be optionally set "editable" allowing to modify all the fields, by default only +the mapping expression is editable. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsfieldmappingmodel.h" +%End + public: + + enum class ColumnDataIndex + { + SourceExpression, + DestinationName, + DestinationType, + DestinationLength, + DestinationPrecision, + DestinationConstraints, + }; + + + struct Field + { + QString originalName; + QgsField field; + QString expression; + }; + + QgsFieldMappingModel( const QgsFields &sourceFields = QgsFields(), + const QgsFields &destinationFields = QgsFields(), + const QMap &expressions = QMap(), + QObject *parent = 0 ); +%Docstring +Constructs a QgsFieldMappingModel from a set of ``sourceFields`` +and ``destinationFields``, initial values for the expressions can be +optionally specified through ``expressions`` which is a map from the original +field name to the corresponding expression. A ``parent`` object +can be also specified. +%End + + bool destinationEditable() const; +%Docstring +Returns ``True`` if the destination fields are editable +%End + + void setDestinationEditable( bool editable ); +%Docstring +Sets the destination fields editable state to ``editable`` +%End + + const QMap dataTypes() const; +%Docstring +Returns a static map of supported data types +%End + + QgsFields sourceFields() const; +%Docstring +Returns a list of source fields +%End + + QList mapping() const; +%Docstring +Returns a list of Field objects representing the current status of the model +%End + + QMap< QString, QgsProperty > fieldPropertyMap() const; +%Docstring +Returns a map of destination field name to QgsProperty definition for field value, +representing the current status of the model. + +.. seealso:: :py:func:`setFieldPropertyMap` +%End + + void setFieldPropertyMap( const QMap< QString, QgsProperty > &map ); +%Docstring +Sets a map of destination field name to QgsProperty definition for field value. + +.. seealso:: :py:func:`fieldPropertyMap` +%End + + void appendField( const QgsField &field, const QString &expression = QString() ); +%Docstring +Appends a new ``field`` to the model, with an optional ``expression`` +%End + + bool removeField( const QModelIndex &index ); +%Docstring +Removes the field at ``index`` from the model, returns ``True`` on success +%End + + bool moveUp( const QModelIndex &index ); +%Docstring +Moves down the field at ``index`` +%End + + bool moveDown( const QModelIndex &index ); +%Docstring +Moves up the field at ``index`` +%End + + void setSourceFields( const QgsFields &sourceFields ); +%Docstring +Set source fields to ``sourceFields`` +%End + + QgsExpressionContextGenerator *contextGenerator() const; +%Docstring +Returns the context generator with the source fields +%End + + void setDestinationFields( const QgsFields &destinationFields, + const QMap &expressions = QMap() ); +%Docstring +Set destination fields to ``destinationFields``, initial values for the expressions can be +optionally specified through ``expressions`` which is a map from the original +field name to the corresponding expression. +%End + + virtual int rowCount( const QModelIndex &parent = QModelIndex() ) const; + + virtual int columnCount( const QModelIndex &parent = QModelIndex() ) const; + + virtual QVariant data( const QModelIndex &index, int role ) const; + + virtual QVariant headerData( int section, Qt::Orientation orientation, int role ) const; + + virtual Qt::ItemFlags flags( const QModelIndex &index ) const; + + virtual bool setData( const QModelIndex &index, const QVariant &value, int role ); + + + public: +}; + + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsfieldmappingmodel.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgsfieldmappingwidget.sip.in b/python/gui/auto_generated/qgsfieldmappingwidget.sip.in new file mode 100644 index 000000000000..37a221103a88 --- /dev/null +++ b/python/gui/auto_generated/qgsfieldmappingwidget.sip.in @@ -0,0 +1,130 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsfieldmappingwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsFieldMappingWidget : QgsPanelWidget +{ +%Docstring +The QgsFieldMappingWidget class creates a mapping from one set of QgsFields to another, +for each set of "destination" fields an expression defines how to obtain the values of the +"destination" fields. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsfieldmappingwidget.h" +%End + public: + + explicit QgsFieldMappingWidget( QWidget *parent = 0, + const QgsFields &sourceFields = QgsFields(), + const QgsFields &destinationFields = QgsFields(), + const QMap &expressions = QMap() ); +%Docstring +Constructs a QgsFieldMappingWidget from a set of ``sourceFields`` +and ``destinationFields``, initial values for the expressions can be +optionally specified through ``expressions`` which is a map from the original +field name to the corresponding expression. A ``parent`` object +can also be specified. +%End + + void setDestinationEditable( bool editable ); +%Docstring +Sets the destination fields editable state to ``editable`` +%End + + bool destinationEditable() const; +%Docstring +Returns ``True`` if the destination fields are editable in the model +%End + + QgsFieldMappingModel *model() const; +%Docstring +Returns the underlying mapping model +%End + + QList mapping() const; +%Docstring +Returns a list of Field objects representing the current status of the underlying mapping model +%End + + QMap< QString, QgsProperty > fieldPropertyMap() const; +%Docstring +Returns a map of destination field name to QgsProperty definition for field value, +representing the current status of the widget. + +.. seealso:: :py:func:`setFieldPropertyMap` +%End + + void setFieldPropertyMap( const QMap< QString, QgsProperty > &map ); +%Docstring +Sets a map of destination field name to QgsProperty definition for field value. + +.. seealso:: :py:func:`fieldPropertyMap` +%End + + QItemSelectionModel *selectionModel(); +%Docstring +Returns the selection model +%End + + void setSourceFields( const QgsFields &sourceFields ); +%Docstring +Set source fields of the underlying mapping model to ``sourceFields`` +%End + + void setDestinationFields( const QgsFields &destinationFields, + const QMap &expressions = QMap() ); +%Docstring +Set destination fields to ``destinationFields`` in the underlying model, +initial values for the expressions can be optionally specified through +``expressions`` which is a map from the original field name to the +corresponding expression. +%End + + void scrollTo( const QModelIndex &index ) const; +%Docstring +Scroll the fields view to ``index`` +%End + + public slots: + + void appendField( const QgsField &field, const QString &expression = QString() ); +%Docstring +Appends a new ``field`` to the model, with an optional ``expression`` +%End + + bool removeSelectedFields( ); +%Docstring +Removes the currently selected field from the model +%End + + bool moveSelectedFieldsUp( ); +%Docstring +Moves down currently selected field +%End + + bool moveSelectedFieldsDown( ); +%Docstring +Moves up the currently selected field +%End + + public: + public: +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgsfieldmappingwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgshighlightablelineedit.sip.in b/python/gui/auto_generated/qgshighlightablelineedit.sip.in new file mode 100644 index 000000000000..cd6504e456a9 --- /dev/null +++ b/python/gui/auto_generated/qgshighlightablelineedit.sip.in @@ -0,0 +1,58 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgshighlightablelineedit.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsHighlightableLineEdit: QgsFilterLineEdit +{ +%Docstring + +A QgsFilterLineEdit subclass with the ability to "highlight" the edges of the widget. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgshighlightablelineedit.h" +%End + public: + + QgsHighlightableLineEdit( QWidget *parent /TransferThis/ = 0 ); +%Docstring +Constructor for QgsHighlightableLineEdit, with the specified ``parent`` widget. +%End + + bool isHighlighted() const; +%Docstring +Returns ``True`` if the line edit is currently highlighted. + +.. seealso:: :py:func:`setHighlighted` +%End + + void setHighlighted( bool highlighted ); +%Docstring +Sets whether the line edit is currently ``highlighted``. + +.. seealso:: :py:func:`isHighlighted` +%End + + protected: + virtual void paintEvent( QPaintEvent *e ); + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/qgshighlightablelineedit.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/qgsmapcanvas.sip.in b/python/gui/auto_generated/qgsmapcanvas.sip.in index 0ab7f1ab185a..c4ed7b9c0cf3 100644 --- a/python/gui/auto_generated/qgsmapcanvas.sip.in +++ b/python/gui/auto_generated/qgsmapcanvas.sip.in @@ -78,6 +78,14 @@ Gets access to properties used for map rendering Sets the temporal controller, this controller will be used to update the canvas temporal range. +.. versionadded:: 3.14 +%End + + const QgsTemporalController *temporalController() const; +%Docstring +Gets access to the temporal controller that will be used to +update the canvas temporal range. + .. versionadded:: 3.14 %End @@ -184,7 +192,15 @@ Returns the combined extent for all layers on the map canvas void setExtent( const QgsRectangle &r, bool magnified = false ); %Docstring -Sets the extent of the map canvas +Sets the extent of the map canvas to the specified rectangle. + +The ``magnified`` argument dictates whether existing canvas constraints such +as a scale lock should be respected or not during the operation. If ``magnified`` is +``True`` then an existing scale lock constraint will be applied. This means that the final +visible canvas extent may not match the specified extent. + +If ``magnified`` is ``False`` then scale lock settings will be ignored, and the specified +rectangle will ALWAYS be visible in the canvas. %End bool setReferencedExtent( const QgsReferencedRectangle &extent ) throw( QgsCsException ); @@ -483,16 +499,22 @@ returns current layer (set by legend widget) Sets wheel zoom factor (should be greater than 1) %End - void zoomScale( double scale ); + void zoomScale( double scale, bool ignoreScaleLock = false ); %Docstring Zooms the canvas to a specific ``scale``. The scale value indicates the scale denominator, e.g. 1000.0 for a 1:1000 map. + +If ``ignoreScaleLock`` is set to ``True``, then any existing constraint on the map scale +of the canvas will be ignored during the zoom operation. %End - void zoomByFactor( double scaleFactor, const QgsPointXY *center = 0 ); + void zoomByFactor( double scaleFactor, const QgsPointXY *center = 0, bool ignoreScaleLock = false ); %Docstring Zoom with the factor supplied. Factor > 1 zooms out, interval (0,1) zooms in -If point is given, re-center on it +If point is given, re-center on it. + +If ``ignoreScaleLock`` is set to ``True``, then any existing constraint on the map scale +of the canvas will be ignored during the zoom operation. %End void zoomWithCenter( int x, int y, bool zoomIn ); diff --git a/python/gui/auto_generated/qgsmaplayerconfigwidgetfactory.sip.in b/python/gui/auto_generated/qgsmaplayerconfigwidgetfactory.sip.in index 9dd7fe506cf3..c0875d47388c 100644 --- a/python/gui/auto_generated/qgsmaplayerconfigwidgetfactory.sip.in +++ b/python/gui/auto_generated/qgsmaplayerconfigwidgetfactory.sip.in @@ -108,7 +108,7 @@ Check if the layer is supported for this widget. :return: ``True`` if this layer is supported for this widget %End - virtual QgsMapLayerConfigWidget *createWidget( QgsMapLayer *layer, QgsMapCanvas *canvas, bool dockWidget = true, QWidget *parent /TransferThis/ = 0 ) const = 0 /Factory/; + virtual QgsMapLayerConfigWidget *createWidget( QgsMapLayer *layer, QgsMapCanvas *canvas, bool dockWidget = true, QWidget *parent = 0 ) const = 0 /Factory/; %Docstring Factory function to create the widget on demand as needed by the dock. diff --git a/python/gui/auto_generated/qgsmaptoolextent.sip.in b/python/gui/auto_generated/qgsmaptoolextent.sip.in index c1fccf94a43c..c9e18c149a4d 100644 --- a/python/gui/auto_generated/qgsmaptoolextent.sip.in +++ b/python/gui/auto_generated/qgsmaptoolextent.sip.in @@ -10,7 +10,6 @@ - class QgsMapToolExtent : QgsMapTool { %Docstring diff --git a/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in b/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in index cbefe2312891..713ee1c23e9a 100644 --- a/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in +++ b/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in @@ -10,7 +10,7 @@ -class QgsNewDatabaseTableNameWidget : QWidget +class QgsNewDatabaseTableNameWidget : QgsPanelWidget { %Docstring The QgsNewDatabaseTableNameWidget class embeds the browser view to @@ -44,6 +44,13 @@ Constructs a new QgsNewDatabaseTableNameWidget shown in the widget, if not specified all providers data items with database capabilities will be shown :param parent: optional parent for this widget +%End + + void setAcceptButtonVisible( bool visible ); +%Docstring +Sets whether the optional "Ok"/accept button should be visible. + +By default this is hidden, to better allow the widget to be embedded inside other widgets and dialogs. %End QString schema() const; @@ -121,9 +128,81 @@ This signal is emitted when the URI of the new table changes, whether or not it :param uri: URI string representation %End + void accepted(); +%Docstring +Emitted when the OK/accept button is clicked. +%End }; + +class QgsNewDatabaseTableNameDialog: QDialog +{ +%Docstring +QgsNewDatabaseTableNameDialog is a dialog which allows selection of a DB schema and a new table name. + +The table name is validated for uniqueness and the selected +data item provider, schema and table names can be retrieved with +getters. + +.. warning:: + + The data provider that originated the data item provider + must support the connections API + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsnewdatabasetablenamewidget.h" +%End + public: + + explicit QgsNewDatabaseTableNameDialog( QgsBrowserGuiModel *browserModel = 0, + const QStringList &providersFilter = QStringList(), + QWidget *parent = 0 ); +%Docstring +Constructs a new QgsNewDatabaseTableNameDialog + +:param browserModel: an existing browser model (typically from app), if NULL an instance will be created +:param providersFilter: optional white list of data provider keys that should be + shown in the widget, if not specified all providers data items with database + capabilities will be shown +:param parent: optional parent for this widget +%End + + QString schema() const; +%Docstring +Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table +%End + + QString uri() const; +%Docstring +Returns the (possibly blank) string representation of the new table data source URI. +The URI might be invalid in case the widget is not in a valid state. +%End + + QString table() const; +%Docstring +Returns the current name of the new table +%End + + QString dataProviderKey() const; +%Docstring +Returns the currently selected data item provider key +%End + + bool isValid() const; +%Docstring +Returns ``True`` if the widget contains a valid new table name +%End + + QString validationError() const; +%Docstring +Returns the validation error or an empty string is the widget status is valid +%End + +}; /************************************************************************ * This file has been generated automatically from * * * diff --git a/python/gui/auto_generated/qgsproviderconnectioncombobox.sip.in b/python/gui/auto_generated/qgsproviderconnectioncombobox.sip.in index 8fcda4f2b014..9d04b3342e43 100644 --- a/python/gui/auto_generated/qgsproviderconnectioncombobox.sip.in +++ b/python/gui/auto_generated/qgsproviderconnectioncombobox.sip.in @@ -33,6 +33,17 @@ The QgsProviderConnectionComboBox class is a combo box which displays the list o %Docstring Constructor for QgsProviderConnectionComboBox, for the specified ``provider``. +.. warning:: + + The provider must support the connection API methods in its QgsProviderMetadata implementation + in order for the model to work correctly. +%End + + + void setProvider( const QString &provider ); +%Docstring +Sets the provider to be used. + .. warning:: The provider must support the connection API methods in its QgsProviderMetadata implementation diff --git a/python/gui/auto_generated/qgsrelationeditorwidget.sip.in b/python/gui/auto_generated/qgsrelationeditorwidget.sip.in index fa7f7b1c0f5b..1cbca547d003 100644 --- a/python/gui/auto_generated/qgsrelationeditorwidget.sip.in +++ b/python/gui/auto_generated/qgsrelationeditorwidget.sip.in @@ -74,6 +74,18 @@ Sets the ``feature`` being edited and updates the UI unless ``update`` is set to void setEditorContext( const QgsAttributeEditorContext &context ); %Docstring Sets the editor ``context`` + +.. note:: + + if context cadDockWidget is null, it won't be possible to digitize + the geometry of a referencing feature from this widget +%End + + QgsAttributeEditorContext editorContext( ) const; +%Docstring +Returns the attribute editor context. + +.. versionadded:: 3.14 %End QgsIFeatureSelectionManager *featureSelectionManager(); diff --git a/python/gui/auto_generated/qgstemporalcontrollerdockwidget.sip.in b/python/gui/auto_generated/qgstemporalcontrollerwidget.sip.in similarity index 72% rename from python/gui/auto_generated/qgstemporalcontrollerdockwidget.sip.in rename to python/gui/auto_generated/qgstemporalcontrollerwidget.sip.in index 6514d5c8d260..5577472cc1eb 100644 --- a/python/gui/auto_generated/qgstemporalcontrollerdockwidget.sip.in +++ b/python/gui/auto_generated/qgstemporalcontrollerwidget.sip.in @@ -1,7 +1,7 @@ /************************************************************************ * This file has been generated automatically from * * * - * src/gui/qgstemporalcontrollerdockwidget.h * + * src/gui/qgstemporalcontrollerwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ @@ -11,22 +11,22 @@ -class QgsTemporalControllerDockWidget : QgsDockWidget +class QgsTemporalControllerWidget : QgsPanelWidget { %Docstring -The QgsTemporalControllerDockWidget class +A widget for controlling playback properties of a QgsTemporalController. .. versionadded:: 3.14 %End %TypeHeaderCode -#include "qgstemporalcontrollerdockwidget.h" +#include "qgstemporalcontrollerwidget.h" %End public: - QgsTemporalControllerDockWidget( const QString &name, QWidget *parent /TransferThis/ = 0 ); + QgsTemporalControllerWidget( QWidget *parent /TransferThis/ = 0 ); %Docstring -Constructor for QgsTemporalControllerDockWidget, with the specified ``parent`` widget. +Constructor for QgsTemporalControllerWidget, with the specified ``parent`` widget. %End QgsTemporalController *temporalController(); @@ -41,7 +41,7 @@ The dock widget retains ownership of the returned object. /************************************************************************ * This file has been generated automatically from * * * - * src/gui/qgstemporalcontrollerdockwidget.h * + * src/gui/qgstemporalcontrollerwidget.h * * * * Do not edit manually ! Edit header and run scripts/sipify.pl again * ************************************************************************/ diff --git a/python/gui/auto_generated/raster/qgsrasterbandcombobox.sip.in b/python/gui/auto_generated/raster/qgsrasterbandcombobox.sip.in index 1ca926852204..06609d6fafb7 100644 --- a/python/gui/auto_generated/raster/qgsrasterbandcombobox.sip.in +++ b/python/gui/auto_generated/raster/qgsrasterbandcombobox.sip.in @@ -56,6 +56,11 @@ Optionally the built in "not set" text can be overridden by specifying a ``string``. .. seealso:: :py:func:`setShowNotSetOption` +%End + + static QString displayBandName( QgsRasterDataProvider *provider, int band ); +%Docstring +Returns a user-friendly band name for the specified ``band``. %End public slots: diff --git a/python/gui/auto_generated/raster/qgsrastercontourrendererwidget.sip.in b/python/gui/auto_generated/raster/qgsrastercontourrendererwidget.sip.in new file mode 100644 index 000000000000..6d71beba279d --- /dev/null +++ b/python/gui/auto_generated/raster/qgsrastercontourrendererwidget.sip.in @@ -0,0 +1,43 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/raster/qgsrastercontourrendererwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + +class QgsRasterContourRendererWidget : QgsRasterRendererWidget +{ +%Docstring +Configuration widget for QgsRasterContourRenderer + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsrastercontourrendererwidget.h" +%End + public: + QgsRasterContourRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); +%Docstring +Constructs the widget +%End + + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) /Factory/; +%Docstring +Widget creation function (mainly for the use by the renderer registry) +%End + + virtual QgsRasterRenderer *renderer(); + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/gui/raster/qgsrastercontourrendererwidget.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/gui/auto_generated/raster/qgsrasterlayerproperties.sip.in b/python/gui/auto_generated/raster/qgsrasterlayerproperties.sip.in index e8d8b3ce8394..bdf404bc4661 100644 --- a/python/gui/auto_generated/raster/qgsrasterlayerproperties.sip.in +++ b/python/gui/auto_generated/raster/qgsrasterlayerproperties.sip.in @@ -34,6 +34,13 @@ Constructor :param canvas: the QgsMapCanvas instance :param parent: the parent of this widget :param fl: windows flag +%End + + void setCurrentPage( const QString &page ); +%Docstring +Sets the dialog ``page`` (by object name) to show. + +.. versionadded:: 3.14 %End protected slots: diff --git a/python/gui/auto_generated/tableeditor/qgstableeditordialog.sip.in b/python/gui/auto_generated/tableeditor/qgstableeditordialog.sip.in index bf9e76c8ce5b..f1341793f12c 100644 --- a/python/gui/auto_generated/tableeditor/qgstableeditordialog.sip.in +++ b/python/gui/auto_generated/tableeditor/qgstableeditordialog.sip.in @@ -34,6 +34,16 @@ Constructor for QgsTableEditorDialog with the specified ``parent`` widget. %Docstring Sets the ``contents`` to show in the editor widget. +.. seealso:: :py:func:`tableContents` +%End + + + bool setTableContentsFromClipboard(); +%Docstring +Parses the clipboard text into contents to show in the editor widget. + +:return: ``True`` if the clipboard was successfully parsed + .. seealso:: :py:func:`tableContents` %End diff --git a/python/gui/gui_auto.sip b/python/gui/gui_auto.sip index e37e508ecf23..16e83f5f76fd 100644 --- a/python/gui/gui_auto.sip +++ b/python/gui/gui_auto.sip @@ -54,6 +54,7 @@ %Include auto_generated/qgscredentialdialog.sip %Include auto_generated/qgscurveeditorwidget.sip %Include auto_generated/qgscustomdrophandler.sip +%Include auto_generated/qgscustomprojectopenhandler.sip %Include auto_generated/qgsdatabaseschemacombobox.sip %Include auto_generated/qgsdatabasetablecombobox.sip %Include auto_generated/qgsdataitemguiprovider.sip @@ -74,12 +75,16 @@ %Include auto_generated/qgsexpressionhighlighter.sip %Include auto_generated/qgsexpressionlineedit.sip %Include auto_generated/qgsexpressionselectiondialog.sip +%Include auto_generated/qgsexpressiontreeview.sip %Include auto_generated/qgsextentgroupbox.sip +%Include auto_generated/qgsextentwidget.sip %Include auto_generated/qgsexternalresourcewidget.sip %Include auto_generated/qgsfeaturelistcombobox.sip %Include auto_generated/qgsfeatureselectiondlg.sip %Include auto_generated/qgsfieldcombobox.sip %Include auto_generated/qgsfieldexpressionwidget.sip +%Include auto_generated/qgsfieldmappingwidget.sip +%Include auto_generated/qgsfieldmappingmodel.sip %Include auto_generated/qgsfieldvalidator.sip %Include auto_generated/qgsfieldvalueslineedit.sip %Include auto_generated/qgsfilecontentsourcelineedit.sip @@ -98,6 +103,7 @@ %Include auto_generated/qgsgui.sip %Include auto_generated/qgshelp.sip %Include auto_generated/qgshighlight.sip +%Include auto_generated/qgshighlightablelineedit.sip %Include auto_generated/qgshistogramwidget.sip %Include auto_generated/qgsidentifymenu.sip %Include auto_generated/qgskeyvaluewidget.sip @@ -197,7 +203,7 @@ %Include auto_generated/qgstablewidgetitem.sip %Include auto_generated/qgstabwidget.sip %Include auto_generated/qgstaskmanagerwidget.sip -%Include auto_generated/qgstemporalcontrollerdockwidget.sip +%Include auto_generated/qgstemporalcontrollerwidget.sip %Include auto_generated/qgstextformatwidget.sip %Include auto_generated/qgstextpreview.sip %Include auto_generated/qgstreewidgetitem.sip @@ -239,6 +245,8 @@ %Include auto_generated/auth/qgsauthsslimportdialog.sip %Include auto_generated/auth/qgsauthtrustedcasdialog.sip %Include auto_generated/callouts/qgscalloutwidget.sip +%Include auto_generated/devtools/qgsdevtoolwidget.sip +%Include auto_generated/devtools/qgsdevtoolwidgetfactory.sip %Include auto_generated/editorwidgets/core/qgseditorconfigwidget.sip %Include auto_generated/editorwidgets/core/qgseditorwidgetautoconf.sip %Include auto_generated/editorwidgets/core/qgseditorwidgetfactory.sip @@ -309,7 +317,9 @@ %Include auto_generated/processing/qgsprocessingmaplayercombobox.sip %Include auto_generated/processing/qgsprocessingmodelerparameterwidget.sip %Include auto_generated/processing/qgsprocessingmultipleselectiondialog.sip +%Include auto_generated/processing/qgsprocessingoutputdestinationwidget.sip %Include auto_generated/processing/qgsprocessingparameterdefinitionwidget.sip +%Include auto_generated/processing/qgsprocessingparameterswidget.sip %Include auto_generated/processing/qgsprocessingrecentalgorithmlog.sip %Include auto_generated/processing/qgsprocessingtoolboxmodel.sip %Include auto_generated/processing/qgsprocessingtoolboxtreeview.sip @@ -325,6 +335,7 @@ %Include auto_generated/raster/qgsmultibandcolorrendererwidget.sip %Include auto_generated/raster/qgspalettedrendererwidget.sip %Include auto_generated/raster/qgsrasterbandcombobox.sip +%Include auto_generated/raster/qgsrastercontourrendererwidget.sip %Include auto_generated/raster/qgsrasterhistogramwidget.sip %Include auto_generated/raster/qgsrasterminmaxwidget.sip %Include auto_generated/raster/qgsrasterrendererwidget.sip diff --git a/python/plugins/db_manager/db_manager_plugin.py b/python/plugins/db_manager/db_manager_plugin.py index 049d4a77e854..5e137a6d7a01 100644 --- a/python/plugins/db_manager/db_manager_plugin.py +++ b/python/plugins/db_manager/db_manager_plugin.py @@ -85,7 +85,7 @@ def unload(self): def onLayerWasAdded(self, aMapLayer): # Be able to update every Db layer from Postgres, Spatialite and Oracle - if hasattr(aMapLayer, 'dataProvider') and aMapLayer.dataProvider().name() in ['postgres', 'spatialite', 'oracle']: + if hasattr(aMapLayer, 'dataProvider') and aMapLayer.dataProvider() and aMapLayer.dataProvider().name() in ['postgres', 'spatialite', 'oracle']: self.iface.addCustomActionForLayer(self.layerAction, aMapLayer) # virtual has QUrl source # url = QUrl(QUrl.fromPercentEncoding(l.source())) diff --git a/python/plugins/db_manager/db_plugins/postgis/connector.py b/python/plugins/db_manager/db_plugins/postgis/connector.py index 5a267c65d020..52621d46069c 100644 --- a/python/plugins/db_manager/db_plugins/postgis/connector.py +++ b/python/plugins/db_manager/db_plugins/postgis/connector.py @@ -732,7 +732,16 @@ def getTableEstimatedExtent(self, table, geom): schema, tablename = self.getSchemaTableName(table) schema_part = u"%s," % self.quoteString(schema) if schema is not None else "" - subquery = u"SELECT st_estimated_extent(%s%s,%s) AS extent" % ( + pgis_versions = self.getSpatialInfo()[0].split('.') + pgis_major_version = int(pgis_versions[0]) + pgis_minor_version = int(pgis_versions[1]) + pgis_old = False + if pgis_major_version < 2: + pgis_old = True + elif pgis_major_version == 2 and pgis_minor_version < 1: + pgis_old = True + subquery = u"SELECT %s(%s%s,%s) AS extent" % ( + 'st_estimated_extent' if pgis_old else 'st_estimatedextent', schema_part, self.quoteString(tablename), self.quoteString(geom)) sql = u"""SELECT st_xmin(extent), st_ymin(extent), st_xmax(extent), st_ymax(extent) FROM (%s) AS subquery """ % subquery diff --git a/python/plugins/db_manager/dlg_create_table.py b/python/plugins/db_manager/dlg_create_table.py index 4dc14020a843..490190ed2a92 100644 --- a/python/plugins/db_manager/dlg_create_table.py +++ b/python/plugins/db_manager/dlg_create_table.py @@ -309,6 +309,19 @@ def createTable(self): except (ConnectionError, DbError) as e: DlgDbError.showError(e, self) - return + + # clear UI + self.editName.clear() + self.fields.model().removeRows(0, self.fields.model().rowCount()) + self.cboPrimaryKey.clear() + self.chkGeomColumn.setChecked(False) + self.chkSpatialIndex.setChecked(False) + self.editGeomSrid.clear() + + self.cboGeomType.setEnabled(False) + self.editGeomColumn.setEnabled(False) + self.spinGeomDim.setEnabled(False) + self.editGeomSrid.setEnabled(False) + self.chkSpatialIndex.setEnabled(False) QMessageBox.information(self, self.tr("DB Manager"), self.tr("Table created successfully.")) diff --git a/python/plugins/db_manager/ui/DlgQueryBuilder.ui b/python/plugins/db_manager/ui/DlgQueryBuilder.ui index 8438266d835f..45817d453597 100644 --- a/python/plugins/db_manager/ui/DlgQueryBuilder.ui +++ b/python/plugins/db_manager/ui/DlgQueryBuilder.ui @@ -108,21 +108,21 @@ - Where + Where - Group by + Group by - Order by + Order by diff --git a/python/plugins/processing/algs/gdal/GdalAlgorithmDialog.py b/python/plugins/processing/algs/gdal/GdalAlgorithmDialog.py index d99fbf41a6b7..f0c0d79601f6 100644 --- a/python/plugins/processing/algs/gdal/GdalAlgorithmDialog.py +++ b/python/plugins/processing/algs/gdal/GdalAlgorithmDialog.py @@ -37,14 +37,14 @@ QgsProcessingParameterDefinition) from qgis.gui import (QgsMessageBar, QgsProjectionSelectionWidget, - QgsProcessingAlgorithmDialogBase) + QgsProcessingAlgorithmDialogBase, + QgsProcessingLayerOutputDestinationWidget) from processing.gui.AlgorithmDialog import AlgorithmDialog from processing.gui.AlgorithmDialogBase import AlgorithmDialogBase from processing.gui.ParametersPanel import ParametersPanel from processing.gui.MultipleInputPanel import MultipleInputPanel from processing.gui.NumberInputPanel import NumberInputPanel -from processing.gui.DestinationSelectionPanel import DestinationSelectionPanel from processing.gui.wrappers import WidgetWrapper from processing.tools.dataobjects import createContext @@ -62,8 +62,9 @@ def getParametersPanel(self, alg, parent): class GdalParametersPanel(ParametersPanel): def __init__(self, parent, alg): - ParametersPanel.__init__(self, parent, alg) + super().__init__(parent, alg) + self.dialog = parent w = QWidget() layout = QVBoxLayout() layout.setMargin(0) @@ -75,7 +76,7 @@ def __init__(self, parent, alg): self.text.setReadOnly(True) layout.addWidget(self.text) w.setLayout(layout) - self.layoutMain.addWidget(w) + self.addExtraWidget(w) self.connectParameterSignals() self.parametersHaveChanged() @@ -99,9 +100,6 @@ def connectParameterSignals(self): for c in w.findChildren(QWidget): self.connectWidgetChangedSignals(c) - for output_widget in self.outputWidgets.values(): - self.connectWidgetChangedSignals(output_widget) - def connectWidgetChangedSignals(self, w): if isinstance(w, QLineEdit): w.textChanged.connect(self.parametersHaveChanged) @@ -115,18 +113,18 @@ def connectWidgetChangedSignals(self, w): w.selectionChanged.connect(self.parametersHaveChanged) elif isinstance(w, NumberInputPanel): w.hasChanged.connect(self.parametersHaveChanged) - elif isinstance(w, DestinationSelectionPanel): + elif isinstance(w, QgsProcessingLayerOutputDestinationWidget): w.destinationChanged.connect(self.parametersHaveChanged) def parametersHaveChanged(self): context = createContext() feedback = QgsProcessingFeedback() try: - parameters = self.parent.getParameterValues() - for output in self.alg.destinationParameterDefinitions(): + parameters = self.dialog.createProcessingParameters() + for output in self.algorithm().destinationParameterDefinitions(): if not output.name() in parameters or parameters[output.name()] is None: parameters[output.name()] = self.tr("[temporary file]") - for p in self.alg.parameterDefinitions(): + for p in self.algorithm().parameterDefinitions(): if p.flags() & QgsProcessingParameterDefinition.FlagHidden: continue @@ -136,7 +134,7 @@ def parametersHaveChanged(self): self.text.setPlainText('') return - commands = self.alg.getConsoleCommands(parameters, context, feedback, executing=False) + commands = self.algorithm().getConsoleCommands(parameters, context, feedback, executing=False) commands = [c for c in commands if c not in ['cmd.exe', '/C ']] self.text.setPlainText(" ".join(commands)) except AlgorithmDialogBase.InvalidParameterValue as e: diff --git a/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py b/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py index bd10cdbf107d..5628537281e4 100644 --- a/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py +++ b/python/plugins/processing/algs/gdal/GdalAlgorithmProvider.py @@ -37,7 +37,7 @@ from .ClipRasterByExtent import ClipRasterByExtent from .ClipRasterByMask import ClipRasterByMask from .ColorRelief import ColorRelief -from .contour import contour +from .contour import contour, contour_polygon from .Datasources2Vrt import Datasources2Vrt from .fillnodata import fillnodata from .gdalinfo import gdalinfo @@ -147,6 +147,7 @@ def loadAlgorithms(self): ClipRasterByMask(), ColorRelief(), contour(), + contour_polygon(), Datasources2Vrt(), fillnodata(), gdalinfo(), diff --git a/python/plugins/processing/algs/gdal/GdalUtils.py b/python/plugins/processing/algs/gdal/GdalUtils.py index 265eda3907cc..c571934b9767 100644 --- a/python/plugins/processing/algs/gdal/GdalUtils.py +++ b/python/plugins/processing/algs/gdal/GdalUtils.py @@ -41,7 +41,9 @@ QgsMessageLog, QgsSettings, QgsCredentials, - QgsDataSourceUri) + QgsDataSourceUri, + QgsProjUtils, + QgsCoordinateReferenceSystem) from processing.core.ProcessingConfig import ProcessingConfig from processing.tools.system import isWindows, isMac @@ -432,8 +434,12 @@ def gdal_crs_string(crs): :param crs: crs to convert :return: gdal friendly string """ - if crs.authid().upper().startswith('EPSG:'): + if crs.authid().upper().startswith('EPSG:') or crs.authid().upper().startswith('IGNF:') or crs.authid().upper().startswith('ESRI:'): return crs.authid() + if QgsProjUtils.projVersionMajor() >= 6: + # use WKT + return crs.toWkt(QgsCoordinateReferenceSystem.WKT2_2018) + # fallback to proj4 string, stripping out newline characters return crs.toProj().replace('\n', ' ').replace('\r', ' ') diff --git a/python/plugins/processing/algs/gdal/contour.py b/python/plugins/processing/algs/gdal/contour.py index 42328dc1f8bc..de09ac58e555 100644 --- a/python/plugins/processing/algs/gdal/contour.py +++ b/python/plugins/processing/algs/gdal/contour.py @@ -139,7 +139,7 @@ def groupId(self): def commandName(self): return 'gdal_contour' - def getConsoleCommands(self, parameters, context, feedback, executing=True): + def _buildArgsList(self, parameters, context, feedback, executing): inLayer = self.parameterAsRasterLayer(parameters, self.INPUT, context) if inLayer is None: raise QgsProcessingException(self.invalidRasterError(parameters, self.INPUT)) @@ -192,5 +192,61 @@ def getConsoleCommands(self, parameters, context, feedback, executing=True): arguments.append(inLayer.source()) arguments.append(output) + return arguments + + def getConsoleCommands(self, parameters, context, feedback, executing=True): + arguments = self._buildArgsList(parameters, context, feedback, executing) + return [self.commandName(), GdalUtils.escapeAndJoin(arguments)] + + +class contour_polygon(contour): + + FIELD_NAME_MIN = 'FIELD_NAME_MIN' + FIELD_NAME_MAX = 'FIELD_NAME_MAX' + + def __init__(self): + super().__init__() + + def initAlgorithm(self, config=None): + super().initAlgorithm(config) + # FIELD_NAME isn't used in polygon mode + self.removeParameter(contour.FIELD_NAME) + + self.addParameter(QgsProcessingParameterString(self.FIELD_NAME_MIN, + self.tr('Attribute name for minimum elevation of contour polygon'), + defaultValue='ELEV_MIN', + optional=True)) + + self.addParameter(QgsProcessingParameterString(self.FIELD_NAME_MAX, + self.tr('Attribute name for maximum elevation of contour polygon'), + defaultValue='ELEV_MAX', + optional=True)) + + # Need to replace the output parameter, as we are producing a different type of output + self.removeParameter(contour.OUTPUT) + self.addParameter(QgsProcessingParameterVectorDestination( + contour.OUTPUT, self.tr('Contours'), QgsProcessing.TypeVectorPolygon)) + + def name(self): + return 'contour_polygon' + + def displayName(self): + return self.tr('Contour Polygons') + + def getConsoleCommands(self, parameters, context, feedback, executing=True): + arguments = self._buildArgsList(parameters, context, feedback, executing) + + fieldNameMin = self.parameterAsString(parameters, self.FIELD_NAME_MIN, context) + fieldNameMax = self.parameterAsString(parameters, self.FIELD_NAME_MAX, context) + + if fieldNameMin: + arguments.insert(0, fieldNameMin) + arguments.insert(0, '-amin') + + if fieldNameMax: + arguments.insert(0, fieldNameMax) + arguments.insert(0, '-amax') + + arguments.insert(0, "-p") return [self.commandName(), GdalUtils.escapeAndJoin(arguments)] diff --git a/python/plugins/processing/algs/gdal/ogr2ogrtabletopostgislist.py b/python/plugins/processing/algs/gdal/ogr2ogrtabletopostgislist.py deleted file mode 100644 index becffe5dba04..000000000000 --- a/python/plugins/processing/algs/gdal/ogr2ogrtabletopostgislist.py +++ /dev/null @@ -1,205 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - ogr2ogrtabletopostgislist.py - --------------------- - Date : November 2012 - Copyright : (C) 2012 by Victor Olaya - Email : volayaf 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__ = 'Victor Olaya' -__date__ = 'November 2012' -__copyright__ = '(C) 2012, Victor Olaya' - -from qgis.core import QgsSettings - -from processing.algs.gdal.GdalAlgorithm import GdalAlgorithm -from processing.algs.gdal.GdalUtils import GdalUtils - -from processing.tools.postgis import uri_from_name, GeoDB -from processing.tools.system import isWindows - - -class Ogr2OgrTableToPostGisList(GdalAlgorithm): - - DATABASE = 'DATABASE' - INPUT_LAYER = 'INPUT_LAYER' - SHAPE_ENCODING = 'SHAPE_ENCODING' - HOST = 'HOST' - PORT = 'PORT' - USER = 'USER' - DBNAME = 'DBNAME' - PASSWORD = 'PASSWORD' - SCHEMA = 'SCHEMA' - TABLE = 'TABLE' - PK = 'PK' - PRIMARY_KEY = 'PRIMARY_KEY' - WHERE = 'WHERE' - GT = 'GT' - OVERWRITE = 'OVERWRITE' - APPEND = 'APPEND' - ADDFIELDS = 'ADDFIELDS' - LAUNDER = 'LAUNDER' - SKIPFAILURES = 'SKIPFAILURES' - PRECISION = 'PRECISION' - OPTIONS = 'OPTIONS' - - def __init__(self): - GdalAlgorithm.__init__(self) - - def dbConnectionNames(self): - settings = QgsSettings() - settings.beginGroup('/PostgreSQL/connections/') - return settings.childGroups() - - def __init__(self): - super().__init__() - - def initAlgorithm(self, config=None): - self.DB_CONNECTIONS = self.dbConnectionNames() - self.addParameter(ParameterSelection(self.DATABASE, - self.tr('Database (connection name)'), self.DB_CONNECTIONS)) - self.addParameter(ParameterTable(self.INPUT_LAYER, - self.tr('Input layer'))) - self.addParameter(ParameterString(self.SHAPE_ENCODING, - self.tr('Shape encoding'), "", optional=True)) - self.addParameter(ParameterString(self.SCHEMA, - self.tr('Schema name'), 'public', optional=True)) - self.addParameter(ParameterString(self.TABLE, - self.tr('Table name, leave blank to use input name'), - '', optional=True)) - self.addParameter(ParameterString(self.PK, - self.tr('Primary key'), 'id', optional=True)) - self.addParameter(ParameterTableField(self.PRIMARY_KEY, - self.tr('Primary key (existing field, used if the above option is left empty)'), - self.INPUT_LAYER, optional=True)) - self.addParameter(ParameterString(self.WHERE, - self.tr('Select features using a SQL "WHERE" statement (Ex: column=\'value\')'), - '', optional=True)) - self.addParameter(ParameterString(self.GT, - self.tr('Group N features per transaction (Default: 20000)'), - '', optional=True)) - self.addParameter(ParameterBoolean(self.OVERWRITE, - self.tr('Overwrite existing table'), True)) - self.addParameter(ParameterBoolean(self.APPEND, - self.tr('Append to existing table'), False)) - self.addParameter(ParameterBoolean(self.ADDFIELDS, - self.tr('Append and add new fields to existing table'), False)) - self.addParameter(ParameterBoolean(self.LAUNDER, - self.tr('Do not launder columns/table names'), False)) - self.addParameter(ParameterBoolean(self.SKIPFAILURES, - self.tr('Continue after a failure, skipping the failed record'), - False)) - self.addParameter(ParameterBoolean(self.PRECISION, - self.tr('Keep width and precision of input attributes'), - True)) - self.addParameter(ParameterString(self.OPTIONS, - self.tr('Additional creation options'), '', optional=True)) - - def name(self): - return 'importlayertableasgeometrylesstableintopostgresqldatabase' - - def displayName(self): - return self.tr('Import layer/table as geometryless table into PostgreSQL database') - - def group(self): - return self.tr('Vector miscellaneous') - - def groupId(self): - return 'vectormiscellaneous' - - def getConsoleCommands(self, parameters, context, feedback, executing=True): - connection = self.DB_CONNECTIONS[self.getParameterValue(self.DATABASE)] - uri = uri_from_name(connection) - if executing: - # to get credentials input when needed - uri = GeoDB(uri=uri).uri - - inLayer = self.getParameterValue(self.INPUT_LAYER) - ogrLayer, layerName = self.getOgrCompatibleSource(self.INPUT, parameters, context, feedback, executing) - shapeEncoding = self.getParameterValue(self.SHAPE_ENCODING) - schema = str(self.getParameterValue(self.SCHEMA)) - table = str(self.getParameterValue(self.TABLE)) - pk = str(self.getParameterValue(self.PK)) - pkstring = "-lco FID=" + pk - primary_key = self.getParameterValue(self.PRIMARY_KEY) - where = str(self.getParameterValue(self.WHERE)) - wherestring = '-where "' + where + '"' - gt = str(self.getParameterValue(self.GT)) - overwrite = self.getParameterValue(self.OVERWRITE) - append = self.getParameterValue(self.APPEND) - addfields = self.getParameterValue(self.ADDFIELDS) - launder = self.getParameterValue(self.LAUNDER) - launderstring = "-lco LAUNDER=NO" - skipfailures = self.getParameterValue(self.SKIPFAILURES) - precision = self.getParameterValue(self.PRECISION) - options = str(self.getParameterValue(self.OPTIONS)) - - arguments = [] - arguments.append('-progress') - arguments.append('--config PG_USE_COPY YES') - if len(shapeEncoding) > 0: - arguments.append('--config') - arguments.append('SHAPE_ENCODING') - arguments.append('"' + shapeEncoding + '"') - arguments.append('-f') - arguments.append('PostgreSQL') - arguments.append('PG:"') - for token in uri.connectionInfo(executing).split(' '): - arguments.append(token) - arguments.append('active_schema={}'.format(schema or 'public')) - arguments.append('"') - arguments.append(ogrLayer) - arguments.append('-nlt NONE') - arguments.append(layerName) - if launder: - arguments.append(launderstring) - if append: - arguments.append('-append') - if addfields: - arguments.append('-addfields') - if overwrite: - arguments.append('-overwrite') - if len(pk) > 0: - arguments.append(pkstring) - elif primary_key is not None: - arguments.append("-lco FID=" + primary_key) - if len(table) == 0: - table = layerName.lower() - if schema: - table = '{}.{}'.format(schema, table) - arguments.append('-nln') - arguments.append(table) - if skipfailures: - arguments.append('-skipfailures') - if where: - arguments.append(wherestring) - if len(gt) > 0: - arguments.append('-gt') - arguments.append(gt) - if not precision: - arguments.append('-lco PRECISION=NO') - if len(options) > 0: - arguments.append(options) - - commands = [] - if isWindows(): - commands = ['cmd.exe', '/C ', 'ogr2ogr.exe', - GdalUtils.escapeAndJoin(arguments)] - else: - commands = ['ogr2ogr', GdalUtils.escapeAndJoin(arguments)] - - return commands - - def commandName(self): - return "ogr2ogr" diff --git a/python/plugins/processing/algs/gdal/ogr2ogrtopostgislist.py b/python/plugins/processing/algs/gdal/ogr2ogrtopostgislist.py index 10a2def429af..2518d7082409 100644 --- a/python/plugins/processing/algs/gdal/ogr2ogrtopostgislist.py +++ b/python/plugins/processing/algs/gdal/ogr2ogrtopostgislist.py @@ -29,12 +29,17 @@ QgsProcessingParameterField, QgsProcessingParameterExtent, QgsProcessingParameterBoolean, - QgsProcessingParameterProviderConnection) + QgsProcessingParameterProviderConnection, + QgsProcessingParameterDatabaseSchema, + QgsProcessingParameterDatabaseTable, + QgsProviderRegistry, + QgsProcessingException, + QgsProviderConnectionException, + QgsDataSourceUri) from processing.algs.gdal.GdalAlgorithm import GdalAlgorithm from processing.algs.gdal.GdalUtils import GdalUtils -from processing.tools.postgis import uri_from_name, GeoDB from processing.tools.system import isWindows @@ -101,22 +106,15 @@ def initAlgorithm(self, config=None): self.addParameter(QgsProcessingParameterCrs(self.S_SRS, self.tr('Override source CRS'), defaultValue='', optional=True)) - schema_param = QgsProcessingParameterString( + schema_param = QgsProcessingParameterDatabaseSchema( self.SCHEMA, - self.tr('Schema (schema name)'), 'public', False, True) - schema_param.setMetadata({ - 'widget_wrapper': { - 'class': 'processing.gui.wrappers_postgis.SchemaWidgetWrapper', - 'connection_param': self.DATABASE}}) + self.tr('Schema (schema name)'), defaultValue='public', connectionParameterName=self.DATABASE, optional=True) self.addParameter(schema_param) - table_param = QgsProcessingParameterString( + table_param = QgsProcessingParameterDatabaseTable( self.TABLE, - self.tr('Table to import to (leave blank to use layer name)'), '', False, True) - table_param.setMetadata({ - 'widget_wrapper': { - 'class': 'processing.gui.wrappers_postgis.TableWidgetWrapper', - 'schema_param': self.SCHEMA}}) + self.tr('Table to import to (leave blank to use layer name)'), defaultValue=None, connectionParameterName=self.DATABASE, + schemaParameterName=self.SCHEMA, optional=True, allowNewTableNames=True) self.addParameter(table_param) self.addParameter(QgsProcessingParameterString(self.PK, @@ -200,19 +198,27 @@ def groupId(self): return 'vectormiscellaneous' def getConsoleCommands(self, parameters, context, feedback, executing=True): - connection = self.parameterAsConnectionName(parameters, self.DATABASE, context) - uri = uri_from_name(connection) - if executing: - # to get credentials input when needed - uri = GeoDB(uri=uri).uri + connection_name = self.parameterAsConnectionName(parameters, self.DATABASE, context) + if not connection_name: + raise QgsProcessingException( + self.tr('No connection specified')) + + # resolve connection details to uri + try: + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(connection_name) + except QgsProviderConnectionException: + raise QgsProcessingException(self.tr('Could not retrieve connection details for {}').format(connection_name)) + + uri = conn.uri() ogrLayer, layername = self.getOgrCompatibleSource(self.INPUT, parameters, context, feedback, executing) shapeEncoding = self.parameterAsString(parameters, self.SHAPE_ENCODING, context) ssrs = self.parameterAsCrs(parameters, self.S_SRS, context) tsrs = self.parameterAsCrs(parameters, self.T_SRS, context) asrs = self.parameterAsCrs(parameters, self.A_SRS, context) - table = self.parameterAsString(parameters, self.TABLE, context) - schema = self.parameterAsString(parameters, self.SCHEMA, context) + table = self.parameterAsDatabaseTableName(parameters, self.TABLE, context) + schema = self.parameterAsSchema(parameters, self.SCHEMA, context) pk = self.parameterAsString(parameters, self.PK, context) pkstring = "-lco FID=" + pk primary_key = self.parameterAsString(parameters, self.PRIMARY_KEY, context) @@ -249,7 +255,7 @@ def getConsoleCommands(self, parameters, context, feedback, executing=True): arguments.append('-f') arguments.append('PostgreSQL') arguments.append('PG:"') - for token in uri.connectionInfo(executing).split(' '): + for token in QgsDataSourceUri(uri).connectionInfo(executing).split(' '): arguments.append(token) arguments.append('active_schema={}'.format(schema or 'public')) arguments.append('"') diff --git a/python/plugins/processing/algs/grass7/description/v.in.lidar.txt b/python/plugins/processing/algs/grass7/description/v.in.lidar.txt index e0989fd7c64a..d0feb7438cf1 100644 --- a/python/plugins/processing/algs/grass7/description/v.in.lidar.txt +++ b/python/plugins/processing/algs/grass7/description/v.in.lidar.txt @@ -1,7 +1,7 @@ v.in.lidar Converts LAS LiDAR point clouds to a GRASS vector map with libLAS. Vector (v.*) -QgsProcessingParameterFile|input|LiDAR input files in LAS format (*.las or *.laz)|QgsProcessingParameterFile.File|las|None|False +QgsProcessingParameterFile|input|LiDAR input files in LAS format (*.las or *.laz)|QgsProcessingParameterFile.File||None|False|Lidar files (*.las *.LAS *.laz *.LAZ) QgsProcessingParameterExtent|spatial|Import subregion only|None|True QgsProcessingParameterRange|zrange|Filter range for z data|QgsProcessingParameterNumber.Double|None|True QgsProcessingParameterEnum|return_filter|Only import points of selected return type|first;last;mid|True|None|True diff --git a/python/plugins/processing/algs/help/qgis.yaml b/python/plugins/processing/algs/help/qgis.yaml index 0011b421c53a..a77487116aa2 100644 --- a/python/plugins/processing/algs/help/qgis.yaml +++ b/python/plugins/processing/algs/help/qgis.yaml @@ -330,15 +330,14 @@ qgis:refactorfields: > The original layer is not modified. A new layer is generated, which contains a modified attribute table, according to the provided fields mapping. + Rows in orange have constraints in the template layer from which these fields were loaded. Treat this information as a hint during configuration. No constraints will be added on an output layer nor will they be checked or enforced by the algorithm. + qgis:regularpoints: > This algorithm creates a point layer with a given number of regular points, all of them within a given extent. qgis:relief: > This algorithm creates a shaded relief layer from digital elevation data. -qgis:removenullgeometries: > - This algorithm removes any features which do not have a geometry from a vector layer. All other features will be copied unchanged. - qgis:reprojectlayer: > This algorithm reprojects a vector layer. It creates a new layer with the same features as the input one, but with geometries reprojected to a new CRS. diff --git a/python/plugins/processing/algs/otb/OtbChoiceWidget.py b/python/plugins/processing/algs/otb/OtbChoiceWidget.py index 7ea0ed945109..f86eb3a3f7bd 100644 --- a/python/plugins/processing/algs/otb/OtbChoiceWidget.py +++ b/python/plugins/processing/algs/otb/OtbChoiceWidget.py @@ -70,10 +70,13 @@ def __updateWrapper(self, name, visible): #Fur Qgis modeler else: - if name in self.dialog.wrappers: - self.__setWrapperVisibility(self.dialog.wrappers[name], visible) - if name in self.dialog.widget_labels: - self.dialog.widget_labels[name].setVisible(visible) + try: + if name in self.dialog.widget.widget.wrappers: + self.__setWrapperVisibility(self.dialog.widget.widget.wrappers[name], visible) + if name in self.dialog.widget.widget.widget_labels: + self.dialog.widget.widget.widget_labels[name].setVisible(visible) + except AttributeError: + pass def __setWrapperVisibility(self, wrapper, v): # For compatibility with 3.x API, we need to check whether the wrapper is diff --git a/python/plugins/processing/algs/otb/OtbUtils.py b/python/plugins/processing/algs/otb/OtbUtils.py index e7e392bf40db..9b1037cf91cd 100644 --- a/python/plugins/processing/algs/otb/OtbUtils.py +++ b/python/plugins/processing/algs/otb/OtbUtils.py @@ -119,10 +119,12 @@ def getExecutableInPath(path, exe): def getAuxiliaryDataDirectories(): gdal_data_dir = None gtiff_csv_dir = None + proj_dir = None otb_folder = OtbUtils.otbFolder() if os.name == 'nt': gdal_data_dir = os.path.join(otb_folder, 'share', 'data') gtiff_csv_dir = os.path.join(otb_folder, 'share', 'epsg_csv') + proj_dir = os.path.join(otb_folder, 'share', 'proj') else: env_profile = os.path.join(otb_folder, 'otbenv.profile') try: @@ -137,12 +139,14 @@ def getAuxiliaryDataDirectories(): gdal_data_dir = line.split("GDAL_DATA=")[1] if 'GEOTIFF_CSV='in line: gtiff_csv_dir = line.split("GEOTIFF_CSV=")[1] + if 'PROJ_LIB='in line: + proj_dir = line.split("PROJ_LIB=")[1] except BaseException as exc: errmsg = "Cannot find gdal and geotiff data directory." + str(exc) QgsMessageLog.logMessage(errmsg, OtbUtils.tr('Processing'), Qgis.Info) pass - return gdal_data_dir, gtiff_csv_dir + return gdal_data_dir, gtiff_csv_dir, proj_dir @staticmethod def executeOtb(commands, feedback, addToLog=True): @@ -150,11 +154,13 @@ def executeOtb(commands, feedback, addToLog=True): 'LC_NUMERIC': 'C', 'GDAL_DRIVER_PATH': 'disable' } - gdal_data_dir, gtiff_csv_dir = OtbUtils.getAuxiliaryDataDirectories() + gdal_data_dir, gtiff_csv_dir, proj_dir = OtbUtils.getAuxiliaryDataDirectories() if gdal_data_dir and os.path.exists(gdal_data_dir): otb_env['GDAL_DATA'] = gdal_data_dir if gtiff_csv_dir and os.path.exists(gtiff_csv_dir): otb_env['GEOTIFF_CSV'] = gtiff_csv_dir + if proj_dir and os.path.exists(proj_dir): + otb_env['PROJ_LIB'] = proj_dir otb_env['OTB_LOGGER_LEVEL'] = OtbUtils.loggerLevel() max_ram_hint = OtbUtils.maxRAMHint() diff --git a/python/plugins/processing/algs/qgis/CheckValidity.py b/python/plugins/processing/algs/qgis/CheckValidity.py index 5b9a69dc4f62..0790e2873987 100644 --- a/python/plugins/processing/algs/qgis/CheckValidity.py +++ b/python/plugins/processing/algs/qgis/CheckValidity.py @@ -96,13 +96,13 @@ def initAlgorithm(self, config=None): self.addParameter(QgsProcessingParameterBoolean(self.IGNORE_RING_SELF_INTERSECTION, self.tr('Ignore ring self intersections'), defaultValue=False)) - self.addParameter(QgsProcessingParameterFeatureSink(self.VALID_OUTPUT, self.tr('Valid output'), QgsProcessing.TypeVectorAnyGeometry, '', True)) + self.addParameter(QgsProcessingParameterFeatureSink(self.VALID_OUTPUT, self.tr('Valid output'), QgsProcessing.TypeVectorAnyGeometry, None, True)) self.addOutput(QgsProcessingOutputNumber(self.VALID_COUNT, self.tr('Count of valid features'))) - self.addParameter(QgsProcessingParameterFeatureSink(self.INVALID_OUTPUT, self.tr('Invalid output'), QgsProcessing.TypeVectorAnyGeometry, '', True)) + self.addParameter(QgsProcessingParameterFeatureSink(self.INVALID_OUTPUT, self.tr('Invalid output'), QgsProcessing.TypeVectorAnyGeometry, None, True)) self.addOutput(QgsProcessingOutputNumber(self.INVALID_COUNT, self.tr('Count of invalid features'))) - self.addParameter(QgsProcessingParameterFeatureSink(self.ERROR_OUTPUT, self.tr('Error output'), QgsProcessing.TypeVectorAnyGeometry, '', True)) + self.addParameter(QgsProcessingParameterFeatureSink(self.ERROR_OUTPUT, self.tr('Error output'), QgsProcessing.TypeVectorAnyGeometry, None, True)) self.addOutput(QgsProcessingOutputNumber(self.ERROR_COUNT, self.tr('Count of errors'))) def name(self): diff --git a/python/plugins/processing/algs/qgis/ImportIntoPostGIS.py b/python/plugins/processing/algs/qgis/ImportIntoPostGIS.py index 6e93c65fa68c..bc87919fb5ac 100644 --- a/python/plugins/processing/algs/qgis/ImportIntoPostGIS.py +++ b/python/plugins/processing/algs/qgis/ImportIntoPostGIS.py @@ -22,7 +22,6 @@ __copyright__ = '(C) 2012, Victor Olaya' from qgis.core import (QgsVectorLayerExporter, - QgsSettings, QgsFeatureSink, QgsProcessing, QgsProcessingException, @@ -31,10 +30,15 @@ QgsProcessingParameterField, QgsProcessingParameterBoolean, QgsProcessingParameterProviderConnection, - QgsWkbTypes) + QgsProcessingParameterDatabaseSchema, + QgsProcessingParameterDatabaseTable, + QgsWkbTypes, + QgsProviderRegistry, + QgsProviderConnectionException, + QgsDataSourceUri, + QgsAbstractDatabaseProviderConnection) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.tools import postgis class ImportIntoPostGIS(QgisAlgorithm): @@ -71,22 +75,15 @@ def initAlgorithm(self, config=None): ) self.addParameter(db_param) - schema_param = QgsProcessingParameterString( + schema_param = QgsProcessingParameterDatabaseSchema( self.SCHEMA, - self.tr('Schema (schema name)'), 'public', False, True) - schema_param.setMetadata({ - 'widget_wrapper': { - 'class': 'processing.gui.wrappers_postgis.SchemaWidgetWrapper', - 'connection_param': self.DATABASE}}) + self.tr('Schema (schema name)'), connectionParameterName=self.DATABASE, defaultValue='public', optional=True) self.addParameter(schema_param) - table_param = QgsProcessingParameterString( + table_param = QgsProcessingParameterDatabaseTable( self.TABLENAME, - self.tr('Table to import to (leave blank to use layer name)'), '', False, True) - table_param.setMetadata({ - 'widget_wrapper': { - 'class': 'processing.gui.wrappers_postgis.TableWidgetWrapper', - 'schema_param': self.SCHEMA}}) + self.tr('Table to import to (leave blank to use layer name)'), defaultValue=None, connectionParameterName=self.DATABASE, + schemaParameterName=self.SCHEMA, optional=True, allowNewTableNames=True) self.addParameter(table_param) self.addParameter(QgsProcessingParameterField(self.PRIMARY_KEY, @@ -122,10 +119,16 @@ def tags(self): return self.tr('import,postgis,table,layer,into,copy').split(',') def processAlgorithm(self, parameters, context, feedback): - connection = self.parameterAsConnectionName(parameters, self.DATABASE, context) - db = postgis.GeoDB.from_name(connection) + connection_name = self.parameterAsConnectionName(parameters, self.DATABASE, context) - schema = self.parameterAsString(parameters, self.SCHEMA, context) + # resolve connection details to uri + try: + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(connection_name) + except QgsProviderConnectionException: + raise QgsProcessingException(self.tr('Could not retrieve connection details for {}').format(connection_name)) + + schema = self.parameterAsSchema(parameters, self.SCHEMA, context) overwrite = self.parameterAsBoolean(parameters, self.OVERWRITE, context) createIndex = self.parameterAsBoolean(parameters, self.CREATEINDEX, context) convertLowerCase = self.parameterAsBoolean(parameters, self.LOWERCASE_NAMES, context) @@ -138,7 +141,7 @@ def processAlgorithm(self, parameters, context, feedback): if source is None: raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) - table = self.parameterAsString(parameters, self.TABLENAME, context) + table = self.parameterAsDatabaseTableName(parameters, self.TABLENAME, context) if table: table.strip() if not table or table == '': @@ -166,8 +169,11 @@ def processAlgorithm(self, parameters, context, feedback): if source.wkbType() == QgsWkbTypes.NoGeometry: geomColumn = None - uri = db.uri - uri.setDataSource(schema, table, geomColumn, '', primaryKeyField) + uri = QgsDataSourceUri(conn.uri()) + uri.setSchema(schema) + uri.setTable(table) + uri.setKeyColumn(primaryKeyField) + uri.setGeometryColumn(geomColumn) if encoding: options['fileEncoding'] = encoding @@ -196,13 +202,16 @@ def processAlgorithm(self, parameters, context, feedback): self.tr('Error importing to PostGIS\n{0}').format(exporter.errorMessage())) if geomColumn and createIndex: - db.create_spatial_index(table, schema, geomColumn) - - db.vacuum_analyze(table, schema) + try: + options = QgsAbstractDatabaseProviderConnection.SpatialIndexOptions() + options.geometryColumnName = geomColumn + conn.createSpatialIndex(schema, table, options) + except QgsProviderConnectionException as e: + raise QgsProcessingException(self.tr('Error creating spatial index:\n{0}').format(e)) + + try: + conn.vacuum(schema, table) + except QgsProviderConnectionException as e: + feedback.reportError(self.tr('Error vacuuming table:\n{0}').format(e)) return {} - - def dbConnectionNames(self): - settings = QgsSettings() - settings.beginGroup('/PostgreSQL/connections/') - return settings.childGroups() diff --git a/python/plugins/processing/algs/qgis/ImportIntoSpatialite.py b/python/plugins/processing/algs/qgis/ImportIntoSpatialite.py index deb937a8a401..b2ec059c30a2 100644 --- a/python/plugins/processing/algs/qgis/ImportIntoSpatialite.py +++ b/python/plugins/processing/algs/qgis/ImportIntoSpatialite.py @@ -32,10 +32,12 @@ QgsProcessingParameterField, QgsProcessingParameterString, QgsProcessingParameterBoolean, - QgsWkbTypes) + QgsWkbTypes, + QgsProviderRegistry, + QgsProviderConnectionException, + QgsAbstractDatabaseProviderConnection) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.tools import spatialite class ImportIntoSpatialite(QgisAlgorithm): @@ -98,6 +100,7 @@ def tags(self): def processAlgorithm(self, parameters, context, feedback): database = self.parameterAsVectorLayer(parameters, self.DATABASE, context) + databaseuri = database.dataProvider().dataSourceUri() uri = QgsDataSourceUri(databaseuri) if uri.database() is '': @@ -106,7 +109,12 @@ def processAlgorithm(self, parameters, context, feedback): elif '|layerid' in databaseuri: databaseuri = databaseuri[:databaseuri.find('|layerid')] uri = QgsDataSourceUri('dbname=\'%s\'' % (databaseuri)) - db = spatialite.GeoDB(uri) + + try: + md = QgsProviderRegistry.instance().providerMetadata('spatialite') + conn = md.createConnection(uri.uri(), {}) + except QgsProviderConnectionException: + raise QgsProcessingException(self.tr('Could not connect to {}').format(uri.uri())) overwrite = self.parameterAsBoolean(parameters, self.OVERWRITE, context) createIndex = self.parameterAsBoolean(parameters, self.CREATEINDEX, context) @@ -148,7 +156,6 @@ def processAlgorithm(self, parameters, context, feedback): if source.wkbType() == QgsWkbTypes.NoGeometry: geomColumn = None - uri = db.uri uri.setDataSource('', table, geomColumn, '', primaryKeyField) if encoding: @@ -178,6 +185,11 @@ def processAlgorithm(self, parameters, context, feedback): self.tr('Error importing to Spatialite\n{0}').format(exporter.errorMessage())) if geomColumn and createIndex: - db.create_spatial_index(table, geomColumn) + try: + options = QgsAbstractDatabaseProviderConnection.SpatialIndexOptions() + options.geometryColumnName = geomColumn + conn.createSpatialIndex('', table, options) + except QgsProviderConnectionException as e: + raise QgsProcessingException(self.tr('Error creating spatial index:\n{0}').format(e)) return {} diff --git a/python/plugins/processing/algs/qgis/PostGISExecuteAndLoadSQL.py b/python/plugins/processing/algs/qgis/PostGISExecuteAndLoadSQL.py index d2ed1f94a6af..5eb5ae66e1b1 100644 --- a/python/plugins/processing/algs/qgis/PostGISExecuteAndLoadSQL.py +++ b/python/plugins/processing/algs/qgis/PostGISExecuteAndLoadSQL.py @@ -23,20 +23,18 @@ __date__ = 'May 2018' __copyright__ = '(C) 2018, Anita Graser' -from qgis.core import (Qgis, - QgsProcessingException, +from qgis.core import (QgsProcessingException, QgsProcessingParameterString, - QgsApplication, QgsVectorLayer, - QgsProject, + QgsDataSourceUri, QgsProcessing, - QgsProcessingException, QgsProcessingOutputVectorLayer, QgsProcessingContext, QgsProcessingParameterProviderConnection, - QgsProcessingFeedback) + QgsProviderRegistry, + QgsProviderConnectionException + ) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.tools import postgis class PostGISExecuteAndLoadSQL(QgisAlgorithm): @@ -92,11 +90,20 @@ def tags(self): return self.tr('postgis,table,database').split(',') def processAlgorithm(self, parameters, context, feedback): - connection = self.parameterAsConnectionName(parameters, self.DATABASE, context) + connection_name = self.parameterAsConnectionName(parameters, self.DATABASE, context) id_field = self.parameterAsString(parameters, self.ID_FIELD, context) geom_field = self.parameterAsString( parameters, self.GEOMETRY_FIELD, context) - uri = postgis.uri_from_name(connection) + + # resolve connection details to uri + try: + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(connection_name) + except QgsProviderConnectionException: + raise QgsProcessingException(self.tr('Could not retrieve connection details for {}').format(connection_name)) + + uri = QgsDataSourceUri(conn.uri()) + sql = self.parameterAsString(parameters, self.SQL, context) sql = sql.replace('\n', ' ') uri.setDataSource("", "(" + sql.rstrip(';') + ")", geom_field, "", id_field) diff --git a/python/plugins/processing/algs/qgis/PostGISExecuteSQL.py b/python/plugins/processing/algs/qgis/PostGISExecuteSQL.py index ec60f05fe4b3..ef767841bbcd 100644 --- a/python/plugins/processing/algs/qgis/PostGISExecuteSQL.py +++ b/python/plugins/processing/algs/qgis/PostGISExecuteSQL.py @@ -24,10 +24,11 @@ from qgis.core import ( QgsProcessingException, QgsProcessingParameterString, - QgsProcessingParameterProviderConnection + QgsProcessingParameterProviderConnection, + QgsProviderRegistry, + QgsProviderConnectionException ) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.tools import postgis class PostGISExecuteSQL(QgisAlgorithm): @@ -64,13 +65,19 @@ def tags(self): return self.tr('postgis,database').split(',') def processAlgorithm(self, parameters, context, feedback): - connection = self.parameterAsConnectionName(parameters, self.DATABASE, context) - db = postgis.GeoDB.from_name(connection) + connection_name = self.parameterAsConnectionName(parameters, self.DATABASE, context) + + # resolve connection details to uri + try: + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(connection_name) + except QgsProviderConnectionException: + raise QgsProcessingException(self.tr('Could not retrieve connection details for {}').format(connection_name)) sql = self.parameterAsString(parameters, self.SQL, context).replace('\n', ' ') try: - db._exec_sql_and_commit(str(sql)) - except postgis.DbError as e: - raise QgsProcessingException( - self.tr('Error executing SQL:\n{0}').format(str(e))) + conn.executeSql(sql) + except QgsProviderConnectionException as e: + raise QgsProcessingException(self.tr('Error executing SQL:\n{0}').format(e)) + return {} diff --git a/python/plugins/processing/algs/qgis/SpatialiteExecuteSQL.py b/python/plugins/processing/algs/qgis/SpatialiteExecuteSQL.py index e775b46cc5fe..9a8fd30766f7 100644 --- a/python/plugins/processing/algs/qgis/SpatialiteExecuteSQL.py +++ b/python/plugins/processing/algs/qgis/SpatialiteExecuteSQL.py @@ -26,10 +26,11 @@ QgsProcessingAlgorithm, QgsProcessingException, QgsProcessingParameterVectorLayer, - QgsProcessingParameterString) + QgsProcessingParameterString, + QgsProviderRegistry, + QgsProviderConnectionException) from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.tools import spatialite class SpatialiteExecuteSQL(QgisAlgorithm): @@ -72,12 +73,17 @@ def processAlgorithm(self, parameters, context, feedback): elif '|layerid' in databaseuri: databaseuri = databaseuri[:databaseuri.find('|layerid')] uri = QgsDataSourceUri('dbname=\'%s\'' % (databaseuri)) - db = spatialite.GeoDB(uri) + + try: + md = QgsProviderRegistry.instance().providerMetadata('spatialite') + conn = md.createConnection(uri.uri(), {}) + except QgsProviderConnectionException: + raise QgsProcessingException(self.tr('Could not connect to {}').format(uri.uri())) + sql = self.parameterAsString(parameters, self.SQL, context).replace('\n', ' ') try: - db._exec_sql_and_commit(str(sql)) - except spatialite.DbError as e: - raise QgsProcessingException( - self.tr('Error executing SQL:\n{0}').format(str(e))) + conn.executeSql(sql) + except QgsProviderConnectionException as e: + raise QgsProcessingException(self.tr('Error executing SQL:\n{0}').format(e)) return {} diff --git a/python/plugins/processing/algs/qgis/UniqueValues.py b/python/plugins/processing/algs/qgis/UniqueValues.py index 27670a5c7933..ccf4c65e7c17 100644 --- a/python/plugins/processing/algs/qgis/UniqueValues.py +++ b/python/plugins/processing/algs/qgis/UniqueValues.py @@ -79,7 +79,7 @@ def initAlgorithm(self, config=None): self.tr('Target field(s)'), parentLayerParameterName=self.INPUT, type=QgsProcessingParameterField.Any, allowMultiple=True)) - self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Unique values'), optional=True, defaultValue='')) + self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, self.tr('Unique values'), optional=True, defaultValue=None)) self.addParameter(QgsProcessingParameterFileDestination(self.OUTPUT_HTML_FILE, self.tr('HTML report'), self.tr('HTML files (*.html)'), None, True)) self.addOutput(QgsProcessingOutputNumber(self.TOTAL_VALUES, self.tr('Total unique values'))) diff --git a/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py b/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py index cee1ff993a6f..b53b9b66bca6 100644 --- a/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py +++ b/python/plugins/processing/algs/qgis/ui/FieldsCalculatorDialog.py @@ -47,8 +47,6 @@ from processing.gui.AlgorithmExecutor import execute from processing.tools import dataobjects from processing.gui.Postprocessing import handleAlgorithmResults -from processing.gui.PostgisTableSelector import PostgisTableSelector -from processing.gui.ParameterGuiUtils import getFileFilter pluginPath = os.path.dirname(__file__) with warnings.catch_warnings(): @@ -153,7 +151,7 @@ def setupSpinboxes(self, index): def selectFile(self): output = self.alg.parameterDefinition('OUTPUT') - fileFilter = getFileFilter(output) + fileFilter = output.createFileFilter() settings = QgsSettings() if settings.contains('/Processing/LastOutputPath'): diff --git a/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py b/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py index 92f91e4ae69b..7587c926d673 100644 --- a/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py +++ b/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py @@ -22,42 +22,35 @@ __copyright__ = '(C) 2014, Arnaud Morvan' import os -from collections import OrderedDict from qgis.PyQt import uic from qgis.PyQt.QtCore import ( QItemSelectionModel, - QAbstractTableModel, QModelIndex, - QVariant, - Qt, pyqtSlot, - QCoreApplication + QCoreApplication, + QVariant, ) + from qgis.PyQt.QtWidgets import ( QComboBox, - QHeaderView, - QLineEdit, QSpacerItem, QMessageBox, - QSpinBox, - QStyledItemDelegate, QWidget, QVBoxLayout ) from qgis.core import ( QgsApplication, - QgsExpression, QgsMapLayerProxyModel, QgsProcessingFeatureSourceDefinition, QgsProcessingUtils, - QgsProject, QgsVectorLayer, + QgsField, + QgsFields, ) -from qgis.gui import QgsFieldExpressionWidget -from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER, DIALOG_BATCH +from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER from processing.tools import dataobjects from processing.algs.qgis.FieldsMapper import FieldsMapper @@ -67,233 +60,6 @@ os.path.join(pluginPath, 'fieldsmappingpanelbase.ui')) -class FieldsMappingModel(QAbstractTableModel): - - fieldTypes = OrderedDict([ - (QVariant.Date, "Date"), - (QVariant.DateTime, "DateTime"), - (QVariant.Double, "Double"), - (QVariant.Int, "Integer"), - (QVariant.LongLong, "Integer64"), - (QVariant.String, "String"), - (QVariant.List, "List"), - (QVariant.Bool, "Boolean")]) - - def __init__(self, parent=None): - super(FieldsMappingModel, self).__init__(parent) - self._mapping = [] - self._layer = None - self.configure() - self._generator = None - - def configure(self): - self.columns = [{ - 'name': 'expression', - 'type': QgsExpression, - 'header': self.tr("Source expression"), - 'persistentEditor': True - }, { - 'name': 'name', - 'type': QVariant.String, - 'header': self.tr("Field name") - }, { - 'name': 'type', - 'type': QVariant.Type, - 'header': self.tr("Type"), - 'persistentEditor': True - }, { - 'name': 'length', - 'type': QVariant.Int, - 'header': self.tr("Length") - }, { - 'name': 'precision', - 'type': QVariant.Int, - 'header': self.tr("Precision") - }] - - def columnIndex(self, column_name): - for index, column in enumerate(self.columns): - if column['name'] == column_name: - return index - - def mapping(self): - return self._mapping - - def setMapping(self, value): - self.beginResetModel() - self._mapping = value - self.endResetModel() - - def setContextGenerator(self, generator): - self._generator = generator - - def contextGenerator(self): - if self._generator: - return self._generator - if self._layer: - return self._layer - return QgsProject.instance() - - def layer(self): - return self._layer - - def setLayer(self, layer): - self._layer = layer - - def columnCount(self, parent=QModelIndex()): - if parent.isValid(): - return 0 - return len(self.columns) - - def rowCount(self, parent=QModelIndex()): - if parent.isValid(): - return 0 - try: - return len(self._mapping) - except TypeError: - return 0 - - def headerData(self, section, orientation, role=Qt.DisplayRole): - if role == Qt.DisplayRole: - if orientation == Qt.Horizontal: - return self.columns[section]['header'] - if orientation == Qt.Vertical: - return section - - def flags(self, index): - return Qt.ItemFlags(Qt.ItemIsSelectable | - Qt.ItemIsEditable | - Qt.ItemIsEnabled) - - def data(self, index, role=Qt.DisplayRole): - field = self._mapping[index.row()] - column_def = self.columns[index.column()] - - if role == Qt.DisplayRole: - value = field[column_def['name']] - if column_def['type'] == QVariant.Type: - if value == QVariant.Invalid: - return '' - return self.fieldTypes[value] - return value - - if role == Qt.EditRole: - return field[column_def['name']] - - if role == Qt.TextAlignmentRole: - if column_def['type'] in [QVariant.Int]: - hAlign = Qt.AlignRight - else: - hAlign = Qt.AlignLeft - return hAlign + Qt.AlignVCenter - - def setData(self, index, value, role=Qt.EditRole): - field = self._mapping[index.row()] - column_def = self.columns[index.column()] - - if role == Qt.EditRole: - field[column_def['name']] = value - self.dataChanged.emit(index, index) - - return True - - def insertRows(self, row, count, index=QModelIndex()): - self.beginInsertRows(index, row, row + count - 1) - - for i in range(count): - field = self.newField() - self._mapping.insert(row + i, field) - - self.endInsertRows() - return True - - def removeRows(self, row, count, index=QModelIndex()): - self.beginRemoveRows(index, row, row + count - 1) - - for i in range(row + count - 1, row + 1): - self._mapping.pop(i) - - self.endRemoveRows() - return True - - def newField(self, field=None): - if field is None: - return {'name': '', - 'type': QVariant.Invalid, - 'length': 0, - 'precision': 0, - 'expression': ''} - - return {'name': field.name(), - 'type': field.type(), - 'length': field.length(), - 'precision': field.precision(), - 'expression': QgsExpression.quotedColumnRef(field.name())} - - def loadLayerFields(self, layer): - self.beginResetModel() - - self._mapping = [] - if layer is not None: - for field in layer.fields(): - self._mapping.append(self.newField(field)) - - self.endResetModel() - - -class FieldTypeDelegate(QStyledItemDelegate): - - def createEditor(self, parent, option, index): - editor = QComboBox(parent) - for key, text in FieldsMappingModel.fieldTypes.items(): - editor.addItem(text, key) - return editor - - def setEditorData(self, editor, index): - if not editor: - return - value = index.model().data(index, Qt.EditRole) - editor.setCurrentIndex(editor.findData(value)) - - def setModelData(self, editor, model, index): - if not editor: - return - value = editor.currentData() - if value is None: - value = QVariant.Invalid - model.setData(index, value) - - -class ExpressionDelegate(QStyledItemDelegate): - - def createEditor(self, parent, option, index): - editor = QgsFieldExpressionWidget(parent) - editor.setLayer(index.model().layer()) - editor.registerExpressionContextGenerator(index.model().contextGenerator()) - editor.fieldChanged.connect(self.on_expression_fieldChange) - editor.setAutoFillBackground(True) - editor.setAllowEvalErrors(self.parent().dialogType == DIALOG_MODELER) - return editor - - def setEditorData(self, editor, index): - if not editor: - return - value = index.model().data(index, Qt.EditRole) - editor.setField(value) - - def setModelData(self, editor, model, index): - if not editor: - return - (value, isExpression, isValid) = editor.currentField() - if isExpression is True: - model.setData(index, value) - else: - model.setData(index, QgsExpression.quotedColumnRef(value)) - - def on_expression_fieldChange(self, fieldName): - self.commitData.emit(self.sender()) - - class FieldsMappingPanel(BASE, WIDGET): def __init__(self, parent=None): @@ -308,35 +74,20 @@ def __init__(self, parent=None): self.configure() - self.model.modelReset.connect(self.on_model_modelReset) - self.model.rowsInserted.connect(self.on_model_rowsInserted) - self.layerCombo.setAllowEmptyLayer(True) self.layerCombo.setFilters(QgsMapLayerProxyModel.VectorLayer) self.dialogType = None + self.layer = None def configure(self): - self.model = FieldsMappingModel() - self.fieldsView.setModel(self.model) - - self.setDelegate('expression', ExpressionDelegate(self)) - self.setDelegate('type', FieldTypeDelegate(self)) - - def setContextGenerator(self, generator): - self.model.setContextGenerator(generator) - - def setDelegate(self, column_name, delegate): - self.fieldsView.setItemDelegateForColumn( - self.model.columnIndex(column_name), - delegate) + self.model = self.fieldsView.model() + self.fieldsView.setDestinationEditable(True) def setLayer(self, layer): - if self.model.layer() == layer: + if layer is None or self.layer == layer: return - self.model.setLayer(layer) - if layer is None: - return - if self.model.rowCount() == 0: + self.layer = layer + if self.model.rowCount(QModelIndex()) == 0: self.on_resetButton_clicked() return dlg = QMessageBox(self) @@ -349,15 +100,43 @@ def setLayer(self, layer): self.on_resetButton_clicked() def value(self): - return self.model.mapping() + # Value is a dict with name, type, length, precision and expression + mapping = self.fieldsView.mapping() + results = [] + for f in mapping: + results.append({ + 'name': f.field.name(), + 'type': f.field.type(), + 'length': f.field.length(), + 'precision': f.field.precision(), + 'expression': f.expression, + }) + return results def setValue(self, value): - self.model.setMapping(value) + if type(value) != dict: + return + destinationFields = QgsFields() + expressions = {} + for field_def in value: + f = QgsField(field_def.get('name'), + field_def.get('type', QVariant.Invalid), + field_def.get(QVariant.typeToName(field_def.get('type', QVariant.Invalid))), + field_def.get('length', 0), + field_def.get('precision', 0)) + try: + expressions[f.name()] = field_def['expressions'] + except AttributeError: + pass + destinationFields.append(f) + + if len(destinationFields): + self.fieldsView.setDestinationFields(destinationFields, expressions) @pyqtSlot(bool, name='on_addButton_clicked') def on_addButton_clicked(self, checked=False): - rowCount = self.model.rowCount() - self.model.insertRows(rowCount, 1) + rowCount = self.model.rowCount(QModelIndex()) + self.model.appendField(QgsField('new_field')) index = self.model.index(rowCount, 0) self.fieldsView.selectionModel().select( index, @@ -367,118 +146,32 @@ def on_addButton_clicked(self, checked=False): QItemSelectionModel.Current | QItemSelectionModel.Rows)) self.fieldsView.scrollTo(index) - self.fieldsView.scrollTo(index) @pyqtSlot(bool, name='on_deleteButton_clicked') def on_deleteButton_clicked(self, checked=False): - sel = self.fieldsView.selectionModel() - if not sel.hasSelection(): - return - - indexes = sel.selectedRows() - for index in indexes: - self.model.removeRows(index.row(), 1) + self.fieldsView.removeSelectedFields() @pyqtSlot(bool, name='on_upButton_clicked') def on_upButton_clicked(self, checked=False): - sel = self.fieldsView.selectionModel() - if not sel.hasSelection(): - return - - row = sel.selectedRows()[0].row() - if row == 0: - return - - self.model.insertRows(row - 1, 1) - - for column in range(self.model.columnCount()): - srcIndex = self.model.index(row + 1, column) - dstIndex = self.model.index(row - 1, column) - value = self.model.data(srcIndex, Qt.EditRole) - self.model.setData(dstIndex, value, Qt.EditRole) - - self.model.removeRows(row + 1, 1) - - sel.select( - self.model.index(row - 1, 0), - QItemSelectionModel.SelectionFlags( - QItemSelectionModel.Clear | - QItemSelectionModel.Select | - QItemSelectionModel.Current | - QItemSelectionModel.Rows)) + self.fieldsView.moveSelectedFieldsUp() @pyqtSlot(bool, name='on_downButton_clicked') def on_downButton_clicked(self, checked=False): - sel = self.fieldsView.selectionModel() - if not sel.hasSelection(): - return - - row = sel.selectedRows()[0].row() - if row == self.model.rowCount() - 1: - return - - self.model.insertRows(row + 2, 1) - - for column in range(self.model.columnCount()): - srcIndex = self.model.index(row, column) - dstIndex = self.model.index(row + 2, column) - value = self.model.data(srcIndex, Qt.EditRole) - self.model.setData(dstIndex, value, Qt.EditRole) - - self.model.removeRows(row, 1) - - sel.select( - self.model.index(row + 1, 0), - QItemSelectionModel.SelectionFlags( - QItemSelectionModel.Clear | - QItemSelectionModel.Select | - QItemSelectionModel.Current | - QItemSelectionModel.Rows)) + self.fieldsView.moveSelectedFieldsDown() @pyqtSlot(bool, name='on_resetButton_clicked') def on_resetButton_clicked(self, checked=False): - self.model.loadLayerFields(self.model.layer()) - - def resizeColumns(self): - header = self.fieldsView.horizontalHeader() - header.resizeSections(QHeaderView.ResizeToContents) - for section in range(header.count()): - size = header.sectionSize(section) - fieldType = self.model.columns[section]['type'] - if fieldType == QgsExpression: - header.resizeSection(section, size + 100) - else: - header.resizeSection(section, size + 20) - - def openPersistentEditors(self, row): - for index, column in enumerate(self.model.columns): - if 'persistentEditor' in column.keys() and column['persistentEditor']: - self.fieldsView.openPersistentEditor(self.model.index(row, index)) - continue - - editor = self.fieldsView.indexWidget(self.model.index(row, index)) - if isinstance(editor, QLineEdit): - editor.deselect() - if isinstance(editor, QSpinBox): - lineEdit = editor.findChild(QLineEdit) - lineEdit.setAlignment(Qt.AlignRight or Qt.AlignVCenter) - lineEdit.deselect() - - def on_model_modelReset(self): - for row in range(0, self.model.rowCount()): - self.openPersistentEditors(row) - self.resizeColumns() - - def on_model_rowsInserted(self, parent, start, end): - for row in range(start, end + 1): - self.openPersistentEditors(row) + """Load fields from layer""" + if self.layer: + self.fieldsView.setDestinationFields(self.layer.fields()) + self.fieldsView.setSourceFields(self.layer.fields()) @pyqtSlot(bool, name='on_loadLayerFieldsButton_clicked') def on_loadLayerFieldsButton_clicked(self, checked=False): layer = self.layerCombo.currentLayer() if layer is None: return - self.model.loadLayerFields(layer) + self.fieldsView.setSourceFields(layer.fields()) class FieldsMappingWidgetWrapper(WidgetWrapper): @@ -494,8 +187,6 @@ def createWidget(self): self.panel = self.createPanel() self.panel.dialogType = self.dialogType - self.panel.setContextGenerator(self) - if self.dialogType == DIALOG_MODELER: self.combobox = QComboBox() self.combobox.addItem(QCoreApplication.translate('Processing', '[Preconfigure]'), None) diff --git a/python/plugins/processing/algs/qgis/ui/fieldsmappingpanelbase.ui b/python/plugins/processing/algs/qgis/ui/fieldsmappingpanelbase.ui index 62f7511232ca..e42076152353 100644 --- a/python/plugins/processing/algs/qgis/ui/fieldsmappingpanelbase.ui +++ b/python/plugins/processing/algs/qgis/ui/fieldsmappingpanelbase.ui @@ -20,18 +20,27 @@ Fields - + + 0 + + + 0 + + + 0 + + 0 - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows + + + + 0 + 0 + @@ -109,7 +118,7 @@ - Load fields from layer + Load fields from template layer @@ -132,7 +141,7 @@ - Load fields from selected layer + Load fields from selected template layer Load Fields @@ -150,6 +159,12 @@
    qgis.gui
    1 + + QgsFieldMappingWidget + QWidget +
    qgis.gui
    + 1 +
    diff --git a/python/plugins/processing/algs/saga/SagaAlgorithmProvider.py b/python/plugins/processing/algs/saga/SagaAlgorithmProvider.py index a8c8e049c3a2..85db6150061a 100755 --- a/python/plugins/processing/algs/saga/SagaAlgorithmProvider.py +++ b/python/plugins/processing/algs/saga/SagaAlgorithmProvider.py @@ -146,6 +146,11 @@ def supportedOutputVectorLayerExtensions(self): def supportedOutputTableExtensions(self): return ['dbf'] + def flags(self): + # push users towards alternative algorithms instead, SAGA algorithms should only be used by experienced + # users who understand and can workaround the frequent issues encountered here + return QgsProcessingProvider.FlagDeemphasiseSearchResults + def supportsNonFileBasedOutput(self): """ SAGA Provider doesn't support non file based outputs diff --git a/python/plugins/processing/core/parameters.py b/python/plugins/processing/core/parameters.py index fb33f5a77741..731dc20f82bd 100755 --- a/python/plugins/processing/core/parameters.py +++ b/python/plugins/processing/core/parameters.py @@ -78,7 +78,7 @@ PARAMETER_MULTIPLE = 'multilayer' PARAMETER_BAND = 'band' PARAMETER_LAYOUTITEM = 'layoutitem' -PARAMETER_MAP_LAYER = 'Map Layer' +PARAMETER_MAP_LAYER = 'layer' PARAMETER_RANGE = 'range' PARAMETER_ENUM = 'enum' PARAMETER_MATRIX = 'matrix' @@ -117,6 +117,10 @@ def getParameterFromString(s, context=''): elif clazz == QgsProcessingParameterMapLayer: if len(params) > 3: params[3] = True if params[3].lower() == 'true' else False + try: + params[4] = [int(p) for p in params[4].split(';')] + except: + params[4] = [getattr(QgsProcessing, p.split(".")[1]) for p in params[4].split(';')] elif clazz == QgsProcessingParameterBoolean: if len(params) > 2: params[2] = True if params[2].lower() == 'true' else False diff --git a/python/plugins/processing/gui/AlgorithmDialog.py b/python/plugins/processing/gui/AlgorithmDialog.py index c6a7628ee46c..6ed92ea6eef8 100644 --- a/python/plugins/processing/gui/AlgorithmDialog.py +++ b/python/plugins/processing/gui/AlgorithmDialog.py @@ -21,31 +21,21 @@ __date__ = 'August 2012' __copyright__ = '(C) 2012, Victor Olaya' -import os from pprint import pformat import time -from qgis.PyQt.QtCore import QCoreApplication, Qt -from qgis.PyQt.QtWidgets import QMessageBox, QPushButton, QSizePolicy, QDialogButtonBox +from qgis.PyQt.QtCore import QCoreApplication +from qgis.PyQt.QtWidgets import QMessageBox, QPushButton, QDialogButtonBox from qgis.PyQt.QtGui import QColor, QPalette from qgis.core import (Qgis, - QgsProject, QgsApplication, - QgsProcessingUtils, - QgsProcessingParameterDefinition, QgsProcessingAlgRunnerTask, QgsProcessingOutputHtml, - QgsProcessingParameterVectorDestination, - QgsProcessingOutputLayerDefinition, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterRasterDestination, QgsProcessingAlgorithm, - QgsProcessingParameters, QgsProxyProgressTask, - QgsTaskManager) + QgsProcessingFeatureSourceDefinition) from qgis.gui import (QgsGui, - QgsMessageBar, QgsProcessingAlgorithmDialogBase) from qgis.utils import iface @@ -57,7 +47,6 @@ from processing.gui.AlgorithmDialogBase import AlgorithmDialogBase from processing.gui.AlgorithmExecutor import executeIterating, execute, execute_in_place from processing.gui.Postprocessing import handleAlgorithmResults -from processing.gui.wrappers import WidgetWrapper from processing.tools import dataobjects @@ -80,18 +69,24 @@ def __init__(self, alg, in_place=False, parent=None): if not self.in_place: self.runAsBatchButton = QPushButton(QCoreApplication.translate("AlgorithmDialog", "Run as Batch Process…")) self.runAsBatchButton.clicked.connect(self.runAsBatch) - self.buttonBox().addButton(self.runAsBatchButton, QDialogButtonBox.ResetRole) # reset role to ensure left alignment + self.buttonBox().addButton(self.runAsBatchButton, + QDialogButtonBox.ResetRole) # reset role to ensure left alignment else: self.active_layer = iface.activeLayer() self.runAsBatchButton = None has_selection = self.active_layer and (self.active_layer.selectedFeatureCount() > 0) - self.buttonBox().button(QDialogButtonBox.Ok).setText(QCoreApplication.translate("AlgorithmDialog", "Modify Selected Features") - if has_selection else QCoreApplication.translate("AlgorithmDialog", "Modify All Features")) - self.buttonBox().button(QDialogButtonBox.Close).setText(QCoreApplication.translate("AlgorithmDialog", "Cancel")) + self.buttonBox().button(QDialogButtonBox.Ok).setText( + QCoreApplication.translate("AlgorithmDialog", "Modify Selected Features") + if has_selection else QCoreApplication.translate("AlgorithmDialog", "Modify All Features")) + self.buttonBox().button(QDialogButtonBox.Close).setText( + QCoreApplication.translate("AlgorithmDialog", "Cancel")) self.setWindowTitle(self.windowTitle() + ' | ' + self.active_layer.name()) + self.updateRunButtonVisibility() + def getParametersPanel(self, alg, parent): - return ParametersPanel(parent, alg, self.in_place) + panel = ParametersPanel(parent, alg, self.in_place) + return panel def runAsBatch(self): self.close() @@ -99,73 +94,22 @@ def runAsBatch(self): dlg.show() dlg.exec_() + def resetAdditionalGui(self): + if not self.in_place: + self.runAsBatchButton.setEnabled(True) + + def blockAdditionalControlsWhileRunning(self): + if not self.in_place: + self.runAsBatchButton.setEnabled(False) + def setParameters(self, parameters): self.mainWidget().setParameters(parameters) - def getParameterValues(self): - parameters = {} - + def createProcessingParameters(self): if self.mainWidget() is None: - return parameters - - for param in self.algorithm().parameterDefinitions(): - if param.flags() & QgsProcessingParameterDefinition.FlagHidden: - continue - if not param.isDestination(): - - if self.in_place and param.name() == 'INPUT': - parameters[param.name()] = self.active_layer - continue - - try: - wrapper = self.mainWidget().wrappers[param.name()] - except KeyError: - continue - - # For compatibility with 3.x API, we need to check whether the wrapper is - # the deprecated WidgetWrapper class. If not, it's the newer - # QgsAbstractProcessingParameterWidgetWrapper class - # TODO QGIS 4.0 - remove - if issubclass(wrapper.__class__, WidgetWrapper): - widget = wrapper.widget - else: - widget = wrapper.wrappedWidget() - - if widget is None: - continue - - value = wrapper.parameterValue() - parameters[param.name()] = value - - if not param.checkValueIsAcceptable(value): - raise AlgorithmDialogBase.InvalidParameterValue(param, widget) - else: - if self.in_place and param.name() == 'OUTPUT': - parameters[param.name()] = 'memory:' - continue - - dest_project = None - if not param.flags() & QgsProcessingParameterDefinition.FlagHidden and \ - isinstance(param, (QgsProcessingParameterRasterDestination, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterVectorDestination)): - if self.mainWidget().checkBoxes[param.name()].isChecked(): - dest_project = QgsProject.instance() - - widget = self.mainWidget().outputWidgets[param.name()] - value = widget.getValue() - - if value and isinstance(value, QgsProcessingOutputLayerDefinition): - value.destinationProject = dest_project - if value: - parameters[param.name()] = value - if param.isDestination(): - context = dataobjects.createContext() - ok, error = self.algorithm().provider().isSupportedOutputValue(value, param, context) - if not ok: - raise AlgorithmDialogBase.InvalidOutputExtension(widget, error) - - return self.algorithm().preprocessParameters(parameters) + return {} + else: + return self.mainWidget().createProcessingParameters() def runAlgorithm(self): self.feedback = self.createFeedback() @@ -173,7 +117,7 @@ def runAlgorithm(self): checkCRS = ProcessingConfig.getSetting(ProcessingConfig.WARN_UNMATCHING_CRS) try: - parameters = self.getParameterValues() + parameters = self.createProcessingParameters() if checkCRS and not self.algorithm().validateInputCrs(parameters, self.context): reply = QMessageBox.question(self, self.tr("Unmatching CRS's"), @@ -189,15 +133,17 @@ def runAlgorithm(self): QMessageBox.warning( self, self.tr('Unable to execute algorithm'), msg) return - self.runButton().setEnabled(False) + + self.blockControlsWhileRunning() + self.setExecutedAnyResult(True) self.cancelButton().setEnabled(False) - buttons = self.mainWidget().iterateButtons + self.iterateParam = None - for i in range(len(list(buttons.values()))): - button = list(buttons.values())[i] - if button.isChecked(): - self.iterateParam = list(buttons.keys())[i] + for param in self.algorithm().parameterDefinitions(): + if isinstance(parameters.get(param.name(), None), QgsProcessingFeatureSourceDefinition) and parameters[ + param.name()].flags & QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature: + self.iterateParam = param.name() break self.clearProgress() @@ -208,12 +154,14 @@ def runAlgorithm(self): self.setProgressText(QCoreApplication.translate('AlgorithmDialog', 'Processing algorithm…')) self.setInfo( - QCoreApplication.translate('AlgorithmDialog', '
    Algorithm \'{0}\' starting…').format(self.algorithm().displayName()), escapeHtml=False) + QCoreApplication.translate('AlgorithmDialog', 'Algorithm \'{0}\' starting…').format( + self.algorithm().displayName()), escapeHtml=False) self.feedback.pushInfo(self.tr('Input parameters:')) display_params = [] for k, v in parameters.items(): - display_params.append("'" + k + "' : " + self.algorithm().parameterDefinition(k).valueAsPythonString(v, self.context)) + display_params.append( + "'" + k + "' : " + self.algorithm().parameterDefinition(k).valueAsPythonString(v, self.context)) self.feedback.pushCommandInfo('{ ' + ', '.join(display_params) + ' }') self.feedback.pushInfo('') start_time = time.time() @@ -244,9 +192,11 @@ def runAlgorithm(self): def on_complete(ok, results): if ok: - self.feedback.pushInfo(self.tr('Execution completed in {0:0.2f} seconds').format(time.time() - start_time)) + self.feedback.pushInfo( + self.tr('Execution completed in {0:0.2f} seconds').format(time.time() - start_time)) self.feedback.pushInfo(self.tr('Results:')) - self.feedback.pushCommandInfo(pformat(results)) + r = {k: v for k, v in results.items() if k not in ('CHILD_RESULTS', 'CHILD_INPUTS')} + self.feedback.pushCommandInfo(pformat(r)) else: self.feedback.reportError( self.tr('Execution failed after {0:0.2f} seconds').format(time.time() - start_time)) @@ -275,7 +225,9 @@ def on_complete(ok, results): task.executed.connect(on_complete) self.setCurrentTask(task) else: - self.proxy_progress = QgsProxyProgressTask(QCoreApplication.translate("AlgorithmDialog", "Executing “{}”").format(self.algorithm().displayName())) + self.proxy_progress = QgsProxyProgressTask( + QCoreApplication.translate("AlgorithmDialog", "Executing “{}”").format( + self.algorithm().displayName())) QgsApplication.taskManager().addTask(self.proxy_progress) self.feedback.progressChanged.connect(self.proxy_progress.setProxyProgress) self.feedback_dialog = self.createProgressDialog() @@ -298,8 +250,9 @@ def on_complete(ok, results): except: pass self.messageBar().clearWidgets() - self.messageBar().pushMessage("", self.tr("Wrong or missing parameter value: {0}").format(e.parameter.description()), - level=Qgis.Warning, duration=5) + self.messageBar().pushMessage("", self.tr("Wrong or missing parameter value: {0}").format( + e.parameter.description()), + level=Qgis.Warning, duration=5) except AlgorithmDialogBase.InvalidOutputExtension as e: try: self.buttonBox().accepted.connect(lambda e=e: @@ -321,7 +274,8 @@ def finish(self, successful, result, context, feedback, in_place=False): # add html results to results dock for out in self.algorithm().outputDefinitions(): if isinstance(out, QgsProcessingOutputHtml) and out.name() in result and result[out.name()]: - resultsList.addResult(icon=self.algorithm().icon(), name=out.description(), timestamp=time.localtime(), + resultsList.addResult(icon=self.algorithm().icon(), name=out.description(), + timestamp=time.localtime(), result=result[out.name()]) if not handleAlgorithmResults(self.algorithm(), context, feedback, not keepOpen, result): self.resetGui() @@ -330,6 +284,7 @@ def finish(self, successful, result, context, feedback, in_place=False): self.setExecuted(True) self.setResults(result) self.setInfo(self.tr('Algorithm \'{0}\' finished').format(self.algorithm().displayName()), escapeHtml=False) + self.algorithmFinished.emit(successful, result) if not in_place and not keepOpen: self.close() diff --git a/python/plugins/processing/gui/AlgorithmLocatorFilter.py b/python/plugins/processing/gui/AlgorithmLocatorFilter.py index 6722593f1cbc..cebf26a55478 100644 --- a/python/plugins/processing/gui/AlgorithmLocatorFilter.py +++ b/python/plugins/processing/gui/AlgorithmLocatorFilter.py @@ -30,7 +30,8 @@ QgsProcessing, QgsWkbTypes, QgsMapLayerType, - QgsFields) + QgsFields, + QgsStringUtils) from processing.gui.MessageBarProgress import MessageBarProgress from processing.gui.MessageDialog import MessageDialog from processing.gui.AlgorithmDialog import AlgorithmDialog @@ -72,17 +73,31 @@ def fetchResults(self, string, context, feedback): a.flags() & QgsProcessingAlgorithm.FlagKnownIssues: continue - if QgsLocatorFilter.stringMatches(a.displayName(), string) or [t for t in a.tags() if QgsLocatorFilter.stringMatches(t, string)] or \ - (context.usingPrefix and not string): - result = QgsLocatorResult() - result.filter = self - result.displayString = a.displayName() - result.icon = a.icon() - result.userData = a.id() - if string and QgsLocatorFilter.stringMatches(a.displayName(), string): - result.score = float(len(string)) / len(a.displayName()) - else: - result.score = 0 + result = QgsLocatorResult() + result.filter = self + result.displayString = a.displayName() + result.icon = a.icon() + result.userData = a.id() + result.score = 0 + + if (context.usingPrefix and not string): + self.resultFetched.emit(result) + + if not string: + return + + string = string.lower() + tagScore = 0 + tags = [*a.tags(), a.provider().name(), a.group()] + + for t in tags: + if string in t.lower(): + tagScore = 1 + break + + result.score = QgsStringUtils.fuzzyScore(result.displayString, string) * 0.5 + tagScore * 0.5 + + if result.score > 0: self.resultFetched.emit(result) def triggerResult(self, result): diff --git a/python/plugins/processing/gui/BatchAlgorithmDialog.py b/python/plugins/processing/gui/BatchAlgorithmDialog.py index e4fd423f9b26..643dd65a8dcc 100644 --- a/python/plugins/processing/gui/BatchAlgorithmDialog.py +++ b/python/plugins/processing/gui/BatchAlgorithmDialog.py @@ -77,6 +77,8 @@ def __init__(self, alg, parent=None): self.btnRunSingle.clicked.connect(self.runAsSingle) self.buttonBox().addButton(self.btnRunSingle, QDialogButtonBox.ResetRole) # reset role to ensure left alignment + self.updateRunButtonVisibility() + def runAsSingle(self): self.close() @@ -85,6 +87,12 @@ def runAsSingle(self): dlg.show() dlg.exec_() + def resetAdditionalGui(self): + self.btnRunSingle.setEnabled(True) + + def blockAdditionalControlsWhileRunning(self): + self.btnRunSingle.setEnabled(False) + def runAlgorithm(self): alg_parameters = [] @@ -94,8 +102,11 @@ def runAlgorithm(self): project = QgsProject.instance() if load_layers else None for row in range(self.mainWidget().batchRowCount()): - parameters = self.mainWidget().parametersForRow(row, destinationProject=project, warnOnInvalid=True) - alg_parameters.append(parameters) + parameters, ok = self.mainWidget().parametersForRow(row, destinationProject=project, warnOnInvalid=True) + if ok: + alg_parameters.append(parameters) + if not alg_parameters: + return task = QgsScopedProxyProgressTask(self.tr('Batch Processing - {0}').format(self.algorithm().displayName())) multi_feedback = BatchFeedback(len(alg_parameters), feedback) @@ -106,7 +117,8 @@ def runAlgorithm(self): with OverrideCursor(Qt.WaitCursor): - self.mainWidget().setEnabled(False) + self.blockControlsWhileRunning() + self.setExecutedAnyResult(True) self.cancelButton().setEnabled(True) # Make sure the Log tab is visible before executing the algorithm @@ -178,7 +190,6 @@ def finish(self, algorithm_results, errors): self.loadHTMLResults(results['results'], count) self.createSummaryTable(algorithm_results, errors) - self.mainWidget().setEnabled(True) self.resetGui() def loadHTMLResults(self, results, num): diff --git a/python/plugins/processing/gui/BatchInputSelectionPanel.py b/python/plugins/processing/gui/BatchInputSelectionPanel.py index f04c851250b3..226a2e5ef368 100644 --- a/python/plugins/processing/gui/BatchInputSelectionPanel.py +++ b/python/plugins/processing/gui/BatchInputSelectionPanel.py @@ -43,7 +43,6 @@ from processing.gui.MultipleInputDialog import MultipleInputDialog -from processing.gui.ParameterGuiUtils import getFileFilter from processing.tools import dataobjects @@ -174,7 +173,7 @@ def showFileDialog(self, seldir): if not seldir: ret, selected_filter = QFileDialog.getOpenFileNames( - self, self.tr('Select Files'), path, getFileFilter(self.param) + self, self.tr('Select Files'), path, self.param.createFileFilter() ) else: ret = QFileDialog.getExistingDirectory(self, self.tr('Select Directory'), path) diff --git a/python/plugins/processing/gui/BatchOutputSelectionPanel.py b/python/plugins/processing/gui/BatchOutputSelectionPanel.py index e24741dfcf17..ced93362ffd8 100644 --- a/python/plugins/processing/gui/BatchOutputSelectionPanel.py +++ b/python/plugins/processing/gui/BatchOutputSelectionPanel.py @@ -37,7 +37,6 @@ from qgis.PyQt.QtWidgets import QWidget, QPushButton, QLineEdit, QHBoxLayout, QSizePolicy, QFileDialog from processing.gui.AutofillDialog import AutofillDialog -from processing.gui.ParameterGuiUtils import getFileFilter class BatchOutputSelectionPanel(QWidget): @@ -70,7 +69,7 @@ def showSelectionDialog(self): self.selectDirectory() return - filefilter = getFileFilter(self.output) + filefilter = self.output.createFileFilter() settings = QgsSettings() if settings.contains('/Processing/LastBatchOutputPath'): path = str(settings.value('/Processing/LastBatchOutputPath')) diff --git a/python/plugins/processing/gui/BatchPanel.py b/python/plugins/processing/gui/BatchPanel.py index 2b7be0d1762b..1042762d3976 100644 --- a/python/plugins/processing/gui/BatchPanel.py +++ b/python/plugins/processing/gui/BatchPanel.py @@ -24,6 +24,7 @@ import os import json import warnings +from pathlib import Path from qgis.PyQt import uic from qgis.PyQt.QtWidgets import ( @@ -60,16 +61,21 @@ QgsProcessingParameterFeatureSource, QgsProcessingParameterRasterDestination, QgsProcessingParameterVectorDestination, + QgsProcessingParameterMultipleLayers, QgsProcessingParameterFeatureSink, QgsProcessingOutputLayerDefinition, QgsExpressionContextUtils, - QgsExpression + QgsProcessing, + QgsExpression, + QgsRasterLayer, + QgsProcessingUtils ) from qgis.gui import ( QgsProcessingParameterWidgetContext, QgsProcessingContextGenerator, QgsFindFilesByPatternDialog, - QgsExpressionBuilderDialog + QgsExpressionBuilderDialog, + QgsPanelWidget ) from qgis.utils import iface @@ -78,6 +84,7 @@ from processing.tools import dataobjects from processing.tools.dataobjects import createContext +from processing.gui.MultipleInputDialog import MultipleInputDialog pluginPath = os.path.split(os.path.dirname(__file__))[0] @@ -137,7 +144,8 @@ def createMenu(self): QgsProcessingParameterRasterLayer, QgsProcessingParameterMeshLayer, QgsProcessingParameterVectorLayer, - QgsProcessingParameterFeatureSource)): + QgsProcessingParameterFeatureSource, + QgsProcessingParameterMultipleLayers)): self.menu.addSeparator() find_by_pattern_action = QAction(QCoreApplication.translate('BatchPanel', 'Add Files by Pattern…'), self.menu) @@ -145,6 +153,22 @@ def createMenu(self): find_by_pattern_action.setToolTip(self.tr('Adds files by a file pattern match')) self.menu.addAction(find_by_pattern_action) + select_file_action = QAction( + QCoreApplication.translate('BatchInputSelectionPanel', 'Select Files…'), self.menu) + select_file_action.triggered.connect(self.showFileSelectionDialog) + self.menu.addAction(select_file_action) + + select_directory_action = QAction( + QCoreApplication.translate('BatchInputSelectionPanel', 'Add All Files from a Directory…'), self.menu) + select_directory_action.triggered.connect(self.showDirectorySelectionDialog) + self.menu.addAction(select_directory_action) + + if not isinstance(self.parameterDefinition, QgsProcessingParameterFile): + select_layer_action = QAction( + QCoreApplication.translate('BatchInputSelectionPanel', 'Select from Open Layers…'), self.menu) + select_layer_action.triggered.connect(self.showLayerSelectionDialog) + self.menu.addAction(select_layer_action) + def fillDown(self): """ Copy the top value down @@ -190,6 +214,117 @@ def addFilesByPattern(self): for row, file in enumerate(files): self.setRowValue(first_row + row, file, context) + def showFileSelectionDialog(self): + settings = QgsSettings() + if settings.contains('/Processing/LastInputPath'): + path = str(settings.value('/Processing/LastInputPath')) + else: + path = QDir.homePath() + + files, selected_filter = QFileDialog.getOpenFileNames( + self, self.tr('Select Files'), path, self.parameterDefinition.createFileFilter() + ) + + if not files: + return + + settings.setValue('/Processing/LastInputPath', os.path.dirname(str(files[0]))) + + context = dataobjects.createContext() + + first_row = self.panel.batchRowCount() if self.panel.batchRowCount() > 1 else 0 + for row, file in enumerate(files): + self.setRowValue(first_row + row, file, context) + + def showDirectorySelectionDialog(self): + settings = QgsSettings() + if settings.contains('/Processing/LastInputPath'): + path = str(settings.value('/Processing/LastInputPath')) + else: + path = QDir.homePath() + + folder = QFileDialog.getExistingDirectory(self, self.tr('Select Directory'), path) + if not folder: + return + + settings.setValue('/Processing/LastInputPath', folder) + + files = [] + for pp in Path(folder).rglob("*"): + if not pp.is_file(): + continue + + p = pp.as_posix() + + if isinstance(self.parameterDefinition, QgsProcessingParameterRasterLayer) or \ + (isinstance(self.parameterDefinition, + QgsProcessingParameterMultipleLayers) and self.param.layerType() == QgsProcessing.TypeRaster): + if not QgsRasterLayer.isValidRasterFileName(p): + continue + + files.append(p) + + if not files: + return + + context = dataobjects.createContext() + + first_row = self.panel.batchRowCount() if self.panel.batchRowCount() > 1 else 0 + for row, file in enumerate(files): + self.setRowValue(first_row + row, file, context) + + def showLayerSelectionDialog(self): + layers = [] + if isinstance(self.parameterDefinition, QgsProcessingParameterRasterLayer): + layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance()) + elif isinstance(self.parameterDefinition, + QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.TypeRaster: + layers = QgsProcessingUtils.compatibleRasterLayers(QgsProject.instance()) + elif isinstance(self.parameterDefinition, QgsProcessingParameterVectorLayer): + layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance()) + elif isinstance(self.parameterDefinition, QgsProcessingParameterMapLayer): + layers = QgsProcessingUtils.compatibleLayers(QgsProject.instance()) + elif isinstance(self.parameterDefinition, QgsProcessingParameterMeshLayer): + layers = QgsProcessingUtils.compatibleMeshLayers(QgsProject.instance()) + elif isinstance(self.parameterDefinition, + QgsProcessingParameterMultipleLayers) and self.parameterDefinition.layerType() == QgsProcessing.TypeMesh: + layers = QgsProcessingUtils.compatibleMeshLayers(QgsProject.instance()) + else: + datatypes = [QgsProcessing.TypeVectorAnyGeometry] + if isinstance(self.parameterDefinition, QgsProcessingParameterFeatureSource): + datatypes = self.parameterDefinition.dataTypes() + elif isinstance(self.parameterDefinition, QgsProcessingParameterMultipleLayers): + datatypes = [self.parameterDefinition.layerType()] + + if QgsProcessing.TypeVectorAnyGeometry not in datatypes: + layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance(), datatypes) + else: + layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance()) + + dlg = MultipleInputDialog([layer.name() for layer in layers]) + dlg.exec_() + + def generate_layer_id(layer): + # prefer layer name if unique + if len([l for l in layers if l.name().lower() == layer.name().lower()]) == 1: + return layer.name() + else: + # otherwise fall back to layer id + return layer.id() + + if not dlg.selectedoptions: + return + + selected = dlg.selectedoptions + + context = dataobjects.createContext() + + first_row = self.panel.batchRowCount() if self.panel.batchRowCount() > 1 else 0 + for row, selected_idx in enumerate(selected): + layer = layers[selected_idx] + value = generate_layer_id(layer) + self.setRowValue(first_row + row, value, context) + def calculateByExpression(self): """ Calculates parameter values by evaluating expressions. @@ -210,7 +345,7 @@ def populateByExpression(self, adding=False): expression_context = context.expressionContext() # use the first row parameter values as a preview during expression creation - params = self.panel.parametersForRow(0, warnOnInvalid=False) + params, ok = self.panel.parametersForRow(0, warnOnInvalid=False) alg_scope = QgsExpressionContextUtils.processingAlgorithmScope(self.panel.alg, params, context) # create explicit variables corresponding to every parameter @@ -247,7 +382,7 @@ def populateByExpression(self, adding=False): self.setRowValue(row + first_row, value, context) else: for row in range(self.panel.batchRowCount()): - params = self.panel.parametersForRow(row, warnOnInvalid=False) + params, ok = self.panel.parametersForRow(row, warnOnInvalid=False) # remove previous algorithm scope -- we need to rebuild this completely, using the # other parameter values from the current row @@ -269,7 +404,7 @@ def populateByExpression(self, adding=False): self.setRowValue(row, value, context) -class BatchPanel(BASE, WIDGET): +class BatchPanel(QgsPanelWidget, WIDGET): PARAMETERS = "PARAMETERS" OUTPUTS = "OUTPUTS" @@ -451,10 +586,9 @@ def save(self): value = wrapper.parameterValue() if not param.checkValueIsAcceptable(value, context): - self.parent.messageBar().pushMessage("", self.tr( - 'Wrong or missing parameter value: {0} (row {1})').format( - param.description(), row + 1), - level=Qgis.Warning, duration=5) + msg = self.tr('Wrong or missing parameter value: {0} (row {1})').format( + param.description(), row + 1) + self.parent.messageBar().pushMessage("", msg, level=Qgis.Warning, duration=5) return algParams[param.name()] = param.valueAsPythonString(value, context) col += 1 @@ -494,6 +628,7 @@ def setCellWrapper(self, row, column, wrapper, context): widget_context = QgsProcessingParameterWidgetContext() widget_context.setProject(QgsProject.instance()) if iface is not None: + widget_context.setActiveLayer(iface.activeLayer()) widget_context.setMapCanvas(iface.mapCanvas()) widget_context.setMessageBar(self.parent.messageBar()) @@ -600,7 +735,7 @@ def parametersForRow(self, row, destinationProject=None, warnOnInvalid=True): self.tr('Wrong or missing parameter value: {0} (row {1})').format( param.description(), row + 1), level=Qgis.Warning, duration=5) - return {} + return {}, False col += 1 count_visible_outputs = 0 for out in self.alg.destinationParameterDefinitions(): @@ -620,8 +755,7 @@ def parametersForRow(self, row, destinationProject=None, warnOnInvalid=True): parameters[out.name()] = text col += 1 else: - self.parent.messageBar().pushMessage("", self.tr('Wrong or missing output value: {0} (row {1})').format( - out.description(), row + 1), - level=Qgis.Warning, duration=5) - return {} - return parameters + msg = self.tr('Wrong or missing output value: {0} (row {1})').format(out.description(), row + 1) + self.parent.messageBar().pushMessage("", msg, level=Qgis.Warning, duration=5) + return {}, False + return parameters, True diff --git a/python/plugins/processing/gui/DestinationSelectionPanel.py b/python/plugins/processing/gui/DestinationSelectionPanel.py deleted file mode 100644 index f92e4bebc826..000000000000 --- a/python/plugins/processing/gui/DestinationSelectionPanel.py +++ /dev/null @@ -1,357 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - OutputSelectionPanel.py - --------------------- - Date : August 2012 - Copyright : (C) 2012 by Victor Olaya - Email : volayaf 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__ = 'Victor Olaya' -__date__ = 'August 2012' -__copyright__ = '(C) 2012, Victor Olaya' - -import re -import os -import warnings - -from qgis.PyQt import uic -from qgis.PyQt.QtCore import QCoreApplication, QDir, pyqtSignal, QFileInfo -from qgis.PyQt.QtWidgets import QDialog, QMenu, QAction, QFileDialog, QInputDialog -from qgis.PyQt.QtGui import QCursor -from qgis.gui import QgsEncodingSelectionDialog -from qgis.core import (QgsProcessing, - QgsDataSourceUri, - QgsCredentials, - QgsExpression, - QgsSettings, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterRasterDestination, - QgsProcessingOutputLayerDefinition, - QgsProcessingParameterDefinition, - QgsProcessingParameterFileDestination, - QgsProcessingParameterFolderDestination, - QgsProcessingParameterVectorDestination) -from processing.core.ProcessingConfig import ProcessingConfig -from processing.tools.dataobjects import createContext -from processing.gui.PostgisTableSelector import PostgisTableSelector -from processing.gui.ParameterGuiUtils import getFileFilter - -pluginPath = os.path.split(os.path.dirname(__file__))[0] - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - WIDGET, BASE = uic.loadUiType( - os.path.join(pluginPath, 'ui', 'widgetBaseSelector.ui')) - - -class DestinationSelectionPanel(BASE, WIDGET): - - SAVE_TO_TEMP_FILE = QCoreApplication.translate( - 'DestinationSelectionPanel', '[Save to temporary file]') - SAVE_TO_TEMP_FOLDER = QCoreApplication.translate( - 'DestinationSelectionPanel', '[Save to temporary folder]') - SAVE_TO_TEMP_LAYER = QCoreApplication.translate( - 'DestinationSelectionPanel', '[Create temporary layer]') - SKIP_OUTPUT = QCoreApplication.translate( - 'DestinationSelectionPanel', '[Skip output]') - - skipOutputChanged = pyqtSignal(bool) - destinationChanged = pyqtSignal() - - def __init__(self, parameter, alg, default_selection=False): - super(DestinationSelectionPanel, self).__init__(None) - self.setupUi(self) - - self.parameter = parameter - self.alg = alg - self.default_selection = default_selection - settings = QgsSettings() - self.encoding = settings.value('/Processing/encoding', 'System') - self.use_temporary = True - - self.setValue(self.parameter.defaultValue()) - - self.btnSelect.clicked.connect(self.selectOutput) - self.leText.textEdited.connect(self.textChanged) - - def textChanged(self): - self.use_temporary = not self.leText.text() - self.destinationChanged.emit() - - def outputIsSkipped(self): - """ - Returns true if output is set to be skipped - """ - return not self.leText.text() and not self.use_temporary - - def skipOutput(self): - self.leText.setPlaceholderText(self.SKIP_OUTPUT) - self.leText.setText('') - self.use_temporary = False - self.skipOutputChanged.emit(True) - self.destinationChanged.emit() - - def selectOutput(self): - popupMenu = QMenu() - - if not self.default_selection: - if self.parameter.flags() & QgsProcessingParameterDefinition.FlagOptional: - actionSkipOutput = QAction( - self.tr('Skip Output'), self.btnSelect) - actionSkipOutput.triggered.connect(self.skipOutput) - popupMenu.addAction(actionSkipOutput) - - if isinstance(self.parameter, QgsProcessingParameterFeatureSink) \ - and self.parameter.supportsNonFileBasedOutput(): - # use memory layers for temporary layers if supported - actionSaveToTemp = QAction( - self.tr('Create Temporary Layer'), self.btnSelect) - elif isinstance(self.parameter, QgsProcessingParameterFolderDestination): - actionSaveToTemp = QAction( - self.tr('Save to a Temporary Directory'), self.btnSelect) - else: - actionSaveToTemp = QAction( - self.tr('Save to a Temporary File'), self.btnSelect) - actionSaveToTemp.triggered.connect(self.saveToTemporary) - popupMenu.addAction(actionSaveToTemp) - - if isinstance(self.parameter, QgsProcessingParameterFolderDestination): - actionSaveToFile = QAction( - QCoreApplication.translate('DestinationSelectionPanel', 'Save to Directory…'), self.btnSelect) - actionSaveToFile.triggered.connect(self.selectDirectory) - else: - actionSaveToFile = QAction( - QCoreApplication.translate('DestinationSelectionPanel', 'Save to File…'), self.btnSelect) - actionSaveToFile.triggered.connect(self.selectFile) - popupMenu.addAction(actionSaveToFile) - - if isinstance(self.parameter, QgsProcessingParameterFeatureSink) \ - and self.parameter.supportsNonFileBasedOutput(): - actionSaveToGpkg = QAction( - QCoreApplication.translate('DestinationSelectionPanel', 'Save to GeoPackage…'), self.btnSelect) - actionSaveToGpkg.triggered.connect(self.saveToGeopackage) - popupMenu.addAction(actionSaveToGpkg) - actionSaveToPostGIS = QAction( - QCoreApplication.translate('DestinationSelectionPanel', 'Save to PostGIS Table…'), self.btnSelect) - actionSaveToPostGIS.triggered.connect(self.saveToPostGIS) - settings = QgsSettings() - settings.beginGroup('/PostgreSQL/connections/') - names = settings.childGroups() - settings.endGroup() - actionSaveToPostGIS.setEnabled(bool(names)) - popupMenu.addAction(actionSaveToPostGIS) - - actionSetEncoding = QAction( - QCoreApplication.translate('DestinationSelectionPanel', 'Change File Encoding ({})…').format(self.encoding), self.btnSelect) - actionSetEncoding.triggered.connect(self.selectEncoding) - popupMenu.addAction(actionSetEncoding) - - popupMenu.exec_(QCursor.pos()) - - def saveToTemporary(self): - if isinstance(self.parameter, QgsProcessingParameterFeatureSink) and self.parameter.supportsNonFileBasedOutput(): - self.leText.setPlaceholderText(self.SAVE_TO_TEMP_LAYER) - elif isinstance(self.parameter, QgsProcessingParameterFolderDestination): - self.leText.setPlaceholderText(self.SAVE_TO_TEMP_FOLDER) - else: - self.leText.setPlaceholderText(self.SAVE_TO_TEMP_FILE) - self.leText.setText('') - self.use_temporary = True - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - - def saveToPostGIS(self): - dlg = PostgisTableSelector(self, self.parameter.name().lower()) - dlg.exec_() - if dlg.connection: - self.use_temporary = False - settings = QgsSettings() - mySettings = '/PostgreSQL/connections/' + dlg.connection - dbname = settings.value(mySettings + '/database') - user = settings.value(mySettings + '/username') - host = settings.value(mySettings + '/host') - port = settings.value(mySettings + '/port') - password = settings.value(mySettings + '/password') - uri = QgsDataSourceUri() - uri.setConnection(host, str(port), dbname, user, password) - uri.setDataSource(dlg.schema, dlg.table, - "the_geom" if isinstance(self.parameter, QgsProcessingParameterFeatureSink) and self.parameter.hasGeometry() else None) - - connInfo = uri.connectionInfo() - (success, user, passwd) = QgsCredentials.instance().get(connInfo, None, None) - if success: - QgsCredentials.instance().put(connInfo, user, passwd) - self.leText.setText("postgis:" + uri.uri()) - - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - - def saveToGeopackage(self): - file_filter = self.tr('GeoPackage files (*.gpkg);;All files (*.*)', 'OutputFile') - - settings = QgsSettings() - if settings.contains('/Processing/LastOutputPath'): - path = settings.value('/Processing/LastOutputPath') - else: - path = ProcessingConfig.getSetting(ProcessingConfig.OUTPUT_FOLDER) - - filename, filter = QFileDialog.getSaveFileName(self, self.tr("Save to GeoPackage"), path, - file_filter, options=QFileDialog.DontConfirmOverwrite) - - if not filename: - return - - layer_name, ok = QInputDialog.getText(self, self.tr('Save to GeoPackage'), self.tr('Layer name'), text=self.parameter.name().lower()) - if ok: - self.use_temporary = False - if not filename.lower().endswith('.gpkg'): - filename += '.gpkg' - settings.setValue('/Processing/LastOutputPath', - os.path.dirname(filename)) - - uri = QgsDataSourceUri() - uri.setDatabase(filename) - uri.setDataSource('', layer_name, - 'geom' if isinstance(self.parameter, QgsProcessingParameterFeatureSink) and self.parameter.hasGeometry() else None) - self.leText.setText("ogr:" + uri.uri()) - - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - - def selectFile(self): - file_filter = getFileFilter(self.parameter) - settings = QgsSettings() - if isinstance(self.parameter, (QgsProcessingParameterFeatureSink, QgsProcessingParameterVectorDestination)): - last_ext_path = '/Processing/LastVectorOutputExt' - last_ext = settings.value(last_ext_path, '.{}'.format(self.parameter.defaultFileExtension())) - elif isinstance(self.parameter, QgsProcessingParameterRasterDestination): - last_ext_path = '/Processing/LastRasterOutputExt' - last_ext = settings.value(last_ext_path, '.{}'.format(self.parameter.defaultFileExtension())) - else: - last_ext_path = None - last_ext = None - - # get default filter - filters = file_filter.split(';;') - try: - last_filter = [f for f in filters if '*{}'.format(last_ext) in f.lower()][0] - except IndexError: - last_filter = None - - if settings.contains('/Processing/LastOutputPath'): - path = settings.value('/Processing/LastOutputPath') - else: - path = ProcessingConfig.getSetting(ProcessingConfig.OUTPUT_FOLDER) - - filename, filter = QFileDialog.getSaveFileName(self, self.tr("Save file"), path, - file_filter, last_filter) - if filename: - self.use_temporary = False - if not filename.lower().endswith( - tuple(re.findall("\\*(\\.[a-z]{1,10})", file_filter))): - ext = re.search("\\*(\\.[a-z]{1,10})", filter) - if ext: - filename += ext.group(1) - self.leText.setText(filename) - settings.setValue('/Processing/LastOutputPath', - os.path.dirname(filename)) - if not last_ext_path is None: - settings.setValue(last_ext_path, os.path.splitext(filename)[1].lower()) - - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - - def selectEncoding(self): - dialog = QgsEncodingSelectionDialog( - self, self.tr('File encoding'), self.encoding) - if dialog.exec_() == QDialog.Accepted: - self.encoding = dialog.encoding() - settings = QgsSettings() - settings.setValue('/Processing/encoding', self.encoding) - self.destinationChanged.emit() - dialog.deleteLater() - - def selectDirectory(self): - lastDir = self.leText.text() - settings = QgsSettings() - if not lastDir: - lastDir = settings.value("/Processing/LastOutputPath", QDir.homePath()) - - dirName = QFileDialog.getExistingDirectory(self, self.tr('Select Directory'), - lastDir, QFileDialog.ShowDirsOnly) - if dirName: - self.leText.setText(QDir.toNativeSeparators(dirName)) - settings.setValue('/Processing/LastOutputPath', dirName) - self.use_temporary = False - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - - def setValue(self, value): - if not value: - if self.parameter.flags() & QgsProcessingParameterDefinition.FlagOptional and \ - not self.parameter.createByDefault(): - self.skipOutput() - else: - self.saveToTemporary() - else: - if value in ('memory:', QgsProcessing.TEMPORARY_OUTPUT): - self.saveToTemporary() - elif isinstance(value, QgsProcessingOutputLayerDefinition): - if value.sink.staticValue() in ('memory:', ''): - self.saveToTemporary() - else: - self.leText.setText(value.sink.staticValue()) - self.use_temporary = False - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - self.encoding = value.createOptions['fileEncoding'] - else: - self.leText.setText(value) - self.use_temporary = False - self.skipOutputChanged.emit(False) - self.destinationChanged.emit() - - def getValue(self): - key = None - if self.use_temporary and isinstance(self.parameter, QgsProcessingParameterFeatureSink): - key = QgsProcessing.TEMPORARY_OUTPUT - elif self.use_temporary and not self.default_selection: - key = QgsProcessing.TEMPORARY_OUTPUT - else: - key = self.leText.text() - - if not key and self.parameter.flags() & QgsProcessingParameterDefinition.FlagOptional: - return None - - if key and not key == QgsProcessing.TEMPORARY_OUTPUT \ - and not key.startswith('memory:') \ - and not key.startswith('ogr:') \ - and not key.startswith('postgres:') \ - and not key.startswith('postgis:'): - # output should be a file path - folder = QFileInfo(key).path() - if folder == '.': - # output name does not include a folder - use default - default_folder = ProcessingConfig.getSetting(ProcessingConfig.OUTPUT_FOLDER) - key = QDir(default_folder).filePath(key) - - if isinstance(self.parameter, QgsProcessingParameterFolderDestination): - return key - - if isinstance(self.parameter, QgsProcessingParameterFileDestination): - return key - - value = QgsProcessingOutputLayerDefinition(key) - value.createOptions = {'fileEncoding': self.encoding} - return value diff --git a/python/plugins/processing/gui/HistoryDialog.py b/python/plugins/processing/gui/HistoryDialog.py index 3bc514ff00d9..cab69b3067ce 100644 --- a/python/plugins/processing/gui/HistoryDialog.py +++ b/python/plugins/processing/gui/HistoryDialog.py @@ -120,7 +120,7 @@ def executeAlgorithm(self): if isinstance(item, TreeLogEntryItem): if item.isAlg: script = 'import processing\n' - script += 'from qgis.core import QgsProcessingOutputLayerDefinition, QgsProcessingFeatureSourceDefinition, QgsProperty, QgsCoordinateReferenceSystem\n' + script += 'from qgis.core import QgsProcessingOutputLayerDefinition, QgsProcessingFeatureSourceDefinition, QgsProperty, QgsCoordinateReferenceSystem, QgsFeatureRequest\n' script += item.entry.text.replace('processing.run(', 'processing.execAlgorithmDialog(') self.close() exec(script) diff --git a/python/plugins/processing/gui/ParameterGuiUtils.py b/python/plugins/processing/gui/ParameterGuiUtils.py deleted file mode 100644 index 6dbc3321258f..000000000000 --- a/python/plugins/processing/gui/ParameterGuiUtils.py +++ /dev/null @@ -1,87 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - ParameterGuiUtils.py - --------------------- - Date : June 2017 - Copyright : (C) 2017 by Nyall Dawson - Email : nyall dot dawson 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__ = 'Nyall Dawson' -__date__ = 'June 2017' -__copyright__ = '(C) 2017, Nyall Dawson' - -from qgis.core import (QgsProcessing, - QgsProviderRegistry, - QgsProcessingFeatureSourceDefinition, - QgsVectorFileWriter, - QgsRasterFileWriter) -from qgis.PyQt.QtCore import QCoreApplication -from processing.tools import dataobjects - - -def tr(string, context=''): - if context == '': - context = 'Processing' - return QCoreApplication.translate(context, string) - - -def getFileFilter(param): - """ - Returns a suitable file filter pattern for the specified parameter definition - :param param: - :return: - """ - if param.type() == 'layer': - vectors = QgsProviderRegistry.instance().fileVectorFilters().split(';;') - vectors.pop(0) - rasters = QgsProviderRegistry.instance().fileRasterFilters().split(';;') - rasters.pop(0) - filters = set(vectors + rasters) - filters = sorted(filters) - return tr('All files (*.*)') + ';;' + ";;".join(filters) - elif param.type() == 'multilayer': - if param.layerType() == QgsProcessing.TypeRaster: - exts = QgsRasterFileWriter.supportedFormatExtensions() - elif param.layerType() == QgsProcessing.TypeFile: - return tr('All files (*.*)', 'QgsProcessingParameterMultipleLayers') - else: - exts = QgsVectorFileWriter.supportedFormatExtensions() - for i in range(len(exts)): - exts[i] = tr('{0} files (*.{1})', 'QgsProcessingParameterMultipleLayers').format(exts[i].upper(), exts[i].lower()) - return tr('All files (*.*)') + ';;' + ';;'.join(exts) - elif param.type() == 'raster': - return QgsProviderRegistry.instance().fileRasterFilters() - elif param.type() == 'rasterDestination': - exts = param.supportedOutputRasterLayerExtensions() - for i in range(len(exts)): - exts[i] = tr('{0} files (*.{1})', 'ParameterRaster').format(exts[i].upper(), exts[i].lower()) - return ';;'.join(exts) + ';;' + tr('All files (*.*)') - elif param.type() in ('sink', 'vectorDestination'): - exts = param.supportedOutputVectorLayerExtensions() - for i in range(len(exts)): - exts[i] = tr('{0} files (*.{1})', 'ParameterVector').format(exts[i].upper(), exts[i].lower()) - return ';;'.join(exts) + ';;' + tr('All files (*.*)') - elif param.type() == 'source': - return QgsProviderRegistry.instance().fileVectorFilters() - elif param.type() == 'vector': - return QgsProviderRegistry.instance().fileVectorFilters() - elif param.type() == 'fileDestination': - return param.fileFilter() + ';;' + tr('All files (*.*)') - elif param.type() == 'mesh': - return tr('All files (*.*)') - if param.defaultFileExtension(): - return tr('Default extension') + ' (*.' + param.defaultFileExtension() + ')' - else: - return '' diff --git a/python/plugins/processing/gui/ParametersPanel.py b/python/plugins/processing/gui/ParametersPanel.py index 21ebea468f9f..af47af7799c1 100644 --- a/python/plugins/processing/gui/ParametersPanel.py +++ b/python/plugins/processing/gui/ParametersPanel.py @@ -25,67 +25,31 @@ __date__ = 'August 2012' __copyright__ = '(C) 2012, Victor Olaya' -import os -import warnings - -from functools import partial - from qgis.core import (QgsProcessingParameterDefinition, QgsProcessingParameterExtent, - QgsProcessingParameterPoint, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterRasterDestination, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterVectorDestination, - QgsProcessingOutputLayerDefinition, QgsProject, QgsProcessingModelAlgorithm, - QgsVectorFileWriter) + QgsProcessingOutputLayerDefinition) from qgis.gui import (QgsProcessingContextGenerator, - QgsProcessingParameterWidgetContext) + QgsProcessingParameterWidgetContext, + QgsProcessingParametersWidget, + QgsGui, + QgsProcessingGui, + QgsProcessingParametersGenerator) from qgis.utils import iface -from qgis.PyQt import uic -from qgis.PyQt.QtCore import QCoreApplication, Qt -from qgis.PyQt.QtWidgets import (QWidget, QHBoxLayout, QToolButton, - QLabel, QCheckBox, QSizePolicy) -from qgis.PyQt.QtGui import QIcon -from osgeo import gdal - -from processing.gui.DestinationSelectionPanel import DestinationSelectionPanel from processing.gui.wrappers import WidgetWrapperFactory, WidgetWrapper +from processing.gui.AlgorithmDialogBase import AlgorithmDialogBase from processing.tools.dataobjects import createContext -pluginPath = os.path.split(os.path.dirname(__file__))[0] - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - WIDGET, BASE = uic.loadUiType( - os.path.join(pluginPath, 'ui', 'widgetParametersPanel.ui')) - - -class ParametersPanel(BASE, WIDGET): - NOT_SELECTED = QCoreApplication.translate('ParametersPanel', '[Not selected]') +class ParametersPanel(QgsProcessingParametersWidget): def __init__(self, parent, alg, in_place=False): - super(ParametersPanel, self).__init__(None) - self.setupUi(self) + super().__init__(alg, parent) self.in_place = in_place - self.grpAdvanced.hide() - - self.scrollAreaWidgetContents.setContentsMargins(4, 4, 4, 4) - self.layoutMain = self.scrollAreaWidgetContents.layout() - self.layoutAdvanced = self.grpAdvanced.layout() - - self.parent = parent - self.alg = alg self.wrappers = {} - self.outputWidgets = {} - self.checkBoxes = {} - self.dependentItems = {} - self.iterateButtons = {} self.processing_context = createContext() @@ -112,38 +76,32 @@ def layerRegistryChanged(self, layers): except AttributeError: pass - def formatParameterTooltip(self, parameter): - return '

    {}

    {}

    '.format( - parameter.description(), - QCoreApplication.translate('ParametersPanel', 'Python identifier: ‘{}’').format('{}'.format(parameter.name())) - ) - def initWidgets(self): - # If there are advanced parameters — show corresponding groupbox - for param in self.alg.parameterDefinitions(): - if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced: - self.grpAdvanced.show() - break + super().initWidgets() widget_context = QgsProcessingParameterWidgetContext() widget_context.setProject(QgsProject.instance()) if iface is not None: widget_context.setMapCanvas(iface.mapCanvas()) - widget_context.setMessageBar(self.parent.messageBar()) - if isinstance(self.alg, QgsProcessingModelAlgorithm): - widget_context.setModel(self.alg) + widget_context.setBrowserModel(iface.browserModel()) + widget_context.setActiveLayer(iface.activeLayer()) + + widget_context.setMessageBar(self.parent().messageBar()) + if isinstance(self.algorithm(), QgsProcessingModelAlgorithm): + widget_context.setModel(self.algorithm()) # Create widgets and put them in layouts - for param in self.alg.parameterDefinitions(): + for param in self.algorithm().parameterDefinitions(): if param.flags() & QgsProcessingParameterDefinition.FlagHidden: continue if param.isDestination(): continue else: - wrapper = WidgetWrapperFactory.create_wrapper(param, self.parent) + wrapper = WidgetWrapperFactory.create_wrapper(param, self.parent()) wrapper.setWidgetContext(widget_context) wrapper.registerProcessingContextGenerator(self.context_generator) + wrapper.registerProcessingParametersGenerator(self) self.wrappers[param.name()] = wrapper # For compatibility with 3.x API, we need to check whether the wrapper is @@ -167,24 +125,6 @@ def initWidgets(self): if is_python_wrapper: widget.setToolTip(param.toolTip()) - if isinstance(param, QgsProcessingParameterFeatureSource): - layout = QHBoxLayout() - layout.setSpacing(6) - layout.setMargin(0) - layout.addWidget(widget) - button = QToolButton() - icon = QIcon(os.path.join(pluginPath, 'images', 'iterate.png')) - button.setIcon(icon) - button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Expanding) - button.setToolTip(self.tr('Iterate over this layer, creating a separate output for every feature in the layer')) - button.setCheckable(True) - layout.addWidget(button) - layout.setAlignment(button, Qt.AlignTop) - self.iterateButtons[param.name()] = button - button.toggled.connect(self.buttonToggled) - widget = QWidget() - widget.setLayout(layout) - label = None if not is_python_wrapper: label = wrapper.createWrappedLabel() @@ -192,93 +132,132 @@ def initWidgets(self): label = wrapper.label if label is not None: - if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced: - self.layoutAdvanced.addWidget(label) - else: - self.layoutMain.insertWidget( - self.layoutMain.count() - 2, label) + self.addParameterLabel(param, label) elif is_python_wrapper: desc = param.description() if isinstance(param, QgsProcessingParameterExtent): desc += self.tr(' (xmin, xmax, ymin, ymax)') - if isinstance(param, QgsProcessingParameterPoint): - desc += self.tr(' (x, y)') if param.flags() & QgsProcessingParameterDefinition.FlagOptional: desc += self.tr(' [optional]') widget.setText(desc) - if param.flags() & QgsProcessingParameterDefinition.FlagAdvanced: - self.layoutAdvanced.addWidget(widget) - else: - self.layoutMain.insertWidget( - self.layoutMain.count() - 2, widget) - for output in self.alg.destinationParameterDefinitions(): + self.addParameterWidget(param, widget) + + for output in self.algorithm().destinationParameterDefinitions(): if output.flags() & QgsProcessingParameterDefinition.FlagHidden: continue - if self.in_place and param.name() in ('INPUT', 'OUTPUT'): + if self.in_place and output.name() in ('INPUT', 'OUTPUT'): continue - label = QLabel(output.description()) - widget = DestinationSelectionPanel(output, self.alg) - self.layoutMain.insertWidget(self.layoutMain.count() - 1, label) - self.layoutMain.insertWidget(self.layoutMain.count() - 1, widget) - if isinstance(output, (QgsProcessingParameterRasterDestination, QgsProcessingParameterFeatureSink, QgsProcessingParameterVectorDestination)): - check = QCheckBox() - check.setText(QCoreApplication.translate('ParametersPanel', 'Open output file after running algorithm')) - - def skipOutputChanged(widget, checkbox, skipped): - - enabled = not skipped - - # Do not try to open formats that are write-only. - value = widget.getValue() - if value and isinstance(value, QgsProcessingOutputLayerDefinition) and isinstance(output, ( - QgsProcessingParameterFeatureSink, QgsProcessingParameterVectorDestination)): - filename = value.sink.staticValue() - if filename not in ('memory:', ''): - path, ext = os.path.splitext(filename) - format = QgsVectorFileWriter.driverForExtension(ext) - drv = gdal.GetDriverByName(format) - if drv: - if drv.GetMetadataItem(gdal.DCAP_OPEN) is None: - enabled = False - - checkbox.setEnabled(enabled) - checkbox.setChecked(enabled) - - check.setChecked(not widget.outputIsSkipped()) - check.setEnabled(not widget.outputIsSkipped()) - widget.skipOutputChanged.connect(partial(skipOutputChanged, widget, check)) - self.layoutMain.insertWidget(self.layoutMain.count() - 1, check) - self.checkBoxes[output.name()] = check - - widget.setToolTip(param.toolTip()) - self.outputWidgets[output.name()] = widget + wrapper = QgsGui.processingGuiRegistry().createParameterWidgetWrapper(output, QgsProcessingGui.Standard) + wrapper.setWidgetContext(widget_context) + wrapper.registerProcessingContextGenerator(self.context_generator) + wrapper.registerProcessingParametersGenerator(self) + self.wrappers[output.name()] = wrapper + + label = wrapper.createWrappedLabel() + if label is not None: + self.addOutputLabel(label) + + widget = wrapper.createWrappedWidget(self.processing_context) + self.addOutputWidget(widget) + + # def skipOutputChanged(widget, checkbox, skipped): + # TODO + # enabled = not skipped + # + # # Do not try to open formats that are write-only. + # value = widget.value() + # if value and isinstance(value, QgsProcessingOutputLayerDefinition) and isinstance(output, ( + # QgsProcessingParameterFeatureSink, QgsProcessingParameterVectorDestination)): + # filename = value.sink.staticValue() + # if filename not in ('memory:', ''): + # path, ext = os.path.splitext(filename) + # format = QgsVectorFileWriter.driverForExtension(ext) + # drv = gdal.GetDriverByName(format) + # if drv: + # if drv.GetMetadataItem(gdal.DCAP_OPEN) is None: + # enabled = False + # + # checkbox.setEnabled(enabled) + # checkbox.setChecked(enabled) for wrapper in list(self.wrappers.values()): wrapper.postInitialize(list(self.wrappers.values())) + def createProcessingParameters(self): + parameters = {} + + for param in self.algorithm().parameterDefinitions(): + if param.flags() & QgsProcessingParameterDefinition.FlagHidden: + continue + if not param.isDestination(): + + if self.in_place and param.name() == 'INPUT': + parameters[param.name()] = self.active_layer + continue + + try: + wrapper = self.wrappers[param.name()] + except KeyError: + continue + + # For compatibility with 3.x API, we need to check whether the wrapper is + # the deprecated WidgetWrapper class. If not, it's the newer + # QgsAbstractProcessingParameterWidgetWrapper class + # TODO QGIS 4.0 - remove + if issubclass(wrapper.__class__, WidgetWrapper): + widget = wrapper.widget + else: + widget = wrapper.wrappedWidget() + + if widget is None: + continue + + value = wrapper.parameterValue() + parameters[param.name()] = value + + if not param.checkValueIsAcceptable(value): + raise AlgorithmDialogBase.InvalidParameterValue(param, widget) + else: + if self.in_place and param.name() == 'OUTPUT': + parameters[param.name()] = 'memory:' + continue + + try: + wrapper = self.wrappers[param.name()] + except KeyError: + continue + + widget = wrapper.wrappedWidget() + value = wrapper.parameterValue() + + dest_project = None + if wrapper.customProperties().get('OPEN_AFTER_RUNNING'): + dest_project = QgsProject.instance() + + if value and isinstance(value, QgsProcessingOutputLayerDefinition): + value.destinationProject = dest_project + if value: + parameters[param.name()] = value + if param.isDestination(): + context = createContext() + ok, error = self.algorithm().provider().isSupportedOutputValue(value, param, context) + if not ok: + raise AlgorithmDialogBase.InvalidOutputExtension(widget, error) + + return self.algorithm().preprocessParameters(parameters) + def setParameters(self, parameters): - for param in self.alg.parameterDefinitions(): + for param in self.algorithm().parameterDefinitions(): if param.flags() & QgsProcessingParameterDefinition.FlagHidden: continue if not param.name() in parameters: continue - if not param.isDestination(): - value = parameters[param.name()] + value = parameters[param.name()] - wrapper = self.wrappers[param.name()] - wrapper.setParameterValue(value, self.processing_context) - else: - dest_widget = self.outputWidgets[param.name()] - dest_widget.setValue(parameters[param.name()]) - - def buttonToggled(self, value): - if value: - sender = self.sender() - for button in list(self.iterateButtons.values()): - if button is not sender: - button.setChecked(False) + wrapper = self.wrappers[param.name()] + wrapper.setParameterValue(value, self.processing_context) diff --git a/python/plugins/processing/gui/PostgisTableSelector.py b/python/plugins/processing/gui/PostgisTableSelector.py deleted file mode 100644 index a7074cd42506..000000000000 --- a/python/plugins/processing/gui/PostgisTableSelector.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - PostgisTableSelector.py - --------------------- - Date : November 2015 - Copyright : (C) 2015 by Victor Olaya - Email : volayaf 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__ = 'Victor Olaya' -__date__ = 'November 2015' -__copyright__ = '(C) 2015, Victor Olaya' - - -import os -import warnings - -from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QTreeWidgetItem, QMessageBox -from qgis.PyQt import uic -from qgis.core import QgsSettings -from processing.tools.postgis import GeoDB - -pluginPath = os.path.split(os.path.dirname(__file__))[0] -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - WIDGET, BASE = uic.loadUiType( - os.path.join(pluginPath, 'ui', 'DlgPostgisTableSelector.ui')) - - -class PostgisTableSelector(BASE, WIDGET): - - def __init__(self, parent, tablename): - super(PostgisTableSelector, self).__init__(parent) - self.connection = None - self.table = None - self.schema = None - self.setupUi(self) - settings = QgsSettings() - settings.beginGroup('/PostgreSQL/connections/') - names = settings.childGroups() - settings.endGroup() - for n in names: - item = ConnectionItem(n) - self.treeConnections.addTopLevelItem(item) - - def itemExpanded(item): - try: - item.populateSchemas() - except: - pass - - self.treeConnections.itemExpanded.connect(itemExpanded) - - self.textTableName.setText(tablename) - - self.buttonBox.accepted.connect(self.okPressed) - self.buttonBox.rejected.connect(self.cancelPressed) - - def cancelPressed(self): - self.close() - - def okPressed(self): - if self.textTableName.text().strip() == "": - self.textTableName.setStyleSheet("QLineEdit{background: yellow}") - return - item = self.treeConnections.currentItem() - if isinstance(item, ConnectionItem): - QMessageBox.warning(self, "Wrong selection", "Select a schema item in the tree") - return - self.schema = item.text(0) - self.table = self.textTableName.text().strip() - self.connection = item.parent().text(0) - self.close() - - -class ConnectionItem(QTreeWidgetItem): - - def __init__(self, connection): - self.connIcon = QIcon(os.path.dirname(__file__) + '/../images/postgis.png') - self.schemaIcon = QIcon(os.path.dirname(__file__) + '/../images/namespace.png') - - QTreeWidgetItem.__init__(self) - self.setChildIndicatorPolicy(QTreeWidgetItem.ShowIndicator) - self.connection = connection - self.setText(0, connection) - self.setIcon(0, self.connIcon) - - def populateSchemas(self): - if self.childCount() != 0: - return - geodb = GeoDB.from_name(self.connection) - schemas = geodb.list_schemas() - for oid, name, owner, perms in schemas: - item = QTreeWidgetItem() - item.setText(0, name) - item.setIcon(0, self.schemaIcon) - self.addChild(item) diff --git a/python/plugins/processing/gui/enummodelerwidget.py b/python/plugins/processing/gui/enummodelerwidget.py deleted file mode 100644 index ba49a43fa8eb..000000000000 --- a/python/plugins/processing/gui/enummodelerwidget.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - EnumModelerWidget.py - --------------------- - Date : May 2018 - Copyright : (C) 2018 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__ = 'May 2018' -__copyright__ = '(C) 2018, Alexander Bruy' - -import os -import warnings - -from qgis.PyQt import uic -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem -from qgis.PyQt.QtWidgets import QMessageBox - -from qgis.core import QgsApplication - -pluginPath = os.path.split(os.path.dirname(__file__))[0] - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - WIDGET, BASE = uic.loadUiType( - os.path.join(pluginPath, 'ui', 'enummodelerwidgetbase.ui')) - - -class EnumModelerWidget(BASE, WIDGET): - - def __init__(self, parent=None): - super(EnumModelerWidget, self).__init__(parent) - self.setupUi(self) - - self.btnAdd.setIcon(QgsApplication.getThemeIcon('/symbologyAdd.svg')) - self.btnRemove.setIcon(QgsApplication.getThemeIcon('/symbologyRemove.svg')) - self.btnClear.setIcon(QgsApplication.getThemeIcon('console/iconClearConsole.svg')) - - self.btnAdd.clicked.connect(self.addItem) - self.btnRemove.clicked.connect(lambda: self.removeItems()) - self.btnClear.clicked.connect(lambda: self.removeItems(True)) - - self.lstItems.setModel(QStandardItemModel()) - - self.lstItems.model().itemChanged.connect(self.onItemChanged) - - def onItemChanged(self, item): - model = self.lstItems.model() - checkedItem = None - for i in range(model.rowCount()): - itm = model.item(i) - if itm.checkState() == Qt.Checked and itm.data() == Qt.Checked: - checkedItem = i - break - - model.blockSignals(True) - if checkedItem is None: - item.setData(item.checkState()) - else: - if self.chkAllowMultiple.isChecked(): - item.setData(item.checkState()) - else: - model.item(checkedItem).setCheckState(Qt.Unchecked) - model.item(checkedItem).setData(Qt.Unchecked) - - item.setData(item.checkState()) - model.blockSignals(False) - - def addItem(self): - model = self.lstItems.model() - - item = QStandardItem('new item') - item.setCheckable(True) - item.setDropEnabled(False) - item.setData(Qt.Unchecked) - - model.appendRow(item) - - def removeItems(self, removeAll=False): - if removeAll: - res = QMessageBox.question(self, self.tr('Clear?'), self.tr('Are you sure you want to delete all items?')) - if res == QMessageBox.Yes: - self.lstItems.model().clear() - else: - self.lstItems.setUpdatesEnabled(False) - indexes = sorted(self.lstItems.selectionModel().selectedIndexes()) - for i in reversed(indexes): - self.lstItems.model().removeRow(i.row()) - self.lstItems.setUpdatesEnabled(True) - - def options(self): - items = [] - model = self.lstItems.model() - for i in range(model.rowCount()): - item = model.item(i) - items.append(item.text()) - - return items - - def defaultOptions(self): - options = [] - model = self.lstItems.model() - for i in range(model.rowCount()): - item = model.item(i) - if item.checkState() == Qt.Checked: - if not self.allowMultiple(): - return i - options.append(i) - return options if len(options) > 0 else None - - def allowMultiple(self): - return self.chkAllowMultiple.isChecked() - - def setOptions(self, options): - model = self.lstItems.model() - for i in options: - item = QStandardItem(i) - item.setCheckable(True) - item.setDropEnabled(False) - item.setData(Qt.Unchecked) - - model.appendRow(item) - - def setDefault(self, indexes): - if indexes is None: - return - model = self.lstItems.model() - if not isinstance(indexes, (list, tuple)): - indexes = [indexes] - for i in indexes: - item = model.item(i) - if item: - item.setCheckState(Qt.Checked) - item.setData(Qt.Checked) - - def setAllowMultiple(self, allowMultiple): - self.chkAllowMultiple.setChecked(allowMultiple) diff --git a/python/plugins/processing/gui/matrixmodelerwidget.py b/python/plugins/processing/gui/matrixmodelerwidget.py deleted file mode 100644 index 3be979f1c00c..000000000000 --- a/python/plugins/processing/gui/matrixmodelerwidget.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - MatrixModelerWidget.py - --------------------- - Date : May 2018 - Copyright : (C) 2018 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__ = 'May 2018' -__copyright__ = '(C) 2018, Alexander Bruy' - -import os -import warnings - -from qgis.PyQt import uic -from qgis.PyQt.QtCore import Qt -from qgis.PyQt.QtGui import QStandardItemModel, QStandardItem -from qgis.PyQt.QtWidgets import QInputDialog, QMessageBox - -from qgis.core import QgsApplication - -pluginPath = os.path.split(os.path.dirname(__file__))[0] - -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=DeprecationWarning) - WIDGET, BASE = uic.loadUiType( - os.path.join(pluginPath, 'ui', 'matrixmodelerwidgetbase.ui')) - - -class MatrixModelerWidget(BASE, WIDGET): - - def __init__(self, parent=None): - super(MatrixModelerWidget, self).__init__(parent) - self.setupUi(self) - - self.btnAddColumn.setIcon(QgsApplication.getThemeIcon('/mActionNewAttribute.svg')) - self.btnRemoveColumn.setIcon(QgsApplication.getThemeIcon('/mActionDeleteAttribute.svg')) - self.btnAddRow.setIcon(QgsApplication.getThemeIcon('/symbologyAdd.svg')) - self.btnRemoveRow.setIcon(QgsApplication.getThemeIcon('/symbologyRemove.svg')) - self.btnClear.setIcon(QgsApplication.getThemeIcon('console/iconClearConsole.svg')) - - self.btnAddColumn.clicked.connect(self.addColumn) - self.btnRemoveColumn.clicked.connect(self.removeColumns) - self.btnAddRow.clicked.connect(self.addRow) - self.btnRemoveRow.clicked.connect(self.removeRows) - self.btnClear.clicked.connect(self.clearTable) - - items = [QStandardItem('0')] - model = QStandardItemModel() - model.appendColumn(items) - self.tblView.setModel(model) - - self.tblView.horizontalHeader().sectionDoubleClicked.connect(self.changeHeader) - - def addColumn(self): - model = self.tblView.model() - items = [QStandardItem('0') for i in range(model.rowCount())] - model.appendColumn(items) - - def removeColumns(self): - indexes = sorted(self.tblView.selectionModel().selectedColumns()) - self.tblView.setUpdatesEnabled(False) - for i in reversed(indexes): - self.tblView.model().removeColumns(i.column(), 1) - self.tblView.setUpdatesEnabled(True) - - def addRow(self): - model = self.tblView.model() - items = [QStandardItem('0') for i in range(model.columnCount())] - model.appendRow(items) - - def removeRows(self): - indexes = sorted(self.tblView.selectionModel().selectedRows()) - self.tblView.setUpdatesEnabled(False) - for i in reversed(indexes): - self.tblView.model().removeRows(i.row(), 1) - self.tblView.setUpdatesEnabled(True) - - def clearTable(self, removeAll=False): - res = QMessageBox.question(self, self.tr('Clear?'), self.tr('Are you sure you want to clear table?')) - if res == QMessageBox.Yes: - self.tblView.model().clear() - - def changeHeader(self, index): - txt, ok = QInputDialog.getText(self, self.tr("Enter column name"), self.tr("Column name")) - if ok: - self.tblView.model().setHeaderData(index, Qt.Horizontal, txt) - - def value(self): - cols = self.tblView.model().columnCount() - rows = self.tblView.model().rowCount() - - items = [] - for row in range(rows): - for col in range(cols): - items.append(str(self.tblView.model().item(row, col).text())) - - return items - - def setValue(self, headers, table): - model = self.tblView.model() - model.setHorizontalHeaderLabels(headers) - - cols = len(headers) - rows = len(table) // cols - model = QStandardItemModel(rows, cols) - - for row in range(rows): - for col in range(cols): - item = QStandardItem(str(table[row * cols + col])) - model.setItem(row, col, item) - self.tblView.setModel(model) - - def headers(self): - headers = [] - model = self.tblView.model() - for i in range(model.columnCount()): - headers.append(str(model.headerData(i, Qt.Horizontal))) - - return headers - - def fixedRows(self): - return self.chkFixedRows.isChecked() - - def setFixedRows(self, fixedRows): - self.chkFixedRows.setChecked(fixedRows) diff --git a/python/plugins/processing/gui/wrappers.py b/python/plugins/processing/gui/wrappers.py index e10ca81094c3..702a2a45e88c 100755 --- a/python/plugins/processing/gui/wrappers.py +++ b/python/plugins/processing/gui/wrappers.py @@ -22,25 +22,18 @@ __date__ = 'May 2016' __copyright__ = '(C) 2016, Arnaud Morvan' -import locale import os import re -from functools import cmp_to_key from inspect import isclass from copy import deepcopy from qgis.core import ( QgsApplication, - QgsUnitTypes, QgsCoordinateReferenceSystem, QgsExpression, - QgsExpressionContextGenerator, QgsFieldProxyModel, - QgsMapLayerProxyModel, - QgsWkbTypes, QgsSettings, QgsProject, - QgsMapLayer, QgsMapLayerType, QgsVectorLayer, QgsProcessing, @@ -53,7 +46,6 @@ QgsProcessingParameterFile, QgsProcessingParameterMultipleLayers, QgsProcessingParameterNumber, - QgsProcessingParameterDistance, QgsProcessingParameterRasterLayer, QgsProcessingParameterEnum, QgsProcessingParameterString, @@ -75,8 +67,6 @@ QgsProcessingOutputString, QgsProcessingOutputNumber, QgsProcessingModelChildParameterSource, - QgsProcessingModelAlgorithm, - QgsRasterDataProvider, NULL, Qgis) @@ -87,12 +77,13 @@ QDialog, QFileDialog, QHBoxLayout, - QVBoxLayout, QLineEdit, QPlainTextEdit, QToolButton, QWidget, + QSizePolicy ) +from qgis.PyQt.QtGui import QIcon from qgis.gui import ( QgsGui, QgsExpressionLineEdit, @@ -107,7 +98,7 @@ QgsAbstractProcessingParameterWidgetWrapper, QgsProcessingMapLayerComboBox ) -from qgis.PyQt.QtCore import pyqtSignal, QObject, QVariant, Qt +from qgis.PyQt.QtCore import QVariant, Qt from qgis.utils import iface from processing.core.ProcessingConfig import ProcessingConfig @@ -122,7 +113,6 @@ from processing.gui.BatchInputSelectionPanel import BatchInputSelectionPanel from processing.gui.FixedTablePanel import FixedTablePanel from processing.gui.ExtentSelectionPanel import ExtentSelectionPanel -from processing.gui.ParameterGuiUtils import getFileFilter from processing.tools import dataobjects @@ -130,6 +120,8 @@ DIALOG_BATCH = QgsProcessingGui.Batch DIALOG_MODELER = QgsProcessingGui.Modeler +pluginPath = os.path.split(os.path.dirname(__file__))[0] + class InvalidParameterValue(Exception): pass @@ -249,7 +241,7 @@ def getFileName(self, initial_value=''): # TODO: should use selectedFilter argument for default file format filename, selected_filter = QFileDialog.getOpenFileName(self.widget, self.tr('Select File'), - path, getFileFilter(self.parameterDefinition())) + path, self.parameterDefinition().createFileFilter()) if filename: settings.setValue('/Processing/LastInputPath', os.path.dirname(str(filename))) @@ -413,6 +405,16 @@ def value(self): class ExtentWidgetWrapper(WidgetWrapper): USE_MIN_COVERING_EXTENT = "[Use min covering extent]" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + """ + .. deprecated:: 3.14 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("ExtentWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + def createWidget(self): if self.dialogType in (DIALOG_STANDARD, DIALOG_BATCH): widget = ExtentSelectionPanel(self.dialog, self.parameterDefinition()) @@ -930,6 +932,17 @@ def value(self): class MapLayerWidgetWrapper(WidgetWrapper): NOT_SELECTED = '[Not selected]' + def __init__(self, param, dialog, row=0, col=0, **kwargs): + """ + .. deprecated:: 3.14 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("MapLayerWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + + super().__init__(param, dialog, row, col, **kwargs) + def createWidget(self): if self.dialogType == DIALOG_STANDARD: self.combo = QgsProcessingMapLayerComboBox(self.parameterDefinition()) @@ -940,14 +953,13 @@ def createWidget(self): self.combo.setValue(self.parameterDefinition().defaultValue(), self.context) else: if self.parameterDefinition().defaultValue(): - self.combo.setvalue(self.parameterDefinition().defaultValue(), self.context) + self.combo.setValue(self.parameterDefinition().defaultValue(), self.context) else: self.combo.setLayer(iface.activeLayer()) except: pass self.combo.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) - self.combo.triggerFileSelection.connect(self.selectFile) return self.combo elif self.dialogType == DIALOG_BATCH: widget = BatchInputSelectionPanel(self.parameterDefinition(), self.row, self.col, self.dialog) @@ -1028,6 +1040,17 @@ def validator(v): class RasterWidgetWrapper(MapLayerWidgetWrapper): + def __init__(self, param, dialog, row=0, col=0, **kwargs): + """ + .. deprecated:: 3.14 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("RasterWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + + super().__init__(param, dialog, row, col, **kwargs) + def getAvailableLayers(self): return self.dialog.getAvailableValuesOfType((QgsProcessingParameterRasterLayer, QgsProcessingParameterString), (QgsProcessingOutputRasterLayer, QgsProcessingOutputFile, QgsProcessingOutputString)) @@ -1050,6 +1073,17 @@ def selectFile(self): class MeshWidgetWrapper(MapLayerWidgetWrapper): + def __init__(self, param, dialog, row=0, col=0, **kwargs): + """ + .. deprecated:: 3.14 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("MeshWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + + super().__init__(param, dialog, row, col, **kwargs) + def getAvailableLayers(self): return self.dialog.getAvailableValuesOfType((QgsProcessingParameterMeshLayer, QgsProcessingParameterString), ()) @@ -1139,20 +1173,32 @@ def value(self): class FeatureSourceWidgetWrapper(WidgetWrapper): NOT_SELECTED = '[Not selected]' + def __init__(self, *args, **kwargs): + """ + .. deprecated:: 3.4 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("FeatureSourceWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + self.map_layer_combo = None + super().__init__(*args, **kwargs) + def createWidget(self): if self.dialogType == DIALOG_STANDARD: - self.combo = QgsProcessingMapLayerComboBox(self.parameterDefinition()) + self.map_layer_combo = QgsProcessingMapLayerComboBox(self.parameterDefinition()) self.context = dataobjects.createContext() try: if iface.activeLayer().type() == QgsMapLayerType.VectorLayer: - self.combo.setLayer(iface.activeLayer()) + self.map_layer_combo.setLayer(iface.activeLayer()) except: pass - self.combo.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) - self.combo.triggerFileSelection.connect(self.selectFile) - return self.combo + self.map_layer_combo.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) + + return self.map_layer_combo + elif self.dialogType == DIALOG_BATCH: widget = BatchInputSelectionPanel(self.parameterDefinition(), self.row, self.col, self.dialog) widget.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) @@ -1182,10 +1228,14 @@ def createWidget(self): widget.setLayout(layout) return widget + def setWidgetContext(self, context): + if self.map_layer_combo: + self.map_layer_combo.setWidgetContext(context) + super().setWidgetContext(context) + def selectFile(self): filename, selected_filter = self.getFileName(self.combo.currentText()) if filename: - filename = dataobjects.getRasterSublayer(filename, self.parameterDefinition()) if isinstance(self.combo, QgsProcessingMapLayerComboBox): self.combo.setValue(filename, self.context) elif isinstance(self.combo, QgsMapLayerComboBox): @@ -1206,7 +1256,7 @@ def setValue(self, value): layer = QgsProject.instance().mapLayer(value) if layer is not None: value = layer - self.combo.setValue(value, self.context) + self.map_layer_combo.setValue(value, self.context) elif self.dialogType == DIALOG_BATCH: self.widget.setValue(value) else: @@ -1215,7 +1265,7 @@ def setValue(self, value): def value(self): if self.dialogType == DIALOG_STANDARD: - return self.combo.value() + return self.map_layer_combo.value() elif self.dialogType == DIALOG_BATCH: return self.widget.getValue() else: @@ -1413,6 +1463,17 @@ def validator(v): class VectorLayerWidgetWrapper(WidgetWrapper): NOT_SELECTED = '[Not selected]' + def __init__(self, param, dialog, row=0, col=0, **kwargs): + """ + .. deprecated:: 3.14 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("VectorLayerWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + + super().__init__(param, dialog, row, col, **kwargs) + def createWidget(self): if self.dialogType == DIALOG_STANDARD: self.combo = QgsProcessingMapLayerComboBox(self.parameterDefinition()) @@ -1425,7 +1486,6 @@ def createWidget(self): pass self.combo.valueChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) - self.combo.triggerFileSelection.connect(self.selectFile) return self.combo elif self.dialogType == DIALOG_BATCH: widget = BatchInputSelectionPanel(self.parameterDefinition(), self.row, self.col, self.dialog) @@ -1655,6 +1715,14 @@ class BandWidgetWrapper(WidgetWrapper): NOT_SET = '[Not set]' def __init__(self, param, dialog, row=0, col=0, **kwargs): + """ + .. deprecated:: 3.14 + Do not use, will be removed in QGIS 4.0 + """ + + from warnings import warn + warn("BandWidgetWrapper is deprecated and will be removed in QGIS 4.0", DeprecationWarning) + super().__init__(param, dialog, row, col, **kwargs) self.context = dataobjects.createContext() @@ -1825,6 +1893,7 @@ def create_wrapper_from_class(param, dialog, row=0, col=0): # deprecated, moved to c++ wrapper = CrsWidgetWrapper elif param.type() == 'extent': + # deprecated, moved to c++ wrapper = ExtentWidgetWrapper elif param.type() == 'point': # deprecated, moved to c++ @@ -1841,6 +1910,7 @@ def create_wrapper_from_class(param, dialog, row=0, col=0): # deprecated, moved to c++ wrapper = DistanceWidgetWrapper elif param.type() == 'raster': + # deprecated, moved to c++ wrapper = RasterWidgetWrapper elif param.type() == 'enum': # deprecated, moved to c++ @@ -1852,15 +1922,19 @@ def create_wrapper_from_class(param, dialog, row=0, col=0): # deprecated, moved to c++ wrapper = ExpressionWidgetWrapper elif param.type() == 'vector': + # deprecated, moved to c++ wrapper = VectorLayerWidgetWrapper elif param.type() == 'field': # deprecated, moved to c++ wrapper = TableFieldWidgetWrapper elif param.type() == 'source': + # deprecated, moved to c++ wrapper = FeatureSourceWidgetWrapper elif param.type() == 'band': + # deprecated, moved to c++ wrapper = BandWidgetWrapper elif param.type() == 'layer': + # deprecated, moved to c++ wrapper = MapLayerWidgetWrapper elif param.type() == 'range': # deprecated, moved to c++ @@ -1869,6 +1943,7 @@ def create_wrapper_from_class(param, dialog, row=0, col=0): # deprecated, moved to c++ wrapper = FixedTableWidgetWrapper elif param.type() == 'mesh': + # deprecated, moved to c++ wrapper = MeshWidgetWrapper else: assert False, param.type() diff --git a/python/plugins/processing/gui/wrappers_postgis.py b/python/plugins/processing/gui/wrappers_postgis.py deleted file mode 100644 index f87c2e0b5ad4..000000000000 --- a/python/plugins/processing/gui/wrappers_postgis.py +++ /dev/null @@ -1,172 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - postgis.py - Postgis widget wrappers - --------------------- - Date : December 2016 - Copyright : (C) 2016 by Arnaud Morvan - Email : arnaud dot morvan at camptocamp 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. * -* * -*************************************************************************** -""" - - -from qgis.core import (QgsProcessingParameterNumber, - QgsProcessingParameterFile, - QgsProcessingParameterField, - QgsProcessingParameterExpression, - QgsProcessingOutputString, - QgsProcessingParameterString) - -from qgis.PyQt.QtWidgets import QComboBox - -from processing.gui.wrappers import ( - WidgetWrapper, - DIALOG_MODELER, -) -from processing.tools.postgis import GeoDB - - -class SchemaWidgetWrapper(WidgetWrapper): - """ - WidgetWrapper for ParameterString that create and manage a combobox widget - with existing schemas from a parent connection parameter. - """ - - def createWidget(self, connection_param=None): - self._connection_param = connection_param - self._connection = None - self._database = None - - self._combo = QComboBox() - self._combo.setEditable(True) - self.refreshItems() - self._combo.currentIndexChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) - self._combo.lineEdit().editingFinished.connect(lambda: self.widgetValueHasChanged.emit(self)) - - return self._combo - - def postInitialize(self, wrappers): - for wrapper in wrappers: - if wrapper.parameterDefinition().name() == self._connection_param: - self.connection_wrapper = wrapper - self.setConnection(wrapper.parameterValue()) - wrapper.widgetValueHasChanged.connect(self.connectionChanged) - break - - def connectionChanged(self, wrapper): - connection = wrapper.parameterValue() - if connection == self._connection: - return - self.setConnection(connection) - - def setConnection(self, connection): - self._connection = connection - # when there is NO connection (yet), this gets called with a ''-connection - if isinstance(connection, str) and connection != '': - self._database = GeoDB.from_name(connection) - else: - self._database = None - self.refreshItems() - self.widgetValueHasChanged.emit(self) - - def refreshItems(self): - value = self.comboValue(combobox=self._combo) - - self._combo.clear() - - if self._database is not None: - for schema in self._database.list_schemas(): - self._combo.addItem(schema[1], schema[1]) - - if self.dialogType == DIALOG_MODELER: - strings = self.dialog.getAvailableValuesOfType( - [QgsProcessingParameterString, QgsProcessingParameterNumber, QgsProcessingParameterFile, - QgsProcessingParameterField, QgsProcessingParameterExpression], QgsProcessingOutputString) - for text, data in [(self.dialog.resolveValueDescription(s), s) for s in strings]: - self._combo.addItem(text, data) - - self.setComboValue(value, self._combo) - - def setValue(self, value): - self.setComboValue(value, self._combo) - self.widgetValueHasChanged.emit(self) - - def value(self): - return self.comboValue(combobox=self._combo) - - def database(self): - return self._database - - -class TableWidgetWrapper(WidgetWrapper): - """ - WidgetWrapper for ParameterString that create and manage a combobox widget - with existing tables from a parent schema parameter. - """ - - def createWidget(self, schema_param=None): - self._schema_param = schema_param - self._database = None - self._schema = None - - self._combo = QComboBox() - self._combo.setEditable(True) - self.refreshItems() - self._combo.currentIndexChanged.connect(lambda: self.widgetValueHasChanged.emit(self)) - self._combo.lineEdit().editingFinished.connect(lambda: self.widgetValueHasChanged.emit(self)) - - return self._combo - - def postInitialize(self, wrappers): - for wrapper in wrappers: - if wrapper.parameterDefinition().name() == self._schema_param: - self.schema_wrapper = wrapper - self.setSchema(wrapper.database(), wrapper.parameterValue()) - wrapper.widgetValueHasChanged.connect(self.schemaChanged) - break - - def schemaChanged(self, wrapper): - database = wrapper.database() - schema = wrapper.parameterValue() - if database == self._database and schema == self._schema: - return - self.setSchema(database, schema) - - def setSchema(self, database, schema): - self._database = database - self._schema = schema - self.refreshItems() - self.widgetValueHasChanged.emit(self) - - def refreshItems(self): - value = self.comboValue(combobox=self._combo) - - self._combo.clear() - - if (self._database is not None and isinstance(self._schema, str)): - for table in self._database.list_geotables(self._schema): - self._combo.addItem(table[0], table[0]) - - if self.dialogType == DIALOG_MODELER: - strings = self.dialog.getAvailableValuesOfType( - [QgsProcessingParameterString, QgsProcessingParameterNumber, QgsProcessingParameterFile, - QgsProcessingParameterField, QgsProcessingParameterExpression], QgsProcessingOutputString) - for text, data in [(self.dialog.resolveValueDescription(s), s) for s in strings]: - self._combo.addItem(text, data) - - self.setComboValue(value, self._combo) - - def setValue(self, value): - self.setComboValue(value, self._combo) - self.widgetValueHasChanged.emit(self) - - def value(self): - return self.comboValue(combobox=self._combo) diff --git a/python/plugins/processing/images/delete.svg b/python/plugins/processing/images/delete.svg deleted file mode 100644 index 6c1a09a1ca6e..000000000000 --- a/python/plugins/processing/images/delete.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/plugins/processing/images/edit.svg b/python/plugins/processing/images/edit.svg deleted file mode 100644 index cc9af01b02bc..000000000000 --- a/python/plugins/processing/images/edit.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/plugins/processing/images/input.svg b/python/plugins/processing/images/input.svg deleted file mode 100644 index 42fec1775544..000000000000 --- a/python/plugins/processing/images/input.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/plugins/processing/images/iterate.png b/python/plugins/processing/images/iterate.png deleted file mode 100644 index 384147dcab29..000000000000 Binary files a/python/plugins/processing/images/iterate.png and /dev/null differ diff --git a/python/plugins/processing/images/minus.svg b/python/plugins/processing/images/minus.svg deleted file mode 100644 index 1cc42c97ce5e..000000000000 --- a/python/plugins/processing/images/minus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/plugins/processing/images/namespace.png b/python/plugins/processing/images/namespace.png deleted file mode 100644 index 4a9423807397..000000000000 Binary files a/python/plugins/processing/images/namespace.png and /dev/null differ diff --git a/python/plugins/processing/images/networkanalysis.svg b/python/plugins/processing/images/networkanalysis.svg deleted file mode 100644 index 1ecbadb6fa96..000000000000 --- a/python/plugins/processing/images/networkanalysis.svg +++ /dev/null @@ -1,798 +0,0 @@ - - - - - Fast road - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - Fast road - 2012-08-13 - - - Robert Szczepanek - - - - - Robert Szczepanek - - - - - vector - network - optimization - - - GIS icons 0.2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/python/plugins/processing/images/output.svg b/python/plugins/processing/images/output.svg deleted file mode 100644 index b11d50657b8b..000000000000 --- a/python/plugins/processing/images/output.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/plugins/processing/images/plus.svg b/python/plugins/processing/images/plus.svg deleted file mode 100644 index eeb05d1a95d8..000000000000 --- a/python/plugins/processing/images/plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/python/plugins/processing/images/postgis.png b/python/plugins/processing/images/postgis.png deleted file mode 100644 index 1ec9992e50cb..000000000000 Binary files a/python/plugins/processing/images/postgis.png and /dev/null differ diff --git a/python/plugins/processing/images/zonalstats.png b/python/plugins/processing/images/zonalstats.png deleted file mode 100644 index 1f812a74b8d9..000000000000 Binary files a/python/plugins/processing/images/zonalstats.png and /dev/null differ diff --git a/python/plugins/processing/modeler/ModelerDialog.py b/python/plugins/processing/modeler/ModelerDialog.py index 3f787e85a968..36d4ba38b2f2 100644 --- a/python/plugins/processing/modeler/ModelerDialog.py +++ b/python/plugins/processing/modeler/ModelerDialog.py @@ -120,12 +120,17 @@ def runModel(self): duration=5) return + def on_finished(successful, results): + self.setLastRunChildAlgorithmResults(dlg.results().get('CHILD_RESULTS', {})) + self.setLastRunChildAlgorithmInputs(dlg.results().get('CHILD_INPUTS', {})) + dlg = AlgorithmDialog(self.model().create(), parent=self) dlg.setParameters(self.model().designerParameterValues()) + dlg.algorithmFinished.connect(on_finished) dlg.exec_() if dlg.wasExecuted(): - self.model().setDesignerParameterValues(dlg.getParameterValues()) + self.model().setDesignerParameterValues(dlg.createProcessingParameters()) def saveInProject(self): if not self.validateSave(): @@ -200,7 +205,7 @@ def repaintModel(self, showControls=True): showComments = QgsSettings().value("/Processing/Modeler/ShowComments", True, bool) if not showComments: - self.scene.setFlag(QgsModelGraphicsScene.FlagHideComments) + scene.setFlag(QgsModelGraphicsScene.FlagHideComments) context = createContext() scene.createItems(self.model(), context) @@ -214,6 +219,7 @@ def create_widget_context(self): widget_context.setProject(QgsProject.instance()) if iface is not None: widget_context.setMapCanvas(iface.mapCanvas()) + widget_context.setActiveLayer(iface.activeLayer()) widget_context.setModel(self.model()) return widget_context diff --git a/python/plugins/processing/modeler/ModelerGraphicItem.py b/python/plugins/processing/modeler/ModelerGraphicItem.py index 9db0500875e1..86cc1c0634a6 100644 --- a/python/plugins/processing/modeler/ModelerGraphicItem.py +++ b/python/plugins/processing/modeler/ModelerGraphicItem.py @@ -55,23 +55,28 @@ def create_widget_context(self): widget_context.setProject(QgsProject.instance()) if iface is not None: widget_context.setMapCanvas(iface.mapCanvas()) + widget_context.setActiveLayer(iface.activeLayer()) + widget_context.setModel(self.model()) return widget_context def edit(self, edit_comment=False): existing_param = self.model().parameterDefinition(self.component().parameterName()) comment = self.component().comment().description() + comment_color = self.component().comment().color() new_param = None if ModelerParameterDefinitionDialog.use_legacy_dialog(param=existing_param): # boo, old api dlg = ModelerParameterDefinitionDialog(self.model(), param=existing_param) dlg.setComments(comment) + dlg.setCommentColor(comment_color) if edit_comment: dlg.switchToCommentTab() if dlg.exec_(): new_param = dlg.param comment = dlg.comments() + comment_color = dlg.commentColor() else: # yay, use new API! context = createContext() @@ -82,12 +87,14 @@ def edit(self, edit_comment=False): definition=existing_param, algorithm=self.model()) dlg.setComments(comment) + dlg.setCommentColor(comment_color) if edit_comment: dlg.switchToCommentTab() if dlg.exec_(): new_param = dlg.createParameter(existing_param.name()) comment = dlg.comments() + comment_color = dlg.commentColor() if new_param is not None: self.aboutToChange.emit(self.tr('Edit {}').format(new_param.description())) @@ -95,6 +102,7 @@ def edit(self, edit_comment=False): self.component().setParameterName(new_param.name()) self.component().setDescription(new_param.name()) self.component().comment().setDescription(comment) + self.component().comment().setColor(comment_color) self.model().addModelParameter(new_param, self.component()) self.setLabel(new_param.description()) self.requestModelRepaint.emit() @@ -123,6 +131,7 @@ def edit(self, edit_comment=False): dlg = ModelerParametersDialog(elemAlg, self.model(), self.component().childId(), self.component().configuration()) dlg.setComments(self.component().comment().description()) + dlg.setCommentColor(self.component().comment().color()) if edit_comment: dlg.switchToCommentTab() if dlg.exec_(): @@ -158,6 +167,7 @@ def edit(self, edit_comment=False): dlg = ModelerParameterDefinitionDialog(self.model(), param=self.model().parameterDefinition(param_name)) dlg.setComments(self.component().comment().description()) + dlg.setCommentColor(self.component().comment().color()) if edit_comment: dlg.switchToCommentTab() @@ -167,6 +177,7 @@ def edit(self, edit_comment=False): model_output.setDefaultValue(dlg.param.defaultValue()) model_output.setMandatory(not (dlg.param.flags() & QgsProcessingParameterDefinition.FlagOptional)) model_output.comment().setDescription(dlg.comments()) + model_output.comment().setColor(dlg.commentColor()) self.aboutToChange.emit(self.tr('Edit {}').format(model_output.description())) self.model().updateDestinationParameters() self.requestModelRepaint.emit() diff --git a/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py b/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py index 070562c64219..ea4bed28ca78 100755 --- a/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py +++ b/python/plugins/processing/modeler/ModelerParameterDefinitionDialog.py @@ -36,31 +36,16 @@ QMessageBox, QTabWidget, QWidget, - QTextEdit) + QTextEdit, + QHBoxLayout) +from qgis.PyQt.QtGui import QColor -from qgis.gui import QgsExpressionLineEdit, QgsProjectionSelectionWidget +from qgis.gui import (QgsProcessingLayerOutputDestinationWidget, + QgsColorButton) from qgis.core import (QgsApplication, QgsSettings, QgsProcessing, - QgsCoordinateReferenceSystem, QgsProcessingParameterDefinition, - QgsProcessingParameterCrs, - QgsProcessingParameterMapLayer, - QgsProcessingParameterExtent, - QgsProcessingParameterPoint, - QgsProcessingParameterMatrix, - QgsProcessingParameterMultipleLayers, - QgsProcessingParameterNumber, - QgsProcessingParameterDistance, - QgsProcessingParameterScale, - QgsProcessingParameterRange, - QgsProcessingParameterRasterLayer, - QgsProcessingParameterEnum, - QgsProcessingParameterExpression, - QgsProcessingParameterVectorLayer, - QgsProcessingParameterField, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterBand, QgsProcessingDestinationParameter, QgsProcessingParameterFeatureSink, QgsProcessingParameterFileDestination, @@ -68,9 +53,6 @@ QgsProcessingParameterRasterDestination, QgsProcessingParameterVectorDestination) -from processing.gui.DestinationSelectionPanel import DestinationSelectionPanel -from processing.gui.enummodelerwidget import EnumModelerWidget -from processing.gui.matrixmodelerwidget import MatrixModelerWidget from processing.core import parameters from processing.modeler.exceptions import UndefinedParameterException @@ -79,34 +61,7 @@ class ModelerParameterDefinitionDialog(QDialog): @staticmethod def use_legacy_dialog(param=None, paramType=None): - if paramType in (parameters.PARAMETER_TABLE_FIELD, - parameters.PARAMETER_BAND, - parameters.PARAMETER_VECTOR, - parameters.PARAMETER_TABLE, - parameters.PARAMETER_MULTIPLE, - parameters.PARAMETER_NUMBER, - parameters.PARAMETER_DISTANCE, - parameters.PARAMETER_SCALE, - parameters.PARAMETER_EXPRESSION, - parameters.PARAMETER_POINT, - parameters.PARAMETER_CRS, - parameters.PARAMETER_ENUM, - parameters.PARAMETER_MATRIX): - return True - elif isinstance(param, (QgsProcessingParameterField, - QgsProcessingParameterBand, - QgsProcessingParameterFeatureSource, - QgsProcessingParameterVectorLayer, - QgsProcessingParameterMultipleLayers, - QgsProcessingParameterNumber, - QgsProcessingParameterDistance, - QgsProcessingParameterScale, - QgsProcessingParameterExpression, - QgsProcessingParameterPoint, - QgsProcessingParameterCrs, - QgsProcessingParameterEnum, - QgsProcessingParameterMatrix, - QgsProcessingDestinationParameter)): + if isinstance(param, QgsProcessingDestinationParameter): return True # yay, use new API! @@ -152,203 +107,9 @@ def setupUi(self): if isinstance(self.param, QgsProcessingParameterDefinition): self.nameTextBox.setText(self.param.description()) - if self.paramType == parameters.PARAMETER_TABLE_FIELD or \ - isinstance(self.param, QgsProcessingParameterField): - self.verticalLayout.addWidget(QLabel(self.tr('Parent layer'))) - self.parentCombo = QComboBox() - idx = 0 - for param in list(self.alg.parameterComponents().values()): - definition = self.alg.parameterDefinition(param.parameterName()) - if isinstance(definition, (QgsProcessingParameterFeatureSource, QgsProcessingParameterVectorLayer)): - self.parentCombo.addItem(definition.description(), definition.name()) - if self.param is not None: - if self.param.parentLayerParameterName() == definition.name(): - self.parentCombo.setCurrentIndex(idx) - idx += 1 - self.verticalLayout.addWidget(self.parentCombo) - - # add the datatype selector - self.verticalLayout.addWidget(QLabel(self.tr('Allowed data type'))) - self.datatypeCombo = QComboBox() - self.datatypeCombo.addItem(self.tr('Any'), -1) - self.datatypeCombo.addItem(self.tr('Number'), 0) - self.datatypeCombo.addItem(self.tr('String'), 1) - self.datatypeCombo.addItem(self.tr('Date/time'), 2) - self.verticalLayout.addWidget(self.datatypeCombo) - - if self.param is not None and self.param.dataType() is not None: - # QComboBoxes indexes start at 0, - # self.param.datatype start with -1 that is why I need to do +1 - datatypeIndex = self.param.dataType() + 1 - self.datatypeCombo.setCurrentIndex(datatypeIndex) - - self.multipleCheck = QCheckBox() - self.multipleCheck.setText(self.tr('Accept multiple fields')) - self.multipleCheck.setChecked(False) - if self.param is not None: - self.multipleCheck.setChecked(self.param.allowMultiple()) - self.verticalLayout.addWidget(self.multipleCheck) - - self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) - self.defaultTextBox = QLineEdit() - self.defaultTextBox.setToolTip( - self.tr('Default field name, or ; separated list of field names for multiple field parameters')) - if self.param is not None: - default = self.param.defaultValue() - if default is not None: - self.defaultTextBox.setText(str(default)) - self.verticalLayout.addWidget(self.defaultTextBox) - - elif self.paramType == parameters.PARAMETER_BAND or \ - isinstance(self.param, QgsProcessingParameterBand): - self.verticalLayout.addWidget(QLabel(self.tr('Parent layer'))) - self.parentCombo = QComboBox() - idx = 0 - for param in list(self.alg.parameterComponents().values()): - definition = self.alg.parameterDefinition(param.parameterName()) - if isinstance(definition, (QgsProcessingParameterRasterLayer)): - self.parentCombo.addItem(definition.description(), definition.name()) - if self.param is not None: - if self.param.parentLayerParameterName() == definition.name(): - self.parentCombo.setCurrentIndex(idx) - idx += 1 - self.verticalLayout.addWidget(self.parentCombo) - elif (self.paramType in ( - parameters.PARAMETER_VECTOR, parameters.PARAMETER_TABLE) or - isinstance(self.param, (QgsProcessingParameterFeatureSource, QgsProcessingParameterVectorLayer))): - self.verticalLayout.addWidget(QLabel(self.tr('Geometry type'))) - self.shapetypeCombo = QComboBox() - self.shapetypeCombo.addItem(self.tr('Geometry Not Required'), QgsProcessing.TypeVector) - self.shapetypeCombo.addItem(self.tr('Point'), QgsProcessing.TypeVectorPoint) - self.shapetypeCombo.addItem(self.tr('Line'), QgsProcessing.TypeVectorLine) - self.shapetypeCombo.addItem(self.tr('Polygon'), QgsProcessing.TypeVectorPolygon) - self.shapetypeCombo.addItem(self.tr('Any Geometry Type'), QgsProcessing.TypeVectorAnyGeometry) - if self.param is not None: - self.shapetypeCombo.setCurrentIndex(self.shapetypeCombo.findData(self.param.dataTypes()[0])) - self.verticalLayout.addWidget(self.shapetypeCombo) - elif (self.paramType == parameters.PARAMETER_MULTIPLE - or isinstance(self.param, QgsProcessingParameterMultipleLayers)): - self.verticalLayout.addWidget(QLabel(self.tr('Data type'))) - self.datatypeCombo = QComboBox() - self.datatypeCombo.addItem(self.tr('Any Map Layer'), QgsProcessing.TypeMapLayer) - self.datatypeCombo.addItem(self.tr('Vector (No Geometry Required)'), QgsProcessing.TypeVector) - self.datatypeCombo.addItem(self.tr('Vector (Point)'), QgsProcessing.TypeVectorPoint) - self.datatypeCombo.addItem(self.tr('Vector (Line)'), QgsProcessing.TypeVectorLine) - self.datatypeCombo.addItem(self.tr('Vector (Polygon)'), QgsProcessing.TypeVectorPolygon) - self.datatypeCombo.addItem(self.tr('Vector (Any Geometry Type)'), QgsProcessing.TypeVectorAnyGeometry) - self.datatypeCombo.addItem(self.tr('Raster'), QgsProcessing.TypeRaster) - self.datatypeCombo.addItem(self.tr('File'), QgsProcessing.TypeFile) - if self.param is not None: - self.datatypeCombo.setCurrentIndex(self.datatypeCombo.findData(self.param.layerType())) - self.verticalLayout.addWidget(self.datatypeCombo) - elif (self.paramType in (parameters.PARAMETER_NUMBER, parameters.PARAMETER_DISTANCE, parameters.PARAMETER_SCALE) - or isinstance(self.param, (QgsProcessingParameterNumber, QgsProcessingParameterDistance, QgsProcessingParameterScale))): - - if (self.paramType == parameters.PARAMETER_DISTANCE - or isinstance(self.param, QgsProcessingParameterDistance)): - self.verticalLayout.addWidget(QLabel(self.tr('Linked input'))) - self.parentCombo = QComboBox() - self.parentCombo.addItem('', '') - idx = 1 - for param in list(self.alg.parameterComponents().values()): - definition = self.alg.parameterDefinition(param.parameterName()) - if isinstance(definition, (QgsProcessingParameterFeatureSource, - QgsProcessingParameterVectorLayer, - QgsProcessingParameterMapLayer, - QgsProcessingParameterCrs)): - self.parentCombo.addItem(definition.description(), definition.name()) - if self.param is not None: - if self.param.parentParameterName() == definition.name(): - self.parentCombo.setCurrentIndex(idx) - idx += 1 - self.verticalLayout.addWidget(self.parentCombo) - elif (self.paramType != parameters.PARAMETER_SCALE and not - isinstance(self.param, QgsProcessingParameterScale)): - self.verticalLayout.addWidget(QLabel(self.tr('Number type'))) - self.type_combo = QComboBox() - self.type_combo.addItem(self.tr('Float'), QgsProcessingParameterNumber.Double) - self.type_combo.addItem(self.tr('Integer'), QgsProcessingParameterNumber.Integer) - if self.param: - self.type_combo.setCurrentIndex(self.type_combo.findData(self.param.dataType())) - self.verticalLayout.addWidget(self.type_combo) - - if (self.paramType != parameters.PARAMETER_SCALE and not - isinstance(self.param, QgsProcessingParameterScale)): - self.verticalLayout.addWidget(QLabel(self.tr('Min value'))) - self.minTextBox = QLineEdit() - self.verticalLayout.addWidget(self.minTextBox) - self.verticalLayout.addWidget(QLabel(self.tr('Max value'))) - self.maxTextBox = QLineEdit() - self.verticalLayout.addWidget(self.maxTextBox) - if self.param is not None: - self.minTextBox.setText(str(self.param.minimum())) - self.maxTextBox.setText(str(self.param.maximum())) - self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) - self.defaultTextBox = QLineEdit() - self.defaultTextBox.setText(self.tr('0')) - if self.param is not None: - default = self.param.defaultValue() - if self.param.dataType() == QgsProcessingParameterNumber.Integer: - default = int(math.floor(float(default))) - if default: - self.defaultTextBox.setText(str(default)) - self.verticalLayout.addWidget(self.defaultTextBox) - elif (self.paramType == parameters.PARAMETER_EXPRESSION - or isinstance(self.param, QgsProcessingParameterExpression)): - self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) - self.defaultEdit = QgsExpressionLineEdit() - if self.param is not None: - self.defaultEdit.setExpression(self.param.defaultValue()) - self.verticalLayout.addWidget(self.defaultEdit) - - self.verticalLayout.addWidget(QLabel(self.tr('Parent layer'))) - self.parentCombo = QComboBox() - self.parentCombo.addItem(self.tr("None"), None) - idx = 1 - for param in list(self.alg.parameterComponents().values()): - definition = self.alg.parameterDefinition(param.parameterName()) - if isinstance(definition, (QgsProcessingParameterFeatureSource, QgsProcessingParameterVectorLayer)): - self.parentCombo.addItem(definition.description(), definition.name()) - if self.param is not None: - if self.param.parentLayerParameterName() == definition.name(): - self.parentCombo.setCurrentIndex(idx) - idx += 1 - self.verticalLayout.addWidget(self.parentCombo) - elif (self.paramType == parameters.PARAMETER_POINT - or isinstance(self.param, QgsProcessingParameterPoint)): - self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) - self.defaultTextBox = QLineEdit() - if self.param is not None: - self.defaultTextBox.setText(self.param.defaultValue()) - self.verticalLayout.addWidget(self.defaultTextBox) - elif (self.paramType == parameters.PARAMETER_CRS - or isinstance(self.param, QgsProcessingParameterCrs)): - self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) - self.selector = QgsProjectionSelectionWidget() - if self.param is not None: - self.selector.setCrs(QgsCoordinateReferenceSystem(self.param.defaultValue())) - else: - self.selector.setCrs(QgsCoordinateReferenceSystem('EPSG:4326')) - self.verticalLayout.addWidget(self.selector) - elif self.paramType == parameters.PARAMETER_ENUM or \ - isinstance(self.param, QgsProcessingParameterEnum): - self.widget = EnumModelerWidget(self) - if self.param is not None: - self.widget.setAllowMultiple(bool(self.param.allowMultiple())) - self.widget.setOptions(self.param.options()) - self.widget.setDefault(self.param.defaultValue()) - self.verticalLayout.addWidget(self.widget) - elif self.paramType == parameters.PARAMETER_MATRIX or \ - isinstance(self.param, QgsProcessingParameterMatrix): - self.widget = MatrixModelerWidget(self) - if self.param is not None: - self.widget.setValue(self.param.headers(), self.param.defaultValue()) - self.widget.setFixedRows(self.param.hasFixedNumberRows()) - self.verticalLayout.addWidget(self.widget) - - elif isinstance(self.param, QgsProcessingDestinationParameter): + if isinstance(self.param, QgsProcessingDestinationParameter): self.verticalLayout.addWidget(QLabel(self.tr('Default value'))) - self.defaultWidget = DestinationSelectionPanel(self.param, self.alg, default_selection=True) + self.defaultWidget = QgsProcessingLayerOutputDestinationWidget(self.param, defaultSelection=True) self.verticalLayout.addWidget(self.defaultWidget) self.verticalLayout.addSpacing(20) @@ -388,15 +149,26 @@ def setupUi(self): self.commentLayout = QVBoxLayout() self.commentEdit = QTextEdit() self.commentEdit.setAcceptRichText(False) - self.commentLayout.addWidget(self.commentEdit) + self.commentLayout.addWidget(self.commentEdit, 1) + + hl = QHBoxLayout() + hl.setContentsMargins(0, 0, 0, 0) + hl.addWidget(QLabel(self.tr('Color'))) + self.comment_color_button = QgsColorButton() + self.comment_color_button.setAllowOpacity(True) + self.comment_color_button.setWindowTitle(self.tr('Comment Color')) + self.comment_color_button.setShowNull(True, self.tr('Default')) + hl.addWidget(self.comment_color_button) + self.commentLayout.addLayout(hl) + w2 = QWidget() w2.setLayout(self.commentLayout) self.tab.addTab(w2, self.tr('Comments')) self.buttonBox = QDialogButtonBox(self) self.buttonBox.setOrientation(Qt.Horizontal) - self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel - | QDialogButtonBox.Ok) + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | + QDialogButtonBox.Ok) self.buttonBox.setObjectName('buttonBox') self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) @@ -411,6 +183,15 @@ def setComments(self, text): def comments(self): return self.commentEdit.toPlainText() + def setCommentColor(self, color): + if color.isValid(): + self.comment_color_button.setColor(color) + else: + self.comment_color_button.setToNull() + + def commentColor(self): + return self.comment_color_button.color() if not self.comment_color_button.isNull() else QColor() + def accept(self): description = self.nameTextBox.text() if description.strip() == '': @@ -428,147 +209,36 @@ def accept(self): i += 1 else: name = self.param.name() - if (self.paramType == parameters.PARAMETER_TABLE_FIELD - or isinstance(self.param, QgsProcessingParameterField)): - if self.parentCombo.currentIndex() < 0: - QMessageBox.warning(self, self.tr('Unable to define parameter'), - self.tr('Wrong or missing parameter values')) - return - parent = self.parentCombo.currentData() - datatype = self.datatypeCombo.currentData() - default = self.defaultTextBox.text() - if not default: - default = None - self.param = QgsProcessingParameterField(name, description, defaultValue=default, - parentLayerParameterName=parent, type=datatype, - allowMultiple=self.multipleCheck.isChecked()) - elif (self.paramType == parameters.PARAMETER_BAND - or isinstance(self.param, QgsProcessingParameterBand)): - if self.parentCombo.currentIndex() < 0: - QMessageBox.warning(self, self.tr('Unable to define parameter'), - self.tr('Wrong or missing parameter values')) - return - parent = self.parentCombo.currentData() - self.param = QgsProcessingParameterBand(name, description, None, parent) - elif (self.paramType == parameters.PARAMETER_MAP_LAYER - or isinstance(self.param, QgsProcessingParameterMapLayer)): - self.param = QgsProcessingParameterMapLayer( - name, description) - elif (self.paramType == parameters.PARAMETER_RASTER - or isinstance(self.param, QgsProcessingParameterRasterLayer)): - self.param = QgsProcessingParameterRasterLayer( - name, description) - elif (self.paramType == parameters.PARAMETER_TABLE - or isinstance(self.param, QgsProcessingParameterVectorLayer)): - self.param = QgsProcessingParameterVectorLayer( - name, description, - [self.shapetypeCombo.currentData()]) - elif (self.paramType == parameters.PARAMETER_VECTOR - or isinstance(self.param, QgsProcessingParameterFeatureSource)): - self.param = QgsProcessingParameterFeatureSource( - name, description, - [self.shapetypeCombo.currentData()]) - elif (self.paramType == parameters.PARAMETER_MULTIPLE - or isinstance(self.param, QgsProcessingParameterMultipleLayers)): - self.param = QgsProcessingParameterMultipleLayers( - name, description, - self.datatypeCombo.currentData()) - elif (self.paramType == parameters.PARAMETER_DISTANCE - or isinstance(self.param, QgsProcessingParameterDistance)): - self.param = QgsProcessingParameterDistance(name, description, - self.defaultTextBox.text()) - try: - vmin = self.minTextBox.text().strip() - if not vmin == '': - self.param.setMinimum(float(vmin)) - vmax = self.maxTextBox.text().strip() - if not vmax == '': - self.param.setMaximum(float(vmax)) - except: - QMessageBox.warning(self, self.tr('Unable to define parameter'), - self.tr('Wrong or missing parameter values')) - return - - if self.parentCombo.currentIndex() < 0: - QMessageBox.warning(self, self.tr('Unable to define parameter'), - self.tr('Wrong or missing parameter values')) - return - parent = self.parentCombo.currentData() - if parent: - self.param.setParentParameterName(parent) - elif (self.paramType == parameters.PARAMETER_SCALE - or isinstance(self.param, QgsProcessingParameterScale)): - self.param = QgsProcessingParameterScale(name, description, - self.defaultTextBox.text()) - elif (self.paramType == parameters.PARAMETER_NUMBER - or isinstance(self.param, QgsProcessingParameterNumber)): - - type = self.type_combo.currentData() - self.param = QgsProcessingParameterNumber(name, description, type, - self.defaultTextBox.text()) - try: - vmin = self.minTextBox.text().strip() - if not vmin == '': - self.param.setMinimum(float(vmin)) - vmax = self.maxTextBox.text().strip() - if not vmax == '': - self.param.setMaximum(float(vmax)) - except: - QMessageBox.warning(self, self.tr('Unable to define parameter'), - self.tr('Wrong or missing parameter values')) - return - elif (self.paramType == parameters.PARAMETER_EXPRESSION - or isinstance(self.param, QgsProcessingParameterExpression)): - parent = self.parentCombo.currentData() - self.param = QgsProcessingParameterExpression(name, description, - str(self.defaultEdit.expression()), - parent) - elif (self.paramType == parameters.PARAMETER_EXTENT - or isinstance(self.param, QgsProcessingParameterExtent)): - self.param = QgsProcessingParameterExtent(name, description) - elif (self.paramType == parameters.PARAMETER_POINT - or isinstance(self.param, QgsProcessingParameterPoint)): - self.param = QgsProcessingParameterPoint(name, description, - str(self.defaultTextBox.text())) - elif (self.paramType == parameters.PARAMETER_CRS - or isinstance(self.param, QgsProcessingParameterCrs)): - self.param = QgsProcessingParameterCrs(name, description, self.selector.crs().authid()) - elif (self.paramType == parameters.PARAMETER_ENUM - or isinstance(self.param, QgsProcessingParameterEnum)): - self.param = QgsProcessingParameterEnum(name, description, self.widget.options(), self.widget.allowMultiple(), self.widget.defaultOptions()) - elif (self.paramType == parameters.PARAMETER_MATRIX - or isinstance(self.param, QgsProcessingParameterMatrix)): - self.param = QgsProcessingParameterMatrix(name, description, hasFixedNumberRows=self.widget.fixedRows(), headers=self.widget.headers(), defaultValue=self.widget.value()) # Destination parameter - elif (isinstance(self.param, QgsProcessingParameterFeatureSink)): + if (isinstance(self.param, QgsProcessingParameterFeatureSink)): self.param = QgsProcessingParameterFeatureSink( name=name, description=self.param.description(), type=self.param.dataType(), - defaultValue=self.defaultWidget.getValue()) + defaultValue=self.defaultWidget.value()) elif (isinstance(self.param, QgsProcessingParameterFileDestination)): self.param = QgsProcessingParameterFileDestination( name=name, description=self.param.description(), fileFilter=self.param.fileFilter(), - defaultValue=self.defaultWidget.getValue()) + defaultValue=self.defaultWidget.value()) elif (isinstance(self.param, QgsProcessingParameterFolderDestination)): self.param = QgsProcessingParameterFolderDestination( name=name, description=self.param.description(), - defaultValue=self.defaultWidget.getValue()) + defaultValue=self.defaultWidget.value()) elif (isinstance(self.param, QgsProcessingParameterRasterDestination)): self.param = QgsProcessingParameterRasterDestination( name=name, description=self.param.description(), - defaultValue=self.defaultWidget.getValue()) + defaultValue=self.defaultWidget.value()) elif (isinstance(self.param, QgsProcessingParameterVectorDestination)): self.param = QgsProcessingParameterVectorDestination( name=name, description=self.param.description(), type=self.param.dataType(), - defaultValue=self.defaultWidget.getValue()) + defaultValue=self.defaultWidget.value()) else: if self.paramType: diff --git a/python/plugins/processing/modeler/ModelerParametersDialog.py b/python/plugins/processing/modeler/ModelerParametersDialog.py index 0bdc8755b054..ad2ae70f7080 100644 --- a/python/plugins/processing/modeler/ModelerParametersDialog.py +++ b/python/plugins/processing/modeler/ModelerParametersDialog.py @@ -23,31 +23,19 @@ import webbrowser -from qgis.PyQt.QtCore import (Qt, - QUrl, - QMetaObject, - QByteArray) +from qgis.PyQt.QtCore import Qt from qgis.PyQt.QtWidgets import (QDialog, QDialogButtonBox, QLabel, QLineEdit, QFrame, QPushButton, QSizePolicy, QVBoxLayout, QHBoxLayout, QWidget, QTabWidget, QTextEdit) +from qgis.PyQt.QtGui import QColor from qgis.core import (Qgis, QgsProject, QgsProcessingParameterDefinition, - QgsProcessingParameterPoint, - QgsProcessingParameterExtent, - QgsProcessingModelAlgorithm, QgsProcessingModelOutput, QgsProcessingModelChildAlgorithm, QgsProcessingModelChildParameterSource, - QgsProcessingParameterFeatureSink, - QgsProcessingParameterMultipleLayers, - QgsProcessingParameterRasterDestination, - QgsProcessingParameterFileDestination, - QgsProcessingParameterFolderDestination, - QgsProcessingParameterVectorDestination, - QgsProcessingOutputDefinition, - QgsSettings) + QgsProcessingOutputDefinition) from qgis.gui import (QgsGui, QgsMessageBar, @@ -56,7 +44,10 @@ QgsHelp, QgsProcessingContextGenerator, QgsProcessingModelerParameterWidget, - QgsProcessingParameterWidgetContext) + QgsProcessingParameterWidgetContext, + QgsPanelWidget, + QgsPanelWidgetStack, + QgsColorButton) from qgis.utils import iface from processing.gui.wrappers import WidgetWrapperFactory @@ -69,15 +60,96 @@ class ModelerParametersDialog(QDialog): def __init__(self, alg, model, algName=None, configuration=None): - QDialog.__init__(self) + super().__init__() + self.setObjectName('ModelerParametersDialog') self.setModal(True) - self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm - self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm - self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time + if iface is not None: + self.setStyleSheet(iface.mainWindow().styleSheet()) + + # dammit this is SUCH as mess... stupid stable API + self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm + self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm + self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time self.configuration = configuration self.context = createContext() + self.setWindowTitle(self._alg.displayName()) + + self.widget = ModelerParametersWidget(alg, model, algName, configuration, context=self.context, dialog=self) + QgsGui.enableAutoGeometryRestore(self) + + self.buttonBox = QDialogButtonBox() + self.buttonBox.setOrientation(Qt.Horizontal) + self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Help) + + self.buttonBox.accepted.connect(self.okPressed) + self.buttonBox.rejected.connect(self.reject) + self.buttonBox.helpRequested.connect(self.openHelp) + + mainLayout = QVBoxLayout() + mainLayout.addWidget(self.widget, 1) + mainLayout.addWidget(self.buttonBox) + self.setLayout(mainLayout) + + def setComments(self, text): + self.widget.setComments(text) + + def comments(self): + return self.widget.comments() + + def setCommentColor(self, color): + self.widget.setCommentColor(color) + + def commentColor(self): + return self.widget.commentColor() + + def switchToCommentTab(self): + self.widget.switchToCommentTab() + + def getAvailableDependencies(self): + return self.widget.getAvailableDependencies() + + def getDependenciesPanel(self): + return self.widget.getDependenciesPanel() + + def getAvailableValuesOfType(self, paramType, outTypes=[], dataTypes=[]): + return self.widget.getAvailableValuesOfType(paramType, outTypes, dataTypes) + + def resolveValueDescription(self, value): + return self.widget.resolveValueDescription(value) + + def setPreviousValues(self): + self.widget.setPreviousValues() + + def createAlgorithm(self): + return self.widget.createAlgorithm() + + def okPressed(self): + if self.createAlgorithm() is not None: + self.accept() + + def openHelp(self): + algHelp = self.widget.algorithm().helpUrl() + if not algHelp: + algHelp = QgsHelp.helpUrl("processing_algs/{}/{}.html#{}".format( + self.widget.algorithm().provider().helpId(), self.algorithm().groupId(), + "{}{}".format(self.algorithm().provider().helpId(), self.algorithm().name()))).toString() + + if algHelp not in [None, ""]: + webbrowser.open(algHelp) + + +class ModelerParametersPanelWidget(QgsPanelWidget): + + def __init__(self, alg, model, algName=None, configuration=None, dialog=None, context=None): + super().__init__() + self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm + self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm + self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time + self.configuration = configuration + self.context = context + self.dialog = dialog self.widget_labels = {} class ContextGenerator(QgsProcessingContextGenerator): @@ -94,48 +166,25 @@ def processingContext(self): self.setupUi() self.params = None - settings = QgsSettings() - self.restoreGeometry(settings.value("/Processing/modelParametersDialogGeometry", QByteArray())) - - def closeEvent(self, event): - settings = QgsSettings() - settings.setValue("/Processing/modelParametersDialogGeometry", self.saveGeometry()) - super(ModelerParametersDialog, self).closeEvent(event) - - def switchToCommentTab(self): - self.tab.setCurrentIndex(1) - self.commentEdit.setFocus() - self.commentEdit.selectAll() + def algorithm(self): + return self._alg def setupUi(self): - self.checkBoxes = {} self.showAdvanced = False self.wrappers = {} - self.valueItems = {} - self.dependentItems = {} self.algorithmItem = None - self.resize(650, 450) - self.mainLayout = QVBoxLayout() - self.tab = QTabWidget() - self.mainLayout.addWidget(self.tab) + self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.buttonBox = QDialogButtonBox() - self.buttonBox.setOrientation(Qt.Horizontal) - self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok | QDialogButtonBox.Help) - self.setSizePolicy(QSizePolicy.Expanding, - QSizePolicy.Expanding) self.verticalLayout = QVBoxLayout() - self.verticalLayout.setSpacing(5) self.bar = QgsMessageBar() self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) self.verticalLayout.addWidget(self.bar) hLayout = QHBoxLayout() - hLayout.setSpacing(5) - hLayout.setMargin(0) + hLayout.setContentsMargins(0, 0, 0, 0) descriptionLabel = QLabel(self.tr("Description")) self.descriptionBox = QLineEdit() self.descriptionBox.setText(self._alg.displayName()) @@ -151,6 +200,8 @@ def setupUi(self): widget_context.setProject(QgsProject.instance()) if iface is not None: widget_context.setMapCanvas(iface.mapCanvas()) + widget_context.setActiveLayer(iface.activeLayer()) + widget_context.setModel(self.model) widget_context.setModelChildAlgorithmId(self.childId) @@ -177,7 +228,7 @@ def setupUi(self): if param.isDestination() or param.flags() & QgsProcessingParameterDefinition.FlagHidden: continue - wrapper = WidgetWrapperFactory.create_wrapper(param, self) + wrapper = WidgetWrapperFactory.create_wrapper(param, self.dialog) self.wrappers[param.name()] = wrapper wrapper.setWidgetContext(widget_context) @@ -187,8 +238,6 @@ def setupUi(self): else: widget = wrapper.widget if widget is not None: - self.valueItems[param.name()] = widget - if issubclass(wrapper.__class__, QgsProcessingModelerParameterWidget): label = wrapper.createLabel() else: @@ -204,18 +253,29 @@ def setupUi(self): self.verticalLayout.addWidget(label) self.verticalLayout.addWidget(widget) - for dest in self._alg.destinationParameterDefinitions(): - if dest.flags() & QgsProcessingParameterDefinition.FlagHidden: + for output in self._alg.destinationParameterDefinitions(): + if output.flags() & QgsProcessingParameterDefinition.FlagHidden: continue - if isinstance(dest, (QgsProcessingParameterRasterDestination, QgsProcessingParameterVectorDestination, - QgsProcessingParameterFeatureSink, QgsProcessingParameterFileDestination, QgsProcessingParameterFolderDestination)): - label = QLabel(dest.description()) - item = QgsFilterLineEdit() - if hasattr(item, 'setPlaceholderText'): - item.setPlaceholderText(self.tr('[Enter name if this is a final result]')) + + widget = QgsGui.processingGuiRegistry().createModelerParameterWidget(self.model, + self.childId, + output, + self.context) + widget.setDialog(self.dialog) + widget.setWidgetContext(widget_context) + widget.registerProcessingContextGenerator(self.context_generator) + + self.wrappers[output.name()] = widget + + item = QgsFilterLineEdit() + if hasattr(item, 'setPlaceholderText'): + item.setPlaceholderText(self.tr('[Enter name if this is a final result]')) + + label = widget.createLabel() + if label is not None: self.verticalLayout.addWidget(label) - self.verticalLayout.addWidget(item) - self.valueItems[dest.name()] = item + + self.verticalLayout.addWidget(widget) label = QLabel(' ') self.verticalLayout.addWidget(label) @@ -226,7 +286,6 @@ def setupUi(self): self.verticalLayout.addStretch(1000) self.setPreviousValues() - self.setWindowTitle(self._alg.displayName()) self.verticalLayout2 = QVBoxLayout() self.verticalLayout2.setSpacing(2) self.verticalLayout2.setMargin(0) @@ -236,36 +295,16 @@ def setupUi(self): self.scrollArea = QgsScrollArea() self.scrollArea.setWidget(self.paramPanel) self.scrollArea.setWidgetResizable(True) + self.scrollArea.setFrameStyle(QFrame.NoFrame) self.verticalLayout2.addWidget(self.scrollArea) - self.buttonBox.accepted.connect(self.okPressed) - self.buttonBox.rejected.connect(self.cancelPressed) - self.buttonBox.helpRequested.connect(self.openHelp) w = QWidget() w.setLayout(self.verticalLayout2) - self.tab.addTab(w, self.tr('Properties')) - - self.commentLayout = QVBoxLayout() - self.commentEdit = QTextEdit() - self.commentEdit.setAcceptRichText(False) - self.commentLayout.addWidget(self.commentEdit) - w2 = QWidget() - w2.setLayout(self.commentLayout) - self.tab.addTab(w2, self.tr('Comments')) - - self.mainLayout.addWidget(self.buttonBox) + self.mainLayout.addWidget(w) self.setLayout(self.mainLayout) - QMetaObject.connectSlotsByName(self) - - def setComments(self, text): - self.commentEdit.setPlainText(text) - - def comments(self): - return self.commentEdit.toPlainText() - - def getAvailableDependencies(self): # spellok + def getAvailableDependencies(self): if self.childId is None: dependent = [] else: @@ -278,7 +317,7 @@ def getAvailableDependencies(self): # spellok return opts def getDependenciesPanel(self): - return MultipleInputPanel([alg.description() for alg in self.getAvailableDependencies()]) # spellok + return MultipleInputPanel([alg.description() for alg in self.getAvailableDependencies()]) def showAdvancedParametersClicked(self): self.showAdvanced = not self.showAdvanced @@ -362,12 +401,37 @@ def setPreviousValues(self): value = value.staticValue() wrapper.setValue(value) - for name, out in alg.modelOutputs().items(): - if out.childOutputName() in self.valueItems: - self.valueItems[out.childOutputName()].setText(out.name()) + for output in self.algorithm().destinationParameterDefinitions(): + if output.flags() & QgsProcessingParameterDefinition.FlagHidden: + continue + + model_output_name = None + for name, out in alg.modelOutputs().items(): + if out.childId() == self.childId and out.childOutputName() == output.name(): + # this destination parameter is linked to a model output + model_output_name = out.name() + break + + value = None + if model_output_name is None and output.name() in alg.parameterSources(): + value = alg.parameterSources()[output.name()] + if isinstance(value, list) and len(value) == 1: + value = value[0] + elif isinstance(value, list) and len(value) == 0: + value = None + + wrapper = self.wrappers[output.name()] + + if model_output_name is not None: + wrapper.setToModelOutput(model_output_name) + elif value is not None or output.defaultValue() is not None: + if value is None: + value = QgsProcessingModelChildParameterSource.fromStaticValue(output.defaultValue()) + + wrapper.setWidgetValue(value) selected = [] - dependencies = self.getAvailableDependencies() # spellok + dependencies = self.getAvailableDependencies() for idx, dependency in enumerate(dependencies): if dependency.childId() in alg.dependencies(): selected.append(idx) @@ -407,9 +471,9 @@ def createAlgorithm(self): [isinstance(subval, QgsProcessingModelChildParameterSource) for subval in val])): val = [QgsProcessingModelChildParameterSource.fromStaticValue(val)] for subval in val: - if (isinstance(subval, QgsProcessingModelChildParameterSource) - and subval.source() == QgsProcessingModelChildParameterSource.StaticValue - and not param.checkValueIsAcceptable(subval.staticValue())) \ + if (isinstance(subval, QgsProcessingModelChildParameterSource) and + subval.source() == QgsProcessingModelChildParameterSource.StaticValue and + not param.checkValueIsAcceptable(subval.staticValue())) \ or (subval is None and not param.flags() & QgsProcessingParameterDefinition.FlagOptional): self.bar.pushMessage(self.tr("Error"), self.tr("Wrong or missing value for parameter '{}'").format( param.description()), @@ -418,52 +482,145 @@ def createAlgorithm(self): alg.addParameterSources(param.name(), val) outputs = {} - for dest in self._alg.destinationParameterDefinitions(): - if not dest.flags() & QgsProcessingParameterDefinition.FlagHidden: - name = self.valueItems[dest.name()].text() - if name.strip() != '': - output = QgsProcessingModelOutput(name, name) - output.setChildId(alg.childId()) - output.setChildOutputName(dest.name()) - outputs[name] = output - - if dest.flags() & QgsProcessingParameterDefinition.FlagIsModelOutput: - if dest.name() not in outputs: - output = QgsProcessingModelOutput(dest.name(), dest.name()) - output.setChildId(alg.childId()) - output.setChildOutputName(dest.name()) - outputs[dest.name()] = output + for output in self._alg.destinationParameterDefinitions(): + if not output.flags() & QgsProcessingParameterDefinition.FlagHidden: + wrapper = self.wrappers[output.name()] + + if wrapper.isModelOutput(): + name = wrapper.modelOutputName() + if name: + model_output = QgsProcessingModelOutput(name, name) + model_output.setChildId(alg.childId()) + model_output.setChildOutputName(output.name()) + outputs[name] = model_output + else: + val = wrapper.value() + + if isinstance(val, QgsProcessingModelChildParameterSource): + val = [val] + + alg.addParameterSources(output.name(), val) + + if output.flags() & QgsProcessingParameterDefinition.FlagIsModelOutput: + if output.name() not in outputs: + model_output = QgsProcessingModelOutput(output.name(), output.name()) + model_output.setChildId(alg.childId()) + model_output.setChildOutputName(output.name()) + outputs[output.name()] = model_output alg.setModelOutputs(outputs) selectedOptions = self.dependenciesPanel.selectedoptions - availableDependencies = self.getAvailableDependencies() # spellok + availableDependencies = self.getAvailableDependencies() dep_ids = [] for selected in selectedOptions: - dep_ids.append(availableDependencies[selected].childId()) # spellok + dep_ids.append(availableDependencies[selected].childId()) alg.setDependencies(dep_ids) - #try: - # self._alg.processBeforeAddingToModeler(alg, self.model) - #except: - # pass - - alg.comment().setDescription(self.comments()) return alg - def okPressed(self): - alg = self.createAlgorithm() - if alg is not None: - self.accept() - def cancelPressed(self): - self.reject() +class ModelerParametersWidget(QWidget): - def openHelp(self): - algHelp = self._alg.helpUrl() - if not algHelp: - algHelp = QgsHelp.helpUrl("processing_algs/{}/{}.html#{}".format( - self._alg.provider().helpId(), self._alg.groupId(), "{}{}".format(self._alg.provider().helpId(), self._alg.name()))).toString() + def __init__(self, alg, model, algName=None, configuration=None, dialog=None, context=None): + super().__init__() + self._alg = alg # The algorithm to define in this dialog. It is an instance of QgsProcessingAlgorithm + self.model = model # The model this algorithm is going to be added to. It is an instance of QgsProcessingModelAlgorithm + self.childId = algName # The name of the algorithm in the model, in case we are editing it and not defining it for the first time + self.configuration = configuration + self.context = context + self.dialog = dialog - if algHelp not in [None, ""]: - webbrowser.open(algHelp) + self.widget = ModelerParametersPanelWidget(alg, model, algName, configuration, dialog, context) + + class ContextGenerator(QgsProcessingContextGenerator): + + def __init__(self, context): + super().__init__() + self.processing_context = context + + def processingContext(self): + return self.processing_context + + self.context_generator = ContextGenerator(self.context) + + self.setupUi() + self.params = None + + def algorithm(self): + return self._alg + + def switchToCommentTab(self): + self.tab.setCurrentIndex(1) + self.commentEdit.setFocus() + self.commentEdit.selectAll() + + def setupUi(self): + self.mainLayout = QVBoxLayout() + self.mainLayout.setContentsMargins(0, 0, 0, 0) + self.tab = QTabWidget() + self.mainLayout.addWidget(self.tab) + + self.param_widget = QgsPanelWidgetStack() + self.widget.setDockMode(True) + self.param_widget.setMainPanel(self.widget) + + self.tab.addTab(self.param_widget, self.tr('Properties')) + + self.commentLayout = QVBoxLayout() + self.commentEdit = QTextEdit() + self.commentEdit.setAcceptRichText(False) + self.commentLayout.addWidget(self.commentEdit, 1) + + hl = QHBoxLayout() + hl.setContentsMargins(0, 0, 0, 0) + hl.addWidget(QLabel(self.tr('Color'))) + self.comment_color_button = QgsColorButton() + self.comment_color_button.setAllowOpacity(True) + self.comment_color_button.setWindowTitle(self.tr('Comment Color')) + self.comment_color_button.setShowNull(True, self.tr('Default')) + hl.addWidget(self.comment_color_button) + self.commentLayout.addLayout(hl) + + w2 = QWidget() + w2.setLayout(self.commentLayout) + self.tab.addTab(w2, self.tr('Comments')) + + self.setLayout(self.mainLayout) + + def setComments(self, text): + self.commentEdit.setPlainText(text) + + def comments(self): + return self.commentEdit.toPlainText() + + def setCommentColor(self, color): + if color.isValid(): + self.comment_color_button.setColor(color) + else: + self.comment_color_button.setToNull() + + def commentColor(self): + return self.comment_color_button.color() if not self.comment_color_button.isNull() else QColor() + + def getAvailableDependencies(self): + return self.widget.getAvailableDependencies() + + def getDependenciesPanel(self): + return self.widget.getDependenciesPanel() + + def getAvailableValuesOfType(self, paramType, outTypes=[], dataTypes=[]): + return self.widget.getAvailableValuesOfType(paramType, outTypes, dataTypes) + + def resolveValueDescription(self, value): + return self.widget.resolveValueDescription(value) + + def setPreviousValues(self): + self.widget.setPreviousValues() + + def createAlgorithm(self): + alg = self.widget.createAlgorithm() + if alg: + alg.comment().setDescription(self.comments()) + alg.comment().setColor(self.commentColor()) + return alg diff --git a/python/plugins/processing/preconfigured/PreconfiguredAlgorithmDialog.py b/python/plugins/processing/preconfigured/PreconfiguredAlgorithmDialog.py index d7c9ed82a4ab..4780a621ff48 100644 --- a/python/plugins/processing/preconfigured/PreconfiguredAlgorithmDialog.py +++ b/python/plugins/processing/preconfigured/PreconfiguredAlgorithmDialog.py @@ -50,7 +50,7 @@ def __init__(self, alg, toolbox): def accept(self): context = dataobjects.createContext() try: - parameters = self.getParameterValues() + parameters = self.createProcessingParameters() self.setOutputValues() ok, msg = self.algorithm().checkParameterValues(parameters, context) if not ok: diff --git a/python/plugins/processing/tests/GdalAlgorithmsGeneralTest.py b/python/plugins/processing/tests/GdalAlgorithmsGeneralTest.py index ca332880bede..38d7978d133a 100644 --- a/python/plugins/processing/tests/GdalAlgorithmsGeneralTest.py +++ b/python/plugins/processing/tests/GdalAlgorithmsGeneralTest.py @@ -36,6 +36,7 @@ QgsProject, QgsVectorLayer, QgsRectangle, + QgsProjUtils, QgsProcessingException, QgsProcessingFeatureSourceDefinition) @@ -318,15 +319,22 @@ def testCrsConversion(self): crs.createFromProj( '+proj=utm +zone=36 +south +a=600000 +b=70000 +towgs84=-143,-90,-294,0,0,0,0 +units=m +no_defs') self.assertTrue(crs.isValid()) - self.assertEqual(GdalUtils.gdal_crs_string(crs), - '+proj=utm +zone=36 +south +a=600000 +b=70000 +towgs84=-143,-90,-294,0,0,0,0 +units=m +no_defs') - # check that newlines are stripped - crs = QgsCoordinateReferenceSystem() - crs.createFromProj( - '+proj=utm +zone=36 +south\n +a=600000 +b=70000 \r\n +towgs84=-143,-90,-294,0,0,0,0 +units=m\n+no_defs') - self.assertTrue(crs.isValid()) - self.assertEqual(GdalUtils.gdal_crs_string(crs), - '+proj=utm +zone=36 +south +a=600000 +b=70000 +towgs84=-143,-90,-294,0,0,0,0 +units=m +no_defs') + + if QgsProjUtils.projVersionMajor() >= 6: + # proj 6, WKT should be used + self.assertEqual(GdalUtils.gdal_crs_string(crs)[:40], 'BOUNDCRS[SOURCECRS[PROJCRS["unknown",BAS') + + self.assertEqual(GdalUtils.gdal_crs_string(QgsCoordinateReferenceSystem('ESRI:102003')), 'ESRI:102003') + else: + self.assertEqual(GdalUtils.gdal_crs_string(crs), + '+proj=utm +zone=36 +south +a=600000 +b=70000 +towgs84=-143,-90,-294,0,0,0,0 +units=m +no_defs') + # check that newlines are stripped + crs = QgsCoordinateReferenceSystem() + crs.createFromProj( + '+proj=utm +zone=36 +south\n +a=600000 +b=70000 \r\n +towgs84=-143,-90,-294,0,0,0,0 +units=m\n+no_defs') + self.assertTrue(crs.isValid()) + self.assertEqual(GdalUtils.gdal_crs_string(crs), + '+proj=utm +zone=36 +south +a=600000 +b=70000 +towgs84=-143,-90,-294,0,0,0,0 +units=m +no_defs') if __name__ == '__main__': diff --git a/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py b/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py index ededbbbd4998..ab7cf7f0190a 100644 --- a/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py +++ b/python/plugins/processing/tests/GdalAlgorithmsRasterTest.py @@ -47,7 +47,7 @@ from processing.algs.gdal.gdal2tiles import gdal2tiles from processing.algs.gdal.gdalcalc import gdalcalc from processing.algs.gdal.gdaltindex import gdaltindex -from processing.algs.gdal.contour import contour +from processing.algs.gdal.contour import contour, contour_polygon from processing.algs.gdal.gdalinfo import gdalinfo from processing.algs.gdal.hillshade import hillshade from processing.algs.gdal.aspect import aspect @@ -404,6 +404,25 @@ def testClipRasterByMask(self): source + ' ' + outdir + '/check.jpg']) + def testContourPolygon(self): + context = QgsProcessingContext() + feedback = QgsProcessingFeedback() + source = os.path.join(testDataPath, 'dem.tif') + alg = contour_polygon() + alg.initAlgorithm() + with tempfile.TemporaryDirectory() as outdir: + self.assertEqual( + alg.getConsoleCommands({'INPUT': source, + 'BAND': 1, + 'FIELD_NAME_MIN': 'min', + 'FIELD_NAME_MAX': 'max', + 'INTERVAL': 5, + 'OUTPUT': outdir + '/check.shp'}, context, feedback), + ['gdal_contour', + '-p -amax max -amin min -b 1 -i 5.0 -f "ESRI Shapefile" ' + + source + ' ' + + outdir + '/check.shp']) + def testContour(self): context = QgsProcessingContext() feedback = QgsProcessingFeedback() diff --git a/python/plugins/processing/tests/GuiTest.py b/python/plugins/processing/tests/GuiTest.py index 9a2ed9ca36f6..5626de01b320 100644 --- a/python/plugins/processing/tests/GuiTest.py +++ b/python/plugins/processing/tests/GuiTest.py @@ -33,7 +33,8 @@ QgsProcessingParameterRasterDestination, QgsProcessingParameterRange, QgsFeature, - QgsVectorLayer, + QgsProcessingModelAlgorithm, + QgsUnitTypes, QgsProject) from qgis.analysis import QgsNativeAlgorithms @@ -41,7 +42,6 @@ from processing.gui.BatchAlgorithmDialog import BatchAlgorithmDialog from processing.modeler.ModelerParametersDialog import ModelerParametersDialog from processing.gui.wrappers import * -from processing.gui.DestinationSelectionPanel import DestinationSelectionPanel start_app() QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms()) @@ -54,7 +54,7 @@ class AlgorithmDialogTest(unittest.TestCase): def testCreation(self): alg = QgsApplication.processingRegistry().createAlgorithmById('native:centroids') a = AlgorithmDialog(alg) - self.assertEqual(a.mainWidget().alg, alg) + self.assertEqual(a.mainWidget().algorithm(), alg) class WrappersTest(unittest.TestCase): @@ -355,134 +355,6 @@ def testNumber(self): def testBand(self): self.checkConstructWrapper(QgsProcessingParameterBand('test'), BandWidgetWrapper) - def testFeatureSink(self): - param = QgsProcessingParameterFeatureSink('test') - alg = QgsApplication.processingRegistry().createAlgorithmById('native:centroids') - panel = DestinationSelectionPanel(param, alg) - - panel.setValue(QgsProcessing.TEMPORARY_OUTPUT) - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), QgsProcessing.TEMPORARY_OUTPUT) - - panel.setValue('memory:') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), QgsProcessing.TEMPORARY_OUTPUT) - - panel.setValue('''ogr:dbname='/me/a.gpkg' table="d" (geom) sql=''') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '''ogr:dbname='/me/a.gpkg' table="d" (geom) sql=''') - - panel.setValue('''postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table="stufff"."output" (the_geom) sql=''') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '''postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table="stufff"."output" (the_geom) sql=''') - - panel.setValue('/home/me/test.shp') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '/home/me/test.shp') - - ProcessingConfig.setSettingValue(ProcessingConfig.OUTPUT_FOLDER, testDataPath) - panel.setValue('test.shp') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), os.path.join(testDataPath, 'test.shp')) - - def testVectorDestination(self): - param = QgsProcessingParameterVectorDestination('test') - alg = QgsApplication.processingRegistry().createAlgorithmById('native:centroids') - panel = DestinationSelectionPanel(param, alg) - - panel.setValue(QgsProcessing.TEMPORARY_OUTPUT) - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.sink.staticValue(), QgsProcessing.TEMPORARY_OUTPUT) - - panel.setValue('''ogr:dbname='/me/a.gpkg' table="d" (geom) sql=''') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '''ogr:dbname='/me/a.gpkg' table="d" (geom) sql=''') - - panel.setValue('''postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table="stufff"."output" (the_geom) sql=''') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '''postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table="stufff"."output" (the_geom) sql=''') - - panel.setValue('/home/me/test.shp') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '/home/me/test.shp') - - ProcessingConfig.setSettingValue(ProcessingConfig.OUTPUT_FOLDER, testDataPath) - panel.setValue('test.shp') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), os.path.join(testDataPath, 'test.shp')) - - def testRasterDestination(self): - param = QgsProcessingParameterRasterDestination('test') - alg = QgsApplication.processingRegistry().createAlgorithmById('native:centroids') - panel = DestinationSelectionPanel(param, alg) - - panel.setValue('/home/me/test.tif') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), '/home/me/test.tif') - - panel.setValue(QgsProcessing.TEMPORARY_OUTPUT) - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.sink.staticValue(), QgsProcessing.TEMPORARY_OUTPUT) - - ProcessingConfig.setSettingValue(ProcessingConfig.OUTPUT_FOLDER, testDataPath) - panel.setValue('test.tif') - v = panel.getValue() - self.assertIsInstance(v, QgsProcessingOutputLayerDefinition) - self.assertEqual(v.createOptions, {'fileEncoding': 'System'}) - self.assertEqual(v.sink.staticValue(), os.path.join(testDataPath, 'test.tif')) - - def testFolderDestination(self): - param = QgsProcessingParameterFolderDestination('test') - alg = QgsApplication.processingRegistry().createAlgorithmById('native:centroids') - panel = DestinationSelectionPanel(param, alg) - - panel.setValue('/home/me/test.tif') - v = panel.getValue() - self.assertEqual(v, '/home/me/test.tif') - - ProcessingConfig.setSettingValue(ProcessingConfig.OUTPUT_FOLDER, testDataPath) - panel.setValue('test.tif') - v = panel.getValue() - self.assertEqual(v, os.path.join(testDataPath, 'test.tif')) - - def testFileDestination(self): - param = QgsProcessingParameterFileDestination('test') - alg = QgsApplication.processingRegistry().createAlgorithmById('native:centroids') - panel = DestinationSelectionPanel(param, alg) - - panel.setValue('/home/me/test.tif') - v = panel.getValue() - self.assertEqual(v, '/home/me/test.tif') - - ProcessingConfig.setSettingValue(ProcessingConfig.OUTPUT_FOLDER, testDataPath) - panel.setValue('test.tif') - v = panel.getValue() - self.assertEqual(v, os.path.join(testDataPath, 'test.tif')) - if __name__ == '__main__': unittest.main() diff --git a/python/plugins/processing/tests/OtbAlgorithmsTest.py b/python/plugins/processing/tests/OtbAlgorithmsTest.py index 0fbf2407984a..465f8802ee7f 100644 --- a/python/plugins/processing/tests/OtbAlgorithmsTest.py +++ b/python/plugins/processing/tests/OtbAlgorithmsTest.py @@ -36,7 +36,8 @@ QgsProcessingContext, QgsProcessingUtils, QgsProcessingFeedback, - QgsProcessingParameterDefinition) + QgsProcessingParameterDefinition, + QgsProcessingModelAlgorithm) from qgis.testing import start_app, unittest from processing.core.ProcessingConfig import ProcessingConfig, Setting from processing.gui.AlgorithmDialog import AlgorithmDialog diff --git a/python/plugins/processing/tests/testdata/custom/multi_polys_with_empty_geoms.gpkg b/python/plugins/processing/tests/testdata/custom/multi_polys_with_empty_geoms.gpkg new file mode 100644 index 000000000000..ab48db8e1d91 Binary files /dev/null and b/python/plugins/processing/tests/testdata/custom/multi_polys_with_empty_geoms.gpkg differ diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_line.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_line.gml new file mode 100644 index 000000000000..44a84a4c234c --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_line.gml @@ -0,0 +1,44 @@ + + + + + -1-3 + 115 + + + + + + 6,2 9,2 9,3 11,5 + + + + + -1,-1 1,-1 + + + + + 2,0 2,2 3,2 3,3 + + + + + 3,1 5,1 + + + + + 7,-3 10,-3 + + + + + 6,-3 10,1 + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_line.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_line.xsd new file mode 100644 index 000000000000..1f47f6781d46 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_line.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_null.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_null.gml new file mode 100644 index 000000000000..4227e492428f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_null.gml @@ -0,0 +1,13 @@ + + + missing + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_null.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_null.xsd new file mode 100644 index 000000000000..114f8d7bdee0 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_null.xsd @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_point.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_point.gml new file mode 100644 index 000000000000..eca047924b29 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_point.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_point.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_point.xsd new file mode 100644 index 000000000000..024331792923 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_point.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_poly.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_poly.gml new file mode 100644 index 000000000000..096e2f894c26 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_poly.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_lines_poly.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_poly.xsd new file mode 100644 index 000000000000..bec3a032db0f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_lines_poly.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_line.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_points_line.gml new file mode 100644 index 000000000000..2e211fc06002 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_line.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_line.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_points_line.xsd new file mode 100644 index 000000000000..13c60d78548f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_line.xsd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_null.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_points_null.gml new file mode 100644 index 000000000000..4d7afbdb8509 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_null.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_null.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_points_null.xsd new file mode 100644 index 000000000000..d9de16356449 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_null.xsd @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_point.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_points_point.gml new file mode 100644 index 000000000000..6504d65999dc --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_point.gml @@ -0,0 +1,77 @@ + + + + + 0-5 + 83 + + + + + + 1,1 + 1 + 2 + + + + + 3,3 + 2 + 1 + + + + + 2,2 + 3 + 0 + + + + + 5,2 + 4 + 2 + + + + + 4,1 + 5 + 1 + + + + + 0,-5 + 6 + 0 + + + + + 8,-1 + 7 + 0 + + + + + 7,-1 + 8 + 0 + + + + + 0,-1 + 9 + 0 + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_point.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_points_point.xsd new file mode 100644 index 000000000000..cd4192236b0b --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_point.xsd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_poly.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_points_poly.gml new file mode 100644 index 000000000000..d2adb0a7182a --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_poly.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_points_poly.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_points_poly.xsd new file mode 100644 index 000000000000..9a33ecc06a3e --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_points_poly.xsd @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_line.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_line.gml new file mode 100644 index 000000000000..7d20eb385ee2 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_line.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_line.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_line.xsd new file mode 100644 index 000000000000..fe878923e6ec --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_line.xsd @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_null.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_null.gml new file mode 100644 index 000000000000..5b2a2382554f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_null.gml @@ -0,0 +1,16 @@ + + + missing + + + + + 120 + -100291.43213 + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_null.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_null.xsd new file mode 100644 index 000000000000..155f78893f83 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_null.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_point.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_point.gml new file mode 100644 index 000000000000..61dfcb5d2a16 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_point.gml @@ -0,0 +1,9 @@ + + + missing + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_point.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_point.xsd new file mode 100644 index 000000000000..e2a350424903 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_point.xsd @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_poly.gml b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_poly.gml new file mode 100644 index 000000000000..8408a8062436 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_poly.gml @@ -0,0 +1,54 @@ + + + + + -1-3 + 106 + + + + + + -1,-1 -1,3 3,3 3,2 2,2 2,-1 -1,-1 + aaaaa + 33 + 44.123456 + + + + + 5,5 6,4 4,4 5,5 + Aaaaa + -33 + 0 + + + + + 2,5 2,6 3,6 3,5 2,5 + bbaaa + + 0.123 + + + + + 6,1 10,1 10,-3 6,-3 6,17,0 7,-2 9,-2 9,0 7,0 + ASDF + 0 + + + + + + 3,2 6,1 6,-3 2,-1 2,2 3,2 + elim + 2 + 3.33 + + + diff --git a/python/plugins/processing/tests/testdata/expected/filter_geom_polys_poly.xsd b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_poly.xsd new file mode 100644 index 000000000000..c0877a87b891 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/filter_geom_polys_poly.xsd @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_non_empty.gml b/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_non_empty.gml new file mode 100644 index 000000000000..58280b3373b2 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_non_empty.gml @@ -0,0 +1,54 @@ + + + + + 1152380.710241179991995.65061761190 + 1155226.440646131994672.97217925230 + + + + + + 1153212.63507019,991995.873315548,0 1153206.63873892,991995.650617612,0 1153206.28716221,992005.149060725,0 1153209.30632804,992005.260950644,0 1153212.28369425,992005.371359061,0 1153212.63507019,991995.873315548,0 + + + + + 1155221.09556005,994660.22300975,0 1155225.70703642,994665.492121154,0 1155226.44064613,994664.81215781,0 1155221.82916974,994659.543046403,0 1155221.09556005,994660.22300975,0 + + + + + 1155218.1610862,994662.942992519,0 1155216.84296042,994661.436981518,0 1155213.8673808,994664.195071552,0 1155215.22680442,994665.662838767,0 1155215.66683585,994665.25490937,0 1155218.1610862,994662.942992519,0 + + + + + 1155215.66683187,994665.254893728,0 1155215.22680937,994665.662853824,0 1155213.02579582,994667.703152789,0 1155215.0023435,994669.961178547,0 1155217.6433796,994667.512919493,0 1155215.66683187,994665.254893728,0 + + + + + 1155222.77277049,994668.212047819,0 1155218.16109298,994662.943021336,0 1155215.66683187,994665.254893728,0 1155217.6433796,994667.512919493,0 1155215.0023435,994669.961178547,0 1155217.63747314,994672.972179252,0 1155222.77277049,994668.212047819,0 + + + + + 1155216.84296042,994661.436981518,0 1155218.1610862,994662.942992519,0 1155218.30792385,994662.806888921,0 1155216.94851719,994661.339140047,0 1155216.84296042,994661.436981518,0 + + + + + 1155218.1610862,994662.942992519,0 1155222.77280779,994668.212065019,0 1155223.08625704,994667.921518022,0 1155225.47071869,994665.711277663,0 1155225.70703642,994665.492121158,0 1155221.09556006,994660.223009752,0 1155218.30792385,994662.806888921,0 1155218.1610862,994662.942992519,0 + + + + + 1152391.40704313,992267.107529733,0 1152383.73923598,992267.815575151,0 1152380.71024118,992271.79589533,0 1152385.61043835,992275.260519417,0 1152391.40704313,992267.107529733,0 + + + diff --git a/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_non_empty.xsd b/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_non_empty.xsd new file mode 100644 index 000000000000..1c40b0d72ed1 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_non_empty.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_one_empty.gpkg b/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_one_empty.gpkg new file mode 100644 index 000000000000..afe6ab8c578c Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/multi_polys_non_null_one_empty.gpkg differ diff --git a/python/plugins/processing/tests/testdata/expected/randompointsonlines.gml b/python/plugins/processing/tests/testdata/expected/randompointsonlines.gml new file mode 100644 index 000000000000..d9ef27fca75a --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/randompointsonlines.gml @@ -0,0 +1,194 @@ + + + + + -0.7437511044553879-1 + 5.4566049102316894.119686133816111 + + + + + + 0.994369616460531,-1.0 + 0 + + + + + 0.865114722736331,-1.0 + 1 + + + + + -0.743751104455388,-1.0 + 2 + + + + + 0.998081030930547,-1.0 + 3 + + + + + -0.527822047403662,-1.0 + 4 + + + + + -0.206838547674781,-1.0 + 5 + + + + + -0.224178519478878,-1.0 + 6 + + + + + 0.339492080894094,-1.0 + 7 + + + + + 0.87107814540936,-1.0 + 8 + + + + + 0.692621836689681,-1.0 + 9 + + + + + 4.06982464840608,1.0 + 10 + + + + + 4.79132461080717,1.0 + 11 + + + + + 4.51438540565048,1.0 + 12 + + + + + 3.78400298357133,1.0 + 13 + + + + + 4.8250159892403,1.0 + 14 + + + + + 5.00502180455731,1.29377556660287 + 15 + + + + + 4.5613480038941,1.0 + 16 + + + + + 4.47082956972335,1.0 + 17 + + + + + 5.00355294633537,1.2078473606193 + 18 + + + + + 5.01293482227269,1.75668710295238 + 19 + + + + + 5.45660491023169,4.11968613381611 + 20 + + + + + 3.78645266020438,2.98379593968487 + 21 + + + + + 2.0,0.844225610300582 + 22 + + + + + 3.65776768699273,4.06779659853037 + 23 + + + + + 4.35274779800432,2.9721280020792 + 24 + + + + + 4.02647088988085,2.97885060722278 + 25 + + + + + 4.03062245091484,2.97876506851358 + 26 + + + + + 2.48398460138168,2.0 + 27 + + + + + 2.0,0.538945986405747 + 28 + + + + + 5.04337850348996,4.10776614131395 + 29 + + + diff --git a/python/plugins/processing/tests/testdata/expected/randompointsonlines.xsd b/python/plugins/processing/tests/testdata/expected/randompointsonlines.xsd new file mode 100644 index 000000000000..86546b28f784 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/randompointsonlines.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/randompointsonlines_min1.gml b/python/plugins/processing/tests/testdata/expected/randompointsonlines_min1.gml new file mode 100644 index 000000000000..1dd1557ce0a0 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/randompointsonlines_min1.gml @@ -0,0 +1,80 @@ + + + + + -0.7437511044553879-1 + 5.414494320374054.102675375546412 + + + + + + 0.994369616460531,-1.0 + 0 + + + + + -0.743751104455388,-1.0 + 1 + + + + + 5.01381944749508,1.80843767846222 + 2 + + + + + 4.21755080717389,1.0 + 3 + + + + + 3.03985013553821,1.0 + 4 + + + + + 2.0,0.455023484569141 + 5 + + + + + 3.22434857680558,2.99537752486052 + 6 + + + + + 2.71714772226361,2.0 + 7 + + + + + 5.41449432037405,2.95025179063198 + 8 + + + + + 4.86689862354882,4.10267537554641 + 9 + + + + + 3.08606511997889,4.05130517832805 + 10 + + + diff --git a/python/plugins/processing/tests/testdata/expected/randompointsonlines_min1.xsd b/python/plugins/processing/tests/testdata/expected/randompointsonlines_min1.xsd new file mode 100644 index 000000000000..6019529d624f --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/randompointsonlines_min1.xsd @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/processing/tests/testdata/expected/results_remove_null_geometries.gpkg b/python/plugins/processing/tests/testdata/expected/results_remove_null_geometries.gpkg new file mode 100644 index 000000000000..0ed28f607b05 Binary files /dev/null and b/python/plugins/processing/tests/testdata/expected/results_remove_null_geometries.gpkg differ diff --git a/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.dbf b/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.dbf index f4ec83fd660d..a27e59b1c18a 100644 Binary files a/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.dbf and b/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.dbf differ diff --git a/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.prj b/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.prj index a30c00a55de1..f45cbadf0074 100644 --- a/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.prj +++ b/python/plugins/processing/tests/testdata/expected/split_lines_with_same2.prj @@ -1 +1 @@ -GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] \ No newline at end of file +GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]] \ No newline at end of file diff --git a/python/plugins/processing/tests/testdata/otb_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/otb_algorithm_tests.yaml index c3cfa0b8d7b4..aac405e0d4c1 100644 --- a/python/plugins/processing/tests/testdata/otb_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/otb_algorithm_tests.yaml @@ -23,7 +23,7 @@ tests: name: raster.tif type: file type: gaussian - type.gaussian.radius: 4.0 + type.gaussian.stdev: 4.0 results: out: hash: b3fbccd6f41052317a435567a2633dae1d9b66772a4d8a3323d9b1c5 diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml index 2b10cbbd8117..5ba1b11de351 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests2.yaml @@ -184,7 +184,7 @@ tests: type: rasterhash - algorithm: native:removenullgeometries - name: Remove null geometries + name: Remove null geometries (do not remove empty) params: INPUT: name: polys.gml @@ -194,6 +194,75 @@ tests: name: expected/remove_null_polys.gml type: vector + - algorithm: native:removenullgeometries + name: Remove null geometries (remove empty) + params: + INPUT: + name: polys.gml + type: vector + REMOVE_EMPTY: true + results: + OUTPUT: + name: expected/remove_null_polys.gml + type: vector + + - algorithm: native:removenullgeometries + name: Remove null geometries (do not remove empty, but there is empty data) + params: + INPUT: + name: custom/multi_polys_with_empty_geoms.gpkg|layername=multi_polys_with_empty_geoms + type: vector + results: + OUTPUT: + name: ogr:dbname='expected/multi_polys_non_null_one_empty.gpkg' table="output" (geom) sql= + uri: expected/multi_polys_non_null_one_empty.gpkg|layername=multi_polys_non_null_one_empty + type: vector + + - algorithm: native:removenullgeometries + name: Remove null geometries (remove empty, and there is empty data) + params: + INPUT: + name: custom/multi_polys_with_empty_geoms.gpkg|layername=multi_polys_with_empty_geoms + type: vector + REMOVE_EMPTY: true + results: + OUTPUT: + name: expected/multi_polys_non_null_non_empty.gml + type: vector + + - algorithm: native:removenullgeometries + name: Remove null geometries, 2 outputs (do not remove empty, but there is empty data) + params: + INPUT: + name: custom/multi_polys_with_empty_geoms.gpkg|layername=multi_polys_with_empty_geoms + type: vector + results: + OUTPUT: + name: ogr:dbname='expected/results_remove_null_geometries.gpkg' table="output" (geom) sql= + uri: expected/results_remove_null_geometries.gpkg|layername=do_not_remove_empty_non_null + type: vector + NULL_OUTPUT: + name: ogr:dbname='expected/results_remove_null_geometries.gpkg' table="output" (geom) sql= + uri: expected/results_remove_null_geometries.gpkg|layername=do_not_remove_empty_null + type: vector + + - algorithm: native:removenullgeometries + name: Remove null geometries, 2 outputs (remove empty, and there is empty data) + params: + INPUT: + name: custom/multi_polys_with_empty_geoms.gpkg|layername=multi_polys_with_empty_geoms + type: vector + REMOVE_EMPTY: true + results: + OUTPUT: + name: ogr:dbname='expected/results_remove_null_geometries.gpkg' table="output" (geom) sql= + uri: expected/results_remove_null_geometries.gpkg|layername=remove_empty_non_null + type: vector + NULL_OUTPUT: + name: ogr:dbname='expected/results_remove_null_geometries.gpkg' table="output" (geom) sql= + uri: expected/results_remove_null_geometries.gpkg|layername=remove_empty_null + type: vector + - algorithm: native:extractbyexpression name: Extract by Expression params: diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml index 60e75cf7d17c..713687fdce1f 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests3.yaml @@ -588,6 +588,38 @@ tests: type: vector name: randompointsinextent.gml + - algorithm: native:randompointsonlines + name: Random points on lines, seed 1, multilines - one empty geom + params: + INCLUDE_LINE_ATTRIBUTES: false + INPUT: + name: multilines.gml|layername=multilines + type: vector + MAX_TRIES_PER_POINT: 10 + MIN_DISTANCE: 0.0 + POINTS_NUMBER: 10 + SEED: 1 + results: + OUTPUT: + name: expected/randompointsonlines.gml + type: vector + + - algorithm: native:randompointsonlines + name: Random points on lines, min distance 1, seed 1, attributes, multilines - one empty geom + params: + INCLUDE_LINE_ATTRIBUTES: true + INPUT: + name: multilines.gml|layername=multilines + type: vector + MAX_TRIES_PER_POINT: 10 + MIN_DISTANCE: 1.0 + POINTS_NUMBER: 10 + SEED: 1 + results: + OUTPUT: + name: expected/randompointsonlines_min1.gml + type: vector + - algorithm: native:randomextract name: Random extract by number params: diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests4.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests4.yaml index 0ca21940ad15..15ee24781b24 100755 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests4.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests4.yaml @@ -2365,5 +2365,65 @@ tests: name: expected/renamed_field.gml type: vector + - algorithm: native:filterbygeometry + name: Filter by geometry type polygon input + params: + INPUT: + name: polys.gml|layername=polys2 + type: vector + results: + LINES: + name: expected/filter_geom_polys_line.gml + type: vector + NO_GEOMETRY: + name: expected/filter_geom_polys_null.gml + type: vector + POINTS: + name: expected/filter_geom_polys_point.gml + type: vector + POLYGONS: + name: expected/filter_geom_polys_poly.gml + type: vector + + - algorithm: native:filterbygeometry + name: Filter by geometry type point input + params: + INPUT: + name: points.gml|layername=points + type: vector + results: + LINES: + name: expected/filter_geom_points_line.gml + type: vector + NO_GEOMETRY: + name: expected/filter_geom_points_null.gml + type: vector + POINTS: + name: expected/filter_geom_points_point.gml + type: vector + POLYGONS: + name: expected/filter_geom_points_poly.gml + type: vector + + - algorithm: native:filterbygeometry + name: Filter by geometry type line input + params: + INPUT: + name: lines.gml|layername=lines + type: vector + results: + LINES: + name: expected/filter_geom_lines_line.gml + type: vector + NO_GEOMETRY: + name: expected/filter_geom_lines_null.gml + type: vector + POINTS: + name: expected/filter_geom_lines_point.gml + type: vector + POLYGONS: + name: expected/filter_geom_lines_poly.gml + type: vector + # See ../README.md for a description of the file format diff --git a/python/plugins/processing/tools/postgis.py b/python/plugins/processing/tools/postgis.py deleted file mode 100644 index 9fc69c34be08..000000000000 --- a/python/plugins/processing/tools/postgis.py +++ /dev/null @@ -1,921 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - postgis.py - --------------------- - Date : November 2012 - Copyright : (C) 2012 by Martin Dobias - Email : volayaf 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__ = 'Martin Dobias' -__date__ = 'November 2012' -__copyright__ = '(C) 2012, Martin Dobias' - -import psycopg2 -import psycopg2.extensions # For isolation levels -import re -import os - -from qgis.core import (QgsProcessingException, - QgsDataSourceUri, - QgsCredentials, - QgsSettings) - -from qgis.PyQt.QtCore import QCoreApplication - - -# Use unicode! -psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) - - -class DbError(Exception): - - def __init__(self, message, query=None): - self.message = str(message) - self.query = (str(query) if query is not None else None) - - def __str__(self): - return 'MESSAGE: %s\nQUERY: %s' % (self.message, self.query) - - -def uri_from_name(conn_name): - settings = QgsSettings() - settings.beginGroup(u"/PostgreSQL/connections/%s" % conn_name) - - if not settings.contains("database"): # non-existent entry? - raise QgsProcessingException(QCoreApplication.translate("PostGIS", 'There is no defined database connection "{0}".').format(conn_name)) - - uri = QgsDataSourceUri() - - settingsList = ["service", "host", "port", "database", "username", "password", "authcfg"] - service, host, port, database, username, password, authcfg = [settings.value(x, "", type=str) for x in settingsList] - - useEstimatedMetadata = settings.value("estimatedMetadata", False, type=bool) - try: - sslmode = settings.value("sslmode", QgsDataSourceUri.SslPrefer, type=int) - except TypeError: - sslmode = QgsDataSourceUri.SslPrefer - - settings.endGroup() - - if hasattr(authcfg, 'isNull') and authcfg.isNull(): - authcfg = '' - - if service: - uri.setConnection(service, database, username, password, sslmode, authcfg) - else: - uri.setConnection(host, port, database, username, password, sslmode, authcfg) - - uri.setUseEstimatedMetadata(useEstimatedMetadata) - - return uri - - -class TableAttribute(object): - - def __init__(self, row): - (self.num, - self.name, - self.data_type, - self.char_max_len, - self.modifier, - self.notnull, - self.hasdefault, - self.default, - ) = row - - -class TableConstraint(object): - - """Class that represents a constraint of a table (relation). - """ - - (TypeCheck, TypeForeignKey, TypePrimaryKey, TypeUnique) = list(range(4)) - types = { - 'c': TypeCheck, - 'f': TypeForeignKey, - 'p': TypePrimaryKey, - 'u': TypeUnique, - } - - on_action = { - 'a': 'NO ACTION', - 'r': 'RESTRICT', - 'c': 'CASCADE', - 'n': 'SET NULL', - 'd': 'SET DEFAULT', - } - match_types = {'u': 'UNSPECIFIED', 'f': 'FULL', 'p': 'PARTIAL'} - - def __init__(self, row): - (self.name, con_type, self.is_deferable, self.is_deferred, keys) = row[:5] - self.keys = list(map(int, keys.split(' '))) - self.con_type = TableConstraint.types[con_type] # Convert to enum - if self.con_type == TableConstraint.TypeCheck: - self.check_src = row[5] - elif self.con_type == TableConstraint.TypeForeignKey: - self.foreign_table = row[6] - self.foreign_on_update = TableConstraint.on_action[row[7]] - self.foreign_on_delete = TableConstraint.on_action[row[8]] - self.foreign_match_type = TableConstraint.match_types[row[9]] - self.foreign_keys = row[10] - - -class TableIndex(object): - - def __init__(self, row): - (self.name, columns) = row - self.columns = list(map(int, columns.split(' '))) - - -class TableField(object): - - def __init__(self, name, data_type, is_null=None, default=None, - modifier=None): - (self.name, self.data_type, self.is_null, self.default, - self.modifier) = (name, data_type, is_null, default, modifier) - - def is_null_txt(self): - if self.is_null: - return 'NULL' - else: - return 'NOT NULL' - - def field_def(self): - """Return field definition as used for CREATE TABLE or - ALTER TABLE command. - """ - - data_type = (self.data_type if not self.modifier or self.modifier - < 0 else '%s(%d)' % (self.data_type, self.modifier)) - txt = '%s %s %s' % (self._quote(self.name), data_type, - self.is_null_txt()) - if self.default and len(self.default) > 0: - txt += ' DEFAULT %s' % self.default - return txt - - def _quote(self, ident): - if re.match(r"^\w+$", ident) is not None: - return ident - else: - return '"%s"' % ident.replace('"', '""') - - -class GeoDB(object): - - @classmethod - def from_name(cls, conn_name): - uri = uri_from_name(conn_name) - return cls(uri=uri) - - def __init__(self, host=None, port=None, dbname=None, user=None, - passwd=None, service=None, uri=None): - # Regular expression for identifiers without need to quote them - self.re_ident_ok = re.compile(r"^\w+$") - port = str(port) - - if uri: - self.uri = uri - else: - self.uri = QgsDataSourceUri() - if service: - self.uri.setConnection(service, dbname, user, passwd) - else: - self.uri.setConnection(host, port, dbname, user, passwd) - - conninfo = self.uri.connectionInfo(False) - err = None - for i in range(4): - expandedConnInfo = self.uri.connectionInfo(True) - try: - self.con = psycopg2.connect(expandedConnInfo) - if err is not None: - QgsCredentials.instance().put(conninfo, - self.uri.username(), - self.uri.password()) - break - except psycopg2.OperationalError as e: - if i == 3: - raise QgsProcessingException(str(e)) - - err = str(e) - user = self.uri.username() - password = self.uri.password() - (ok, user, password) = QgsCredentials.instance().get(conninfo, - user, - password, - err) - if not ok: - raise QgsProcessingException(QCoreApplication.translate("PostGIS", 'Action canceled by user')) - if user: - self.uri.setUsername(user) - if password: - self.uri.setPassword(password) - finally: - # remove certs (if any) of the expanded connectionInfo - expandedUri = QgsDataSourceUri(expandedConnInfo) - - sslCertFile = expandedUri.param("sslcert") - if sslCertFile: - sslCertFile = sslCertFile.replace("'", "") - try: - os.remove(sslCertFile) - except OSError: - pass - - sslKeyFile = expandedUri.param("sslkey") - if sslKeyFile: - sslKeyFile = sslKeyFile.replace("'", "") - try: - os.remove(sslKeyFile) - except OSError: - pass - - sslCAFile = expandedUri.param("sslrootcert") - if sslCAFile: - sslCAFile = sslCAFile.replace("'", "") - try: - os.remove(sslCAFile) - except OSError: - pass - - self.has_postgis = self.check_postgis() - - def get_info(self): - c = self.con.cursor() - self._exec_sql(c, 'SELECT version()') - return c.fetchone()[0] - - def check_postgis(self): - """Check whether postgis_version is present in catalog. - """ - - c = self.con.cursor() - self._exec_sql(c, - "SELECT COUNT(*) FROM pg_proc WHERE proname = 'postgis_version'") - return c.fetchone()[0] > 0 - - def get_postgis_info(self): - """Returns tuple about PostGIS support: - - lib version - - installed scripts version - - released scripts version - - geos version - - proj version - - whether uses stats - """ - - c = self.con.cursor() - self._exec_sql(c, - 'SELECT postgis_lib_version(), postgis_scripts_installed(), \ - postgis_scripts_released(), postgis_geos_version(), \ - postgis_proj_version(), postgis_uses_stats()') - return c.fetchone() - - def list_schemas(self): - """Get list of schemas in tuples: (oid, name, owner, perms). - """ - - c = self.con.cursor() - sql = "SELECT oid, nspname, pg_get_userbyid(nspowner), nspacl \ - FROM pg_namespace \ - WHERE nspname !~ '^pg_' AND nspname != 'information_schema'" - self._exec_sql(c, sql) - return c.fetchall() - - def list_geotables(self, schema=None): - """Get list of tables with schemas, whether user has privileges, - whether table has geometry column(s) etc. - - Geometry_columns: - - f_table_schema - - f_table_name - - f_geometry_column - - coord_dimension - - srid - - type - """ - - c = self.con.cursor() - - if schema: - schema_where = " AND nspname = '%s' " % self._quote_unicode(schema) - else: - schema_where = \ - " AND (nspname != 'information_schema' AND nspname !~ 'pg_') " - - # LEFT OUTER JOIN: like LEFT JOIN but if there are more matches, - # for join, all are used (not only one) - - # First find out whether PostGIS is enabled - if not self.has_postgis: - # Get all tables and views - sql = """SELECT pg_class.relname, pg_namespace.nspname, - pg_class.relkind, pg_get_userbyid(relowner), - reltuples, relpages, NULL, NULL, NULL, NULL - FROM pg_class - JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace - WHERE pg_class.relkind IN ('v', 'r', 'm', 'p')""" \ - + schema_where + 'ORDER BY nspname, relname' - else: - # Discovery of all tables and whether they contain a - # geometry column - sql = """SELECT pg_class.relname, pg_namespace.nspname, - pg_class.relkind, pg_get_userbyid(relowner), - reltuples, relpages, pg_attribute.attname, - pg_attribute.atttypid::regtype, NULL, NULL - FROM pg_class - JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace - LEFT OUTER JOIN pg_attribute ON - pg_attribute.attrelid = pg_class.oid AND - (pg_attribute.atttypid = 'geometry'::regtype - OR pg_attribute.atttypid IN - (SELECT oid FROM pg_type - WHERE typbasetype='geometry'::regtype)) - WHERE pg_class.relkind IN ('v', 'r', 'm', 'p') """ \ - + schema_where + 'ORDER BY nspname, relname, attname' - - self._exec_sql(c, sql) - items = c.fetchall() - - # Get geometry info from geometry_columns if exists - if self.has_postgis: - sql = """SELECT relname, nspname, relkind, - pg_get_userbyid(relowner), reltuples, relpages, - geometry_columns.f_geometry_column, - geometry_columns.type, - geometry_columns.coord_dimension, - geometry_columns.srid - FROM pg_class - JOIN pg_namespace ON relnamespace=pg_namespace.oid - LEFT OUTER JOIN geometry_columns ON - relname=f_table_name AND nspname=f_table_schema - WHERE relkind IN ('r','v','m','p') """ \ - + schema_where + 'ORDER BY nspname, relname, \ - f_geometry_column' - self._exec_sql(c, sql) - - # Merge geometry info to "items" - for (i, geo_item) in enumerate(c.fetchall()): - if geo_item[7]: - items[i] = geo_item - - return items - - def get_table_rows(self, table, schema=None): - c = self.con.cursor() - self._exec_sql(c, 'SELECT COUNT(*) FROM %s' % self._table_name(schema, - table)) - return c.fetchone()[0] - - def get_table_fields(self, table, schema=None): - """Return list of columns in table""" - - c = self.con.cursor() - schema_where = (" AND nspname='%s' " - % self._quote_unicode(schema) if schema is not None else '' - ) - - version_number = int(self.get_info().split(' ')[1].split('.')[0]) - ad_col_name = 'adsrc' if version_number < 12 else 'adbin' - - sql = """SELECT a.attnum AS ordinal_position, - a.attname AS column_name, - t.typname AS data_type, - a.attlen AS char_max_len, - a.atttypmod AS modifier, - a.attnotnull AS notnull, - a.atthasdef AS hasdefault, - adef.%s AS default_value - FROM pg_class c - JOIN pg_attribute a ON a.attrelid = c.oid - JOIN pg_type t ON a.atttypid = t.oid - JOIN pg_namespace nsp ON c.relnamespace = nsp.oid - LEFT JOIN pg_attrdef adef ON adef.adrelid = a.attrelid - AND adef.adnum = a.attnum - WHERE - c.relname = '%s' %s AND - a.attnum > 0 - ORDER BY a.attnum""" \ - % (ad_col_name, self._quote_unicode(table), schema_where) - - self._exec_sql(c, sql) - attrs = [] - for row in c.fetchall(): - attrs.append(TableAttribute(row)) - return attrs - - def get_table_indexes(self, table, schema=None): - """Get info about table's indexes. ignore primary key and unique - index, they get listed in constraints. - """ - - c = self.con.cursor() - - schema_where = (" AND nspname='%s' " - % self._quote_unicode(schema) if schema is not None else '' - ) - sql = """SELECT relname, indkey - FROM pg_class, pg_index - WHERE pg_class.oid = pg_index.indexrelid AND pg_class.oid IN ( - SELECT indexrelid - FROM pg_index, pg_class - JOIN pg_namespace nsp ON pg_class.relnamespace = nsp.oid - WHERE pg_class.relname='%s' %s AND - pg_class.oid=pg_index.indrelid - AND indisunique != 't' AND indisprimary != 't' )""" \ - % (self._quote_unicode(table), schema_where) - self._exec_sql(c, sql) - indexes = [] - for row in c.fetchall(): - indexes.append(TableIndex(row)) - return indexes - - def get_table_constraints(self, table, schema=None): - c = self.con.cursor() - - schema_where = (" AND nspname='%s' " - % self._quote_unicode(schema) if schema is not None else '' - ) - - version_number = int(self.get_info().split(' ')[1].split('.')[0]) - con_col_name = 'consrc' if version_number < 12 else 'conbin' - - sql = """SELECT c.conname, c.contype, c.condeferrable, c.condeferred, - array_to_string(c.conkey, ' '), c.%s, t2.relname, - c.confupdtype, c.confdeltype, c.confmatchtype, - array_to_string(c.confkey, ' ') - FROM pg_constraint c - LEFT JOIN pg_class t ON c.conrelid = t.oid - LEFT JOIN pg_class t2 ON c.confrelid = t2.oid - JOIN pg_namespace nsp ON t.relnamespace = nsp.oid - WHERE t.relname = '%s' %s """ \ - % (con_col_name, self._quote_unicode(table), schema_where) - - self._exec_sql(c, sql) - - constrs = [] - for row in c.fetchall(): - constrs.append(TableConstraint(row)) - return constrs - - def get_view_definition(self, view, schema=None): - """Returns definition of the view.""" - - schema_where = (" AND nspname='%s' " - % self._quote_unicode(schema) if schema is not None else '' - ) - sql = """SELECT pg_get_viewdef(c.oid) - FROM pg_class c - JOIN pg_namespace nsp ON c.relnamespace = nsp.oid - WHERE relname='%s' %s AND relkind IN ('v','m')""" \ - % (self._quote_unicode(view), schema_where) - c = self.con.cursor() - self._exec_sql(c, sql) - return c.fetchone()[0] - - def add_geometry_column(self, table, geom_type, schema=None, - geom_column='the_geom', srid=-1, dim=2): - # Use schema if explicitly specified - if schema: - schema_part = "'%s', " % self._quote_unicode(schema) - else: - schema_part = '' - sql = "SELECT AddGeometryColumn(%s'%s', '%s', %d, '%s', %d)" % ( - schema_part, - self._quote_unicode(table), - self._quote_unicode(geom_column), - srid, - self._quote_unicode(geom_type), - dim, - ) - self._exec_sql_and_commit(sql) - - def delete_geometry_column(self, table, geom_column, schema=None): - """Use PostGIS function to delete geometry column correctly.""" - - if schema: - schema_part = "'%s', " % self._quote_unicode(schema) - else: - schema_part = '' - sql = "SELECT DropGeometryColumn(%s'%s', '%s')" % (schema_part, - self._quote_unicode(table), self._quote_unicode(geom_column)) - self._exec_sql_and_commit(sql) - - def delete_geometry_table(self, table, schema=None): - """Delete table with one or more geometries using PostGIS function.""" - - if schema: - schema_part = "'%s', " % self._quote_unicode(schema) - else: - schema_part = '' - sql = "SELECT DropGeometryTable(%s'%s')" % (schema_part, - self._quote_unicode(table)) - self._exec_sql_and_commit(sql) - - def create_table(self, table, fields, pkey=None, schema=None): - """Create ordinary table. - - 'fields' is array containing instances of TableField - 'pkey' contains name of column to be used as primary key - """ - - if len(fields) == 0: - return False - - table_name = self._table_name(schema, table) - - sql = 'CREATE TABLE %s (%s' % (table_name, fields[0].field_def()) - for field in fields[1:]: - sql += ', %s' % field.field_def() - if pkey: - sql += ', PRIMARY KEY (%s)' % self._quote(pkey) - sql += ')' - self._exec_sql_and_commit(sql) - return True - - def delete_table(self, table, schema=None): - """Delete table from the database.""" - - table_name = self._table_name(schema, table) - sql = 'DROP TABLE %s' % table_name - self._exec_sql_and_commit(sql) - - def empty_table(self, table, schema=None): - """Delete all rows from table.""" - - table_name = self._table_name(schema, table) - sql = 'DELETE FROM %s' % table_name - self._exec_sql_and_commit(sql) - - def rename_table(self, table, new_table, schema=None): - """Rename a table in database.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s RENAME TO %s' % (table_name, - self._quote(new_table)) - self._exec_sql_and_commit(sql) - - # Update geometry_columns if PostGIS is enabled - if self.has_postgis: - sql = "UPDATE geometry_columns SET f_table_name='%s' \ - WHERE f_table_name='%s'" \ - % (self._quote_unicode(new_table), self._quote_unicode(table)) - if schema is not None: - sql += " AND f_table_schema='%s'" % self._quote_unicode(schema) - self._exec_sql_and_commit(sql) - - def create_view(self, name, query, schema=None): - view_name = self._table_name(schema, name) - sql = 'CREATE VIEW %s AS %s' % (view_name, query) - self._exec_sql_and_commit(sql) - - def delete_view(self, name, schema=None): - view_name = self._table_name(schema, name) - sql = 'DROP VIEW %s' % view_name - self._exec_sql_and_commit(sql) - - def rename_view(self, name, new_name, schema=None): - """Rename view in database.""" - - self.rename_table(name, new_name, schema) - - def create_schema(self, schema): - """Create a new empty schema in database.""" - - sql = 'CREATE SCHEMA %s' % self._quote(schema) - self._exec_sql_and_commit(sql) - - def delete_schema(self, schema): - """Drop (empty) schema from database.""" - - sql = 'DROP SCHEMA %s' % self._quote(schema) - self._exec_sql_and_commit(sql) - - def rename_schema(self, schema, new_schema): - """Rename a schema in database.""" - - sql = 'ALTER SCHEMA %s RENAME TO %s' % (self._quote(schema), - self._quote(new_schema)) - self._exec_sql_and_commit(sql) - - # Update geometry_columns if PostGIS is enabled - if self.has_postgis: - sql = \ - "UPDATE geometry_columns SET f_table_schema='%s' \ - WHERE f_table_schema='%s'" \ - % (self._quote_unicode(new_schema), self._quote_unicode(schema)) - self._exec_sql_and_commit(sql) - - def table_add_column(self, table, field, schema=None): - """Add a column to table (passed as TableField instance).""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s ADD %s' % (table_name, field.field_def()) - self._exec_sql_and_commit(sql) - - def table_delete_column(self, table, field, schema=None): - """Delete column from a table.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s DROP %s' % (table_name, self._quote(field)) - self._exec_sql_and_commit(sql) - - def table_column_rename(self, table, name, new_name, schema=None): - """Rename column in a table.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s RENAME %s TO %s' % (table_name, - self._quote(name), self._quote(new_name)) - self._exec_sql_and_commit(sql) - - # Update geometry_columns if PostGIS is enabled - if self.has_postgis: - sql = "UPDATE geometry_columns SET f_geometry_column='%s' \ - WHERE f_geometry_column='%s' AND f_table_name='%s'" \ - % (self._quote_unicode(new_name), self._quote_unicode(name), - self._quote_unicode(table)) - if schema is not None: - sql += " AND f_table_schema='%s'" % self._quote(schema) - self._exec_sql_and_commit(sql) - - def table_column_set_type(self, table, column, data_type, schema=None): - """Change column type.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s ALTER %s TYPE %s' % (table_name, - self._quote(column), data_type) - self._exec_sql_and_commit(sql) - - def table_column_set_default(self, table, column, default, schema=None): - """Change column's default value. - - If default=None drop default value. - """ - - table_name = self._table_name(schema, table) - if default: - sql = 'ALTER TABLE %s ALTER %s SET DEFAULT %s' % (table_name, - self._quote(column), default) - else: - sql = 'ALTER TABLE %s ALTER %s DROP DEFAULT' % (table_name, - self._quote(column)) - self._exec_sql_and_commit(sql) - - def table_column_set_null(self, table, column, is_null, schema=None): - """Change whether column can contain null values.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s ALTER %s ' % (table_name, self._quote(column)) - if is_null: - sql += 'DROP NOT NULL' - else: - sql += 'SET NOT NULL' - self._exec_sql_and_commit(sql) - - def table_add_primary_key(self, table, column, schema=None): - """Add a primery key (with one column) to a table.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s ADD PRIMARY KEY (%s)' % (table_name, - self._quote(column)) - self._exec_sql_and_commit(sql) - - def table_add_unique_constraint(self, table, column, schema=None): - """Add a unique constraint to a table.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s ADD UNIQUE (%s)' % (table_name, - self._quote(column)) - self._exec_sql_and_commit(sql) - - def table_delete_constraint(self, table, constraint, schema=None): - """Delete constraint in a table.""" - - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s DROP CONSTRAINT %s' % (table_name, - self._quote(constraint)) - self._exec_sql_and_commit(sql) - - def table_move_to_schema(self, table, new_schema, schema=None): - if new_schema == schema: - return - table_name = self._table_name(schema, table) - sql = 'ALTER TABLE %s SET SCHEMA %s' % (table_name, - self._quote(new_schema)) - self._exec_sql_and_commit(sql) - - # Update geometry_columns if PostGIS is enabled - if self.has_postgis: - sql = "UPDATE geometry_columns SET f_table_schema='%s' \ - WHERE f_table_name='%s'" \ - % (self._quote_unicode(new_schema), self._quote_unicode(table)) - if schema is not None: - sql += " AND f_table_schema='%s'" % self._quote_unicode(schema) - self._exec_sql_and_commit(sql) - - def create_index(self, table, name, column, schema=None): - """Create index on one column using default options.""" - - table_name = self._table_name(schema, table) - idx_name = self._quote(name) - sql = 'CREATE INDEX "%s" ON %s (%s)' % (idx_name, table_name, - self._quote(column)) - self._exec_sql_and_commit(sql) - - def create_spatial_index(self, table, schema=None, geom_column='the_geom'): - table_name = self._table_name(schema, table) - idx_name = self._quote(u"sidx_%s_%s" % (table, geom_column)) - sql = 'CREATE INDEX "%s" ON %s USING GIST(%s)' % (idx_name, table_name, - self._quote(geom_column)) - self._exec_sql_and_commit(sql) - - def delete_index(self, name, schema=None): - index_name = self._table_name(schema, name) - sql = 'DROP INDEX "%s"' % index_name - self._exec_sql_and_commit(sql) - - def get_database_privileges(self): - """DB privileges: (can create schemas, can create temp. tables). - """ - - sql = "SELECT has_database_privilege('%(d)s', 'CREATE'), \ - has_database_privilege('%(d)s', 'TEMP')" \ - % {'d': self._quote_unicode(self.uri.database())} - c = self.con.cursor() - self._exec_sql(c, sql) - return c.fetchone() - - def get_schema_privileges(self, schema): - """Schema privileges: (can create new objects, can access objects - in schema).""" - - sql = "SELECT has_schema_privilege('%(s)s', 'CREATE'), \ - has_schema_privilege('%(s)s', 'USAGE')" \ - % {'s': self._quote_unicode(schema)} - c = self.con.cursor() - self._exec_sql(c, sql) - return c.fetchone() - - def get_table_privileges(self, table, schema=None): - """Table privileges: (select, insert, update, delete). - """ - - t = self._table_name(schema, table) - sql = """SELECT has_table_privilege('%(t)s', 'SELECT'), - has_table_privilege('%(t)s', 'INSERT'), - has_table_privilege('%(t)s', 'UPDATE'), - has_table_privilege('%(t)s', 'DELETE')""" \ - % {'t': self._quote_unicode(t)} - c = self.con.cursor() - self._exec_sql(c, sql) - return c.fetchone() - - def vacuum_analyze(self, table, schema=None): - """Run VACUUM ANALYZE on a table.""" - - t = self._table_name(schema, table) - - # VACUUM ANALYZE must be run outside transaction block - we - # have to change isolation level - self.con.set_isolation_level( - psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT) - c = self.con.cursor() - self._exec_sql(c, 'VACUUM ANALYZE %s' % t) - self.con.set_isolation_level( - psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED) - - def sr_info_for_srid(self, srid): - if not self.has_postgis: - return 'Unknown' - - try: - c = self.con.cursor() - self._exec_sql(c, - "SELECT srtext FROM spatial_ref_sys WHERE srid = '%d'" - % srid) - srtext = c.fetchone()[0] - - # Try to extract just SR name (should be quoted in double - # quotes) - x = re.search('"([^"]+)"', srtext) - if x is not None: - srtext = x.group() - return srtext - except DbError: - return 'Unknown' - - def insert_table_row(self, table, values, schema=None, cursor=None): - """Insert a row with specified values to a table. - - If a cursor is specified, it doesn't commit (expecting that - there will be more inserts) otherwise it commits immediately. - """ - - t = self._table_name(schema, table) - sql = '' - for value in values: - # TODO: quote values? - if sql: - sql += ', ' - sql += value - sql = 'INSERT INTO %s VALUES (%s)' % (t, sql) - if cursor: - self._exec_sql(cursor, sql) - else: - self._exec_sql_and_commit(sql) - - def _exec_sql(self, cursor, sql): - try: - cursor.execute(sql) - except psycopg2.Error as e: - raise QgsProcessingException(str(e) + ' QUERY: ' - + e.cursor.query.decode(e.cursor.connection.encoding)) - - def _exec_sql_and_commit(self, sql): - """Tries to execute and commit some action, on error it rolls - back the change. - """ - - try: - c = self.con.cursor() - self._exec_sql(c, sql) - self.con.commit() - except DbError: - self.con.rollback() - raise - - def _quote(self, identifier): - """Quote identifier if needed.""" - - # Make sure it's python unicode string - identifier = str(identifier) - - # Is it needed to quote the identifier? - if self.re_ident_ok.match(identifier) is not None: - return identifier - - # It's needed - let's quote it (and double the double-quotes) - return u'"%s"' % identifier.replace('"', '""') - - def _quote_unicode(self, txt): - """Make the string safe - replace ' with ''. - """ - - # make sure it's python unicode string - txt = str(txt) - return txt.replace("'", "''") - - def _table_name(self, schema, table): - if not schema: - return self._quote(table) - else: - return u'"%s"."%s"' % (self._quote(schema), self._quote(table)) - - -# For debugging / testing -if __name__ == '__main__': - - db = GeoDB(host='localhost', dbname='gis', user='gisak', passwd='g') - # fix_print_with_import - print(db.list_schemas()) - # fix_print_with_import - print('==========') - - for row in db.list_geotables(): - # fix_print_with_import - print(row) - # fix_print_with_import - print('==========') - - for row in db.get_table_indexes('trencin'): - # fix_print_with_import - print(row) - # fix_print_with_import - print('==========') - - for row in db.get_table_constraints('trencin'): - # fix_print_with_import - print(row) - # fix_print_with_import - print('==========') - - # fix_print_with_import - print(db.get_table_rows('trencin')) - - # for fld in db.get_table_metadata('trencin'): - # ....print fld - # try: - # ....db.create_table('trrrr', [('id','serial'), ('test','text')]) - # except DbError, e: - # ....print unicode(e), e.query diff --git a/python/plugins/processing/tools/spatialite.py b/python/plugins/processing/tools/spatialite.py deleted file mode 100644 index aa7d2b78a206..000000000000 --- a/python/plugins/processing/tools/spatialite.py +++ /dev/null @@ -1,129 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -*************************************************************************** - spatialite.py - --------------------- - Date : November 2015 - Copyright : (C) 2015 by René-Luc Dhont - Email : volayaf 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__ = 'René-Luc Dhont' -__date__ = 'November 2015' -__copyright__ = '(C) 2015, René-Luc Dhont' - -from qgis.utils import spatialite_connect -import sqlite3 as sqlite -import re - - -class DbError(Exception): - - def __init__(self, message, query=None): - # Save error. funny that the variables are in utf-8 - self.message = str(message) - self.query = (str(query) if query is not None else None) - - def __str__(self): - return 'MESSAGE: %s\nQUERY: %s' % (self.message, self.query) - - -class GeoDB: - - def __init__(self, uri=None): - self.uri = uri - self.dbname = uri.database() - - try: - self.con = spatialite_connect(self.con_info()) - - except (sqlite.InterfaceError, sqlite.OperationalError) as e: - raise DbError(str(e)) - - self.has_spatialite = self.check_spatialite() - if not self.has_spatialite: - self.has_spatialite = self.init_spatialite() - - def con_info(self): - return str(self.dbname) - - def init_spatialite(self): - # Get SpatiaLite version - c = self.con.cursor() - try: - self._exec_sql(c, u'SELECT spatialite_version()') - rep = c.fetchall() - v = [int(x) if x.isdigit() else x for x in re.findall(r"\d+|[a-zA-Z]+", rep[0][0])] - - # Add SpatiaLite support - if v >= [4, 1, 0]: - # 4.1 and above - sql = "SELECT initspatialmetadata(1)" - else: - # Under 4.1 - sql = "SELECT initspatialmetadata()" - self._exec_sql_and_commit(sql) - except: - return False - finally: - self.con.close() - - try: - self.con = spatialite_connect(self.con_info()) - - except (sqlite.InterfaceError, sqlite.OperationalError) as e: - raise DbError(str(e)) - - return self.check_spatialite() - - def check_spatialite(self): - try: - c = self.con.cursor() - self._exec_sql(c, u"SELECT CheckSpatialMetaData()") - v = c.fetchone()[0] - self.has_geometry_columns = v == 1 or v == 3 - self.has_spatialite4 = v == 3 - except Exception: - self.has_geometry_columns = False - self.has_spatialite4 = False - - self.has_geometry_columns_access = self.has_geometry_columns - return self.has_geometry_columns - - def _exec_sql(self, cursor, sql): - try: - cursor.execute(sql) - except (sqlite.Error, sqlite.ProgrammingError, sqlite.Warning, sqlite.InterfaceError, sqlite.OperationalError) as e: - raise DbError(str(e), sql) - - def _exec_sql_and_commit(self, sql): - """Tries to execute and commit some action, on error it rolls - back the change. - """ - - try: - c = self.con.cursor() - self._exec_sql(c, sql) - self.con.commit() - except DbError: - self.con.rollback() - raise - - def create_spatial_index(self, table, geom_column='the_geom'): - sql = u"SELECT CreateSpatialIndex(%s, %s)" % (self._quote(table), self._quote(geom_column)) - self._exec_sql_and_commit(sql) - - def _quote(self, identifier): - """Quote identifier.""" - - # quote identifier, and double the double-quotes - return u"'%s'" % identifier.replace("'", "''") diff --git a/python/plugins/processing/ui/DlgAlgorithmBase.ui b/python/plugins/processing/ui/DlgAlgorithmBase.ui deleted file mode 100644 index 282f5619cc17..000000000000 --- a/python/plugins/processing/ui/DlgAlgorithmBase.ui +++ /dev/null @@ -1,151 +0,0 @@ - - - Dialog - - - - 0 - 0 - 685 - 525 - - - - Dialog - - - - - - - 0 - 0 - - - - Qt::Horizontal - - - 16 - - - - - 2 - 0 - - - - 0 - - - - Parameters - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - Log - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - true - - - - - - - - - - 0 - 0 - - - - - - - false - - - - - - - - - - - - - - - 0 - - - - - 0 - - - - - - - Cancel - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Close|QDialogButtonBox::Help|QDialogButtonBox::Ok - - - - - - - - diff --git a/python/plugins/processing/ui/DlgNumberInput.ui b/python/plugins/processing/ui/DlgNumberInput.ui deleted file mode 100644 index 1f284381393d..000000000000 --- a/python/plugins/processing/ui/DlgNumberInput.ui +++ /dev/null @@ -1,105 +0,0 @@ - - - DlgNumberInput - - - - 0 - 0 - 445 - 300 - - - - Enter number or expression - - - - 6 - - - 9 - - - - - <html><head/><body><p>Enter expression in the text field. Double-click on elements in the tree to add their values to the expression.</p></body></html> - - - true - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Warning</span>: if expression result is float value, but integer required, result will be rounded to integer.</p></body></html> - - - true - - - - - - - false - - - - 1 - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - accepted() - DlgNumberInput - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - DlgNumberInput - reject() - - - 316 - 260 - - - 286 - 274 - - - - - diff --git a/python/plugins/processing/ui/DlgPostgisTableSelector.ui b/python/plugins/processing/ui/DlgPostgisTableSelector.ui deleted file mode 100644 index 577e60af6230..000000000000 --- a/python/plugins/processing/ui/DlgPostgisTableSelector.ui +++ /dev/null @@ -1,64 +0,0 @@ - - - Dialog - - - - 0 - 0 - 464 - 395 - - - - output table - - - - - - Select connection and schema - - - - - - - false - - - - 1 - - - - - - - - - - Table name - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - diff --git a/python/plugins/processing/ui/widgetBaseSelector.ui b/python/plugins/processing/ui/widgetBaseSelector.ui index be82e5801e6c..1cac30036eef 100644 --- a/python/plugins/processing/ui/widgetBaseSelector.ui +++ b/python/plugins/processing/ui/widgetBaseSelector.ui @@ -7,7 +7,7 @@ 0 0 249 - 23 + 27 @@ -17,7 +17,16 @@ 6 - + + 0 + + + 0 + + + 0 + + 0 diff --git a/python/plugins/processing/ui/widgetGeometryPredicateSelector.ui b/python/plugins/processing/ui/widgetGeometryPredicateSelector.ui deleted file mode 100644 index a9eb59a8699f..000000000000 --- a/python/plugins/processing/ui/widgetGeometryPredicateSelector.ui +++ /dev/null @@ -1,80 +0,0 @@ - - - Form - - - - 0 - 0 - 609 - 213 - - - - Form - - - - 0 - - - - - equals - - - - - - - contains - - - - - - - touches - - - - - - - intersects - - - - - - - within - - - - - - - overlaps - - - - - - - crosses - - - - - - - disjoint - - - - - - - - diff --git a/python/plugins/processing/ui/widgetLayerSelector.ui b/python/plugins/processing/ui/widgetLayerSelector.ui deleted file mode 100644 index 0b5432010f2d..000000000000 --- a/python/plugins/processing/ui/widgetLayerSelector.ui +++ /dev/null @@ -1,69 +0,0 @@ - - - Form - - - - 0 - 0 - 608 - 20 - - - - Form - - - - 2 - - - 0 - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - - - - - - - - 0 - 1 - - - - Iterate over this layer - - - - - - true - - - - - - - - diff --git a/python/processing/algfactory.py b/python/processing/algfactory.py index 58fa7d1e1fe2..14fda409de26 100644 --- a/python/processing/algfactory.py +++ b/python/processing/algfactory.py @@ -59,6 +59,11 @@ QgsProcessingParameterLayout, QgsProcessingParameterLayoutItem, QgsProcessingParameterDateTime, + QgsProcessingParameterMapTheme, + QgsProcessingParameterProviderConnection, + QgsProcessingParameterDatabaseSchema, + QgsProcessingParameterDatabaseTable, + QgsProcessingParameterCoordinateOperation, QgsProcessingOutputString, QgsProcessingOutputBoolean, QgsProcessingOutputFile, @@ -328,6 +333,11 @@ class ProcessingAlgFactory(): LAYOUT = "LAYOUT" LAYOUT_ITEM = "LAYOUT_ITEM" DATETIME = "DATETIME" + MAP_THEME = "MAP_THEME" + PROVIDER_CONNECTION = "PROVIDER_CONNECTION" + DATABASE_SCHEMA = "DATABASE_SCHEMA" + DATABASE_TABLE = "DATABASE_TABLE" + COORDINATE_OPERATION = "COORDINATE_OPERATION" def __init__(self): self._current = None @@ -461,6 +471,11 @@ def input(self, type, *args, **kwargs): alg.LAYOUT_ITEM: QgsProcessingParameterLayoutItem alg.COLOR: QgsProcessingParameterColor alg.DATETIME: QgsProcessingParameterDateTime + alg.MAP_THEME: QgsProcessingParameterMapTheme + alg.PROVIDER_CONNECTION: QgsProcessingParameterProviderConnection + alg.DATABASE_SCHEMA: QgsProcessingParameterDatabaseSchema + alg.DATABASE_TABLE: QgsProcessingParameterDatabaseTable + alg.COORDINATE_OPERATION: QgsProcessingParameterCoordinateOperation :param type: The type of the input. This should be a type define on `alg` like alg.STRING, alg.DISTANCE :keyword label: The label of the output. Translates into `description` arg. @@ -512,7 +527,12 @@ def dec(f): ProcessingAlgFactory.LAYOUT: QgsProcessingParameterLayout, ProcessingAlgFactory.LAYOUT_ITEM: QgsProcessingParameterLayoutItem, ProcessingAlgFactory.COLOR: QgsProcessingParameterColor, - ProcessingAlgFactory.DATETIME: QgsProcessingParameterDateTime + ProcessingAlgFactory.DATETIME: QgsProcessingParameterDateTime, + ProcessingAlgFactory.MAP_THEME: QgsProcessingParameterMapTheme, + ProcessingAlgFactory.PROVIDER_CONNECTION: QgsProcessingParameterProviderConnection, + ProcessingAlgFactory.DATABASE_SCHEMA: QgsProcessingParameterDatabaseSchema, + ProcessingAlgFactory.DATABASE_TABLE: QgsProcessingParameterDatabaseTable, + ProcessingAlgFactory.COORDINATE_OPERATION: QgsProcessingParameterCoordinateOperation } output_type_mapping = { diff --git a/python/pyplugin_installer/installer.py b/python/pyplugin_installer/installer.py index 0b0ac08c6b24..0e50e976117c 100644 --- a/python/pyplugin_installer/installer.py +++ b/python/pyplugin_installer/installer.py @@ -220,14 +220,25 @@ def exportPluginsToManager(self): "installed": plugin["installed"] and "true" or "false", "available": plugin["available"] and "true" or "false", "status": plugin["status"], + "status_exp": plugin["status_exp"], "error": plugin["error"], "error_details": plugin["error_details"], + "create_date": plugin["create_date"], + "update_date": plugin["update_date"], + "create_date_stable": plugin["create_date_stable"], + "update_date_stable": plugin["update_date_stable"], + "create_date_experimental": plugin["create_date_experimental"], + "update_date_experimental": plugin["update_date_experimental"], "experimental": plugin["experimental"] and "true" or "false", "deprecated": plugin["deprecated"] and "true" or "false", "trusted": plugin["trusted"] and "true" or "false", "version_available": plugin["version_available"], + "version_available_stable": plugin["version_available_stable"] or "", + "version_available_experimental": plugin["version_available_experimental"] or "", "zip_repository": plugin["zip_repository"], "download_url": plugin["download_url"], + "download_url_stable": plugin["download_url_stable"], + "download_url_experimental": plugin["download_url_experimental"], "filename": plugin["filename"], "downloads": plugin["downloads"], "average_vote": plugin["average_vote"], @@ -282,19 +293,20 @@ def upgradeAllUpgradeable(self): self.installPlugin(key, quiet=True) # ----------------------------------------- # - def installPlugin(self, key, quiet=False): + def installPlugin(self, key, quiet=False, stable=True): """ Install given plugin """ error = False + status_key = 'status' if stable else 'status_exp' infoString = ('', '') plugin = plugins.all()[key] - previousStatus = plugin["status"] + previousStatus = plugin[status_key] if not plugin: return - if plugin["status"] == "newer" and not plugin["error"]: # ask for confirmation if user downgrades an usable plugin + if plugin[status_key] == "newer" and not plugin["error"]: # ask for confirmation if user downgrades an usable plugin if QMessageBox.warning(iface.mainWindow(), self.tr("QGIS Python Plugin Installer"), self.tr("Are you sure you want to downgrade the plugin to the latest available version? The installed one is newer!"), QMessageBox.Yes, QMessageBox.No) == QMessageBox.No: return - dlg = QgsPluginInstallerInstallingDialog(iface.mainWindow(), plugin) + dlg = QgsPluginInstallerInstallingDialog(iface.mainWindow(), plugin, stable=stable) dlg.exec_() if dlg.result(): diff --git a/python/pyplugin_installer/installer_data.py b/python/pyplugin_installer/installer_data.py index 2c04973399b4..a6f2df9c12a4 100644 --- a/python/pyplugin_installer/installer_data.py +++ b/python/pyplugin_installer/installer_data.py @@ -85,6 +85,8 @@ "downloads" unicode, # number of downloads "average_vote" unicode, # average vote "rating_votes" unicode, # number of votes + "create_date" unicode, # ISO datetime when the plugin has been created + "update_date" unicode, # ISO datetime when the plugin has been last updated "plugin_dependencies" unicode, # PIP-style comma separated list of plugin dependencies }} """ @@ -418,16 +420,23 @@ def xmlDownloaded(self): else: plugin_id = None + version = pluginNodes.item(i).toElement().attribute("version") + download_url = pluginNodes.item(i).firstChildElement("download_url").text().strip() + plugin = { "id": name, "plugin_id": plugin_id, "name": pluginNodes.item(i).toElement().attribute("name"), - "version_available": pluginNodes.item(i).toElement().attribute("version"), + "version_available": version, + "version_available_stable": version if not experimental else "", + "version_available_experimental": version if experimental else "", "description": pluginNodes.item(i).firstChildElement("description").text().strip(), "about": pluginNodes.item(i).firstChildElement("about").text().strip(), "author_name": pluginNodes.item(i).firstChildElement("author_name").text().strip(), "homepage": pluginNodes.item(i).firstChildElement("homepage").text().strip(), - "download_url": pluginNodes.item(i).firstChildElement("download_url").text().strip(), + "download_url": download_url, + "download_url_stable": download_url if not experimental else "", + "download_url_experimental": download_url if experimental else "", "category": pluginNodes.item(i).firstChildElement("category").text().strip(), "tags": pluginNodes.item(i).firstChildElement("tags").text().strip(), "changelog": pluginNodes.item(i).firstChildElement("changelog").text().strip(), @@ -437,6 +446,12 @@ def xmlDownloaded(self): "downloads": pluginNodes.item(i).firstChildElement("downloads").text().strip(), "average_vote": pluginNodes.item(i).firstChildElement("average_vote").text().strip(), "rating_votes": pluginNodes.item(i).firstChildElement("rating_votes").text().strip(), + "create_date": pluginNodes.item(i).firstChildElement("create_date").text().strip(), + "update_date": pluginNodes.item(i).firstChildElement("update_date").text().strip(), + "create_date_stable": pluginNodes.item(i).firstChildElement("create_date").text().strip() if not experimental else "", + "update_date_stable": pluginNodes.item(i).firstChildElement("update_date").text().strip() if not experimental else "", + "create_date_experimental": pluginNodes.item(i).firstChildElement("create_date").text().strip() if experimental else "", + "update_date_experimental": pluginNodes.item(i).firstChildElement("update_date").text().strip() if experimental else "", "icon": icon, "experimental": experimental, "deprecated": deprecated, @@ -445,6 +460,7 @@ def xmlDownloaded(self): "installed": False, "available": True, "status": "not installed", + "status_exp": "not installed", "error": "", "error_details": "", "version_installed": "", @@ -668,15 +684,26 @@ def pluginMetadata(fct): "deprecated": pluginMetadata("deprecated").strip().upper() in ["TRUE", "YES"], "trusted": False, "version_available": "", + "version_available_stable": "", + "version_available_experimental": "", "zip_repository": "", "download_url": path, # warning: local path as url! + "download_url_stable": "", + "download_url_experimental": "", "filename": "", "downloads": "", "average_vote": "", "rating_votes": "", + "create_date": pluginMetadata("create_date"), + "update_date": pluginMetadata("update_date"), + "create_date_stable": pluginMetadata("create_date_stable"), + "update_date_stable": pluginMetadata("update_date_stable"), + "create_date_experimental": pluginMetadata("create_date_experimental"), + "update_date_experimental": pluginMetadata("update_date_experimental"), "available": False, # Will be overwritten, if any available version found. "installed": True, "status": "orphan", # Will be overwritten, if any available version found. + "status_exp": "orphan", # Will be overwritten, if any available version found. "error": error, "error_details": errorDetails, "readonly": readOnly, @@ -736,7 +763,12 @@ def rebuild(self): # check if the plugin is allowed and if there isn't any better one added already. if (allowExperimental or not plugin["experimental"]) \ and (allowDeprecated or not plugin["deprecated"]) \ - and not (key in self.mPlugins and self.mPlugins[key]["version_available"] and compareVersions(self.mPlugins[key]["version_available"], plugin["version_available"]) < 2): + and not ( + key in self.mPlugins and self.mPlugins[key]["version_available"] + and compareVersions(self.mPlugins[key]["version_available"], plugin["version_available"]) < 2 + and self.mPlugins[key]["experimental"] and not plugin["experimental"] + ): + # The mPlugins dict contains now locally installed plugins. # Now, add the available one if not present yet or update it if present already. if key not in self.mPlugins: @@ -753,36 +785,67 @@ def rebuild(self): # other remote metadata is preferred: for attrib in ["name", "plugin_id", "description", "about", "category", "tags", "changelog", "author_name", "author_email", "homepage", "tracker", "code_repository", "experimental", "deprecated", "version_available", "zip_repository", - "download_url", "filename", "downloads", "average_vote", "rating_votes", "trusted", "plugin_dependencies"]: + "download_url", "filename", "downloads", "average_vote", "rating_votes", "trusted", "plugin_dependencies", + "version_available_stable", "version_available_experimental", "download_url_stable", "download_url_experimental", + "create_date", "update_date", "create_date_stable", "update_date_stable", "create_date_experimental", "update_date_experimental"]: if attrib not in translatableAttributes or attrib == "name": # include name! if plugin.get(attrib, False): self.mPlugins[key][attrib] = plugin[attrib] + + # If the stable version is higher than the experimental version, we ignore the experimental version + if compareVersions(self.mPlugins[key]["version_available_stable"], self.mPlugins[key]["version_available_experimental"]) == 1: + self.mPlugins[key]["version_available_experimental"] = '' + # set status # # installed available status # --------------------------------------- # none any "not installed" (will be later checked if is "new") - # any none "orphan" + # no none "none available" + # yes none "orphan" # same same "installed" # less greater "upgradeable" # greater less "newer" - if not self.mPlugins[key]["version_available"]: + if not self.mPlugins[key]["version_available_stable"] and not self.mPlugins[key]["version_installed"]: + self.mPlugins[key]["status"] = "none available" + elif not self.mPlugins[key]["version_available_stable"] and self.mPlugins[key]["version_installed"]: self.mPlugins[key]["status"] = "orphan" elif not self.mPlugins[key]["version_installed"]: self.mPlugins[key]["status"] = "not installed" elif self.mPlugins[key]["version_installed"] in ["?", "-1"]: self.mPlugins[key]["status"] = "installed" - elif compareVersions(self.mPlugins[key]["version_available"], self.mPlugins[key]["version_installed"]) == 0: + elif compareVersions(self.mPlugins[key]["version_available_stable"], self.mPlugins[key]["version_installed"]) == 0: self.mPlugins[key]["status"] = "installed" - elif compareVersions(self.mPlugins[key]["version_available"], self.mPlugins[key]["version_installed"]) == 1: + elif compareVersions(self.mPlugins[key]["version_available_stable"], self.mPlugins[key]["version_installed"]) == 1: self.mPlugins[key]["status"] = "upgradeable" else: self.mPlugins[key]["status"] = "newer" # debug: test if the status match the "installed" tag: - if self.mPlugins[key]["status"] in ["not installed"] and self.mPlugins[key]["installed"]: - raise Exception("Error: plugin status is ambiguous (1)") + if self.mPlugins[key]["status"] in ["not installed", "none available"] and self.mPlugins[key]["installed"]: + raise Exception("Error: plugin status is ambiguous (1) for plugin {}".format(key)) if self.mPlugins[key]["status"] in ["installed", "orphan", "upgradeable", "newer"] and not self.mPlugins[key]["installed"]: - raise Exception("Error: plugin status is ambiguous (2)") + raise Exception("Error: plugin status is ambiguous (2) for plugin {}".format(key)) + + if not self.mPlugins[key]["version_available_experimental"] and not self.mPlugins[key]["version_installed"]: + self.mPlugins[key]["status_exp"] = "none available" + elif not self.mPlugins[key]["version_available_experimental"] and self.mPlugins[key]["version_installed"]: + self.mPlugins[key]["status_exp"] = "orphan" + elif not self.mPlugins[key]["version_installed"]: + self.mPlugins[key]["status_exp"] = "not installed" + elif self.mPlugins[key]["version_installed"] in ["?", "-1"]: + self.mPlugins[key]["status_exp"] = "installed" + elif compareVersions(self.mPlugins[key]["version_available_experimental"], self.mPlugins[key]["version_installed"]) == 0: + self.mPlugins[key]["status_exp"] = "installed" + elif compareVersions(self.mPlugins[key]["version_available_experimental"], self.mPlugins[key]["version_installed"]) == 1: + self.mPlugins[key]["status_exp"] = "upgradeable" + else: + self.mPlugins[key]["status_exp"] = "newer" + # debug: test if the status_exp match the "installed" tag: + if self.mPlugins[key]["status_exp"] in ["not installed", "none available"] and self.mPlugins[key]["installed"]: + raise Exception("Error: plugin status_exp is ambiguous (1) for plugin {}".format(key)) + if self.mPlugins[key]["status_exp"] in ["installed", "orphan", "upgradeable", "newer"] and not self.mPlugins[key]["installed"]: + raise Exception("Error: plugin status_exp is ambiguous (2) for plugin {} (status_exp={})".format(key, self.mPlugins[key]["status_exp"])) + self.markNews() # ----------------------------------------- # diff --git a/python/pyplugin_installer/qgsplugininstallerinstallingdialog.py b/python/pyplugin_installer/qgsplugininstallerinstallingdialog.py index 212c8be22807..1b4dc7552b4d 100644 --- a/python/pyplugin_installer/qgsplugininstallerinstallingdialog.py +++ b/python/pyplugin_installer/qgsplugininstallerinstallingdialog.py @@ -40,7 +40,7 @@ class QgsPluginInstallerInstallingDialog(QDialog, Ui_QgsPluginInstallerInstallingDialogBase): # ----------------------------------------- # - def __init__(self, parent, plugin): + def __init__(self, parent, plugin, stable=True): QDialog.__init__(self, parent) self.setupUi(self) self.plugin = plugin @@ -50,7 +50,7 @@ def __init__(self, parent, plugin): self.labelName.setText(plugin["name"]) self.buttonBox.clicked.connect(self.abort) - self.url = QUrl(plugin["download_url"]) + self.url = QUrl(plugin["download_url_stable"] if stable else plugin["download_url_experimental"]) self.redirectionCounter = 0 fileName = plugin["filename"] diff --git a/resources/function_help/json/display_expression b/resources/function_help/json/display_expression new file mode 100644 index 000000000000..0c612ad7e35c --- /dev/null +++ b/resources/function_help/json/display_expression @@ -0,0 +1,39 @@ +{ + "name": "display_expression", + "type": "function", + "description": "Returns the display expression for a given feature in a layer. If called with no parameters, it evaluates the current feature. The expression is evaluated by default.", + "arguments": [ + { + "arg": "feature", + "optional": true, + "default": "current feature", + "description": "The feature which should be evaluated." + }, + { + "arg": "layer", + "optional": true, + "default": "current layer", + "description": "The layer (or its id or name)." + }, + { + "arg": "evaluate", + "description": "If the expression must be evaluated. If false, the expression will be returned as a string literal only (which could potentially be later evaluated using the 'eval' function).", + "optional": true, + "default": "true" + } + ], + "examples": [ + { + "expression": "display_expression()", + "returns": "The display expression of the current feature." + }, + { + "expression": "display_expression($currentfeature)", + "returns": "The display expression for a given feature." + }, + { + "expression": "display_expression('a_layer_id', $currentfeature, 'False')", + "returns": "The display expression of the current feature not evaluated." + } + ] +} diff --git a/resources/function_help/json/eval_template b/resources/function_help/json/eval_template new file mode 100644 index 000000000000..0fc55f2c35d5 --- /dev/null +++ b/resources/function_help/json/eval_template @@ -0,0 +1,13 @@ +{ + "name": "eval_template", + "type": "function", + "description": "Evaluates a template which is passed in a string. Useful to expand dynamic parameters passed as context variables or fields.", + "arguments": [{ + "arg": "template", + "description": "a template string" + }], + "examples": [{ + "expression": "eval_template('QGIS [% upper(\\\\'rocks\\\\') %]')", + "returns": "QGIS ROCKS" + }] +} diff --git a/resources/function_help/json/layer_property b/resources/function_help/json/layer_property index df547bdde6ae..fbc4810f56d7 100644 --- a/resources/function_help/json/layer_property +++ b/resources/function_help/json/layer_property @@ -4,7 +4,7 @@ "description": "Returns a matching layer property or metadata value.", "arguments": [ {"arg":"layer", "description":"a string, representing either a layer name or layer ID"}, - {"arg":"property", "description":"a string corresponding to the property to return. Valid options are:
    • name: layer name
    • id: layer ID
    • title: metadata title string
    • abstract: metadata abstract string
    • keywords: metadata keywords
    • data_url: metadata URL
    • attribution: metadata attribution string
    • attribution_url: metadata attribution URL
    • source: layer source
    • min_scale: minimum display scale for layer
    • max_scale: maximum display scale for layer
    • crs: layer CRS
    • crs_definition: layer CRS full definition
    • crs_description: layer CRS description
    • extent: layer extent (as a geometry object)
    • type: layer type, e.g., Vector or Raster
    • storage_type: storage format (vector layers only)
    • geometry_type: geometry type, e.g., Point (vector layers only)
    • feature_count: approximate feature count for layer (vector layers only)
    • path: File path to the layer data source. Only available for file based layers.
    "} + {"arg":"property", "description":"a string corresponding to the property to return. Valid options are:
    • name: layer name
    • id: layer ID
    • title: metadata title string
    • abstract: metadata abstract string
    • keywords: metadata keywords
    • data_url: metadata URL
    • attribution: metadata attribution string
    • attribution_url: metadata attribution URL
    • source: layer source
    • min_scale: minimum display scale for layer
    • max_scale: maximum display scale for layer
    • is_editable: if layer is in edit mode
    • crs: layer CRS
    • crs_definition: layer CRS full definition
    • crs_description: layer CRS description
    • extent: layer extent (as a geometry object)
    • distance_units: layer distance units
    • type: layer type, e.g., Vector or Raster
    • storage_type: storage format (vector layers only)
    • geometry_type: geometry type, e.g., Point (vector layers only)
    • feature_count: approximate feature count for layer (vector layers only)
    • path: File path to the layer data source. Only available for file based layers.
    "} ], "examples": [ { "expression":"layer_property('streets','title')", "returns":"'Basemap Streets'"}, diff --git a/resources/function_help/json/maptip b/resources/function_help/json/maptip new file mode 100644 index 000000000000..13011c128191 --- /dev/null +++ b/resources/function_help/json/maptip @@ -0,0 +1,39 @@ +{ + "name": "maptip", + "type": "function", + "description": "Returns the maptip for a given feature in a layer. If called with no parameters, it evaluates the current feature. The maptip is evaluated by default.", + "arguments": [ + { + "arg": "feature", + "optional": true, + "default": "current feature", + "description": "The feature which should be evaluated." + }, + { + "arg": "layer", + "optional": true, + "default": "current layer", + "description": "The layer (or its id or name)." + }, + { + "arg": "evaluate", + "description": "If the expression must be evaluated. If false, the expression will be returned as a string literal only (which could potentially be later evaluated using the 'eval_template' function).", + "optional": true, + "default": "true" + } + ], + "examples": [ + { + "expression": "maptip()", + "returns": "The maptip of the current feature." + }, + { + "expression": "maptip($currentFeature)", + "returns": "The maptip of the current feature." + }, + { + "expression": "maptip('a_layer_id', $currentFeature, 'False')", + "returns": "The maptip of the current feature not evaluated." + } + ] +} diff --git a/resources/themes/Blend of Gray/style.qss b/resources/themes/Blend of Gray/style.qss index d5cf9b366971..c5b844d8fc65 100644 --- a/resources/themes/Blend of Gray/style.qss +++ b/resources/themes/Blend of Gray/style.qss @@ -241,6 +241,10 @@ QToolButton:checked { border: 1px solid @selection; } +QToolButton:checked:disabled +{ + border: 1px solid rgba(240,240,240,100); +} QToolButton::menu-arrow { image: url(@theme_path/icons/arrow-down.svg); diff --git a/resources/themes/Night Mapping/style.qss b/resources/themes/Night Mapping/style.qss index 9ba96a00a575..3269ab1496cf 100644 --- a/resources/themes/Night Mapping/style.qss +++ b/resources/themes/Night Mapping/style.qss @@ -251,6 +251,10 @@ QToolButton:checked { border: 1px solid @focus; } +QToolButton:checked:disabled +{ + border: 1px solid rgba(215,128,26,100); +} QToolButton::menu-arrow { image: url(@theme_path/icons/arrow-down.svg); diff --git a/rpm/qgis.spec.template b/rpm/qgis.spec.template index 6e6086b9f9bc..be74ad0e1200 100644 --- a/rpm/qgis.spec.template +++ b/rpm/qgis.spec.template @@ -87,6 +87,9 @@ BuildRequires: sqlite-devel BuildRequires: hdf5-devel BuildRequires: netcdf-devel BuildRequires: fcgi-devel +BuildRequires: protobuf-compiler +BuildRequires: protobuf-devel +BuildRequires: protobuf-lite-devel # OpenCL BuildRequires: opencl-headers @@ -99,7 +102,6 @@ BuildRequires: python3-OWSLib BuildRequires: python3-psycopg2 BuildRequires: python3-pygments BuildRequires: python3-PyYAML -BuildRequires: python3-qscintilla-devel BuildRequires: python3-qscintilla-qt5 BuildRequires: python3-qscintilla-qt5-devel BuildRequires: python3-qt5-devel @@ -197,7 +199,6 @@ Requires: python3-OWSLib Requires: python3-psycopg2 Requires: python3-pygments Requires: python3-PyYAML -Requires: python3-qscintilla Requires: python3-qscintilla-qt5 Requires: python3-qt5 %{?_sip_api:Requires: python3-pyqt5-sip-api(%{_sip_api_major}) >= %{_sip_api}} diff --git a/scripts/astyle.sh b/scripts/astyle.sh index 7217f9acee60..29565ee2eff5 100755 --- a/scripts/astyle.sh +++ b/scripts/astyle.sh @@ -105,7 +105,7 @@ astyleit() { for f in "$@"; do case "$f" in - src/plugins/grass/qtermwidget/*|external/o2/*|external/qt-unix-signals/*|external/rtree/*|external/astyle/*|external/kdbush/*|external/poly2tri/*|external/wintoast/*|external/qt3dextra-headers/*|external/meshOptimizer/*|python/ext-libs/*|ui_*.py|*.astyle|tests/testdata/*|editors/*) + src/plugins/grass/qtermwidget/*|external/o2/*|external/qt-unix-signals/*|external/rtree/*|external/astyle/*|external/kdbush/*|external/poly2tri/*|external/wintoast/*|external/qt3dextra-headers/*|external/meshOptimizer/*|external/mapbox-vector-tile/*|python/ext-libs/*|ui_*.py|*.astyle|tests/testdata/*|editors/*) echo -ne "$f skipped $elcr" continue ;; diff --git a/scripts/customwidget.cpp.template b/scripts/customwidget.cpp.template index a96740828310..504eb95f79db 100644 --- a/scripts/customwidget.cpp.template +++ b/scripts/customwidget.cpp.template @@ -19,8 +19,8 @@ %CLASSMIXEDCASE%Plugin::%CLASSMIXEDCASE%Plugin( QObject *parent ) - : QObject( parent ) - , mInitialized( false ) + : QObject( parent ) + , mInitialized( false ) { } @@ -61,7 +61,7 @@ bool %CLASSMIXEDCASE%Plugin::isInitialized() const void %CLASSMIXEDCASE%Plugin::initialize( QDesignerFormEditorInterface *core ) { - Q_UNUSED( core ); + Q_UNUSED( core ) if ( mInitialized ) return; mInitialized = true; @@ -81,7 +81,7 @@ QString %CLASSMIXEDCASE%Plugin::whatsThis() const QString %CLASSMIXEDCASE%Plugin::domXml() const { return QString( "\n" - " \n" + " \n" " \n" " \n" " 0\n" diff --git a/scripts/customwidget.h.template b/scripts/customwidget.h.template index 75becbaedaed..9b33b557d129 100644 --- a/scripts/customwidget.h.template +++ b/scripts/customwidget.h.template @@ -20,6 +20,7 @@ #include #include #include +#include "qgis_customwidgets.h" class CUSTOMWIDGETS_EXPORT %CLASSMIXEDCASE%Plugin : public QObject, public QDesignerCustomWidgetInterface diff --git a/scripts/customwidget_create.sh b/scripts/customwidget_create.sh index 282af0641ef0..4c18fccbdf9f 100755 --- a/scripts/customwidget_create.sh +++ b/scripts/customwidget_create.sh @@ -9,6 +9,12 @@ set -e +# GNU prefix command for mac os support (gsed, gsplit) +GP= +if [[ "$OSTYPE" =~ darwin* ]]; then + GP=g +fi + CLASSNAME=$1 TODAY=$(date '+%d.%m.%Y') @@ -19,6 +25,7 @@ EMAIL=$(git config user.email) CLASSUPPER="${CLASSNAME^^}" CLASSLOWER="${CLASSNAME,,}" +CLASSWITHOUTQGS=$(${GP}sed 's/^Qgs//' <<< ${CLASSNAME}) DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" @@ -27,13 +34,12 @@ for i in "${EXT[@]}" do DESTFILE=$DIR/../src/customwidgets/${CLASSLOWER}plugin.$i cp "$DIR"/customwidget."$i".template "$DESTFILE" - sed -i s/%DATE%/"$TODAY"/g "$DESTFILE" - sed -i s/%YEAR%/"$YEAR"/g "$DESTFILE" - sed -i s/%AUTHOR%/"$AUTHOR"/g "$DESTFILE" - sed -i s/%EMAIL%/"$EMAIL"/g "$DESTFILE" - sed -i s/%CLASSUPPERCASE%/"$CLASSUPPER"/g "$DESTFILE" - sed -i s/%CLASSLOWERCASE%/"$CLASSLOWER"/g "$DESTFILE" - sed -i s/%CLASSMIXEDCASE%/"$CLASSNAME"/g "$DESTFILE" + ${GP}sed -i s/%DATE%/${TODAY}/g ${DESTFILE} + ${GP}sed -i s/%YEAR%/${YEAR}/g ${DESTFILE} + ${GP}sed -i s/%AUTHOR%/${AUTHOR}/g ${DESTFILE} + ${GP}sed -i s/%EMAIL%/${EMAIL}/g ${DESTFILE} + ${GP}sed -i s/%CLASSUPPERCASE%/${CLASSUPPER}/g ${DESTFILE} + ${GP}sed -i s/%CLASSLOWERCASE%/${CLASSLOWER}/g ${DESTFILE} + ${GP}sed -i s/%CLASSMIXEDCASE%/${CLASSNAME}/g ${DESTFILE} + ${GP}sed -i s/%CLASSWITHOUTQGS%/${CLASSWITHOUTQGS}/g ${DESTFILE} done - - diff --git a/scripts/sipify.pl b/scripts/sipify.pl index b08a19d0773a..4f90a44bc693 100755 --- a/scripts/sipify.pl +++ b/scripts/sipify.pl @@ -1210,8 +1210,8 @@ sub detect_non_method_member{ # multiline definition (parenthesis left open) if ( $MULTILINE_DEFINITION != MULTILINE_NO ){ dbg_info("on multiline"); - # https://regex101.com/r/DN01iM/2 - if ( $LINE =~ m/^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)[^()]*$/){ + # https://regex101.com/r/DN01iM/4 + if ( $LINE =~ m/^([^()]+(\((?:[^()]++|(?1))*\)))*[^()]*\)([^()](throw\([^()]+\))?)*$/){ dbg_info("ending multiline"); # remove potential following body if ( $MULTILINE_DEFINITION != MULTILINE_CONDITIONAL_STATEMENT && $LINE !~ m/(\{.*\}|;)\s*(\/\/.*)?$/ ){ diff --git a/scripts/spell_check/.agignore b/scripts/spell_check/.agignore index 5af581c846dc..e967542641f7 100644 --- a/scripts/spell_check/.agignore +++ b/scripts/spell_check/.agignore @@ -14,6 +14,7 @@ src/app/gps/qwtpolar-1.0/ src/app/gps/qwtpolar-1.1.1/ src/core/pal src/plugins/grass/qtermwidget/ +tests/testdata/provider/postgresraster/ #Extensions *.*.orig diff --git a/scripts/spell_check/spelling.dat b/scripts/spell_check/spelling.dat index 98b127a64fd3..20443d04cce4 100644 --- a/scripts/spell_check/spelling.dat +++ b/scripts/spell_check/spelling.dat @@ -4233,7 +4233,7 @@ kwno:know labatory:laboratory labelling:labeling lable:label -labled:labeled +labled:labeled:* lables:labels:* labour:labor labratory:laboratory diff --git a/src/3d/mesh/qgsmesh3dentity_p.cpp b/src/3d/mesh/qgsmesh3dentity_p.cpp index a1345dbe27a0..bd812f107311 100644 --- a/src/3d/mesh/qgsmesh3dentity_p.cpp +++ b/src/3d/mesh/qgsmesh3dentity_p.cpp @@ -54,7 +54,7 @@ void QgsMeshDataset3dEntity::buildGeometry() if ( !layer() ) return; - mesh->setGeometry( new QgsMeshDataset3dGeometry( layer(), mMapSettings.origin(), mSymbol, mesh ) ); + mesh->setGeometry( new QgsMeshDataset3dGeometry( layer(), mMapSettings.temporalRange(), mMapSettings.origin(), mSymbol, mesh ) ); addComponent( mesh ); } @@ -63,11 +63,11 @@ void QgsMeshDataset3dEntity::applyMaterial() if ( mSymbol.renderingStyle() == QgsMesh3DSymbol::ColorRamp2DRendering && layer() ) { const QgsMeshRendererSettings rendererSettings = layer()->rendererSettings(); - const QgsMeshDatasetIndex datasetIndex = rendererSettings.activeScalarDataset(); - if ( datasetIndex.isValid() ) - mSymbol.setColorRampShader( rendererSettings.scalarSettings( datasetIndex.group() ).colorRampShader() ); + int datasetGroupIndex = rendererSettings.activeScalarDatasetGroup(); + if ( datasetGroupIndex >= 0 ) + mSymbol.setColorRampShader( rendererSettings.scalarSettings( datasetGroupIndex ).colorRampShader() ); } - QgsMesh3dMaterial *material = new QgsMesh3dMaterial( layer(), mMapSettings.origin(), mSymbol, QgsMesh3dMaterial::ScalarDataSet ); + QgsMesh3dMaterial *material = new QgsMesh3dMaterial( layer(), mMapSettings.temporalRange(), mMapSettings.origin(), mSymbol, QgsMesh3dMaterial::ScalarDataSet ); addComponent( material ); } @@ -93,7 +93,11 @@ void QgsMesh3dTerrainTileEntity::buildGeometry() void QgsMesh3dTerrainTileEntity::applyMaterial() { - QgsMesh3dMaterial *material = new QgsMesh3dMaterial( layer(), mMapSettings.origin(), mSymbol, QgsMesh3dMaterial::ZValue ); + QgsMesh3dMaterial *material = new QgsMesh3dMaterial( + layer(), mMapSettings.temporalRange(), + mMapSettings.origin(), + mSymbol, + QgsMesh3dMaterial::ZValue ); addComponent( material ); } diff --git a/src/3d/mesh/qgsmesh3dgeometry_p.cpp b/src/3d/mesh/qgsmesh3dgeometry_p.cpp index 8830fdf83cfd..40050d047d2e 100644 --- a/src/3d/mesh/qgsmesh3dgeometry_p.cpp +++ b/src/3d/mesh/qgsmesh3dgeometry_p.cpp @@ -324,12 +324,14 @@ QgsMesh3dGeometry::QgsMesh3dGeometry( QgsMeshLayer *layer, QgsMeshDataset3dGeometry::QgsMeshDataset3dGeometry( QgsMeshLayer *layer, + const QgsDateTimeRange &timeRange, const QgsVector3D &origin, const QgsMesh3DSymbol &symbol, Qt3DCore::QNode *parent ): QgsMesh3dGeometry( layer, origin, symbol, parent ), mIsVerticalMagnitudeRelative( symbol.isVerticalMagnitudeRelative() ), - mVerticalGroupDatasetIndex( symbol.verticalDatasetGroupIndex() ) + mVerticalGroupDatasetIndex( symbol.verticalDatasetGroupIndex() ), + mTimeRange( timeRange ) { init(); } @@ -395,7 +397,8 @@ int QgsMeshDataset3dGeometry::extractDataset( QVector &verticalMagnitude if ( !layer ) return 0; - QgsMeshDatasetIndex scalarDatasetIndex = layer->rendererSettings().activeScalarDataset(); + QgsMeshDatasetIndex scalarDatasetIndex = layer->activeScalarDatasetAtTime( mTimeRange ); + if ( !scalarDatasetIndex.isValid() ) return 0; @@ -405,7 +408,6 @@ int QgsMeshDataset3dGeometry::extractDataset( QVector &verticalMagnitude QgsTriangularMesh triangularMesh = *layer->triangularMesh(); const QgsMesh nativeMesh = *layer->nativeMesh(); - //extract the scalar dataset used to render vertical magnitude of geometry //define the vertical magnitude datasetIndex int verticalDataSetIndexNumber = 0; diff --git a/src/3d/mesh/qgsmesh3dgeometry_p.h b/src/3d/mesh/qgsmesh3dgeometry_p.h index 472cb8073f9e..5c60c31db8f4 100644 --- a/src/3d/mesh/qgsmesh3dgeometry_p.h +++ b/src/3d/mesh/qgsmesh3dgeometry_p.h @@ -83,6 +83,7 @@ class QgsMeshDataset3dGeometry: public QgsMesh3dGeometry public: //! Constructs a mesh layer geometry from triangular mesh. explicit QgsMeshDataset3dGeometry( QgsMeshLayer *layer, + const QgsDateTimeRange &timeRange, const QgsVector3D &origin, const QgsMesh3DSymbol &symbol, QNode *parent ); @@ -96,6 +97,7 @@ class QgsMeshDataset3dGeometry: public QgsMesh3dGeometry bool mIsVerticalMagnitudeRelative; int mVerticalGroupDatasetIndex; + QgsDateTimeRange mTimeRange; }; diff --git a/src/3d/mesh/qgsmesh3dmaterial_p.cpp b/src/3d/mesh/qgsmesh3dmaterial_p.cpp index 0c60cd95a05f..e75fe2f419b5 100644 --- a/src/3d/mesh/qgsmesh3dmaterial_p.cpp +++ b/src/3d/mesh/qgsmesh3dmaterial_p.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -230,7 +231,11 @@ class ArrowsGridTexture: public Qt3DRender::QAbstractTextureImage }; -QgsMesh3dMaterial::QgsMesh3dMaterial( QgsMeshLayer *layer, const QgsVector3D &origin, const QgsMesh3DSymbol &symbol, MagnitudeType magnitudeType ): +QgsMesh3dMaterial::QgsMesh3dMaterial( QgsMeshLayer *layer, + const QgsDateTimeRange &timeRange, + const QgsVector3D &origin, + const QgsMesh3DSymbol &symbol, + MagnitudeType magnitudeType ): mSymbol( symbol ), mMagnitudeType( magnitudeType ), mOrigin( origin ) @@ -238,7 +243,7 @@ QgsMesh3dMaterial::QgsMesh3dMaterial( QgsMeshLayer *layer, const QgsVector3D &or Qt3DRender::QEffect *eff = new Qt3DRender::QEffect( this ); configure(); - configureArrows( layer ); + configureArrows( layer, timeRange ); eff->addTechnique( mTechnique ); setEffect( eff ); @@ -309,12 +314,12 @@ void QgsMesh3dMaterial::configure() mTechnique->addParameter( new Qt3DRender::QParameter( "isScalarMagnitude", ( mMagnitudeType == QgsMesh3dMaterial::ScalarDataSet ) ) ); } -void QgsMesh3dMaterial::configureArrows( QgsMeshLayer *layer ) +void QgsMesh3dMaterial::configureArrows( QgsMeshLayer *layer, const QgsDateTimeRange &timeRange ) { if ( !layer ) return; - QgsMeshDatasetIndex datasetIndex = layer->rendererSettings().activeVectorDataset(); + QgsMeshDatasetIndex datasetIndex = layer->activeVectorDatasetAtTime( timeRange ); mTechnique->addParameter( new Qt3DRender::QParameter( "arrowsSpacing", float( mSymbol.arrowsSpacing() ) ) ) ; QColor arrowsColor = layer->rendererSettings().vectorSettings( datasetIndex.group() ).color(); @@ -326,7 +331,7 @@ void QgsMesh3dMaterial::configureArrows( QgsMeshLayer *layer ) QSize gridSize; QgsPointXY minCorner; Qt3DRender::QParameter *arrowsEnabledParameter = new Qt3DRender::QParameter( "arrowsEnabled", false ); - if ( mMagnitudeType != MagnitudeType::ScalarDataSet || !mSymbol.arrowsEnabled() ) + if ( mMagnitudeType != MagnitudeType::ScalarDataSet || !mSymbol.arrowsEnabled() || meta.isScalar() || !datasetIndex.isValid() ) arrowsEnabledParameter->setValue( false ); else { diff --git a/src/3d/mesh/qgsmesh3dmaterial_p.h b/src/3d/mesh/qgsmesh3dmaterial_p.h index 0ba8226c117a..dda571f06b54 100644 --- a/src/3d/mesh/qgsmesh3dmaterial_p.h +++ b/src/3d/mesh/qgsmesh3dmaterial_p.h @@ -66,7 +66,11 @@ class QgsMesh3dMaterial : public Qt3DRender::QMaterial }; //! Constructor - QgsMesh3dMaterial( QgsMeshLayer *layer, const QgsVector3D &origin, const QgsMesh3DSymbol &symbol, MagnitudeType magnitudeType = ZValue ); + QgsMesh3dMaterial( QgsMeshLayer *layer, + const QgsDateTimeRange &timeRange, + const QgsVector3D &origin, + const QgsMesh3DSymbol &symbol, + MagnitudeType magnitudeType = ZValue ); private: QgsMesh3DSymbol mSymbol; @@ -75,7 +79,7 @@ class QgsMesh3dMaterial : public Qt3DRender::QMaterial QgsVector3D mOrigin; void configure(); - void configureArrows( QgsMeshLayer *layer ); + void configureArrows( QgsMeshLayer *layer, const QgsDateTimeRange &timeRange ); }; ///@endcond diff --git a/src/3d/mesh/qgsmeshterraingenerator.cpp b/src/3d/mesh/qgsmeshterraingenerator.cpp index ef3afe755811..bea257be8c97 100644 --- a/src/3d/mesh/qgsmeshterraingenerator.cpp +++ b/src/3d/mesh/qgsmeshterraingenerator.cpp @@ -60,7 +60,10 @@ QgsChunkLoader *QgsMeshTerrainGenerator::createChunkLoader( QgsChunkNode *node ) return new QgsMeshTerrainTileLoader( mTerrain, node, meshLayer(), symbol() ); } -float QgsMeshTerrainGenerator::rootChunkError( const Qgs3DMapSettings &map ) const {Q_UNUSED( map ); return 0;} +float QgsMeshTerrainGenerator::rootChunkError( const Qgs3DMapSettings & ) const +{ + return 0; +} void QgsMeshTerrainGenerator::rootChunkHeightRange( float &hMin, float &hMax ) const { @@ -77,7 +80,7 @@ void QgsMeshTerrainGenerator::rootChunkHeightRange( float &hMin, float &hMax ) c for ( int i = 0; i < triangularMesh->vertices().count(); ++i ) { - float zValue = triangularMesh->vertices().at( i ).z(); + float zValue = static_cast< float >( triangularMesh->vertices().at( i ).z() ); if ( min > zValue ) min = zValue; if ( max < zValue ) @@ -131,7 +134,7 @@ QgsRectangle QgsMeshTerrainGenerator::extent() const { extentInMap = terrainToMapTransform.transform( mLayer->extent() ); } - catch ( QgsCsException &e ) + catch ( QgsCsException & ) { extentInMap = mLayer->extent(); } diff --git a/src/3d/qgs3dmapscene.cpp b/src/3d/qgs3dmapscene.cpp index 52c7d7f12551..3869730a3d30 100644 --- a/src/3d/qgs3dmapscene.cpp +++ b/src/3d/qgs3dmapscene.cpp @@ -538,6 +538,18 @@ void Qgs3DMapScene::onLayersChanged() } } +void Qgs3DMapScene::updateTemporal() +{ + for ( auto layer : mLayerEntities.keys() ) + { + if ( layer->temporalProperties()->isActive() ) + { + removeLayerEntity( layer ); + addLayerEntity( layer ); + } + } +} + void Qgs3DMapScene::addLayerEntity( QgsMapLayer *layer ) { bool needsSceneUpdate = false; diff --git a/src/3d/qgs3dmapscene.h b/src/3d/qgs3dmapscene.h index e08bbcd2e80f..4edfc3de5cff 100644 --- a/src/3d/qgs3dmapscene.h +++ b/src/3d/qgs3dmapscene.h @@ -114,6 +114,10 @@ class _3D_EXPORT Qgs3DMapScene : public Qt3DCore::QEntity //! Emitted when the scene's state has changed void sceneStateChanged(); + public slots: + //! Updates the temporale entities + void updateTemporal(); + private slots: void onCameraChanged(); void onFrameTriggered( float dt ); diff --git a/src/3d/qgs3dmapsettings.cpp b/src/3d/qgs3dmapsettings.cpp index 74f597903854..299f16ef34fc 100644 --- a/src/3d/qgs3dmapsettings.cpp +++ b/src/3d/qgs3dmapsettings.cpp @@ -31,6 +31,7 @@ Qgs3DMapSettings::Qgs3DMapSettings( const Qgs3DMapSettings &other ) : QObject( nullptr ) + , QgsTemporalRangeObject( other ) , mOrigin( other.mOrigin ) , mCrs( other.mCrs ) , mBackgroundColor( other.mBackgroundColor ) @@ -197,6 +198,11 @@ void Qgs3DMapSettings::readXml( const QDomElement &elem, const QgsReadWriteConte mShowTerrainBoundingBoxes = elemDebug.attribute( QStringLiteral( "bounding-boxes" ), QStringLiteral( "0" ) ).toInt(); mShowTerrainTileInfo = elemDebug.attribute( QStringLiteral( "terrain-tile-info" ), QStringLiteral( "0" ) ).toInt(); mShowCameraViewCenter = elemDebug.attribute( QStringLiteral( "camera-view-center" ), QStringLiteral( "0" ) ).toInt(); + + QDomElement elemTemporalRange = elem.firstChildElement( QStringLiteral( "temporal-range" ) ); + QDateTime start = QDateTime::fromString( elemTemporalRange.attribute( QStringLiteral( "start" ) ), Qt::ISODate ); + QDateTime end = QDateTime::fromString( elemTemporalRange.attribute( QStringLiteral( "end" ) ), Qt::ISODate ); + setTemporalRange( QgsDateTimeRange( start, end ) ); } QDomElement Qgs3DMapSettings::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const @@ -279,6 +285,10 @@ QDomElement Qgs3DMapSettings::writeXml( QDomDocument &doc, const QgsReadWriteCon elemDebug.setAttribute( QStringLiteral( "camera-view-center" ), mShowCameraViewCenter ? 1 : 0 ); elem.appendChild( elemDebug ); + QDomElement elemTemporalRange = doc.createElement( QStringLiteral( "temporal-range" ) ); + elemTemporalRange.setAttribute( QStringLiteral( "start" ), temporalRange().begin().toString( Qt::ISODate ) ); + elemTemporalRange.setAttribute( QStringLiteral( "end" ), temporalRange().end().toString( Qt::ISODate ) ); + return elem; } diff --git a/src/3d/qgs3dmapsettings.h b/src/3d/qgs3dmapsettings.h index f16b0a201fb7..955a5210dc77 100644 --- a/src/3d/qgs3dmapsettings.h +++ b/src/3d/qgs3dmapsettings.h @@ -48,7 +48,7 @@ class QDomElement; * * \since QGIS 3.0 */ -class _3D_EXPORT Qgs3DMapSettings : public QObject +class _3D_EXPORT Qgs3DMapSettings : public QObject, public QgsTemporalRangeObject { Q_OBJECT public: diff --git a/src/3d/qgsmeshlayer3drenderer.cpp b/src/3d/qgsmeshlayer3drenderer.cpp index f5216002b082..3984296ec0f4 100644 --- a/src/3d/qgsmeshlayer3drenderer.cpp +++ b/src/3d/qgsmeshlayer3drenderer.cpp @@ -77,7 +77,8 @@ Qt3DCore::QEntity *QgsMeshLayer3DRenderer::createEntity( const Qgs3DMapSettings if ( !meshLayer || !meshLayer->dataProvider() ) return nullptr; - if ( meshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ) + if ( meshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) || + !mSymbol->isEnabled() ) { // 3D not implemented for 1D meshes return nullptr; diff --git a/src/3d/symbols/qgsmesh3dsymbol.cpp b/src/3d/symbols/qgsmesh3dsymbol.cpp index c053c3bf4fe1..c793dac86272 100644 --- a/src/3d/symbols/qgsmesh3dsymbol.cpp +++ b/src/3d/symbols/qgsmesh3dsymbol.cpp @@ -41,6 +41,7 @@ void QgsMesh3DSymbol::writeXml( QDomElement &elem, const QgsReadWriteContext &co //Advanced symbol QDomElement elemAdvancedSettings = doc.createElement( QStringLiteral( "advanced-settings" ) ); + elemAdvancedSettings.setAttribute( QStringLiteral( "renderer-3d-enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); elemAdvancedSettings.setAttribute( QStringLiteral( "smoothed-triangle" ), mSmoothedTriangles ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); elemAdvancedSettings.setAttribute( QStringLiteral( "wireframe-enabled" ), mWireframeEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); elemAdvancedSettings.setAttribute( QStringLiteral( "wireframe-line-width" ), mWireframeLineWidth ); @@ -78,6 +79,7 @@ void QgsMesh3DSymbol::readXml( const QDomElement &elem, const QgsReadWriteContex //Advanced symbol QDomElement elemAdvancedSettings = elem.firstChildElement( QStringLiteral( "advanced-settings" ) ); + mEnabled = elemAdvancedSettings.attribute( QStringLiteral( "renderer-3d-enabled" ) ).toInt(); mSmoothedTriangles = elemAdvancedSettings.attribute( QStringLiteral( "smoothed-triangle" ) ).toInt(); mWireframeEnabled = elemAdvancedSettings.attribute( QStringLiteral( "wireframe-enabled" ) ).toInt(); mWireframeLineWidth = elemAdvancedSettings.attribute( QStringLiteral( "wireframe-line-width" ) ).toDouble(); @@ -237,3 +239,13 @@ void QgsMesh3DSymbol::setArrowsFixedSize( bool arrowsFixeSize ) { mArrowsFixedSize = arrowsFixeSize; } + +bool QgsMesh3DSymbol::isEnabled() const +{ + return mEnabled; +} + +void QgsMesh3DSymbol::setEnabled( bool enabled ) +{ + mEnabled = enabled; +} diff --git a/src/3d/symbols/qgsmesh3dsymbol.h b/src/3d/symbols/qgsmesh3dsymbol.h index 6364bd7e1d37..06c5da715c28 100644 --- a/src/3d/symbols/qgsmesh3dsymbol.h +++ b/src/3d/symbols/qgsmesh3dsymbol.h @@ -76,6 +76,20 @@ class _3D_EXPORT QgsMesh3DSymbol : public QgsAbstract3DSymbol void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const override; void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) override; + /** + * Returns if the 3d rendering is enabled + * + * \since QGIS 3.14 + */ + bool isEnabled() const; + + /** + * Sets if the 3d rendering is enabled + * + * \since QGIS 3.14 + */ + void setEnabled( bool enabled ); + //! Returns method that determines altitude (whether to clamp to feature to terrain) Qgs3DTypes::AltitudeClamping altitudeClamping() const { return mAltClamping; } //! Sets method that determines altitude (whether to clamp to feature to terrain) @@ -309,6 +323,8 @@ class _3D_EXPORT QgsMesh3DSymbol : public QgsAbstract3DSymbol QgsPhongMaterialSettings mMaterial; //!< Defines appearance of objects bool mAddBackFaces = false; + bool mEnabled = true; + //! Triangles settings bool mSmoothedTriangles = false; bool mWireframeEnabled = false; diff --git a/src/analysis/CMakeLists.txt b/src/analysis/CMakeLists.txt index 0e781d198c18..6aab06b8da4c 100644 --- a/src/analysis/CMakeLists.txt +++ b/src/analysis/CMakeLists.txt @@ -66,6 +66,7 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmfiledownloader.cpp processing/qgsalgorithmfillnodata.cpp processing/qgsalgorithmfilter.cpp + processing/qgsalgorithmfilterbygeometry.cpp processing/qgsalgorithmfiltervertices.cpp processing/qgsalgorithmfixgeometries.cpp processing/qgsalgorithmforcerhr.cpp @@ -111,6 +112,7 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmraiseexception.cpp processing/qgsalgorithmrandomextract.cpp processing/qgsalgorithmrandompointsextent.cpp + processing/qgsalgorithmrandompointsonlines.cpp processing/qgsalgorithmrasterlayeruniquevalues.cpp processing/qgsalgorithmrasterlogicalop.cpp processing/qgsalgorithmrasterize.cpp @@ -128,7 +130,9 @@ SET(QGIS_ANALYSIS_SRCS processing/qgsalgorithmrepairshapefile.cpp processing/qgsalgorithmreverselinedirection.cpp processing/qgsalgorithmrotate.cpp + processing/qgsalgorithmroundrastervalues.cpp processing/qgsalgorithmruggedness.cpp + processing/qgsalgorithmsavelog.cpp processing/qgsalgorithmsaveselectedfeatures.cpp processing/qgsalgorithmsegmentize.cpp processing/qgsalgorithmserviceareafromlayer.cpp diff --git a/src/analysis/mesh/qgsmeshcalcutils.cpp b/src/analysis/mesh/qgsmeshcalcutils.cpp index 2a2f6e244ca5..87783de4dd9c 100644 --- a/src/analysis/mesh/qgsmeshcalcutils.cpp +++ b/src/analysis/mesh/qgsmeshcalcutils.cpp @@ -107,7 +107,7 @@ std::shared_ptr QgsMeshCalcUtils::create( const QStri mMeshLayer->nativeMesh(), mMeshLayer->triangularMesh(), nullptr, - mMeshLayer->rendererSettings().scalarSettings( groupIndex ).dataInterpolationMethod() + mMeshLayer->rendererSettings().scalarSettings( groupIndex ).dataResamplingMethod() ); Q_ASSERT( dataX.size() == resultCount ); QVector dataY = @@ -116,7 +116,7 @@ std::shared_ptr QgsMeshCalcUtils::create( const QStri mMeshLayer->nativeMesh(), mMeshLayer->triangularMesh(), nullptr, - mMeshLayer->rendererSettings().scalarSettings( groupIndex ).dataInterpolationMethod() + mMeshLayer->rendererSettings().scalarSettings( groupIndex ).dataResamplingMethod() ); Q_ASSERT( dataY.size() == resultCount ); diff --git a/src/analysis/mesh/qgsmeshcontours.cpp b/src/analysis/mesh/qgsmeshcontours.cpp index d71408d499f2..ad4129602a03 100644 --- a/src/analysis/mesh/qgsmeshcontours.cpp +++ b/src/analysis/mesh/qgsmeshcontours.cpp @@ -58,7 +58,7 @@ QgsGeometry QgsMeshContours::exportPolygons( const QgsMeshDatasetIndex &index, double min_value, double max_value, - QgsMeshRendererScalarSettings::DataInterpolationMethod method, + QgsMeshRendererScalarSettings::DataResamplingMethod method, QgsFeedback *feedback ) { @@ -254,7 +254,7 @@ QgsGeometry QgsMeshContours::exportPolygons( QgsGeometry QgsMeshContours::exportLines( const QgsMeshDatasetIndex &index, double value, - QgsMeshRendererScalarSettings::DataInterpolationMethod method, + QgsMeshRendererScalarSettings::DataResamplingMethod method, QgsFeedback *feedback ) { // Check if the layer/mesh is valid @@ -380,7 +380,7 @@ QgsGeometry QgsMeshContours::exportLines( const QgsMeshDatasetIndex &index, } } -void QgsMeshContours::populateCache( const QgsMeshDatasetIndex &index, QgsMeshRendererScalarSettings::DataInterpolationMethod method ) +void QgsMeshContours::populateCache( const QgsMeshDatasetIndex &index, QgsMeshRendererScalarSettings::DataResamplingMethod method ) { if ( mCachedIndex != index ) { diff --git a/src/analysis/mesh/qgsmeshcontours.h b/src/analysis/mesh/qgsmeshcontours.h index c231104f3c36..e1d9031c1a63 100644 --- a/src/analysis/mesh/qgsmeshcontours.h +++ b/src/analysis/mesh/qgsmeshcontours.h @@ -69,7 +69,7 @@ class ANALYSIS_EXPORT QgsMeshContours */ QgsGeometry exportLines( const QgsMeshDatasetIndex &index, double value, - QgsMeshRendererScalarSettings::DataInterpolationMethod method, + QgsMeshRendererScalarSettings::DataResamplingMethod method, QgsFeedback *feedback = nullptr ); /** @@ -84,13 +84,13 @@ class ANALYSIS_EXPORT QgsMeshContours QgsGeometry exportPolygons( const QgsMeshDatasetIndex &index, double min_value, double max_value, - QgsMeshRendererScalarSettings::DataInterpolationMethod method, + QgsMeshRendererScalarSettings::DataResamplingMethod method, QgsFeedback *feedback = nullptr ); private: void populateCache( const QgsMeshDatasetIndex &index, - QgsMeshRendererScalarSettings::DataInterpolationMethod method ); + QgsMeshRendererScalarSettings::DataResamplingMethod method ); QgsMeshLayer *mMeshLayer = nullptr; diff --git a/src/analysis/processing/qgsalgorithmbuffer.cpp b/src/analysis/processing/qgsalgorithmbuffer.cpp index 079146bfb089..abed3531b2ae 100644 --- a/src/analysis/processing/qgsalgorithmbuffer.cpp +++ b/src/analysis/processing/qgsalgorithmbuffer.cpp @@ -62,7 +62,7 @@ void QgsBufferAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( new QgsProcessingParameterNumber( QStringLiteral( "MITER_LIMIT" ), QObject::tr( "Miter limit" ), QgsProcessingParameterNumber::Double, 2, false, 1 ) ); addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "DISSOLVE" ), QObject::tr( "Dissolve result" ), false ) ); - addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Buffered" ), QgsProcessing::TypeVectorPolygon ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Buffered" ), QgsProcessing::TypeVectorPolygon, QVariant(), false, true, true ) ); } QString QgsBufferAlgorithm::shortHelpString() const @@ -179,6 +179,36 @@ QgsProcessingAlgorithm::Flags QgsBufferAlgorithm::flags() const return f; } +QgsProcessingAlgorithm::VectorProperties QgsBufferAlgorithm::sinkProperties( const QString &sink, const QVariantMap ¶meters, QgsProcessingContext &context, const QMap &sourceProperties ) const +{ + QgsProcessingAlgorithm::VectorProperties result; + if ( sink == QStringLiteral( "OUTPUT" ) ) + { + if ( sourceProperties.value( QStringLiteral( "INPUT" ) ).availability == QgsProcessingAlgorithm::Available ) + { + const VectorProperties inputProps = sourceProperties.value( QStringLiteral( "INPUT" ) ); + result.fields = inputProps.fields; + result.crs = inputProps.crs; + result.wkbType = QgsWkbTypes::MultiPolygon; + result.availability = Available; + return result; + } + else + { + std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( source ) + { + result.fields = source->fields(); + result.crs = source->sourceCrs(); + result.wkbType = QgsWkbTypes::MultiPolygon; + result.availability = Available; + return result; + } + } + } + return result; +} + bool QgsBufferAlgorithm::supportInPlaceEdit( const QgsMapLayer *layer ) const { const QgsVectorLayer *vlayer = qobject_cast< const QgsVectorLayer * >( layer ); diff --git a/src/analysis/processing/qgsalgorithmbuffer.h b/src/analysis/processing/qgsalgorithmbuffer.h index a68f641b3d5f..eeead26c0518 100644 --- a/src/analysis/processing/qgsalgorithmbuffer.h +++ b/src/analysis/processing/qgsalgorithmbuffer.h @@ -48,6 +48,10 @@ class QgsBufferAlgorithm : public QgsProcessingAlgorithm bool supportInPlaceEdit( const QgsMapLayer *layer ) const override; QgsProcessingAlgorithm::Flags flags() const override; + QgsProcessingAlgorithm::VectorProperties sinkProperties( const QString &sink, + const QVariantMap ¶meters, + QgsProcessingContext &context, + const QMap< QString, QgsProcessingAlgorithm::VectorProperties > &sourceProperties ) const override; protected: diff --git a/src/analysis/processing/qgsalgorithmdetectdatasetchanges.cpp b/src/analysis/processing/qgsalgorithmdetectdatasetchanges.cpp index 6706acf75ed2..a3092a028005 100644 --- a/src/analysis/processing/qgsalgorithmdetectdatasetchanges.cpp +++ b/src/analysis/processing/qgsalgorithmdetectdatasetchanges.cpp @@ -69,7 +69,7 @@ void QgsDetectVectorChangesAlgorithm::initAlgorithm( const QVariantMap & ) addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "ADDED" ), QObject::tr( "Added features" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, true ) ); addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "DELETED" ), QObject::tr( "Deleted features" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, true ) ); - addOutput( new QgsProcessingOutputNumber( QStringLiteral( "UNCHAGED_COUNT" ), QObject::tr( "Count of unchanged features" ) ) ); + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "UNCHANGED_COUNT" ), QObject::tr( "Count of unchanged features" ) ) ); addOutput( new QgsProcessingOutputNumber( QStringLiteral( "ADDED_COUNT" ), QObject::tr( "Count of features added in revised layer" ) ) ); addOutput( new QgsProcessingOutputNumber( QStringLiteral( "DELETED_COUNT" ), QObject::tr( "Count of features deleted from original layer" ) ) ); } diff --git a/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp b/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp new file mode 100644 index 000000000000..52e8f101ea30 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp @@ -0,0 +1,314 @@ +/*************************************************************************** + qgsalgorithmfilterbygeometry.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsalgorithmfilterbygeometry.h" + +///@cond PRIVATE + +QString QgsFilterByGeometryAlgorithm::name() const +{ + return QStringLiteral( "filterbygeometry" ); +} + +QString QgsFilterByGeometryAlgorithm::displayName() const +{ + return QObject::tr( "Filter by geometry type" ); +} + +QStringList QgsFilterByGeometryAlgorithm::tags() const +{ + return QObject::tr( "extract,filter,geometry,linestring,point,polygon" ).split( ',' ); +} + +QString QgsFilterByGeometryAlgorithm::group() const +{ + return QObject::tr( "Vector selection" ); +} + +QString QgsFilterByGeometryAlgorithm::groupId() const +{ + return QStringLiteral( "vectorselection" ); +} + +QgsProcessingAlgorithm::Flags QgsFilterByGeometryAlgorithm::flags() const +{ + Flags f = QgsProcessingAlgorithm::flags(); + f |= QgsProcessingAlgorithm::FlagHideFromToolbox; + return f; +} + +void QgsFilterByGeometryAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ), + QList< int >() << QgsProcessing::TypeVector ) ); + + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "POINTS" ), QObject::tr( "Point features" ), + QgsProcessing::TypeVectorPoint, QVariant(), true, true ) ); + + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "LINES" ), QObject::tr( "Line features" ), + QgsProcessing::TypeVectorLine, QVariant(), true, true ) ); + + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "POLYGONS" ), QObject::tr( "Polygon features" ), + QgsProcessing::TypeVectorPolygon, QVariant(), true, true ) ); + + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "NO_GEOMETRY" ), QObject::tr( "Features with no geometry" ), + QgsProcessing::TypeVector, QVariant(), true, true ) ); + + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "POINT_COUNT" ), QObject::tr( "Total count of point features" ) ) ); + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "LINE_COUNT" ), QObject::tr( "Total count of line features" ) ) ); + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "POLYGON_COUNT" ), QObject::tr( "Total count of polygon features" ) ) ); + addOutput( new QgsProcessingOutputNumber( QStringLiteral( "NO_GEOMETRY_COUNT" ), QObject::tr( "Total count of features without geometry" ) ) ); +} + +QString QgsFilterByGeometryAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm filters features by their geometry type. Incoming features will be directed to different " + "outputs based on whether they have a point, line or polygon geometry." ); +} + +QString QgsFilterByGeometryAlgorithm::shortDescription() const +{ + return QObject::tr( "Filters features by geometry type" ); +} + +QgsFilterByGeometryAlgorithm *QgsFilterByGeometryAlgorithm::createInstance() const +{ + return new QgsFilterByGeometryAlgorithm(); +} + +QVariantMap QgsFilterByGeometryAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( !source ) + throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + + const bool hasM = QgsWkbTypes::hasM( source->wkbType() ); + const bool hasZ = QgsWkbTypes::hasZ( source->wkbType() ); + + QgsWkbTypes::Type pointType = QgsWkbTypes::Point; + QgsWkbTypes::Type lineType = QgsWkbTypes::LineString; + QgsWkbTypes::Type polygonType = QgsWkbTypes::Polygon; + if ( hasM ) + { + pointType = QgsWkbTypes::addM( pointType ); + lineType = QgsWkbTypes::addM( lineType ); + polygonType = QgsWkbTypes::addM( polygonType ); + } + if ( hasZ ) + { + pointType = QgsWkbTypes::addZ( pointType ); + lineType = QgsWkbTypes::addZ( lineType ); + polygonType = QgsWkbTypes::addZ( polygonType ); + } + + QString pointSinkId; + std::unique_ptr< QgsFeatureSink > pointSink( parameterAsSink( parameters, QStringLiteral( "POINTS" ), context, pointSinkId, source->fields(), + pointType, source->sourceCrs() ) ); + if ( parameters.value( QStringLiteral( "POINTS" ), QVariant() ).isValid() && !pointSink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "POINTS" ) ) ); + + QString lineSinkId; + std::unique_ptr< QgsFeatureSink > lineSink( parameterAsSink( parameters, QStringLiteral( "LINES" ), context, lineSinkId, source->fields(), + lineType, source->sourceCrs() ) ); + if ( parameters.value( QStringLiteral( "LINES" ), QVariant() ).isValid() && !lineSink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "LINES" ) ) ); + + QString polygonSinkId; + std::unique_ptr< QgsFeatureSink > polygonSink( parameterAsSink( parameters, QStringLiteral( "POLYGONS" ), context, polygonSinkId, source->fields(), + polygonType, source->sourceCrs() ) ); + if ( parameters.value( QStringLiteral( "POLYGONS" ), QVariant() ).isValid() && !polygonSink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "POLYGONS" ) ) ); + + QString noGeomSinkId; + std::unique_ptr< QgsFeatureSink > noGeomSink( parameterAsSink( parameters, QStringLiteral( "NO_GEOMETRY" ), context, noGeomSinkId, source->fields(), + QgsWkbTypes::NoGeometry ) ); + if ( parameters.value( QStringLiteral( "NO_GEOMETRY" ), QVariant() ).isValid() && !noGeomSink ) + throw QgsProcessingException( invalidSinkError( parameters, QStringLiteral( "NO_GEOMETRY" ) ) ); + + long count = source->featureCount(); + long long pointCount = 0; + long long lineCount = 0; + long long polygonCount = 0; + long long nullCount = 0; + + double step = count > 0 ? 100.0 / count : 1; + int current = 0; + + QgsFeatureIterator it = source->getFeatures(); + QgsFeature f; + while ( it.nextFeature( f ) ) + { + if ( feedback->isCanceled() ) + { + break; + } + + if ( f.hasGeometry() ) + { + switch ( f.geometry().type() ) + { + case QgsWkbTypes::PointGeometry: + if ( pointSink ) + { + pointSink->addFeature( f, QgsFeatureSink::FastInsert ); + } + pointCount++; + break; + case QgsWkbTypes::LineGeometry: + if ( lineSink ) + { + lineSink->addFeature( f, QgsFeatureSink::FastInsert ); + } + lineCount++; + break; + case QgsWkbTypes::PolygonGeometry: + if ( polygonSink ) + { + polygonSink->addFeature( f, QgsFeatureSink::FastInsert ); + } + polygonCount++; + break; + case QgsWkbTypes::NullGeometry: + case QgsWkbTypes::UnknownGeometry: + break; + } + } + else + { + if ( noGeomSink ) + { + noGeomSink->addFeature( f, QgsFeatureSink::FastInsert ); + } + nullCount++; + } + + feedback->setProgress( current * step ); + current++; + } + + QVariantMap outputs; + + if ( pointSink ) + outputs.insert( QStringLiteral( "POINTS" ), pointSinkId ); + if ( lineSink ) + outputs.insert( QStringLiteral( "LINES" ), lineSinkId ); + if ( polygonSink ) + outputs.insert( QStringLiteral( "POLYGONS" ), polygonSinkId ); + if ( noGeomSink ) + outputs.insert( QStringLiteral( "NO_GEOMETRY" ), noGeomSinkId ); + + outputs.insert( QStringLiteral( "POINT_COUNT" ), pointCount ); + outputs.insert( QStringLiteral( "LINE_COUNT" ), lineCount ); + outputs.insert( QStringLiteral( "POLYGON_COUNT" ), polygonCount ); + outputs.insert( QStringLiteral( "NO_GEOMETRY_COUNT" ), nullCount ); + + return outputs; +} + + + +// +// QgsFilterByLayerTypeAlgorithm +// + +QString QgsFilterByLayerTypeAlgorithm::name() const +{ + return QStringLiteral( "filterlayersbytype" ); +} + +QString QgsFilterByLayerTypeAlgorithm::displayName() const +{ + return QObject::tr( "Filter layers by type" ); +} + +QStringList QgsFilterByLayerTypeAlgorithm::tags() const +{ + return QObject::tr( "filter,vector,raster,select" ).split( ',' ); +} + +QString QgsFilterByLayerTypeAlgorithm::group() const +{ + return QObject::tr( "Layer tools" ); +} + +QString QgsFilterByLayerTypeAlgorithm::groupId() const +{ + return QStringLiteral( "layertools" ); +} + +QgsProcessingAlgorithm::Flags QgsFilterByLayerTypeAlgorithm::flags() const +{ + Flags f = QgsProcessingAlgorithm::flags(); + f |= FlagHideFromToolbox | FlagPruneModelBranchesBasedOnAlgorithmResults; + return f; +} + +void QgsFilterByLayerTypeAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterMapLayer( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + + addParameter( new QgsProcessingParameterVectorDestination( QStringLiteral( "VECTOR" ), QObject::tr( "Vector features" ), + QgsProcessing::TypeVectorAnyGeometry, QVariant(), true, false ) ); + + addParameter( new QgsProcessingParameterRasterDestination( QStringLiteral( "RASTER" ), QObject::tr( "Raster layer" ), QVariant(), true, false ) ); +} + +QString QgsFilterByLayerTypeAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm filters layer by their type. Incoming layers will be directed to different " + "outputs based on whether they are a vector or raster layer." ); +} + +QString QgsFilterByLayerTypeAlgorithm::shortDescription() const +{ + return QObject::tr( "Filters layers by type" ); +} + +QgsFilterByLayerTypeAlgorithm *QgsFilterByLayerTypeAlgorithm::createInstance() const +{ + return new QgsFilterByLayerTypeAlgorithm(); +} + +QVariantMap QgsFilterByLayerTypeAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) +{ + const QgsMapLayer *layer = parameterAsLayer( parameters, QStringLiteral( "INPUT" ), context ); + if ( !layer ) + throw QgsProcessingException( QObject::tr( "Could not load input layer" ) ); + + QVariantMap outputs; + + switch ( layer->type() ) + { + case QgsMapLayerType::VectorLayer: + outputs.insert( QStringLiteral( "VECTOR" ), parameters.value( QStringLiteral( "INPUT" ) ) ); + break; + + case QgsMapLayerType::RasterLayer: + outputs.insert( QStringLiteral( "RASTER" ), parameters.value( QStringLiteral( "INPUT" ) ) ); + break; + + case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: + break; + } + + return outputs; +} + +///@endcond + diff --git a/src/analysis/processing/qgsalgorithmfilterbygeometry.h b/src/analysis/processing/qgsalgorithmfilterbygeometry.h new file mode 100644 index 000000000000..7c45c36be94f --- /dev/null +++ b/src/analysis/processing/qgsalgorithmfilterbygeometry.h @@ -0,0 +1,87 @@ +/*************************************************************************** + qgsalgorithmfilterbygeometry.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSALGORITHMFILTERBYGEOMETRY_H +#define QGSALGORITHMFILTERBYGEOMETRY_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" + +///@cond PRIVATE + +/** + * Native filter by geometry type algorithm. + */ +class QgsFilterByGeometryAlgorithm : public QgsProcessingAlgorithm +{ + + public: + + QgsFilterByGeometryAlgorithm() = default; + Flags flags() const override; + 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; + QString shortDescription() const override; + QgsFilterByGeometryAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + +}; + + +/** + * Native filter by layer type algorithm. + */ +class QgsFilterByLayerTypeAlgorithm : public QgsProcessingAlgorithm +{ + + public: + + QgsFilterByLayerTypeAlgorithm() = default; + Flags flags() const override; + 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; + QString shortDescription() const override; + QgsFilterByLayerTypeAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) override; + +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMFILTERBYGEOMETRY_H + + diff --git a/src/analysis/processing/qgsalgorithmpackage.cpp b/src/analysis/processing/qgsalgorithmpackage.cpp index f7d214385b4a..1461a494dde8 100644 --- a/src/analysis/processing/qgsalgorithmpackage.cpp +++ b/src/analysis/processing/qgsalgorithmpackage.cpp @@ -177,6 +177,12 @@ QVariantMap QgsPackageAlgorithm::processAlgorithm( const QVariantMap ¶meters feedback->pushDebugInfo( QObject::tr( "Packaging mesh layers is not supported." ) ); errored = true; break; + + case QgsMapLayerType::VectorTileLayer: + //not supported + feedback->pushDebugInfo( QObject::tr( "Packaging vector tile layers is not supported." ) ); + errored = true; + break; } } diff --git a/src/analysis/processing/qgsalgorithmrandompointsonlines.cpp b/src/analysis/processing/qgsalgorithmrandompointsonlines.cpp new file mode 100644 index 000000000000..a8fdad83cbe5 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmrandompointsonlines.cpp @@ -0,0 +1,337 @@ +/*************************************************************************** + qgsalgorithmrandompointsonlines.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Håvard Tveite + email : havard dot tveite at nmbu dot no + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 "qgsalgorithmrandompointsonlines.h" +#include "random" + +// The algorithm parameter names: +static const QString INPUT = QStringLiteral( "INPUT" ); +static const QString POINTS_NUMBER = QStringLiteral( "POINTS_NUMBER" ); +static const QString MIN_DISTANCE = QStringLiteral( "MIN_DISTANCE" ); +static const QString MAX_TRIES_PER_POINT = QStringLiteral( "MAX_TRIES_PER_POINT" ); +static const QString SEED = QStringLiteral( "SEED" ); +static const QString INCLUDE_LINE_ATTRIBUTES = QStringLiteral( "INCLUDE_LINE_ATTRIBUTES" ); +static const QString OUTPUT = QStringLiteral( "OUTPUT" ); +static const QString OUTPUT_POINTS = QStringLiteral( "OUTPUT_POINTS" ); +static const QString POINTS_MISSED = QStringLiteral( "POINTS_MISSED" ); +static const QString LINES_WITH_MISSED_POINTS = QStringLiteral( "LINES_WITH_MISSED_POINTS" ); +static const QString FEATURES_WITH_EMPTY_OR_NO_GEOMETRY = QStringLiteral( "FEATURES_WITH_EMPTY_OR_NO_GEOMETRY" ); + +///@cond PRIVATE + +QString QgsRandomPointsOnLinesAlgorithm::name() const +{ + return QStringLiteral( "randompointsonlines" ); +} + +QString QgsRandomPointsOnLinesAlgorithm::displayName() const +{ + return QObject::tr( "Random points on lines" ); +} + +QStringList QgsRandomPointsOnLinesAlgorithm::tags() const +{ + return QObject::tr( "seed,attributes,create" ).split( ',' ); +} + +QString QgsRandomPointsOnLinesAlgorithm::group() const +{ + return QObject::tr( "Vector creation" ); +} + +QString QgsRandomPointsOnLinesAlgorithm::groupId() const +{ + return QStringLiteral( "vectorcreation" ); +} + +void QgsRandomPointsOnLinesAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFeatureSource( INPUT, QObject::tr( "Input line layer" ), QList< int >() << QgsProcessing::TypeVectorLine ) ); + std::unique_ptr< QgsProcessingParameterNumber > numberPointsParam = qgis::make_unique< QgsProcessingParameterNumber >( POINTS_NUMBER, QObject::tr( "Number of points for each feature" ), QgsProcessingParameterNumber::Integer, 1, false, 1 ); + numberPointsParam->setIsDynamic( true ); + numberPointsParam->setDynamicPropertyDefinition( QgsPropertyDefinition( POINTS_NUMBER, QObject::tr( "Number of points for each feature" ), QgsPropertyDefinition::IntegerPositive ) ); + numberPointsParam->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); + addParameter( numberPointsParam.release() ); + + std::unique_ptr< QgsProcessingParameterDistance > minDistParam = qgis::make_unique< QgsProcessingParameterDistance >( MIN_DISTANCE, QObject::tr( "Minimum distance between points" ), 0, INPUT, true, 0 ); + minDistParam->setIsDynamic( true ); + minDistParam->setDynamicPropertyDefinition( QgsPropertyDefinition( MIN_DISTANCE, QObject::tr( "Minimum distance between points" ), QgsPropertyDefinition::DoublePositive ) ); + minDistParam->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); + addParameter( minDistParam.release() ); + + std::unique_ptr< QgsProcessingParameterNumber > maxAttemptsParam = qgis::make_unique< QgsProcessingParameterNumber >( MAX_TRIES_PER_POINT, QObject::tr( "Maximum number of search attempts (for Min. dist. > 0)" ), QgsProcessingParameterNumber::Integer, 10, true, 1 ); + maxAttemptsParam->setFlags( maxAttemptsParam->flags() | QgsProcessingParameterDefinition::FlagAdvanced ); + maxAttemptsParam->setIsDynamic( true ); + maxAttemptsParam->setDynamicPropertyDefinition( QgsPropertyDefinition( MAX_TRIES_PER_POINT, QObject::tr( "Maximum number of search attempts (for Min. dist. > 0)" ), QgsPropertyDefinition::IntegerPositiveGreaterZero ) ); + maxAttemptsParam->setDynamicLayerParameterName( QStringLiteral( "INPUT" ) ); + addParameter( maxAttemptsParam.release() ); + + std::unique_ptr< QgsProcessingParameterNumber > randomSeedParam = qgis::make_unique< QgsProcessingParameterNumber >( SEED, QObject::tr( "Random seed" ), QgsProcessingParameterNumber::Integer, QVariant(), true, 1 ); + randomSeedParam->setFlags( randomSeedParam->flags() | QgsProcessingParameterDefinition::FlagAdvanced ); + addParameter( randomSeedParam.release() ); + + std::unique_ptr< QgsProcessingParameterBoolean > includeLineAttrParam = qgis::make_unique< QgsProcessingParameterBoolean >( INCLUDE_LINE_ATTRIBUTES, QObject::tr( "Include line attributes" ), true ); + includeLineAttrParam->setFlags( includeLineAttrParam->flags() | QgsProcessingParameterDefinition::FlagAdvanced ); + addParameter( includeLineAttrParam.release() ); + + addParameter( new + QgsProcessingParameterFeatureSink( OUTPUT, QObject::tr( "Random points on lines" ), QgsProcessing::TypeVectorPoint ) ); + + addOutput( new QgsProcessingOutputNumber( OUTPUT_POINTS, QObject::tr( "Total number of points generated" ) ) ); + addOutput( new QgsProcessingOutputNumber( POINTS_MISSED, QObject::tr( "Number of missed points" ) ) ); + addOutput( new QgsProcessingOutputNumber( LINES_WITH_MISSED_POINTS, QObject::tr( "Number of lines with missed points" ) ) ); + addOutput( new QgsProcessingOutputNumber( FEATURES_WITH_EMPTY_OR_NO_GEOMETRY, QObject::tr( "Number of features with empty or no geometry" ) ) ); +} + +QString QgsRandomPointsOnLinesAlgorithm::shortHelpString() const +{ + return QObject::tr( "

    This algorithm creates a point layer, with points placed randomly " + "on the lines of the Input line layer.

    " + "
    • For each feature in the Input line layer, the algorithm attempts to add " + "the specified Number of points for each feature to the output layer.
    • " + "
    • A Minimum distance between points can be specified.
      " + "A point will not be generated if there is an already generated point " + "(on any line feature) within this (Euclidean) distance from " + "the generated location. " + "If the Minimum distance between points is too large, it may not be possible to generate " + "the specified Number of points for each feature.
    • " + "
    • The Maximum number of attempts per point " + "is only relevant if the Minimum distance between points is greater than 0. " + "The total number of points will be
      'number of input features' * " + "Number of points for each feature
      if there are no misses.
    • " + "
    • The seed for the random generator can be provided (Random seed " + "- integer, greater than 0).
    • " + "
    • The user can choose not to Include line feature attributes in " + "the attributes of the generated point features.
    • " + "
    " + "

    Output from the algorithm:

    " + "
      " + "
    • A point layer containing the random points (OUTPUT).
    • " + "
    • The number of generated features (POINTS_GENERATED).
    • " + "
    • The number of missed points (POINTS_MISSED).
    • " + "
    • The number of features with non-empty geometry and missing points (LINES_WITH_MISSED_POINTS).
    • " + "
    • The number of features with an empty or no geometry (LINES_WITH_EMPTY_OR_NO_GEOMETRY).
    • " + "
    " + ); +} + + +QgsRandomPointsOnLinesAlgorithm *QgsRandomPointsOnLinesAlgorithm::createInstance() const +{ + return new QgsRandomPointsOnLinesAlgorithm(); +} + +bool QgsRandomPointsOnLinesAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) +{ + mNumPoints = parameterAsInt( parameters, POINTS_NUMBER, context ); + mDynamicNumPoints = QgsProcessingParameters::isDynamic( parameters, POINTS_NUMBER ); + if ( mDynamicNumPoints ) + mNumPointsProperty = parameters.value( POINTS_NUMBER ).value< QgsProperty >(); + + mMinDistance = parameterAsDouble( parameters, MIN_DISTANCE, context ); + mDynamicMinDistance = QgsProcessingParameters::isDynamic( parameters, MIN_DISTANCE ); + if ( mDynamicMinDistance ) + mMinDistanceProperty = parameters.value( MIN_DISTANCE ).value< QgsProperty >(); + + mMaxAttempts = parameterAsInt( parameters, MAX_TRIES_PER_POINT, context ); + mDynamicMaxAttempts = QgsProcessingParameters::isDynamic( parameters, MAX_TRIES_PER_POINT ); + if ( mDynamicMaxAttempts ) + mMaxAttemptsProperty = parameters.value( MAX_TRIES_PER_POINT ).value< QgsProperty >(); + + mUseRandomSeed = parameters.value( SEED ).isValid(); + mRandSeed = parameterAsInt( parameters, SEED, context ); + mIncludeLineAttr = parameterAsBoolean( parameters, INCLUDE_LINE_ATTRIBUTES, context ); + return true; +} + +QVariantMap QgsRandomPointsOnLinesAlgorithm::processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + std::unique_ptr< QgsProcessingFeatureSource > lineSource( parameterAsSource( parameters, INPUT, context ) ); + if ( !lineSource ) + throw QgsProcessingException( invalidSourceError( parameters, INPUT ) ); + + QgsFields fields; + fields.append( QgsField( QStringLiteral( "rand_point_id" ), QVariant::LongLong ) ); + if ( mIncludeLineAttr ) + fields.extend( lineSource->fields() ); + + QString ldest; + std::unique_ptr< QgsFeatureSink > sink( parameterAsSink( parameters, OUTPUT, + context, ldest, fields, QgsWkbTypes::Point, lineSource->sourceCrs() ) ); + if ( !sink ) + throw QgsProcessingException( invalidSinkError( parameters, OUTPUT ) ); + + QgsExpressionContext expressionContext = createExpressionContext( parameters, context, lineSource.get() ); + + // Initialize random engine + std::random_device rd; + std::mt19937 mt( !mUseRandomSeed ? rd() : mRandSeed ); + std::uniform_real_distribution<> uniformDist( 0, 1 ); + + // Index for finding close points (mMinDistance > 0) + QgsSpatialIndex index; + + int totNPoints = 0; + int missedPoints = 0; + int missedLines = 0; + int emptyOrNullGeom = 0; + + long featureCount = 0; + long numberOfFeatures = lineSource->featureCount(); + long long desiredNumberOfPoints = 0; + const double featureProgressStep = 100.0 / ( numberOfFeatures > 0 ? numberOfFeatures : 1 ); + double baseFeatureProgress = 0.0; + QgsFeature lFeat; + QgsFeatureIterator fitL = mIncludeLineAttr || mDynamicNumPoints || mDynamicMinDistance || mDynamicMaxAttempts ? lineSource->getFeatures() + : lineSource->getFeatures( QgsFeatureRequest().setNoAttributes() ); + while ( fitL.nextFeature( lFeat ) ) + { + if ( feedback->isCanceled() ) + { + feedback->setProgress( 0 ); + break; + } + if ( !lFeat.hasGeometry() ) + { + // Increment invalid features count + emptyOrNullGeom++; + featureCount++; + baseFeatureProgress += featureProgressStep; + feedback->setProgress( baseFeatureProgress ); + continue; + } + QgsGeometry lGeom( lFeat.geometry() ); + if ( lGeom.isEmpty() ) + { + // Increment invalid features count + emptyOrNullGeom++; + featureCount++; + baseFeatureProgress += featureProgressStep; + feedback->setProgress( baseFeatureProgress ); + continue; + } + + if ( mDynamicNumPoints || mDynamicMinDistance || mDynamicMaxAttempts ) + { + expressionContext.setFeature( lFeat ); + } + + double lineLength = lGeom.length(); + int pointsAddedForThisFeature = 0; + + int numberPointsForThisFeature = mNumPoints; + if ( mDynamicNumPoints ) + numberPointsForThisFeature = mNumPointsProperty.valueAsInt( expressionContext, numberPointsForThisFeature ); + desiredNumberOfPoints += numberPointsForThisFeature; + + int maxAttemptsForThisFeature = mMaxAttempts; + if ( mDynamicMaxAttempts ) + maxAttemptsForThisFeature = mMaxAttemptsProperty.valueAsInt( expressionContext, maxAttemptsForThisFeature ); + + double minDistanceForThisFeature = mMinDistance; + if ( mDynamicMinDistance ) + minDistanceForThisFeature = mMinDistanceProperty.valueAsDouble( expressionContext, minDistanceForThisFeature ); + + const double pointProgressIncrement = featureProgressStep / ( numberPointsForThisFeature * maxAttemptsForThisFeature ); + + double pointProgress = 0.0; + for ( long pointIndex = 0; pointIndex < numberPointsForThisFeature; pointIndex++ ) + { + if ( feedback->isCanceled() ) + { + break; + } + // Try to add a point (mMaxAttempts attempts) + int distCheckIterations = 0; + while ( distCheckIterations < maxAttemptsForThisFeature ) + { + if ( feedback->isCanceled() ) + { + break; + } + // Generate a random point + double randPos = lineLength * uniformDist( mt ); + QgsGeometry rpGeom = QgsGeometry( lGeom.interpolate( randPos ) ); + distCheckIterations++; + pointProgress += pointProgressIncrement; + + if ( !rpGeom.isNull() && !rpGeom.isEmpty() ) + { + if ( minDistanceForThisFeature != 0 && totNPoints > 0 ) + { + // Have to check minimum distance to existing points + QList neighbors = index.nearestNeighbor( rpGeom, 1, minDistanceForThisFeature ); + if ( !neighbors.empty() ) + { + feedback->setProgress( baseFeatureProgress + pointProgress ); + continue; + } + } + // point OK to add + QgsFeature f = QgsFeature( totNPoints ); + QgsAttributes pAttrs = QgsAttributes(); + pAttrs.append( totNPoints ); + if ( mIncludeLineAttr ) + { + pAttrs.append( lFeat.attributes() ); + } + f.setAttributes( pAttrs ); + f.setGeometry( rpGeom ); + + if ( minDistanceForThisFeature != 0 ) + { + index.addFeature( f ); + } + sink->addFeature( f, QgsFeatureSink::FastInsert ); + totNPoints++; + pointsAddedForThisFeature++; + pointProgress += pointProgressIncrement * ( maxAttemptsForThisFeature - distCheckIterations ); + break; + } + else + { + feedback->setProgress( baseFeatureProgress + pointProgress ); + } + } // while not maxattempts + feedback->setProgress( baseFeatureProgress + pointProgress ); + } // for points + baseFeatureProgress += featureProgressStep; + if ( pointsAddedForThisFeature < numberPointsForThisFeature ) + { + missedLines++; + } + featureCount++; + feedback->setProgress( baseFeatureProgress ); + } // while features + missedPoints = desiredNumberOfPoints - totNPoints; + feedback->pushInfo( QObject::tr( "Total number of points generated: " + " %1\nNumber of missed points: %2\nLines with missing points: " + " %3\nFeatures with empty or missing geometries: %4" + ).arg( totNPoints ).arg( missedPoints ).arg( missedLines ).arg( emptyOrNullGeom ) ); + QVariantMap outputs; + outputs.insert( OUTPUT, ldest ); + outputs.insert( OUTPUT_POINTS, totNPoints ); + outputs.insert( POINTS_MISSED, missedPoints ); + outputs.insert( LINES_WITH_MISSED_POINTS, missedLines ); + outputs.insert( FEATURES_WITH_EMPTY_OR_NO_GEOMETRY, emptyOrNullGeom ); + + return outputs; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmrandompointsonlines.h b/src/analysis/processing/qgsalgorithmrandompointsonlines.h new file mode 100644 index 000000000000..61999a09605a --- /dev/null +++ b/src/analysis/processing/qgsalgorithmrandompointsonlines.h @@ -0,0 +1,76 @@ +/*************************************************************************** + qgsalgorithmrandompointsonlines.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Håvard Tveite + email : havard dot tveite at nmbu dot no + ***************************************************************************/ + +/*************************************************************************** + * * + * 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 QGSALGORITHMRANDOMPOINTSONLINES_H +#define QGSALGORITHMRANDOMPOINTSONLINES_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" +#include "qgsapplication.h" + +///@cond PRIVATE + +/** + * Native random points on lines creation algorithm. + */ +class QgsRandomPointsOnLinesAlgorithm : public QgsProcessingAlgorithm +{ + public: + + QgsRandomPointsOnLinesAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QIcon icon() const override { return QgsApplication::getThemeIcon( QStringLiteral( "/algorithms/mAlgorithmRandomPointsOnLines.svg" ) ); } + QString svgIconPath() const override { return QgsApplication::iconPath( QStringLiteral( "/algorithms/mAlgorithmRandomPointsOnLines.svg" ) ); } + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsRandomPointsOnLinesAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + bool prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback * ) override; + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, + QgsProcessingFeedback *feedback ) override; + + + private: + int mNumPoints; + bool mDynamicNumPoints = false; + QgsProperty mNumPointsProperty; + + double mMinDistance = 0; + bool mDynamicMinDistance = false; + QgsProperty mMinDistanceProperty; + + int mMaxAttempts = 10; + bool mDynamicMaxAttempts = false; + QgsProperty mMaxAttemptsProperty; + + bool mUseRandomSeed = false; + int mRandSeed = 0; + bool mIncludeLineAttr = false; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMRANDOMPOINTSONLINES_H diff --git a/src/analysis/processing/qgsalgorithmremovenullgeometry.cpp b/src/analysis/processing/qgsalgorithmremovenullgeometry.cpp index 62eb8038ea12..2590efd5ffe4 100644 --- a/src/analysis/processing/qgsalgorithmremovenullgeometry.cpp +++ b/src/analysis/processing/qgsalgorithmremovenullgeometry.cpp @@ -47,6 +47,7 @@ QString QgsRemoveNullGeometryAlgorithm::groupId() const void QgsRemoveNullGeometryAlgorithm::initAlgorithm( const QVariantMap & ) { addParameter( new QgsProcessingParameterFeatureSource( QStringLiteral( "INPUT" ), QObject::tr( "Input layer" ) ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "REMOVE_EMPTY" ), QObject::tr( "Also remove empty geometries" ), false ) ); addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), QObject::tr( "Non null geometries" ), QgsProcessing::TypeVectorAnyGeometry, QVariant(), true ) ); @@ -60,7 +61,10 @@ QString QgsRemoveNullGeometryAlgorithm::shortHelpString() const { return QObject::tr( "This algorithm removes any features which do not have a geometry from a vector layer. " "All other features will be copied unchanged.\n\n" - "Optionally, the features with null geometries can be saved to a separate output." ); + "Optionally, the features with null geometries can be saved to a separate output.\n\n" + "If 'Also remove empty geometries' is checked, the algorithm removes features whose geometries " + "have no coordinates, i.e., geometries that are empty. In that case, also the null " + "output will reflect this option, containing both null and empty geometries." ); } QgsRemoveNullGeometryAlgorithm *QgsRemoveNullGeometryAlgorithm::createInstance() const @@ -74,6 +78,8 @@ QVariantMap QgsRemoveNullGeometryAlgorithm::processAlgorithm( const QVariantMap if ( !source ) throw QgsProcessingException( invalidSourceError( parameters, QStringLiteral( "INPUT" ) ) ); + const bool removeEmpty = parameterAsBoolean( parameters, QStringLiteral( "REMOVE_EMPTY" ), context ); + QString nonNullSinkId; std::unique_ptr< QgsFeatureSink > nonNullSink( parameterAsSink( parameters, QStringLiteral( "OUTPUT" ), context, nonNullSinkId, source->fields(), source->wkbType(), source->sourceCrs() ) ); @@ -95,11 +101,11 @@ QVariantMap QgsRemoveNullGeometryAlgorithm::processAlgorithm( const QVariantMap break; } - if ( f.hasGeometry() && nonNullSink ) + if ( ( ( !removeEmpty && f.hasGeometry() ) || ( removeEmpty && !f.geometry().isEmpty() ) ) && nonNullSink ) { nonNullSink->addFeature( f, QgsFeatureSink::FastInsert ); } - else if ( !f.hasGeometry() && nullSink ) + else if ( ( ( !removeEmpty && !f.hasGeometry() ) || ( removeEmpty && f.geometry().isEmpty() ) ) && nullSink ) { nullSink->addFeature( f, QgsFeatureSink::FastInsert ); } diff --git a/src/analysis/processing/qgsalgorithmroundrastervalues.cpp b/src/analysis/processing/qgsalgorithmroundrastervalues.cpp new file mode 100644 index 000000000000..ab8b3e2fb62b --- /dev/null +++ b/src/analysis/processing/qgsalgorithmroundrastervalues.cpp @@ -0,0 +1,261 @@ +/*************************************************************************** + qgsalgorithmroundrastervalues.cpp + --------------------- + begin : April 2020 + copyright : (C) 2020 by Clemens Raffler + email : clemens dot raffler 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 "qgsalgorithmroundrastervalues.h" +#include "qgsrasterfilewriter.h" + +///@cond PRIVATE + +QString QgsRoundRasterValuesAlgorithm::name() const +{ + return QStringLiteral( "roundrastervalues" ); +} + +QString QgsRoundRasterValuesAlgorithm::displayName() const +{ + return QObject::tr( "Round raster" ); +} + +QStringList QgsRoundRasterValuesAlgorithm::tags() const +{ + return QObject::tr( "data,cells,round,truncate" ).split( ',' ); +} + +QString QgsRoundRasterValuesAlgorithm::group() const +{ + return QObject::tr( "Raster analysis" ); +} + +QString QgsRoundRasterValuesAlgorithm::groupId() const +{ + return QStringLiteral( "rasteranalysis" ); +} + +void QgsRoundRasterValuesAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterRasterLayer( QStringLiteral( "INPUT" ), QStringLiteral( "Input raster" ) ) ); + addParameter( new QgsProcessingParameterBand( QStringLiteral( "BAND" ), QObject::tr( "Band number" ), 1, QStringLiteral( "INPUT" ) ) ); + addParameter( new QgsProcessingParameterEnum( QStringLiteral( "ROUNDING_DIRECTION" ), QObject::tr( "Rounding direction" ), QStringList() << QObject::tr( "Round up" ) << QObject::tr( "Round to nearest" ) << QObject::tr( "Round down" ), false, 1 ) ); + addParameter( new QgsProcessingParameterNumber( QStringLiteral( "DECIMAL_PLACES" ), QObject::tr( "Number of decimals places" ), QgsProcessingParameterNumber::Integer, 2 ) ); + addParameter( new QgsProcessingParameterRasterDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Output raster" ) ) ); + std::unique_ptr< QgsProcessingParameterDefinition > baseParameter = qgis::make_unique< QgsProcessingParameterNumber >( QStringLiteral( "BASE_N" ), QObject::tr( "Base n for rounding to multiples of n" ), QgsProcessingParameterNumber::Integer, 10, true, 1 ); + baseParameter->setFlags( QgsProcessingParameterDefinition::FlagAdvanced ); + addParameter( baseParameter.release() ); +} + +QString QgsRoundRasterValuesAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm rounds the cell values of a raster dataset according to the specified number of decimals.\n " + "Alternatively, a negative number of decimal places may be used to round values to powers of a base n " + "(specified in the advanced parameter Base n). For example, with a Base value n of 10 and Decimal places of -1 " + "the algorithm rounds cell values to multiples of 10, -2 rounds to multiples of 100, and so on. Arbitrary base values " + "may be chosen, the algorithm applies the same multiplicative principle. Rounding cell values to multiples of " + "a base n may be used to generalize raster layers.\n" + "The algorithm preserves the data type of the input raster. Therefore byte/integer rasters can only be rounded " + "to multiples of a base n, otherwise a warning is raised and the raster gets copied as byte/integer raster" ); +} + +QgsRoundRasterValuesAlgorithm *QgsRoundRasterValuesAlgorithm::createInstance() const +{ + return new QgsRoundRasterValuesAlgorithm(); +} + +bool QgsRoundRasterValuesAlgorithm::prepareAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + Q_UNUSED( feedback ); + QgsRasterLayer *inputRaster = parameterAsRasterLayer( parameters, QStringLiteral( "INPUT" ), context ); + mDecimalPrecision = parameterAsInt( parameters, QStringLiteral( "DECIMAL_PLACES" ), context ); + mBaseN = parameterAsInt( parameters, QStringLiteral( "BASE_N" ), context ); + mMultipleOfBaseN = pow( mBaseN, abs( mDecimalPrecision ) ); + mScaleFactor = std::pow( 10.0, mDecimalPrecision ); + + if ( !inputRaster ) + throw QgsProcessingException( invalidRasterError( parameters, QStringLiteral( "INPUT" ) ) ); + + mBand = parameterAsInt( parameters, QStringLiteral( "BAND" ), context ); + if ( mBand < 1 || mBand > inputRaster->bandCount() ) + throw QgsProcessingException( QObject::tr( "Invalid band number for BAND (%1): Valid values for input raster are 1 to %2" ).arg( mBand ).arg( inputRaster->bandCount() ) ); + + mRoundingDirection = parameterAsEnum( parameters, QStringLiteral( "ROUNDING_DIRECTION" ), context ); + + mInterface.reset( inputRaster->dataProvider()->clone() ); + mDataType = mInterface->dataType( mBand ); + + switch ( mDataType ) + { + case Qgis::Byte: + case Qgis::Int16: + case Qgis::UInt16: + case Qgis::Int32: + case Qgis::UInt32: + mIsInteger = true; + if ( mDecimalPrecision > -1 ) + feedback->reportError( QObject::tr( "Input raster is of byte or integer type. The cell values cannot be rounded and will be output using the same data type." ), false ); + break; + default: + mIsInteger = false; + break; + } + + mInputNoDataValue = inputRaster->dataProvider()->sourceNoDataValue( mBand ); + mExtent = inputRaster->extent(); + mLayerWidth = inputRaster->width(); + mLayerHeight = inputRaster->height(); + mCrs = inputRaster->crs(); + mNbCellsXProvider = mInterface->xSize(); + mNbCellsYProvider = mInterface->ySize(); + return true; +} + +QVariantMap QgsRoundRasterValuesAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + //prepare output dataset + const QString outputFile = parameterAsOutputLayer( parameters, QStringLiteral( "OUTPUT" ), context ); + QFileInfo fi( outputFile ); + const QString outputFormat = QgsRasterFileWriter::driverForExtension( fi.suffix() ); + std::unique_ptr< QgsRasterFileWriter > writer = qgis::make_unique< QgsRasterFileWriter >( outputFile ); + writer->setOutputProviderKey( QStringLiteral( "gdal" ) ); + writer->setOutputFormat( outputFormat ); + std::unique_ptr provider( writer->createOneBandRaster( mInterface->dataType( mBand ), mNbCellsXProvider, mNbCellsYProvider, mExtent, mCrs ) ); + if ( !provider ) + throw QgsProcessingException( QObject::tr( "Could not create raster output: %1" ).arg( outputFile ) ); + if ( !provider->isValid() ) + throw QgsProcessingException( QObject::tr( "Could not create raster output %1: %2" ).arg( outputFile, provider->error().message( QgsErrorMessage::Text ) ) ); + + //prepare output provider + QgsRasterDataProvider *destinationRasterProvider; + destinationRasterProvider = provider.get(); + destinationRasterProvider->setEditable( true ); + destinationRasterProvider->setNoDataValue( 1, mInputNoDataValue ); + + int maxWidth = QgsRasterIterator::DEFAULT_MAXIMUM_TILE_WIDTH; + int maxHeight = QgsRasterIterator::DEFAULT_MAXIMUM_TILE_HEIGHT; + int nbBlocksWidth = static_cast< int >( std::ceil( 1.0 * mLayerWidth / maxWidth ) ); + int nbBlocksHeight = static_cast< int >( std::ceil( 1.0 * mLayerHeight / maxHeight ) ); + int nbBlocks = nbBlocksWidth * nbBlocksHeight; + + QgsRasterIterator iter( mInterface.get() ); + iter.startRasterRead( mBand, mLayerWidth, mLayerHeight, mExtent ); + int iterLeft = 0; + int iterTop = 0; + int iterCols = 0; + int iterRows = 0; + std::unique_ptr< QgsRasterBlock > analysisRasterBlock; + while ( iter.readNextRasterPart( mBand, iterCols, iterRows, analysisRasterBlock, iterLeft, iterTop ) ) + { + if ( feedback ) + feedback->setProgress( 100 * ( ( iterTop / maxHeight * nbBlocksWidth ) + iterLeft / maxWidth ) / nbBlocks ); + if ( mIsInteger && mDecimalPrecision > -1 ) + { + //nothing to round, just write raster block + analysisRasterBlock->setNoDataValue( mInputNoDataValue ); + destinationRasterProvider->writeBlock( analysisRasterBlock.get(), mBand, iterLeft, iterTop ); + } + else + { + for ( int row = 0; row < iterRows; row++ ) + { + if ( feedback && feedback->isCanceled() ) + break; + for ( int column = 0; column < iterCols; column++ ) + { + bool isNoData = false; + double val = analysisRasterBlock->valueAndNoData( row, column, isNoData ); + if ( isNoData ) + { + analysisRasterBlock->setValue( row, column, mInputNoDataValue ); + } + else + { + double roundedVal = mInputNoDataValue; + if ( mRoundingDirection == 0 && mDecimalPrecision < 0 ) + { + roundedVal = roundUpBaseN( val ); + } + else if ( mRoundingDirection == 0 && mDecimalPrecision > -1 ) + { + double m = ( val < 0.0 ) ? -1.0 : 1.0; + roundedVal = roundUp( val, m ); + } + else if ( mRoundingDirection == 1 && mDecimalPrecision < 0 ) + { + roundedVal = roundNearestBaseN( val ); + } + else if ( mRoundingDirection == 1 && mDecimalPrecision > -1 ) + { + double m = ( val < 0.0 ) ? -1.0 : 1.0; + roundedVal = roundNearest( val, m ); + } + else if ( mRoundingDirection == 2 && mDecimalPrecision < 0 ) + { + roundedVal = roundDownBaseN( val ); + } + else + { + double m = ( val < 0.0 ) ? -1.0 : 1.0; + roundedVal = roundDown( val, m ); + } + //integer values get automatically cast to double when reading and back to int when writing + analysisRasterBlock->setValue( row, column, roundedVal ); + } + } + } + destinationRasterProvider->writeBlock( analysisRasterBlock.get(), mBand, iterLeft, iterTop ); + } + } + destinationRasterProvider->setEditable( false ); + + QVariantMap outputs; + outputs.insert( QStringLiteral( "OUTPUT" ), outputFile ); + return outputs; +} + +double QgsRoundRasterValuesAlgorithm::roundNearest( double value, double m ) +{ + return ( std::round( value * m * mScaleFactor ) / mScaleFactor ) * m; +} + +double QgsRoundRasterValuesAlgorithm::roundUp( double value, double m ) +{ + return ( std::ceil( value * m * mScaleFactor ) / mScaleFactor ) * m; +} + +double QgsRoundRasterValuesAlgorithm::roundDown( double value, double m ) +{ + return ( std::floor( value * m * mScaleFactor ) / mScaleFactor ) * m; +} + + +double QgsRoundRasterValuesAlgorithm::roundNearestBaseN( double value ) +{ + return static_cast( mMultipleOfBaseN * round( value / mMultipleOfBaseN ) ); +} + +double QgsRoundRasterValuesAlgorithm::roundUpBaseN( double value ) +{ + return static_cast( mMultipleOfBaseN * ceil( value / mMultipleOfBaseN ) ); +} + +double QgsRoundRasterValuesAlgorithm::roundDownBaseN( double value ) +{ + return static_cast( mMultipleOfBaseN * floor( value / mMultipleOfBaseN ) ); +} + + + + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmroundrastervalues.h b/src/analysis/processing/qgsalgorithmroundrastervalues.h new file mode 100644 index 000000000000..8511628eb57e --- /dev/null +++ b/src/analysis/processing/qgsalgorithmroundrastervalues.h @@ -0,0 +1,85 @@ +/*************************************************************************** + qgsalgorithmroundrastervalues.h + --------------------- + begin : April 2020 + copyright : (C) 2020 by Clemens Raffler + email : clemens dot raffler 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 QGSALGORITHMROUNDRASTERVALUES_H +#define QGSALGORITHMROUNDRASTERVALUES_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" +#include "qgsapplication.h" + +///@cond PRIVATE + +/** + * Round raster values algorithm: + * This algorithm rounds the Values of floating point raster datasets + * based on a predefined precision value. + */ +class QgsRoundRasterValuesAlgorithm : public QgsProcessingAlgorithm +{ + public: + + QgsRoundRasterValuesAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + QIcon icon() const override { return QgsApplication::getThemeIcon( QStringLiteral( "/algorithms/mAlgorithmRoundRastervalues.svg" ) ); } + QString svgIconPath() const override { return QgsApplication::iconPath( QStringLiteral( "/algorithms/mAlgorithmRoundRastervalues.svg" ) ); } + QString name() const override; + QString displayName() const override; + QStringList tags() const override; + QString group() const override; + QString groupId() const override; + QString shortHelpString() const override; + QgsRoundRasterValuesAlgorithm *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: + double roundNearest( double value, double m ); + double roundUp( double value, double m ); + double roundDown( double value, double m ); + double roundNearestBaseN( double value ); + double roundUpBaseN( double value ); + double roundDownBaseN( double value ); + + int mDecimalPrecision = 2; + int mBaseN = 10; + double mScaleFactor; + int mMultipleOfBaseN; + int mBand; + int mRoundingDirection; + std::unique_ptr< QgsRasterInterface > mInterface; + Qgis::DataType mDataType; + bool mIsInteger; + QgsRectangle mExtent; + QgsCoordinateReferenceSystem mCrs; + int mLayerWidth; + int mLayerHeight; + int mNbCellsXProvider = 0; + int mNbCellsYProvider = 0; + double mInputNoDataValue; +}; + +///@endcond PRIVATE + +#endif // QGSALGORITHMROUNDRASTERVALUES_H diff --git a/src/analysis/processing/qgsalgorithmsavelog.cpp b/src/analysis/processing/qgsalgorithmsavelog.cpp new file mode 100644 index 000000000000..0594f6cddef0 --- /dev/null +++ b/src/analysis/processing/qgsalgorithmsavelog.cpp @@ -0,0 +1,93 @@ +/*************************************************************************** + qgsalgorithmsavelog.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsalgorithmsavelog.h" + +///@cond PRIVATE + +QString QgsSaveLogToFileAlgorithm::name() const +{ + return QStringLiteral( "savelog" ); +} + +QgsProcessingAlgorithm::Flags QgsSaveLogToFileAlgorithm::flags() const +{ + return QgsProcessingAlgorithm::flags() | FlagHideFromToolbox | FlagSkipGenericModelLogging; +} + +QString QgsSaveLogToFileAlgorithm::displayName() const +{ + return QObject::tr( "Save log to file" ); +} + +QStringList QgsSaveLogToFileAlgorithm::tags() const +{ + return QObject::tr( "record,messages,logged" ).split( ',' ); +} + +QString QgsSaveLogToFileAlgorithm::group() const +{ + return QObject::tr( "Modeler tools" ); +} + +QString QgsSaveLogToFileAlgorithm::groupId() const +{ + return QStringLiteral( "modelertools" ); +} + +QString QgsSaveLogToFileAlgorithm::shortHelpString() const +{ + return QObject::tr( "This algorithm saves the model's execution log to a file.\n" + "Optionally, the log can be saved in a HTML formatted version." ); +} + +QString QgsSaveLogToFileAlgorithm::shortDescription() const +{ + return QObject::tr( "Saves the model's log contents to a file." ); +} + +QgsSaveLogToFileAlgorithm *QgsSaveLogToFileAlgorithm::createInstance() const +{ + return new QgsSaveLogToFileAlgorithm(); +} + +void QgsSaveLogToFileAlgorithm::initAlgorithm( const QVariantMap & ) +{ + addParameter( new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Log file" ), QObject::tr( "Text files (*.txt);;HTML files (*.html *.HTML)" ) ) ); + addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "USE_HTML" ), QObject::tr( "Use HTML formatting" ), false ) ); +} + +QVariantMap QgsSaveLogToFileAlgorithm::processAlgorithm( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +{ + const QString file = parameterAsFile( parameters, QStringLiteral( "OUTPUT" ), context ); + const bool useHtml = parameterAsBool( parameters, QStringLiteral( "USE_HTML" ), context ); + if ( !file.isEmpty() ) + { + QFile exportFile( file ); + if ( !exportFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) + { + throw QgsProcessingException( QObject::tr( "Could not save log to file %1" ).arg( file ) ); + } + QTextStream fout( &exportFile ); + fout << ( useHtml ? feedback->htmlLog() : feedback->textLog() ); + } + QVariantMap res; + res.insert( QStringLiteral( "OUTPUT" ), file ); + return res; +} + +///@endcond diff --git a/src/analysis/processing/qgsalgorithmsavelog.h b/src/analysis/processing/qgsalgorithmsavelog.h new file mode 100644 index 000000000000..b6f28901d1db --- /dev/null +++ b/src/analysis/processing/qgsalgorithmsavelog.h @@ -0,0 +1,57 @@ +/*************************************************************************** + qgsalgorithmsavelog.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSALGORITHMSAVELOG_H +#define QGSALGORITHMSAVELOG_H + +#define SIP_NO_FILE + +#include "qgis_sip.h" +#include "qgsprocessingalgorithm.h" +#include "qgsapplication.h" + +///@cond PRIVATE + +/** + * Native save log to file algorithm. + */ +class QgsSaveLogToFileAlgorithm : public QgsProcessingAlgorithm +{ + public: + QgsSaveLogToFileAlgorithm() = default; + void initAlgorithm( const QVariantMap &configuration = QVariantMap() ) override; + Flags flags() const 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; + QString shortDescription() const override; + QgsSaveLogToFileAlgorithm *createInstance() const override SIP_FACTORY; + + protected: + + QVariantMap processAlgorithm( const QVariantMap ¶meters, + QgsProcessingContext &context, QgsProcessingFeedback * ) override; + +}; + + +///@endcond PRIVATE + +#endif // QGSALGORITHMSAVELOG_H diff --git a/src/analysis/processing/qgsnativealgorithms.cpp b/src/analysis/processing/qgsnativealgorithms.cpp index 3709ce20fa13..9db2f1ced3c9 100644 --- a/src/analysis/processing/qgsnativealgorithms.cpp +++ b/src/analysis/processing/qgsnativealgorithms.cpp @@ -61,6 +61,7 @@ #include "qgsalgorithmfiledownloader.h" #include "qgsalgorithmfillnodata.h" #include "qgsalgorithmfilter.h" +#include "qgsalgorithmfilterbygeometry.h" #include "qgsalgorithmfiltervertices.h" #include "qgsalgorithmfixgeometries.h" #include "qgsalgorithmforcerhr.h" @@ -105,6 +106,7 @@ #include "qgsalgorithmraiseexception.h" #include "qgsalgorithmrandomextract.h" #include "qgsalgorithmrandompointsextent.h" +#include "qgsalgorithmrandompointsonlines.h" #include "qgsalgorithmrasterlayeruniquevalues.h" #include "qgsalgorithmrasterlogicalop.h" #include "qgsalgorithmrasterize.h" @@ -122,7 +124,9 @@ #include "qgsalgorithmrepairshapefile.h" #include "qgsalgorithmreverselinedirection.h" #include "qgsalgorithmrotate.h" +#include "qgsalgorithmroundrastervalues.h" #include "qgsalgorithmruggedness.h" +#include "qgsalgorithmsavelog.h" #include "qgsalgorithmsaveselectedfeatures.h" #include "qgsalgorithmsegmentize.h" #include "qgsalgorithmserviceareafromlayer.h" @@ -251,6 +255,8 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsFileDownloaderAlgorithm() ); addAlgorithm( new QgsFillNoDataAlgorithm() ); addAlgorithm( new QgsFilterAlgorithm() ); + addAlgorithm( new QgsFilterByGeometryAlgorithm() ); + addAlgorithm( new QgsFilterByLayerTypeAlgorithm() ); addAlgorithm( new QgsFilterVerticesByM() ); addAlgorithm( new QgsFilterVerticesByZ() ); addAlgorithm( new QgsFixGeometriesAlgorithm() ); @@ -304,6 +310,7 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsRaiseWarningAlgorithm() ); addAlgorithm( new QgsRandomExtractAlgorithm() ); addAlgorithm( new QgsRandomPointsExtentAlgorithm() ); + addAlgorithm( new QgsRandomPointsOnLinesAlgorithm() ); addAlgorithm( new QgsRasterLayerUniqueValuesReportAlgorithm() ); addAlgorithm( new QgsRasterLayerZonalStatsAlgorithm() ); addAlgorithm( new QgsRasterLogicalAndAlgorithm() ); @@ -325,7 +332,9 @@ void QgsNativeAlgorithms::loadAlgorithms() addAlgorithm( new QgsRepairShapefileAlgorithm() ); addAlgorithm( new QgsReverseLineDirectionAlgorithm() ); addAlgorithm( new QgsRotateFeaturesAlgorithm() ); + addAlgorithm( new QgsRoundRasterValuesAlgorithm() ); addAlgorithm( new QgsRuggednessAlgorithm() ); + addAlgorithm( new QgsSaveLogToFileAlgorithm() ); addAlgorithm( new QgsSaveSelectedFeatures() ); addAlgorithm( new QgsSegmentizeByMaximumAngleAlgorithm() ); addAlgorithm( new QgsSegmentizeByMaximumDistanceAlgorithm() ); diff --git a/src/app/3d/qgs3dmapcanvas.cpp b/src/app/3d/qgs3dmapcanvas.cpp index cc329e82a988..49f0d0b6362f 100644 --- a/src/app/3d/qgs3dmapcanvas.cpp +++ b/src/app/3d/qgs3dmapcanvas.cpp @@ -28,6 +28,7 @@ #include "qgswindow3dengine.h" #include "qgs3dnavigationwidget.h" #include "qgssettings.h" +#include "qgstemporalcontroller.h" Qgs3DMapCanvas::Qgs3DMapCanvas( QWidget *parent ) : QWidget( parent ) @@ -106,6 +107,8 @@ void Qgs3DMapCanvas::setMap( Qgs3DMapSettings *map ) mNavigationWidget->updateFromCamera(); } ); + + emit mapSettingsChanged(); } QgsCameraController *Qgs3DMapCanvas::cameraController() @@ -189,10 +192,24 @@ bool Qgs3DMapCanvas::eventFilter( QObject *watched, QEvent *event ) return false; } - void Qgs3DMapCanvas::setOnScreenNavigationVisibility( bool visibility ) { mNavigationWidget->setVisible( visibility ); QgsSettings setting; setting.setValue( QStringLiteral( "/3D/navigationWidget/visibility" ), visibility, QgsSettings::Gui ); } + +void Qgs3DMapCanvas::setTemporalController( QgsTemporalController *temporalController ) +{ + if ( mTemporalController ) + disconnect( mTemporalController, &QgsTemporalController::updateTemporalRange, this, &Qgs3DMapCanvas::updateTemporalRange ); + + mTemporalController = temporalController; + connect( mTemporalController, &QgsTemporalController::updateTemporalRange, this, &Qgs3DMapCanvas::updateTemporalRange ); +} + +void Qgs3DMapCanvas::updateTemporalRange( const QgsDateTimeRange &temporalrange ) +{ + mMap->setTemporalRange( temporalrange ); + mScene->updateTemporal(); +} diff --git a/src/app/3d/qgs3dmapcanvas.h b/src/app/3d/qgs3dmapcanvas.h index 70c6e85a3552..d5405359ea43 100644 --- a/src/app/3d/qgs3dmapcanvas.h +++ b/src/app/3d/qgs3dmapcanvas.h @@ -19,6 +19,8 @@ #include #include +#include "qgsrange.h" + namespace Qt3DExtras { class Qt3DWindow; @@ -31,6 +33,7 @@ class QgsWindow3DEngine; class QgsCameraController; class QgsPointXY; class Qgs3DNavigationWidget; +class QgsTemporalController; class Qgs3DMapCanvas : public QWidget @@ -74,14 +77,25 @@ class Qgs3DMapCanvas : public QWidget Qgs3DMapTool *mapTool() const { return mMapTool; } /** - * Set the visibility of on-screen navigation widget. + * Sets the visibility of on-screen navigation widget. */ void setOnScreenNavigationVisibility( bool visibility ); + /** + * Sets the temporal controller + */ + void setTemporalController( QgsTemporalController *temporalController ); + signals: //! Emitted when the 3D map canvas was successfully saved as image void savedAsImage( QString fileName ); + //! Emitted when the the map setting is changed + void mapSettingsChanged(); + + private slots: + void updateTemporalRange( const QgsDateTimeRange &timeRange ); + protected: void resizeEvent( QResizeEvent *ev ) override; bool eventFilter( QObject *watched, QEvent *event ) override; @@ -104,6 +118,8 @@ class Qgs3DMapCanvas : public QWidget //! On-Screen Navigation widget. Qgs3DNavigationWidget *mNavigationWidget = nullptr; + + QgsTemporalController *mTemporalController = nullptr; }; #endif // QGS3DMAPCANVAS_H diff --git a/src/app/3d/qgs3dmapcanvasdockwidget.cpp b/src/app/3d/qgs3dmapcanvasdockwidget.cpp index 121669e569f6..d0c10d07cbd2 100644 --- a/src/app/3d/qgs3dmapcanvasdockwidget.cpp +++ b/src/app/3d/qgs3dmapcanvasdockwidget.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "qgisapp.h" #include "qgs3dmapcanvas.h" @@ -33,6 +34,8 @@ #include "qgsapplication.h" #include "qgssettings.h" #include "qgsgui.h" +#include "qgsmapthemecollection.h" +#include "qgstemporalcontroller.h" #include "qgs3danimationsettings.h" #include "qgs3danimationwidget.h" @@ -98,6 +101,19 @@ Qgs3DMapCanvasDockWidget::Qgs3DMapCanvasDockWidget( QWidget *parent ) toolBar->addSeparator(); + // Map Theme Menu + mMapThemeMenu = new QMenu(); + connect( mMapThemeMenu, &QMenu::aboutToShow, this, &Qgs3DMapCanvasDockWidget::mapThemeMenuAboutToShow ); + + mBtnMapThemes = new QToolButton(); + mBtnMapThemes->setAutoRaise( true ); + mBtnMapThemes->setToolTip( tr( "Set View Theme" ) ); + mBtnMapThemes->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionShowAllLayers.svg" ) ) ); + mBtnMapThemes->setPopupMode( QToolButton::InstantPopup ); + mBtnMapThemes->setMenu( mMapThemeMenu ); + + toolBar->addWidget( mBtnMapThemes ); + toolBar->addAction( QgsApplication::getThemeIcon( QStringLiteral( "mActionOptions.svg" ) ), tr( "Configure…" ), this, &Qgs3DMapCanvasDockWidget::configure ); @@ -210,6 +226,9 @@ void Qgs3DMapCanvasDockWidget::setMapSettings( Qgs3DMapSettings *map ) mAnimationWidget->setCameraController( mCanvas->scene()->cameraController() ); mAnimationWidget->setMap( map ); + + // Disable button for switching the map theme if the terrain generator is a mesh + mBtnMapThemes->setDisabled( mCanvas->map()->terrainGenerator()->type() == QgsTerrainGenerator::Mesh ); } void Qgs3DMapCanvasDockWidget::setMainCanvas( QgsMapCanvas *canvas ) @@ -266,6 +285,9 @@ void Qgs3DMapCanvasDockWidget::configure() newCameraPose.setCenterPoint( p ); mCanvas->cameraController()->setCameraPose( newCameraPose ); } + + // Disable map theme button if the terrain generator is a mesh + mBtnMapThemes->setDisabled( map->terrainGenerator()->type() == QgsTerrainGenerator::Mesh ); } void Qgs3DMapCanvasDockWidget::onMainCanvasLayersChanged() @@ -286,3 +308,40 @@ void Qgs3DMapCanvasDockWidget::onTotalPendingJobsCountChanged() if ( count ) mLabelPendingJobs->setText( tr( "Loading %1 tiles" ).arg( count ) ); } + +void Qgs3DMapCanvasDockWidget::mapThemeMenuAboutToShow() +{ + qDeleteAll( mMapThemeMenuPresetActions ); + mMapThemeMenuPresetActions.clear(); + + QString currentTheme = mCanvas->map()->terrainMapTheme(); + + QAction *actionFollowMain = new QAction( tr( "(none)" ), mMapThemeMenu ); + actionFollowMain->setCheckable( true ); + if ( currentTheme.isEmpty() || !QgsProject::instance()->mapThemeCollection()->hasMapTheme( currentTheme ) ) + { + actionFollowMain->setChecked( true ); + } + connect( actionFollowMain, &QAction::triggered, this, [ = ] + { + mCanvas->map()->setTerrainMapTheme( QString() ); + } ); + mMapThemeMenuPresetActions.append( actionFollowMain ); + + const auto constMapThemes = QgsProject::instance()->mapThemeCollection()->mapThemes(); + for ( const QString &grpName : constMapThemes ) + { + QAction *a = new QAction( grpName, mMapThemeMenu ); + a->setCheckable( true ); + if ( grpName == currentTheme ) + { + a->setChecked( true ); + } + connect( a, &QAction::triggered, this, [a, this] + { + mCanvas->map()->setTerrainMapTheme( a->text() ); + } ); + mMapThemeMenuPresetActions.append( a ); + } + mMapThemeMenu->addActions( mMapThemeMenuPresetActions ); +} diff --git a/src/app/3d/qgs3dmapcanvasdockwidget.h b/src/app/3d/qgs3dmapcanvasdockwidget.h index 22551ebb42f9..457f010dee42 100644 --- a/src/app/3d/qgs3dmapcanvasdockwidget.h +++ b/src/app/3d/qgs3dmapcanvasdockwidget.h @@ -16,8 +16,10 @@ #ifndef QGS3DMAPCANVASDOCKWIDGET_H #define QGS3DMAPCANVASDOCKWIDGET_H +#include "qmenu.h" #include "qgsdockwidget.h" #include "qgis_app.h" +#include "qtoolbutton.h" #define SIP_NO_FILE @@ -62,6 +64,7 @@ class APP_EXPORT Qgs3DMapCanvasDockWidget : public QgsDockWidget void onMainCanvasLayersChanged(); void onMainCanvasColorChanged(); void onTotalPendingJobsCountChanged(); + void mapThemeMenuAboutToShow(); private: Qgs3DMapCanvas *mCanvas = nullptr; @@ -71,6 +74,9 @@ class APP_EXPORT Qgs3DMapCanvasDockWidget : public QgsDockWidget QLabel *mLabelPendingJobs = nullptr; Qgs3DMapToolIdentify *mMapToolIdentify = nullptr; Qgs3DMapToolMeasureLine *mMapToolMeasureLine = nullptr; + QMenu *mMapThemeMenu = nullptr; + QList mMapThemeMenuPresetActions; + QToolButton *mBtnMapThemes = nullptr; }; #endif // QGS3DMAPCANVASDOCKWIDGET_H diff --git a/src/app/3d/qgs3dmapconfigwidget.cpp b/src/app/3d/qgs3dmapconfigwidget.cpp index 17b7d6c633f4..12d8c8e69843 100644 --- a/src/app/3d/qgs3dmapconfigwidget.cpp +++ b/src/app/3d/qgs3dmapconfigwidget.cpp @@ -107,14 +107,6 @@ Qgs3DMapConfigWidget::Qgs3DMapConfigWidget( Qgs3DMapSettings *map, QgsMapCanvas widgetTerrainMaterial->setDiffuseVisible( false ); widgetTerrainMaterial->setMaterial( mMap->terrainShadingMaterial() ); - // populate combo box with map themes - const QStringList mapThemeNames = QgsProject::instance()->mapThemeCollection()->mapThemes(); - cboTerrainMapTheme->addItem( tr( "(none)" ) ); // item for no map theme - for ( QString themeName : mapThemeNames ) - cboTerrainMapTheme->addItem( themeName ); - - cboTerrainMapTheme->setCurrentText( mMap->terrainMapTheme() ); - widgetLights->setPointLights( mMap->pointLights() ); connect( cboTerrainType, static_cast( &QComboBox::currentIndexChanged ), this, &Qgs3DMapConfigWidget::onTerrainTypeChanged ); @@ -233,8 +225,6 @@ void Qgs3DMapConfigWidget::apply() mMap->setTerrainShadingEnabled( groupTerrainShading->isChecked() ); mMap->setTerrainShadingMaterial( widgetTerrainMaterial->material() ); - mMap->setTerrainMapTheme( cboTerrainMapTheme->currentText() ); - mMap->setPointLights( widgetLights->pointLights() ); } @@ -249,8 +239,6 @@ void Qgs3DMapConfigWidget::onTerrainTypeChanged() labelTerrainLayer->setVisible( genType == QgsTerrainGenerator::Dem || genType == QgsTerrainGenerator::Mesh ); cboTerrainLayer->setVisible( genType == QgsTerrainGenerator::Dem || genType == QgsTerrainGenerator::Mesh ); groupMeshTerrainShading->setVisible( genType == QgsTerrainGenerator::Mesh ); - labelTerrainMapTheme->setVisible( !( genType == QgsTerrainGenerator::Mesh ) ); - cboTerrainMapTheme->setVisible( !( genType == QgsTerrainGenerator::Mesh ) ); QgsMapLayer *oldTerrainLayer = cboTerrainLayer->currentLayer(); if ( cboTerrainType->currentData() == QgsTerrainGenerator::Dem ) diff --git a/src/app/3d/qgs3dmaptool.cpp b/src/app/3d/qgs3dmaptool.cpp index ee57dc901ab0..f37bab0102bb 100644 --- a/src/app/3d/qgs3dmaptool.cpp +++ b/src/app/3d/qgs3dmaptool.cpp @@ -51,6 +51,11 @@ QCursor Qgs3DMapTool::cursor() const return Qt::CrossCursor; } +void Qgs3DMapTool::onMapSettingsChanged() +{ + +} + Qgs3DMapCanvas *Qgs3DMapTool::canvas() { return mCanvas; diff --git a/src/app/3d/qgs3dmaptool.h b/src/app/3d/qgs3dmaptool.h index 6518c66f8f23..199a0ca62b5f 100644 --- a/src/app/3d/qgs3dmaptool.h +++ b/src/app/3d/qgs3dmaptool.h @@ -54,6 +54,10 @@ class Qgs3DMapTool : public QObject Qgs3DMapCanvas *canvas(); + private slots: + //! Called when canvas's map setting is changed + virtual void onMapSettingsChanged(); + protected: Qgs3DMapCanvas *mCanvas = nullptr; }; diff --git a/src/app/3d/qgs3dmaptoolidentify.cpp b/src/app/3d/qgs3dmaptoolidentify.cpp index 9e97916eebd1..92c602bb2a6b 100644 --- a/src/app/3d/qgs3dmaptoolidentify.cpp +++ b/src/app/3d/qgs3dmaptoolidentify.cpp @@ -17,6 +17,7 @@ #include "qgsapplication.h" #include "qgs3dmapcanvas.h" +#include "qgs3dmapcanvasdockwidget.h" #include "qgs3dmapscene.h" #include "qgs3dutils.h" #include "qgsterrainentity_p.h" @@ -63,9 +64,8 @@ void Qgs3DMapToolIdentifyPickHandler::handlePickOnVectorLayer( QgsVectorLayer *v Qgs3DMapToolIdentify::Qgs3DMapToolIdentify( Qgs3DMapCanvas *canvas ) : Qgs3DMapTool( canvas ) { - connect( mCanvas->scene(), &Qgs3DMapScene::terrainEntityChanged, this, &Qgs3DMapToolIdentify::onTerrainEntityChanged ); - mPickHandler.reset( new Qgs3DMapToolIdentifyPickHandler( this ) ); + connect( canvas, &Qgs3DMapCanvas::mapSettingsChanged, this, &Qgs3DMapToolIdentify::onMapSettingsChanged ); } Qgs3DMapToolIdentify::~Qgs3DMapToolIdentify() = default; @@ -87,6 +87,7 @@ void Qgs3DMapToolIdentify::activate() } mCanvas->scene()->registerPickHandler( mPickHandler.get() ); + mIsActive = true; } void Qgs3DMapToolIdentify::deactivate() @@ -97,6 +98,7 @@ void Qgs3DMapToolIdentify::deactivate() } mCanvas->scene()->unregisterPickHandler( mPickHandler.get() ); + mIsActive = false; } QCursor Qgs3DMapToolIdentify::cursor() const @@ -104,6 +106,13 @@ QCursor Qgs3DMapToolIdentify::cursor() const return QgsApplication::getThemeCursor( QgsApplication::Cursor::Identify ); } +void Qgs3DMapToolIdentify::onMapSettingsChanged() +{ + if ( !mIsActive ) + return; + connect( mCanvas->scene(), &Qgs3DMapScene::terrainEntityChanged, this, &Qgs3DMapToolIdentify::onTerrainEntityChanged ); +} + void Qgs3DMapToolIdentify::onTerrainPicked( Qt3DRender::QPickEvent *event ) { if ( event->button() != Qt3DRender::QPickEvent::LeftButton ) @@ -148,6 +157,8 @@ void Qgs3DMapToolIdentify::onTerrainPicked( Qt3DRender::QPickEvent *event ) void Qgs3DMapToolIdentify::onTerrainEntityChanged() { + if ( !mIsActive ) + return; // no need to disconnect from the previous entity: it has been destroyed // start listening to the new terrain entity if ( QgsTerrainEntity *terrainEntity = mCanvas->scene()->terrainEntity() ) diff --git a/src/app/3d/qgs3dmaptoolidentify.h b/src/app/3d/qgs3dmaptoolidentify.h index 2a9098f2815e..58a9b8be0009 100644 --- a/src/app/3d/qgs3dmaptoolidentify.h +++ b/src/app/3d/qgs3dmaptoolidentify.h @@ -48,10 +48,13 @@ class Qgs3DMapToolIdentify : public Qgs3DMapTool private slots: void onTerrainPicked( Qt3DRender::QPickEvent *event ); void onTerrainEntityChanged(); + void onMapSettingsChanged() override; private: std::unique_ptr mPickHandler; + bool mIsActive = false; + friend class Qgs3DMapToolIdentifyPickHandler; }; diff --git a/src/app/3d/qgs3dmaptoolmeasureline.cpp b/src/app/3d/qgs3dmaptoolmeasureline.cpp index 47fd59352226..5a84dc261594 100644 --- a/src/app/3d/qgs3dmaptoolmeasureline.cpp +++ b/src/app/3d/qgs3dmaptoolmeasureline.cpp @@ -50,7 +50,6 @@ void Qgs3DMapToolMeasureLinePickHandler::handlePickOnVectorLayer( QgsVectorLayer Qgs3DMapToolMeasureLine::Qgs3DMapToolMeasureLine( Qgs3DMapCanvas *canvas ) : Qgs3DMapTool( canvas ) { - connect( mCanvas->scene(), &Qgs3DMapScene::terrainEntityChanged, this, &Qgs3DMapToolMeasureLine::onTerrainEntityChanged ); mPickHandler.reset( new Qgs3DMapToolMeasureLinePickHandler( this ) ); // Dialog @@ -58,8 +57,7 @@ Qgs3DMapToolMeasureLine::Qgs3DMapToolMeasureLine( Qgs3DMapCanvas *canvas ) mDialog->setWindowFlags( mDialog->windowFlags() | Qt::Tool ); mDialog->restorePosition(); - // Update scale if the terrain vertical scale changed - connect( mCanvas->map(), &Qgs3DMapSettings::terrainVerticalScaleChanged, this, &Qgs3DMapToolMeasureLine::updateMeasurementLayer ); + connect( canvas, &Qgs3DMapCanvas::mapSettingsChanged, this, &Qgs3DMapToolMeasureLine::onMapSettingsChanged ); } Qgs3DMapToolMeasureLine::~Qgs3DMapToolMeasureLine() = default; @@ -124,6 +122,16 @@ QCursor Qgs3DMapToolMeasureLine::cursor() const return Qt::CrossCursor; } +void Qgs3DMapToolMeasureLine::onMapSettingsChanged() +{ + if ( !mIsAlreadyActivated ) + return; + connect( mCanvas->scene(), &Qgs3DMapScene::terrainEntityChanged, this, &Qgs3DMapToolMeasureLine::onTerrainEntityChanged ); + + // Update scale if the terrain vertical scale changed + connect( mCanvas->map(), &Qgs3DMapSettings::terrainVerticalScaleChanged, this, &Qgs3DMapToolMeasureLine::updateMeasurementLayer ); +} + void Qgs3DMapToolMeasureLine::onTerrainPicked( Qt3DRender::QPickEvent *event ) { handleClick( event, event->worldIntersection() ); @@ -131,6 +139,8 @@ void Qgs3DMapToolMeasureLine::onTerrainPicked( Qt3DRender::QPickEvent *event ) void Qgs3DMapToolMeasureLine::onTerrainEntityChanged() { + if ( !mIsAlreadyActivated ) + return; // no need to disconnect from the previous entity: it has been destroyed // start listening to the new terrain entity if ( QgsTerrainEntity *terrainEntity = mCanvas->scene()->terrainEntity() ) @@ -169,6 +179,8 @@ void Qgs3DMapToolMeasureLine::handleClick( Qt3DRender::QPickEvent *event, const void Qgs3DMapToolMeasureLine::updateMeasurementLayer() { + if ( !mMeasurementLayer ) + return; double verticalScale = canvas()->map()->terrainVerticalScale(); QgsLineString *line; if ( verticalScale != 1.0 ) @@ -199,6 +211,8 @@ void Qgs3DMapToolMeasureLine::updateMeasurementLayer() void Qgs3DMapToolMeasureLine::updateSettings() { + if ( !mMeasurementLayer ) + return; // Line style QgsLine3DSymbol *lineSymbol = new QgsLine3DSymbol; lineSymbol->setRenderAsSimpleLines( true ); diff --git a/src/app/3d/qgs3dmaptoolmeasureline.h b/src/app/3d/qgs3dmaptoolmeasureline.h index 27631b9dae95..2be6c3ba4c1d 100644 --- a/src/app/3d/qgs3dmaptoolmeasureline.h +++ b/src/app/3d/qgs3dmaptoolmeasureline.h @@ -71,6 +71,7 @@ class Qgs3DMapToolMeasureLine : public Qgs3DMapTool void onTerrainPicked( Qt3DRender::QPickEvent *event ); void onTerrainEntityChanged(); void handleClick( Qt3DRender::QPickEvent *event, const QgsVector3D &worldIntersection ); + void onMapSettingsChanged() override; private: std::unique_ptr mPickHandler; diff --git a/src/app/3d/qgsmesh3dsymbolwidget.cpp b/src/app/3d/qgsmesh3dsymbolwidget.cpp index 477cdd30439c..02414f91a0f0 100644 --- a/src/app/3d/qgsmesh3dsymbolwidget.cpp +++ b/src/app/3d/qgsmesh3dsymbolwidget.cpp @@ -71,7 +71,7 @@ QgsMesh3dSymbolWidget::QgsMesh3dSymbolWidget( QgsMeshLayer *meshLayer, QWidget * void QgsMesh3dSymbolWidget::setSymbol( const QgsMesh3DSymbol &symbol ) { - // Advanced symbology + mSymbol = symbol; mChkSmoothTriangles->setChecked( symbol.smoothedTriangles() ); mChkWireframe->setChecked( symbol.wireframeEnabled() ); mColorButtonWireframe->setColor( symbol.wireframeLineColor() ); @@ -143,7 +143,7 @@ double QgsMesh3dSymbolWidget::lineEditValue( const QLineEdit *lineEdit ) const QgsMesh3DSymbol QgsMesh3dSymbolWidget::symbol() const { - QgsMesh3DSymbol sym; + QgsMesh3DSymbol sym = mSymbol; sym.setSmoothedTriangles( mChkSmoothTriangles->isChecked() ); sym.setWireframeEnabled( mChkWireframe->isChecked() ); diff --git a/src/app/3d/qgsmesh3dsymbolwidget.h b/src/app/3d/qgsmesh3dsymbolwidget.h index a28c68d9ac4e..fdb5e352f4da 100644 --- a/src/app/3d/qgsmesh3dsymbolwidget.h +++ b/src/app/3d/qgsmesh3dsymbolwidget.h @@ -18,6 +18,7 @@ #include +#include "qgsmesh3dsymbol.h" #include "ui_qgsmesh3dpropswidget.h" class QgsMeshDatasetGroupListModel; @@ -57,6 +58,7 @@ class QgsMesh3dSymbolWidget : public QWidget, private Ui::QgsMesh3dPropsWidget void setColorRampMinMax( double min, double max ); QgsMeshLayer *mLayer = nullptr; QgsMeshDatasetGroupListModel *mDatasetGroupListModel = nullptr; + QgsMesh3DSymbol mSymbol; }; diff --git a/src/app/3d/qgsmeshlayer3drendererwidget.cpp b/src/app/3d/qgsmeshlayer3drendererwidget.cpp index b60cbd899fa2..4618c4e7a6a3 100644 --- a/src/app/3d/qgsmeshlayer3drendererwidget.cpp +++ b/src/app/3d/qgsmeshlayer3drendererwidget.cpp @@ -31,14 +31,14 @@ QgsMeshLayer3DRendererWidget::QgsMeshLayer3DRendererWidget( QgsMeshLayer *layer, QVBoxLayout *layout = new QVBoxLayout( this ); layout->setContentsMargins( 0, 0, 0, 0 ); - chkEnabled = new QCheckBox( tr( "Enable 3D Renderer" ), this ); - layout->addWidget( chkEnabled ); + mChkEnabled = new QCheckBox( tr( "Enable 3D Renderer" ), this ); + layout->addWidget( mChkEnabled ); mWidgetMesh = new QgsMesh3dSymbolWidget( layer, this ); mWidgetMesh->configureForDataset(); layout->addWidget( mWidgetMesh ); - connect( chkEnabled, &QCheckBox::clicked, this, &QgsMeshLayer3DRendererWidget::onEnabledClicked ); + connect( mChkEnabled, &QCheckBox::clicked, this, &QgsMeshLayer3DRendererWidget::onEnabledClicked ); connect( mWidgetMesh, &QgsMesh3dSymbolWidget::changed, this, &QgsMeshLayer3DRendererWidget::widgetChanged ); } @@ -52,6 +52,7 @@ void QgsMeshLayer3DRendererWidget::setLayer( QgsMeshLayer *layer ) { QgsMeshLayer3DRenderer *meshRenderer = static_cast( r ); setRenderer( meshRenderer ); + mWidgetMesh->setEnabled( meshRenderer->symbol()->isEnabled() ); } else { @@ -62,23 +63,15 @@ void QgsMeshLayer3DRendererWidget::setLayer( QgsMeshLayer *layer ) void QgsMeshLayer3DRendererWidget::setRenderer( const QgsMeshLayer3DRenderer *renderer ) { mRenderer.reset( renderer ? renderer->clone() : nullptr ); - whileBlocking( chkEnabled )->setChecked( ( bool )mRenderer ); + whileBlocking( mChkEnabled )->setChecked( renderer ? renderer->symbol()->isEnabled() : false ); } QgsMeshLayer3DRenderer *QgsMeshLayer3DRendererWidget::renderer() { - if ( chkEnabled->isChecked() ) - { - QgsMesh3DSymbol *sym = new QgsMesh3DSymbol( mWidgetMesh->symbol() ); - QgsMeshLayer3DRenderer *r = new QgsMeshLayer3DRenderer( sym ); - r->setLayer( qobject_cast( mLayer ) ); - mRenderer.reset( r ); - } - else - { - mRenderer.reset(); - } - + QgsMesh3DSymbol *sym = new QgsMesh3DSymbol( mWidgetMesh->symbol() ); + sym->setEnabled( mChkEnabled->isChecked() ); + mRenderer.reset( new QgsMeshLayer3DRenderer( sym ) ); + mRenderer->setLayer( qobject_cast( mLayer ) ); return mRenderer.get(); } @@ -90,6 +83,6 @@ void QgsMeshLayer3DRendererWidget::apply() void QgsMeshLayer3DRendererWidget::onEnabledClicked() { - mWidgetMesh->setEnabled( chkEnabled->isChecked() ); + mWidgetMesh->setEnabled( mChkEnabled->isChecked() ); emit widgetChanged(); } diff --git a/src/app/3d/qgsmeshlayer3drendererwidget.h b/src/app/3d/qgsmeshlayer3drendererwidget.h index 8662f5f214ef..b456e9d006e6 100644 --- a/src/app/3d/qgsmeshlayer3drendererwidget.h +++ b/src/app/3d/qgsmeshlayer3drendererwidget.h @@ -49,7 +49,7 @@ class QgsMeshLayer3DRendererWidget : public QgsMapLayerConfigWidget void onEnabledClicked(); private: - QCheckBox *chkEnabled = nullptr; + QCheckBox *mChkEnabled = nullptr; QgsMesh3dSymbolWidget *mWidgetMesh = nullptr; std::unique_ptr mRenderer; }; diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 84e34c814fee..0fc069c3ebf6 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -27,6 +27,7 @@ SET(QGIS_APP_SRCS qgscustomization.cpp qgscustomprojectiondialog.cpp qgsdatumtransformtablewidget.cpp + qgsdevtoolspanelwidget.cpp qgsdiscoverrelationsdialog.cpp qgsdxfexportdialog.cpp qgsformannotationdialog.cpp @@ -61,6 +62,7 @@ SET(QGIS_APP_SRCS qgsstatusbarmagnifierwidget.cpp qgsstatusbarscalewidget.cpp qgstemplateprojectsmodel.cpp + qgstemporalcontrollerdockwidget.cpp qgsversioninfo.cpp qgsrecentprojectsitemsmodel.cpp qgsvectorlayerdigitizingproperties.cpp @@ -162,6 +164,12 @@ SET(QGIS_APP_SRCS browser/qgsinbuiltdataitemproviders.cpp + devtools/qgsappdevtoolutils.cpp + devtools/networklogger/qgsnetworklogger.cpp + devtools/networklogger/qgsnetworkloggernode.cpp + devtools/networklogger/qgsnetworkloggerpanelwidget.cpp + devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp + labeling/qgslabelpropertydialog.cpp labeling/qgsmaptoolchangelabelproperties.cpp labeling/qgsmaptoolpinlabels.cpp @@ -211,7 +219,7 @@ SET(QGIS_APP_SRCS mesh/qgsmeshrendereractivedatasetwidget.cpp mesh/qgsmeshdatasetgrouptreeview.cpp mesh/qgsmeshcalculatordialog.cpp - mesh/qgsmeshtimeformatdialog.cpp + mesh/qgsmeshstaticdatasetwidget.cpp ) IF (WITH_3D) @@ -371,6 +379,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/app ${CMAKE_SOURCE_DIR}/src/app/decorations + ${CMAKE_SOURCE_DIR}/src/app/devtools/networklogger ${CMAKE_SOURCE_DIR}/src/app/labeling ${CMAKE_SOURCE_DIR}/src/app/layout ${CMAKE_SOURCE_DIR}/src/app/pluginmanager @@ -403,11 +412,13 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/effects ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/attributeformconfig ${CMAKE_SOURCE_DIR}/src/gui/symbology ${CMAKE_SOURCE_DIR}/src/gui/attributetable ${CMAKE_SOURCE_DIR}/src/gui/auth + ${CMAKE_SOURCE_DIR}/src/gui/devtools ${CMAKE_SOURCE_DIR}/src/gui/labeling ${CMAKE_SOURCE_DIR}/src/gui/numericformats ${CMAKE_SOURCE_DIR}/src/gui/ogr @@ -419,6 +430,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/gui/layertree ${CMAKE_SOURCE_DIR}/src/gui/locator ${CMAKE_SOURCE_DIR}/src/gui/vector + ${CMAKE_SOURCE_DIR}/src/gui/vectortile ${CMAKE_SOURCE_DIR}/src/plugins ${CMAKE_SOURCE_DIR}/src/python ${CMAKE_SOURCE_DIR}/src/native diff --git a/src/app/browser/qgsinbuiltdataitemproviders.cpp b/src/app/browser/qgsinbuiltdataitemproviders.cpp index de79dfaa8d47..6d6898a26f9c 100644 --- a/src/app/browser/qgsinbuiltdataitemproviders.cpp +++ b/src/app/browser/qgsinbuiltdataitemproviders.cpp @@ -447,6 +447,7 @@ void QgsLayerItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *men case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } } ); diff --git a/src/app/decorations/qgsdecorationcopyrightdialog.cpp b/src/app/decorations/qgsdecorationcopyrightdialog.cpp index b60e31399192..64df48c2da95 100644 --- a/src/app/decorations/qgsdecorationcopyrightdialog.cpp +++ b/src/app/decorations/qgsdecorationcopyrightdialog.cpp @@ -130,5 +130,5 @@ void QgsDecorationCopyrightDialog::apply() void QgsDecorationCopyrightDialog::showHelp() { - QgsHelp::openHelp( QStringLiteral( "introduction/general_tools.html#copyright-and-title-labels" ) ); + QgsHelp::openHelp( QStringLiteral( "introduction/general_tools.html#copyright_decoration" ) ); } diff --git a/src/app/decorations/qgsdecorationscalebar.cpp b/src/app/decorations/qgsdecorationscalebar.cpp index 5bb26ec8ea4f..9f65da5c58c8 100644 --- a/src/app/decorations/qgsdecorationscalebar.cpp +++ b/src/app/decorations/qgsdecorationscalebar.cpp @@ -35,6 +35,7 @@ email : sbr00pwb@users.sourceforge.net #include "qgsunittypes.h" #include "qgssettings.h" #include "qgssymbollayerutils.h" +#include "qgsfillsymbollayer.h" #include "qgsdoubleboxscalebarrenderer.h" #include "qgsnumericscalebarrenderer.h" @@ -153,21 +154,54 @@ void QgsDecorationScaleBar::setupScaleBar() std::unique_ptr< QgsTicksScaleBarRenderer > tickStyle = qgis::make_unique< QgsTicksScaleBarRenderer >(); tickStyle->setTickPosition( mStyleIndex == 0 ? QgsTicksScaleBarRenderer::TicksDown : QgsTicksScaleBarRenderer::TicksUp ); mStyle = std::move( tickStyle ); - mSettings.setFillColor( mColor ); - mSettings.setLineColor( mColor ); // Compatibility with pre 3.2 configuration + + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + fillSymbol->setColor( mColor ); // Compatibility with pre 3.2 configuration + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( fillSymbol->symbolLayer( 0 ) ) ) + { + fill->setStrokeStyle( Qt::NoPen ); + } + mSettings.setFillSymbol( fillSymbol.release() ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + lineSymbol->setColor( mColor ); // Compatibility with pre 3.2 configuration + lineSymbol->setWidth( 0.3 ); + lineSymbol->setOutputUnit( QgsUnitTypes::RenderMillimeters ); + mSettings.setLineSymbol( lineSymbol.release() ); mSettings.setHeight( 2.2 ); - mSettings.setLineWidth( 0.3 ); break; } case 2: case 3: + { mStyle = qgis::make_unique< QgsSingleBoxScaleBarRenderer >(); - mSettings.setFillColor( mColor ); - mSettings.setFillColor2( QColor( "transparent" ) ); - mSettings.setLineColor( mOutlineColor ); + + + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + fillSymbol->setColor( mColor ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( fillSymbol->symbolLayer( 0 ) ) ) + { + fill->setStrokeStyle( Qt::NoPen ); + } + mSettings.setFillSymbol( fillSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2 = qgis::make_unique< QgsFillSymbol >(); + fillSymbol2->setColor( QColor( 255, 255, 255, 0 ) ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( fillSymbol2->symbolLayer( 0 ) ) ) + { + fill->setStrokeStyle( Qt::NoPen ); + } + mSettings.setAlternateFillSymbol( fillSymbol2.release() ); + mSettings.setHeight( mStyleIndex == 2 ? 1 : 3 ); - mSettings.setLineWidth( mStyleIndex == 2 ? 0.2 : 0.3 ); + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + lineSymbol->setColor( mOutlineColor ); // Compatibility with pre 3.2 configuration + lineSymbol->setWidth( mStyleIndex == 2 ? 0.2 : 0.3 ); + lineSymbol->setOutputUnit( QgsUnitTypes::RenderMillimeters ); + mSettings.setLineSymbol( lineSymbol.release() ); + break; + } } mSettings.setLabelBarSpace( 1.8 ); } @@ -317,7 +351,7 @@ void QgsDecorationScaleBar::render( const QgsMapSettings &mapSettings, QgsRender scaleContext.scale = mapSettings.scale(); //Calculate total width of scale bar and label - QSizeF size = mStyle->calculateBoxSize( mSettings, scaleContext ); + QSizeF size = mStyle->calculateBoxSize( context, mSettings, scaleContext ); size.setWidth( context.convertToPainterUnits( size.width(), QgsUnitTypes::RenderMillimeters ) ); size.setHeight( context.convertToPainterUnits( size.height(), QgsUnitTypes::RenderMillimeters ) ); diff --git a/src/app/decorations/qgsdecorationtitledialog.cpp b/src/app/decorations/qgsdecorationtitledialog.cpp index bb8ea571a478..eeb524f99d7b 100644 --- a/src/app/decorations/qgsdecorationtitledialog.cpp +++ b/src/app/decorations/qgsdecorationtitledialog.cpp @@ -138,5 +138,5 @@ void QgsDecorationTitleDialog::apply() void QgsDecorationTitleDialog::showHelp() { - QgsHelp::openHelp( QStringLiteral( "introduction/general_tools.html#copyright-and-title-labels" ) ); + QgsHelp::openHelp( QStringLiteral( "introduction/general_tools.html#title_label_decoration" ) ); } diff --git a/src/app/devtools/networklogger/qgsnetworklogger.cpp b/src/app/devtools/networklogger/qgsnetworklogger.cpp new file mode 100644 index 000000000000..06539218f2fd --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworklogger.cpp @@ -0,0 +1,333 @@ +/*************************************************************************** + qgsnetworklogger.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsnetworklogger.h" +#include "qgsnetworkloggernode.h" +#include "qgssettings.h" +#include "qgis.h" +#include +#include +#include + +QgsNetworkLogger::QgsNetworkLogger( QgsNetworkAccessManager *manager, QObject *parent ) + : QAbstractItemModel( parent ) + , mNam( manager ) + , mRootNode( qgis::make_unique< QgsNetworkLoggerRootNode >() ) +{ + // logger must be created on the main thread + Q_ASSERT( QThread::currentThread() == QApplication::instance()->thread() ); + Q_ASSERT( mNam->thread() == QApplication::instance()->thread() ); + + if ( QgsSettings().value( QStringLiteral( "logNetworkRequests" ), false, QgsSettings::App ).toBool() ) + enableLogging( true ); +} + +bool QgsNetworkLogger::isLogging() const +{ + return mIsLogging; +} + +QgsNetworkLogger::~QgsNetworkLogger() = default; + +void QgsNetworkLogger::enableLogging( bool enabled ) +{ + if ( enabled ) + { + connect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestAboutToBeCreated ), this, &QgsNetworkLogger::requestAboutToBeCreated, Qt::UniqueConnection ); + connect( mNam, qgis::overload< QgsNetworkReplyContent >::of( &QgsNetworkAccessManager::finished ), this, &QgsNetworkLogger::requestFinished, Qt::UniqueConnection ); + connect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsNetworkLogger::requestTimedOut, Qt::UniqueConnection ); + connect( mNam, &QgsNetworkAccessManager::downloadProgress, this, &QgsNetworkLogger::downloadProgress, Qt::UniqueConnection ); + connect( mNam, &QgsNetworkAccessManager::requestEncounteredSslErrors, this, &QgsNetworkLogger::requestEncounteredSslErrors, Qt::UniqueConnection ); + } + else + { + disconnect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestAboutToBeCreated ), this, &QgsNetworkLogger::requestAboutToBeCreated ); + disconnect( mNam, qgis::overload< QgsNetworkReplyContent >::of( &QgsNetworkAccessManager::finished ), this, &QgsNetworkLogger::requestFinished ); + disconnect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsNetworkLogger::requestTimedOut ); + disconnect( mNam, &QgsNetworkAccessManager::downloadProgress, this, &QgsNetworkLogger::downloadProgress ); + disconnect( mNam, &QgsNetworkAccessManager::requestEncounteredSslErrors, this, &QgsNetworkLogger::requestEncounteredSslErrors ); + } + mIsLogging = enabled; +} + +void QgsNetworkLogger::clear() +{ + beginResetModel(); + mRequestGroups.clear(); + mRootNode->clear(); + endResetModel(); +} + +void QgsNetworkLogger::requestAboutToBeCreated( QgsNetworkRequestParameters parameters ) +{ + const int childCount = mRootNode->childCount(); + + beginInsertRows( QModelIndex(), childCount, childCount ); + + std::unique_ptr< QgsNetworkLoggerRequestGroup > group = qgis::make_unique< QgsNetworkLoggerRequestGroup >( parameters ); + mRequestGroups.insert( parameters.requestId(), group.get() ); + mRootNode->addChild( std::move( group ) ); + endInsertRows(); +} + +void QgsNetworkLogger::requestFinished( QgsNetworkReplyContent content ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( content.requestId() ); + if ( !requestGroup ) + return; + + // find the row: the position of the request in the rootNode + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + beginInsertRows( requestIndex, requestGroup->childCount(), requestGroup->childCount() ); + requestGroup->setReply( content ); + endInsertRows(); + + emit dataChanged( requestIndex, requestIndex ); +} + +void QgsNetworkLogger::requestTimedOut( QgsNetworkRequestParameters parameters ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( parameters.requestId() ); + if ( !requestGroup ) + return; + + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + requestGroup->setTimedOut(); + + emit dataChanged( requestIndex, requestIndex ); +} + +void QgsNetworkLogger::downloadProgress( int requestId, qint64 bytesReceived, qint64 bytesTotal ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( requestId ); + if ( !requestGroup ) + return; + + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + requestGroup->setProgress( bytesReceived, bytesTotal ); + + emit dataChanged( requestIndex, requestIndex, QVector() << Qt::ToolTipRole ); +} + +void QgsNetworkLogger::requestEncounteredSslErrors( int requestId, const QList &errors ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( requestId ); + if ( !requestGroup ) + return; + + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + beginInsertRows( requestIndex, requestGroup->childCount(), requestGroup->childCount() ); + requestGroup->setSslErrors( errors ); + endInsertRows(); + + emit dataChanged( requestIndex, requestIndex ); +} + +QgsNetworkLoggerNode *QgsNetworkLogger::index2node( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return mRootNode.get(); + + return reinterpret_cast( index.internalPointer() ); +} + +QList QgsNetworkLogger::actions( const QModelIndex &index, QObject *parent ) +{ + QgsNetworkLoggerNode *node = index2node( index ); + if ( !node ) + return QList< QAction * >(); + + return node->actions( parent ); +} + +QModelIndex QgsNetworkLogger::node2index( QgsNetworkLoggerNode *node ) const +{ + if ( !node || !node->parent() ) + return QModelIndex(); // this is the only root item -> invalid index + + QModelIndex parentIndex = node2index( node->parent() ); + + int row = node->parent()->indexOf( node ); + Q_ASSERT( row >= 0 ); + return index( row, 0, parentIndex ); +} + +QModelIndex QgsNetworkLogger::indexOfParentLayerTreeNode( QgsNetworkLoggerNode *parentNode ) const +{ + Q_ASSERT( parentNode ); + + QgsNetworkLoggerGroup *grandParentNode = parentNode->parent(); + if ( !grandParentNode ) + return QModelIndex(); // root node -> invalid index + + int row = grandParentNode->indexOf( parentNode ); + Q_ASSERT( row >= 0 ); + + return createIndex( row, 0, parentNode ); +} + +void QgsNetworkLogger::removeRows( const QList &rows ) +{ + QList< int > res = rows; + std::sort( res.begin(), res.end(), std::greater< int >() ); + + for ( int row : qgis::as_const( res ) ) + { + int popId = data( index( row, 0, QModelIndex() ), QgsNetworkLoggerNode::RoleId ).toInt(); + mRequestGroups.remove( popId ); + + beginRemoveRows( QModelIndex(), row, row ); + mRootNode->removeRow( row ); + endRemoveRows(); + } +} + +QgsNetworkLoggerRootNode *QgsNetworkLogger::rootGroup() +{ + return mRootNode.get(); +} + +int QgsNetworkLogger::rowCount( const QModelIndex &parent ) const +{ + QgsNetworkLoggerNode *n = index2node( parent ); + if ( !n ) + return 0; + + return n->childCount(); +} + +int QgsNetworkLogger::columnCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ) + return 1; +} + +QModelIndex QgsNetworkLogger::index( int row, int column, const QModelIndex &parent ) const +{ + if ( column < 0 || column >= columnCount( parent ) || + row < 0 || row >= rowCount( parent ) ) + return QModelIndex(); + + QgsNetworkLoggerGroup *n = dynamic_cast< QgsNetworkLoggerGroup * >( index2node( parent ) ); + if ( !n ) + return QModelIndex(); // have no children + + return createIndex( row, column, n->childAt( row ) ); +} + +QModelIndex QgsNetworkLogger::parent( const QModelIndex &child ) const +{ + if ( !child.isValid() ) + return QModelIndex(); + + if ( QgsNetworkLoggerNode *n = index2node( child ) ) + { + return indexOfParentLayerTreeNode( n->parent() ); // must not be null + } + else + { + Q_ASSERT( false ); + return QModelIndex(); + } +} + +QVariant QgsNetworkLogger::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() || index.column() > 1 ) + return QVariant(); + + QgsNetworkLoggerNode *node = index2node( index ); + if ( !node ) + return QVariant(); + + return node->data( role ); +} + +Qt::ItemFlags QgsNetworkLogger::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + { + Qt::ItemFlags rootFlags = Qt::ItemFlags(); + return rootFlags; + } + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + return f; +} + +QVariant QgsNetworkLogger::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) + return tr( "Requests" ); + return QVariant(); +} + + +// +// QgsNetworkLoggerProxyModel +// + +QgsNetworkLoggerProxyModel::QgsNetworkLoggerProxyModel( QgsNetworkLogger *logger, QObject *parent ) + : QSortFilterProxyModel( parent ) + , mLogger( logger ) +{ + setSourceModel( mLogger ); +} + +void QgsNetworkLoggerProxyModel::setFilterString( const QString &string ) +{ + mFilterString = string; + invalidateFilter(); +} + +void QgsNetworkLoggerProxyModel::setShowSuccessful( bool show ) +{ + mShowSuccessful = show; + invalidateFilter(); +} + +void QgsNetworkLoggerProxyModel::setShowTimeouts( bool show ) +{ + mShowTimeouts = show; + invalidateFilter(); +} + +bool QgsNetworkLoggerProxyModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const +{ + QgsNetworkLoggerNode *node = mLogger->index2node( mLogger->index( source_row, 0, source_parent ) ); + if ( QgsNetworkLoggerRequestGroup *request = dynamic_cast< QgsNetworkLoggerRequestGroup * >( node ) ) + { + if ( ( request->status() == QgsNetworkLoggerRequestGroup::Status::Complete || request->status() == QgsNetworkLoggerRequestGroup::Status::Canceled ) + & !mShowSuccessful ) + return false; + else if ( request->status() == QgsNetworkLoggerRequestGroup::Status::TimeOut && !mShowTimeouts ) + return false; + return mFilterString.isEmpty() || request->url().url().contains( mFilterString, Qt::CaseInsensitive ); + } + + return true; +} diff --git a/src/app/devtools/networklogger/qgsnetworklogger.h b/src/app/devtools/networklogger/qgsnetworklogger.h new file mode 100644 index 000000000000..eea54102aa5d --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworklogger.h @@ -0,0 +1,169 @@ +/*************************************************************************** + qgsnetworklogger.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSNETWORKLOGGER_H +#define QGSNETWORKLOGGER_H + +#include +#include +#include +#include "qgsnetworkaccessmanager.h" + +class QgsNetworkLoggerNode; +class QgsNetworkLoggerRequestGroup; +class QgsNetworkLoggerRootNode; +class QAction; + +/** + * \ingroup app + * \class QgsNetworkLogger + * \brief Logs network requests from a QgsNetworkAccessManager, converting them + * to a QAbstractItemModel representing the request and response details. + * + * \since QGIS 3.14 + */ +class QgsNetworkLogger : public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsNetworkLogger, logging requests from the specified \a manager. + * + * \warning QgsNetworkLogger must be created on the main thread, using the main thread's + * QgsNetworkAccessManager instance. + */ + QgsNetworkLogger( QgsNetworkAccessManager *manager, QObject *parent ); + ~QgsNetworkLogger() override; + + /** + * Returns TRUE if the logger is currently logging activity. + */ + bool isLogging() const; + + // Implementation of virtual functions from QAbstractItemModel + + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + + /** + * Returns node for given index. Returns root node for invalid index. + */ + QgsNetworkLoggerNode *index2node( const QModelIndex &index ) const; + + /** + * Returns a list of actions corresponding to the item at the specified \a index. + * + * The actions should be parented to \a parent. + */ + QList< QAction * > actions( const QModelIndex &index, QObject *parent ); + + + /** + * Removes a list of request \a rows from the log. + */ + void removeRows( const QList< int > &rows ); + + /** + * Returns the root node of the log. + */ + QgsNetworkLoggerRootNode *rootGroup(); + + static constexpr int MAX_LOGGED_REQUESTS = 1000; + + public slots: + + /** + * Enables or disables logging, depending on the value of \a enabled. + */ + void enableLogging( bool enabled ); + + /** + * Clears all logged entries. + */ + void clear(); + + private slots: + void requestAboutToBeCreated( QgsNetworkRequestParameters parameters ); + void requestFinished( QgsNetworkReplyContent content ); + void requestTimedOut( QgsNetworkRequestParameters parameters ); + void downloadProgress( int requestId, qint64 bytesReceived, qint64 bytesTotal ); + void requestEncounteredSslErrors( int requestId, const QList &errors ); + + private: + + //! Returns index for a given node + QModelIndex node2index( QgsNetworkLoggerNode *node ) const; + QModelIndex indexOfParentLayerTreeNode( QgsNetworkLoggerNode *parentNode ) const; + + QgsNetworkAccessManager *mNam = nullptr; + bool mIsLogging = false; + + std::unique_ptr< QgsNetworkLoggerRootNode > mRootNode; + + QHash< int, QgsNetworkLoggerRequestGroup * > mRequestGroups; + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerProxyModel + * \brief A proxy model for filtering QgsNetworkLogger models by url string subsets + * or request status. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerProxyModel : public QSortFilterProxyModel +{ + public: + + /** + * Constructor for QgsNetworkLoggerProxyModel, filtering the specified network \a logger. + */ + QgsNetworkLoggerProxyModel( QgsNetworkLogger *logger, QObject *parent ); + + /** + * Sets a filter \a string to apply to request URLs. + */ + void setFilterString( const QString &string ); + + /** + * Sets whether successful requests should be shown. + */ + void setShowSuccessful( bool show ); + + /** + * Sets whether timed out requests should be shown. + */ + void setShowTimeouts( bool show ); + + protected: + bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override; + + private: + + QgsNetworkLogger *mLogger = nullptr; + + QString mFilterString; + bool mShowSuccessful = true; + bool mShowTimeouts = true; +}; + +#endif // QGSNETWORKLOGGER_H diff --git a/src/app/devtools/networklogger/qgsnetworkloggernode.cpp b/src/app/devtools/networklogger/qgsnetworkloggernode.cpp new file mode 100644 index 000000000000..2f0d1bbf3186 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggernode.cpp @@ -0,0 +1,627 @@ +/*************************************************************************** + qgsnetworkloggernode.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsnetworkloggernode.h" +#include "qgis.h" +#include "qgsjsonutils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// +// QgsNetworkLoggerNode +// + +QgsNetworkLoggerNode::QgsNetworkLoggerNode() = default; +QgsNetworkLoggerNode::~QgsNetworkLoggerNode() = default; + + +// +// QgsNetworkLoggerGroup +// + +QgsNetworkLoggerGroup::QgsNetworkLoggerGroup( const QString &title ) + : mGroupTitle( title ) +{ +} + +void QgsNetworkLoggerGroup::addChild( std::unique_ptr child ) +{ + if ( !child ) + return; + + Q_ASSERT( !child->mParent ); + child->mParent = this; + + mChildren.emplace_back( std::move( child ) ); +} + +int QgsNetworkLoggerGroup::indexOf( QgsNetworkLoggerNode *child ) const +{ + Q_ASSERT( child->mParent == this ); + auto it = std::find_if( mChildren.begin(), mChildren.end(), [&]( const std::unique_ptr &p ) + { + return p.get() == child; + } ); + if ( it != mChildren.end() ) + return std::distance( mChildren.begin(), it ); + return -1; +} + +QgsNetworkLoggerNode *QgsNetworkLoggerGroup::childAt( int index ) +{ + Q_ASSERT( static_cast< std::size_t >( index ) < mChildren.size() ); + return mChildren[ index ].get(); +} + +void QgsNetworkLoggerGroup::clear() +{ + mChildren.clear(); +} + +QVariant QgsNetworkLoggerGroup::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + return mGroupTitle; + + default: + break; + } + return QVariant(); +} + +QVariant QgsNetworkLoggerGroup::toVariant() const +{ + QVariantMap res; + for ( const std::unique_ptr< QgsNetworkLoggerNode > &child : mChildren ) + { + if ( const QgsNetworkLoggerValueNode *valueNode = dynamic_cast< const QgsNetworkLoggerValueNode *>( child.get() ) ) + { + res.insert( valueNode->key(), valueNode->value() ); + } + } + return res; +} + +// +// QgsNetworkLoggerRootNode +// + +QgsNetworkLoggerRootNode::QgsNetworkLoggerRootNode() + : QgsNetworkLoggerGroup( QString() ) +{ + +} + +QVariant QgsNetworkLoggerRootNode::data( int ) const +{ + return QVariant(); +} + +void QgsNetworkLoggerRootNode::removeRow( int row ) +{ + mChildren.erase( mChildren.begin() + row ); +} + +QVariant QgsNetworkLoggerRootNode::toVariant() const +{ + QVariantList res; + for ( const std::unique_ptr< QgsNetworkLoggerNode > &child : mChildren ) + res << child->toVariant(); + return res; +} + + +// +// QgsNetworkLoggerValueNode +// +QgsNetworkLoggerValueNode::QgsNetworkLoggerValueNode( const QString &key, const QString &value, const QColor &color ) + : mKey( key ) + , mValue( value ) + , mColor( color ) +{ + +} + +QVariant QgsNetworkLoggerValueNode::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + case Qt::ToolTipRole: + { + return QStringLiteral( "%1: %2" ).arg( mKey.leftJustified( 30, ' ' ), mValue ); + } + + case Qt::ForegroundRole: + { + if ( mColor.isValid() ) + return QBrush( mColor ); + break; + } + default: + break; + } + return QVariant(); +} + +// +// QgsNetworkLoggerGroup +// + +void QgsNetworkLoggerGroup::addKeyValueNode( const QString &key, const QString &value, const QColor &color ) +{ + addChild( qgis::make_unique< QgsNetworkLoggerValueNode >( key, value, color ) ); +} + + +// +// QgsNetworkLoggerRequestGroup +// + +QgsNetworkLoggerRequestGroup::QgsNetworkLoggerRequestGroup( const QgsNetworkRequestParameters &request ) + : QgsNetworkLoggerGroup( QString() ) + , mUrl( request.request().url() ) + , mRequestId( request.requestId() ) + , mOperation( request.operation() ) + , mData( request.content() ) +{ + const QList headers = request.request().rawHeaderList(); + for ( const QByteArray &header : headers ) + { + mHeaders.append( qMakePair( QString( header ), QString( request.request().rawHeader( header ) ) ) ); + } + + std::unique_ptr< QgsNetworkLoggerRequestDetailsGroup > detailsGroup = qgis::make_unique< QgsNetworkLoggerRequestDetailsGroup >( request ); + mDetailsGroup = detailsGroup.get(); + addChild( std::move( detailsGroup ) ); + + mTimer.start(); +} + +QVariant QgsNetworkLoggerRequestGroup::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + return QStringLiteral( "%1 %2 %3" ).arg( QString::number( mRequestId ), + operationToString( mOperation ), + mUrl.url() ); + + case Qt::ToolTipRole: + { + QString bytes = QObject::tr( "unknown" ); + if ( mBytesTotal != 0 ) + { + if ( mBytesReceived > 0 && mBytesReceived < mBytesTotal ) + bytes = QStringLiteral( "%1/%2" ).arg( QString::number( mBytesReceived ), QString::number( mBytesTotal ) ); + else if ( mBytesReceived > 0 && mBytesReceived == mBytesTotal ) + bytes = QString::number( mBytesTotal ); + } + // ?? adding
    instead of \n after (very long) url seems to break url up + // COMPLETE, Status: 200 - text/xml; charset=utf-8 - 2334 bytes - 657 milliseconds + return QStringLiteral( "%1
    %2 - Status: %3 - %4 - %5 bytes - %6 msec - %7 replies" ) + .arg( mUrl.url(), + statusToString( mStatus ), + QString::number( mHttpStatus ), + mContentType, + bytes, + mStatus == Status::Pending ? QString::number( mTimer.elapsed() / 1000 ) : QString::number( mTotalTime ), + QString::number( mReplies ) ); + } + + case RoleStatus: + return static_cast< int >( mStatus ); + + case RoleId: + return mRequestId; + + case Qt::ForegroundRole: + { + if ( mHasSslErrors ) + return QBrush( QColor( 180, 65, 210 ) ); + switch ( mStatus ) + { + case QgsNetworkLoggerRequestGroup::Status::Pending: + case QgsNetworkLoggerRequestGroup::Status::Canceled: + return QBrush( QColor( 0, 0, 0, 100 ) ); + case QgsNetworkLoggerRequestGroup::Status::Error: + return QBrush( QColor( 235, 10, 10 ) ); + case QgsNetworkLoggerRequestGroup::Status::TimeOut: + return QBrush( QColor( 235, 10, 10 ) ); + case QgsNetworkLoggerRequestGroup::Status::Complete: + break; + } + break; + } + + case Qt::FontRole: + { + if ( mStatus == Status::Canceled ) + { + QFont f; + f.setStrikeOut( true ); + return f; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QList QgsNetworkLoggerRequestGroup::actions( QObject *parent ) +{ + QList< QAction * > res; + QAction *openUrlAction = new QAction( QObject::tr( "Open URL" ), parent ); + QObject::connect( openUrlAction, &QAction::triggered, openUrlAction, [ = ] + { + QDesktopServices::openUrl( mUrl ); + } ); + res << openUrlAction; + + QAction *copyUrlAction = new QAction( QObject::tr( "Copy URL" ), parent ); + QObject::connect( copyUrlAction, &QAction::triggered, openUrlAction, [ = ] + { + QApplication::clipboard()->setText( mUrl.url() ); + } ); + res << copyUrlAction; + + QAction *copyAsCurlAction = new QAction( QObject::tr( "Copy As cURL" ), parent ); + QObject::connect( copyAsCurlAction, &QAction::triggered, copyAsCurlAction, [ = ] + { + QString curlHeaders; + for ( const QPair< QString, QString > &header : qgis::as_const( mHeaders ) ) + curlHeaders += QStringLiteral( "-H '%1: %2' " ).arg( header.first, header.second ); + + QString curlData; + if ( mOperation == QNetworkAccessManager::PostOperation || mOperation == QNetworkAccessManager::PutOperation ) + curlData = QStringLiteral( "--data '%1' " ).arg( QString( mData ) ); + + QString curlCmd = QStringLiteral( "curl '%1' %2 %3--compressed" ).arg( + mUrl.url(), + curlHeaders, + curlData ); + QApplication::clipboard()->setText( curlCmd ); + } ); + res << copyAsCurlAction; + + QAction *copyJsonAction = new QAction( QObject::tr( "Copy as JSON" ), parent ); + QObject::connect( copyJsonAction, &QAction::triggered, openUrlAction, [ = ] + { + const QVariant value = toVariant(); + const QString json = QString::fromStdString( QgsJsonUtils::jsonFromVariant( value ).dump( 2 ) ); + QApplication::clipboard()->setText( json ); + + } ); + res << copyJsonAction; + + return res; +} + +QVariant QgsNetworkLoggerRequestGroup::toVariant() const +{ + QVariantMap res; + res.insert( QStringLiteral( "URL" ), mUrl.url() ); + res.insert( QStringLiteral( "Total time (ms)" ), mTotalTime ); + res.insert( QStringLiteral( "Bytes Received" ), mBytesReceived ); + res.insert( QStringLiteral( "Bytes Total" ), mBytesTotal ); + res.insert( QStringLiteral( "Replies" ), mReplies ); + if ( mDetailsGroup ) + { + const QVariantMap detailsMap = mDetailsGroup->toVariant().toMap(); + for ( auto it = detailsMap.constBegin(); it != detailsMap.constEnd(); ++it ) + res.insert( it.key(), it.value() ); + } + if ( mReplyGroup ) + { + res.insert( QObject::tr( "Reply" ), mReplyGroup->toVariant() ); + } + if ( mSslErrorsGroup ) + { + res.insert( QObject::tr( "SSL Errors" ), mSslErrorsGroup->toVariant() ); + } + return res; +} + +void QgsNetworkLoggerRequestGroup::setReply( const QgsNetworkReplyContent &reply ) +{ + switch ( reply.error() ) + { + case QNetworkReply::OperationCanceledError: + mStatus = Status::Canceled; + break; + + case QNetworkReply::NoError: + mStatus = Status::Complete; + break; + + default: + mStatus = Status::Error; + break; + } + + mTotalTime = mTimer.elapsed(); + mHttpStatus = reply.attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + mContentType = reply.rawHeader( "Content - Type" ); + + std::unique_ptr< QgsNetworkLoggerReplyGroup > replyGroup = qgis::make_unique< QgsNetworkLoggerReplyGroup >( reply ) ; + mReplyGroup = replyGroup.get(); + addChild( std::move( replyGroup ) ); +} + +void QgsNetworkLoggerRequestGroup::setTimedOut() +{ + mStatus = Status::TimeOut; +} + +void QgsNetworkLoggerRequestGroup::setProgress( qint64 bytesReceived, qint64 bytesTotal ) +{ + mReplies++; + mBytesReceived = bytesReceived; + mBytesTotal = bytesTotal; +} + +void QgsNetworkLoggerRequestGroup::setSslErrors( const QList &errors ) +{ + mHasSslErrors = !errors.empty(); + if ( mHasSslErrors ) + { + std::unique_ptr< QgsNetworkLoggerSslErrorGroup > errorGroup = qgis::make_unique< QgsNetworkLoggerSslErrorGroup >( errors ); + mSslErrorsGroup = errorGroup.get(); + addChild( std::move( errorGroup ) ); + } +} + +QString QgsNetworkLoggerRequestGroup::operationToString( QNetworkAccessManager::Operation operation ) +{ + switch ( operation ) + { + case QNetworkAccessManager::HeadOperation: + return QStringLiteral( "HEAD" ); + case QNetworkAccessManager::GetOperation: + return QStringLiteral( "GET" ); + case QNetworkAccessManager::PutOperation: + return QStringLiteral( "PUT" ); + case QNetworkAccessManager::PostOperation: + return QStringLiteral( "POST" ); + case QNetworkAccessManager::DeleteOperation: + return QStringLiteral( "DELETE" ); + case QNetworkAccessManager::UnknownOperation: + return QStringLiteral( "UNKNOWN" ); + case QNetworkAccessManager::CustomOperation: + return QStringLiteral( "CUSTOM" ); + } + return QString(); +} + +QString QgsNetworkLoggerRequestGroup::statusToString( QgsNetworkLoggerRequestGroup::Status status ) +{ + switch ( status ) + { + case QgsNetworkLoggerRequestGroup::Status::Pending: + return QObject::tr( "Pending" ); + case QgsNetworkLoggerRequestGroup::Status::Complete: + return QObject::tr( "Complete" ); + case QgsNetworkLoggerRequestGroup::Status::Error: + return QObject::tr( "Error" ); + case QgsNetworkLoggerRequestGroup::Status::TimeOut: + return QObject::tr( "Timeout" ); + case QgsNetworkLoggerRequestGroup::Status::Canceled: + return QObject::tr( "Canceled" ); + } + return QString(); +} + +QString QgsNetworkLoggerRequestGroup::cacheControlToString( QNetworkRequest::CacheLoadControl control ) +{ + switch ( control ) + { + case QNetworkRequest::AlwaysNetwork: + return QObject::tr( "Always load from network, do not check cache" ); + case QNetworkRequest::PreferNetwork: + return QObject::tr( "Load from the network if the cached entry is older than the network entry" ); + case QNetworkRequest::PreferCache: + return QObject::tr( "Load from cache if available, otherwise load from network" ); + case QNetworkRequest::AlwaysCache: + return QObject::tr( "Only load from cache, error if no cached entry available" ); + } + return QString(); +} + + +// +// QgsNetworkLoggerRequestDetailsGroup +// + +QgsNetworkLoggerRequestDetailsGroup::QgsNetworkLoggerRequestDetailsGroup( const QgsNetworkRequestParameters &request ) + : QgsNetworkLoggerGroup( QObject::tr( "Request" ) ) +{ + addKeyValueNode( QObject::tr( "Operation" ), QgsNetworkLoggerRequestGroup::operationToString( request.operation() ) ); + addKeyValueNode( QObject::tr( "Thread" ), request.originatingThreadId() ); + addKeyValueNode( QObject::tr( "Initiator" ), request.initiatorClassName().isEmpty() ? QObject::tr( "unknown" ) : request.initiatorClassName() ); + if ( request.initiatorRequestId().isValid() ) + addKeyValueNode( QObject::tr( "ID" ), request.initiatorRequestId().toString() ); + addKeyValueNode( QObject::tr( "Cache (control)" ), QgsNetworkLoggerRequestGroup::cacheControlToString( static_cast< QNetworkRequest::CacheLoadControl >( request.request().attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt() ) ) ); + addKeyValueNode( QObject::tr( "Cache (save)" ), request.request().attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool() ? QObject::tr( "Can store result in cache" ) : QObject::tr( "Result cannot be stored in cache" ) ); + + if ( !QUrlQuery( request.request().url() ).queryItems().isEmpty() ) + { + std::unique_ptr< QgsNetworkLoggerRequestQueryGroup > queryGroup = qgis::make_unique< QgsNetworkLoggerRequestQueryGroup >( request.request().url() ); + mQueryGroup = queryGroup.get(); + addChild( std::move( queryGroup ) ); + } + + std::unique_ptr< QgsNetworkLoggerRequestHeadersGroup > requestHeadersGroup = qgis::make_unique< QgsNetworkLoggerRequestHeadersGroup >( request ); + mRequestHeaders = requestHeadersGroup.get(); + addChild( std::move( requestHeadersGroup ) ); + + switch ( request.operation() ) + { + case QNetworkAccessManager::PostOperation: + case QNetworkAccessManager::PutOperation: + { + std::unique_ptr< QgsNetworkLoggerPostContentGroup > postContentGroup = qgis::make_unique< QgsNetworkLoggerPostContentGroup >( request ); + mPostContent = postContentGroup.get(); + addChild( std::move( postContentGroup ) ); + break; + } + + case QNetworkAccessManager::GetOperation: + case QNetworkAccessManager::HeadOperation: + case QNetworkAccessManager::DeleteOperation: + case QNetworkAccessManager::UnknownOperation: + case QNetworkAccessManager::CustomOperation: + break; + } +} + +QVariant QgsNetworkLoggerRequestDetailsGroup::toVariant() const +{ + QVariantMap res = QgsNetworkLoggerGroup::toVariant().toMap(); + if ( mQueryGroup ) + res.insert( QObject::tr( "Query" ), mQueryGroup->toVariant() ); + if ( mRequestHeaders ) + res.insert( QObject::tr( "Headers" ), mRequestHeaders->toVariant() ); + if ( mPostContent ) + res.insert( QObject::tr( "Content" ), mPostContent->toVariant() ); + return res; +} + + +// +// QgsNetworkLoggerRequestQueryGroup +// + +QgsNetworkLoggerRequestQueryGroup::QgsNetworkLoggerRequestQueryGroup( const QUrl &url ) + : QgsNetworkLoggerGroup( QObject::tr( "Query" ) ) +{ + QUrlQuery query( url ); + const QList > queryItems = query.queryItems(); + + for ( const QPair< QString, QString > &query : queryItems ) + { + addKeyValueNode( query.first, query.second ); + } +} + + +// +// QgsNetworkLoggerRequestHeadersGroup +// +QgsNetworkLoggerRequestHeadersGroup::QgsNetworkLoggerRequestHeadersGroup( const QgsNetworkRequestParameters &request ) + : QgsNetworkLoggerGroup( QObject::tr( "Headers" ) ) +{ + const QList headers = request.request().rawHeaderList(); + for ( const QByteArray &header : headers ) + { + addKeyValueNode( header, request.request().rawHeader( header ) ); + } +} + +// +// QgsNetworkLoggerPostContentGroup +// + +QgsNetworkLoggerPostContentGroup::QgsNetworkLoggerPostContentGroup( const QgsNetworkRequestParameters ¶meters ) + : QgsNetworkLoggerGroup( QObject::tr( "Content" ) ) +{ + addKeyValueNode( QObject::tr( "Data" ), parameters.content() ); +} + + +// +// QgsNetworkLoggerReplyGroup +// + +QgsNetworkLoggerReplyGroup::QgsNetworkLoggerReplyGroup( const QgsNetworkReplyContent &reply ) + : QgsNetworkLoggerGroup( QObject::tr( "Reply" ) ) +{ + addKeyValueNode( QObject::tr( "Status" ), reply.attribute( QNetworkRequest::HttpStatusCodeAttribute ).toString() ); + if ( reply.error() != QNetworkReply::NoError ) + { + addKeyValueNode( QObject::tr( "Error Code" ), QString::number( static_cast< int >( reply.error() ) ) ); + addKeyValueNode( QObject::tr( "Error" ), reply.errorString() ); + } + addKeyValueNode( QObject::tr( "Cache (result)" ), reply.attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool() ? QObject::tr( "Used entry from cache" ) : QObject::tr( "Read from network" ) ); + + std::unique_ptr< QgsNetworkLoggerReplyHeadersGroup > headersGroup = qgis::make_unique< QgsNetworkLoggerReplyHeadersGroup >( reply ); + mReplyHeaders = headersGroup.get(); + addChild( std::move( headersGroup ) ); +} + +QVariant QgsNetworkLoggerReplyGroup::toVariant() const +{ + QVariantMap res = QgsNetworkLoggerGroup::toVariant().toMap(); + if ( mReplyHeaders ) + { + res.insert( QObject::tr( "Headers" ), mReplyHeaders->toVariant() ); + } + return res; +} + + +// +// QgsNetworkLoggerReplyHeadersGroup +// +QgsNetworkLoggerReplyHeadersGroup::QgsNetworkLoggerReplyHeadersGroup( const QgsNetworkReplyContent &reply ) + : QgsNetworkLoggerGroup( QObject::tr( "Headers" ) ) +{ + const QList headers = reply.rawHeaderList(); + for ( const QByteArray &header : headers ) + { + addKeyValueNode( header, reply.rawHeader( header ) ); + } +} + +// +// QgsNetworkLoggerSslErrorGroup +// +QgsNetworkLoggerSslErrorGroup::QgsNetworkLoggerSslErrorGroup( const QList &errors ) + : QgsNetworkLoggerGroup( QObject::tr( "SSL errors" ) ) +{ + for ( const QSslError &error : errors ) + { + addKeyValueNode( QObject::tr( "Error" ), error.errorString(), QColor( 180, 65, 210 ) ); + } +} + +QVariant QgsNetworkLoggerSslErrorGroup::data( int role ) const +{ + if ( role == Qt::ForegroundRole ) + return QBrush( QColor( 180, 65, 210 ) ); + + return QgsNetworkLoggerGroup::data( role ); +} + +QList QgsNetworkLoggerNode::actions( QObject * ) +{ + return QList< QAction * >(); +} + +QVariant QgsNetworkLoggerNode::toVariant() const +{ + return QVariant(); +} diff --git a/src/app/devtools/networklogger/qgsnetworkloggernode.h b/src/app/devtools/networklogger/qgsnetworkloggernode.h new file mode 100644 index 000000000000..d24035be5977 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggernode.h @@ -0,0 +1,534 @@ +/*************************************************************************** + qgsnetworkloggernode.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSNETWORKLOGGERNODE_H +#define QGSNETWORKLOGGERNODE_H + +#include "qgsnetworkaccessmanager.h" +#include +#include +#include +#include +#include + +class QAction; +class QgsNetworkLoggerGroup; + +/** + * \ingroup app + * \class QgsNetworkLoggerNode + * \brief Base class for nodes in the network logger model. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerNode +{ + public: + + //! Custom node data roles + enum Roles + { + RoleStatus = Qt::UserRole + 1, //!< Request status role + RoleId, //!< Request ID role + }; + + virtual ~QgsNetworkLoggerNode(); + + /** + * Returns the node's parent node. + * + * If parent is NULLPTR, the node is a root node + */ + QgsNetworkLoggerGroup *parent() { return mParent; } + + /** + * Returns the node's data for the specified model \a role. + */ + virtual QVariant data( int role = Qt::DisplayRole ) const = 0; + + /** + * Returns the number of child nodes owned by this node. + */ + virtual int childCount() const = 0; + + /** + * Returns a list of actions relating to the node. + * + * The actions should be parented to \a parent. + */ + virtual QList< QAction * > actions( QObject *parent ); + + /** + * Converts the node's contents to a variant. + */ + virtual QVariant toVariant() const; + + protected: + + QgsNetworkLoggerNode(); + + private: + + QgsNetworkLoggerGroup *mParent = nullptr; + friend class QgsNetworkLoggerGroup; +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerGroup + * \brief Base class for network logger model "group" nodes, which contain children of their own. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerGroup : public QgsNetworkLoggerNode +{ + public: + + /** + * Adds a \a child node to this node. + */ + void addChild( std::unique_ptr< QgsNetworkLoggerNode > child ); + + /** + * Returns the index of the specified \a child node. + * + * \warning \a child must be a valid child of this node. + */ + int indexOf( QgsNetworkLoggerNode *child ) const; + + /** + * Returns the child at the specified \a index. + */ + QgsNetworkLoggerNode *childAt( int index ); + + /** + * Clears the group, removing all its children. + */ + void clear(); + + int childCount() const override final { return mChildren.size(); } + QVariant data( int role = Qt::DisplayRole ) const override; + QVariant toVariant() const override; + + protected: + + /** + * Constructor for a QgsNetworkLoggerGroup, with the specified \a title. + */ + QgsNetworkLoggerGroup( const QString &title ); + + /** + * Adds a simple \a key: \a value node to the group. + */ + void addKeyValueNode( const QString &key, const QString &value, const QColor &color = QColor() ); + + private: + + std::deque< std::unique_ptr< QgsNetworkLoggerNode > > mChildren; + QString mGroupTitle; + friend class QgsNetworkLoggerRootNode; + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerValueNode + * \brief A "key: value" style node for the network logger model. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerValueNode : public QgsNetworkLoggerNode +{ + public: + + /** + * Constructor for QgsNetworkLoggerValueNode, with the specified \a key (usually translated) and \a value. + */ + QgsNetworkLoggerValueNode( const QString &key, const QString &value, const QColor &color = QColor() ); + + /** + * Returns the node's key. + */ + QString key() const { return mKey; } + + /** + * Returns the node's value. + */ + QString value() const { return mValue; } + + QVariant data( int role = Qt::DisplayRole ) const override final; + int childCount() const override final { return 0; } + + private: + + QString mKey; + QString mValue; + QColor mColor; +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerRootNode + * \brief Root node for the network logger model. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRootNode final : public QgsNetworkLoggerGroup +{ + public: + + QgsNetworkLoggerRootNode(); + QVariant data( int role = Qt::DisplayRole ) const override final; + + /** + * Removes a \a row from the root group. + */ + void removeRow( int row ); + + QVariant toVariant() const override; +}; + +class QgsNetworkLoggerRequestDetailsGroup; +class QgsNetworkLoggerReplyGroup; +class QgsNetworkLoggerSslErrorGroup; + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestGroup + * \brief Parent group for all network requests, showing the request id, type, url, + * and containing child groups with detailed request and response information. + * + * Visually, a QgsNetworkLoggerRequestGroup is structured by: + * + * |__ QgsNetworkLoggerRequestGroup (showing id, type (GET etc) url) + * |__ QgsNetworkLoggerRequestDetailsGroup (holding Request details) + * |__ QgsNetworkLoggerValueNode (key-value pairs with info) + * ... + * |__ QgsNetworkLoggerRequestQueryGroup (holding query info) + * |__ ... + * |__ QgsNetworkLoggerRequestHeadersGroup ('Headers') + * |__ ... + * |__ QgsNetworkLoggerPostContentGroup (showing Data in case of POST) + * |__ ... + * |__ QgsNetworkLoggerReplyGroup (holding Reply details) + * |__ QgsNetworkLoggerReplyHeadersGroup (Reply 'Headers') + * |__ ... + * |__ QgsNetworkLoggerSslErrorGroup (holding SSL error details, if encountered) + * |__ ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestGroup final : public QgsNetworkLoggerGroup +{ + public: + + //! Request statu + enum class Status + { + Pending, //!< Request underway + Complete, //!< Request was successfully completed + Error, //!< Request encountered an error + TimeOut, //!< Request timed out + Canceled, //!< Request was manually canceled + }; + + /** + * Constructor for QgsNetworkLoggerRequestGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerRequestGroup( const QgsNetworkRequestParameters &request ); + QVariant data( int role = Qt::DisplayRole ) const override; + QList< QAction * > actions( QObject *parent ) override final; + QVariant toVariant() const override; + + /** + * Returns the request's status. + */ + Status status() const { return mStatus; } + + /** + * Returns the request's URL. + */ + QUrl url() const { return mUrl; } + + /** + * Called to set the \a reply associated with the request. + * + * Will automatically create children encapsulating the reply details. + */ + void setReply( const QgsNetworkReplyContent &reply ); + + /** + * Flags the reply as having timed out. + */ + void setTimedOut(); + + /** + * Sets the requests download progress. + */ + void setProgress( qint64 bytesReceived, qint64 bytesTotal ); + + /** + * Reports any SSL errors encountered while processing the request. + */ + void setSslErrors( const QList &errors ); + + /** + * Converts a network \a operation to a string value. + */ + static QString operationToString( QNetworkAccessManager::Operation operation ); + + /** + * Converts a request \a status to a translated string value. + */ + static QString statusToString( Status status ); + + /** + * Converts a cache load \a control value to a translated string. + */ + static QString cacheControlToString( QNetworkRequest::CacheLoadControl control ); + + private: + + QUrl mUrl; + int mRequestId = 0; + QNetworkAccessManager::Operation mOperation; + QElapsedTimer mTimer; + qint64 mTotalTime = 0; + int mHttpStatus = -1; + QString mContentType; + qint64 mBytesReceived = 0; + qint64 mBytesTotal = 0; + int mReplies = 0; + QByteArray mData; + Status mStatus = Status::Pending; + bool mHasSslErrors = false; + QList< QPair< QString, QString > > mHeaders; + QgsNetworkLoggerRequestDetailsGroup *mDetailsGroup = nullptr; + QgsNetworkLoggerReplyGroup *mReplyGroup = nullptr; + QgsNetworkLoggerSslErrorGroup *mSslErrorsGroup = nullptr; +}; + +class QgsNetworkLoggerRequestQueryGroup; +class QgsNetworkLoggerRequestHeadersGroup; +class QgsNetworkLoggerPostContentGroup; + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestGroup + * \brief Parent group for all network request details, showing the request parameters + * and header information. + * + * Visually, a QgsNetworkLoggerRequestDetailsGroup is structured by: + * + * |__ QgsNetworkLoggerRequestDetailsGroup (holding Request details) + * |__ QgsNetworkLoggerValueNode (key-value pairs with info) + * Operation: ... + * Thread: ... + * Initiator: ... + * ID: ... + * Cache (control): ... + * Cache (save): ... + * |__ QgsNetworkLoggerRequestQueryGroup (holding query info) + * |__ ... + * |__ QgsNetworkLoggerRequestHeadersGroup ('Headers') + * |__ ... + * |__ QgsNetworkLoggerPostContentGroup (showing Data in case of POST) + * |__ ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestDetailsGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerRequestDetailsGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerRequestDetailsGroup( const QgsNetworkRequestParameters &request ); + QVariant toVariant() const override; + + private: + + QgsNetworkLoggerRequestQueryGroup *mQueryGroup = nullptr; + QgsNetworkLoggerRequestHeadersGroup *mRequestHeaders = nullptr; + QgsNetworkLoggerPostContentGroup *mPostContent = nullptr; +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestHeadersGroup + * \brief Parent group for all network request header information. + * + * Visually, a QgsNetworkLoggerRequestHeadersGroup is structured by: + * + * |__ QgsNetworkLoggerRequestHeadersGroup (holding Request details) + * User-Agent: ... + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestHeadersGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerRequestHeadersGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerRequestHeadersGroup( const QgsNetworkRequestParameters &request ); + +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestQueryGroup + * \brief Parent group for all network request query information. + * + * Visually, a QgsNetworkLoggerRequestQueryGroup is structured by: + * + * |__ QgsNetworkLoggerRequestQueryGroup (holding query info) + * query param: value + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestQueryGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerRequestQueryGroup, populated from the + * specified \a url. + */ + QgsNetworkLoggerRequestQueryGroup( const QUrl &url ); + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerPostContentGroup + * \brief Parent group for all request post data, showing POST data. + * + * Visually, a QgsNetworkLoggerPostContentGroup is structured by: + * + * |__ QgsNetworkLoggerPostContentGroup (holding POST data) + * |__ Data: POST data + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerPostContentGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerPostContentGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerPostContentGroup( const QgsNetworkRequestParameters ¶meters ); +}; + +class QgsNetworkLoggerReplyHeadersGroup; + +/** + * \ingroup app + * \class QgsNetworkLoggerReplyGroup + * \brief Parent group for all network replies, showing the reply details. + * + * Visually, a QgsNetworkLoggerReplyGroup is structured by: + * + * |__ QgsNetworkLoggerReplyGroup (holding Reply details) + * |__ Status: reply status (e.g. 'Canceled') + * |__ Error code: code (if applicable) + * |__ Cache (result): whether reply was taken from cache or network + * |__ QgsNetworkLoggerReplyHeadersGroup (Reply 'Headers') + * |__ ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerReplyGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerReplyGroup, populated from the + * specified \a reply details. + */ + QgsNetworkLoggerReplyGroup( const QgsNetworkReplyContent &reply ); + QVariant toVariant() const override; + + private: + + QgsNetworkLoggerReplyHeadersGroup *mReplyHeaders = nullptr; + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerReplyHeadersGroup + * \brief Parent group for network reply headers, showing the reply header details. + * + * Visually, a QgsNetworkLoggerReplyHeadersGroup is structured by: + * + * |__ QgsNetworkLoggerReplyHeadersGroup (holding reply Header details) + * Content-Type: ... + * Content-Length: ... + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerReplyHeadersGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerReplyHeadersGroup, populated from the + * specified \a reply details. + */ + QgsNetworkLoggerReplyHeadersGroup( const QgsNetworkReplyContent &reply ); + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerSslErrorNode + * \brief Parent group for SSQL errors, showing the error details. + * + * Visually, a QgsNetworkLoggerSslErrorGroup is structured by: + * + * |__ QgsNetworkLoggerSslErrorGroup (holding error details) + * Error: ... + * Error: ... + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerSslErrorGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerSslErrorGroup, populated from the + * specified \a errors. + */ + QgsNetworkLoggerSslErrorGroup( const QList &errors ); + QVariant data( int role = Qt::DisplayRole ) const override; +}; + + + +#endif // QGSNETWORKLOGGERNODE_H diff --git a/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp new file mode 100644 index 000000000000..ae89bff79c63 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp @@ -0,0 +1,203 @@ +/*************************************************************************** + qgsnetworkloggerpanelwidget.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsnetworkloggerpanelwidget.h" +#include "qgsguiutils.h" +#include "qgsnetworklogger.h" +#include "qgssettings.h" +#include "qgsnetworkloggernode.h" +#include "qgsjsonutils.h" +#include +#include +#include +#include +#include +#include + +// +// QgsNetworkLoggerTreeView +// + +QgsNetworkLoggerTreeView::QgsNetworkLoggerTreeView( QgsNetworkLogger *logger, QWidget *parent ) + : QTreeView( parent ) + , mLogger( logger ) +{ + connect( this, &QTreeView::expanded, this, &QgsNetworkLoggerTreeView::itemExpanded ); + + setFont( QFontDatabase::systemFont( QFontDatabase::FixedFont ) ); + + mProxyModel = new QgsNetworkLoggerProxyModel( mLogger, this ); + setModel( mProxyModel ); + + setContextMenuPolicy( Qt::CustomContextMenu ); + connect( this, &QgsNetworkLoggerTreeView::customContextMenuRequested, this, &QgsNetworkLoggerTreeView::contextMenu ); + + connect( verticalScrollBar(), &QAbstractSlider::sliderMoved, this, [this]( int value ) + { + if ( value == verticalScrollBar()->maximum() ) + mAutoScroll = true; + else + mAutoScroll = false; + } ); + + connect( mLogger, &QAbstractItemModel::rowsInserted, this, [ = ] + { + if ( mLogger->rowCount() > ( QgsNetworkLogger::MAX_LOGGED_REQUESTS * 1.2 ) ) // 20 % more as buffer + { + // never trim expanded nodes + const int toTrim = mLogger->rowCount() - QgsNetworkLogger::MAX_LOGGED_REQUESTS; + int trimmed = 0; + QList< int > rowsToTrim; + rowsToTrim.reserve( toTrim ); + for ( int i = 0; i < mLogger->rowCount(); ++i ) + { + const QModelIndex proxyIndex = mProxyModel->mapFromSource( mLogger->index( i, 0 ) ); + if ( !proxyIndex.isValid() || !isExpanded( proxyIndex ) ) + { + rowsToTrim << i; + trimmed++; + } + if ( trimmed == toTrim ) + break; + } + + mLogger->removeRows( rowsToTrim ); + } + + if ( mAutoScroll ) + scrollToBottom(); + } ); + + mMenu = new QMenu( this ); +} + +void QgsNetworkLoggerTreeView::setFilterString( const QString &string ) +{ + mProxyModel->setFilterString( string ); +} + +void QgsNetworkLoggerTreeView::setShowSuccessful( bool show ) +{ + mProxyModel->setShowSuccessful( show ); +} + +void QgsNetworkLoggerTreeView::setShowTimeouts( bool show ) +{ + mProxyModel->setShowTimeouts( show ); +} + +void QgsNetworkLoggerTreeView::itemExpanded( const QModelIndex &index ) +{ + // if the item is a QgsNetworkLoggerRequestGroup item, open all children (show ALL info of it) + // we want to scroll to last request + + // only expand all children on QgsNetworkLoggerRequestGroup nodes (which don't have a valid parent!) + if ( !index.parent().isValid() ) + expandChildren( index ); + + // make ALL request information visible by scrolling view to it + scrollTo( index ); +} + +void QgsNetworkLoggerTreeView::contextMenu( QPoint point ) +{ + const QModelIndex viewModelIndex = indexAt( point ); + const QModelIndex modelIndex = mProxyModel->mapToSource( viewModelIndex ); + + if ( modelIndex.isValid() ) + { + mMenu->clear(); + + const QList< QAction * > actions = mLogger->actions( modelIndex, mMenu ); + mMenu->addActions( actions ); + if ( !mMenu->actions().empty() ) + { + mMenu->exec( viewport()->mapToGlobal( point ) ); + } + } +} + +void QgsNetworkLoggerTreeView::expandChildren( const QModelIndex &index ) +{ + if ( !index.isValid() ) + return; + + const int count = model()->rowCount( index ); + for ( int i = 0; i < count; ++i ) + { + const QModelIndex childIndex = model()->index( i, 0, index ); + expandChildren( childIndex ); + } + if ( !isExpanded( index ) ) + expand( index ); +} + + +// +// QgsNetworkLoggerPanelWidget +// + +QgsNetworkLoggerPanelWidget::QgsNetworkLoggerPanelWidget( QgsNetworkLogger *logger, QWidget *parent ) + : QgsDevToolWidget( parent ) + , mLogger( logger ) +{ + setupUi( this ); + + mTreeView = new QgsNetworkLoggerTreeView( mLogger ); + verticalLayout->addWidget( mTreeView ); + mToolbar->setIconSize( QgsGuiUtils::iconSize( true ) ); + + mFilterLineEdit->setShowClearButton( true ); + mFilterLineEdit->setShowSearchIcon( true ); + mFilterLineEdit->setPlaceholderText( tr( "Filter requests" ) ); + + mActionShowTimeouts->setChecked( true ); + mActionShowSuccessful->setChecked( true ); + mActionRecord->setChecked( mLogger->isLogging() ); + + connect( mFilterLineEdit, &QgsFilterLineEdit::textChanged, mTreeView, &QgsNetworkLoggerTreeView::setFilterString ); + connect( mActionShowTimeouts, &QAction::toggled, mTreeView, &QgsNetworkLoggerTreeView::setShowTimeouts ); + connect( mActionShowSuccessful, &QAction::toggled, mTreeView, &QgsNetworkLoggerTreeView::setShowSuccessful ); + connect( mActionClear, &QAction::triggered, mLogger, &QgsNetworkLogger::clear ); + connect( mActionRecord, &QAction::toggled, this, [ = ]( bool enabled ) + { + QgsSettings().setValue( QStringLiteral( "logNetworkRequests" ), enabled, QgsSettings::App ); + mLogger->enableLogging( enabled ); + } ); + connect( mActionSaveLog, &QAction::triggered, this, [ = ]() + { + if ( QMessageBox::warning( this, tr( "Save Network Log" ), + tr( "Security warning: network logs may contain sensitive data including usernames or passwords. Treat this log as confidential and be careful who you share it with. Continue?" ), QMessageBox::Yes | QMessageBox::No ) == QMessageBox::No ) + return; + + QString saveFilePath = QFileDialog::getSaveFileName( this, tr( "Save Network Log" ), QDir::homePath(), tr( "Log files" ) + " (*.json)" ); + if ( saveFilePath.isEmpty() ) + { + return; + } + + QFile exportFile( saveFilePath ); + if ( !exportFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) ) + { + return; + } + QTextStream fout( &exportFile ); + + const QVariant value = mLogger->rootGroup()->toVariant(); + const QString json = QString::fromStdString( QgsJsonUtils::jsonFromVariant( value ).dump( 2 ) ); + + fout << json; + } ); +} diff --git a/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h new file mode 100644 index 000000000000..2c8bc280b206 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h @@ -0,0 +1,98 @@ +/*************************************************************************** + qgsnetworkloggerpanelwidget.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSNETWORKLOGGERPANELWIDGET_H +#define QGSNETWORKLOGGERPANELWIDGET_H + +#include "qgsdevtoolwidget.h" +#include "ui_qgsnetworkloggerpanelbase.h" +#include + +class QgsNetworkLogger; +class QgsNetworkLoggerProxyModel; + +/** + * \ingroup app + * \class QgsNetworkLoggerTreeView + * \brief A custom QTreeView subclass for showing logged network requests. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerTreeView: public QTreeView +{ + Q_OBJECT + public: + + /** + * Constructor for QgsNetworkLoggerTreeView, attached to the specified \a logger. + */ + QgsNetworkLoggerTreeView( QgsNetworkLogger *logger, QWidget *parent = nullptr ); + + public slots: + + /** + * Sets a filter \a string to apply to request URLs. + */ + void setFilterString( const QString &string ); + + /** + * Sets whether successful requests should be shown. + */ + void setShowSuccessful( bool show ); + + /** + * Sets whether timed out requests should be shown. + */ + void setShowTimeouts( bool show ); + + private slots: + void itemExpanded( const QModelIndex &index ); + void contextMenu( QPoint point ); + + private: + + void expandChildren( const QModelIndex &index ); + QMenu *mMenu = nullptr; + QgsNetworkLogger *mLogger = nullptr; + QgsNetworkLoggerProxyModel *mProxyModel = nullptr; + bool mAutoScroll = true; +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerPanelWidget + * \brief A panel widget showing logged network requests. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerPanelWidget : public QgsDevToolWidget, private Ui::QgsNetworkLoggerPanelBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsNetworkLoggerPanelWidget, linked with the specified \a logger. + */ + QgsNetworkLoggerPanelWidget( QgsNetworkLogger *logger, QWidget *parent ); + + private: + + QgsNetworkLoggerTreeView *mTreeView = nullptr; + QgsNetworkLogger *mLogger = nullptr; +}; + + +#endif // QGSNETWORKLOGGERPANELWIDGET_H diff --git a/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp new file mode 100644 index 000000000000..acddd2819990 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp @@ -0,0 +1,29 @@ +/*************************************************************************** + qgsnetworkloggerwidgetfactory.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsnetworkloggerwidgetfactory.h" +#include "qgsnetworkloggerpanelwidget.h" +#include "qgsapplication.h" + +QgsNetworkLoggerWidgetFactory::QgsNetworkLoggerWidgetFactory( QgsNetworkLogger *logger ) + : QgsDevToolWidgetFactory( QObject::tr( "Network Logger" ), QgsApplication::getThemeIcon( QStringLiteral( "mIconNetworkLogger.svg" ) ) ) + , mLogger( logger ) +{ +} + +QgsDevToolWidget *QgsNetworkLoggerWidgetFactory::createWidget( QWidget *parent ) const +{ + return new QgsNetworkLoggerPanelWidget( mLogger, parent ); +} diff --git a/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h new file mode 100644 index 000000000000..ebb916c1fbc1 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h @@ -0,0 +1,35 @@ +/*************************************************************************** + qgsnetworkloggerwidgetfactory.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSNETWORKLOGGERWIDGETFACTORY_H +#define QGSNETWORKLOGGERWIDGETFACTORY_H + +#include "qgsdevtoolwidgetfactory.h" + +class QgsNetworkLogger; + +class QgsNetworkLoggerWidgetFactory: public QgsDevToolWidgetFactory +{ + public: + + QgsNetworkLoggerWidgetFactory( QgsNetworkLogger *logger ); + QgsDevToolWidget *createWidget( QWidget *parent = nullptr ) const override; + + private: + + QgsNetworkLogger *mLogger = nullptr; +}; + + +#endif // QGSNETWORKLOGGERWIDGETFACTORY_H diff --git a/src/app/devtools/qgsappdevtoolutils.cpp b/src/app/devtools/qgsappdevtoolutils.cpp new file mode 100644 index 000000000000..b61744d6230d --- /dev/null +++ b/src/app/devtools/qgsappdevtoolutils.cpp @@ -0,0 +1,36 @@ +/*************************************************************************** + qgsappdevtoolutils.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsappdevtoolutils.h" + +#include "qgisapp.h" +#include "qgis.h" +#include "qgsdevtoolwidgetfactory.h" + +QgsScopedDevToolWidgetFactory::QgsScopedDevToolWidgetFactory() = default; + +QgsScopedDevToolWidgetFactory::~QgsScopedDevToolWidgetFactory() +{ + if ( mFactory ) + QgisApp::instance()->unregisterDevToolFactory( mFactory.get() ); +} + +void QgsScopedDevToolWidgetFactory::reset( std::unique_ptr factory ) +{ + if ( mFactory ) + QgisApp::instance()->unregisterDevToolFactory( mFactory.get() ); + mFactory = std::move( factory ); + if ( mFactory ) + QgisApp::instance()->registerDevToolFactory( mFactory.get() ); +} diff --git a/src/app/devtools/qgsappdevtoolutils.h b/src/app/devtools/qgsappdevtoolutils.h new file mode 100644 index 000000000000..cc66b07f38c1 --- /dev/null +++ b/src/app/devtools/qgsappdevtoolutils.h @@ -0,0 +1,43 @@ + +/*************************************************************************** + qgsappdevtoolutils.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSAPPDEVTOOLUTILS_H +#define QGSAPPDEVTOOLUTILS_H + +class QgsDevToolWidgetFactory; +#include + +/** + * \ingroup core + * + * Manages lifetime of a QgsScopedDevToolWidgetFactory, automatically + * registering and unregistering it as required. + * + * \since QGIS 3.14 + */ +class QgsScopedDevToolWidgetFactory +{ + public: + QgsScopedDevToolWidgetFactory(); + ~QgsScopedDevToolWidgetFactory(); + + void reset( std::unique_ptr< QgsDevToolWidgetFactory > factory = nullptr ); + + private: + std::unique_ptr< QgsDevToolWidgetFactory > mFactory; +}; + + +#endif // QGSAPPDEVTOOLUTILS_H diff --git a/src/app/gps/qgsgpsinformationwidget.cpp b/src/app/gps/qgsgpsinformationwidget.cpp index fd2fc87d3d90..ddb9f84e3896 100644 --- a/src/app/gps/qgsgpsinformationwidget.cpp +++ b/src/app/gps/qgsgpsinformationwidget.cpp @@ -943,7 +943,7 @@ void QgsGpsInformationWidget::displayGPSInformation( const QgsGpsInformation &in if ( radRecenterMap->isChecked() || ( radRecenterWhenNeeded->isChecked() && !myExtentLimit.contains( myPoint ) ) ) { - mMapCanvas->setExtent( myRect ); + mMapCanvas->setExtent( myRect, true ); mMapCanvas->refresh(); } } diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index c2f9ed638a56..2addf0b8b173 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -2651,6 +2651,15 @@ void QgsLayoutDesignerDialog::exportAtlasToRaster() if ( !printAtlas || !printAtlas->enabled() ) return; + if ( !printAtlas->coverageLayer() ) + { + QMessageBox::warning( this, tr( "Export Atlas" ), + tr( "Error: No coverage layer is set." ), + QMessageBox::Ok, + QMessageBox::Ok ); + return; + } + // else, it has an atlas to render, so a directory must first be selected if ( printAtlas->filenameExpression().isEmpty() ) { @@ -2823,6 +2832,15 @@ void QgsLayoutDesignerDialog::exportAtlasToSvg() if ( !printAtlas || !printAtlas->enabled() ) return; + if ( !printAtlas->coverageLayer() ) + { + QMessageBox::warning( this, tr( "Export Atlas" ), + tr( "Error: No coverage layer is set." ), + QMessageBox::Ok, + QMessageBox::Ok ); + return; + } + if ( containsWmsLayers() ) { showWmsPrintingWarning(); @@ -2991,6 +3009,15 @@ void QgsLayoutDesignerDialog::exportAtlasToPdf() if ( !printAtlas || !printAtlas->enabled() ) return; + if ( !printAtlas->coverageLayer() ) + { + QMessageBox::warning( this, tr( "Export Atlas" ), + tr( "Error: No coverage layer is set." ), + QMessageBox::Ok, + QMessageBox::Ok ); + return; + } + if ( containsWmsLayers() ) { showWmsPrintingWarning(); diff --git a/src/app/locator/qgsinbuiltlocatorfilters.cpp b/src/app/locator/qgsinbuiltlocatorfilters.cpp index 299544866177..f4de8668ef50 100644 --- a/src/app/locator/qgsinbuiltlocatorfilters.cpp +++ b/src/app/locator/qgsinbuiltlocatorfilters.cpp @@ -24,7 +24,6 @@ #include "qgslayertree.h" #include "qgsfeedback.h" #include "qgisapp.h" -#include "qgsstringutils.h" #include "qgsmaplayermodel.h" #include "qgslayoutmanager.h" #include "qgsmapcanvas.h" @@ -48,15 +47,26 @@ void QgsLayerTreeLocatorFilter::fetchResults( const QString &string, const QgsLo const QList layers = tree->findLayers(); for ( QgsLayerTreeLayer *layer : layers ) { - if ( layer->layer() && ( stringMatches( layer->layer()->name(), string ) || ( context.usingPrefix && string.isEmpty() ) ) ) + // if the layer is broken, don't include it in the results + if ( ! layer->layer() ) + continue; + + QgsLocatorResult result; + result.displayString = layer->layer()->name(); + result.userData = layer->layerId(); + result.icon = QgsMapLayerModel::iconForLayer( layer->layer() ); + + // return all the layers in case the string query is empty using an equal default score + if ( context.usingPrefix && string.isEmpty() ) { - QgsLocatorResult result; - result.displayString = layer->layer()->name(); - result.userData = layer->layerId(); - result.icon = QgsMapLayerModel::iconForLayer( layer->layer() ); - result.score = static_cast< double >( string.length() ) / layer->layer()->name().length(); emit resultFetched( result ); + continue; } + + result.score = fuzzyScore( result.displayString, string ); + + if ( result.score > 0 ) + emit resultFetched( result ); } } @@ -85,15 +95,24 @@ void QgsLayoutLocatorFilter::fetchResults( const QString &string, const QgsLocat const QList< QgsMasterLayoutInterface * > layouts = QgsProject::instance()->layoutManager()->layouts(); for ( QgsMasterLayoutInterface *layout : layouts ) { - if ( layout && ( stringMatches( layout->name(), string ) || ( context.usingPrefix && string.isEmpty() ) ) ) + // if the layout is broken, don't include it in the results + if ( ! layout ) + continue; + + QgsLocatorResult result; + result.displayString = layout->name(); + result.userData = layout->name(); + + if ( context.usingPrefix && string.isEmpty() ) { - QgsLocatorResult result; - result.displayString = layout->name(); - result.userData = layout->name(); - //result.icon = QgsMapLayerModel::iconForLayer( layer->layer() ); - result.score = static_cast< double >( string.length() ) / layout->name().length(); emit resultFetched( result ); + continue; } + + result.score = fuzzyScore( result.displayString, string ); + + if ( result.score > 0 ) + emit resultFetched( result ); } } @@ -107,9 +126,6 @@ void QgsLayoutLocatorFilter::triggerResult( const QgsLocatorResult &result ) QgisApp::instance()->openLayoutDesignerDialog( layout ); } - - - QgsActionLocatorFilter::QgsActionLocatorFilter( const QList &parentObjectsForActions, QObject *parent ) : QgsLocatorFilter( parent ) , mActionParents( parentObjectsForActions ) @@ -131,7 +147,7 @@ void QgsActionLocatorFilter::fetchResults( const QString &string, const QgsLocat for ( QWidget *object : qgis::as_const( mActionParents ) ) { - searchActions( string, object, found ); + searchActions( string, object, found ); } } @@ -191,15 +207,16 @@ void QgsActionLocatorFilter::searchActions( const QString &string, QWidget *pare searchText += QStringLiteral( " (%1)" ).arg( tooltip.trimmed() ); } - if ( stringMatches( searchText, string ) ) + QgsLocatorResult result; + result.displayString = searchText; + result.userData = QVariant::fromValue( action ); + result.icon = action->icon(); + result.score = fuzzyScore( result.displayString, string ); + + if ( result.score > 0 ) { - QgsLocatorResult result; - result.displayString = searchText; - result.userData = QVariant::fromValue( action ); - result.icon = action->icon(); - result.score = static_cast< double >( string.length() ) / searchText.length(); - emit resultFetched( result ); found << action; + emit resultFetched( result ); } } } @@ -369,7 +386,12 @@ void QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const Qg enhancedSearch.replace( ' ', '%' ); req.setFilterExpression( QStringLiteral( "%1 ILIKE '%%2%'" ) .arg( layer->displayExpression(), enhancedSearch ) ); - req.setLimit( 30 ); + req.setLimit( 6 ); + + QgsFeatureRequest exactMatchRequest = req; + exactMatchRequest.setFilterExpression( QStringLiteral( "%1 ILIKE '%2'" ) + .arg( layer->displayExpression(), enhancedSearch ) ); + exactMatchRequest.setLimit( 10 ); std::shared_ptr preparedLayer( new PreparedLayer() ); preparedLayer->expression = expression; @@ -378,6 +400,7 @@ void QgsAllLayersFeaturesLocatorFilter::prepare( const QString &string, const Qg preparedLayer->layerName = layer->name(); preparedLayer->featureSource.reset( new QgsVectorLayerFeatureSource( layer ) ); preparedLayer->request = req; + preparedLayer->exactMatchRequest = exactMatchRequest; preparedLayer->layerIcon = QgsMapLayerModel::iconForLayer( layer ); mPreparedLayers.append( preparedLayer ); @@ -394,12 +417,47 @@ void QgsAllLayersFeaturesLocatorFilter::fetchResults( const QString &string, con for ( auto preparedLayer : qgis::as_const( mPreparedLayers ) ) { foundInCurrentLayer = 0; + + QgsFeatureIds foundFeatureIds; + + QgsFeatureIterator exactMatchIt = preparedLayer->featureSource->getFeatures( preparedLayer->exactMatchRequest ); + while ( exactMatchIt.nextFeature( f ) ) + { + if ( feedback->isCanceled() ) + return; + + QgsLocatorResult result; + result.group = preparedLayer->layerName; + + preparedLayer->context.setFeature( f ); + + result.displayString = preparedLayer->expression.evaluate( &( preparedLayer->context ) ).toString(); + + result.userData = QVariantList() << f.id() << preparedLayer->layerId; + foundFeatureIds << f.id(); + result.icon = preparedLayer->layerIcon; + result.score = static_cast< double >( string.length() ) / result.displayString.size(); + + result.actions << QgsLocatorResult::ResultAction( OpenForm, tr( "Open form…" ) ); + emit resultFetched( result ); + + foundInCurrentLayer++; + foundInTotal++; + if ( foundInCurrentLayer >= mMaxResultsPerLayer ) + break; + } + if ( foundInTotal >= mMaxTotalResults ) + break; + QgsFeatureIterator it = preparedLayer->featureSource->getFeatures( preparedLayer->request ); while ( it.nextFeature( f ) ) { if ( feedback->isCanceled() ) return; + if ( foundFeatureIds.contains( f.id() ) ) + continue; + QgsLocatorResult result; result.group = preparedLayer->layerName; @@ -524,30 +582,21 @@ void QgsSettingsLocatorFilter::fetchResults( const QString &string, const QgsLoc for ( auto optionsPagesIterator = optionsPagesMap.constBegin(); optionsPagesIterator != optionsPagesMap.constEnd(); ++optionsPagesIterator ) { QString title = optionsPagesIterator.key(); - if ( stringMatches( title, string ) || ( context.usingPrefix && string.isEmpty() ) ) - { - matchingSettingsPagesMap.insert( title + " (" + tr( "Options" ) + ")", settingsPage( QStringLiteral( "optionpage" ), QString::number( optionsPagesIterator.value() ) ) ); - } + matchingSettingsPagesMap.insert( title + " (" + tr( "Options" ) + ")", settingsPage( QStringLiteral( "optionpage" ), QString::number( optionsPagesIterator.value() ) ) ); } QMap projectPropertyPagesMap = QgisApp::instance()->projectPropertiesPagesMap(); for ( auto projectPropertyPagesIterator = projectPropertyPagesMap.constBegin(); projectPropertyPagesIterator != projectPropertyPagesMap.constEnd(); ++projectPropertyPagesIterator ) { QString title = projectPropertyPagesIterator.key(); - if ( stringMatches( title, string ) || ( context.usingPrefix && string.isEmpty() ) ) - { - matchingSettingsPagesMap.insert( title + " (" + tr( "Project Properties" ) + ")", settingsPage( QStringLiteral( "projectpropertypage" ), projectPropertyPagesIterator.value() ) ); - } + matchingSettingsPagesMap.insert( title + " (" + tr( "Project Properties" ) + ")", settingsPage( QStringLiteral( "projectpropertypage" ), projectPropertyPagesIterator.value() ) ); } QMap settingPagesMap = QgisApp::instance()->settingPagesMap(); for ( auto settingPagesIterator = settingPagesMap.constBegin(); settingPagesIterator != settingPagesMap.constEnd(); ++settingPagesIterator ) { QString title = settingPagesIterator.key(); - if ( stringMatches( title, string ) || ( context.usingPrefix && string.isEmpty() ) ) - { - matchingSettingsPagesMap.insert( title, settingsPage( QStringLiteral( "settingspage" ), settingPagesIterator.value() ) ); - } + matchingSettingsPagesMap.insert( title, settingsPage( QStringLiteral( "settingspage" ), settingPagesIterator.value() ) ); } for ( auto matchingSettingsPagesIterator = matchingSettingsPagesMap.constBegin(); matchingSettingsPagesIterator != matchingSettingsPagesMap.constEnd(); ++matchingSettingsPagesIterator ) @@ -558,8 +607,17 @@ void QgsSettingsLocatorFilter::fetchResults( const QString &string, const QgsLoc result.filter = this; result.displayString = title; result.userData.setValue( settingsPage ); - result.score = static_cast< double >( string.length() ) / title.length(); - emit resultFetched( result ); + + if ( context.usingPrefix && string.isEmpty() ) + { + emit resultFetched( result ); + continue; + } + + result.score = fuzzyScore( result.displayString, string );; + + if ( result.score > 0 ) + emit resultFetched( result ); } } @@ -614,22 +672,28 @@ void QgsBookmarkLocatorFilter::fetchResults( const QString &string, const QgsLoc while ( i.hasNext() ) { i.next(); + if ( feedback->isCanceled() ) return; QString name = i.key(); + QModelIndex index = i.value(); + QgsLocatorResult result; + result.filter = this; + result.displayString = name; + result.userData = index; + result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/mItemBookmark.svg" ) ); - if ( stringMatches( name, string ) || ( context.usingPrefix && string.isEmpty() ) ) + if ( context.usingPrefix && string.isEmpty() ) { - QModelIndex index = i.value(); - QgsLocatorResult result; - result.filter = this; - result.displayString = name; - result.userData = index; - result.icon = QgsApplication::getThemeIcon( QStringLiteral( "/mItemBookmark.svg" ) ); - result.score = static_cast< double >( string.length() ) / name.length(); emit resultFetched( result ); + continue; } + + result.score = fuzzyScore( result.displayString, string ); + + if ( result.score > 0 ) + emit resultFetched( result ); } } diff --git a/src/app/locator/qgsinbuiltlocatorfilters.h b/src/app/locator/qgsinbuiltlocatorfilters.h index 25bbc36e3ec9..173862cd050c 100644 --- a/src/app/locator/qgsinbuiltlocatorfilters.h +++ b/src/app/locator/qgsinbuiltlocatorfilters.h @@ -134,6 +134,7 @@ class APP_EXPORT QgsAllLayersFeaturesLocatorFilter : public QgsLocatorFilter QgsExpressionContext context; std::unique_ptr featureSource; QgsFeatureRequest request; + QgsFeatureRequest exactMatchRequest; QString layerName; QString layerId; QIcon layerIcon; diff --git a/src/app/main.cpp b/src/app/main.cpp index 5797b28e31f1..94134d9af948 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -1110,8 +1110,7 @@ int main( int argc, char *argv[] ) // An app bundled with QGIS_MACAPP_BUNDLE > 0 is considered a release bundle. QString relLibPath( QDir::cleanPath( QCoreApplication::applicationDirPath().append( "/../PlugIns" ) ) ); // Note: relLibPath becomes the defacto QT_PLUGINS_DIR of a release app bundle - if ( QFile::exists( relLibPath + QStringLiteral( "/imageformats" ) ) - && QFile::exists( relLibPath + QStringLiteral( "/codecs" ) ) ) + if ( QFile::exists( relLibPath + QStringLiteral( "/imageformats" ) ) ) { // We are in a release app bundle. // Strip QT_PLUGINS_DIR because it will crash a launched release app bundle, since diff --git a/src/app/mesh/qgsmeshdatasetgrouptreeview.cpp b/src/app/mesh/qgsmeshdatasetgrouptreeview.cpp index c258d59ea61a..af89ff84ab67 100644 --- a/src/app/mesh/qgsmeshdatasetgrouptreeview.cpp +++ b/src/app/mesh/qgsmeshdatasetgrouptreeview.cpp @@ -470,8 +470,8 @@ void QgsMeshDatasetGroupTreeView::setActiveGroupFromActiveDataset() if ( mMeshLayer ) { const QgsMeshRendererSettings rendererSettings = mMeshLayer->rendererSettings(); - scalarGroup = rendererSettings.activeScalarDataset().group(); - vectorGroup = rendererSettings.activeVectorDataset().group(); + scalarGroup = rendererSettings.activeScalarDatasetGroup(); + vectorGroup = rendererSettings.activeVectorDatasetGroup(); } setActiveScalarGroup( scalarGroup ); diff --git a/src/app/mesh/qgsmeshlayerproperties.cpp b/src/app/mesh/qgsmeshlayerproperties.cpp index fbdc011a453f..2edcbeb72e7e 100644 --- a/src/app/mesh/qgsmeshlayerproperties.cpp +++ b/src/app/mesh/qgsmeshlayerproperties.cpp @@ -21,12 +21,15 @@ #include "qgisapp.h" #include "qgsapplication.h" #include "qgscoordinatetransform.h" +#include "qgsfileutils.h" #include "qgshelp.h" #include "qgslogger.h" #include "qgsmapcanvas.h" #include "qgsmaplayerstylemanager.h" +#include "qgsmaplayerstyleguiutils.h" #include "qgsmeshlayer.h" #include "qgsmeshlayerproperties.h" +#include "qgsmeshstaticdatasetwidget.h" #include "qgsproject.h" #include "qgsprojectionselectiondialog.h" #include "qgsrenderermeshpropertieswidget.h" @@ -49,6 +52,13 @@ QgsMeshLayerProperties::QgsMeshLayerProperties( QgsMapLayer *lyr, QgsMapCanvas * mRendererMeshPropertiesWidget = new QgsRendererMeshPropertiesWidget( mMeshLayer, canvas, this ); mOptsPage_StyleContent->layout()->addWidget( mRendererMeshPropertiesWidget ); + mStaticScalarWidget->setLayer( mMeshLayer ); + + mTemporalProviderTimeUnitComboBox->addItem( tr( "Seconds" ), QgsUnitTypes::TemporalSeconds ); + mTemporalProviderTimeUnitComboBox->addItem( tr( "Minutes" ), QgsUnitTypes::TemporalMinutes ); + mTemporalProviderTimeUnitComboBox->addItem( tr( "Hours" ), QgsUnitTypes::TemporalHours ); + mTemporalProviderTimeUnitComboBox->addItem( tr( "Days" ), QgsUnitTypes::TemporalDays ); + connect( mLayerOrigNameLineEd, &QLineEdit::textEdited, this, &QgsMeshLayerProperties::updateLayerName ); connect( mCrsSelector, &QgsProjectionSelectionWidget::crsChanged, this, &QgsMeshLayerProperties::changeCrs ); connect( mAddDatasetButton, &QPushButton::clicked, this, &QgsMeshLayerProperties::addDataset ); @@ -64,12 +74,20 @@ QgsMeshLayerProperties::QgsMeshLayerProperties( QgsMapLayer *lyr, QgsMapCanvas * connect( buttonBox->button( QDialogButtonBox::Apply ), &QAbstractButton::clicked, this, &QgsMeshLayerProperties::apply ); connect( mMeshLayer, &QgsMeshLayer::dataChanged, this, &QgsMeshLayerProperties::syncAndRepaint ); + connect( buttonBox, &QDialogButtonBox::helpRequested, this, &QgsMeshLayerProperties::showHelp ); + + connect( mTemporalReloadButton, &QPushButton::clicked, this, &QgsMeshLayerProperties::reloadTemporalProperties ); + connect( mTemporalDateTimeReference, &QDateTimeEdit::dateTimeChanged, this, &QgsMeshLayerProperties::onTimeReferenceChange ); + connect( mTemporalStaticDatasetCheckBox, &QCheckBox::toggled, this, &QgsMeshLayerProperties::onStaticDatasetCheckBoxChanged ); + connect( mMeshLayer, &QgsMeshLayer::activeScalarDatasetGroupChanged, mStaticScalarWidget, &QgsMeshStaticDatasetWidget::setScalarDatasetGroup ); + connect( mMeshLayer, &QgsMeshLayer::activeVectorDatasetGroupChanged, mStaticScalarWidget, &QgsMeshStaticDatasetWidget::setVectorDatasetGroup ); #ifdef HAVE_3D mMesh3DWidget = new QgsMeshLayer3DRendererWidget( mMeshLayer, canvas, mOptsPage_3DView ); mOptsPage_3DView->setLayout( new QVBoxLayout( mOptsPage_3DView ) ); mOptsPage_3DView->layout()->addWidget( mMesh3DWidget ); + mOptsPage_3DView->setProperty( "helpPage", QStringLiteral( "working_with_mesh/mesh_properties.html#d-view-properties" ) ); #else delete mOptsPage_3DView; // removes both the "3d view" list item and its page #endif @@ -91,6 +109,25 @@ QgsMeshLayerProperties::QgsMeshLayerProperties( QgsMapLayer *lyr, QgsMapCanvas * if ( !mMeshLayer->styleManager()->isDefault( mMeshLayer->styleManager()->currentStyle() ) ) title += QStringLiteral( " (%1)" ).arg( mMeshLayer->styleManager()->currentStyle() ); restoreOptionsBaseUi( title ); + + //Add help page references + mOptsPage_Information->setProperty( "helpPage", QStringLiteral( "working_with_mesh/mesh_properties.html#information-properties" ) ); + mOptsPage_Source->setProperty( "helpPage", QStringLiteral( "working_with_mesh/mesh_properties.html#source-properties" ) ); + mOptsPage_Style->setProperty( "helpPage", QStringLiteral( "working_with_mesh/mesh_properties.html#symbology-properties" ) ); + mOptsPage_Rendering->setProperty( "helpPage", QStringLiteral( "working_with_mesh/mesh_properties.html#rendering-properties" ) ); + + + QPushButton *btnStyle = new QPushButton( tr( "Style" ) ); + QMenu *menuStyle = new QMenu( this ); + menuStyle->addAction( tr( "Load Style…" ), this, &QgsMeshLayerProperties::loadStyle ); + menuStyle->addAction( tr( "Save Style…" ), this, &QgsMeshLayerProperties::saveStyleAs ); + menuStyle->addSeparator(); + menuStyle->addAction( tr( "Save as Default" ), this, &QgsMeshLayerProperties::saveDefaultStyle ); + menuStyle->addAction( tr( "Restore Default" ), this, &QgsMeshLayerProperties::loadDefaultStyle ); + btnStyle->setMenu( menuStyle ); + connect( menuStyle, &QMenu::aboutToShow, this, &QgsMeshLayerProperties::aboutToShowStyleMenu ); + + buttonBox->addButton( btnStyle, QDialogButtonBox::ResetRole ); } void QgsMeshLayerProperties::syncToLayer() @@ -154,6 +191,21 @@ void QgsMeshLayerProperties::syncToLayer() mSimplifyMeshGroupBox->setChecked( simplifySettings.isEnabled() ); mSimplifyReductionFactorSpinBox->setValue( simplifySettings.reductionFactor() ); mSimplifyMeshResolutionSpinBox->setValue( simplifySettings.meshResolution() ); + + QgsDebugMsgLevel( QStringLiteral( "populate temporal tab" ), 4 ); + whileBlocking( mTemporalDateTimeReference )->setDateTime( mMeshLayer->temporalProperties()->referenceTime() ); + const QgsDateTimeRange timeRange = mMeshLayer->temporalProperties()->timeExtent(); + mTemporalDateTimeStart->setDateTime( timeRange.begin() ); + mTemporalDateTimeEnd->setDateTime( timeRange.end() ); + if ( mMeshLayer->dataProvider() ) + { + mTemporalProviderTimeUnitComboBox->setCurrentIndex( + mTemporalProviderTimeUnitComboBox->findData( mMeshLayer->dataProvider()->temporalCapabilities()->temporalUnit() ) ); + } + + mStaticScalarWidget->syncToLayer(); + mStaticScalarWidget->setVisible( !mMeshLayer->temporalProperties()->isActive() ); + mTemporalStaticDatasetCheckBox->setChecked( !mMeshLayer->temporalProperties()->isActive() ); } void QgsMeshLayerProperties::addDataset() @@ -170,7 +222,7 @@ void QgsMeshLayerProperties::addDataset() if ( openFileString.isEmpty() ) { - return; //canceled by the user + return; // canceled by the user } QFileInfo openFileInfo( openFileString ); @@ -189,6 +241,107 @@ void QgsMeshLayerProperties::addDataset() } } +void QgsMeshLayerProperties::loadDefaultStyle() +{ + bool defaultLoadedFlag = false; + QString myMessage = mMeshLayer->loadDefaultStyle( defaultLoadedFlag ); + // reset if the default style was loaded OK only + if ( defaultLoadedFlag ) + { + syncToLayer(); + } + else + { + // otherwise let the user know what went wrong + QMessageBox::information( this, + tr( "Default Style" ), + myMessage + ); + } +} + +void QgsMeshLayerProperties::saveDefaultStyle() +{ + apply(); // make sure the style to save is up-to-date + + // a flag passed by reference + bool defaultSavedFlag = false; + // after calling this the above flag will be set true for success + // or false if the save operation failed + QString myMessage = mMeshLayer->saveDefaultStyle( defaultSavedFlag ); + if ( !defaultSavedFlag ) + { + // let the user know what went wrong + QMessageBox::information( this, + tr( "Default Style" ), + myMessage + ); + } +} + +void QgsMeshLayerProperties::loadStyle() +{ + QgsSettings settings; + QString lastUsedDir = settings.value( QStringLiteral( "style/lastStyleDir" ), QDir::homePath() ).toString(); + + QString fileName = QFileDialog::getOpenFileName( + this, + tr( "Load rendering setting from style file" ), + lastUsedDir, + tr( "QGIS Layer Style File" ) + " (*.qml)" ); + if ( fileName.isEmpty() ) + return; + + // ensure the user never omits the extension from the file name + if ( !fileName.endsWith( QLatin1String( ".qml" ), Qt::CaseInsensitive ) ) + fileName += QLatin1String( ".qml" ); + + mOldStyle = mMeshLayer->styleManager()->style( mMeshLayer->styleManager()->currentStyle() ); + + bool defaultLoadedFlag = false; + QString message = mMeshLayer->loadNamedStyle( fileName, defaultLoadedFlag ); + if ( defaultLoadedFlag ) + { + settings.setValue( QStringLiteral( "style/lastStyleDir" ), QFileInfo( fileName ).absolutePath() ); + syncToLayer(); + } + else + { + QMessageBox::information( this, tr( "Load Style" ), message ); + } +} + +void QgsMeshLayerProperties::saveStyleAs() +{ + QgsSettings settings; + QString lastUsedDir = settings.value( QStringLiteral( "style/lastStyleDir" ), QDir::homePath() ).toString(); + + QString outputFileName = QFileDialog::getSaveFileName( + this, + tr( "Save layer properties as style file" ), + lastUsedDir, + tr( "QGIS Layer Style File" ) + " (*.qml)" ); + if ( outputFileName.isEmpty() ) + return; + + // ensure the user never omits the extension from the file name + outputFileName = QgsFileUtils::ensureFileNameHasExtension( outputFileName, QStringList() << QStringLiteral( "qml" ) ); + + apply(); // make sure the style to save is up-to-date + + // then export style + bool defaultLoadedFlag = false; + QString message; + message = mMeshLayer->saveNamedStyle( outputFileName, defaultLoadedFlag ); + + if ( defaultLoadedFlag ) + { + settings.setValue( QStringLiteral( "style/lastStyleDir" ), QFileInfo( outputFileName ).absolutePath() ); + } + else + QMessageBox::information( this, tr( "Save Style" ), message ); +} + void QgsMeshLayerProperties::apply() { Q_ASSERT( mRendererMeshPropertiesWidget ); @@ -225,9 +378,26 @@ void QgsMeshLayerProperties::apply() mMeshLayer->setMeshSimplificationSettings( simplifySettings ); + QgsDebugMsgLevel( QStringLiteral( "processing temporal tab" ), 4 ); + /* + * Temporal Tab + */ + + mMeshLayer->setReferenceTime( mTemporalDateTimeReference->dateTime() ); + if ( mMeshLayer->dataProvider() ) + mMeshLayer->dataProvider()->setTemporalUnit( + static_cast( mTemporalProviderTimeUnitComboBox->currentData().toInt() ) ); + + mStaticScalarWidget->apply(); + bool needEmitRendererChanged = mMeshLayer->temporalProperties()->isActive() == mTemporalStaticDatasetCheckBox->isChecked(); + mMeshLayer->temporalProperties()->setIsActive( !mTemporalStaticDatasetCheckBox->isChecked() ); + if ( needMeshUpdating ) mMeshLayer->reload(); + if ( needEmitRendererChanged ) + emit mMeshLayer->rendererChanged(); + //make sure the layer is redrawn mMeshLayer->triggerRepaint(); @@ -251,3 +421,57 @@ void QgsMeshLayerProperties::syncAndRepaint() syncToLayer(); mMeshLayer->triggerRepaint(); } + +void QgsMeshLayerProperties::showHelp() +{ + const QVariant helpPage = mOptionsStackedWidget->currentWidget()->property( "helpPage" ); + + if ( helpPage.isValid() ) + { + QgsHelp::openHelp( helpPage.toString() ); + } + else + { + QgsHelp::openHelp( QStringLiteral( "working_with_mesh/mesh_properties.html" ) ); + } +} + +void QgsMeshLayerProperties::aboutToShowStyleMenu() +{ + QMenu *m = qobject_cast( sender() ); + + QgsMapLayerStyleGuiUtils::instance()->removesExtraMenuSeparators( m ); + // re-add style manager actions! + m->addSeparator(); + QgsMapLayerStyleGuiUtils::instance()->addStyleManagerActions( m, mMeshLayer ); +} + +void QgsMeshLayerProperties::reloadTemporalProperties() +{ + QgsMeshDataProviderTemporalCapabilities *temporalCapabalities = mMeshLayer->dataProvider()->temporalCapabilities(); + QgsDateTimeRange timeExtent; + QDateTime referenceTime = temporalCapabalities->referenceTime(); + if ( referenceTime.isValid() ) + { + timeExtent = temporalCapabalities->timeExtent(); + whileBlocking( mTemporalDateTimeReference )->setDateTime( referenceTime ); + } + else + // The reference time already here is used again to define the time extent + timeExtent = temporalCapabalities->timeExtent( mTemporalDateTimeReference->dateTime() ); + + mTemporalDateTimeStart->setDateTime( timeExtent.begin() ); + mTemporalDateTimeEnd->setDateTime( timeExtent.end() ); +} + +void QgsMeshLayerProperties::onTimeReferenceChange() +{ + const QgsDateTimeRange &timeExtent = mMeshLayer->dataProvider()->temporalCapabilities()->timeExtent( mTemporalDateTimeReference->dateTime() ); + mTemporalDateTimeStart->setDateTime( timeExtent.begin() ); + mTemporalDateTimeEnd->setDateTime( timeExtent.end() ); +} + +void QgsMeshLayerProperties::onStaticDatasetCheckBoxChanged() +{ + mStaticScalarWidget->setVisible( mTemporalStaticDatasetCheckBox->isChecked() ); +} diff --git a/src/app/mesh/qgsmeshlayerproperties.h b/src/app/mesh/qgsmeshlayerproperties.h index b6fcbe80bdeb..68b2dfadab17 100644 --- a/src/app/mesh/qgsmeshlayerproperties.h +++ b/src/app/mesh/qgsmeshlayerproperties.h @@ -19,6 +19,7 @@ #include "ui_qgsmeshlayerpropertiesbase.h" +#include "qgsmaplayerstylemanager.h" #include "qgsoptionsdialogbase.h" #include "qgsguiutils.h" #include "qgis_app.h" @@ -28,6 +29,7 @@ class QgsMapCanvas; class QgsMeshLayer; class QgsRendererMeshPropertiesWidget; class QgsMeshLayer3DRendererWidget; +class QgsMeshStaticDatasetWidget; /** * Property sheet for a mesh map layer. @@ -46,20 +48,35 @@ class APP_EXPORT QgsMeshLayerProperties : public QgsOptionsDialogBase, private U QgsMeshLayerProperties( QgsMapLayer *lyr, QgsMapCanvas *canvas, QWidget *parent = nullptr, Qt::WindowFlags = QgsGuiUtils::ModalDialogFlags ); private slots: - //! Synchronize widgets state with associated mesh layer + //! Synchronizes widgets state with associated mesh layer void syncToLayer(); //!Applies the settings made in the dialog without closing the box void apply(); //! \brief Slot to update layer display name as original is edited. void updateLayerName( const QString &text ); - //! Synchronize GUI state with associated mesh layer and trigger repaint + //! Synchronizes GUI state with associated mesh layer and trigger repaint void syncAndRepaint(); - //! Change layer coordinate reference system + //! Changes layer coordinate reference system void changeCrs( const QgsCoordinateReferenceSystem &crs ); - //! Associate dataset to the mesh layer + //! Associates dataset to the mesh layer void addDataset(); + //! Loads the default style when appropriate button is pressed + void loadDefaultStyle(); + //! Saves the default style when appropriate button is pressed + void saveDefaultStyle(); + //! Loads a saved style when appropriate button is pressed + void loadStyle(); + //! Saves a style when appriate button is pressed + void saveStyleAs(); + //! Prepares style menu + void aboutToShowStyleMenu(); + //! Reloads temporal properties from the provider + void reloadTemporalProperties(); + void onTimeReferenceChange(); + + void onStaticDatasetCheckBoxChanged(); private: //! Pointer to the mesh styling widget QgsRendererMeshPropertiesWidget *mRendererMeshPropertiesWidget = nullptr; @@ -70,7 +87,14 @@ class APP_EXPORT QgsMeshLayerProperties : public QgsOptionsDialogBase, private U //! Pointer to mesh 3d styling widget QgsMeshLayer3DRendererWidget *mMesh3DWidget = nullptr; + /** + * Previous layer style. Used to reset style to previous state if new style + * was loaded but dialog is canceled */ + QgsMapLayerStyle mOldStyle; + friend class TestQgsMeshLayerPropertiesDialog; + + void showHelp(); }; diff --git a/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp b/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp index 32345a812386..777f448c2628 100644 --- a/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp +++ b/src/app/mesh/qgsmeshrendereractivedatasetwidget.cpp @@ -23,54 +23,25 @@ #include "qgsmeshlayer.h" #include "qgsmessagelog.h" #include "qgsmeshrenderersettings.h" -#include "qgsmeshtimeformatdialog.h" QgsMeshRendererActiveDatasetWidget::QgsMeshRendererActiveDatasetWidget( QWidget *parent ) : QWidget( parent ) { setupUi( this ); - connect( mTimeComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsMeshRendererActiveDatasetWidget::onActiveTimeChanged ); - connect( mDatasetSlider, &QSlider::valueChanged, mTimeComboBox, &QComboBox::setCurrentIndex ); - - connect( mTimeFormatButton, &QToolButton::clicked, this, &QgsMeshRendererActiveDatasetWidget::onTimeSettingsClicked ); - connect( mFirstDatasetButton, &QToolButton::clicked, this, &QgsMeshRendererActiveDatasetWidget::onFirstTimeClicked ); - connect( mPreviousDatasetButton, &QToolButton::clicked, this, &QgsMeshRendererActiveDatasetWidget::onPreviousTimeClicked ); - connect( mNextDatasetButton, &QToolButton::clicked, this, &QgsMeshRendererActiveDatasetWidget::onNextTimeClicked ); - connect( mLastDatasetButton, &QToolButton::clicked, this, &QgsMeshRendererActiveDatasetWidget::onLastTimeClicked ); - connect( mDatasetPlaybackButton, &QToolButton::clicked, this, &QgsMeshRendererActiveDatasetWidget::onDatasetPlaybackClicked ); - connect( mDatasetGroupTreeView, &QgsMeshDatasetGroupTreeView::activeScalarGroupChanged, this, &QgsMeshRendererActiveDatasetWidget::onActiveScalarGroupChanged ); connect( mDatasetGroupTreeView, &QgsMeshDatasetGroupTreeView::activeVectorGroupChanged, this, &QgsMeshRendererActiveDatasetWidget::onActiveVectorGroupChanged ); - - mDatasetPlaybackTimer = new QTimer( this ); - connect( mDatasetPlaybackTimer, &QTimer::timeout, - this, qgis::overload<>::of( &QgsMeshRendererActiveDatasetWidget::datasetPlaybackTick ) ); - - mDatasetPlaybackButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionPlay.svg" ) ) ); - mFirstDatasetButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFirst.svg" ) ) ); - mPreviousDatasetButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionPrevious.svg" ) ) ); - mNextDatasetButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionNext.svg" ) ) ); - mLastDatasetButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionLast.svg" ) ) ); } -QgsMeshRendererActiveDatasetWidget::~QgsMeshRendererActiveDatasetWidget() -{ - mDatasetPlaybackTimer->stop(); -} +QgsMeshRendererActiveDatasetWidget::~QgsMeshRendererActiveDatasetWidget() = default; + void QgsMeshRendererActiveDatasetWidget::setLayer( QgsMeshLayer *layer ) { mMeshLayer = layer; mDatasetGroupTreeView->setLayer( layer ); - setTimeRange(); - - if ( layer ) - { - connect( mMeshLayer, &QgsMeshLayer::timeSettingsChanged, this, &QgsMeshRendererActiveDatasetWidget::setTimeRange ); - } } int QgsMeshRendererActiveDatasetWidget::activeScalarDatasetGroup() const @@ -83,79 +54,6 @@ int QgsMeshRendererActiveDatasetWidget::activeVectorDatasetGroup() const return mActiveVectorDatasetGroup; } -QgsMeshDatasetIndex QgsMeshRendererActiveDatasetWidget::activeScalarDataset() const -{ - return mActiveScalarDataset; -} - -QgsMeshDatasetIndex QgsMeshRendererActiveDatasetWidget::activeVectorDataset() const -{ - return mActiveVectorDataset; -} - -void QgsMeshRendererActiveDatasetWidget::setTimeRange() -{ - // figure out which dataset group contains the greatest number of datasets. - // this group will be used to initialize the time combo box. - int datasetCount = 0; - int groupWithMaximumDatasets = -1; - if ( mMeshLayer && mMeshLayer->dataProvider() ) - { - for ( int i = 0; i < mMeshLayer->dataProvider()->datasetGroupCount(); ++i ) - { - int currentCount = mMeshLayer->dataProvider()->datasetCount( i ); - if ( currentCount > datasetCount ) - { - datasetCount = currentCount; - groupWithMaximumDatasets = i; - } - } - } - - // update slider - mDatasetSlider->blockSignals( true ); - mDatasetSlider->setMinimum( 0 ); - mDatasetSlider->setMaximum( datasetCount - 1 ); - mDatasetSlider->blockSignals( false ); - - // update combobox - mTimeComboBox->blockSignals( true ); - int currentIndex = mTimeComboBox->currentIndex(); - mTimeComboBox->clear(); - if ( groupWithMaximumDatasets > -1 ) - { - for ( int i = 0; i < datasetCount; ++i ) - { - QgsMeshDatasetIndex index( groupWithMaximumDatasets, i ); - QgsMeshDatasetMetadata meta = mMeshLayer->dataProvider()->datasetMetadata( index ); - double time = meta.time(); - mTimeComboBox->addItem( mMeshLayer->formatTime( time ), time ); - } - } - mTimeComboBox->setCurrentIndex( currentIndex ); - mTimeComboBox->blockSignals( false ); - updateMetadata(); - // enable/disable time controls depending on whether the data set is time varying - enableTimeControls(); -} - -void QgsMeshRendererActiveDatasetWidget::enableTimeControls() -{ - const QgsMeshDataProvider *provider = mMeshLayer->dataProvider(); - if ( !provider ) - return; - const int scalarDatesets = provider->datasetCount( mActiveScalarDatasetGroup ); - const int vectorDatesets = provider->datasetCount( mActiveVectorDatasetGroup ); - const bool isTimeVarying = ( scalarDatesets > 1 ) || ( vectorDatesets > 1 ); - mTimeComboBox->setEnabled( isTimeVarying ); - mDatasetSlider->setEnabled( isTimeVarying ); - mTimeFormatButton->setEnabled( isTimeVarying ); - mFirstDatasetButton->setEnabled( isTimeVarying ); - mPreviousDatasetButton->setEnabled( isTimeVarying ); - mNextDatasetButton->setEnabled( isTimeVarying ); - mLastDatasetButton->setEnabled( isTimeVarying ); - mDatasetPlaybackButton->setEnabled( isTimeVarying ); -} void QgsMeshRendererActiveDatasetWidget::onActiveScalarGroupChanged( int groupIndex ) { @@ -163,13 +61,9 @@ void QgsMeshRendererActiveDatasetWidget::onActiveScalarGroupChanged( int groupIn return; mActiveScalarDatasetGroup = groupIndex; - - // enable/disable time slider controls - enableTimeControls(); - - // keep the same timestep if possible - onActiveTimeChanged( mTimeComboBox->currentIndex() ); + updateMetadata(); emit activeScalarGroupChanged( mActiveScalarDatasetGroup ); + emit widgetChanged(); } void QgsMeshRendererActiveDatasetWidget::onActiveVectorGroupChanged( int groupIndex ) @@ -178,127 +72,9 @@ void QgsMeshRendererActiveDatasetWidget::onActiveVectorGroupChanged( int groupIn return; mActiveVectorDatasetGroup = groupIndex; - // enable/disable time slider controls - enableTimeControls(); - - // keep the same timestep if possible - onActiveTimeChanged( mTimeComboBox->currentIndex() ); + updateMetadata(); emit activeVectorGroupChanged( mActiveVectorDatasetGroup ); -} - -void QgsMeshRendererActiveDatasetWidget::onActiveTimeChanged( int value ) -{ - if ( !mMeshLayer || !mMeshLayer->dataProvider() ) - return; - - bool changed = false; - - QgsMeshDatasetIndex activeScalarDataset( - mActiveScalarDatasetGroup, - std::min( value, mMeshLayer->dataProvider()->datasetCount( mActiveScalarDatasetGroup ) - 1 ) - ); - if ( activeScalarDataset != mActiveScalarDataset ) - { - mActiveScalarDataset = activeScalarDataset; - changed = true; - } - - QgsMeshDatasetIndex activeVectorDataset( - mActiveVectorDatasetGroup, - std::min( value, mMeshLayer->dataProvider()->datasetCount( mActiveVectorDatasetGroup ) - 1 ) - ); - if ( activeVectorDataset != mActiveVectorDataset ) - { - mActiveVectorDataset = activeVectorDataset; - changed = true; - } - - if ( changed ) - { - whileBlocking( mDatasetSlider )->setValue( value ); - updateMetadata(); - emit widgetChanged(); - } -} - -void QgsMeshRendererActiveDatasetWidget::onTimeSettingsClicked() -{ - if ( !mMeshLayer ) - return; - QgsMeshTimeFormatDialog dlg( mMeshLayer ); - dlg.setModal( true ); - dlg.exec(); -} - -void QgsMeshRendererActiveDatasetWidget::onFirstTimeClicked() -{ - mTimeComboBox->setCurrentIndex( 0 ); -} - -void QgsMeshRendererActiveDatasetWidget::onPreviousTimeClicked() -{ - int idx = mTimeComboBox->currentIndex() - 1; - if ( idx >= 0 ) - mTimeComboBox->setCurrentIndex( idx ); -} - -void QgsMeshRendererActiveDatasetWidget::onNextTimeClicked() -{ - int idx = mTimeComboBox->currentIndex() + 1; - if ( idx < mTimeComboBox->count() ) - mTimeComboBox->setCurrentIndex( idx ); -} - -void QgsMeshRendererActiveDatasetWidget::onLastTimeClicked() -{ - mTimeComboBox->setCurrentIndex( mTimeComboBox->count() - 1 ); -} - -void QgsMeshRendererActiveDatasetWidget::onDatasetPlaybackClicked() -{ - if ( mDatasetIsPlaying ) - { - // stop playing - mDatasetIsPlaying = false; - mTimeComboBox->setEnabled( true ); - mTimeFormatButton->setEnabled( true ); - mDatasetSlider->setEnabled( true ); - mFirstDatasetButton->setEnabled( true ); - mPreviousDatasetButton->setEnabled( true ); - mNextDatasetButton->setEnabled( true ); - mLastDatasetButton->setEnabled( true ); - mDatasetPlaybackTimer->stop(); - mDatasetPlaybackButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionPlay.svg" ) ) ); - } - else - { - // start - mDatasetIsPlaying = true; - mTimeComboBox->setEnabled( false ); - mTimeFormatButton->setEnabled( false ); - mDatasetSlider->setEnabled( false ); - mFirstDatasetButton->setEnabled( false ); - mPreviousDatasetButton->setEnabled( false ); - mNextDatasetButton->setEnabled( false ); - mLastDatasetButton->setEnabled( false ); - int intervalMs = 3000; - if ( mMeshLayer ) - { - intervalMs = static_cast( mMeshLayer->timeSettings().datasetPlaybackInterval() * 1000 ); - } - datasetPlaybackTick(); - mDatasetPlaybackTimer->start( intervalMs ); - mDatasetPlaybackButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionStop.svg" ) ) ); - } -} - -void QgsMeshRendererActiveDatasetWidget::datasetPlaybackTick() -{ - int nextIdx = mTimeComboBox->currentIndex() + 1; - if ( nextIdx >= mTimeComboBox->count() ) - nextIdx = 0; - - mTimeComboBox->setCurrentIndex( nextIdx ); + emit widgetChanged(); } void QgsMeshRendererActiveDatasetWidget::updateMetadata() @@ -312,33 +88,33 @@ void QgsMeshRendererActiveDatasetWidget::updateMetadata() } else { - if ( mActiveScalarDataset.isValid() ) + if ( mActiveScalarDatasetGroup > -1 ) { - if ( mActiveVectorDataset.isValid() ) + if ( mActiveVectorDatasetGroup > -1 ) { - if ( mActiveScalarDataset == mActiveVectorDataset ) + if ( mActiveScalarDatasetGroup == mActiveVectorDatasetGroup ) { - msg += metadata( mActiveScalarDataset ); + msg += metadata( mActiveScalarDatasetGroup ); } else { msg += QStringLiteral( "

    %1

    " ).arg( tr( "Scalar dataset" ) ); - msg += metadata( mActiveScalarDataset ); + msg += metadata( mActiveScalarDatasetGroup ); msg += QStringLiteral( "

    %1

    " ).arg( tr( "Vector dataset" ) ); - msg += metadata( mActiveVectorDataset ); + msg += metadata( mActiveVectorDatasetGroup ); msg += QStringLiteral( "

    " ); } } else { - msg += metadata( mActiveScalarDataset ); + msg += metadata( mActiveScalarDatasetGroup ); } } else { - if ( mActiveVectorDataset.isValid() ) + if ( mActiveVectorDatasetGroup > -1 ) { - msg += metadata( mActiveVectorDataset ); + msg += metadata( mActiveVectorDatasetGroup ); } else { @@ -357,17 +133,6 @@ QString QgsMeshRendererActiveDatasetWidget::metadata( QgsMeshDatasetIndex datase QString msg; msg += QStringLiteral( "" ); - const QgsMeshDatasetMetadata meta = mMeshLayer->dataProvider()->datasetMetadata( datasetIndex ); - msg += QStringLiteral( "" ) - .arg( tr( "Is valid" ) ) - .arg( meta.isValid() ? tr( "Yes" ) : tr( "No" ) ); - - const double time = meta.time(); - msg += QStringLiteral( "" ) - .arg( tr( "Time" ) ) - .arg( mMeshLayer->formatTime( time ) ) - .arg( time ); - QString definedOnMesh; if ( mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Face ) ) { @@ -439,26 +204,12 @@ void QgsMeshRendererActiveDatasetWidget::syncToLayer() const QgsMeshRendererSettings rendererSettings = mMeshLayer->rendererSettings(); mActiveScalarDatasetGroup = mDatasetGroupTreeView->activeScalarGroup(); mActiveVectorDatasetGroup = mDatasetGroupTreeView->activeVectorGroup(); - mActiveScalarDataset = rendererSettings.activeScalarDataset(); - mActiveVectorDataset = rendererSettings.activeVectorDataset(); } else { mActiveScalarDatasetGroup = -1; mActiveVectorDatasetGroup = -1; - mActiveScalarDataset = QgsMeshDatasetIndex(); - mActiveVectorDataset = QgsMeshDatasetIndex(); } - setTimeRange(); - - int val = 0; - if ( mActiveScalarDataset.isValid() ) - val = mActiveScalarDataset.dataset(); - else if ( mActiveVectorDataset.isValid() ) - val = mActiveVectorDataset.dataset(); - - whileBlocking( mTimeComboBox )->setCurrentIndex( val ); - whileBlocking( mDatasetSlider )->setValue( val ); updateMetadata(); } diff --git a/src/app/mesh/qgsmeshrendereractivedatasetwidget.h b/src/app/mesh/qgsmeshrendereractivedatasetwidget.h index bca868f9e568..545dd12c8a56 100644 --- a/src/app/mesh/qgsmeshrendereractivedatasetwidget.h +++ b/src/app/mesh/qgsmeshrendereractivedatasetwidget.h @@ -55,12 +55,6 @@ class APP_EXPORT QgsMeshRendererActiveDatasetWidget : public QWidget, private Ui //! Returns index of the active vector dataset group int activeVectorDatasetGroup() const; - //! Gets index of the selected/active scalar dataset - QgsMeshDatasetIndex activeScalarDataset() const; - - //! Gets index of the selected/active vector dataset - QgsMeshDatasetIndex activeVectorDataset() const; - //! Synchronizes widgets state with associated mesh layer void syncToLayer(); @@ -78,32 +72,14 @@ class APP_EXPORT QgsMeshRendererActiveDatasetWidget : public QWidget, private Ui private slots: void onActiveScalarGroupChanged( int groupIndex ); void onActiveVectorGroupChanged( int groupIndex ); - void onActiveTimeChanged( int value ); - void onTimeSettingsClicked(); - void onFirstTimeClicked(); - void onPreviousTimeClicked(); - void onNextTimeClicked(); - void onLastTimeClicked(); - void onDatasetPlaybackClicked(); - void datasetPlaybackTick(); QString metadata( QgsMeshDatasetIndex datasetIndex ); private: - //! Loops through all dataset groups and finds the maximum number of datasets - void setTimeRange(); - - //! Enables/Disables time controls depending on whether the selected datasets are time varying - void enableTimeControls(); - void updateMetadata(); QgsMeshLayer *mMeshLayer = nullptr; // not owned int mActiveScalarDatasetGroup = -1; int mActiveVectorDatasetGroup = -1; - QgsMeshDatasetIndex mActiveScalarDataset; - QgsMeshDatasetIndex mActiveVectorDataset; - bool mDatasetIsPlaying = false; - QTimer *mDatasetPlaybackTimer = nullptr; }; #endif // QGSMESHRENDERERSCALARSETTINGSWIDGET_H diff --git a/src/app/mesh/qgsmeshrendererscalarsettingswidget.cpp b/src/app/mesh/qgsmeshrendererscalarsettingswidget.cpp index 0dbe304b6a39..83abc60e709e 100644 --- a/src/app/mesh/qgsmeshrendererscalarsettingswidget.cpp +++ b/src/app/mesh/qgsmeshrendererscalarsettingswidget.cpp @@ -55,13 +55,13 @@ QgsMeshRendererScalarSettingsWidget::QgsMeshRendererScalarSettingsWidget( QWidge void QgsMeshRendererScalarSettingsWidget::setLayer( QgsMeshLayer *layer ) { mMeshLayer = layer; - mScalarInterpolationTypeComboBox->setEnabled( dataIsDefinedOnFaces() ); + mScalarInterpolationTypeComboBox->setEnabled( !dataIsDefinedOnEdges() ); } void QgsMeshRendererScalarSettingsWidget::setActiveDatasetGroup( int groupIndex ) { mActiveDatasetGroup = groupIndex; - mScalarInterpolationTypeComboBox->setEnabled( dataIsDefinedOnFaces() ); + mScalarInterpolationTypeComboBox->setEnabled( !dataIsDefinedOnEdges() ); } QgsMeshRendererScalarSettings QgsMeshRendererScalarSettingsWidget::settings() const @@ -70,7 +70,7 @@ QgsMeshRendererScalarSettings QgsMeshRendererScalarSettingsWidget::settings() co settings.setColorRampShader( mScalarColorRampShaderWidget->shader() ); settings.setClassificationMinimumMaximum( lineEditValue( mScalarMinLineEdit ), lineEditValue( mScalarMaxLineEdit ) ); settings.setOpacity( mOpacityWidget->opacity() ); - settings.setDataInterpolationMethod( dataIntepolationMethod() ); + settings.setDataResamplingMethod( dataIntepolationMethod() ); settings.setEdgeWidth( mScalarEdgeWidthSpinBox->value() ); settings.setEdgeWidthUnit( mScalarEdgeWidthUnitSelectionWidget->unit() ); return settings; @@ -98,7 +98,7 @@ void QgsMeshRendererScalarSettingsWidget::syncToLayer( ) whileBlocking( mScalarColorRampShaderWidget )->setFromShader( shader ); whileBlocking( mScalarColorRampShaderWidget )->setMinimumMaximum( min, max ); whileBlocking( mOpacityWidget )->setOpacity( settings.opacity() ); - int index = mScalarInterpolationTypeComboBox->findData( settings.dataInterpolationMethod() ); + int index = mScalarInterpolationTypeComboBox->findData( settings.dataResamplingMethod() ); whileBlocking( mScalarInterpolationTypeComboBox )->setCurrentIndex( index ); bool hasEdges = ( mMeshLayer->dataProvider() && @@ -140,18 +140,11 @@ void QgsMeshRendererScalarSettingsWidget::recalculateMinMaxButtonClicked() mScalarColorRampShaderWidget->setMinimumMaximumAndClassify( min, max ); } -QgsMeshRendererScalarSettings::DataInterpolationMethod QgsMeshRendererScalarSettingsWidget::dataIntepolationMethod() const +QgsMeshRendererScalarSettings::DataResamplingMethod QgsMeshRendererScalarSettingsWidget::dataIntepolationMethod() const { - if ( dataIsDefinedOnFaces() ) - { - const int data = mScalarInterpolationTypeComboBox->currentData().toInt(); - const QgsMeshRendererScalarSettings::DataInterpolationMethod method = static_cast( data ); - return method; - } - else - { - return QgsMeshRendererScalarSettings::None; - } + const int data = mScalarInterpolationTypeComboBox->currentData().toInt(); + const QgsMeshRendererScalarSettings::DataResamplingMethod method = static_cast( data ); + return method; } bool QgsMeshRendererScalarSettingsWidget::dataIsDefinedOnFaces() const @@ -167,4 +160,17 @@ bool QgsMeshRendererScalarSettingsWidget::dataIsDefinedOnFaces() const return onFaces; } +bool QgsMeshRendererScalarSettingsWidget::dataIsDefinedOnEdges() const +{ + if ( !mMeshLayer || !mMeshLayer->dataProvider() || !mMeshLayer->dataProvider()->isValid() ) + return false; + + if ( mActiveDatasetGroup < 0 ) + return false; + + QgsMeshDatasetGroupMetadata meta = mMeshLayer->dataProvider()->datasetGroupMetadata( mActiveDatasetGroup ); + const bool onEdges = ( meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnEdges ); + return onEdges; +} + diff --git a/src/app/mesh/qgsmeshrendererscalarsettingswidget.h b/src/app/mesh/qgsmeshrendererscalarsettingswidget.h index 3b081d8e18b5..dbae76d8c7f4 100644 --- a/src/app/mesh/qgsmeshrendererscalarsettingswidget.h +++ b/src/app/mesh/qgsmeshrendererscalarsettingswidget.h @@ -65,9 +65,10 @@ class APP_EXPORT QgsMeshRendererScalarSettingsWidget : public QWidget, private U private: double lineEditValue( const QLineEdit *lineEdit ) const; - QgsMeshRendererScalarSettings::DataInterpolationMethod dataIntepolationMethod() const; + QgsMeshRendererScalarSettings::DataResamplingMethod dataIntepolationMethod() const; bool dataIsDefinedOnFaces() const; + bool dataIsDefinedOnEdges() const; QgsMeshLayer *mMeshLayer = nullptr; // not owned int mActiveDatasetGroup = -1; diff --git a/src/app/mesh/qgsmeshrenderervectorsettingswidget.cpp b/src/app/mesh/qgsmeshrenderervectorsettingswidget.cpp index 0fca69057d7b..c7f2315eea8d 100644 --- a/src/app/mesh/qgsmeshrenderervectorsettingswidget.cpp +++ b/src/app/mesh/qgsmeshrenderervectorsettingswidget.cpp @@ -26,7 +26,19 @@ QgsMeshRendererVectorSettingsWidget::QgsMeshRendererVectorSettingsWidget( QWidge mShaftLengthComboBox->setCurrentIndex( -1 ); + mColoringMethodComboBox->addItem( tr( "Single Color" ), QgsMeshRendererVectorSettings::SingleColor ); + mColoringMethodComboBox->addItem( tr( "Color Ramp Shader" ), QgsMeshRendererVectorSettings::ColorRamp ); + connect( mColorWidget, &QgsColorButton::colorChanged, this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); + connect( mColoringMethodComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), + this, &QgsMeshRendererVectorSettingsWidget::onColoringMethodChanged ); + connect( mColorRampShaderWidget, &QgsColorRampShaderWidget::widgetChanged, + this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); + connect( mColorRampShaderMinimumEditLine, &QLineEdit::textEdited, + this, &QgsMeshRendererVectorSettingsWidget::onColorRampMinMaxChanged ); + connect( mColorRampShaderMaximumEditLine, &QLineEdit::textEdited, + this, &QgsMeshRendererVectorSettingsWidget::onColorRampMinMaxChanged ); + connect( mLineWidthSpinBox, qgis::overload::of( &QgsDoubleSpinBox::valueChanged ), this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); @@ -38,6 +50,10 @@ QgsMeshRendererVectorSettingsWidget::QgsMeshRendererVectorSettingsWidget( QWidge connect( mDisplayVectorsOnGridGroupBox, &QGroupBox::toggled, this, &QgsMeshRendererVectorSettingsWidget::widgetChanged ); + connect( mColorRampShaderLoadButton, &QPushButton::clicked, this, &QgsMeshRendererVectorSettingsWidget::loadColorRampShader ); + + onColoringMethodChanged(); + QVector widgets; widgets << mMinMagLineEdit << mMaxMagLineEdit << mHeadWidthLineEdit << mHeadLengthLineEdit @@ -102,6 +118,9 @@ QgsMeshRendererVectorSettings QgsMeshRendererVectorSettingsWidget::settings() co // basic settings.setColor( mColorWidget->color() ); settings.setLineWidth( mLineWidthSpinBox->value() ); + settings.setColoringMethod( static_cast + ( mColoringMethodComboBox->currentData().toInt() ) ); + settings.setColorRampShader( mColorRampShaderWidget->shader() ); // filter by magnitude double val = filterValue( mMinMagLineEdit->text(), -1 ); @@ -179,6 +198,10 @@ void QgsMeshRendererVectorSettingsWidget::syncToLayer( ) // basic mColorWidget->setColor( settings.color() ); mLineWidthSpinBox->setValue( settings.lineWidth() ); + mColoringMethodComboBox->setCurrentIndex( mColoringMethodComboBox->findData( settings.coloringMethod() ) ); + mColorRampShaderWidget->setFromShader( settings.colorRampShader() ); + mColorRampShaderMinimumEditLine->setText( QString::number( settings.colorRampShader().minimumValue() ) ); + mColorRampShaderMaximumEditLine->setText( QString::number( settings.colorRampShader().maximumValue() ) ); // filter by magnitude if ( settings.filterMin() > 0 ) @@ -246,6 +269,46 @@ void QgsMeshRendererVectorSettingsWidget::onStreamLineSeedingMethodChanged( int mDisplayVectorsOnGridGroupBox->setEnabled( !enabled ); } +void QgsMeshRendererVectorSettingsWidget::onColoringMethodChanged() +{ + mColorRampShaderGroupBox->setVisible( mColoringMethodComboBox->currentData() == QgsMeshRendererVectorSettings::ColorRamp ); + mColorWidget->setVisible( mColoringMethodComboBox->currentData() == QgsMeshRendererVectorSettings::SingleColor ); + mSimgleColorLabel->setVisible( mColoringMethodComboBox->currentData() == QgsMeshRendererVectorSettings::SingleColor ); + + if ( mColorRampShaderWidget->shader().colorRampItemList().isEmpty() ) + loadColorRampShader(); + + emit widgetChanged(); +} + +void QgsMeshRendererVectorSettingsWidget::onColorRampMinMaxChanged() +{ + mColorRampShaderWidget->setMinimumMaximumAndClassify( + filterValue( mColorRampShaderMinimumEditLine->text(), 0 ), + filterValue( mColorRampShaderMaximumEditLine->text(), 0 ) ); +} + +void QgsMeshRendererVectorSettingsWidget::loadColorRampShader() +{ + if ( !mMeshLayer ) + return; + + QgsMeshDataProvider *provider = mMeshLayer->dataProvider(); + int currentVectorDataSetGroupIndex = mMeshLayer->rendererSettings().activeVectorDatasetGroup(); + if ( !provider || + currentVectorDataSetGroupIndex < 0 || + !provider->datasetGroupMetadata( currentVectorDataSetGroupIndex ).isVector() ) + return; + + const QgsMeshDatasetGroupMetadata meta = provider->datasetGroupMetadata( currentVectorDataSetGroupIndex ); + double min = meta.minimum(); + double max = meta.maximum(); + + mColorRampShaderWidget->setMinimumMaximumAndClassify( min, max ); + whileBlocking( mColorRampShaderMinimumEditLine )->setText( QString::number( min ) ); + whileBlocking( mColorRampShaderMaximumEditLine )->setText( QString::number( max ) ); +} + double QgsMeshRendererVectorSettingsWidget::filterValue( const QString &text, double errVal ) const { if ( text.isEmpty() ) diff --git a/src/app/mesh/qgsmeshrenderervectorsettingswidget.h b/src/app/mesh/qgsmeshrenderervectorsettingswidget.h index f5433a25b88e..b60f52b8f9bb 100644 --- a/src/app/mesh/qgsmeshrenderervectorsettingswidget.h +++ b/src/app/mesh/qgsmeshrenderervectorsettingswidget.h @@ -63,6 +63,9 @@ class APP_EXPORT QgsMeshRendererVectorSettingsWidget : public QWidget, private U private slots: void onSymbologyChanged( int currentIndex ); void onStreamLineSeedingMethodChanged( int currentIndex ); + void onColoringMethodChanged(); + void onColorRampMinMaxChanged(); + void loadColorRampShader(); private: diff --git a/src/app/mesh/qgsmeshstaticdatasetwidget.cpp b/src/app/mesh/qgsmeshstaticdatasetwidget.cpp new file mode 100644 index 000000000000..f5318dbf4f67 --- /dev/null +++ b/src/app/mesh/qgsmeshstaticdatasetwidget.cpp @@ -0,0 +1,118 @@ +/*************************************************************************** + qgsmeshstaticdatasetwidget.cpp + ------------------------------------- + begin : March 2020 + copyright : (C) 2020 by Vincent Cloarec + email : vcloarec 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 "qgsmeshstaticdatasetwidget.h" + +#include "qgsmeshlayer.h" + +QgsMeshStaticDatasetWidget::QgsMeshStaticDatasetWidget( QWidget *parent ): QWidget( parent ) +{ + setupUi( this ); + + mDatasetScalarModel = new QgsMeshDatasetListModel( this ); + mScalarDatasetComboBox->setModel( mDatasetScalarModel ); + mDatasetVectorModel = new QgsMeshDatasetListModel( this ); + mVectorDatasetComboBox->setModel( mDatasetVectorModel ); +} + +void QgsMeshStaticDatasetWidget::setLayer( QgsMeshLayer *layer ) +{ + mLayer = layer; +} + +void QgsMeshStaticDatasetWidget::syncToLayer() +{ + if ( !mLayer ) + return; + + mScalarDatasetGroup = mLayer->rendererSettings().activeScalarDatasetGroup(); + mVectorDatasetGroup = mLayer->rendererSettings().activeVectorDatasetGroup(); + mDatasetScalarModel->setMeshLayer( mLayer ); + mDatasetScalarModel->setDatasetGroup( mScalarDatasetGroup ); + mDatasetVectorModel->setMeshLayer( mLayer ); + mDatasetVectorModel->setDatasetGroup( mVectorDatasetGroup ); + + mScalarDatasetComboBox->setCurrentIndex( mLayer->staticScalarDatasetIndex().dataset() + 1 ); + mVectorDatasetComboBox->setCurrentIndex( mLayer->staticVectorDatasetIndex().dataset() + 1 ); +} + +void QgsMeshStaticDatasetWidget::apply() +{ + if ( !mLayer ) + return; + + mLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex( mScalarDatasetGroup, mScalarDatasetComboBox->currentIndex() - 1 ) ); + mLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex( mVectorDatasetGroup, mVectorDatasetComboBox->currentIndex() - 1 ) ); +} + +void QgsMeshStaticDatasetWidget::setScalarDatasetGroup( int index ) +{ + mScalarDatasetGroup = index; + mDatasetScalarModel->setDatasetGroup( index ); + mScalarName->setText( mLayer->dataProvider()->datasetGroupMetadata( index ).name() ); +} + +void QgsMeshStaticDatasetWidget::setVectorDatasetGroup( int index ) +{ + mVectorDatasetGroup = index; + mDatasetVectorModel->setDatasetGroup( index ); + mVectorName->setText( mLayer->dataProvider()->datasetGroupMetadata( index ).name() ); +} + +QgsMeshDatasetListModel::QgsMeshDatasetListModel( QObject *parent ): QAbstractListModel( parent ) +{} + +void QgsMeshDatasetListModel::setMeshLayer( QgsMeshLayer *layer ) +{ + beginResetModel(); + mLayer = layer; + endResetModel(); +} + +void QgsMeshDatasetListModel::setDatasetGroup( int group ) +{ + beginResetModel(); + mDatasetGroup = group; + endResetModel(); +} + +int QgsMeshDatasetListModel::rowCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ) + + if ( mLayer && mLayer->dataProvider() ) + return mLayer->dataProvider()->datasetCount( mDatasetGroup ) + 1; + else + return 0; +} + +QVariant QgsMeshDatasetListModel::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() || !mLayer || !mLayer->dataProvider() || mDatasetGroup < 0 ) + return QVariant(); + + if ( role == Qt::DisplayRole ) + { + if ( index.row() == 0 ) + return tr( "none" ); + else + { + qint64 time = mLayer->dataProvider()->temporalCapabilities()->datasetTime( QgsMeshDatasetIndex( mDatasetGroup, index.row() - 1 ) ); + return mLayer->formatTime( time / 3600.0 / 1000.0 ); + } + } + + return QVariant(); +} diff --git a/src/app/mesh/qgsmeshstaticdatasetwidget.h b/src/app/mesh/qgsmeshstaticdatasetwidget.h new file mode 100644 index 000000000000..9cf70384c432 --- /dev/null +++ b/src/app/mesh/qgsmeshstaticdatasetwidget.h @@ -0,0 +1,86 @@ +/*************************************************************************** + qgsmeshstaticdatasetwidget.h + ------------------------------------- + begin : March 2020 + copyright : (C) 2020 by Vincent Cloarec + email : vcloarec 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 QGSMESHSTATICDATASETWIDGET_H +#define QGSMESHSTATICDATASETWIDGET_H + +#include + +#include "qgis_app.h" +#include "ui_qgsmeshstaticdatasetwidgetbase.h" +#include "qgsmeshdataset.h" + +class QgsMeshLayer; +class QgsMeshDataProvider; + + +/** + * List mdel for dataset contained in dataset group, + * used to display by time dataset in widget + */ +class APP_NO_EXPORT QgsMeshDatasetListModel: public QAbstractListModel +{ + public: + //! Constructor + QgsMeshDatasetListModel( QObject *parent ); + + //! Sets the layer + void setMeshLayer( QgsMeshLayer *layer ); + //! Sets the dataset group + void setDatasetGroup( int group ); + + int rowCount( const QModelIndex &parent ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + + private: + QgsMeshLayer *mLayer = nullptr; + int mDatasetGroup = -1; +}; + +/** + * A widget for setup of the static dataset of a mesh layer. + */ +class APP_EXPORT QgsMeshStaticDatasetWidget : public QWidget, private Ui::QgsMeshStaticDatasetWidget +{ + Q_OBJECT + public: + //! Constructor + QgsMeshStaticDatasetWidget( QWidget *parent = nullptr ); + + //! Sets the layer + void setLayer( QgsMeshLayer *layer ); + + //! Synchronizes widgets state with associated mesh layer + void syncToLayer(); + + //! Applies the settings made in the widget + void apply(); + + public slots: + //! Sets the scalar dataset group + void setScalarDatasetGroup( int index ); + //! Sets the vector dataset group + void setVectorDatasetGroup( int index ); + + private: + int mScalarDatasetGroup = -1; + int mVectorDatasetGroup = -1; + + QgsMeshDatasetListModel *mDatasetScalarModel = nullptr; + QgsMeshDatasetListModel *mDatasetVectorModel = nullptr; + + QgsMeshLayer *mLayer; +}; + +#endif // QGSMESHSTATICDATASETWIDGET_H diff --git a/src/app/mesh/qgsmeshtimeformatdialog.cpp b/src/app/mesh/qgsmeshtimeformatdialog.cpp deleted file mode 100644 index ee51f58edb40..000000000000 --- a/src/app/mesh/qgsmeshtimeformatdialog.cpp +++ /dev/null @@ -1,123 +0,0 @@ -/*************************************************************************** - qgsmeshtimeformatdialog.cpp - --------------------------- - begin : March 2019 - copyright : (C) 2019 by Peter Petrik - email : zilolv 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 "qgsmeshtimeformatdialog.h" -#include "qgsgui.h" -#include "qgsmeshtimesettings.h" -#include "qgsmeshlayerutils.h" - -QgsMeshTimeFormatDialog::QgsMeshTimeFormatDialog( QgsMeshLayer *meshLayer, QWidget *parent, Qt::WindowFlags f ) - : QDialog( parent, f ), - mLayer( meshLayer ) -{ - setupUi( this ); - QgsGui::enableAutoGeometryRestore( this ); - - if ( !meshLayer ) - return; - - loadSettings(); - - mReloadReferenceTimeButton->setEnabled( layerHasReferenceTime() ); - - connect( mUseTimeComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mReferenceDateTimeEdit, &QDateTimeEdit::dateTimeChanged, this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mAbsoluteTimeFormatComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mUseTimeComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mRelativeTimeFormatComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mOffsetHoursSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mPlaybackIntervalSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - connect( mProviderTimeUnitComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsMeshTimeFormatDialog::saveSettings ); - - connect( mReloadReferenceTimeButton, &QPushButton::clicked, this, &QgsMeshTimeFormatDialog::loadProviderReferenceTime ); - -} - -void QgsMeshTimeFormatDialog::loadProviderReferenceTime() -{ - mReferenceDateTimeEdit->setDateTime( QgsMeshLayerUtils::firstReferenceTime( mLayer ) ); -} - -void QgsMeshTimeFormatDialog::loadSettings() -{ - const QgsMeshTimeSettings settings = mLayer->timeSettings(); - - enableGroups( settings.useAbsoluteTime() ) ; - if ( settings.useAbsoluteTime() ) - { - mUseTimeComboBox->setCurrentIndex( 0 ); - } - else - { - mUseTimeComboBox->setCurrentIndex( 1 ); - } - - // Sets the reference time, if not valid, sets the current date + time 00:00:00 - if ( settings.absoluteTimeReferenceTime().isValid() ) - mReferenceDateTimeEdit->setDateTime( settings.absoluteTimeReferenceTime() ); - else - mReferenceDateTimeEdit->setDateTime( QDateTime( QDate::currentDate(), QTime( 00, 00, 00 ) ) ); - - mReferenceDateTimeEdit->setDisplayFormat( settings.absoluteTimeFormat() ); - - int index = mAbsoluteTimeFormatComboBox->findText( settings.absoluteTimeFormat() ); - if ( index < 0 ) - { - index = mAbsoluteTimeFormatComboBox->count(); - mAbsoluteTimeFormatComboBox->addItem( settings.absoluteTimeFormat() ); - } - mAbsoluteTimeFormatComboBox->setCurrentIndex( index ); - - index = mRelativeTimeFormatComboBox->findText( settings.relativeTimeFormat() ); - if ( index < 0 ) - { - index = mRelativeTimeFormatComboBox->count(); - mRelativeTimeFormatComboBox->addItem( settings.relativeTimeFormat() ); - } - mRelativeTimeFormatComboBox->setCurrentIndex( index ); - - mOffsetHoursSpinBox->setValue( settings.relativeTimeOffsetHours() ); - mPlaybackIntervalSpinBox->setValue( settings.datasetPlaybackInterval() ); - - mProviderTimeUnitComboBox->setCurrentIndex( settings.providerTimeUnit() ); -} - -void QgsMeshTimeFormatDialog::saveSettings() -{ - QgsMeshTimeSettings settings; - settings.setUseAbsoluteTime( mUseTimeComboBox->currentIndex() == 0 ); - settings.setAbsoluteTimeReferenceTime( mReferenceDateTimeEdit->dateTime() ); - settings.setAbsoluteTimeFormat( mAbsoluteTimeFormatComboBox->currentText() ); - settings.setRelativeTimeOffsetHours( mOffsetHoursSpinBox->value() ); - settings.setRelativeTimeFormat( mRelativeTimeFormatComboBox->currentText() ); - settings.setDatasetPlaybackInterval( mPlaybackIntervalSpinBox->value() ); - settings.setProviderTimeUnit( static_cast( mProviderTimeUnitComboBox->currentIndex() ) ); - enableGroups( settings.useAbsoluteTime() ) ; - mLayer->setTimeSettings( settings ); -} - -void QgsMeshTimeFormatDialog::enableGroups( bool useAbsoluteTime ) -{ - mAbsoluteTimeGroupBox->setEnabled( useAbsoluteTime ); - mRelativeTimeGroupBox->setEnabled( ! useAbsoluteTime ); -} - -bool QgsMeshTimeFormatDialog::layerHasReferenceTime() const -{ - return QgsMeshLayerUtils::firstReferenceTime( mLayer ).isValid(); -} - -QgsMeshTimeFormatDialog::~QgsMeshTimeFormatDialog() = default; diff --git a/src/app/mesh/qgsmeshtimeformatdialog.h b/src/app/mesh/qgsmeshtimeformatdialog.h deleted file mode 100644 index 91ec6002e823..000000000000 --- a/src/app/mesh/qgsmeshtimeformatdialog.h +++ /dev/null @@ -1,53 +0,0 @@ -/*************************************************************************** - qgsmeshtimeformatdialog.h - ------------------------- - begin : March 2019 - copyright : (C) 2019 by Peter Petrik - email : zilolv 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 QGSMESHTIMEFORMATDIALOG_H -#define QGSMESHTIMEFORMATDIALOG_H - -#include "ui_qgsmeshtimeformatdialog.h" -#include "qgsmeshcalculator.h" -#include "qgshelp.h" -#include "qgis_app.h" - -//! A dialog to enter a mesh calculation expression -class APP_EXPORT QgsMeshTimeFormatDialog: public QDialog, private Ui::QgsMeshTimeFormatDialog -{ - Q_OBJECT - public: - - /** - * Constructor for raster calculator dialog - * \param meshLayer main mesh layer, will be used for default extent and projection - * \param parent widget - * \param f window flags - */ - QgsMeshTimeFormatDialog( QgsMeshLayer *meshLayer = nullptr, QWidget *parent = nullptr, Qt::WindowFlags f = nullptr ); - ~QgsMeshTimeFormatDialog(); - - private slots: - void loadProviderReferenceTime(); - private: - void loadSettings(); - void saveSettings(); - void enableGroups( bool useAbsoluteTime ); - - bool layerHasReferenceTime() const; - - QgsMeshLayer *mLayer; -}; - -#endif // QGSMESHTIMEFORMATDIALOG_H diff --git a/src/app/mesh/qgsrenderermeshpropertieswidget.cpp b/src/app/mesh/qgsrenderermeshpropertieswidget.cpp index 7c5691c674c0..3c7b9d46502d 100644 --- a/src/app/mesh/qgsrenderermeshpropertieswidget.cpp +++ b/src/app/mesh/qgsrenderermeshpropertieswidget.cpp @@ -94,27 +94,27 @@ void QgsRendererMeshPropertiesWidget::apply() triangularMeshSettings.setEnabled( triangularMeshRenderingIsEnabled ); // SCALAR - QgsMeshDatasetIndex activeScalarDatasetIndex = mMeshRendererActiveDatasetWidget->activeScalarDataset(); + int activeScalarDatasetGroupIndex = mMeshRendererActiveDatasetWidget->activeScalarDatasetGroup(); if ( !mContoursGroupBox->isChecked() ) - activeScalarDatasetIndex = QgsMeshDatasetIndex(); + activeScalarDatasetGroupIndex = -1; // VECTOR - QgsMeshDatasetIndex activeVectorDatasetIndex = mMeshRendererActiveDatasetWidget->activeVectorDataset(); + int activeVectorDatasetGroupIndex = mMeshRendererActiveDatasetWidget->activeVectorDatasetGroup(); if ( !mVectorsGroupBox->isChecked() ) - activeVectorDatasetIndex = QgsMeshDatasetIndex(); + activeVectorDatasetGroupIndex = -1; QgsMeshRendererSettings settings = mMeshLayer->rendererSettings(); settings.setEdgeMeshSettings( edgeMeshSettings ); settings.setNativeMeshSettings( nativeMeshSettings ); settings.setTriangularMeshSettings( triangularMeshSettings ); - settings.setActiveScalarDataset( activeScalarDatasetIndex ); - if ( activeScalarDatasetIndex.isValid() ) - settings.setScalarSettings( activeScalarDatasetIndex.group(), mMeshRendererScalarSettingsWidget->settings() ); + settings.setActiveScalarDatasetGroup( activeScalarDatasetGroupIndex ); + if ( activeScalarDatasetGroupIndex > -1 ) + settings.setScalarSettings( activeScalarDatasetGroupIndex, mMeshRendererScalarSettingsWidget->settings() ); - settings.setActiveVectorDataset( activeVectorDatasetIndex ); - if ( activeVectorDatasetIndex.isValid() ) - settings.setVectorSettings( activeVectorDatasetIndex.group(), mMeshRendererVectorSettingsWidget->settings() ); + settings.setActiveVectorDatasetGroup( activeVectorDatasetGroupIndex ); + if ( activeVectorDatasetGroupIndex > -1 ) + settings.setVectorSettings( activeVectorDatasetGroupIndex, mMeshRendererVectorSettingsWidget->settings() ); //set the blend mode for the layer mMeshLayer->setBlendMode( mBlendModeComboBox->blendMode() ); @@ -123,6 +123,9 @@ void QgsRendererMeshPropertiesWidget::apply() settings.setAveragingMethod( averagingMethod.get() ); mMeshLayer->setRendererSettings( settings ); mMeshLayer->triggerRepaint(); + + QgsSettings windowsSettings; + windowsSettings.setValue( QStringLiteral( "/Windows/RendererMeshProperties/tab" ), mStyleOptionsTab->currentIndex() ); } void QgsRendererMeshPropertiesWidget::syncToLayer() @@ -137,8 +140,8 @@ void QgsRendererMeshPropertiesWidget::syncToLayer() mTriangularMeshGroup->setChecked( mMeshLayer ? mMeshLayer->rendererSettings().triangularMeshSettings().isEnabled() : false ); mEdgeMeshGroup->setChecked( mMeshLayer ? mMeshLayer->rendererSettings().edgeMeshSettings().isEnabled() : false ); - onActiveScalarGroupChanged( mMeshRendererActiveDatasetWidget->activeScalarDatasetGroup() ); - onActiveVectorGroupChanged( mMeshRendererActiveDatasetWidget->activeVectorDatasetGroup() ); + onActiveScalarGroupChanged( mMeshLayer->rendererSettings().activeScalarDatasetGroup() ); + onActiveVectorGroupChanged( mMeshLayer->rendererSettings().activeVectorDatasetGroup() ); bool hasFaces = ( mMeshLayer->dataProvider() && mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Face ) ); @@ -147,6 +150,12 @@ void QgsRendererMeshPropertiesWidget::syncToLayer() bool hasEdges = ( mMeshLayer->dataProvider() && mMeshLayer->dataProvider()->contains( QgsMesh::ElementType::Edge ) ); mEdgeMeshGroupBox->setVisible( hasEdges ); + + QgsSettings settings; + if ( !settings.contains( QStringLiteral( "/Windows/RendererMeshProperties/tab" ) ) ) + settings.setValue( QStringLiteral( "/Windows/RendererMeshProperties/tab" ), 0 ); + else + mStyleOptionsTab->setCurrentIndex( settings.value( QStringLiteral( "/Windows/RendererMeshProperties/tab" ) ).toInt() ); } void QgsRendererMeshPropertiesWidget::onActiveScalarGroupChanged( int groupIndex ) @@ -155,6 +164,7 @@ void QgsRendererMeshPropertiesWidget::onActiveScalarGroupChanged( int groupIndex mMeshRendererScalarSettingsWidget->syncToLayer(); mContoursGroupBox->setChecked( groupIndex >= 0 ); mContoursGroupBox->setEnabled( groupIndex >= 0 ); + emit mMeshLayer->activeScalarDatasetGroupChanged( groupIndex ); } void QgsRendererMeshPropertiesWidget::onActiveVectorGroupChanged( int groupIndex ) @@ -165,4 +175,5 @@ void QgsRendererMeshPropertiesWidget::onActiveVectorGroupChanged( int groupIndex mMeshRendererVectorSettingsWidget->syncToLayer(); mVectorsGroupBox->setChecked( groupIndex >= 0 ); mVectorsGroupBox->setEnabled( groupIndex >= 0 ); + emit mMeshLayer->activeVectorDatasetGroupChanged( groupIndex ); } diff --git a/src/app/pluginmanager/qgspluginmanager.cpp b/src/app/pluginmanager/qgspluginmanager.cpp index d65e73467eaa..510b797caafb 100644 --- a/src/app/pluginmanager/qgspluginmanager.cpp +++ b/src/app/pluginmanager/qgspluginmanager.cpp @@ -31,6 +31,8 @@ #include #include +#include "qgsmessagelog.h" + #include "qgis.h" #include "qgisapp.h" #include "qgsapplication.h" @@ -73,6 +75,7 @@ QgsPluginManager::QgsPluginManager( QWidget *parent, bool pluginsAreEnabled, Qt: connect( leFilter, &QgsFilterLineEdit::textChanged, this, &QgsPluginManager::leFilter_textChanged ); connect( buttonUpgradeAll, &QPushButton::clicked, this, &QgsPluginManager::buttonUpgradeAll_clicked ); connect( buttonInstall, &QPushButton::clicked, this, &QgsPluginManager::buttonInstall_clicked ); + connect( buttonInstallExperimental, &QPushButton::clicked, this, &QgsPluginManager::buttonInstallExperimental_clicked ); connect( buttonUninstall, &QPushButton::clicked, this, &QgsPluginManager::buttonUninstall_clicked ); connect( treeRepositories, &QTreeWidget::itemSelectionChanged, this, &QgsPluginManager::treeRepositories_itemSelectionChanged ); connect( treeRepositories, &QTreeWidget::doubleClicked, this, &QgsPluginManager::treeRepositories_doubleClicked ); @@ -138,6 +141,7 @@ QgsPluginManager::QgsPluginManager( QWidget *parent, bool pluginsAreEnabled, Qt: // Hide widgets only suitable with Python support enabled (they will be uncovered back in setPythonUtils) buttonUpgradeAll->hide(); buttonInstall->hide(); + buttonInstallExperimental->hide(); buttonUninstall->hide(); frameSettings->setHidden( true ); mOptionsListWidget->item( PLUGMAN_TAB_INSTALL_FROM_ZIP )->setHidden( true ); @@ -179,6 +183,7 @@ void QgsPluginManager::setPythonUtils( QgsPythonUtils *pythonUtils ) mOptionsListWidget->item( PLUGMAN_TAB_INSTALL_FROM_ZIP )->setHidden( false ); buttonUpgradeAll->show(); buttonInstall->show(); + buttonInstallExperimental->show(); buttonUninstall->show(); frameSettings->setHidden( false ); labelNoPython->setHidden( true ); @@ -191,25 +196,36 @@ void QgsPluginManager::setPythonUtils( QgsPythonUtils *pythonUtils ) QAction *actionSortByDownloads = new QAction( tr( "Sort by Downloads" ), vwPlugins ); QAction *actionSortByVote = new QAction( tr( "Sort by Vote" ), vwPlugins ); QAction *actionSortByStatus = new QAction( tr( "Sort by Status" ), vwPlugins ); + QAction *actionSortByDateCreated = new QAction( tr( "Sort by Date Created" ), vwPlugins ); + QAction *actionSortByDateUpdated = new QAction( tr( "Sort by Date Updated" ), vwPlugins ); actionSortByName->setCheckable( true ); actionSortByDownloads->setCheckable( true ); actionSortByVote->setCheckable( true ); actionSortByStatus->setCheckable( true ); + actionSortByDateCreated->setCheckable( true ); + actionSortByDateUpdated->setCheckable( true ); QActionGroup *group = new QActionGroup( vwPlugins ); actionSortByName->setActionGroup( group ); actionSortByDownloads->setActionGroup( group ); actionSortByVote->setActionGroup( group ); actionSortByStatus->setActionGroup( group ); + actionSortByDateCreated->setActionGroup( group ); + actionSortByDateUpdated->setActionGroup( group ); actionSortByName->setChecked( true ); vwPlugins->addAction( actionSortByName ); vwPlugins->addAction( actionSortByDownloads ); vwPlugins->addAction( actionSortByVote ); vwPlugins->addAction( actionSortByStatus ); + // TODO "create_date" and "update_date" are actually both representing "update_date" from the server side. Blocked by https://github.com/qgis/QGIS-Django/issues/69 + // vwPlugins->addAction( actionSortByDateCreated ); + vwPlugins->addAction( actionSortByDateUpdated ); vwPlugins->setContextMenuPolicy( Qt::ActionsContextMenu ); connect( actionSortByName, &QAction::triggered, mModelProxy, &QgsPluginSortFilterProxyModel::sortPluginsByName ); connect( actionSortByDownloads, &QAction::triggered, mModelProxy, &QgsPluginSortFilterProxyModel::sortPluginsByDownloads ); connect( actionSortByVote, &QAction::triggered, mModelProxy, &QgsPluginSortFilterProxyModel::sortPluginsByVote ); connect( actionSortByStatus, &QAction::triggered, mModelProxy, &QgsPluginSortFilterProxyModel::sortPluginsByStatus ); + connect( actionSortByDateCreated, &QAction::triggered, mModelProxy, &QgsPluginSortFilterProxyModel::sortPluginsByDateCreated ); + connect( actionSortByDateUpdated, &QAction::triggered, mModelProxy, &QgsPluginSortFilterProxyModel::sortPluginsByDateUpdated ); // get the QgsSettings group from the installer QString settingsGroup; @@ -417,6 +433,8 @@ void QgsPluginManager::getCppPluginsMetadata() version_t *pVersion = ( version_t * ) cast_to_fptr( myLib->resolve( "version" ) ); icon_t *pIcon = ( icon_t * ) cast_to_fptr( myLib->resolve( "icon" ) ); experimental_t *pExperimental = ( experimental_t * ) cast_to_fptr( myLib->resolve( "experimental" ) ); + create_date_t *pCreateDate = ( create_date_t * ) cast_to_fptr( myLib->resolve( "create_date" ) ); + update_date_t *pUpdateDate = ( update_date_t * ) cast_to_fptr( myLib->resolve( "update_date" ) ); // show the values (or lack of) for each function if ( pName ) @@ -455,6 +473,26 @@ void QgsPluginManager::getCppPluginsMetadata() { QgsDebugMsg( "Plugin icon: " + pIcon() ); } + else + { + QgsDebugMsg( QStringLiteral( "Plugin icon not returned when queried" ) ); + } + if ( pCreateDate ) + { + QgsDebugMsg( "Plugin create date: " + pCreateDate() ); + } + else + { + QgsDebugMsg( QStringLiteral( "Plugin create date not returned when queried" ) ); + } + if ( pUpdateDate ) + { + QgsDebugMsg( "Plugin update date: " + pUpdateDate() ); + } + else + { + QgsDebugMsg( QStringLiteral( "Plugin update date not returned when queried" ) ); + } if ( !pName || !pDesc || !pVersion ) { @@ -479,6 +517,8 @@ void QgsPluginManager::getCppPluginsMetadata() metadata[QStringLiteral( "readonly" )] = QStringLiteral( "true" ); metadata[QStringLiteral( "status" )] = QStringLiteral( "orphan" ); metadata[QStringLiteral( "experimental" )] = ( pExperimental ? pExperimental() : QString() ); + metadata[QStringLiteral( "create_date" )] = ( pCreateDate ? pCreateDate() : QString() ); + metadata[QStringLiteral( "update_date" )] = ( pUpdateDate ? pUpdateDate() : QString() ); mPlugins.insert( baseName, metadata ); delete myLib; @@ -525,17 +565,23 @@ void QgsPluginManager::reloadModelData() QString pluginName = it->value( QStringLiteral( "name" ) ); QString description = it->value( QStringLiteral( "description" ) ); QString author = it->value( QStringLiteral( "author_name" ) ); + QString createDate = it->value( QStringLiteral( "create_date" ) ); + QString updateDate = it->value( QStringLiteral( "update_date" ) ); QString iconPath = it->value( QStringLiteral( "icon" ) ); QString status = it->value( QStringLiteral( "status" ) ); + QString status_exp = it->value( QStringLiteral( "status_exp" ) ); QString error = it->value( QStringLiteral( "error" ) ); QStandardItem *mypDetailItem = new QStandardItem( pluginName ); mypDetailItem->setData( baseName, PLUGIN_BASE_NAME_ROLE ); mypDetailItem->setData( status, PLUGIN_STATUS_ROLE ); + mypDetailItem->setData( status_exp, PLUGIN_STATUSEXP_ROLE ); mypDetailItem->setData( error, PLUGIN_ERROR_ROLE ); mypDetailItem->setData( description, PLUGIN_DESCRIPTION_ROLE ); mypDetailItem->setData( author, PLUGIN_AUTHOR_ROLE ); + mypDetailItem->setData( createDate, PLUGIN_CREATE_DATE ); + mypDetailItem->setData( updateDate, PLUGIN_UPDATE_DATE ); mypDetailItem->setData( it->value( QStringLiteral( "tags" ) ), PLUGIN_TAGS_ROLE ); mypDetailItem->setData( it->value( QStringLiteral( "downloads" ) ).rightJustified( 10, '0' ), PLUGIN_DOWNLOADS_ROLE ); mypDetailItem->setData( it->value( QStringLiteral( "average_vote" ) ), PLUGIN_VOTE_ROLE ); @@ -787,21 +833,21 @@ void QgsPluginManager::showPluginDetails( QStandardItem *item ) "
    %1%2
    %1%2 (%3)
    " ).arg( errorMsg ); } - if ( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "upgradeable" ) ) + if ( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "upgradeable" ) || metadata->value( QStringLiteral( "status_exp" ) ) == QLatin1String( "upgradeable" ) ) { html += QString( "" " " "
    %1
    " ).arg( tr( "There is a new version available" ) ); } - if ( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "new" ) ) + if ( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "new" ) || metadata->value( QStringLiteral( "status_exp" ) ) == QLatin1String( "new" ) ) { html += QString( "" " " "
    %1
    " ).arg( tr( "This is a new plugin" ) ); } - if ( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "newer" ) ) + if ( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "newer" ) && metadata->value( QStringLiteral( "status_exp" ) ) == QLatin1String( "newer" ) ) { html += QString( "" " " @@ -814,7 +860,7 @@ void QgsPluginManager::showPluginDetails( QStandardItem *item ) " " - "
    %1
    " " %1" "
    " ).arg( tr( "This plugin is experimental" ) ); + "" ).arg( tr( "This plugin has an experimental version available" ) ); } if ( metadata->value( QStringLiteral( "deprecated" ) ) == QLatin1String( "true" ) ) @@ -968,19 +1014,54 @@ void QgsPluginManager::showPluginDetails( QStandardItem *item ) QUrl::fromLocalFile( localDir ).toString(), ver ); } - if ( ! metadata->value( QStringLiteral( "version_available" ) ).isEmpty() ) + + // if we allow experimental, we show both stable and experimental versions + if ( ! metadata->value( QStringLiteral( "version_available_stable" ) ).isEmpty() ) + { + QString downloadUrl = metadata->value( QStringLiteral( "download_url_stable" ) ); + if ( downloadUrl.contains( QStringLiteral( "plugins.qgis.org" ) ) ) + { + // For the main repo, open the plugin version page instead of the download link. For other repositories the download link is the only known endpoint. + downloadUrl = downloadUrl.replace( QStringLiteral( "download/" ), QString() ); + } + + QString dateUpdatedStr; + if ( ! metadata->value( QStringLiteral( "update_date" ) ).isEmpty() ) + { + const QDateTime dateUpdated = QDateTime::fromString( metadata->value( QStringLiteral( "update_date_stable" ) ).trimmed(), Qt::ISODate ); + if ( dateUpdated.isValid() ) + dateUpdatedStr += QStringLiteral( "%1 %2" ).arg( tr( "updated at" ), dateUpdated.toString() ); + } + + html += QStringLiteral( "%1 %3 %4" + ).arg( tr( "Available version (stable)" ), + downloadUrl, + metadata->value( QStringLiteral( "version_available_stable" ) ), + dateUpdatedStr ); + } + + if ( ! metadata->value( QStringLiteral( "version_available_experimental" ) ).isEmpty() ) { - QString downloadUrl = metadata->value( QStringLiteral( "download_url" ) ); + QString downloadUrl = metadata->value( QStringLiteral( "download_url_experimental" ) ); if ( downloadUrl.contains( QStringLiteral( "plugins.qgis.org" ) ) ) { // For the main repo, open the plugin version page instead of the download link. For other repositories the download link is the only known endpoint. downloadUrl = downloadUrl.replace( QStringLiteral( "download/" ), QString() ); } - html += QStringLiteral( "%1 %3" - ).arg( tr( "Available version" ), + QString dateUpdatedStr; + if ( !metadata->value( QStringLiteral( "update_date_experimental" ) ).isEmpty() ) + { + const QDateTime dateUpdated = QDateTime::fromString( metadata->value( QStringLiteral( "update_date_experimental" ) ).trimmed(), Qt::ISODate ); + if ( dateUpdated.isValid() ) + dateUpdatedStr += QStringLiteral( "%1 %2" ).arg( tr( "updated at" ), dateUpdated.toString() ); + } + + html += QStringLiteral( "%1 %3 %4" + ).arg( tr( "Available version (experimental)" ), downloadUrl, - metadata->value( QStringLiteral( "version_available" ) ) ); + metadata->value( QStringLiteral( "version_available_experimental" ) ), + dateUpdatedStr ); } if ( ! metadata->value( QStringLiteral( "changelog" ) ).isEmpty() ) @@ -1024,10 +1105,44 @@ void QgsPluginManager::showPluginDetails( QStandardItem *item ) buttonInstall->setText( tr( "Reinstall Plugin" ) ); } + // Set buttonInstall text (and sometimes focus) + buttonInstallExperimental->setDefault( false ); + if ( metadata->value( QStringLiteral( "status_exp" ) ) == QLatin1String( "upgradeable" ) ) + { + buttonInstallExperimental->setText( tr( "Upgrade Experimental Plugin" ) ); + } + else if ( metadata->value( QStringLiteral( "status_exp" ) ) == QLatin1String( "newer" ) ) + { + buttonInstallExperimental->setText( tr( "Downgrade Experimental Plugin" ) ); + } + else if ( metadata->value( QStringLiteral( "status_exp" ) ) == QLatin1String( "not installed" ) || metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "new" ) ) + { + buttonInstallExperimental->setText( tr( "Install Experimental Plugin" ) ); + } + else + { + // Default (will be grayed out if not available for reinstallation) + buttonInstallExperimental->setText( tr( "Reinstall Experimental Plugin" ) ); + } + + // DEBUG TODO REMOVE + // buttonInstall->setText( buttonInstall->text() + QStringLiteral(" | ") + metadata->value( QStringLiteral( "status" ) ) ); + // buttonInstallExperimental->setText( buttonInstallExperimental->text() + QStringLiteral(" | ") + metadata->value( QStringLiteral( "status_exp" ) ) ); + // Enable/disable buttons - buttonInstall->setEnabled( metadata->value( QStringLiteral( "pythonic" ) ).toUpper() == QLatin1String( "TRUE" ) && metadata->value( QStringLiteral( "status" ) ) != QLatin1String( "orphan" ) ); - buttonUninstall->setEnabled( metadata->value( QStringLiteral( "pythonic" ) ).toUpper() == QLatin1String( "TRUE" ) && metadata->value( QStringLiteral( "readonly" ) ) != QLatin1String( "true" ) && metadata->value( QStringLiteral( "status" ) ) != QLatin1String( "not installed" ) && metadata->value( QStringLiteral( "status" ) ) != QLatin1String( "new" ) ); - buttonUninstall->setHidden( metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "not installed" ) || metadata->value( QStringLiteral( "status" ) ) == QLatin1String( "new" ) ); + + bool installEnabled = metadata->value( QStringLiteral( "pythonic" ) ).toUpper() == QLatin1String( "TRUE" ) && metadata->value( QStringLiteral( "status" ) ) != QLatin1String( "orphan" ) && metadata->value( QStringLiteral( "status" ) ) != QLatin1String( "none available" ); + bool installExpEnabled = metadata->value( QStringLiteral( "pythonic" ) ).toUpper() == QLatin1String( "TRUE" ) && metadata->value( QStringLiteral( "status_exp" ) ) != QLatin1String( "orphan" ) && metadata->value( QStringLiteral( "status_exp" ) ) != QLatin1String( "none available" ); + buttonInstall->setEnabled( installEnabled ); + buttonInstall->setVisible( installEnabled || !installExpEnabled ); + buttonInstallExperimental->setEnabled( installExpEnabled ); + buttonInstallExperimental->setVisible( installExpEnabled || !installEnabled ); + + buttonUninstall->setEnabled( metadata->value( QStringLiteral( "pythonic" ) ).toUpper() == QLatin1String( "TRUE" ) && metadata->value( QStringLiteral( "readonly" ) ) != QLatin1String( "true" ) && ! metadata->value( QStringLiteral( "version_installed" ) ).isEmpty() ); + + buttonUninstall->setHidden( + metadata->value( QStringLiteral( "version_installed" ) ).isEmpty() + ); // Store the id of the currently displayed plugin mCurrentlyDisplayedPlugin = metadata->value( QStringLiteral( "id" ) ); @@ -1199,7 +1314,7 @@ void QgsPluginManager::setCurrentTab( int idx ) { case PLUGMAN_TAB_ALL: // all (statuses ends with Z are for spacers to always sort properly) - acceptedStatuses << QStringLiteral( "installed" ) << QStringLiteral( "not installed" ) << QStringLiteral( "new" ) << QStringLiteral( "orphan" ) << QStringLiteral( "newer" ) << QStringLiteral( "upgradeable" ) << QStringLiteral( "not installedZ" ) << QStringLiteral( "installedZ" ) << QStringLiteral( "upgradeableZ" ) << QStringLiteral( "orphanZ" ) << QStringLiteral( "newerZZ" ) << QString(); + acceptedStatuses << QStringLiteral( "installed" ) << QStringLiteral( "not installed" ) << QStringLiteral( "new" ) << QStringLiteral( "orphan" ) << QStringLiteral( "none available" ) << QStringLiteral( "newer" ) << QStringLiteral( "upgradeable" ) << QStringLiteral( "not installedZ" ) << QStringLiteral( "installedZ" ) << QStringLiteral( "upgradeableZ" ) << QStringLiteral( "orphanZ" ) << QStringLiteral( "newerZZ" ) << QString(); tabTitle = QStringLiteral( "all_plugins" ); break; case PLUGMAN_TAB_INSTALLED: @@ -1372,7 +1487,13 @@ void QgsPluginManager::buttonUpgradeAll_clicked() void QgsPluginManager::buttonInstall_clicked() { - QgsPythonRunner::run( QStringLiteral( "pyplugin_installer.instance().installPlugin('%1')" ).arg( mCurrentlyDisplayedPlugin ) ); + QgsPythonRunner::run( QStringLiteral( "pyplugin_installer.instance().installPlugin('%1', stable=True)" ).arg( mCurrentlyDisplayedPlugin ) ); +} + + +void QgsPluginManager::buttonInstallExperimental_clicked() +{ + QgsPythonRunner::run( QStringLiteral( "pyplugin_installer.instance().installPlugin('%1', stable=False)" ).arg( mCurrentlyDisplayedPlugin ) ); } diff --git a/src/app/pluginmanager/qgspluginmanager.h b/src/app/pluginmanager/qgspluginmanager.h index 24868275a930..d6d7f4ec3781 100644 --- a/src/app/pluginmanager/qgspluginmanager.h +++ b/src/app/pluginmanager/qgspluginmanager.h @@ -127,6 +127,9 @@ class QgsPluginManager : public QgsOptionsDialogBase, private Ui::QgsPluginManag //! Install selected plugin void buttonInstall_clicked(); + //! Install selected plugin + void buttonInstallExperimental_clicked(); + //! Uninstall selected plugin void buttonUninstall_clicked(); diff --git a/src/app/pluginmanager/qgspluginsortfilterproxymodel.cpp b/src/app/pluginmanager/qgspluginsortfilterproxymodel.cpp index 491f5d6617a0..0a4f48bad2fe 100644 --- a/src/app/pluginmanager/qgspluginsortfilterproxymodel.cpp +++ b/src/app/pluginmanager/qgspluginsortfilterproxymodel.cpp @@ -66,10 +66,11 @@ bool QgsPluginSortFilterProxyModel::filterByStatus( QModelIndex &index ) const } QString status = sourceModel()->data( index, PLUGIN_STATUS_ROLE ).toString(); + QString statusexp = sourceModel()->data( index, PLUGIN_STATUSEXP_ROLE ).toString(); if ( status.endsWith( 'Z' ) ) status.chop( 1 ); if ( ! mAcceptedStatuses.isEmpty() && ! mAcceptedStatuses.contains( QStringLiteral( "invalid" ) ) - && ! mAcceptedStatuses.contains( status ) ) + && !( mAcceptedStatuses.contains( status ) || mAcceptedStatuses.contains( statusexp ) ) ) { // Don't accept if the status doesn't match return false; @@ -154,6 +155,22 @@ void QgsPluginSortFilterProxyModel::sortPluginsByStatus() +void QgsPluginSortFilterProxyModel::sortPluginsByDateCreated() +{ + setAcceptedSpacers(); + sort( 0, Qt::DescendingOrder ); + setSortRole( PLUGIN_CREATE_DATE ); +} + + +void QgsPluginSortFilterProxyModel::sortPluginsByDateUpdated() +{ + setAcceptedSpacers(); + sort( 0, Qt::DescendingOrder ); + setSortRole( PLUGIN_UPDATE_DATE ); +} + + bool QgsPluginSortFilterProxyModel::lessThan( const QModelIndex &source_left, const QModelIndex &source_right ) const { // Always move deprecated plugins to bottom, regardless of the sort order. diff --git a/src/app/pluginmanager/qgspluginsortfilterproxymodel.h b/src/app/pluginmanager/qgspluginsortfilterproxymodel.h index cb25c4d35d4a..791b25b2c047 100644 --- a/src/app/pluginmanager/qgspluginsortfilterproxymodel.h +++ b/src/app/pluginmanager/qgspluginsortfilterproxymodel.h @@ -30,7 +30,10 @@ const int PLUGIN_STATUS_ROLE = Qt::UserRole + 6; // for filtering and sort const int PLUGIN_DOWNLOADS_ROLE = Qt::UserRole + 7; // for sorting const int PLUGIN_VOTE_ROLE = Qt::UserRole + 8; // for sorting const int PLUGIN_ISDEPRECATED_ROLE = Qt::UserRole + 9; // for styling -const int SPACER_ROLE = Qt::UserRole + 20; // for sorting +const int PLUGIN_STATUSEXP_ROLE = Qt::UserRole + 10; // for filtering and sorting +const int PLUGIN_CREATE_DATE = Qt::UserRole + 11; // for sorting +const int PLUGIN_UPDATE_DATE = Qt::UserRole + 12; // for sorting +const int SPACER_ROLE = Qt::UserRole + 20; // for sorting @@ -58,6 +61,8 @@ class QgsPluginSortFilterProxyModel : public QSortFilterProxyModel void sortPluginsByDownloads(); void sortPluginsByVote(); void sortPluginsByStatus(); + void sortPluginsByDateCreated(); + void sortPluginsByDateUpdated(); protected: //! Filter by status: this method is used in both filterAcceptsRow and countWithCurrentStatus. diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 7adc82bce26c..50396102d7ad 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -78,6 +78,7 @@ #include "qgsrelationmanager.h" #include "qgsapplication.h" #include "qgslayerstylingwidget.h" +#include "qgsdevtoolspanelwidget.h" #include "qgstaskmanager.h" #include "qgsweakrelation.h" #include "qgsziputils.h" @@ -88,6 +89,7 @@ #include "qgssourceselectprovider.h" #include "qgsprovidermetadata.h" #include "qgsfixattributedialog.h" +#include "qgsprojecttimesettings.h" #include "qgsanalysis.h" #include "qgsgeometrycheckregistry.h" @@ -181,6 +183,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgscoordinateutils.h" #include "qgscredentialdialog.h" #include "qgscustomdrophandler.h" +#include "qgscustomprojectopenhandler.h" #include "qgscustomization.h" #include "qgscustomlayerorderwidget.h" #include "qgscustomprojectiondialog.h" @@ -258,6 +261,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsmapoverviewcanvas.h" #include "qgsmapsettings.h" #include "qgsmaptip.h" +#include "qgsmbtilesreader.h" #include "qgsmenuheader.h" #include "qgsmergeattributesdialog.h" #include "qgsmessageviewer.h" @@ -335,6 +339,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsvectorlayer.h" #include "qgsvectorlayerproperties.h" #include "qgsvectorlayerdigitizingproperties.h" +#include "qgsvectortilelayer.h" #include "qgsmapthemes.h" #include "qgsmessagelogviewer.h" #include "qgsdataitem.h" @@ -365,9 +370,10 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsbearingnumericformat.h" #include "qgsprojectdisplaysettings.h" #include "qgstemporalcontrollerdockwidget.h" - +#include "qgsnetworklogger.h" #include "qgsuserprofilemanager.h" #include "qgsuserprofile.h" +#include "qgsnetworkloggerwidgetfactory.h" #include "browser/qgsinbuiltdataitemproviders.h" @@ -831,6 +837,11 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh connect( mUserProfileManager, &QgsUserProfileManager::profilesChanged, this, &QgisApp::refreshProfileMenu ); endProfile(); + // start the network logger early, we want all requests logged! + startProfile( QStringLiteral( "Network logger" ) ); + mNetworkLogger = new QgsNetworkLogger( QgsNetworkAccessManager::instance(), this ); + endProfile(); + // load GUI: actions, menus, toolbars profiler->beginGroup( QStringLiteral( "qgisapp" ) ); profiler->beginGroup( QStringLiteral( "startup" ) ); @@ -1121,6 +1132,23 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh mMapStylingDock->hide(); endProfile(); + startProfile( QStringLiteral( "Dev Tools dock" ) ); + mDevToolsDock = new QgsDockWidget( this ); + mDevToolsDock->setWindowTitle( tr( "Debugging/Development Tools" ) ); + mDevToolsDock->setObjectName( QStringLiteral( "DevTools" ) ); + QShortcut *showDevToolsDock = new QShortcut( QKeySequence( tr( "F12" ) ), this ); + connect( showDevToolsDock, &QShortcut::activated, mDevToolsDock, &QgsDockWidget::toggleUserVisible ); + showDevToolsDock->setObjectName( QStringLiteral( "ShowDevToolsPanel" ) ); + showDevToolsDock->setWhatsThis( tr( "Show Debugging/Development Tools" ) ); + + mDevToolsWidget = new QgsDevToolsPanelWidget( mDevToolFactories ); + mDevToolsDock->setWidget( mDevToolsWidget ); +// connect( mDevToolsDock, &QDockWidget::visibilityChanged, mActionStyleDock, &QAction::setChecked ); + + addDockWidget( Qt::RightDockWidgetArea, mDevToolsDock ); + mDevToolsDock->hide(); + endProfile(); + startProfile( QStringLiteral( "Snapping dialog" ) ); mSnappingDialog = new QgsSnappingWidget( QgsProject::instance(), mMapCanvas, this ); connect( mSnappingDialog, &QgsSnappingWidget::snappingConfigChanged, QgsProject::instance(), [ = ] { QgsProject::instance()->setSnappingConfig( mSnappingDialog->config() ); } ); @@ -1580,6 +1608,8 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh mBearingNumericFormat.reset( QgsLocalDefaultSettings::bearingFormat() ); + mNetworkLoggerWidgetFactory.reset( qgis::make_unique< QgsNetworkLoggerWidgetFactory >( mNetworkLogger ) ); + // update windows qApp->processEvents(); @@ -1672,6 +1702,8 @@ QgisApp::~QgisApp() // shouldn't be needed, but from this stage on, we don't want/need ANY map canvas refreshes to take place mFreezeCount = 1000000; + mNetworkLoggerWidgetFactory.reset(); + delete mInternalClipboard; delete mQgisInterface; delete mStyleSheetBuilder; @@ -1934,6 +1966,17 @@ void QgisApp::unregisterCustomDropHandler( QgsCustomDropHandler *handler ) } } +void QgisApp::registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) +{ + if ( !mCustomProjectOpenHandlers.contains( handler ) ) + mCustomProjectOpenHandlers << handler; +} + +void QgisApp::unregisterCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) +{ + mCustomProjectOpenHandlers.removeOne( handler ); +} + QVector > QgisApp::customDropHandlers() const { return mCustomDropHandlers; @@ -1984,6 +2027,11 @@ void QgisApp::handleDropUriList( const QgsMimeDataUtils::UriList &lst ) QgsMeshLayer *layer = new QgsMeshLayer( uri, u.name, u.providerKey ); addMapLayer( layer ); } + else if ( u.layerType == QLatin1String( "vector-tile" ) ) + { + QgsVectorTileLayer *layer = new QgsVectorTileLayer( uri, u.name ); + addMapLayer( layer ); + } else if ( u.layerType == QLatin1String( "plugin" ) ) { addPluginLayer( uri, u.name, u.providerKey ); @@ -2549,6 +2597,7 @@ void QgisApp::createActions() connect( mActionSelectFreehand, &QAction::triggered, this, &QgisApp::selectByFreehand ); connect( mActionSelectRadius, &QAction::triggered, this, &QgisApp::selectByRadius ); connect( mActionDeselectAll, &QAction::triggered, this, &QgisApp::deselectAll ); + connect( mActionDeselectActiveLayer, &QAction::triggered, this, &QgisApp::deselectActiveLayer ); connect( mActionSelectAll, &QAction::triggered, this, &QgisApp::selectAll ); connect( mActionReselect, &QAction::triggered, this, [ = ] { @@ -2649,6 +2698,8 @@ void QgisApp::createActions() connect( mActionHideAllLayers, &QAction::triggered, this, &QgisApp::hideAllLayers ); connect( mActionShowSelectedLayers, &QAction::triggered, this, &QgisApp::showSelectedLayers ); connect( mActionHideSelectedLayers, &QAction::triggered, this, &QgisApp::hideSelectedLayers ); + connect( mActionToggleSelectedLayers, &QAction::triggered, this, &QgisApp::toggleSelectedLayers ); + connect( mActionToggleSelectedLayersIndependently, &QAction::triggered, this, &QgisApp::toggleSelectedLayersIndependently ); connect( mActionHideDeselectedLayers, &QAction::triggered, this, &QgisApp::hideDeselectedLayers ); // Plugin Menu Items @@ -2806,6 +2857,7 @@ void QgisApp::createActionGroups() mMapToolGroup->addAction( mActionSelectFreehand ); mMapToolGroup->addAction( mActionSelectRadius ); mMapToolGroup->addAction( mActionDeselectAll ); + mMapToolGroup->addAction( mActionDeselectActiveLayer ); mMapToolGroup->addAction( mActionSelectAll ); mMapToolGroup->addAction( mActionReselect ); mMapToolGroup->addAction( mActionInvertSelection ); @@ -3108,66 +3160,88 @@ void QgisApp::createToolBars() mToolbarMenu->addActions( toolbarMenuActions ); - // selection tool button - + // advanced selection tool button QToolButton *bt = new QToolButton( mAttributesToolBar ); bt->setPopupMode( QToolButton::MenuButtonPopup ); - QList selectionActions; - selectionActions << mActionSelectByForm << mActionSelectByExpression << mActionSelectAll - << mActionInvertSelection; - bt->addActions( selectionActions ); + bt->addAction( mActionSelectByForm ); + bt->addAction( mActionSelectByExpression ); + bt->addAction( mActionSelectAll ); + bt->addAction( mActionInvertSelection ); - QAction *defSelectionAction = mActionSelectByForm; + QAction *defAdvancedSelectionAction = mActionSelectByForm; switch ( settings.value( QStringLiteral( "UI/selectionTool" ), 0 ).toInt() ) { case 0: - defSelectionAction = mActionSelectByForm; + defAdvancedSelectionAction = mActionSelectByForm; break; case 1: - defSelectionAction = mActionSelectByExpression; + defAdvancedSelectionAction = mActionSelectByExpression; break; case 2: - defSelectionAction = mActionSelectAll; + defAdvancedSelectionAction = mActionSelectAll; break; case 3: - defSelectionAction = mActionInvertSelection; + defAdvancedSelectionAction = mActionInvertSelection; break; } - bt->setDefaultAction( defSelectionAction ); - QAction *selectionAction = mAttributesToolBar->insertWidget( mActionDeselectAll, bt ); - selectionAction->setObjectName( QStringLiteral( "ActionSelection" ) ); + bt->setDefaultAction( defAdvancedSelectionAction ); + QAction *advancedSelectionAction = mAttributesToolBar->insertWidget( mActionOpenTable, bt ); + advancedSelectionAction->setObjectName( QStringLiteral( "ActionSelection" ) ); connect( bt, &QToolButton::triggered, this, &QgisApp::toolButtonActionTriggered ); - // select tool button + // mouse select tool button bt = new QToolButton( mAttributesToolBar ); bt->setPopupMode( QToolButton::MenuButtonPopup ); - QList selectActions; - selectActions << mActionSelectFeatures << mActionSelectPolygon - << mActionSelectFreehand << mActionSelectRadius; - bt->addActions( selectActions ); + bt->addAction( mActionSelectFeatures ); + bt->addAction( mActionSelectPolygon ); + bt->addAction( mActionSelectFreehand ); + bt->addAction( mActionSelectRadius ); - QAction *defSelectAction = mActionSelectFeatures; + QAction *defMouseSelectAction = mActionSelectFeatures; switch ( settings.value( QStringLiteral( "UI/selectTool" ), 1 ).toInt() ) { case 1: - defSelectAction = mActionSelectFeatures; + defMouseSelectAction = mActionSelectFeatures; break; case 2: - defSelectAction = mActionSelectRadius; + defMouseSelectAction = mActionSelectRadius; break; case 3: - defSelectAction = mActionSelectPolygon; + defMouseSelectAction = mActionSelectPolygon; break; case 4: - defSelectAction = mActionSelectFreehand; + defMouseSelectAction = mActionSelectFreehand; break; } - bt->setDefaultAction( defSelectAction ); - QAction *selectAction = mAttributesToolBar->insertWidget( selectionAction, bt ); - selectAction->setObjectName( QStringLiteral( "ActionSelect" ) ); + bt->setDefaultAction( defMouseSelectAction ); + QAction *mouseSelectionAction = mAttributesToolBar->insertWidget( advancedSelectionAction, bt ); + mouseSelectionAction->setObjectName( QStringLiteral( "ActionSelect" ) ); connect( bt, &QToolButton::triggered, this, &QgisApp::toolButtonActionTriggered ); + + // deselection tool button + bt = new QToolButton( mAttributesToolBar ); + bt->setPopupMode( QToolButton::MenuButtonPopup ); + bt->addAction( mActionDeselectAll ); + bt->addAction( mActionDeselectActiveLayer ); + + QAction *defDeselectionAction = mActionDeselectAll; + switch ( settings.value( QStringLiteral( "UI/deselectionTool" ), 0 ).toInt() ) + { + case 0: + defDeselectionAction = mActionDeselectAll; + break; + case 1: + defDeselectionAction = mActionDeselectActiveLayer; + break; + } + bt->setDefaultAction( defDeselectionAction ); + QAction *deselectionAction = mAttributesToolBar->insertWidget( mActionOpenTable, bt ); + deselectionAction->setObjectName( QStringLiteral( "ActionDeselection" ) ); + connect( bt, &QToolButton::triggered, this, &QgisApp::toolButtonActionTriggered ); + + // feature action tool button bt = new QToolButton( mAttributesToolBar ); @@ -3178,7 +3252,7 @@ void QgisApp::createToolBars() connect( mFeatureActionMenu, &QMenu::triggered, this, &QgisApp::doFeatureAction ); connect( mFeatureActionMenu, &QMenu::aboutToShow, this, &QgisApp::refreshFeatureActions ); bt->setMenu( mFeatureActionMenu ); - QAction *featureActionAction = mAttributesToolBar->insertWidget( selectAction, bt ); + QAction *featureActionAction = mAttributesToolBar->insertWidget( mouseSelectionAction, bt ); featureActionAction->setObjectName( QStringLiteral( "ActionFeatureAction" ) ); // measure tool button @@ -3879,6 +3953,7 @@ void QgisApp::setTheme( const QString &themeName ) mActionSelectFreehand->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSelectFreehand.svg" ) ) ); mActionSelectRadius->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSelectRadius.svg" ) ) ); mActionDeselectAll->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionDeselectAll.svg" ) ) ); + mActionDeselectActiveLayer->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionDeselectActiveLayer.svg" ) ) ); mActionSelectAll->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionSelectAll.svg" ) ) ); mActionInvertSelection->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionInvertSelection.svg" ) ) ); mActionSelectByExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpressionSelect.svg" ) ) ); @@ -4459,7 +4534,7 @@ void QgisApp::freezeCanvases( bool frozen ) QgsMessageBar *QgisApp::messageBar() { - Q_ASSERT( mInfoBar ); + // Q_ASSERT( mInfoBar ); return mInfoBar; } @@ -4822,7 +4897,7 @@ void QgisApp::updateRecentProjectPaths() } // add this file to the recently opened/saved projects list -void QgisApp::saveRecentProjectPath( bool savePreviewImage ) +void QgisApp::saveRecentProjectPath( bool savePreviewImage, const QIcon &iconOverlay ) { // first, re-read the recent project paths. This prevents loss of recent // projects when multiple QGIS sessions are open @@ -4858,7 +4933,7 @@ void QgisApp::saveRecentProjectPath( bool savePreviewImage ) projectData.previewImagePath = QStringLiteral( "%1/%2.png" ).arg( previewDir, fileName ); QDir().mkdir( previewDir ); - createPreviewImage( projectData.previewImagePath ); + createPreviewImage( projectData.previewImagePath, iconOverlay ); } else { @@ -5417,6 +5492,15 @@ QgsMeshLayer *QgisApp::addMeshLayerPrivate( const QString &url, const QString &b return nullptr; } + // Manage default reference time, if not reference time is present by default in the layer -> assign one + if ( ! layer->temporalProperties()->referenceTime().isValid() ) + { + QDateTime referenceTime = QgsProject::instance()->timeSettings()->temporalRange().begin(); + if ( !referenceTime.isValid() ) // If project reference time is invalid, use current date + referenceTime = QDateTime( QDate::currentDate(), QTime( 0, 0, 0, Qt::UTC ) ); + layer->temporalProperties()->setReferenceTime( referenceTime, layer->dataProvider()->temporalCapabilities() ); + } + QgsProject::instance()->addMapLayer( layer.get() ); askUserForDatumTransform( layer->crs(), QgsProject::instance()->crs(), layer.get() ); @@ -5983,20 +6067,31 @@ void QgisApp::fileExit() if ( QgsApplication::taskManager()->countActiveTasks() > 0 ) { QStringList tasks; - const auto constActiveTasks = QgsApplication::taskManager()->activeTasks(); - for ( QgsTask *task : constActiveTasks ) + const QList< QgsTask * > activeTasks = QgsApplication::taskManager()->activeTasks(); + for ( QgsTask *task : activeTasks ) { + if ( task->flags() & QgsTask::CancelWithoutPrompt ) + continue; + tasks << tr( " • %1" ).arg( task->description() ); } - // active tasks - if ( QMessageBox::question( this, tr( "Active Tasks" ), - tr( "The following tasks are currently running in the background:\n\n%1\n\nDo you want to try canceling these active tasks?" ).arg( tasks.join( QStringLiteral( "\n" ) ) ), - QMessageBox::Yes | QMessageBox::No ) == QMessageBox::Yes ) + // prompt if any tasks which require user confirmation remain, otherwise just cancel them directly and continue with shutdown. + if ( tasks.empty() ) { + // all tasks can be silently terminated without warning QgsApplication::taskManager()->cancelAll(); } - return; + else + { + if ( QMessageBox::question( this, tr( "Active Tasks" ), + tr( "The following tasks are currently running in the background:\n\n%1\n\nDo you want to try canceling these active tasks?" ).arg( tasks.join( QStringLiteral( "\n" ) ) ), + QMessageBox::Yes | QMessageBox::No ) == QMessageBox::Yes ) + { + QgsApplication::taskManager()->cancelAll(); + } + return; + } } QgsCanvasRefreshBlocker refreshBlocker; @@ -6509,18 +6604,40 @@ void QgisApp::fileOpen() // Retrieve last used project dir from persistent settings QgsSettings settings; QString lastUsedDir = settings.value( QStringLiteral( "UI/lastProjectDir" ), QDir::homePath() ).toString(); + + + QStringList fileFilters; + QStringList extensions; + fileFilters << tr( "QGIS files" ) + QStringLiteral( " (*.qgs *.qgz *.QGS *.QGZ)" ); + extensions << QStringLiteral( "qgs" ) << QStringLiteral( "qgz" ); + for ( QgsCustomProjectOpenHandler *handler : qgis::as_const( mCustomProjectOpenHandlers ) ) + { + if ( handler ) + { + const QStringList filters = handler->filters(); + fileFilters.append( filters ); + for ( const QString &filter : filters ) + extensions.append( QgsFileUtils::extensionsFromFilter( filter ) ); + } + } + + // generate master "all projects" extension list + QString allEntry = tr( "All Project Files" ) + QStringLiteral( " (" ); + for ( const QString &extension : extensions ) + allEntry += QStringLiteral( "*.%1 *.%2 " ).arg( extension.toLower(), extension.toUpper() ); + allEntry.chop( 1 ); // remove trailing ' ' + allEntry += ')'; + fileFilters.insert( 0, allEntry ); + QString fullPath = QFileDialog::getOpenFileName( this, - tr( "Choose a QGIS Project File to Open" ), + tr( "Open Project" ), lastUsedDir, - tr( "QGIS files" ) + " (*.qgs *.qgz *.QGS)" ); + fileFilters.join( QStringLiteral( ";;" ) ) ); if ( fullPath.isNull() ) { return; } - // Fix by Tim - getting the dirPath from the dialog - // directly truncates the last node in the dir path. - // This is a workaround for that QFileInfo myFI( fullPath ); QString myPath = myFI.path(); // Persist last used project dir @@ -6575,7 +6692,22 @@ bool QgisApp::addProject( const QString &projectFile ) bool autoSetupOnFirstLayer = mLayerTreeCanvasBridge->autoSetupOnFirstLayer(); mLayerTreeCanvasBridge->setAutoSetupOnFirstLayer( false ); - if ( !QgsProject::instance()->read( projectFile ) && !QgsZipUtils::isZipFile( projectFile ) ) + // give custom handlers a chance first + bool usedCustomHandler = false; + bool customHandlerWantsThumbnail = false; + QIcon customHandlerIcon; + for ( QgsCustomProjectOpenHandler *handler : qgis::as_const( mCustomProjectOpenHandlers ) ) + { + if ( handler && handler->handleProjectOpen( projectFile ) ) + { + usedCustomHandler = true; + customHandlerWantsThumbnail = handler->createDocumentThumbnailAfterOpen(); + customHandlerIcon = handler->icon(); + break; + } + } + + if ( !usedCustomHandler && !QgsProject::instance()->read( projectFile ) && !QgsZipUtils::isZipFile( projectFile ) ) { QString backupFile = projectFile + "~"; QString loadBackupPrompt; @@ -6668,7 +6800,20 @@ bool QgisApp::addProject( const QString &projectFile ) // specific plug-in state // add this to the list of recently used project files - saveRecentProjectPath( false ); + // if a custom handler was used, then we generate a thumbnail + if ( !usedCustomHandler || !customHandlerWantsThumbnail ) + saveRecentProjectPath( false ); + else if ( !QgsProject::instance()->fileName().isEmpty() ) + { + // we have to delay the thumbnail creation until after the canvas has refreshed for the first time + QMetaObject::Connection *connection = new QMetaObject::Connection(); + *connection = connect( mMapCanvas, &QgsMapCanvas::mapCanvasRefreshed, [ = ]() + { + QObject::disconnect( *connection ); + delete connection; + saveRecentProjectPath( true, customHandlerIcon ); + } ); + } QApplication::restoreOverrideCursor(); @@ -7058,12 +7203,32 @@ bool QgisApp::openLayer( const QString &fileName, bool allowInteractive ) if ( fileName.endsWith( QStringLiteral( ".mbtiles" ), Qt::CaseInsensitive ) ) { - // prefer to use WMS provider's implementation to open MBTiles rasters - QUrlQuery uq; - uq.addQueryItem( "type", "mbtiles" ); - uq.addQueryItem( "url", QUrl::fromLocalFile( fileName ).toString() ); - if ( addRasterLayer( uq.toString(), fileInfo.completeBaseName(), QStringLiteral( "wms" ) ) ) - return true; + QgsMBTilesReader reader( fileName ); + if ( reader.open() ) + { + if ( reader.metadataValue( "format" ) == QStringLiteral( "pbf" ) ) + { + // these are vector tiles + QUrlQuery uq; + uq.addQueryItem( QStringLiteral( "type" ), QStringLiteral( "mbtiles" ) ); + uq.addQueryItem( QStringLiteral( "url" ), fileName ); + std::unique_ptr vtLayer( new QgsVectorTileLayer( uq.toString(), fileInfo.completeBaseName() ) ); + if ( vtLayer->isValid() ) + { + QgsProject::instance()->addMapLayer( vtLayer.release() ); + return true; + } + } + else // raster tiles + { + // prefer to use WMS provider's implementation to open MBTiles rasters + QUrlQuery uq; + uq.addQueryItem( QStringLiteral( "type" ), QStringLiteral( "mbtiles" ) ); + uq.addQueryItem( QStringLiteral( "url" ), QUrl::fromLocalFile( fileName ).toString() ); + if ( addRasterLayer( uq.toString(), fileInfo.completeBaseName(), QStringLiteral( "wms" ) ) ) + return true; + } + } } // try to load it as raster @@ -7529,6 +7694,35 @@ void QgisApp::hideSelectedLayers() } } +void QgisApp::toggleSelectedLayers() +{ + QgsDebugMsg( QStringLiteral( "toggling selected layers!" ) ); + + const auto constSelectedNodes = mLayerTreeView->selectedNodes(); + if ( ! constSelectedNodes.isEmpty() ) + { + bool isFirstNodeChecked = constSelectedNodes[0]->itemVisibilityChecked(); + for ( QgsLayerTreeNode *node : constSelectedNodes ) + { + node->setItemVisibilityChecked( ! isFirstNodeChecked ); + } + } +} + +void QgisApp::toggleSelectedLayersIndependently() +{ + QgsDebugMsg( QStringLiteral( "toggling selected layers independently!" ) ); + + const auto constSelectedNodes = mLayerTreeView->selectedNodes(); + if ( ! constSelectedNodes.isEmpty() ) + { + for ( QgsLayerTreeNode *node : constSelectedNodes ) + { + node->setItemVisibilityChecked( ! node->itemVisibilityChecked() ); + } + } +} + void QgisApp::hideDeselectedLayers() { QList selectedLayerNodes = mLayerTreeView->selectedLayerNodes(); @@ -7798,6 +7992,9 @@ void QgisApp::changeDataSource( QgsMapLayer *layer ) vlayer->setSubsetString( subsetString ); } + if ( vlayer ) + vlayer->updateExtents(); + // All the following code is necessary to refresh the layer QgsLayerTreeModel *model = qobject_cast( mLayerTreeView->model() ); if ( model ) @@ -7821,20 +8018,45 @@ void QgisApp::changeDataSource( QgsMapLayer *layer ) const QVariantMap fixedUriParts = QgsProviderRegistry::instance()->decodeUri( layer->providerType(), layer->source() ); // next, we loop through to see if we can auto-fix any other layers with the same source - const QMap< QString, QgsMapLayer * > layers = QgsProject::instance()->mapLayers( false ); - for ( auto it = layers.begin(); it != layers.end(); ++it ) + if ( originalSourceParts.contains( QStringLiteral( "path" ) ) ) { - if ( it.value()->isValid() ) - continue; + const QString originalPath = originalSourceParts.value( QStringLiteral( "path" ) ).toString(); + const QFileInfo originalPathFi( originalPath ); - QVariantMap thisParts = QgsProviderRegistry::instance()->decodeUri( it.value()->providerType(), it.value()->source() ); - if ( thisParts.contains( QStringLiteral( "path" ) ) && thisParts.value( QStringLiteral( "path" ) ) == originalSourceParts.value( QStringLiteral( "path" ) ) ) + const QMap< QString, QgsMapLayer * > layers = QgsProject::instance()->mapLayers( false ); + for ( auto it = layers.begin(); it != layers.end(); ++it ) { - // found a broken layer with the same original path, fix this one too - uri.uri = it.value()->source().replace( thisParts.value( QStringLiteral( "path" ) ).toString(), - fixedUriParts.value( QStringLiteral( "path" ) ).toString() ); - uri.providerKey = it.value()->providerType(); - fixLayer( it.value(), uri ); + if ( it.value()->isValid() ) + continue; + + QVariantMap thisParts = QgsProviderRegistry::instance()->decodeUri( it.value()->providerType(), it.value()->source() ); + if ( thisParts.contains( QStringLiteral( "path" ) ) ) + { + const QString thisBrokenPath = thisParts.value( QStringLiteral( "path" ) ).toString(); + QString fixedPath; + + const QFileInfo thisBrokenPathFi( thisBrokenPath ); + if ( thisBrokenPath == originalPath ) + { + // found a broken layer with the same original path, fix this one too + fixedPath = fixedUriParts.value( QStringLiteral( "path" ) ).toString(); + } + else if ( thisBrokenPathFi.path() == originalPathFi.path() ) + { + // file from same original directory + QDir fixedDir = QFileInfo( fixedUriParts.value( QStringLiteral( "path" ) ).toString() ).dir(); + const QString newCandidatePath = fixedDir.filePath( thisBrokenPathFi.fileName() ); + if ( QFileInfo::exists( newCandidatePath ) ) + fixedPath = newCandidatePath; + } + + if ( !fixedPath.isEmpty() ) + { + uri.uri = it.value()->source().replace( thisBrokenPath, fixedPath ); + uri.providerKey = it.value()->providerType(); + fixLayer( it.value(), uri ); + } + } } } } @@ -8275,6 +8497,7 @@ QString QgisApp::saveAsFile( QgsMapLayer *layer, const bool onlySelected, const return saveAsVectorFileGeneral( qobject_cast( layer ), true, onlySelected, defaultToAddToMap ); case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: return QString(); } @@ -9286,7 +9509,7 @@ void QgisApp::modifyAttributesOfSelectedFeatures() //dummy feature QgsFeature f; - QgsAttributeEditorContext context; + QgsAttributeEditorContext context( createAttributeEditorContext() ); context.setAllowCustomUi( false ); context.setVectorLayerTools( mVectorLayerTools ); context.setCadDockWidget( mAdvancedDigitizingDockWidget ); @@ -9519,6 +9742,23 @@ void QgisApp::deselectAll() } } +void QgisApp::deselectActiveLayer() +{ + QgsVectorLayer *vlayer = qobject_cast( mMapCanvas->currentLayer() ); + + if ( !vlayer ) + { + visibleMessageBar()->pushMessage( + tr( "No active vector layer" ), + tr( "To deselect all features, choose a vector layer in the legend" ), + Qgis::Info, + messageTimeout() ); + return; + } + + vlayer->removeSelection(); +} + void QgisApp::invertSelection() { QgsVectorLayer *vlayer = qobject_cast( mMapCanvas->currentLayer() ); @@ -9673,8 +9913,24 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer ) if ( !( geom.isEmpty() || geom.isNull( ) ) ) { // avoid intersection if enabled in digitize settings - geom.avoidIntersections( QgsProject::instance()->avoidIntersectionsLayers() ); - // Count collapsed geometries + QList avoidIntersectionsLayers; + switch ( QgsProject::instance()->avoidIntersectionsMode() ) + { + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + avoidIntersectionsLayers.append( pasteVectorLayer ); + break; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers: + avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); + break; + case QgsProject::AvoidIntersectionsMode::AllowIntersections: + break; + } + if ( avoidIntersectionsLayers.size() > 0 ) + { + geom.avoidIntersections( avoidIntersectionsLayers ); + } + + // count collapsed geometries if ( geom.isEmpty() || geom.isNull( ) ) invalidGeometriesCount++; } @@ -10894,7 +11150,7 @@ void QgisApp::duplicateLayers( const QList &lyrList ) Qgis::Critical, messageTimeout() ); else if ( qobject_cast( dupLayer ) ) visibleMessageBar()->pushMessage( tr( "Layer duplication complete" ), - tr( "Note that it's using the same data source." ), + dupLayer->providerType() != QLatin1String( "memory" ) ? tr( "Note that it's using the same data source." ) : QString(), Qgis::Info, messageTimeout() ); if ( !newSelection ) @@ -11462,9 +11718,7 @@ QMap< QString, int > QgisApp::optionsPagesMap() sOptionsPagesMap.insert( QCoreApplication::translate( "QgsOptionsBase", "Network" ), 13 ); sOptionsPagesMap.insert( QCoreApplication::translate( "QgsOptionsBase", "Locator" ), 14 ); sOptionsPagesMap.insert( QCoreApplication::translate( "QgsOptionsBase", "Advanced" ), 15 ); -#ifdef HAVE_OPENCL sOptionsPagesMap.insert( QCoreApplication::translate( "QgsOptionsBase", "Acceleration" ), 16 ); -#endif } ); QMap< QString, int > map = sOptionsPagesMap; @@ -11790,6 +12044,22 @@ void QgisApp::unregisterOptionsWidgetFactory( QgsOptionsWidgetFactory *factory ) mOptionsWidgetFactories.removeAll( factory ); } +void QgisApp::registerDevToolFactory( QgsDevToolWidgetFactory *factory ) +{ + mDevToolFactories << factory; + if ( mDevToolsWidget ) + { + // widget was already created, so we manually need to push this factory to the widget + mDevToolsWidget->addToolFactory( factory ); + } +} + +void QgisApp::unregisterDevToolFactory( QgsDevToolWidgetFactory *factory ) +{ + mDevToolsWidget->removeToolFactory( factory ); + mDevToolFactories.removeAll( factory ); +} + QgsMapLayer *QgisApp::activeLayer() { return mLayerTreeView ? mLayerTreeView->currentLayer() : nullptr; @@ -12120,6 +12390,7 @@ void QgisApp::new3DMapCanvas() map->setSelectionColor( mMapCanvas->selectionColor() ); map->setBackgroundColor( mMapCanvas->canvasColor() ); map->setLayers( mMapCanvas->layers() ); + map->setTemporalRange( mMapCanvas->temporalRange() ); map->setTransformContext( QgsProject::instance()->transformContext() ); map->setPathResolver( QgsProject::instance()->pathResolver() ); @@ -12169,6 +12440,7 @@ Qgs3DMapCanvasDockWidget *QgisApp::createNew3DMapCanvasDock( const QString &name map3DWidget->setWindowTitle( name ); map3DWidget->mapCanvas3D()->setObjectName( name ); map3DWidget->setMainCanvas( mMapCanvas ); + map3DWidget->mapCanvas3D()->setTemporalController( mTemporalControllerWidget->temporalController() ); return map3DWidget; #else Q_UNUSED( name ) @@ -13939,6 +14211,10 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer ) mActionIdentify->setEnabled( true ); break; + case QgsMapLayerType::VectorTileLayer: + // TODO + break; + case QgsMapLayerType::PluginLayer: break; @@ -14404,7 +14680,7 @@ void QgisApp::generateProjectAttachedFiles( QgsStringMap &files ) previewImage->deleteLater(); } -void QgisApp::createPreviewImage( const QString &path ) +void QgisApp::createPreviewImage( const QString &path, const QIcon &icon ) { // Render the map canvas QSize previewSize( 250, 177 ); // h = w / std::sqrt(2) @@ -14416,6 +14692,13 @@ void QgisApp::createPreviewImage( const QString &path ) QPainter previewPainter( &previewImage ); mMapCanvas->render( &previewPainter, QRect( QPoint(), previewSize ), previewRect ); + if ( !icon.isNull() ) + { + QPixmap pixmap = icon.pixmap( QSize( 24, 24 ) ); + previewPainter.drawPixmap( QPointF( 250 - 24 - 5, 177 - 24 - 5 ), pixmap ); + } + previewPainter.end(); + // Save previewImage.save( path ); } @@ -14797,7 +15080,7 @@ void QgisApp::readProject( const QDomDocument &doc ) } } -void QgisApp::showLayerProperties( QgsMapLayer *mapLayer ) +void QgisApp::showLayerProperties( QgsMapLayer *mapLayer, const QString &page ) { /* TODO: Consider reusing the property dialogs again. @@ -14821,6 +15104,9 @@ void QgisApp::showLayerProperties( QgsMapLayer *mapLayer ) case QgsMapLayerType::RasterLayer: { QgsRasterLayerProperties *rasterLayerPropertiesDialog = new QgsRasterLayerProperties( mapLayer, mMapCanvas, this ); + if ( !page.isEmpty() ) + rasterLayerPropertiesDialog->setCurrentPage( page ); + // Cannot use exec here due to raster transparency map tool: // in order to pass focus to the canvas, the dialog needs to // be hidden and shown in non-modal mode. @@ -14888,6 +15174,12 @@ void QgisApp::showLayerProperties( QgsMapLayer *mapLayer ) break; } + case QgsMapLayerType::VectorTileLayer: + { + // TODO + break; + } + case QgsMapLayerType::PluginLayer: { QgsPluginLayer *pl = qobject_cast( mapLayer ); @@ -15064,6 +15356,10 @@ void QgisApp::toolButtonActionTriggered( QAction *action ) settings.setValue( QStringLiteral( "UI/selectionTool" ), 2 ); else if ( action == mActionInvertSelection ) settings.setValue( QStringLiteral( "UI/selectionTool" ), 3 ); + else if ( action == mActionDeselectAll ) + settings.setValue( QStringLiteral( "UI/deselectionTool" ), 0 ); + else if ( action == mActionDeselectActiveLayer ) + settings.setValue( QStringLiteral( "UI/deselectionTool" ), 1 ); else if ( action == mActionMeasure ) settings.setValue( QStringLiteral( "UI/measureTool" ), 0 ); else if ( action == mActionMeasureArea ) @@ -15569,3 +15865,13 @@ void QgisApp::triggerCrashHandler() RaiseException( 0x12345678, 0, 0, nullptr ); #endif } + +QgsAttributeEditorContext QgisApp::createAttributeEditorContext() +{ + QgsAttributeEditorContext context; + context.setVectorLayerTools( vectorLayerTools() ); + context.setMapCanvas( mapCanvas() ); + context.setCadDockWidget( cadDockWidget() ); + context.setMainMessageBar( messageBar() ); + return context; +} diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 9c4144a9dbe8..2b71718dbfb6 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -50,6 +50,7 @@ class QgsComposerManager; class QgsContrastEnhancement; class QgsCoordinateReferenceSystem; class QgsCustomDropHandler; +class QgsCustomProjectOpenHandler; class QgsCustomLayerOrderWidget; class QgsDockWidget; class QgsDoubleSpinBox; @@ -139,6 +140,10 @@ class QgsLayoutCustomDropHandler; class QgsProxyProgressTask; class QgsNetworkRequestParameters; class QgsBearingNumericFormat; +class QgsDevToolsPanelWidget; +class QgsDevToolWidgetFactory; +class QgsNetworkLogger; +class QgsNetworkLoggerWidgetFactory; #include #include @@ -165,6 +170,7 @@ class QgsBearingNumericFormat; #include "ui_qgisapp.h" #include "qgis_app.h" #include "qgsvectorlayerref.h" +#include "devtools/qgsappdevtoolutils.h" #include #include @@ -544,6 +550,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QAction *actionHideAllLayers() { return mActionHideAllLayers; } QAction *actionShowAllLayers() { return mActionShowAllLayers; } QAction *actionHideSelectedLayers() { return mActionHideSelectedLayers; } + QAction *actionToggleSelectedLayers() { return mActionToggleSelectedLayers; } + QAction *actionToggleSelectedLayersIndependently() { return mActionToggleSelectedLayersIndependently; } QAction *actionHideDeselectedLayers() { return mActionHideDeselectedLayers; } QAction *actionShowSelectedLayers() { return mActionShowSelectedLayers; } @@ -633,7 +641,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsLocatorWidget *locatorWidget() { return mLocatorWidget; } //! show layer properties - void showLayerProperties( QgsMapLayer *mapLayer ); + void showLayerProperties( QgsMapLayer *mapLayer, const QString &page = QString() ); //! returns pointer to map legend QgsLayerTreeView *layerTreeView(); @@ -682,12 +690,34 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! Unregister a previously registered tab in the options dialog void unregisterOptionsWidgetFactory( QgsOptionsWidgetFactory *factory ); + //! Register a dev tool factory + void registerDevToolFactory( QgsDevToolWidgetFactory *factory ); + + //! Unregister a previously registered dev tool factory + void unregisterDevToolFactory( QgsDevToolWidgetFactory *factory ); + //! Register a new custom drop handler. void registerCustomDropHandler( QgsCustomDropHandler *handler ); //! Unregister a previously registered custom drop handler. void unregisterCustomDropHandler( QgsCustomDropHandler *handler ); + /** + * Register a new custom project open \a handler. + * \note Ownership of \a handler is not transferred, and the handler must + * be unregistered when plugin is unloaded. + * \see QgsCustomProjectOpenHandler + * \see unregisterCustomProjectOpenHandler() + */ + void registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ); + + /** + * Unregister a previously registered custom project open \a handler. + * \see QgsCustomDropHandler + * \see registerCustomProjectOpenHandler() + */ + void unregisterCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ); + //! Returns a list of registered custom drop handlers. QVector> customDropHandlers() const; @@ -1109,6 +1139,12 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! Create a new spatial bookmark void newBookmark( bool inProject = false ); + /** + * Creates a default attribute editor context using the main map canvas and the main edit tools and message bar + * \since QGIS 3.12 + */ + QgsAttributeEditorContext createAttributeEditorContext(); + protected: //! Handle state changes (WindowTitleChange) @@ -1406,6 +1442,10 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void showAllLayers(); //reimplements method from base (gui) class void hideSelectedLayers(); + //! Toggles the visibility of the selected layers depending on the state of the first layer in selection (first clicked) + void toggleSelectedLayers(); + //! Toggles the visibility of the selected layers independently + void toggleSelectedLayersIndependently(); //! Hides any layers which are not selected void hideDeselectedLayers(); //reimplements method from base (gui) class @@ -1497,6 +1537,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow //! deselect features from all layers void deselectAll(); + //! deselect features from the current active layer + void deselectActiveLayer(); + //! select all features void selectAll(); @@ -1820,7 +1863,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow void activeLayerChanged( QgsMapLayer *layer ); private: - void createPreviewImage( const QString &path ); + void createPreviewImage( const QString &path, const QIcon &overlayIcon = QIcon() ); void startProfile( const QString &name ); void endProfile(); void functionProfile( void ( QgisApp::*fnc )(), QgisApp *instance, const QString &name ); @@ -1875,8 +1918,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow * instance simultaneously results in data loss. * * \param savePreviewImage Set to false when the preview image should not be saved. E.g. project load. + * \param iconOverlay optional icon to overlay when saving a preview image */ - void saveRecentProjectPath( bool savePreviewImage = true ); + void saveRecentProjectPath( bool savePreviewImage = true, const QIcon &iconOverlay = QIcon() ); //! Save recent projects list to settings void saveRecentProjects(); //! Update project menu with the current list of recently accessed projects @@ -2325,6 +2369,8 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsUserProfileManager *mUserProfileManager = nullptr; QgsDockWidget *mMapStylingDock = nullptr; QgsLayerStylingWidget *mMapStyleWidget = nullptr; + QgsDockWidget *mDevToolsDock = nullptr; + QgsDevToolsPanelWidget *mDevToolsWidget = nullptr; //! Persistent tile scale slider QgsTileScaleWidget *mpTileScaleWidget = nullptr; @@ -2368,7 +2414,10 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QList mMapLayerPanelFactories; QList> mOptionsWidgetFactories; + QList mDevToolFactories; + QVector> mCustomDropHandlers; + QVector> mCustomProjectOpenHandlers; QVector> mCustomLayoutDropHandlers; QgsLayoutCustomDropHandler *mLayoutQptDropHandler = nullptr; @@ -2419,6 +2468,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow std::unique_ptr< QgsBearingNumericFormat > mBearingNumericFormat; + QgsNetworkLogger *mNetworkLogger = nullptr; + QgsScopedDevToolWidgetFactory mNetworkLoggerWidgetFactory; + class QgsCanvasRefreshBlocker { public: diff --git a/src/app/qgisappinterface.cpp b/src/app/qgisappinterface.cpp index 51867c98de2c..bb75ca01030d 100644 --- a/src/app/qgisappinterface.cpp +++ b/src/app/qgisappinterface.cpp @@ -536,6 +536,16 @@ void QgisAppInterface::unregisterOptionsWidgetFactory( QgsOptionsWidgetFactory * qgis->unregisterOptionsWidgetFactory( factory ); } +void QgisAppInterface::registerDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) +{ + qgis->registerDevToolFactory( factory ); +} + +void QgisAppInterface::unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) +{ + qgis->unregisterDevToolFactory( factory ); +} + void QgisAppInterface::registerCustomDropHandler( QgsCustomDropHandler *handler ) { qgis->registerCustomDropHandler( handler ); @@ -556,6 +566,16 @@ void QgisAppInterface::unregisterCustomDropHandler( QgsCustomDropHandler *handle qgis->unregisterCustomDropHandler( handler ); } +void QgisAppInterface::registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) +{ + qgis->registerCustomProjectOpenHandler( handler ); +} + +void QgisAppInterface::unregisterCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) +{ + qgis->unregisterCustomProjectOpenHandler( handler ); +} + QMenu *QgisAppInterface::projectMenu() { return qgis->projectMenu(); } QMenu *QgisAppInterface::editMenu() { return qgis->editMenu(); } QMenu *QgisAppInterface::viewMenu() { return qgis->viewMenu(); } @@ -684,6 +704,8 @@ QAction *QgisAppInterface::actionRemoveAllFromOverview() { return qgis->actionRe QAction *QgisAppInterface::actionHideAllLayers() { return qgis->actionHideAllLayers(); } QAction *QgisAppInterface::actionShowAllLayers() { return qgis->actionShowAllLayers(); } QAction *QgisAppInterface::actionHideSelectedLayers() { return qgis->actionHideSelectedLayers(); } +QAction *QgisAppInterface::actionToggleSelectedLayers() { return qgis->actionToggleSelectedLayers(); } +QAction *QgisAppInterface::actionToggleSelectedLayersIndependently() { return qgis->actionToggleSelectedLayersIndependently(); } QAction *QgisAppInterface::actionHideDeselectedLayers() { return qgis->actionHideDeselectedLayers(); } QAction *QgisAppInterface::actionShowSelectedLayers() { return qgis->actionShowSelectedLayers(); } @@ -755,12 +777,8 @@ QgsAttributeDialog *QgisAppInterface::getFeatureForm( QgsVectorLayer *l, QgsFeat myDa.setSourceCrs( l->crs(), QgsProject::instance()->transformContext() ); myDa.setEllipsoid( QgsProject::instance()->ellipsoid() ); - QgsAttributeEditorContext context; + QgsAttributeEditorContext context( QgisApp::instance()->createAttributeEditorContext() ); context.setDistanceArea( myDa ); - context.setVectorLayerTools( qgis->vectorLayerTools() ); - context.setMapCanvas( qgis->mapCanvas() ); - context.setCadDockWidget( qgis->cadDockWidget() ); - context.setMainMessageBar( qgis->messageBar() ); QgsAttributeDialog *dialog = new QgsAttributeDialog( l, &feature, false, qgis, true, context ); if ( !feature.isValid() ) { diff --git a/src/app/qgisappinterface.h b/src/app/qgisappinterface.h index 1468d0fe0172..60ab70e75adf 100644 --- a/src/app/qgisappinterface.h +++ b/src/app/qgisappinterface.h @@ -143,8 +143,12 @@ class APP_EXPORT QgisAppInterface : public QgisInterface void unregisterMapLayerConfigWidgetFactory( QgsMapLayerConfigWidgetFactory *factory ) override; void registerOptionsWidgetFactory( QgsOptionsWidgetFactory *factory ) override; void unregisterOptionsWidgetFactory( QgsOptionsWidgetFactory *factory ) override; + void registerDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override; + void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) override; void registerCustomDropHandler( QgsCustomDropHandler *handler ) override; void unregisterCustomDropHandler( QgsCustomDropHandler *handler ) override; + void registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) override; + void unregisterCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) override; void registerCustomLayoutDropHandler( QgsLayoutCustomDropHandler *handler ) override; void unregisterCustomLayoutDropHandler( QgsLayoutCustomDropHandler *handler ) override; QMenu *projectMenu() override; @@ -254,6 +258,8 @@ class APP_EXPORT QgisAppInterface : public QgisInterface QAction *actionHideAllLayers() override; QAction *actionShowAllLayers() override; QAction *actionHideSelectedLayers() override; + QAction *actionToggleSelectedLayers() override; + QAction *actionToggleSelectedLayersIndependently() override; QAction *actionHideDeselectedLayers() override; QAction *actionShowSelectedLayers() override; QAction *actionManagePlugins() override; diff --git a/src/app/qgisappstylesheet.cpp b/src/app/qgisappstylesheet.cpp index c4dde545df4e..c824378e7eb6 100644 --- a/src/app/qgisappstylesheet.cpp +++ b/src/app/qgisappstylesheet.cpp @@ -16,14 +16,16 @@ * * ***************************************************************************/ +#include +#include + #include "qgisappstylesheet.h" #include "qgsapplication.h" #include "qgisapp.h" +#include "qgsproxystyle.h" #include "qgslogger.h" #include "qgssettings.h" -#include -#include QgisAppStyleSheet::QgisAppStyleSheet( QObject *parent ) : QObject( parent ) @@ -106,18 +108,24 @@ void QgisAppStyleSheet::buildStyleSheet( const QMap &opts ) if ( fontSize != defaultSize || fontFamily != defaultFamily ) ss += QStringLiteral( "* { font: %1pt \"%2\"} " ).arg( fontSize, fontFamily ); +#if QT_VERSION < QT_VERSION_CHECK(5, 12, 2) // Fix for macOS Qt 5.9+, where close boxes do not show on document mode tab bar tabs - // See: https://bugreports.qt.io/browse/QTBUG-61092 - // https://bugreports.qt.io/browse/QTBUG-61742 + // See: https://bugreports.qt.io/browse/QTBUG-61092 => fixed in 5.12.2 / 5.14 + // https://bugreports.qt.io/browse/QTBUG-61742 => fixed in 5.9.2 // Setting any stylesheet makes the default close button disappear. // Specifically setting a custom close button temporarily works around issue. - // TODO: Remove when regression is fixed (Qt 5.9.3 or 5.10?); though hard to tell, - // since we are overriding the default close button image now. if ( mMacStyle ) { ss += QLatin1String( "QTabBar::close-button{ image: url(:/images/themes/default/mIconCloseTab.svg); }" ); ss += QLatin1String( "QTabBar::close-button:hover{ image: url(:/images/themes/default/mIconCloseTabHover.svg); }" ); } +#endif + if ( mMacStyle ) + { + ss += QLatin1String( "QWidget#QgsTextFormatWidgetBase QTabWidget#mOptionsTab QTabBar::tab," ); + ss += QLatin1String( "QWidget#QgsRendererMeshPropsWidgetBase QTabWidget#mStyleOptionsTab" ); + ss += QLatin1String( "QTabBar::tab { width: 1.2em; }" ); + } ss += QLatin1String( "QGroupBox{ font-weight: 600; }" ); @@ -191,7 +199,8 @@ void QgisAppStyleSheet::saveToSettings( const QMap &opts ) void QgisAppStyleSheet::setActiveValues() { - mStyle = qApp->style()->objectName(); // active style name (lowercase) + QgsAppStyle *style = dynamic_cast( qApp->style() ); + mStyle = style ? style->baseStyle() : qApp->style()->objectName(); // active style name (lowercase) QgsDebugMsgLevel( QStringLiteral( "Style name: %1" ).arg( mStyle ), 2 ); mMacStyle = mStyle.contains( QLatin1String( "macintosh" ) ); // macintosh (aqua) diff --git a/src/app/qgsapplayertreeviewmenuprovider.cpp b/src/app/qgsapplayertreeviewmenuprovider.cpp index 83b08fe6923d..bf852fd8a0aa 100644 --- a/src/app/qgsapplayertreeviewmenuprovider.cpp +++ b/src/app/qgsapplayertreeviewmenuprovider.cpp @@ -117,6 +117,11 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu() menu->addAction( actions->actionMoveToTop( menu ) ); } + if ( !( mView->selectedNodes( true ).count() == 1 && idx.row() == idx.model()->rowCount() - 1 ) ) + { + menu->addAction( actions->actionMoveToBottom( menu ) ); + } + menu->addSeparator(); if ( mView->selectedNodes( true ).count() >= 2 ) @@ -204,6 +209,11 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu() menu->addAction( actions->actionMoveToTop( menu ) ); } + if ( !( mView->selectedNodes( true ).count() == 1 && idx.row() == idx.model()->rowCount() - 1 ) ) + { + menu->addAction( actions->actionMoveToBottom( menu ) ); + } + QAction *checkAll = actions->actionCheckAndAllParents( menu ); if ( checkAll ) menu->addAction( checkAll ); diff --git a/src/app/qgsattributetabledialog.cpp b/src/app/qgsattributetabledialog.cpp index 3f53b06b6b50..3ba326f4818a 100644 --- a/src/app/qgsattributetabledialog.cpp +++ b/src/app/qgsattributetabledialog.cpp @@ -152,12 +152,9 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *layer, QgsAttr QgsDistanceArea da; da.setSourceCrs( mLayer->crs(), QgsProject::instance()->transformContext() ); da.setEllipsoid( QgsProject::instance()->ellipsoid() ); - mEditorContext.setDistanceArea( da ); - mEditorContext.setVectorLayerTools( QgisApp::instance()->vectorLayerTools() ); - mEditorContext.setMapCanvas( QgisApp::instance()->mapCanvas() ); - mEditorContext.setMainMessageBar( QgisApp::instance()->messageBar() ); - mEditorContext.setCadDockWidget( QgisApp::instance()->cadDockWidget() ); + QgsAttributeEditorContext editorContext = QgisApp::instance()->createAttributeEditorContext(); + editorContext.setDistanceArea( da ); QgsFeatureRequest r; bool needsGeom = false; @@ -177,12 +174,12 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *layer, QgsAttr r.setFlags( QgsFeatureRequest::NoGeometry ); // Initialize dual view - mMainView->init( mLayer, QgisApp::instance()->mapCanvas(), r, mEditorContext, false ); + mMainView->init( mLayer, QgisApp::instance()->mapCanvas(), r, editorContext, false ); QgsAttributeTableConfig config = mLayer->attributeTableConfig(); mMainView->setAttributeTableConfig( config ); - mFeatureFilterWidget->init( mLayer, mEditorContext, mMainView, QgisApp::instance()->messageBar(), QgisApp::instance()->messageTimeout() ); + mFeatureFilterWidget->init( mLayer, editorContext, mMainView, QgisApp::instance()->messageBar(), QgisApp::instance()->messageTimeout() ); mActionFeatureActions = new QToolButton(); mActionFeatureActions->setAutoRaise( false ); diff --git a/src/app/qgsattributetabledialog.h b/src/app/qgsattributetabledialog.h index 92df0e463884..ec3923892329 100644 --- a/src/app/qgsattributetabledialog.h +++ b/src/app/qgsattributetabledialog.h @@ -221,7 +221,6 @@ class APP_EXPORT QgsAttributeTableDialog : public QDialog, private Ui::QgsAttrib QPointer< QgsVectorLayer > mLayer = nullptr; QStringList mVisibleFields; - QgsAttributeEditorContext mEditorContext; void updateMultiEditButtonState(); void deleteFeature( QgsFeatureId fid ); diff --git a/src/app/qgsdevtoolspanelwidget.cpp b/src/app/qgsdevtoolspanelwidget.cpp new file mode 100644 index 000000000000..03756c9500f2 --- /dev/null +++ b/src/app/qgsdevtoolspanelwidget.cpp @@ -0,0 +1,77 @@ +/*************************************************************************** + qgsdevtoolspanelwidget.cpp + --------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 "qgsdevtoolspanelwidget.h" +#include "qgisapp.h" +#include "qgsdevtoolwidgetfactory.h" +#include "qgsdevtoolwidget.h" +#include "qgspanelwidgetstack.h" + + +QgsDevToolsPanelWidget::QgsDevToolsPanelWidget( const QList &factories, QWidget *parent ) + : QWidget( parent ) +{ + setupUi( this ); + + mOptionsListWidget->setIconSize( QgisApp::instance()->iconSize( false ) ); + mOptionsListWidget->setMaximumWidth( static_cast< int >( mOptionsListWidget->iconSize().width() * 1.18 ) ); + + for ( QgsDevToolWidgetFactory *factory : factories ) + addToolFactory( factory ); + + connect( mOptionsListWidget, &QListWidget::currentRowChanged, this, &QgsDevToolsPanelWidget::setCurrentTool ); +} + +QgsDevToolsPanelWidget::~QgsDevToolsPanelWidget() = default; + +void QgsDevToolsPanelWidget::addToolFactory( QgsDevToolWidgetFactory *factory ) +{ + if ( QgsDevToolWidget *toolWidget = factory->createWidget( this ) ) + { + QgsPanelWidgetStack *toolStack = new QgsPanelWidgetStack(); + toolStack->setMainPanel( toolWidget ); + mStackedWidget->addWidget( toolStack ); + + QListWidgetItem *item = new QListWidgetItem( factory->icon(), QString() ); + item->setToolTip( factory->title() ); + mOptionsListWidget->addItem( item ); + int row = mOptionsListWidget->row( item ); + mFactoryPages[factory] = row; + + if ( mOptionsListWidget->count() == 1 ) + { + setCurrentTool( 0 ); + } + } +} + +void QgsDevToolsPanelWidget::removeToolFactory( QgsDevToolWidgetFactory *factory ) +{ + if ( mFactoryPages.contains( factory ) ) + { + int currentRow = mStackedWidget->currentIndex(); + int row = mFactoryPages.value( factory ); + mStackedWidget->removeWidget( mStackedWidget->widget( row ) ); + mOptionsListWidget->removeItemWidget( mOptionsListWidget->item( row ) ); + mFactoryPages.remove( factory ); + if ( currentRow == row ) + setCurrentTool( 0 ); + } +} + +void QgsDevToolsPanelWidget::setCurrentTool( int row ) +{ + whileBlocking( mOptionsListWidget )->setCurrentRow( row ); + mStackedWidget->setCurrentIndex( row ); +} diff --git a/src/app/qgsdevtoolspanelwidget.h b/src/app/qgsdevtoolspanelwidget.h new file mode 100644 index 000000000000..100db97f5a62 --- /dev/null +++ b/src/app/qgsdevtoolspanelwidget.h @@ -0,0 +1,44 @@ +/*************************************************************************** + qgsdevtoolspanelwidget.h + --------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 QGSDEVTOOLSPANELWIDGET_H +#define QGSDEVTOOLSPANELWIDGET_H + +#include "ui_qgsdevtoolswidgetbase.h" +#include "qgis_app.h" + +class QgsDevToolWidgetFactory; + +class APP_EXPORT QgsDevToolsPanelWidget : public QWidget, private Ui::QgsDevToolsWidgetBase +{ + Q_OBJECT + public: + + QgsDevToolsPanelWidget( const QList &factories, QWidget *parent = nullptr ); + ~QgsDevToolsPanelWidget() override; + + void addToolFactory( QgsDevToolWidgetFactory *factory ); + + void removeToolFactory( QgsDevToolWidgetFactory *factory ); + + private slots: + + void setCurrentTool( int row ); + + private: + + QMap< QgsDevToolWidgetFactory *, int> mFactoryPages; +}; + +#endif // QGSDEVTOOLSPANELWIDGET_H diff --git a/src/app/qgsfeatureaction.cpp b/src/app/qgsfeatureaction.cpp index 3ce0b7a1d0fd..b3c46a55ed39 100644 --- a/src/app/qgsfeatureaction.cpp +++ b/src/app/qgsfeatureaction.cpp @@ -53,7 +53,7 @@ QgsAttributeDialog *QgsFeatureAction::newDialog( bool cloneFeature ) { QgsFeature *f = cloneFeature ? new QgsFeature( *mFeature ) : mFeature; - QgsAttributeEditorContext context; + QgsAttributeEditorContext context( QgisApp::instance()->createAttributeEditorContext() ); QgsDistanceArea myDa; @@ -61,14 +61,11 @@ QgsAttributeDialog *QgsFeatureAction::newDialog( bool cloneFeature ) myDa.setEllipsoid( QgsProject::instance()->ellipsoid() ); context.setDistanceArea( myDa ); - context.setVectorLayerTools( QgisApp::instance()->vectorLayerTools() ); - context.setMapCanvas( QgisApp::instance()->mapCanvas() ); - context.setCadDockWidget( QgisApp::instance()->cadDockWidget() ); - context.setMainMessageBar( QgisApp::instance()->messageBar() ); context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); QgsAttributeDialog *dialog = new QgsAttributeDialog( mLayer, f, cloneFeature, parentWidget(), true, context ); dialog->setWindowFlags( dialog->windowFlags() | Qt::Tool ); + dialog->setObjectName( QStringLiteral( "featureactiondlg:%1:%2" ).arg( mLayer->id() ).arg( f->id() ) ); QList actions = mLayer->actions()->actions( QStringLiteral( "Feature" ) ); diff --git a/src/app/qgsfixattributedialog.cpp b/src/app/qgsfixattributedialog.cpp index 57a83bc8ff5e..c4e81e2bffac 100644 --- a/src/app/qgsfixattributedialog.cpp +++ b/src/app/qgsfixattributedialog.cpp @@ -16,6 +16,7 @@ #include "qgsattributeform.h" #include "qgsapplication.h" +#include "qgisapp.h" #include @@ -33,6 +34,7 @@ void QgsFixAttributeDialog::init( QgsVectorLayer *layer ) setLayout( new QGridLayout() ); layout()->setMargin( 0 ); context.setFormMode( QgsAttributeEditorContext::StandaloneDialog ); + context.setVectorLayerTools( QgisApp::instance()->vectorLayerTools() ); mUnfixedFeatures = mFeatures; mCurrentFeature = mFeatures.begin(); diff --git a/src/app/qgshandlebadlayers.cpp b/src/app/qgshandlebadlayers.cpp index 491ee9de7ae5..9fa17ce11fb3 100644 --- a/src/app/qgshandlebadlayers.cpp +++ b/src/app/qgshandlebadlayers.cpp @@ -44,7 +44,8 @@ void QgsHandleBadLayersHandler::handleBadLayers( const QList &layers ) { - QApplication::setOverrideCursor( Qt::ArrowCursor ); + QgsTemporaryCursorRestoreOverride cursorOverride; + QgsHandleBadLayers *dialog = new QgsHandleBadLayers( layers ); dialog->buttonBox->button( QDialogButtonBox::Ignore )->setToolTip( tr( "Import all unavailable layers unmodified (you can fix them later)." ) ); @@ -70,7 +71,6 @@ void QgsHandleBadLayersHandler::handleBadLayers( const QList &layers ) } delete dialog; - QApplication::restoreOverrideCursor(); } QgsHandleBadLayers::QgsHandleBadLayers( const QList &layers ) @@ -113,6 +113,9 @@ QgsHandleBadLayers::QgsHandleBadLayers( const QList &layers ) << tr( "Datasource" ) ); + mLayerList->horizontalHeader()->setSectionsMovable( true ); + mLayerList->horizontalHeader()->setSectionResizeMode( QHeaderView::Interactive ); + int j = 0; for ( int i = 0; i < mLayers.size(); i++ ) { diff --git a/src/app/qgsidentifyresultsdialog.cpp b/src/app/qgsidentifyresultsdialog.cpp index 617219d98f7f..6f38ea368484 100644 --- a/src/app/qgsidentifyresultsdialog.cpp +++ b/src/app/qgsidentifyresultsdialog.cpp @@ -38,6 +38,7 @@ #include #include #include +#include //graph #include @@ -77,6 +78,7 @@ #include "qgsfiledownloaderdialog.h" #include "qgsfieldformatterregistry.h" #include "qgsfieldformatter.h" +#include "qgsfieldmodel.h" #include "qgssettings.h" #include "qgsgui.h" #include "qgsexpressioncontextutils.h" @@ -497,6 +499,10 @@ void QgsIdentifyResultsDialog::addFeature( const QgsMapToolIdentify::IdentifyRes addFeature( qobject_cast( result.mLayer ), result.mLabel, result.mAttributes, result.mDerivedAttributes ); break; + case QgsMapLayerType::VectorTileLayer: + // TODO + break; + case QgsMapLayerType::PluginLayer: break; } @@ -511,6 +517,9 @@ void QgsIdentifyResultsDialog::addFeature( QgsVectorLayer *vlayer, const QgsFeat layItem = new QTreeWidgetItem( QStringList() << vlayer->name() ); layItem->setData( 0, Qt::UserRole, QVariant::fromValue( qobject_cast( vlayer ) ) ); lstResults->addTopLevelItem( layItem ); + QFont boldFont; + boldFont.setBold( true ); + layItem->setFont( 0, boldFont ); connect( vlayer, &QObject::destroyed, this, &QgsIdentifyResultsDialog::layerDestroyed ); connect( vlayer, &QgsMapLayer::crsChanged, this, &QgsIdentifyResultsDialog::layerDestroyed ); @@ -526,6 +535,12 @@ void QgsIdentifyResultsDialog::addFeature( QgsVectorLayer *vlayer, const QgsFeat featItem->setData( 0, Qt::UserRole + 1, mFeatures.size() ); mFeatures << f; layItem->addChild( featItem ); + layItem->setFirstColumnSpanned( true ); + + QString countSuffix = layItem->childCount() > 1 + ? QStringLiteral( " [%1]" ).arg( layItem->childCount() ) + : QString(); + layItem->setText( 0, QStringLiteral( "%1 %2" ).arg( vlayer->name(), countSuffix ) ); if ( derivedAttributes.size() >= 0 ) { @@ -616,7 +631,7 @@ void QgsIdentifyResultsDialog::addFeature( QgsVectorLayer *vlayer, const QgsFeat featItem->addChild( attrItem ); attrItem->setData( 0, Qt::DisplayRole, vlayer->attributeDisplayName( i ) ); - attrItem->setToolTip( 0, vlayer->attributeDisplayName( i ) ); + attrItem->setToolTip( 0, QgsFieldModel::fieldToolTipExtended( fields.at( i ), vlayer ) ); attrItem->setData( 0, Qt::UserRole, fields.at( i ).name() ); attrItem->setData( 0, Qt::UserRole + 1, i ); @@ -1873,7 +1888,7 @@ void QgsIdentifyResultsDialog::zoomToFeature() rect.scale( 0.5, &c ); } - mCanvas->setExtent( rect ); + mCanvas->setExtent( rect, true ); mCanvas->refresh(); } diff --git a/src/app/qgslayerstylingwidget.cpp b/src/app/qgslayerstylingwidget.cpp index 1ab0b863bbb1..c98d4e9e51e2 100644 --- a/src/app/qgslayerstylingwidget.cpp +++ b/src/app/qgslayerstylingwidget.cpp @@ -35,6 +35,8 @@ #include "qgsmaplayer.h" #include "qgsstyle.h" #include "qgsvectorlayer.h" +#include "qgsvectortilelayer.h" +#include "qgsvectortilebasicrendererwidget.h" #include "qgsmeshlayer.h" #include "qgsproject.h" #include "qgsundowidget.h" @@ -225,6 +227,16 @@ void QgsLayerStylingWidget::setLayer( QgsMapLayer *layer ) break; } + case QgsMapLayerType::VectorTileLayer: + { + QListWidgetItem *symbolItem = new QListWidgetItem( QgsApplication::getThemeIcon( QStringLiteral( "propertyicons/symbology.svg" ) ), QString() ); + symbolItem->setData( Qt::UserRole, Symbology ); + symbolItem->setToolTip( tr( "Symbology" ) ); + mOptionsListWidget->addItem( symbolItem ); + + break; + } + case QgsMapLayerType::PluginLayer: break; } @@ -600,6 +612,25 @@ void QgsLayerStylingWidget::updateCurrentWidgetLayer() break; } + case QgsMapLayerType::VectorTileLayer: + { + QgsVectorTileLayer *vtLayer = qobject_cast( mCurrentLayer ); + switch ( row ) + { + case 0: // Style + { + mVectorTileStyleWidget = new QgsVectorTileBasicRendererWidget( vtLayer, mMapCanvas, mMessageBar, mWidgetStack ); + mVectorTileStyleWidget->setDockMode( true ); + connect( mVectorTileStyleWidget, &QgsPanelWidget::widgetChanged, this, &QgsLayerStylingWidget::autoApply ); + mWidgetStack->setMainPanel( mVectorTileStyleWidget ); + break; + } + default: + break; + } + break; + } + case QgsMapLayerType::PluginLayer: { mStackedWidget->setCurrentIndex( mNotSupportedPage ); @@ -724,6 +755,9 @@ bool QgsLayerStyleManagerWidgetFactory::supportsLayer( QgsMapLayer *layer ) cons case QgsMapLayerType::MeshLayer: return true; + case QgsMapLayerType::VectorTileLayer: + return false; // TODO + case QgsMapLayerType::PluginLayer: return false; } diff --git a/src/app/qgslayerstylingwidget.h b/src/app/qgslayerstylingwidget.h index c1ca3f5bcf35..52bef55b003a 100644 --- a/src/app/qgslayerstylingwidget.h +++ b/src/app/qgslayerstylingwidget.h @@ -44,6 +44,7 @@ class QgsMapLayerStyleManagerWidget; class QgsVectorLayer3DRendererWidget; class QgsMeshLayer3DRendererWidget; class QgsMessageBar; +class QgsVectorTileBasicRendererWidget; class APP_EXPORT QgsLayerStyleManagerWidgetFactory : public QgsMapLayerConfigWidgetFactory { @@ -149,6 +150,7 @@ class APP_EXPORT QgsLayerStylingWidget : public QWidget, private Ui::QgsLayerSty #endif QgsRendererRasterPropertiesWidget *mRasterStyleWidget = nullptr; QgsRendererMeshPropertiesWidget *mMeshStyleWidget = nullptr; + QgsVectorTileBasicRendererWidget *mVectorTileStyleWidget = nullptr; QList mPageFactories; QMap mUserPages; QgsLayerStyleManagerWidgetFactory *mStyleManagerFactory = nullptr; diff --git a/src/app/qgslayertreeviewindicatorprovider.cpp b/src/app/qgslayertreeviewindicatorprovider.cpp index daa382d75900..66f680a79ce9 100644 --- a/src/app/qgslayertreeviewindicatorprovider.cpp +++ b/src/app/qgslayertreeviewindicatorprovider.cpp @@ -21,6 +21,7 @@ #include "qgslayertreeview.h" #include "qgsvectorlayer.h" #include "qgsrasterlayer.h" +#include "qgsmeshlayer.h" #include "qgisapp.h" #include "qgsapplication.h" @@ -96,7 +97,9 @@ void QgsLayerTreeViewIndicatorProvider::onLayerLoaded() if ( !layerNode ) return; - if ( !( qobject_cast( layerNode->layer() ) || qobject_cast( layerNode->layer() ) ) ) + if ( !( qobject_cast( layerNode->layer() ) || + qobject_cast( layerNode->layer() ) || + qobject_cast( layerNode->layer() ) ) ) return; if ( QgsMapLayer *mapLayer = layerNode->layer() ) diff --git a/src/app/qgslayertreeviewtemporalindicator.cpp b/src/app/qgslayertreeviewtemporalindicator.cpp index 133bd40523cc..c99b563fcf07 100644 --- a/src/app/qgslayertreeviewtemporalindicator.cpp +++ b/src/app/qgslayertreeviewtemporalindicator.cpp @@ -20,7 +20,9 @@ #include "qgslayertree.h" #include "qgslayertreemodel.h" #include "qgslayertreeutils.h" +#include "qgsmeshlayer.h" #include "qgsrasterlayer.h" +#include "qgsrasterlayerproperties.h" #include "qgisapp.h" QgsLayerTreeViewTemporalIndicatorProvider::QgsLayerTreeViewTemporalIndicatorProvider( QgsLayerTreeView *view ) @@ -30,7 +32,7 @@ QgsLayerTreeViewTemporalIndicatorProvider::QgsLayerTreeViewTemporalIndicatorProv void QgsLayerTreeViewTemporalIndicatorProvider::connectSignals( QgsMapLayer *layer ) { - if ( !layer->temporalProperties() ) + if ( !layer || !layer->temporalProperties() ) return; connect( layer->temporalProperties(), &QgsMapLayerTemporalProperties::changed, this, [ this, layer ]( ) { this->onLayerChanged( layer ); } ); @@ -49,12 +51,14 @@ void QgsLayerTreeViewTemporalIndicatorProvider::onIndicatorClicked( const QModel switch ( layer->type() ) { case QgsMapLayerType::RasterLayer: - QgisApp::instance()->showLayerProperties( qobject_cast( layer ) ); + QgisApp::instance()->showLayerProperties( qobject_cast( layer ), QStringLiteral( "mOptsPage_Temporal" ) ); break; - - case QgsMapLayerType::VectorLayer: case QgsMapLayerType::MeshLayer: + QgisApp::instance()->showLayerProperties( qobject_cast( layer ), QStringLiteral( "mOptsPage_Temporal" ) ); + break; + case QgsMapLayerType::VectorLayer: case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::VectorTileLayer: break; } } @@ -69,31 +73,14 @@ bool QgsLayerTreeViewTemporalIndicatorProvider::acceptLayer( QgsMapLayer *layer return false; } -QString QgsLayerTreeViewTemporalIndicatorProvider::iconName( QgsMapLayer *layer ) +QString QgsLayerTreeViewTemporalIndicatorProvider::iconName( QgsMapLayer * ) { - switch ( layer->temporalProperties()->temporalSource() ) - { - case QgsMapLayerTemporalProperties::TemporalSource::Project: - return QStringLiteral( "/mIndicatorTimeFromProject.svg" ); - - case QgsMapLayerTemporalProperties::TemporalSource::Layer: - return QStringLiteral( "/mIndicatorTemporal.svg" ); - } - - return QString(); + return QStringLiteral( "/mIndicatorTemporal.svg" ); } -QString QgsLayerTreeViewTemporalIndicatorProvider::tooltipText( QgsMapLayer *layer ) +QString QgsLayerTreeViewTemporalIndicatorProvider::tooltipText( QgsMapLayer * ) { - switch ( layer->temporalProperties()->temporalSource() ) - { - case QgsMapLayerTemporalProperties::TemporalSource::Project: - return tr( "Temporal layer, currently using project's time range " ); - - case QgsMapLayerTemporalProperties::TemporalSource::Layer: - return tr( "Temporal layer " ); - } - return QString(); + return tr( "Temporal layer" ); } void QgsLayerTreeViewTemporalIndicatorProvider::onLayerChanged( QgsMapLayer *layer ) diff --git a/src/app/qgsmapthemes.cpp b/src/app/qgsmapthemes.cpp index 687cb3bd97e1..1151e4ee4448 100644 --- a/src/app/qgsmapthemes.cpp +++ b/src/app/qgsmapthemes.cpp @@ -42,6 +42,8 @@ QgsMapThemes::QgsMapThemes() mMenu->addAction( QgisApp::instance()->actionHideAllLayers() ); mMenu->addAction( QgisApp::instance()->actionShowSelectedLayers() ); mMenu->addAction( QgisApp::instance()->actionHideSelectedLayers() ); + mMenu->addAction( QgisApp::instance()->actionToggleSelectedLayers() ); + mMenu->addAction( QgisApp::instance()->actionToggleSelectedLayersIndependently() ); mMenu->addAction( QgisApp::instance()->actionHideDeselectedLayers() ); mMenu->addSeparator(); diff --git a/src/app/qgsmaptooladdfeature.cpp b/src/app/qgsmaptooladdfeature.cpp index c2e60b674c51..1669f6b0a9fc 100644 --- a/src/app/qgsmaptooladdfeature.cpp +++ b/src/app/qgsmaptooladdfeature.cpp @@ -33,6 +33,7 @@ #include "qgsfeatureaction.h" #include "qgisapp.h" #include "qgsexpressioncontextutils.h" +#include "qgsrubberband.h" #include @@ -69,7 +70,9 @@ void QgsMapToolAddFeature::digitized( const QgsFeature &f ) { //add points to other features to keep topology up-to-date bool topologicalEditing = QgsProject::instance()->topologicalEditing(); - if ( mode() == CaptureLine || mode() == CapturePolygon ) + QgsProject::AvoidIntersectionsMode avoidIntersectionsMode = QgsProject::instance()->avoidIntersectionsMode(); + if ( topologicalEditing && avoidIntersectionsMode == QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers && + ( mode() == CaptureLine || mode() == CapturePolygon ) ) { //use always topological editing for avoidIntersection. @@ -91,7 +94,6 @@ void QgsMapToolAddFeature::digitized( const QgsFeature &f ) if ( topologicalEditing ) { QList sm = snappingMatches(); - Q_ASSERT( f.geometry().constGet()->vertexCount() == sm.size() ); for ( int i = 0; i < sm.size() ; ++i ) { if ( sm.at( i ).layer() ) diff --git a/src/app/qgsmaptooladdpart.cpp b/src/app/qgsmaptooladdpart.cpp index e931289e0693..22d4ac4d8be5 100644 --- a/src/app/qgsmaptooladdpart.cpp +++ b/src/app/qgsmaptooladdpart.cpp @@ -159,7 +159,23 @@ void QgsMapToolAddPart::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) QgsCurvePolygon *cp = new QgsCurvePolygon(); cp->setExteriorRing( curveToAdd ); QgsGeometry *geom = new QgsGeometry( cp ); - geom->avoidIntersections( QgsProject::instance()->avoidIntersectionsLayers() ); + + QList avoidIntersectionsLayers; + switch ( QgsProject::instance()->avoidIntersectionsMode() ) + { + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + avoidIntersectionsLayers.append( vlayer ); + break; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers: + avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); + break; + case QgsProject::AvoidIntersectionsMode::AllowIntersections: + break; + } + if ( avoidIntersectionsLayers.size() > 0 ) + { + geom->avoidIntersections( avoidIntersectionsLayers ); + } const QgsCurvePolygon *cpGeom = qgsgeometry_cast( geom->constGet() ); if ( !cpGeom ) diff --git a/src/app/qgsmaptoolfillring.cpp b/src/app/qgsmaptoolfillring.cpp index 8b5ccfc186f5..0a1cca0d2f0b 100644 --- a/src/app/qgsmaptoolfillring.cpp +++ b/src/app/qgsmaptoolfillring.cpp @@ -164,7 +164,11 @@ void QgsMapToolFillRing::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) } else { - QgsAttributeDialog *dialog = new QgsAttributeDialog( vlayer, &ft, false, nullptr, true ); + QgsAttributeEditorContext context; + // don't set cadDockwidget in context because we don't want to be able to create geometries from this dialog + // there is one modified and one created feature, so it's a mess of we start to digitize a relation feature geometry + context.setVectorLayerTools( QgisApp::instance()->vectorLayerTools() ); + QgsAttributeDialog *dialog = new QgsAttributeDialog( vlayer, &ft, false, nullptr, true, context ); dialog->setMode( QgsAttributeEditorContext::AddFeatureMode ); res = dialog->exec(); // will also add the feature } diff --git a/src/app/qgsmaptoolreshape.cpp b/src/app/qgsmaptoolreshape.cpp index e7a2f9bb95fe..a2beff5d2508 100644 --- a/src/app/qgsmaptoolreshape.cpp +++ b/src/app/qgsmaptoolreshape.cpp @@ -157,12 +157,27 @@ void QgsMapToolReshape::reshape( QgsVectorLayer *vlayer ) QHash > ignoreFeatures; ignoreFeatures.insert( vlayer, vlayer->allFeatureIds() ); - if ( geom.avoidIntersections( QgsProject::instance()->avoidIntersectionsLayers(), ignoreFeatures ) != 0 ) + QList avoidIntersectionsLayers; + switch ( QgsProject::instance()->avoidIntersectionsMode() ) { - emit messageEmitted( tr( "An error was reported during intersection removal" ), Qgis::Critical ); - vlayer->destroyEditCommand(); - stopCapturing(); - return; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + avoidIntersectionsLayers.append( vlayer ); + break; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers: + avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); + break; + case QgsProject::AvoidIntersectionsMode::AllowIntersections: + break; + } + if ( avoidIntersectionsLayers.size() > 0 ) + { + if ( geom.avoidIntersections( QgsProject::instance()->avoidIntersectionsLayers(), ignoreFeatures ) != 0 ) + { + emit messageEmitted( tr( "An error was reported during intersection removal" ), Qgis::Critical ); + vlayer->destroyEditCommand(); + stopCapturing(); + return; + } } if ( geom.isEmpty() ) //intersection removal might have removed the whole geometry diff --git a/src/app/qgsmaptoolsplitparts.h b/src/app/qgsmaptoolsplitparts.h index 0f5ca3065e4f..b8926db564a2 100644 --- a/src/app/qgsmaptoolsplitparts.h +++ b/src/app/qgsmaptoolsplitparts.h @@ -19,11 +19,11 @@ #include "qgsmaptoolcapture.h" //! A map tool that draws a line and splits the parts cut by the line -class QgsMapToolSplitParts: public QgsMapToolCapture +class APP_EXPORT QgsMapToolSplitParts: public QgsMapToolCapture { Q_OBJECT public: - explicit QgsMapToolSplitParts( QgsMapCanvas *canvas ); + QgsMapToolSplitParts( QgsMapCanvas *canvas ); void cadCanvasReleaseEvent( QgsMapMouseEvent *e ) override; }; diff --git a/src/app/qgsnewspatialitelayerdialog.cpp b/src/app/qgsnewspatialitelayerdialog.cpp index 47f661f7d949..09f75a2288a2 100644 --- a/src/app/qgsnewspatialitelayerdialog.cpp +++ b/src/app/qgsnewspatialitelayerdialog.cpp @@ -22,12 +22,16 @@ #include "qgis.h" #include "qgsapplication.h" -#include "qgsproviderregistry.h" +#include "qgsabstractdatabaseproviderconnection.h" #include "qgisapp.h" // <- for theme icons #include "qgsvectorlayer.h" #include "qgsproject.h" #include "qgscoordinatereferencesystem.h" +#include "qgsfileutils.h" #include "qgsprojectionselectiondialog.h" +#include "qgsproviderconnectionmodel.h" +#include "qgsprovidermetadata.h" +#include "qgsproviderregistry.h" #include "qgsspatialiteutils.h" #include "qgslogger.h" #include "qgssettings.h" @@ -51,7 +55,7 @@ QgsNewSpatialiteLayerDialog::QgsNewSpatialiteLayerDialog( QWidget *parent, Qt::W connect( mGeometryTypeBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsNewSpatialiteLayerDialog::mGeometryTypeBox_currentIndexChanged ); connect( mTypeBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsNewSpatialiteLayerDialog::mTypeBox_currentIndexChanged ); connect( pbnFindSRID, &QPushButton::clicked, this, &QgsNewSpatialiteLayerDialog::pbnFindSRID_clicked ); - connect( toolButtonNewDatabase, &QToolButton::clicked, this, &QgsNewSpatialiteLayerDialog::toolButtonNewDatabase_clicked ); + connect( toolButtonNewDatabase, &QToolButton::clicked, this, &QgsNewSpatialiteLayerDialog::createDb ); connect( buttonBox, &QDialogButtonBox::accepted, this, &QgsNewSpatialiteLayerDialog::buttonBox_accepted ); connect( buttonBox, &QDialogButtonBox::rejected, this, &QgsNewSpatialiteLayerDialog::buttonBox_rejected ); @@ -75,20 +79,7 @@ QgsNewSpatialiteLayerDialog::QgsNewSpatialiteLayerDialog( QWidget *parent, Qt::W mTypeBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFieldInteger.svg" ) ), tr( "Whole number" ), "integer" ); mTypeBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mIconFieldFloat.svg" ) ), tr( "Decimal number" ), "real" ); - // Populate the database list from the stored connections - QgsSettings settings; - settings.beginGroup( QStringLiteral( "SpatiaLite/connections" ) ); - QStringList keys = settings.childGroups(); - QStringList::Iterator it = keys.begin(); - mDatabaseComboBox->clear(); - while ( it != keys.end() ) - { - // retrieving the SQLite DB name and full path - QString text = settings.value( *it + "/sqlitepath", "###unknown###" ).toString(); - mDatabaseComboBox->addItem( text ); - ++it; - } - settings.endGroup(); + mDatabaseComboBox->setProvider( QStringLiteral( "spatialite" ) ); mOkButton = buttonBox->button( QDialogButtonBox::Ok ); mOkButton->setEnabled( false ); @@ -132,26 +123,6 @@ void QgsNewSpatialiteLayerDialog::mTypeBox_currentIndexChanged( int index ) } } -void QgsNewSpatialiteLayerDialog::toolButtonNewDatabase_clicked() -{ - QString fileName = QFileDialog::getSaveFileName( this, tr( "New SpatiaLite Database File" ), - QDir::homePath(), - tr( "SpatiaLite" ) + " (*.sqlite *.db *.sqlite3 *.db3 *.s3db)", nullptr, QFileDialog::DontConfirmOverwrite ); - - if ( fileName.isEmpty() ) - return; - - if ( !fileName.endsWith( QLatin1String( ".sqlite" ), Qt::CaseInsensitive ) && !fileName.endsWith( QLatin1String( ".db" ), Qt::CaseInsensitive ) ) - { - fileName += QLatin1String( ".sqlite" ); - } - - mDatabaseComboBox->insertItem( 0, fileName ); - mDatabaseComboBox->setCurrentIndex( 0 ); - - createDb(); -} - QString QgsNewSpatialiteLayerDialog::selectedType() const { return mGeometryTypeBox->currentData( Qt::UserRole ).toString(); @@ -206,11 +177,14 @@ void QgsNewSpatialiteLayerDialog::mRemoveAttributeButton_clicked() void QgsNewSpatialiteLayerDialog::pbnFindSRID_clicked() { + const QgsDataSourceUri dbUri = mDatabaseComboBox->currentConnectionUri(); + const QString dbPath = dbUri.database(); + // first get list of supported SRID from the selected SpatiaLite database // to build filter for projection selector sqlite3_database_unique_ptr database; bool status = true; - int rc = database.open_v2( mDatabaseComboBox->currentText(), SQLITE_OPEN_READONLY, nullptr ); + int rc = database.open_v2( dbPath, SQLITE_OPEN_READONLY, nullptr ); if ( rc != SQLITE_OK ) { QMessageBox::warning( this, tr( "SpatiaLite Database" ), tr( "Unable to open the database" ) ); @@ -274,10 +248,15 @@ void QgsNewSpatialiteLayerDialog::selectionChanged() bool QgsNewSpatialiteLayerDialog::createDb() { - QString dbPath = mDatabaseComboBox->currentText(); + QString dbPath = QFileDialog::getSaveFileName( this, tr( "New SpatiaLite Database File" ), + QDir::homePath(), + tr( "SpatiaLite" ) + " (*.sqlite *.db *.sqlite3 *.db3 *.s3db)", nullptr, QFileDialog::DontConfirmOverwrite ); + if ( dbPath.isEmpty() ) return false; + QgsFileUtils::ensureFileNameHasExtension( dbPath, QStringList() << QStringLiteral( ".sqlite" ) << QLatin1String( ".db" ) << QLatin1String( ".sqlite3" ) + << QLatin1String( ".db3" ) << QLatin1String( ".s3db" ) ); QFile newDb( dbPath ); if ( newDb.exists() ) { @@ -316,24 +295,21 @@ bool QgsNewSpatialiteLayerDialog::createDb() if ( !fi.exists() ) { pbnFindSRID->setEnabled( false ); - return false; } - - QString key = "/SpatiaLite/connections/" + fi.fileName() + "/sqlitepath"; - - QgsSettings settings; - if ( !settings.contains( key ) ) + else { - settings.setValue( QStringLiteral( "SpatiaLite/connections/selected" ), fi.fileName() + tr( "@" ) + fi.canonicalFilePath() ); - settings.setValue( key, fi.canonicalFilePath() ); - - // Reload connections to refresh browser panel - QgisApp::instance()->reloadConnections(); + QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "spatialite" ) ) }; + std::unique_ptr conn( static_cast( md->createConnection( QStringLiteral( "dbname='%1'" ).arg( dbPath ), QVariantMap() ) ) ); + if ( conn ) + { + md->saveConnection( conn.get(), fi.fileName() ); + mDatabaseComboBox->setConnection( fi.fileName() ); + pbnFindSRID->setEnabled( true ); + return true; + } } - pbnFindSRID->setEnabled( true ); - - return true; + return false; } void QgsNewSpatialiteLayerDialog::buttonBox_accepted() @@ -349,6 +325,9 @@ void QgsNewSpatialiteLayerDialog::buttonBox_rejected() bool QgsNewSpatialiteLayerDialog::apply() { + const QgsDataSourceUri dbUri = mDatabaseComboBox->currentConnectionUri(); + const QString dbPath = dbUri.database(); + // Build up the sql statement for creating the table QString sql = QStringLiteral( "create table %1(" ).arg( quotedIdentifier( leLayerName->text() ) ); QString delim; @@ -369,16 +348,16 @@ bool QgsNewSpatialiteLayerDialog::apply() // complete the create table statement sql += ')'; - QgsDebugMsg( QStringLiteral( "Creating table in database %1" ).arg( mDatabaseComboBox->currentText() ) ); + QgsDebugMsg( QStringLiteral( "Creating table in database %1" ).arg( dbPath ) ); QgsDebugMsg( sql ); spatialite_database_unique_ptr database; - int rc = database.open( mDatabaseComboBox->currentText() ); + int rc = database.open( dbPath ); if ( rc != SQLITE_OK ) { QMessageBox::warning( this, tr( "SpatiaLite Database" ), - tr( "Unable to open the database: %1" ).arg( mDatabaseComboBox->currentText() ) ); + tr( "Unable to open the database: %1" ).arg( dbPath ) ); return false; } @@ -433,8 +412,10 @@ bool QgsNewSpatialiteLayerDialog::apply() } const QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() }; - QgsVectorLayer *layer = new QgsVectorLayer( QStringLiteral( "dbname='%1' table='%2'%3 sql=" ) - .arg( mDatabaseComboBox->currentText(), + const QString uri = QStringLiteral( "dbname='%1' table='%2'%3 sql=" ).arg( dbPath, leLayerName->text(), + mGeometryTypeBox->currentIndex() != 0 ? QStringLiteral( "(%1)" ).arg( leGeometryColumn->text() ) : QString() ); + QgsVectorLayer *layer = new QgsVectorLayer( QStringLiteral( "%1 table='%2'%3 sql=" ) + .arg( mDatabaseComboBox->currentConnectionUri(), leLayerName->text(), mGeometryTypeBox->currentIndex() != 0 ? QStringLiteral( "(%1)" ).arg( leGeometryColumn->text() ) : QString() ), leLayerName->text(), QStringLiteral( "spatialite" ), options ); diff --git a/src/app/qgsnewspatialitelayerdialog.h b/src/app/qgsnewspatialitelayerdialog.h index 4b63cbfbf72f..5594fded4b44 100644 --- a/src/app/qgsnewspatialitelayerdialog.h +++ b/src/app/qgsnewspatialitelayerdialog.h @@ -44,7 +44,6 @@ class APP_EXPORT QgsNewSpatialiteLayerDialog: public QDialog, private Ui::QgsNew void mGeometryTypeBox_currentIndexChanged( int index ); void mTypeBox_currentIndexChanged( int index ); void pbnFindSRID_clicked(); - void toolButtonNewDatabase_clicked(); void nameChanged( const QString & ); void selectionChanged(); void checkOk(); diff --git a/src/app/qgsoptions.cpp b/src/app/qgsoptions.cpp index c27f18e7ec3e..f80a4b1d4a59 100644 --- a/src/app/qgsoptions.cpp +++ b/src/app/qgsoptions.cpp @@ -1196,9 +1196,18 @@ QgsOptions::QgsOptions( QWidget *parent, Qt::WindowFlags fl, const QListremoveItemWidget( mOptionsListWidget->findItems( tr( "Acceleration" ), Qt::MatchExactly ).first() ); - mOptionsStackedWidget->removeWidget( mOptionsPageAcceleration ); - + mGPUEnableCheckBox->setChecked( false ); + for ( int idx = 0; idx < mOptionsPageAccelerationLayout->count(); ++idx ) + { + QWidget *item = mOptionsPageAccelerationLayout->itemAt( idx )->widget(); + if ( item ) + { + item->setEnabled( false ); + } + } + QLabel *noOpenCL = new QLabel( tr( "QGIS is compiled without OpenCL support. " + "GPU acceleration is not available." ), this ); + mOptionsPageAccelerationLayout->insertWidget( 0, noOpenCL ); #endif diff --git a/src/app/qgsprojectproperties.cpp b/src/app/qgsprojectproperties.cpp index fbaf5621e86f..0aa3158c58ad 100644 --- a/src/app/qgsprojectproperties.cpp +++ b/src/app/qgsprojectproperties.cpp @@ -70,6 +70,7 @@ #include "qgsbearingnumericformat.h" #include "qgsprojectdisplaysettings.h" #include "qgsprojecttimesettings.h" +#include "qgstemporalutils.h" //qt includes #include @@ -235,26 +236,12 @@ QgsProjectProperties::QgsProjectProperties( QgsMapCanvas *mapCanvas, QWidget *pa // Set time settings input QgsDateTimeRange range = QgsProject::instance()->timeSettings()->temporalRange(); - QLocale locale; - mStartDateTimeEdit->setDisplayFormat( - locale.dateTimeFormat( QLocale::ShortFormat ) ); - mEndDateTimeEdit->setDisplayFormat( - locale.dateTimeFormat( QLocale::ShortFormat ) ); + mStartDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); + mEndDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); - if ( range.begin().isValid() && range.end().isValid() ) - { - mStartDateTimeEdit->setDateTime( range.begin() ); - mEndDateTimeEdit->setDateTime( range.end() ); - - mCurrentRangeLabel->setText( tr( "Current selected range: %1 to %2" ).arg( - mStartDateTimeEdit->dateTime().toString( locale.dateTimeFormat() ), - mEndDateTimeEdit->dateTime().toString( locale.dateTimeFormat() ) ) ); - } - else - { - mCurrentRangeLabel->setText( tr( "Project range is not set" ) ); - } + mStartDateTimeEdit->setDateTime( range.begin() ); + mEndDateTimeEdit->setDateTime( range.end() ); mAutoTransaction->setChecked( QgsProject::instance()->autoTransaction() ); title( QgsProject::instance()->title() ); @@ -1045,7 +1032,7 @@ void QgsProjectProperties::apply() QgsProject::instance()->setTrustLayerMetadata( mTrustProjectCheckBox->isChecked() ); // Time settings - QDateTime start = mStartDateTimeEdit->dateTime(); + QDateTime start = mStartDateTimeEdit->dateTime(); QDateTime end = mEndDateTimeEdit->dateTime(); QgsProject::instance()->timeSettings()->setTemporalRange( QgsDateTimeRange( start, end ) ); @@ -2510,54 +2497,9 @@ void QgsProjectProperties::mButtonAddColor_clicked() void QgsProjectProperties::calculateFromLayersButton_clicked() { - const QMap &mapLayers = QgsProject::instance()->mapLayers(); - QgsMapLayer *currentLayer = nullptr; - - QDateTime minDate; - QDateTime maxDate; - - for ( QMap::const_iterator it = mapLayers.constBegin(); it != mapLayers.constEnd(); ++it ) - { - currentLayer = it.value(); - - if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() ) - continue; - - if ( currentLayer->type() == QgsMapLayerType::RasterLayer ) - { - QgsRasterLayer *rasterLayer = qobject_cast( currentLayer ); - - QgsDateTimeRange layerRange; - switch ( rasterLayer->temporalProperties()->mode() ) - { - case QgsRasterLayerTemporalProperties::ModeFixedTemporalRange: - layerRange = rasterLayer->temporalProperties()->fixedTemporalRange(); - break; - - case QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider: - layerRange = rasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange(); - break; - } - - if ( !minDate.isValid() || layerRange.begin() < minDate ) - minDate = layerRange.begin(); - if ( !maxDate.isValid() || layerRange.end() > maxDate ) - maxDate = layerRange.end(); - } - } - - if ( !minDate.isValid() || !maxDate.isValid() ) - return; - - mStartDateTimeEdit->setDateTime( minDate ); - mEndDateTimeEdit->setDateTime( maxDate ); - - QLocale locale; - mCurrentRangeLabel->setText( tr( "Current selected range: %1 to %2" ).arg( - mStartDateTimeEdit->dateTime().toString( - locale.dateTimeFormat( QLocale::ShortFormat ) ), - mEndDateTimeEdit->dateTime().toString( - locale.dateTimeFormat( QLocale::ShortFormat ) ) ) ); + const QgsDateTimeRange range = QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject::instance() ); + mStartDateTimeEdit->setDateTime( range.begin() ); + mEndDateTimeEdit->setDateTime( range.end() ); } QListWidgetItem *QgsProjectProperties::addScaleToScaleList( const QString &newScale ) diff --git a/src/app/qgssnappinglayertreemodel.cpp b/src/app/qgssnappinglayertreemodel.cpp index 1fb0e9732e37..a5a4af3d9cb7 100644 --- a/src/app/qgssnappinglayertreemodel.cpp +++ b/src/app/qgssnappinglayertreemodel.cpp @@ -27,6 +27,7 @@ #include "qgssnappingconfig.h" #include "qgsvectorlayer.h" #include "qgsapplication.h" +#include "qgsscalewidget.h" QgsSnappingLayerDelegate::QgsSnappingLayerDelegate( QgsMapCanvas *canvas, QObject *parent ) : QItemDelegate( parent ) @@ -100,6 +101,20 @@ QWidget *QgsSnappingLayerDelegate::createEditor( QWidget *parent, const QStyleOp return w; } + if ( index.column() == QgsSnappingLayerTreeModel::MinScaleColumn ) + { + QgsScaleWidget *minLimitSp = new QgsScaleWidget( parent ); + minLimitSp->setToolTip( tr( "Min Scale" ) ); + return minLimitSp; + } + + if ( index.column() == QgsSnappingLayerTreeModel::MaxScaleColumn ) + { + QgsScaleWidget *maxLimitSp = new QgsScaleWidget( parent ); + maxLimitSp->setToolTip( tr( "Max Scale" ) ); + return maxLimitSp; + } + return nullptr; } @@ -141,6 +156,22 @@ void QgsSnappingLayerDelegate::setEditorData( QWidget *editor, const QModelIndex w->setCurrentIndex( w->findData( units ) ); } } + else if ( index.column() == QgsSnappingLayerTreeModel::MinScaleColumn ) + { + QgsScaleWidget *w = qobject_cast( editor ); + if ( w ) + { + w->setScale( val.toDouble() ); + } + } + else if ( index.column() == QgsSnappingLayerTreeModel::MaxScaleColumn ) + { + QgsScaleWidget *w = qobject_cast( editor ); + if ( w ) + { + w->setScale( val.toDouble() ); + } + } } void QgsSnappingLayerDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const @@ -183,6 +214,22 @@ void QgsSnappingLayerDelegate::setModelData( QWidget *editor, QAbstractItemModel model->setData( index, w->value(), Qt::EditRole ); } } + else if ( index.column() == QgsSnappingLayerTreeModel::MinScaleColumn ) + { + QgsScaleWidget *w = qobject_cast( editor ); + if ( w ) + { + model->setData( index, w->scale(), Qt::EditRole ); + } + } + else if ( index.column() == QgsSnappingLayerTreeModel::MaxScaleColumn ) + { + QgsScaleWidget *w = qobject_cast( editor ); + if ( w ) + { + model->setData( index, w->scale(), Qt::EditRole ); + } + } } @@ -191,6 +238,7 @@ QgsSnappingLayerTreeModel::QgsSnappingLayerTreeModel( QgsProject *project, QgsMa , mProject( project ) , mCanvas( canvas ) , mIndividualLayerSettings( project->snappingConfig().individualLayerSettings() ) + , mEnableMinMaxColumn( project->snappingConfig().scaleDependencyMode() == QgsSnappingConfig::PerLayer ) { connect( project, &QgsProject::snappingConfigChanged, this, &QgsSnappingLayerTreeModel::onSnappingSettingsChanged ); @@ -200,7 +248,7 @@ QgsSnappingLayerTreeModel::QgsSnappingLayerTreeModel( QgsProject *project, QgsMa int QgsSnappingLayerTreeModel::columnCount( const QModelIndex &parent ) const { Q_UNUSED( parent ) - return 5; + return 7; } Qt::ItemFlags QgsSnappingLayerTreeModel::flags( const QModelIndex &idx ) const @@ -231,6 +279,13 @@ Qt::ItemFlags QgsSnappingLayerTreeModel::flags( const QModelIndex &idx ) const return Qt::NoItemFlags; } } + else if ( idx.column() == MaxScaleColumn || idx.column() == MinScaleColumn ) + { + if ( mEnableMinMaxColumn ) + { + return Qt::ItemIsEnabled | Qt::ItemIsEditable; + } + } else { return Qt::ItemIsEnabled | Qt::ItemIsEditable; @@ -290,6 +345,7 @@ void QgsSnappingLayerTreeModel::setFilterText( const QString &filterText ) void QgsSnappingLayerTreeModel::onSnappingSettingsChanged() { const QHash oldSettings = mIndividualLayerSettings; + bool wasMinMaxEnabled = mEnableMinMaxColumn; for ( auto it = oldSettings.constBegin(); it != oldSettings.constEnd(); ++it ) { @@ -313,17 +369,18 @@ void QgsSnappingLayerTreeModel::onSnappingSettingsChanged() } } - hasRowchanged( mLayerTreeModel->rootGroup(), oldSettings ); + mEnableMinMaxColumn = ( mProject->snappingConfig().scaleDependencyMode() == QgsSnappingConfig::PerLayer ); + hasRowchanged( mLayerTreeModel->rootGroup(), oldSettings, wasMinMaxEnabled != mEnableMinMaxColumn ); } -void QgsSnappingLayerTreeModel::hasRowchanged( QgsLayerTreeNode *node, const QHash &oldSettings ) +void QgsSnappingLayerTreeModel::hasRowchanged( QgsLayerTreeNode *node, const QHash &oldSettings, bool forceRefresh ) { if ( node->nodeType() == QgsLayerTreeNode::NodeGroup ) { const auto constChildren = node->children(); for ( QgsLayerTreeNode *child : constChildren ) { - hasRowchanged( child, oldSettings ); + hasRowchanged( child, oldSettings, forceRefresh ); } } else @@ -334,7 +391,7 @@ void QgsSnappingLayerTreeModel::hasRowchanged( QgsLayerTreeNode *node, const QHa { emit dataChanged( QModelIndex(), idx ); } - if ( oldSettings.value( vl ) != mProject->snappingConfig().individualLayerSettings().value( vl ) ) + if ( oldSettings.value( vl ) != mProject->snappingConfig().individualLayerSettings().value( vl ) || forceRefresh ) { mIndividualLayerSettings.insert( vl, mProject->snappingConfig().individualLayerSettings().value( vl ) ); emit dataChanged( idx, index( idx.row(), columnCount( idx ) - 1 ) ); @@ -400,6 +457,10 @@ QVariant QgsSnappingLayerTreeModel::headerData( int section, Qt::Orientation ori return tr( "Units" ); case 4: return tr( "Avoid overlap" ); + case 5: + return tr( "Min Scale" ); + case 6: + return tr( "Max Scale" ); default: return QVariant(); } @@ -579,6 +640,46 @@ QVariant QgsSnappingLayerTreeModel::data( const QModelIndex &idx, int role ) con } } } + + if ( idx.column() == MinScaleColumn ) + { + if ( role == Qt::DisplayRole ) + { + if ( ls.minimumScale() <= 0.0 ) + { + return QString( tr( "not set" ) ); + } + else + { + return QString::number( ls.minimumScale() ); + } + } + + if ( role == Qt::UserRole ) + { + return ls.minimumScale(); + } + } + + if ( idx.column() == MaxScaleColumn ) + { + if ( role == Qt::DisplayRole ) + { + if ( ls.maximumScale() <= 0.0 ) + { + return QString( tr( "not set" ) ); + } + else + { + return QString::number( ls.maximumScale() ); + } + } + + if ( role == Qt::UserRole ) + { + return ls.maximumScale(); + } + } } return QVariant(); @@ -712,5 +813,47 @@ bool QgsSnappingLayerTreeModel::setData( const QModelIndex &index, const QVarian } } + if ( index.column() == MinScaleColumn && role == Qt::EditRole ) + { + QgsVectorLayer *vl = vectorLayer( index ); + if ( vl ) + { + if ( !mIndividualLayerSettings.contains( vl ) ) + return false; + + QgsSnappingConfig::IndividualLayerSettings ls = mIndividualLayerSettings.value( vl ); + if ( !ls.valid() ) + return false; + + ls.setMinimumScale( value.toDouble() ); + QgsSnappingConfig config = mProject->snappingConfig(); + config.setIndividualLayerSettings( vl, ls ); + mProject->setSnappingConfig( config ); + emit dataChanged( index, index ); + return true; + } + } + + if ( index.column() == MaxScaleColumn && role == Qt::EditRole ) + { + QgsVectorLayer *vl = vectorLayer( index ); + if ( vl ) + { + if ( !mIndividualLayerSettings.contains( vl ) ) + return false; + + QgsSnappingConfig::IndividualLayerSettings ls = mIndividualLayerSettings.value( vl ); + if ( !ls.valid() ) + return false; + + ls.setMaximumScale( value.toDouble() ); + QgsSnappingConfig config = mProject->snappingConfig(); + config.setIndividualLayerSettings( vl, ls ); + mProject->setSnappingConfig( config ); + emit dataChanged( index, index ); + return true; + } + } + return false; } diff --git a/src/app/qgssnappinglayertreemodel.h b/src/app/qgssnappinglayertreemodel.h index 0c38e6abd219..9be2c3711416 100644 --- a/src/app/qgssnappinglayertreemodel.h +++ b/src/app/qgssnappinglayertreemodel.h @@ -56,7 +56,9 @@ class APP_EXPORT QgsSnappingLayerTreeModel : public QSortFilterProxyModel TypeColumn, ToleranceColumn, UnitsColumn, - AvoidIntersectionColumn + AvoidIntersectionColumn, + MinScaleColumn, + MaxScaleColumn }; QgsSnappingLayerTreeModel( QgsProject *project, QgsMapCanvas *canvas, QObject *parent = nullptr ); @@ -93,8 +95,9 @@ class APP_EXPORT QgsSnappingLayerTreeModel : public QSortFilterProxyModel QString mFilterText; QHash mIndividualLayerSettings; QgsLayerTreeModel *mLayerTreeModel = nullptr; + bool mEnableMinMaxColumn = true; - void hasRowchanged( QgsLayerTreeNode *node, const QHash &oldSettings ); + void hasRowchanged( QgsLayerTreeNode *node, const QHash &oldSettings, bool forceRefresh ); }; #endif // QGSSNAPPINGLAYERTREEVIEW_H diff --git a/src/app/qgssnappingwidget.cpp b/src/app/qgssnappingwidget.cpp index 0ec635399719..8775fa530f07 100644 --- a/src/app/qgssnappingwidget.cpp +++ b/src/app/qgssnappingwidget.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include "qgisapp.h" #include "qgsapplication.h" @@ -40,6 +41,7 @@ #include "qgssnappingwidget.h" #include "qgsunittypes.h" #include "qgssettings.h" +#include "qgsscalewidget.h" QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, QWidget *parent ) @@ -111,6 +113,25 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, mEnabledAction->setObjectName( QStringLiteral( "EnableSnappingAction" ) ); connect( mEnabledAction, &QAction::toggled, this, &QgsSnappingWidget::enableSnapping ); + // avoid intersection mode button + mAvoidIntersectionsModeButton = new QToolButton(); + mAvoidIntersectionsModeButton->setToolTip( tr( "When avoid overlap is enabled, digitized features will be clipped to not overlapped existing ones." ) ); + mAvoidIntersectionsModeButton->setPopupMode( QToolButton::InstantPopup ); + QMenu *avoidIntersectionsModeMenu = new QMenu( tr( "Set Avoid Overlap Mode" ), this ); + mAllowIntersectionsAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mActionAllowIntersections.svg" ) ), tr( "Allow Overlap" ), avoidIntersectionsModeMenu ); + mAvoidIntersectionsCurrentLayerAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mActionAvoidIntersectionsCurrentLayer.svg" ) ), tr( "Avoid Overlap on Active Layer" ), avoidIntersectionsModeMenu ); + mAvoidIntersectionsLayersAction = new QAction( QIcon( QgsApplication::getThemeIcon( "/mActionAvoidIntersectionsLayers.svg" ) ), tr( "Follow Advanced Configuration" ), avoidIntersectionsModeMenu ); + avoidIntersectionsModeMenu->addAction( mAllowIntersectionsAction ); + avoidIntersectionsModeMenu->addAction( mAvoidIntersectionsCurrentLayerAction ); + avoidIntersectionsModeMenu->addAction( mAvoidIntersectionsLayersAction ); + mAvoidIntersectionsModeButton->setMenu( avoidIntersectionsModeMenu ); + mAvoidIntersectionsModeButton->setObjectName( QStringLiteral( "AvoidIntersectionsModeButton" ) ); + if ( mDisplayMode == Widget ) + { + mAvoidIntersectionsModeButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); + } + connect( mAvoidIntersectionsModeButton, &QToolButton::triggered, this, &QgsSnappingWidget::avoidIntersectionsModeButtonTriggered ); + // mode button mModeButton = new QToolButton(); mModeButton->setToolTip( tr( "Snapping Mode" ) ); @@ -173,6 +194,35 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, mToleranceSpinBox->setObjectName( QStringLiteral( "SnappingToleranceSpinBox" ) ); connect( mToleranceSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsSnappingWidget::changeTolerance ); + mMinScaleWidget = new QgsScaleWidget(); + mMinScaleWidget->setToolTip( tr( "Minimum scale from which snapping is enabled" ) ); + mMinScaleWidget->setObjectName( QStringLiteral( "SnappingMinScaleSpinBox" ) ); + connect( mMinScaleWidget, &QgsScaleWidget::scaleChanged, this, &QgsSnappingWidget::changeMinScale ); + + mMaxScaleWidget = new QgsScaleWidget(); + mMaxScaleWidget->setToolTip( tr( "Maximum scale up to which snapping is enabled" ) ); + mMaxScaleWidget->setObjectName( QStringLiteral( "SnappingMaxScaleSpinBox" ) ); + connect( mMaxScaleWidget, &QgsScaleWidget::scaleChanged, this, &QgsSnappingWidget::changeMaxScale ); + + + mSnappingScaleModeButton = new QToolButton(); + mSnappingScaleModeButton->setToolTip( tr( "Snapping scale mode" ) ); + mSnappingScaleModeButton->setPopupMode( QToolButton::InstantPopup ); + QMenu *scaleModeMenu = new QMenu( tr( "Set snapping scale mode" ), this ); + mDefaultSnappingScaleAct = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingOnScale.svg" ) ), tr( "Disabled" ), scaleModeMenu ); + mDefaultSnappingScaleAct->setToolTip( tr( "Scale dependency disabled" ) ); + mGlobalSnappingScaleAct = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingOnScale.svg" ) ), tr( "Global" ), scaleModeMenu ); + mGlobalSnappingScaleAct->setToolTip( tr( "Scale dependency global" ) ); + mPerLayerSnappingScaleAct = new QAction( QIcon( QgsApplication::getThemeIcon( "/mIconSnappingOnScale.svg" ) ), tr( "Per layer" ), scaleModeMenu ); + mPerLayerSnappingScaleAct->setToolTip( tr( "Scale dependency per layer" ) ); + scaleModeMenu->addAction( mDefaultSnappingScaleAct ); + scaleModeMenu->addAction( mGlobalSnappingScaleAct ); + scaleModeMenu->addAction( mPerLayerSnappingScaleAct ); + mSnappingScaleModeButton->setMenu( scaleModeMenu ); + mSnappingScaleModeButton->setObjectName( QStringLiteral( "SnappingScaleModeButton" ) ); + mSnappingScaleModeButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); + connect( mSnappingScaleModeButton, &QToolButton::triggered, this, &QgsSnappingWidget::snappingScaleModeTriggered ); + // units mUnitsComboBox = new QComboBox(); mUnitsComboBox->addItem( tr( "px" ), QgsTolerance::Pixels ); @@ -261,7 +311,9 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, mTypeAction = tb->addWidget( mTypeButton ); mToleranceAction = tb->addWidget( mToleranceSpinBox ); mUnitAction = tb->addWidget( mUnitsComboBox ); + tb->addAction( mTopologicalEditingAction ); + mAvoidIntersectionsModeAction = tb->addWidget( mAvoidIntersectionsModeButton ); tb->addAction( mIntersectionSnappingAction ); tb->addAction( mEnableTracingAction ); } @@ -279,6 +331,10 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, layout->addWidget( mTypeButton ); layout->addWidget( mToleranceSpinBox ); layout->addWidget( mUnitsComboBox ); + mSnappingScaleModeButton->setDefaultAction( mDefaultSnappingScaleAct ); + layout->addWidget( mSnappingScaleModeButton ); + layout->addWidget( mMinScaleWidget ); + layout->addWidget( mMaxScaleWidget ); QToolButton *topoButton = new QToolButton(); topoButton->addAction( mTopologicalEditingAction ); @@ -286,6 +342,8 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, topoButton->setToolButtonStyle( Qt::ToolButtonTextBesideIcon ); layout->addWidget( topoButton ); + layout->addWidget( mAvoidIntersectionsModeButton ); + QToolButton *interButton = new QToolButton(); interButton->addAction( mIntersectionSnappingAction ); interButton->setDefaultAction( mIntersectionSnappingAction ); @@ -305,6 +363,7 @@ QgsSnappingWidget::QgsSnappingWidget( QgsProject *project, QgsMapCanvas *canvas, // connect settings changed and map units changed to properly update the widget connect( project, &QgsProject::snappingConfigChanged, this, &QgsSnappingWidget::projectSnapSettingsChanged ); connect( project, &QgsProject::topologicalEditingChanged, this, &QgsSnappingWidget::projectTopologicalEditingChanged ); + connect( project, &QgsProject::avoidIntersectionsModeChanged, this, &QgsSnappingWidget::projectAvoidIntersectionModeChanged ); connect( mCanvas, &QgsMapCanvas::destinationCrsChanged, this, &QgsSnappingWidget::updateToleranceDecimals ); // Slightly modify the config so the settings changed code doesn't early exit @@ -401,12 +460,61 @@ void QgsSnappingWidget::projectSnapSettingsChanged() mToleranceSpinBox->setValue( config.tolerance() ); } + if ( mMinScaleWidget->scale() != config.minimumScale() ) + { + mMinScaleWidget->setScale( config.minimumScale() ); + } + + if ( mMaxScaleWidget->scale() != config.maximumScale() ) + { + mMaxScaleWidget->setScale( config.maximumScale() ); + } + + if ( config.scaleDependencyMode() == QgsSnappingConfig::Disabled ) + { + mSnappingScaleModeButton->setDefaultAction( mDefaultSnappingScaleAct ); + } + else if ( config.scaleDependencyMode() == QgsSnappingConfig::Global ) + { + mSnappingScaleModeButton->setDefaultAction( mGlobalSnappingScaleAct ); + } + else if ( config.scaleDependencyMode() == QgsSnappingConfig::PerLayer ) + { + mSnappingScaleModeButton->setDefaultAction( mPerLayerSnappingScaleAct ); + } + if ( config.intersectionSnapping() != mIntersectionSnappingAction->isChecked() ) { mIntersectionSnappingAction->setChecked( config.intersectionSnapping() ); } toggleSnappingWidgets( config.enabled() ); + +} + +void QgsSnappingWidget::projectAvoidIntersectionModeChanged() +{ + switch ( mProject->avoidIntersectionsMode() ) + { + case QgsProject::AvoidIntersectionsMode::AllowIntersections: + mAvoidIntersectionsModeButton->setDefaultAction( mAllowIntersectionsAction ); + mAllowIntersectionsAction->setChecked( true ); + mAvoidIntersectionsCurrentLayerAction->setChecked( false ); + mAvoidIntersectionsLayersAction->setChecked( false ); + break; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + mAvoidIntersectionsModeButton->setDefaultAction( mAvoidIntersectionsCurrentLayerAction ); + mAllowIntersectionsAction->setChecked( false ); + mAvoidIntersectionsCurrentLayerAction->setChecked( true ); + mAvoidIntersectionsLayersAction->setChecked( false ); + break; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers: + mAvoidIntersectionsModeButton->setDefaultAction( mAvoidIntersectionsLayersAction ); + mAllowIntersectionsAction->setChecked( false ); + mAvoidIntersectionsCurrentLayerAction->setChecked( false ); + mAvoidIntersectionsLayersAction->setChecked( true ); + break; + } } void QgsSnappingWidget::projectTopologicalEditingChanged() @@ -429,7 +537,16 @@ void QgsSnappingWidget::toggleSnappingWidgets( bool enabled ) mModeButton->setEnabled( enabled ); mTypeButton->setEnabled( enabled ); mToleranceSpinBox->setEnabled( enabled ); + mSnappingScaleModeButton->setEnabled( enabled ); + mMinScaleWidget->setEnabled( enabled && mConfig.scaleDependencyMode() == QgsSnappingConfig::Global ); + mMaxScaleWidget->setEnabled( enabled && mConfig.scaleDependencyMode() == QgsSnappingConfig::Global ); mUnitsComboBox->setEnabled( enabled ); + + if ( mEditAdvancedConfigAction ) + { + mEditAdvancedConfigAction->setEnabled( enabled ); + } + if ( mAdvancedConfigWidget ) { mAdvancedConfigWidget->setEnabled( enabled ); @@ -444,6 +561,18 @@ void QgsSnappingWidget::changeTolerance( double tolerance ) mProject->setSnappingConfig( mConfig ); } +void QgsSnappingWidget::changeMinScale( double minScale ) +{ + mConfig.setMinimumScale( minScale ); + mProject->setSnappingConfig( mConfig ); +} + +void QgsSnappingWidget::changeMaxScale( double maxScale ) +{ + mConfig.setMaximumScale( maxScale ); + mProject->setSnappingConfig( mConfig ); +} + void QgsSnappingWidget::changeUnit( int idx ) { QgsTolerance::UnitType unit = static_cast( mUnitsComboBox->itemData( idx ).toInt() ); @@ -470,6 +599,33 @@ void QgsSnappingWidget::onSnappingTreeLayersChanged() mLayerTreeView->resizeColumnToContents( 0 ); } +void QgsSnappingWidget::avoidIntersectionsModeButtonTriggered( QAction *action ) +{ + if ( action != mAllowIntersectionsAction && + action != mAvoidIntersectionsCurrentLayerAction && + action != mAvoidIntersectionsLayersAction ) + { + return; + } + + if ( action != mAvoidIntersectionsModeButton->defaultAction() ) + { + mAvoidIntersectionsModeButton->setDefaultAction( action ); + if ( action == mAllowIntersectionsAction ) + { + mProject->setAvoidIntersectionsMode( QgsProject::AvoidIntersectionsMode::AllowIntersections ); + } + else if ( action == mAvoidIntersectionsCurrentLayerAction ) + { + mProject->setAvoidIntersectionsMode( QgsProject::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer ); + } + else if ( action == mAvoidIntersectionsLayersAction ) + { + mProject->setAvoidIntersectionsMode( QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers ); + } + } +} + void QgsSnappingWidget::modeButtonTriggered( QAction *action ) { if ( action != mAllLayersAction && @@ -529,6 +685,30 @@ void QgsSnappingWidget::typeButtonTriggered( QAction *action ) mProject->setSnappingConfig( mConfig ); } +void QgsSnappingWidget::snappingScaleModeTriggered( QAction *action ) +{ + mSnappingScaleModeButton->setDefaultAction( action ); + QgsSnappingConfig::ScaleDependencyMode mode = mConfig.scaleDependencyMode(); + + if ( action == mDefaultSnappingScaleAct ) + { + mode = QgsSnappingConfig::Disabled; + } + else if ( action == mGlobalSnappingScaleAct ) + { + mode = QgsSnappingConfig::Global; + } + else if ( action == mPerLayerSnappingScaleAct ) + { + mode = QgsSnappingConfig::PerLayer; + } + + mMinScaleWidget->setEnabled( mode == QgsSnappingConfig::Global ); + mMaxScaleWidget->setEnabled( mode == QgsSnappingConfig::Global ); + mConfig.setScaleDependencyMode( mode ); + mProject->setSnappingConfig( mConfig ); +} + void QgsSnappingWidget::updateToleranceDecimals() { if ( mConfig.units() == QgsTolerance::Pixels ) @@ -570,6 +750,9 @@ void QgsSnappingWidget::modeChanged() { mAdvancedConfigWidget->setVisible( advanced ); } + mSnappingScaleModeButton->setVisible( advanced ); + mMinScaleWidget->setVisible( advanced ); + mMaxScaleWidget->setVisible( advanced ); } } diff --git a/src/app/qgssnappingwidget.h b/src/app/qgssnappingwidget.h index d2e1ea0ef1c4..42081e6edf6e 100644 --- a/src/app/qgssnappingwidget.h +++ b/src/app/qgssnappingwidget.h @@ -23,6 +23,7 @@ class QDoubleSpinBox; class QFont; class QToolButton; class QTreeView; +class QCheckBox; class QgsDoubleSpinBox; class QgsFloatingWidget; @@ -31,6 +32,7 @@ class QgsLayerTreeNode; class QgsLayerTreeView; class QgsMapCanvas; class QgsProject; +class QgsScaleWidget; #include "qgssnappingconfig.h" @@ -91,6 +93,8 @@ class APP_EXPORT QgsSnappingWidget : public QWidget private slots: void projectSnapSettingsChanged(); + void projectAvoidIntersectionModeChanged(); + void projectTopologicalEditingChanged(); void enableSnapping( bool checked ); @@ -100,6 +104,10 @@ class APP_EXPORT QgsSnappingWidget : public QWidget void changeTolerance( double tolerance ); + void changeMinScale( double minScale ); + + void changeMaxScale( double maxScale ); + void changeUnit( int idx ); void enableTopologicalEditing( bool enabled ); @@ -107,7 +115,9 @@ class APP_EXPORT QgsSnappingWidget : public QWidget void enableIntersectionSnapping( bool enabled ); void modeButtonTriggered( QAction *action ); + void avoidIntersectionsModeButtonTriggered( QAction *action ); void typeButtonTriggered( QAction *action ); + void snappingScaleModeTriggered( QAction *action ); //! number of decimals of the tolerance spin box depends on map units void updateToleranceDecimals(); @@ -130,6 +140,11 @@ class APP_EXPORT QgsSnappingWidget : public QWidget QgsMapCanvas *mCanvas = nullptr; QAction *mEnabledAction = nullptr; + QToolButton *mAvoidIntersectionsModeButton = nullptr; + QAction *mAvoidIntersectionsModeAction = nullptr; // hide widget does not work on toolbar, action needed + QAction *mAllowIntersectionsAction = nullptr; + QAction *mAvoidIntersectionsCurrentLayerAction = nullptr; + QAction *mAvoidIntersectionsLayersAction = nullptr; QToolButton *mModeButton = nullptr; QAction *mModeAction = nullptr; // hide widget does not work on toolbar, action needed QAction *mAllLayersAction = nullptr; @@ -144,7 +159,13 @@ class APP_EXPORT QgsSnappingWidget : public QWidget QAction *mCentroidAction = nullptr; QAction *mMiddleAction = nullptr; QDoubleSpinBox *mToleranceSpinBox = nullptr; + QgsScaleWidget *mMinScaleWidget = nullptr; + QgsScaleWidget *mMaxScaleWidget = nullptr; QAction *mToleranceAction = nullptr; // hide widget does not work on toolbar, action needed + QToolButton *mSnappingScaleModeButton = nullptr; + QAction *mDefaultSnappingScaleAct = nullptr; + QAction *mGlobalSnappingScaleAct = nullptr; + QAction *mPerLayerSnappingScaleAct = nullptr; QComboBox *mUnitsComboBox = nullptr; QAction *mUnitAction = nullptr; // hide widget does not work on toolbar, action needed QAction *mTopologicalEditingAction = nullptr; diff --git a/src/app/qgsstatisticalsummarydockwidget.cpp b/src/app/qgsstatisticalsummarydockwidget.cpp index 2afdace646ba..70e990848afc 100644 --- a/src/app/qgsstatisticalsummarydockwidget.cpp +++ b/src/app/qgsstatisticalsummarydockwidget.cpp @@ -34,7 +34,7 @@ typedef QList< QgsStatisticalSummary::Statistic > StatsList; typedef QList< QgsStringStatisticalSummary::Statistic > StringStatsList; typedef QList< QgsDateTimeStatisticalSummary::Statistic > DateTimeStatsList; Q_GLOBAL_STATIC_WITH_ARGS( StatsList, sDisplayStats, ( {QgsStatisticalSummary::Count, QgsStatisticalSummary::Sum, QgsStatisticalSummary::Mean, QgsStatisticalSummary::Median, QgsStatisticalSummary::StDev, QgsStatisticalSummary::StDevSample, QgsStatisticalSummary::Min, QgsStatisticalSummary::Max, QgsStatisticalSummary::Range, QgsStatisticalSummary::Minority, QgsStatisticalSummary::Majority, QgsStatisticalSummary::Variety, QgsStatisticalSummary::FirstQuartile, QgsStatisticalSummary::ThirdQuartile, QgsStatisticalSummary::InterQuartileRange} ) ) -Q_GLOBAL_STATIC_WITH_ARGS( StringStatsList, sDisplayStringStats, ( {QgsStringStatisticalSummary::Count, QgsStringStatisticalSummary::CountDistinct, QgsStringStatisticalSummary::CountMissing, QgsStringStatisticalSummary::Min, QgsStringStatisticalSummary::Max, QgsStringStatisticalSummary::MinimumLength, QgsStringStatisticalSummary::MaximumLength} ) ) +Q_GLOBAL_STATIC_WITH_ARGS( StringStatsList, sDisplayStringStats, ( {QgsStringStatisticalSummary::Count, QgsStringStatisticalSummary::CountDistinct, QgsStringStatisticalSummary::CountMissing, QgsStringStatisticalSummary::Min, QgsStringStatisticalSummary::Max, QgsStringStatisticalSummary::Minority, QgsStringStatisticalSummary::Majority, QgsStringStatisticalSummary::MinimumLength, QgsStringStatisticalSummary::MaximumLength, QgsStringStatisticalSummary::MeanLength} ) ) Q_GLOBAL_STATIC_WITH_ARGS( DateTimeStatsList, sDisplayDateTimeStats, ( {QgsDateTimeStatisticalSummary::Count, QgsDateTimeStatisticalSummary::CountDistinct, QgsDateTimeStatisticalSummary::CountMissing, QgsDateTimeStatisticalSummary::Min, QgsDateTimeStatisticalSummary::Max, QgsDateTimeStatisticalSummary::Range} ) ) #define MISSING_VALUES -1 diff --git a/src/app/qgstemporalcontrollerdockwidget.cpp b/src/app/qgstemporalcontrollerdockwidget.cpp new file mode 100644 index 000000000000..faceeb25e135 --- /dev/null +++ b/src/app/qgstemporalcontrollerdockwidget.cpp @@ -0,0 +1,37 @@ +/*************************************************************************** + qgstemporalcontrollerdockwidget.cpp + ------------------------------ + begin : February 2020 + copyright : (C) 2020 by Samweli Mwakisambwe + email : samweli at kartoza 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 "qgstemporalcontrollerdockwidget.h" +#include "qgstemporalcontrollerwidget.h" +#include "qgspanelwidgetstack.h" + +QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString &name, QWidget *parent ) + : QgsDockWidget( parent ) +{ + setWindowTitle( name ); + mControllerWidget = new QgsTemporalControllerWidget(); + mControllerWidget->setDockMode( true ); + + QgsPanelWidgetStack *stack = new QgsPanelWidgetStack(); + stack->setMainPanel( mControllerWidget ); + setWidget( stack ); +} + +QgsTemporalController *QgsTemporalControllerDockWidget::temporalController() +{ + return mControllerWidget->temporalController(); +} diff --git a/src/gui/qgstemporalmapsettingsdialog.h b/src/app/qgstemporalcontrollerdockwidget.h similarity index 51% rename from src/gui/qgstemporalmapsettingsdialog.h rename to src/app/qgstemporalcontrollerdockwidget.h index 220ed75783e9..df692e0e3bb8 100644 --- a/src/gui/qgstemporalmapsettingsdialog.h +++ b/src/app/qgstemporalcontrollerdockwidget.h @@ -1,7 +1,7 @@ /*************************************************************************** - qgstemporalmapsettingsdialog.h + qgstemporalcontrollerdockwidget.h --------------- - begin : March 2020 + begin : February 2020 copyright : (C) 2020 by Samweli Mwakisambwe email : samweli at kartoza dot com ***************************************************************************/ @@ -15,39 +15,43 @@ * * ***************************************************************************/ -#ifndef QGSTEMPORALMAPSETTINGSDIALOG_H -#define QGSTEMPORALMAPSETTINGSDIALOG_H +#ifndef QGSTEMPORALCONTROLLERDOCKWIDGET_H +#define QGSTEMPORALCONTROLLERDOCKWIDGET_H -#include "ui_qgstemporalmapsettingsdialogbase.h" -#include "qgis_gui.h" +#include "qgsdockwidget.h" +#include "qgis_app.h" -class QgsTemporalMapSettingsWidget; +class QgsTemporalControllerWidget; +class QgsTemporalController; -#define SIP_NO_FILE - -///@cond PRIVATE - -class GUI_EXPORT QgsTemporalMapSettingsDialog : public QDialog, private Ui::QgsTemporalMapSettingsDialogBase +/** + * \ingroup app + * The QgsTemporalControllerDockWidget class + * + * \since QGIS 3.14 + */ +class APP_EXPORT QgsTemporalControllerDockWidget : public QgsDockWidget { Q_OBJECT public: /** - * Constructor for QgsTemporalMapSettingsDialog, with the specified \a parent widget. + * Constructor for QgsTemporalControllerDockWidget, with the specified \a parent widget. */ - QgsTemporalMapSettingsDialog( QWidget *parent = nullptr, Qt::WindowFlags flags = Qt::WindowFlags() ); + QgsTemporalControllerDockWidget( const QString &name, QWidget *parent SIP_TRANSFERTHIS = nullptr ); /** - * Returns the widget used in getting settings input from user. + * Returns the temporal controller object used by this object in navigation. + * + * The dock widget retains ownership of the returned object. */ - QgsTemporalMapSettingsWidget *mapSettingsWidget(); + QgsTemporalController *temporalController(); private: - //! Widget for handling temporal map settings - QgsTemporalMapSettingsWidget *mTemporalMapSettingsWidget = nullptr; -}; + QgsTemporalControllerWidget *mControllerWidget = nullptr; -///@endcond -#endif // QGSTEMPORALMAPSETTINGSDIALOG_H +}; + +#endif // QGSTEMPORALCONTROLLERDOCKWIDGET_H diff --git a/src/app/vertextool/qgsvertexeditor.cpp b/src/app/vertextool/qgsvertexeditor.cpp index be3fcc11164b..6e15f25a4a21 100644 --- a/src/app/vertextool/qgsvertexeditor.cpp +++ b/src/app/vertextool/qgsvertexeditor.cpp @@ -433,9 +433,10 @@ void QgsVertexEditor::updateVertexSelection( const QItemSelection &, const QItem try { QgsRectangle transformedBbox = t.transform( *bbox ); - QgsRectangle canvasExtent = mCanvas->mapSettings().extent(); + const QgsRectangle canvasExtent = mCanvas->mapSettings().visibleExtent(); transformedBbox.combineExtentWith( canvasExtent ); - mCanvas->setExtent( transformedBbox ); + mCanvas->setExtent( transformedBbox, true ); + mCanvas->refresh(); } catch ( QgsCsException &cse ) { diff --git a/src/app/vertextool/qgsvertextool.cpp b/src/app/vertextool/qgsvertextool.cpp index 52169dcbeefa..407cf6bb97af 100644 --- a/src/app/vertextool/qgsvertextool.cpp +++ b/src/app/vertextool/qgsvertextool.cpp @@ -769,7 +769,7 @@ QgsPointLocator::Match QgsVertexTool::snapToEditableLayer( QgsMapMouseEvent *e ) continue; config.setIndividualLayerSettings( vlayer, QgsSnappingConfig::IndividualLayerSettings( - vlayer == currentVlayer, static_cast( QgsSnappingConfig::VertexFlag | QgsSnappingConfig::SegmentFlag ), tol, QgsTolerance::ProjectUnits ) ); + vlayer == currentVlayer, static_cast( QgsSnappingConfig::VertexFlag | QgsSnappingConfig::SegmentFlag ), tol, QgsTolerance::ProjectUnits, 0.0, 0.0 ) ); } snapUtils->setConfig( config ); @@ -796,7 +796,7 @@ QgsPointLocator::Match QgsVertexTool::snapToEditableLayer( QgsMapMouseEvent *e ) continue; config.setIndividualLayerSettings( vlayer, QgsSnappingConfig::IndividualLayerSettings( - vlayer->isEditable(), static_cast( QgsSnappingConfig::VertexFlag | QgsSnappingConfig::SegmentFlag ), tol, QgsTolerance::ProjectUnits ) ); + vlayer->isEditable(), static_cast( QgsSnappingConfig::VertexFlag | QgsSnappingConfig::SegmentFlag ), tol, QgsTolerance::ProjectUnits, 0.0, 0.0 ) ); } snapUtils->setConfig( config ); diff --git a/src/auth/oauth2/qgsauthoauth2edit.cpp b/src/auth/oauth2/qgsauthoauth2edit.cpp index ec5a7cdf6a9b..d2a6eacedce5 100644 --- a/src/auth/oauth2/qgsauthoauth2edit.cpp +++ b/src/auth/oauth2/qgsauthoauth2edit.cpp @@ -993,7 +993,7 @@ void QgsAuthOAuth2Edit::parseSoftwareStatement( const QString &path ) if ( !grantTypes.isEmpty( ) ) { QString grantType = grantTypes[0]; - if ( grantType == QLatin1Literal( "authorization_code" ) ) + if ( grantType == QLatin1String( "authorization_code" ) ) { updateGrantFlow( static_cast( QgsAuthOAuth2Config::AuthCode ) ); } @@ -1120,10 +1120,10 @@ void QgsAuthOAuth2Edit::registerSoftStatement( const QString ®istrationUrl ) QByteArray json = QJsonWrapper::toJson( QVariant( mSoftwareStatement ), &res, &errStr ); QNetworkRequest registerRequest( regUrl ); QgsSetRequestInitiatorClass( registerRequest, QStringLiteral( "QgsAuthOAuth2Edit" ) ); - registerRequest.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1Literal( "application/json" ) ); + registerRequest.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/json" ) ); QNetworkReply *registerReply; // For testability: use GET if protocol is file:// - if ( regUrl.scheme() == QLatin1Literal( "file" ) ) + if ( regUrl.scheme() == QLatin1String( "file" ) ) registerReply = QgsNetworkAccessManager::instance()->get( registerRequest ); else registerReply = QgsNetworkAccessManager::instance()->post( registerRequest, json ); diff --git a/src/auth/oauth2/qgso2.cpp b/src/auth/oauth2/qgso2.cpp index cc7131a7cdb8..3b01419add01 100644 --- a/src/auth/oauth2/qgso2.cpp +++ b/src/auth/oauth2/qgso2.cpp @@ -233,7 +233,7 @@ void QgsO2::link() QUrl url( tokenUrl_ ); QNetworkRequest tokenRequest( url ); QgsSetRequestInitiatorClass( tokenRequest, QStringLiteral( "QgsO2" ) ); - tokenRequest.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1Literal( "application/x-www-form-urlencoded" ) ); + tokenRequest.setHeader( QNetworkRequest::ContentTypeHeader, QLatin1String( "application/x-www-form-urlencoded" ) ); QNetworkReply *tokenReply = getManager()->post( tokenRequest, payload ); connect( tokenReply, SIGNAL( finished() ), this, SLOT( onTokenReplyFinished() ), Qt::QueuedConnection ); diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index d11d2b650d97..987144aa8dda 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -97,6 +97,7 @@ SET(QGIS_CORE_SRCS layertree/qgslayertreeregistrybridge.cpp layertree/qgslayertreeutils.cpp layertree/qgslayertree.cpp + layertree/qgslegendpatchshape.cpp metadata/qgsabstractmetadatabase.cpp metadata/qgslayermetadata.cpp @@ -108,6 +109,7 @@ SET(QGIS_CORE_SRCS numericformats/qgsbearingnumericformat.cpp numericformats/qgscurrencynumericformat.cpp numericformats/qgsfallbacknumericformat.cpp + numericformats/qgsfractionnumericformat.cpp numericformats/qgsnumericformat.cpp numericformats/qgsnumericformatregistry.cpp numericformats/qgspercentagenumericformat.cpp @@ -154,6 +156,7 @@ SET(QGIS_CORE_SRCS processing/models/qgsprocessingmodelchildparametersource.cpp processing/models/qgsprocessingmodelcomment.cpp processing/models/qgsprocessingmodelcomponent.cpp + processing/models/qgsprocessingmodelgroupbox.cpp processing/models/qgsprocessingmodelparameter.cpp processing/models/qgsprocessingmodeloutput.cpp @@ -181,10 +184,13 @@ SET(QGIS_CORE_SRCS providers/ogr/qgsogrtransaction.cpp scalebar/qgsdoubleboxscalebarrenderer.cpp + scalebar/qgshollowscalebarrenderer.cpp scalebar/qgsnumericscalebarrenderer.cpp scalebar/qgsscalebarrenderer.cpp + scalebar/qgsscalebarrendererregistry.cpp scalebar/qgsscalebarsettings.cpp scalebar/qgssingleboxscalebarrenderer.cpp + scalebar/qgssteppedlinescalebarrenderer.cpp scalebar/qgsticksscalebarrenderer.cpp qgis.cpp @@ -219,6 +225,7 @@ SET(QGIS_CORE_SRCS qgscolorscheme.cpp qgscolorschemeregistry.cpp qgsconditionalstyle.cpp + qgsconnectionregistry.cpp qgscoordinateformatter.cpp qgscoordinatereferencesystem.cpp qgscoordinatetransform.cpp @@ -308,6 +315,7 @@ SET(QGIS_CORE_SRCS qgsmapunitscale.cpp qgsmargins.cpp qgsmaskidprovider.cpp + qgsmbtilesreader.cpp qgsmessagelog.cpp qgsmessageoutput.cpp qgsmimedatautils.cpp @@ -353,6 +361,7 @@ SET(QGIS_CORE_SRCS qgsproviderconnectionmodel.cpp qgsprovidermetadata.cpp qgsproviderregistry.cpp + qgsproxyfeaturesink.cpp qgsproxyprogresstask.cpp qgspythonrunner.cpp qgsreadwritecontext.cpp @@ -361,6 +370,7 @@ SET(QGIS_CORE_SRCS qgsrelationcontext.cpp qgsweakrelation.cpp qgsrelationmanager.cpp + qgsremappingproxyfeaturesink.cpp qgsrenderchecker.cpp qgsrendercontext.cpp qgsrunprocess.cpp @@ -387,9 +397,11 @@ SET(QGIS_CORE_SRCS qgstemporalnavigationobject.cpp qgstemporalproperty.cpp qgstemporalrangeobject.cpp + qgstemporalutils.cpp qgstessellator.cpp qgstextrenderer.cpp qgstilecache.cpp + qgstiles.cpp qgstolerance.cpp qgstracer.cpp qgstranslationcontext.cpp @@ -458,6 +470,7 @@ SET(QGIS_CORE_SRCS layout/qgslayoutitemmapgrid.cpp layout/qgslayoutitemmapitem.cpp layout/qgslayoutitemmapoverview.cpp + layout/qgslayoutitemmarker.cpp layout/qgslayoutitemnodeitem.cpp layout/qgslayoutitempage.cpp layout/qgslayoutitempicture.cpp @@ -474,6 +487,7 @@ SET(QGIS_CORE_SRCS layout/qgslayoutmodel.cpp layout/qgslayoutmultiframe.cpp layout/qgslayoutmultiframeundocommand.cpp + layout/qgslayoutnortharrowhandler.cpp layout/qgslayoutobject.cpp layout/qgslayoutpagecollection.cpp layout/qgslayoutrendercontext.cpp @@ -516,6 +530,7 @@ SET(QGIS_CORE_SRCS raster/qgsraster.cpp raster/qgsrasterblock.cpp raster/qgsrasterchecker.cpp + raster/qgsrastercontourrenderer.cpp raster/qgsrasterdataprovider.cpp raster/qgsrasterdataprovidertemporalcapabilities.cpp raster/qgsrasterfilewritertask.cpp @@ -552,6 +567,7 @@ SET(QGIS_CORE_SRCS mesh/qgsmesh3daveraging.cpp mesh/qgsmeshdataprovider.cpp + mesh/qgsmeshdataset.cpp mesh/qgsmeshlayer.cpp mesh/qgsmeshlayerinterpolator.cpp mesh/qgsmeshlayerrenderer.cpp @@ -563,6 +579,8 @@ SET(QGIS_CORE_SRCS mesh/qgsmeshtimesettings.cpp mesh/qgsmeshtracerenderer.cpp mesh/qgsmeshsimplificationsettings.cpp + mesh/qgsmeshlayertemporalproperties.cpp + mesh/qgsmeshdataprovidertemporalcapabilities.cpp labeling/qgslabelfeature.cpp labeling/qgslabelingengine.cpp @@ -630,6 +648,16 @@ SET(QGIS_CORE_SRCS validity/qgsvaliditycheckcontext.cpp validity/qgsvaliditycheckregistry.cpp + vectortile/qgsvectortilebasicrenderer.cpp + vectortile/qgsvectortileconnection.cpp + vectortile/qgsvectortiledataitems.cpp + vectortile/qgsvectortilelayer.cpp + vectortile/qgsvectortilelayerrenderer.cpp + vectortile/qgsvectortileloader.cpp + vectortile/qgsvectortilemvtdecoder.cpp + vectortile/qgsvectortileprovidermetadata.cpp + vectortile/qgsvectortileutils.cpp + ${CMAKE_CURRENT_BINARY_DIR}/qgsexpression_texts.cpp qgsuserprofile.cpp @@ -725,6 +753,7 @@ SET(QGIS_CORE_HDRS qgscolorschemeregistry.h qgsconditionalstyle.h qgsconnectionpool.h + qgsconnectionregistry.h qgscoordinateformatter.h qgscoordinatereferencesystem.h qgscoordinatetransform.h @@ -775,6 +804,7 @@ SET(QGIS_CORE_HDRS qgsfieldproxymodel.h qgsfields.h qgsfiledownloader.h + qgsfilefiltergenerator.h qgsfileutils.h qgsfontutils.h qgsgdalutils.h @@ -826,6 +856,7 @@ SET(QGIS_CORE_HDRS qgsmapunitscale.h qgsmargins.h qgsmaskidprovider.h + qgsmbtilesreader.h qgsmessagelog.h qgsmessageoutput.h qgsmimedatautils.h @@ -873,6 +904,7 @@ SET(QGIS_CORE_HDRS qgsproviderconnectionmodel.h qgsprovidermetadata.h qgsproviderregistry.h + qgsproxyfeaturesink.h qgsproxyprogresstask.h qgspythonrunner.h qgsrange.h @@ -880,6 +912,7 @@ SET(QGIS_CORE_HDRS qgsreadwritelocker.h qgsrelation.h qgsrelationcontext.h + qgsremappingproxyfeaturesink.h qgsweakrelation.h qgsrelationmanager.h qgsrenderchecker.h @@ -911,11 +944,13 @@ SET(QGIS_CORE_HDRS qgstemporalnavigationobject.h qgstemporalproperty.h qgstemporalrangeobject.h + qgstemporalutils.h qgstessellator.h qgstestutils.h qgstextrenderer.h qgsthreadingutils.h qgstilecache.h + qgstiles.h qgstolerance.h qgstracer.h qgstrackedvectorlayertools.h @@ -1086,6 +1121,8 @@ SET(QGIS_CORE_HDRS layertree/qgslayertreenode.h layertree/qgslayertreeregistrybridge.h layertree/qgslayertreeutils.h + layertree/qgslegendpatchshape.h + layout/qgsabstractlayoutiterator.h layout/qgsabstractreportsection.h layout/qgscompositionconverter.h @@ -1110,6 +1147,7 @@ SET(QGIS_CORE_HDRS layout/qgslayoutitemmapgrid.h layout/qgslayoutitemmapitem.h layout/qgslayoutitemmapoverview.h + layout/qgslayoutitemmarker.h layout/qgslayoutitemnodeitem.h layout/qgslayoutitempage.h layout/qgslayoutitempicture.h @@ -1126,6 +1164,7 @@ SET(QGIS_CORE_HDRS layout/qgslayoutmodel.h layout/qgslayoutmultiframe.h layout/qgslayoutmultiframeundocommand.h + layout/qgslayoutnortharrowhandler.h layout/qgslayoutobject.h layout/qgslayoutpagecollection.h layout/qgslayoutpoint.h @@ -1154,6 +1193,7 @@ SET(QGIS_CORE_HDRS mesh/qgsmesh3daveraging.h mesh/qgsmeshdataprovider.h + mesh/qgsmeshdataset.h mesh/qgsmeshlayer.h mesh/qgsmeshlayerinterpolator.h mesh/qgsmeshlayerrenderer.h @@ -1165,6 +1205,8 @@ SET(QGIS_CORE_HDRS mesh/qgstriangularmesh.h mesh/qgsmeshtracerenderer.h mesh/qgsmeshsimplificationsettings.h + mesh/qgsmeshlayertemporalproperties.h + mesh/qgsmeshdataprovidertemporalcapabilities.h metadata/qgsabstractmetadatabase.h metadata/qgslayermetadata.h @@ -1176,6 +1218,7 @@ SET(QGIS_CORE_HDRS numericformats/qgsbearingnumericformat.h numericformats/qgscurrencynumericformat.h numericformats/qgsfallbacknumericformat.h + numericformats/qgsfractionnumericformat.h numericformats/qgsnumericformat.h numericformats/qgsnumericformatregistry.h numericformats/qgspercentagenumericformat.h @@ -1186,6 +1229,7 @@ SET(QGIS_CORE_HDRS processing/models/qgsprocessingmodelchildparametersource.h processing/models/qgsprocessingmodelcomment.h processing/models/qgsprocessingmodelcomponent.h + processing/models/qgsprocessingmodelgroupbox.h processing/models/qgsprocessingmodeloutput.h processing/models/qgsprocessingmodelparameter.h processing/qgsprocessing.h @@ -1234,6 +1278,7 @@ SET(QGIS_CORE_HDRS raster/qgsrasterbandstats.h raster/qgsrasterblock.h raster/qgsrasterchecker.h + raster/qgsrastercontourrenderer.h raster/qgsrasterdataprovider.h raster/qgsrasterdataprovidertemporalcapabilities.h raster/qgsrasterdrawer.h @@ -1265,10 +1310,13 @@ SET(QGIS_CORE_HDRS raster/qgssinglebandpseudocolorrenderer.h scalebar/qgsdoubleboxscalebarrenderer.h + scalebar/qgshollowscalebarrenderer.h scalebar/qgsnumericscalebarrenderer.h scalebar/qgsscalebarrenderer.h + scalebar/qgsscalebarrendererregistry.h scalebar/qgsscalebarsettings.h scalebar/qgssingleboxscalebarrenderer.h + scalebar/qgssteppedlinescalebarrenderer.h scalebar/qgsticksscalebarrenderer.h symbology/qgs25drenderer.h @@ -1308,6 +1356,17 @@ SET(QGIS_CORE_HDRS validity/qgsabstractvaliditycheck.h validity/qgsvaliditycheckcontext.h validity/qgsvaliditycheckregistry.h + + vectortile/qgsvectortilebasicrenderer.h + vectortile/qgsvectortileconnection.h + vectortile/qgsvectortiledataitems.h + vectortile/qgsvectortilelayer.h + vectortile/qgsvectortilelayerrenderer.h + vectortile/qgsvectortileloader.h + vectortile/qgsvectortilemvtdecoder.h + vectortile/qgsvectortileprovidermetadata.h + vectortile/qgsvectortilerenderer.h + vectortile/qgsvectortileutils.h ) SET(QGIS_CORE_PRIVATE_HDRS @@ -1351,6 +1410,18 @@ IF(NOT MSVC) ENDIF () ENDIF(NOT MSVC) +# Generate cpp+header file from .proto file using "protoc" tool (to support MVT encoding of vector tiles) +protobuf_generate_cpp(VECTOR_TILE_PROTO_SRCS VECTOR_TILE_PROTO_HDRS vectortile/vector_tile.proto) +SET(QGIS_CORE_SRCS ${QGIS_CORE_SRCS} ${VECTOR_TILE_PROTO_SRCS}) +SET(QGIS_CORE_HDRS ${QGIS_CORE_HDRS} ${VECTOR_TILE_PROTO_HDRS}) +IF (MSVC) + SET_SOURCE_FILES_PROPERTIES(${VECTOR_TILE_PROTO_SRCS} vectortile/qgsvectortilemvtdecoder.cpp PROPERTIES COMPILE_DEFINITIONS PROTOBUF_USE_DLLS) +ELSE (MSVC) + # automatically generated file produces warnings (unused-parameter, unused-variable, misleading-indentation) + SET_SOURCE_FILES_PROPERTIES(${VECTOR_TILE_PROTO_SRCS} PROPERTIES COMPILE_FLAGS -w) +ENDIF (MSVC) + + # install headers # install qgsconfig.h and plugin.h here so they can get into # the OS X framework target @@ -1368,10 +1439,11 @@ INCLUDE_DIRECTORIES( 3d annotations auth + callouts classification dxf effects - ${CMAKE_SOURCE_DIR}/src/core/expression + expression fieldformatter geometry geocms @@ -1396,6 +1468,7 @@ INCLUDE_DIRECTORIES( symbology mesh validity + vectortile ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/external/nlohmann ${CMAKE_SOURCE_DIR}/external/kdbush/include @@ -1403,6 +1476,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/external/poly2tri ${CMAKE_SOURCE_DIR}/external/rtree/include ${CMAKE_SOURCE_DIR}/external/meshOptimizer + ${CMAKE_SOURCE_DIR}/external/mapbox-vector-tile ) INCLUDE_DIRECTORIES(SYSTEM @@ -1417,6 +1491,8 @@ INCLUDE_DIRECTORIES(SYSTEM ${QCA_INCLUDE_DIR} ${QTKEYCHAIN_INCLUDE_DIR} ${Qt5SerialPort_INCLUDE_DIRS} + ${Protobuf_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} ) @@ -1551,6 +1627,8 @@ TARGET_LINK_LIBRARIES(qgis_core ${SQLITE3_LIBRARY} ${SPATIALITE_LIBRARY} ${LIBZIP_LIBRARY} + ${Protobuf_LITE_LIBRARY} + ${ZLIB_LIBRARIES} ) IF (FORCE_STATIC_PROVIDERS) diff --git a/src/core/expression/qgsexpression.cpp b/src/core/expression/qgsexpression.cpp index c76efdea3034..14002a1b8141 100644 --- a/src/core/expression/qgsexpression.cpp +++ b/src/core/expression/qgsexpression.cpp @@ -700,6 +700,12 @@ void QgsExpression::initVariableHelp() sVariableHelpTexts()->insert( QStringLiteral( "project_home" ), QCoreApplication::translate( "variable_help", "Home path of current project." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_crs" ), QCoreApplication::translate( "variable_help", "Coordinate reference system of project (e.g., 'EPSG:4326')." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_crs_definition" ), QCoreApplication::translate( "variable_help", "Coordinate reference system of project (full definition)." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "project_units" ), QCoreApplication::translate( "variable_help", "Unit of the project's CRS." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "project_crs_description" ), QCoreApplication::translate( "variable_help", "Name of the coordinate reference system of the project." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "project_crs_acronym" ), QCoreApplication::translate( "variable_help", "Acronym of the coordinate reference system of the project." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "project_crs_ellipsoid" ), QCoreApplication::translate( "variable_help", "Acronym of the ellipsoid of the coordinate reference system of the project." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "project_crs_proj4" ), QCoreApplication::translate( "variable_help", "Proj4 definition of the coordinate reference system of the project." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "project_crs_wkt" ), QCoreApplication::translate( "variable_help", "WKT definition of the coordinate reference system of the project." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_author" ), QCoreApplication::translate( "variable_help", "Project author, taken from project metadata." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_abstract" ), QCoreApplication::translate( "variable_help", "Project abstract, taken from project metadata." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_creation_date" ), QCoreApplication::translate( "variable_help", "Project creation date, taken from project metadata." ) ); @@ -708,6 +714,8 @@ void QgsExpression::initVariableHelp() sVariableHelpTexts()->insert( QStringLiteral( "project_area_units" ), QCoreApplication::translate( "variable_help", "Area unit for current project, used when calculating areas of geometries." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_distance_units" ), QCoreApplication::translate( "variable_help", "Distance unit for current project, used when calculating lengths of geometries." ) ); sVariableHelpTexts()->insert( QStringLiteral( "project_ellipsoid" ), QCoreApplication::translate( "variable_help", "Name of ellipsoid of current project, used when calculating geodetic areas and lengths of geometries." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "layer_ids" ), QCoreApplication::translate( "variable_help", "List of all map layer IDs from the current project." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "layers" ), QCoreApplication::translate( "variable_help", "List of all map layers from the current project." ) ); //layer variables sVariableHelpTexts()->insert( QStringLiteral( "layer_name" ), QCoreApplication::translate( "variable_help", "Name of current layer." ) ); @@ -752,11 +760,11 @@ void QgsExpression::initVariableHelp() sVariableHelpTexts()->insert( QStringLiteral( "map_crs" ), QCoreApplication::translate( "variable_help", "Coordinate reference system of map (e.g., 'EPSG:4326')." ) ); sVariableHelpTexts()->insert( QStringLiteral( "map_crs_description" ), QCoreApplication::translate( "variable_help", "Name of the coordinate reference system of the map." ) ); sVariableHelpTexts()->insert( QStringLiteral( "map_units" ), QCoreApplication::translate( "variable_help", "Units for map measurements." ) ); - sVariableHelpTexts()->insert( QStringLiteral( "map_crs_definition" ), QCoreApplication::translate( "variable_help", "Coordinate reference system of map (full definition)." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "map_crs_definition" ), QCoreApplication::translate( "variable_help", "Coordinate reference system of the map (full definition)." ) ); sVariableHelpTexts()->insert( QStringLiteral( "map_crs_acronym" ), QCoreApplication::translate( "variable_help", "Acronym of the coordinate reference system of the map." ) ); sVariableHelpTexts()->insert( QStringLiteral( "map_crs_ellipsoid" ), QCoreApplication::translate( "variable_help", "Acronym of the ellipsoid of the coordinate reference system of the map." ) ); - sVariableHelpTexts()->insert( QStringLiteral( "map_crs_proj4" ), QCoreApplication::translate( "variable_help", "Proj4 definition of the coordinate reference system." ) ); - sVariableHelpTexts()->insert( QStringLiteral( "map_crs_wkt" ), QCoreApplication::translate( "variable_help", "WKT definition of the coordinate reference system." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "map_crs_proj4" ), QCoreApplication::translate( "variable_help", "Proj4 definition of the coordinate reference system of the map." ) ); + sVariableHelpTexts()->insert( QStringLiteral( "map_crs_wkt" ), QCoreApplication::translate( "variable_help", "WKT definition of the coordinate reference system of the map." ) ); sVariableHelpTexts()->insert( QStringLiteral( "map_layer_ids" ), QCoreApplication::translate( "variable_help", "List of map layer IDs visible in the map." ) ); sVariableHelpTexts()->insert( QStringLiteral( "map_layers" ), QCoreApplication::translate( "variable_help", "List of map layers visible in the map." ) ); diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 49f044d9237a..760a5ee58f68 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -57,6 +57,7 @@ #include "qgsapplication.h" #include "qgis.h" #include "qgsexpressioncontextutils.h" +#include "qgsunittypes.h" typedef QList ExpressionFunctionList; @@ -272,6 +273,12 @@ static QVariant fcnGetVariable( const QVariantList &values, const QgsExpressionC return context->variable( name ); } +static QVariant fcnEvalTemplate( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + QString templateString = QgsExpressionUtils::getStringValue( values.at( 0 ), parent ); + return QgsExpression::replaceExpressionText( templateString, context ); +} + static QVariant fcnEval( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) { if ( !context ) @@ -1513,6 +1520,96 @@ static QVariant fcnAttributes( const QVariantList &values, const QgsExpressionCo return result; } +static QVariant fcnCoreFeatureMaptipDisplay( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const bool isMaptip ) +{ + QgsVectorLayer *layer = nullptr; + QgsFeature feature; + bool evaluate = true; + + if ( values.isEmpty() ) + { + feature = context->feature(); + layer = QgsExpressionUtils::getVectorLayer( context->variable( QStringLiteral( "layer" ) ), parent ); + } + else if ( values.size() == 1 ) + { + layer = QgsExpressionUtils::getVectorLayer( context->variable( QStringLiteral( "layer" ) ), parent ); + feature = QgsExpressionUtils::getFeature( values.at( 0 ), parent ); + } + else if ( values.size() == 2 ) + { + layer = QgsExpressionUtils::getVectorLayer( values.at( 0 ), parent ); + feature = QgsExpressionUtils::getFeature( values.at( 1 ), parent ); + } + else if ( values.size() == 3 ) + { + layer = QgsExpressionUtils::getVectorLayer( values.at( 0 ), parent ); + feature = QgsExpressionUtils::getFeature( values.at( 1 ), parent ); + evaluate = values.value( 2 ).toBool(); + } + else + { + if ( isMaptip ) + { + parent->setEvalErrorString( QObject::tr( "Function `maptip` requires no more than three parameters. %1 given." ).arg( values.length() ) ); + } + else + { + parent->setEvalErrorString( QObject::tr( "Function `display` requires no more than three parameters. %1 given." ).arg( values.length() ) ); + } + return QVariant(); + } + + if ( !layer ) + { + parent->setEvalErrorString( QObject::tr( "The layer is not valid." ) ); + return QVariant( ); + } + + if ( !feature.isValid() ) + { + parent->setEvalErrorString( QObject::tr( "The feature is not valid." ) ); + return QVariant( ); + } + + if ( ! evaluate ) + { + if ( isMaptip ) + { + return layer->mapTipTemplate(); + } + else + { + return layer->displayExpression(); + } + } + + QgsExpressionContext subContext( *context ); + subContext.appendScopes( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + subContext.setFeature( feature ); + + if ( isMaptip ) + { + return QgsExpression::replaceExpressionText( layer->mapTipTemplate(), &subContext ); + } + else + { + QgsExpression exp( layer->displayExpression() ); + exp.prepare( &subContext ); + return exp.evaluate( &subContext ).toString(); + } +} + +static QVariant fcnFeatureDisplayExpression( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + return fcnCoreFeatureMaptipDisplay( values, context, parent, false ); +} + +static QVariant fcnFeatureMaptip( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) +{ + return fcnCoreFeatureMaptipDisplay( values, context, parent, true ); +} + static QVariant fcnIsSelected( const QVariantList &values, const QgsExpressionContext *context, QgsExpression *parent, const QgsExpressionNodeFunction * ) { QgsVectorLayer *layer = nullptr; @@ -4675,6 +4772,8 @@ static QVariant fcnGetLayerProperty( const QVariantList &values, const QgsExpres return layer->minimumScale(); else if ( QString::compare( layerProperty, QStringLiteral( "max_scale" ), Qt::CaseInsensitive ) == 0 ) return layer->maximumScale(); + else if ( QString::compare( layerProperty, QStringLiteral( "is_editable" ), Qt::CaseInsensitive ) == 0 ) + return layer->isEditable(); else if ( QString::compare( layerProperty, QStringLiteral( "crs" ), Qt::CaseInsensitive ) == 0 ) return layer->crs().authid(); else if ( QString::compare( layerProperty, QStringLiteral( "crs_definition" ), Qt::CaseInsensitive ) == 0 ) @@ -4687,6 +4786,8 @@ static QVariant fcnGetLayerProperty( const QVariantList &values, const QgsExpres QVariant result = QVariant::fromValue( extentGeom ); return result; } + else if ( QString::compare( layerProperty, QStringLiteral( "distance_units" ), Qt::CaseInsensitive ) == 0 ) + return QgsUnitTypes::encodeUnit( layer->crs().mapUnits() ); else if ( QString::compare( layerProperty, QStringLiteral( "type" ), Qt::CaseInsensitive ) == 0 ) { switch ( layer->type() ) @@ -4697,6 +4798,8 @@ static QVariant fcnGetLayerProperty( const QVariantList &values, const QgsExpres return QCoreApplication::translate( "expressions", "Raster" ); case QgsMapLayerType::MeshLayer: return QCoreApplication::translate( "expressions", "Mesh" ); + case QgsMapLayerType::VectorTileLayer: + return QCoreApplication::translate( "expressions", "Vector Tile" ); case QgsMapLayerType::PluginLayer: return QCoreApplication::translate( "expressions", "Plugin" ); } @@ -5899,9 +6002,36 @@ const QList &QgsExpression::Functions() fcnGetFeature, QStringLiteral( "Record and Attributes" ), QString(), false, QSet(), false, QStringList() << QStringLiteral( "QgsExpressionUtils::getFeature" ) ) << new QgsStaticExpressionFunction( QStringLiteral( "get_feature_by_id" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "layer" ) ) << QgsExpressionFunction::Parameter( QStringLiteral( "feature_id" ) ), - fcnGetFeatureById, QStringLiteral( "Record and Attributes" ), QString(), false, QSet(), false ) - << new QgsStaticExpressionFunction( QStringLiteral( "attributes" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "feature" ), true ), - fcnAttributes, QStringLiteral( "Record and Attributes" ), QString(), false, QSet() << QgsFeatureRequest::ALL_ATTRIBUTES ); + fcnGetFeatureById, QStringLiteral( "Record and Attributes" ), QString(), false, QSet(), false ); + + QgsStaticExpressionFunction *attributesFunc = new QgsStaticExpressionFunction( QStringLiteral( "attributes" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "feature" ), true ), + fcnAttributes, QStringLiteral( "Record and Attributes" ), QString(), false, QSet() << QgsFeatureRequest::ALL_ATTRIBUTES ); + attributesFunc->setIsStatic( false ); + functions << attributesFunc; + + QgsStaticExpressionFunction *maptipFunc = new QgsStaticExpressionFunction( + QStringLiteral( "maptip" ), + -1, + fcnFeatureMaptip, + QStringLiteral( "Record and Attributes" ), + QString(), + false, + QSet() + ); + maptipFunc->setIsStatic( false ); + functions << maptipFunc; + + QgsStaticExpressionFunction *displayFunc = new QgsStaticExpressionFunction( + QStringLiteral( "display_expression" ), + -1, + fcnFeatureDisplayExpression, + QStringLiteral( "Record and Attributes" ), + QString(), + false, + QSet() + ); + displayFunc->setIsStatic( false ); + functions << displayFunc; QgsStaticExpressionFunction *isSelectedFunc = new QgsStaticExpressionFunction( QStringLiteral( "is_selected" ), @@ -6015,6 +6145,9 @@ const QList &QgsExpression::Functions() functions << varFunction; + + functions << new QgsStaticExpressionFunction( QStringLiteral( "eval_template" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "template" ) ), fcnEvalTemplate, QStringLiteral( "General" ), QString(), true ); + QgsStaticExpressionFunction *evalFunc = new QgsStaticExpressionFunction( QStringLiteral( "eval" ), QgsExpressionFunction::ParameterList() << QgsExpressionFunction::Parameter( QStringLiteral( "expression" ) ), fcnEval, QStringLiteral( "General" ), QString(), true, QSet() << QgsFeatureRequest::ALL_ATTRIBUTES ); evalFunc->setIsStaticFunction( []( const QgsExpressionNodeFunction * node, QgsExpression * parent, const QgsExpressionContext * context ) diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp index 8070d720e3c6..1bfea625d322 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.cpp @@ -140,25 +140,32 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte QgsFeatureRequest request; request.setFlags( QgsFeatureRequest::NoGeometry ); - request.setSubsetOfAttributes( QgsAttributeList() << ki << vi ); + QgsAttributeIds subsetOfAttributes { ki, vi }; - const QString expression = config.value( QStringLiteral( "FilterExpression" ) ).toString(); + const QString descriptionExpressionString = config.value( "Description" ).toString(); + QgsExpression descriptionExpression( descriptionExpressionString ); + QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); + descriptionExpression.prepare( &context ); + subsetOfAttributes += descriptionExpression.referencedAttributeIndexes( layer->fields() ); + request.setSubsetOfAttributes( subsetOfAttributes.toList() ); + + const QString filterExpression = config.value( QStringLiteral( "FilterExpression" ) ).toString(); // Skip the filter and build a full cache if the form scope is required and the feature // is not valid or the attributes required for the filter have no valid value // Note: parent form scope is not checked for usability because it's supposed to // be used into a coalesce that retrieve the current value of the parent // from the parent layer when used outside of an embedded form - if ( ! expression.isEmpty() && ( !( expressionRequiresFormScope( expression ) ) - || expressionIsUsable( expression, formFeature ) ) ) + if ( ! filterExpression.isEmpty() && ( !( expressionRequiresFormScope( filterExpression ) ) + || expressionIsUsable( filterExpression, formFeature ) ) ) { - QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( layer ) ); - if ( formFeature.isValid( ) && QgsValueRelationFieldFormatter::expressionRequiresFormScope( expression ) ) - context.appendScope( QgsExpressionContextUtils::formScope( formFeature ) ); - if ( parentFormFeature.isValid() && QgsValueRelationFieldFormatter::expressionRequiresParentFormScope( expression ) ) - context.appendScope( QgsExpressionContextUtils::parentFormScope( parentFormFeature ) ); - request.setExpressionContext( context ); - request.setFilterExpression( expression ); + QgsExpressionContext filterContext = context; + if ( formFeature.isValid( ) && QgsValueRelationFieldFormatter::expressionRequiresFormScope( filterExpression ) ) + filterContext.appendScope( QgsExpressionContextUtils::formScope( formFeature ) ); + if ( parentFormFeature.isValid() && QgsValueRelationFieldFormatter::expressionRequiresParentFormScope( filterExpression ) ) + filterContext.appendScope( QgsExpressionContextUtils::parentFormScope( parentFormFeature ) ); + request.setExpressionContext( filterContext ); + request.setFilterExpression( filterExpression ); } QgsFeatureIterator fit = layer->getFeatures( request ); @@ -166,7 +173,13 @@ QgsValueRelationFieldFormatter::ValueRelationCache QgsValueRelationFieldFormatte QgsFeature f; while ( fit.nextFeature( f ) ) { - cache.append( ValueRelationItem( f.attribute( ki ), f.attribute( vi ).toString() ) ); + QString description; + if ( descriptionExpression.isValid() ) + { + context.setFeature( f ); + description = descriptionExpression.evaluate( &context ).toString(); + } + cache.append( ValueRelationItem( f.attribute( ki ), f.attribute( vi ).toString(), description ) ); } if ( config.value( QStringLiteral( "OrderByValue" ) ).toBool() ) diff --git a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h index 05852e5fb877..14ca2426b1c2 100644 --- a/src/core/fieldformatter/qgsvaluerelationfieldformatter.h +++ b/src/core/fieldformatter/qgsvaluerelationfieldformatter.h @@ -39,9 +39,10 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter struct ValueRelationItem { //! Constructor for ValueRelationItem - ValueRelationItem( const QVariant &key, const QString &value ) + ValueRelationItem( const QVariant &key, const QString &value, const QString &description = QString() ) : key( key ) , value( value ) + , description( description ) {} //! Constructor for ValueRelationItem @@ -49,6 +50,7 @@ class CORE_EXPORT QgsValueRelationFieldFormatter : public QgsFieldFormatter QVariant key; QString value; + QString description; }; typedef QVector < QgsValueRelationFieldFormatter::ValueRelationItem > ValueRelationCache; diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 38f0b4653a50..ffa0fb02b13b 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -813,14 +813,14 @@ QgsGeometry::OperationResult QgsGeometry::rotate( double rotation, const QgsPoin return QgsGeometry::Success; } -QgsGeometry::OperationResult QgsGeometry::splitGeometry( const QVector &splitLine, QVector &newGeometries, bool topological, QVector &topologyTestPoints ) +QgsGeometry::OperationResult QgsGeometry::splitGeometry( const QVector &splitLine, QVector &newGeometries, bool topological, QVector &topologyTestPoints, bool splitFeature ) { QgsPointSequence split, topology; convertPointList( splitLine, split ); convertPointList( topologyTestPoints, topology ); - return splitGeometry( split, newGeometries, topological, topology ); + return splitGeometry( split, newGeometries, topological, topology, splitFeature ); } -QgsGeometry::OperationResult QgsGeometry::splitGeometry( const QgsPointSequence &splitLine, QVector &newGeometries, bool topological, QgsPointSequence &topologyTestPoints ) +QgsGeometry::OperationResult QgsGeometry::splitGeometry( const QgsPointSequence &splitLine, QVector &newGeometries, bool topological, QgsPointSequence &topologyTestPoints, bool splitFeature ) { if ( !d->geometry ) { @@ -836,8 +836,8 @@ QgsGeometry::OperationResult QgsGeometry::splitGeometry( const QgsPointSequence if ( result == QgsGeometryEngine::Success ) { - *this = newGeoms.takeAt( 0 ); - + if ( splitFeature ) + *this = newGeoms.takeAt( 0 ); newGeometries = newGeoms; } @@ -1310,6 +1310,129 @@ json QgsGeometry::asJsonObject( int precision ) const } +QVector QgsGeometry::coerceToType( const QgsWkbTypes::Type type ) const +{ + QVector< QgsGeometry > res; + if ( isNull() ) + return res; + + if ( wkbType() == type || type == QgsWkbTypes::Unknown ) + { + res << *this; + return res; + } + + if ( type == QgsWkbTypes::NoGeometry ) + { + return res; + } + + QgsGeometry newGeom = *this; + + // Curved -> straight + if ( !QgsWkbTypes::isCurvedType( type ) && QgsWkbTypes::isCurvedType( newGeom.wkbType() ) ) + { + newGeom = QgsGeometry( d->geometry.get()->segmentize() ); + } + + // polygon -> line + if ( QgsWkbTypes::geometryType( type ) == QgsWkbTypes::LineGeometry && + newGeom.type() == QgsWkbTypes::PolygonGeometry ) + { + // boundary gives us a (multi)line string of exterior + interior rings + newGeom = QgsGeometry( newGeom.constGet()->boundary() ); + } + // line -> polygon + if ( QgsWkbTypes::geometryType( type ) == QgsWkbTypes::PolygonGeometry && + newGeom.type() == QgsWkbTypes::LineGeometry ) + { + std::unique_ptr< QgsGeometryCollection > gc( QgsGeometryFactory::createCollectionOfType( type ) ); + const QgsGeometry source = newGeom; + for ( auto part = source.const_parts_begin(); part != source.const_parts_end(); ++part ) + { + std::unique_ptr< QgsAbstractGeometry > exterior( ( *part )->clone() ); + if ( QgsCurve *curve = qgsgeometry_cast< QgsCurve * >( exterior.get() ) ) + { + if ( QgsWkbTypes::isCurvedType( type ) ) + { + std::unique_ptr< QgsCurvePolygon > cp = qgis::make_unique< QgsCurvePolygon >(); + cp->setExteriorRing( curve ); + exterior.release(); + gc->addGeometry( cp.release() ); + } + else + { + std::unique_ptr< QgsPolygon > p = qgis::make_unique< QgsPolygon >(); + p->setExteriorRing( qgsgeometry_cast< QgsLineString * >( curve ) ); + exterior.release(); + gc->addGeometry( p.release() ); + } + } + } + newGeom = QgsGeometry( std::move( gc ) ); + } + + // line/polygon -> points + if ( QgsWkbTypes::geometryType( type ) == QgsWkbTypes::PointGeometry && + ( newGeom.type() == QgsWkbTypes::LineGeometry || + newGeom.type() == QgsWkbTypes::PolygonGeometry ) ) + { + // lines/polygons to a point layer, extract all vertices + std::unique_ptr< QgsMultiPoint > mp = qgis::make_unique< QgsMultiPoint >(); + const QgsGeometry source = newGeom; + QSet< QgsPoint > added; + for ( auto vertex = source.vertices_begin(); vertex != source.vertices_end(); ++vertex ) + { + if ( added.contains( *vertex ) ) + continue; // avoid duplicate points, e.g. start/end of rings + mp->addGeometry( ( *vertex ).clone() ); + added.insert( *vertex ); + } + newGeom = QgsGeometry( std::move( mp ) ); + } + + // Single -> multi + if ( QgsWkbTypes::isMultiType( type ) && ! newGeom.isMultipart( ) ) + { + newGeom.convertToMultiType(); + } + // Drop Z/M + if ( newGeom.constGet()->is3D() && ! QgsWkbTypes::hasZ( type ) ) + { + newGeom.get()->dropZValue(); + } + if ( newGeom.constGet()->isMeasure() && ! QgsWkbTypes::hasM( type ) ) + { + newGeom.get()->dropMValue(); + } + // Add Z/M back, set to 0 + if ( ! newGeom.constGet()->is3D() && QgsWkbTypes::hasZ( type ) ) + { + newGeom.get()->addZValue( 0.0 ); + } + if ( ! newGeom.constGet()->isMeasure() && QgsWkbTypes::hasM( type ) ) + { + newGeom.get()->addMValue( 0.0 ); + } + + // Multi -> single + if ( ! QgsWkbTypes::isMultiType( type ) && newGeom.isMultipart( ) ) + { + const QgsGeometryCollection *parts( static_cast< const QgsGeometryCollection * >( newGeom.constGet() ) ); + QgsAttributeMap attrMap; + res.reserve( parts->partCount() ); + for ( int i = 0; i < parts->partCount( ); i++ ) + { + res << QgsGeometry( parts->geometryN( i )->clone() ); + } + } + else + { + res << newGeom; + } + return res; +} + QgsGeometry QgsGeometry::convertToType( QgsWkbTypes::GeometryType destType, bool destMultipart ) const { switch ( destType ) diff --git a/src/core/geometry/qgsgeometry.h b/src/core/geometry/qgsgeometry.h index 2396635a2131..0c555ace8678 100644 --- a/src/core/geometry/qgsgeometry.h +++ b/src/core/geometry/qgsgeometry.h @@ -881,10 +881,11 @@ class CORE_EXPORT QgsGeometry * \param[out] newGeometries list of new geometries that have been created with the split * \param topological TRUE if topological editing is enabled * \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset + * \param splitFeature Set to True if you want to split a feature, otherwise set to False to split parts * \returns OperationResult a result code: success or reason of failure * \deprecated since QGIS 3.12 - will be removed in QGIS 4.0. Use the variant which accepts QgsPoint objects instead of QgsPointXY. */ - Q_DECL_DEPRECATED OperationResult splitGeometry( const QVector &splitLine, QVector &newGeometries SIP_OUT, bool topological, QVector &topologyTestPoints SIP_OUT ) SIP_DEPRECATED; + Q_DECL_DEPRECATED OperationResult splitGeometry( const QVector &splitLine, QVector &newGeometries SIP_OUT, bool topological, QVector &topologyTestPoints SIP_OUT, bool splitFeature = true ) SIP_DEPRECATED; /** * Splits this geometry according to a given line. @@ -892,9 +893,11 @@ class CORE_EXPORT QgsGeometry * \param[out] newGeometries list of new geometries that have been created with the split * \param topological TRUE if topological editing is enabled * \param[out] topologyTestPoints points that need to be tested for topological completeness in the dataset + * \param splitFeature Set to True if you want to split a feature, otherwise set to False to split parts + * fix this bug? * \returns OperationResult a result code: success or reason of failure */ - OperationResult splitGeometry( const QgsPointSequence &splitLine, QVector &newGeometries SIP_OUT, bool topological, QgsPointSequence &topologyTestPoints SIP_OUT ); + OperationResult splitGeometry( const QgsPointSequence &splitLine, QVector &newGeometries SIP_OUT, bool topological, QgsPointSequence &topologyTestPoints SIP_OUT, bool splitFeature = true ); /** * Replaces a part of this geometry with another line @@ -1564,14 +1567,42 @@ class CORE_EXPORT QgsGeometry */ virtual json asJsonObject( int precision = 17 ) const SIP_SKIP; + /** + * Attempts to coerce this geometry into the specified destination \a type. + * + * This method will do anything possible to force the current geometry into the specified type. E.g. + * - lines or polygons will be converted to points by return either a single multipoint geometry or multiple + * single point geometries. + * - polygons will be converted to lines by extracting their exterior and interior rings, returning + * either a multilinestring or multiple single line strings as dictated by \a type. + * - lines will be converted to polygon rings if \a type is a polygon type + * - curved geometries will be segmented if \a type is non-curved. + * - multi geometries will be converted to a list of single geometries + * - single geometries will be upgraded to multi geometries + * - z or m values will be added or dropped as required. + * + * \note This method is much stricter than convertToType(), as it considers the exact WKB type + * of geometries instead of the geometry family (point/line/polygon), and tries more exhaustively + * to coerce geometries to the desired \a type. It also correctly maintains curves and z/m values + * wherever appropriate. + * + * \since QGIS 3.14 + */ + QVector< QgsGeometry > coerceToType( QgsWkbTypes::Type type ) const; + /** * Try to convert the geometry to the requested type * \param destType the geometry type to be converted to * \param destMultipart determines if the output geometry will be multipart or not * \returns the converted geometry or NULLPTR if the conversion fails. + * + * \note The coerceToType() method applies much stricter and more exhaustive attempts to convert + * between geometry types, and is recommended instead of this method. This method force drops + * curves and any z or m values present in the geometry. + * * \since QGIS 2.2 */ - QgsGeometry convertToType( QgsWkbTypes::GeometryType destType, bool destMultipart = false ) const SIP_FACTORY; + QgsGeometry convertToType( QgsWkbTypes::GeometryType destType, bool destMultipart = false ) const; /* Accessor functions for getting geometry data */ diff --git a/src/core/geometry/qgsgeometrycollection.cpp b/src/core/geometry/qgsgeometrycollection.cpp index e7cd9c25ca9c..f86a611290a4 100644 --- a/src/core/geometry/qgsgeometrycollection.cpp +++ b/src/core/geometry/qgsgeometrycollection.cpp @@ -477,8 +477,28 @@ QgsRectangle QgsGeometryCollection::calculateBoundingBox() const QgsRectangle bbox = mGeometries.at( 0 )->boundingBox(); for ( int i = 1; i < mGeometries.size(); ++i ) { + if ( mGeometries.at( i )->isEmpty() ) + continue; + QgsRectangle geomBox = mGeometries.at( i )->boundingBox(); - bbox.combineExtentWith( geomBox ); + if ( bbox.isNull() ) + { + // workaround treatment of a QgsRectangle(0,0,0,0) as a "null"/invalid rectangle + // if bbox is null, then the first geometry must have returned a bounding box of (0,0,0,0) + // so just manually include that as a point... ew. + geomBox.combineExtentWith( QPointF( 0, 0 ) ); + bbox = geomBox; + } + else if ( geomBox.isNull() ) + { + // ...as above... this part must have a bounding box of (0,0,0,0). + // if we try to combine the extent with this "null" box it will just be ignored. + bbox.combineExtentWith( QPointF( 0, 0 ) ); + } + else + { + bbox.combineExtentWith( geomBox ); + } } return bbox; } diff --git a/src/core/layertree/qgslayertreelayer.cpp b/src/core/layertree/qgslayertreelayer.cpp index 659a3c41a07a..6e2759be3e07 100644 --- a/src/core/layertree/qgslayertreelayer.cpp +++ b/src/core/layertree/qgslayertreelayer.cpp @@ -39,6 +39,7 @@ QgsLayerTreeLayer::QgsLayerTreeLayer( const QgsLayerTreeLayer &other ) : QgsLayerTreeNode( other ) , mRef( other.mRef ) , mLayerName( other.mLayerName ) + , mPatchShape( other.mPatchShape ) { attachToLayer(); } @@ -110,6 +111,7 @@ QgsLayerTreeLayer *QgsLayerTreeLayer::readXml( QDomElement &element, const QgsRe Qt::CheckState checked = QgsLayerTreeUtils::checkStateFromXml( element.attribute( QStringLiteral( "checked" ) ) ); bool isExpanded = ( element.attribute( QStringLiteral( "expanded" ), QStringLiteral( "1" ) ) == QLatin1String( "1" ) ); + QString labelExpression = element.attribute( QStringLiteral( "legend_exp" ) ); // needs to have the layer reference resolved later QgsLayerTreeLayer *nodeLayer = new QgsLayerTreeLayer( layerID, layerName, source, providerKey ); @@ -118,6 +120,16 @@ QgsLayerTreeLayer *QgsLayerTreeLayer::readXml( QDomElement &element, const QgsRe nodeLayer->setItemVisibilityChecked( checked != Qt::Unchecked ); nodeLayer->setExpanded( isExpanded ); + nodeLayer->setLabelExpression( labelExpression ); + + const QDomElement patchElem = element.firstChildElement( QStringLiteral( "patch" ) ); + if ( !patchElem.isNull() ) + { + QgsLegendPatchShape patch; + patch.readXml( patchElem, context ); + nodeLayer->setPatchShape( patch ); + } + return nodeLayer; } @@ -144,6 +156,14 @@ void QgsLayerTreeLayer::writeXml( QDomElement &parentElement, const QgsReadWrite elem.setAttribute( QStringLiteral( "checked" ), mChecked ? QStringLiteral( "Qt::Checked" ) : QStringLiteral( "Qt::Unchecked" ) ); elem.setAttribute( QStringLiteral( "expanded" ), mExpanded ? "1" : "0" ); + elem.setAttribute( QStringLiteral( "legend_exp" ), mLabelExpression ); + + if ( !mPatchShape.isNull() ) + { + QDomElement patchElem = doc.createElement( QStringLiteral( "patch" ) ); + mPatchShape.writeXml( patchElem, doc, context ); + elem.appendChild( patchElem ); + } writeCommonXml( elem ); @@ -194,3 +214,13 @@ void QgsLayerTreeLayer::setLabelExpression( const QString &expression ) mLabelExpression = expression; } +QgsLegendPatchShape QgsLayerTreeLayer::patchShape() const +{ + return mPatchShape; +} + +void QgsLayerTreeLayer::setPatchShape( const QgsLegendPatchShape &shape ) +{ + mPatchShape = shape; +} + diff --git a/src/core/layertree/qgslayertreelayer.h b/src/core/layertree/qgslayertreelayer.h index d4186228269f..a723ad808b8b 100644 --- a/src/core/layertree/qgslayertreelayer.h +++ b/src/core/layertree/qgslayertreelayer.h @@ -21,6 +21,7 @@ #include "qgslayertreenode.h" #include "qgsmaplayerref.h" #include "qgsreadwritecontext.h" +#include "qgslegendpatchshape.h" class QgsMapLayer; @@ -142,6 +143,22 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode */ QString labelExpression() const { return mLabelExpression; } + /** + * Returns the symbol patch shape to use when rendering the legend node symbol. + * + * \see setPatchShape() + * \since QGIS 3.14 + */ + QgsLegendPatchShape patchShape() const; + + /** + * Sets the symbol patch \a shape to use when rendering the legend node symbol. + * + * \see patchShape() + * \since QGIS 3.14 + */ + void setPatchShape( const QgsLegendPatchShape &shape ); + signals: /** @@ -191,6 +208,8 @@ class CORE_EXPORT QgsLayerTreeLayer : public QgsLayerTreeNode */ QgsLayerTreeLayer( const QgsLayerTreeLayer &other ); #endif + + QgsLegendPatchShape mPatchShape; }; diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index 84dfaeb122af..909078f1f116 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -203,6 +203,9 @@ QVariant QgsLayerTreeModel::data( const QModelIndex &index, int role ) const case QgsMapLayerType::MeshLayer: return QgsLayerItem::iconMesh(); + case QgsMapLayerType::VectorTileLayer: + return QgsLayerItem::iconVectorTile(); + case QgsMapLayerType::VectorLayer: case QgsMapLayerType::PluginLayer: break; diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp index 2153765031db..3bdecaa534bb 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.cpp +++ b/src/core/layertree/qgslayertreemodellegendnode.cpp @@ -303,6 +303,23 @@ QString QgsSymbolLegendNode::symbolLabel() const return label; } +QgsLegendPatchShape QgsSymbolLegendNode::patchShape() const +{ + if ( mEmbeddedInParent ) + { + return mLayerNode->patchShape(); + } + else + { + return mPatchShape; + } +} + +void QgsSymbolLegendNode::setPatchShape( const QgsLegendPatchShape &shape ) +{ + mPatchShape = shape; +} + void QgsSymbolLegendNode::setSymbol( QgsSymbol *symbol ) { if ( !symbol ) @@ -493,6 +510,7 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC // setup temporary render context QgsRenderContext *context = nullptr; std::unique_ptr< QgsRenderContext > tempRenderContext; + QgsLegendPatchShape patchShape = ctx ? ctx->patchShape : QgsLegendPatchShape(); if ( ctx && ctx->context ) context = ctx->context; else @@ -577,7 +595,7 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC imagePainter.setRenderHint( QPainter::Antialiasing ); context->setPainter( &imagePainter ); imagePainter.translate( maxBleed, maxBleed ); - s->drawPreviewIcon( &imagePainter, symbolSize, context ); + s->drawPreviewIcon( &imagePainter, symbolSize, context, false, nullptr, &patchShape ); imagePainter.translate( -maxBleed, -maxBleed ); context->setPainter( ctx->painter ); //reduce opacity of image @@ -589,7 +607,7 @@ QSizeF QgsSymbolLegendNode::drawSymbol( const QgsLegendSettings &settings, ItemC } else { - s->drawPreviewIcon( p, QSize( static_cast< int >( std::round( width * dotsPerMM ) ), static_cast< int >( std::round( height * dotsPerMM ) ) ), context ); + s->drawPreviewIcon( p, QSize( static_cast< int >( std::round( width * dotsPerMM ) ), static_cast< int >( std::round( height * dotsPerMM ) ) ), context, false, nullptr, &patchShape ); } if ( !mTextOnSymbolLabel.isEmpty() ) diff --git a/src/core/layertree/qgslayertreemodellegendnode.h b/src/core/layertree/qgslayertreemodellegendnode.h index 32a05931e7bc..589956200c02 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.h +++ b/src/core/layertree/qgslayertreemodellegendnode.h @@ -28,6 +28,7 @@ #include "qgsrasterdataprovider.h" // for QgsImageFetcher dtor visibility #include "qgsexpressioncontext.h" +#include "qgslegendpatchshape.h" class QgsLayerTreeLayer; class QgsLayerTreeModel; @@ -145,6 +146,12 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject */ double maxSiblingSymbolWidth = 0.0; + /** + * The patch shape to render for the node. + * + * \since QGIS 3.14 + */ + QgsLegendPatchShape patchShape; }; struct ItemMetrics @@ -221,6 +228,7 @@ class CORE_EXPORT QgsLayerTreeModelLegendNode : public QObject QgsLayerTreeLayer *mLayerNode = nullptr; bool mEmbeddedInParent; QString mUserLabel; + QgsLegendPatchShape mPatchShape; }; #include "qgslegendsymbolitem.h" @@ -335,6 +343,22 @@ class CORE_EXPORT QgsSymbolLegendNode : public QgsLayerTreeModelLegendNode */ QString symbolLabel() const; + /** + * Returns the symbol patch shape to use when rendering the legend node symbol. + * + * \see setPatchShape() + * \since QGIS 3.14 + */ + QgsLegendPatchShape patchShape() const; + + /** + * Sets the symbol patch \a shape to use when rendering the legend node symbol. + * + * \see patchShape() + * \since QGIS 3.14 + */ + void setPatchShape( const QgsLegendPatchShape &shape ); + /** * Evaluates and returns the text label of the current node * \param context extra QgsExpressionContext to use for evaluating the expression diff --git a/src/core/layertree/qgslayertreenode.cpp b/src/core/layertree/qgslayertreenode.cpp index 4a79eeb08028..b473b62b3a7f 100644 --- a/src/core/layertree/qgslayertreenode.cpp +++ b/src/core/layertree/qgslayertreenode.cpp @@ -155,6 +155,18 @@ QList QgsLayerTreeNode::checkedLayers() const return layers; } +int QgsLayerTreeNode::depth() const +{ + int depth = 0; + QgsLayerTreeNode *node = mParent; + while ( node ) + { + node = node->parent(); + ++depth; + } + return depth; +} + void QgsLayerTreeNode::setExpanded( bool expanded ) { if ( mExpanded == expanded ) diff --git a/src/core/layertree/qgslayertreenode.h b/src/core/layertree/qgslayertreenode.h index c0e0c5703fbd..d39853429aef 100644 --- a/src/core/layertree/qgslayertreenode.h +++ b/src/core/layertree/qgslayertreenode.h @@ -208,6 +208,12 @@ class CORE_EXPORT QgsLayerTreeNode : public QObject */ QList< QgsMapLayer * > checkedLayers() const; + /** + * Returns the depth of this node, i.e. the number of its ancestors + * \since QGIS 3.14 + */ + int depth() const; + //! Returns whether the node should be shown as expanded or collapsed in GUI bool isExpanded() const; //! Sets whether the node should be shown as expanded or collapsed in GUI diff --git a/src/core/layertree/qgslegendpatchshape.cpp b/src/core/layertree/qgslegendpatchshape.cpp new file mode 100644 index 000000000000..89918b7720b8 --- /dev/null +++ b/src/core/layertree/qgslegendpatchshape.cpp @@ -0,0 +1,221 @@ +/*************************************************************************** + qgslegendpatchshape.cpp + ------------------- +begin : April 2020 +copyright : (C) 2020 by Nyall Dawson +email : nyall dot dawson 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 "qgslegendpatchshape.h" +#include "qgsmultilinestring.h" +#include "qgslinestring.h" +#include "qgspolygon.h" + +QgsLegendPatchShape::QgsLegendPatchShape( QgsSymbol::SymbolType type, const QgsGeometry &geometry, bool preserveAspectRatio ) + : mSymbolType( type ) + , mGeometry( geometry ) + , mPreserveAspectRatio( preserveAspectRatio ) +{ + +} + +bool QgsLegendPatchShape::isNull() const +{ + return mGeometry.isNull() || mGeometry.isEmpty(); +} + +QgsGeometry QgsLegendPatchShape::geometry() const +{ + return mGeometry; +} + +void QgsLegendPatchShape::setGeometry( const QgsGeometry &geometry ) +{ + mGeometry = geometry; +} + +bool QgsLegendPatchShape::preserveAspectRatio() const +{ + return mPreserveAspectRatio; +} + +void QgsLegendPatchShape::setPreserveAspectRatio( bool preserveAspectRatio ) +{ + mPreserveAspectRatio = preserveAspectRatio; +} + +QPolygonF lineStringToQPolygonF( const QgsLineString *line ) +{ + const double *srcX = line->xData(); + const double *srcY = line->yData(); + const int count = line->numPoints(); + QPolygonF thisRes( count ); + QPointF *dest = thisRes.data(); + for ( int i = 0; i < count; ++i ) + { + *dest++ = QPointF( *srcX++, *srcY++ ); + } + return thisRes; +} + +QList > QgsLegendPatchShape::toQPolygonF( QgsSymbol::SymbolType type, QSizeF size ) const +{ + if ( isNull() || type != mSymbolType ) + return defaultPatch( type, size ); + + // scale and translate to desired size + + const QRectF bounds = mGeometry.boundingBox().toRectF(); + + double dx = 0; + double dy = 0; + if ( mPreserveAspectRatio && bounds.height() > 0 && bounds.width() > 0 ) + { + const double scaling = std::min( size.width() / bounds.width(), size.height() / bounds.height() ); + const QSizeF scaledSize = bounds.size() * scaling; + dx = ( size.width() - scaledSize.width() ) / 2.0; + dy = ( size.height() - scaledSize.height() ) / 2.0; + size = scaledSize; + } + + // important -- the transform needs to flip from north-up to painter style "increasing y down" coordinates + QPolygonF targetRectPoly = QPolygonF() << QPointF( dx, dy + size.height() ) + << QPointF( dx + size.width(), dy + size.height() ) + << QPointF( dx + size.width(), dy ) + << QPointF( dx, dy ); + QPolygonF patchRectPoly = QPolygonF( bounds ); + //workaround QT Bug #21329 + patchRectPoly.pop_back(); + QTransform t; + QTransform::quadToQuad( patchRectPoly, targetRectPoly, t ); + + QgsGeometry geom = mGeometry; + geom.transform( t ); + + geom.convertToStraightSegment(); + + switch ( mSymbolType ) + { + case QgsSymbol::Marker: + { + QPolygonF points; + + if ( QgsWkbTypes::flatType( mGeometry.wkbType() ) == QgsWkbTypes::MultiPoint ) + { + const QgsGeometry patch = geom; + for ( auto it = patch.vertices_begin(); it != patch.vertices_end(); ++it ) + points << QPointF( ( *it ).x(), ( *it ).y() ); + } + else + { + points << QPointF( size.width() / 2, size.height() / 2 ); + } + return QList< QList >() << ( QList< QPolygonF >() << points ); + } + + case QgsSymbol::Line: + { + if ( !geom.isMultipart() ) + return QList< QList >() << ( QList< QPolygonF >() << geom.asQPolygonF() ); + else + { + QList< QList > res; + const QgsGeometry patch = geom; + for ( auto it = patch.const_parts_begin(); it != patch.const_parts_end(); ++it ) + { + const QgsLineString *line = qgsgeometry_cast< const QgsLineString * >( *it ); + if ( !line ) + continue; + + res << ( QList< QPolygonF >() << lineStringToQPolygonF( line ) ); + } + return res; + } + } + + case QgsSymbol::Fill: + { + QList< QList > res; + + const QgsGeometry patch = geom; + for ( auto it = patch.const_parts_begin(); it != patch.const_parts_end(); ++it ) + { + QList thisPart; + const QgsPolygon *polygon = qgsgeometry_cast< const QgsPolygon * >( *it ); + if ( !polygon ) + continue; + + if ( !polygon->exteriorRing() ) + continue; + + thisPart << lineStringToQPolygonF( qgsgeometry_cast< const QgsLineString * >( polygon->exteriorRing() ) ); + for ( int i = 0; i < polygon->numInteriorRings(); ++i ) + thisPart << lineStringToQPolygonF( qgsgeometry_cast< const QgsLineString * >( polygon->interiorRing( i ) ) ); + res << thisPart; + } + + return res; + } + + case QgsSymbol::Hybrid: + return QList< QList >(); + } + + return QList< QList >(); +} + +QList > QgsLegendPatchShape::defaultPatch( QgsSymbol::SymbolType type, QSizeF size ) +{ + switch ( type ) + { + case QgsSymbol::Marker: + return QList< QList< QPolygonF > >() << ( QList< QPolygonF >() << ( QPolygonF() << QPointF( static_cast< int >( size.width() ) / 2, + static_cast< int >( size.height() ) / 2 ) ) ); + + case QgsSymbol::Line: + // we're adding 0.5 to get rid of blurred preview: + // drawing antialiased lines of width 1 at (x,0)-(x,100) creates 2px line + return QList< QList >() << ( QList< QPolygonF >() << ( QPolygonF() << QPointF( 0, static_cast< int >( size.height() ) / 2 + 0.5 ) << QPointF( size.width(), static_cast< int >( size.height() ) / 2 + 0.5 ) ) ); + + case QgsSymbol::Fill: + return QList< QList >() << ( QList< QPolygonF> () << ( QRectF( QPointF( 0, 0 ), QPointF( static_cast< int >( size.width() ), static_cast< int >( size.height() ) ) ) ) ); + + case QgsSymbol::Hybrid: + return QList< QList >(); + } + + return QList< QList >(); +} + +void QgsLegendPatchShape::readXml( const QDomElement &element, const QgsReadWriteContext & ) +{ + mGeometry = QgsGeometry::fromWkt( element.attribute( QStringLiteral( "wkt" ) ) ); + mPreserveAspectRatio = element.attribute( QStringLiteral( "preserveAspect" ) ).toInt(); + mSymbolType = static_cast< QgsSymbol::SymbolType >( element.attribute( QStringLiteral( "type" ) ).toInt() ); +} + +void QgsLegendPatchShape::writeXml( QDomElement &element, QDomDocument &, const QgsReadWriteContext & ) const +{ + element.setAttribute( QStringLiteral( "wkt" ), mGeometry.isNull() ? QString() : mGeometry.asWkt( ) ); + element.setAttribute( QStringLiteral( "preserveAspect" ), mPreserveAspectRatio ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + element.setAttribute( QStringLiteral( "type" ), QString::number( mSymbolType ) ); +} + +QgsSymbol::SymbolType QgsLegendPatchShape::symbolType() const +{ + return mSymbolType; +} + +void QgsLegendPatchShape::setSymbolType( QgsSymbol::SymbolType type ) +{ + mSymbolType = type; +} diff --git a/src/core/layertree/qgslegendpatchshape.h b/src/core/layertree/qgslegendpatchshape.h new file mode 100644 index 000000000000..2ca21e533b51 --- /dev/null +++ b/src/core/layertree/qgslegendpatchshape.h @@ -0,0 +1,153 @@ +/*************************************************************************** + qgslegendpatchshape.h + ------------------- +begin : April 2020 +copyright : (C) 2020 by Nyall Dawson +email : nyall dot dawson 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 QGSLEGENDPATCHSHAPE_H +#define QGSLEGENDPATCHSHAPE_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgssymbol.h" + +/** + * \ingroup core + * Represents a patch shape for use in map legends. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsLegendPatchShape +{ + public: + + /** + * Constructor for a null QgsLegendPatchShape. + * + * A null QgsLegendPatchShape indicates that the default legend patch shape + * should be used instead. + */ + QgsLegendPatchShape() = default; + + /** + * Constructor for QgsLegendPatchShape. + * + * The \a type argument specifies the symbol type associated with this patch. + * + * The \a geometry argument gives the shape of the patch to render. See setGeometry() + * for further details on the geometry requirements. + * + * If \a preserveAspectRatio is TRUE, then the patch shape should preserve its aspect ratio when + * it is resized to fit a desired legend patch size. + */ + QgsLegendPatchShape( QgsSymbol::SymbolType type, + const QgsGeometry &geometry, + bool preserveAspectRatio = true ); + + /** + * Returns TRUE if the patch shape is a null QgsLegendPatchShape, + * which indicates that the default legend patch shape should be used instead. + */ + bool isNull() const; + + /** + * Returns the symbol type associated with this patch. + * + * \see setSymbolType() + */ + QgsSymbol::SymbolType symbolType() const; + + /** + * Sets the symbol \a type associated with this patch. + * + * \see symbolType() + */ + void setSymbolType( QgsSymbol::SymbolType type ); + + /** + * Returns the geometry for the patch shape. + * + * \see setGeometry() + */ + QgsGeometry geometry() const; + + /** + * Sets the \a geometry for the patch shape. + * + * The origin and size of the \a geometry is not important, as the legend + * renderer will automatically scale and transform the geometry to match + * the desired overall patch bounds. + * + * Geometries for legend patches are rendered respecting the traditional + * "y values increase toward the top of the map" convention, as opposed + * to the standard computer graphics convention of "y values increase toward + * the bottom of the display". + * + * \warning The geometry type should match the patch shape's symbolType(), + * e.g. a fill symbol type should only have Polygon or MultiPolygon geometries + * set, while a line symbol type must have LineString or MultiLineString geometries. + * + * \see geometry() + */ + void setGeometry( const QgsGeometry &geometry ); + + /** + * Returns TRUE if the patch shape should preserve its aspect ratio when + * it is resized to fit a desired legend patch size. + * + * \see setPreserveAspectRatio() + */ + bool preserveAspectRatio() const; + + /** + * Sets whether the patch shape should \a preserve its aspect ratio when + * it is resized to fit a desired legend patch size. + * + * The default behavior is to respect the geometry()'s aspect ratio. + * + * \see setPreserveAspectRatio() + */ + void setPreserveAspectRatio( bool preserve ); + + /** + * Converts the patch shape to a set of QPolygonF objects representing + * how the patch should be drawn for a symbol of the given \a type at the specified \a size (as + * geometry parts and rings). + */ + QList< QList< QPolygonF > > toQPolygonF( QgsSymbol::SymbolType type, QSizeF size ) const; + + /** + * Returns the default patch geometry for the given symbol \a type and \a size as a set of QPolygonF objects (parts and rings). + */ + static QList< QList< QPolygonF > > defaultPatch( QgsSymbol::SymbolType type, QSizeF size ); + + /** + * Read settings from a DOM \a element. + * \see writeXml() + */ + void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + /** + * Write settings into a DOM \a element. + * \see readXml() + */ + void writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) const; + + private: + QgsSymbol::SymbolType mSymbolType = QgsSymbol::Fill; + QgsGeometry mGeometry; + bool mPreserveAspectRatio = true; + +}; + +#endif // QGSLEGENDPATCHSHAPE_H diff --git a/src/core/layout/qgscompositionconverter.cpp b/src/core/layout/qgscompositionconverter.cpp index 3363656699bb..3899d2fffae9 100644 --- a/src/core/layout/qgscompositionconverter.cpp +++ b/src/core/layout/qgscompositionconverter.cpp @@ -30,6 +30,7 @@ #include "qgsproject.h" #include "qgsmaplayerstylemanager.h" #include "qgsvectorlayer.h" +#include "qgslinesymbollayer.h" #include "qgsprintlayout.h" #include "qgslayoutatlas.h" @@ -53,6 +54,7 @@ #include "qgslayoutmultiframe.h" #include "qgslayoutframe.h" #include "qgslayoutguidecollection.h" +#include "qgslayoutnortharrowhandler.h" QgsPropertiesDefinition QgsCompositionConverter::sPropertyDefinitions; @@ -688,8 +690,8 @@ bool QgsCompositionConverter::readPictureXml( QgsLayoutItemPicture *layoutItem, } //rotation map - layoutItem->mNorthMode = static_cast< QgsLayoutItemPicture::NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ); - layoutItem->mNorthOffset = itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble(); + layoutItem->mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ) ); + layoutItem->mNorthArrowHandler->setNorthOffset( itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble() ); QString rotationMapId = itemElem.attribute( QStringLiteral( "mapId" ), QStringLiteral( "-1" ) ); if ( rotationMapId != QStringLiteral( "-1" ) ) @@ -1029,10 +1031,8 @@ bool QgsCompositionConverter::readScaleBarXml( QgsLayoutItemScaleBar *layoutItem layoutItem->setMaximumBarWidth( itemElem.attribute( QStringLiteral( "maxBarWidth" ), QStringLiteral( "150" ) ).toDouble() ); layoutItem->mSegmentMillimeters = itemElem.attribute( QStringLiteral( "segmentMillimeters" ), QStringLiteral( "0.0" ) ).toDouble(); layoutItem->setMapUnitsPerScaleBarUnit( itemElem.attribute( QStringLiteral( "numMapUnitsPerScaleBarUnit" ), QStringLiteral( "1.0" ) ).toDouble() ); - layoutItem->setLineWidth( itemElem.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "0.3" ) ).toDouble() ); layoutItem->setUnitLabel( itemElem.attribute( QStringLiteral( "unitLabel" ) ) ); - layoutItem->setLineJoinStyle( QgsSymbolLayerUtils::decodePenJoinStyle( itemElem.attribute( QStringLiteral( "lineJoinStyle" ), QStringLiteral( "miter" ) ) ) ); - layoutItem->setLineCapStyle( QgsSymbolLayerUtils::decodePenCapStyle( itemElem.attribute( QStringLiteral( "lineCapStyle" ), QStringLiteral( "square" ) ) ) ); + QFont f; if ( !QgsFontUtils::setFromXmlChildNode( f, itemElem, QStringLiteral( "scaleBarFont" ) ) ) { @@ -1058,12 +1058,12 @@ bool QgsCompositionConverter::readScaleBarXml( QgsLayoutItemScaleBar *layoutItem if ( redOk && greenOk && blueOk && alphaOk ) { - layoutItem->setFillColor( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); + layoutItem->fillSymbol()->setColor( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); } } else { - layoutItem->setFillColor( QColor( itemElem.attribute( QStringLiteral( "brushColor" ), QStringLiteral( "#000000" ) ) ) ); + layoutItem->fillSymbol()->setColor( QColor( itemElem.attribute( QStringLiteral( "brushColor" ), QStringLiteral( "#000000" ) ) ) ); } //fill color 2 @@ -1081,14 +1081,20 @@ bool QgsCompositionConverter::readScaleBarXml( QgsLayoutItemScaleBar *layoutItem if ( redOk && greenOk && blueOk && alphaOk ) { - layoutItem->setFillColor2( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); + layoutItem->alternateFillSymbol()->setColor( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); } } else { - layoutItem->setFillColor2( QColor( itemElem.attribute( QStringLiteral( "brush2Color" ), QStringLiteral( "#ffffff" ) ) ) ); + layoutItem->alternateFillSymbol()->setColor( QColor( itemElem.attribute( QStringLiteral( "brush2Color" ), QStringLiteral( "#ffffff" ) ) ) ); } + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( itemElem.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "0.3" ) ).toDouble() ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setPenJoinStyle( QgsSymbolLayerUtils::decodePenJoinStyle( itemElem.attribute( QStringLiteral( "lineJoinStyle" ), QStringLiteral( "miter" ) ) ) ); + lineSymbolLayer->setPenCapStyle( QgsSymbolLayerUtils::decodePenCapStyle( itemElem.attribute( QStringLiteral( "lineCapStyle" ), QStringLiteral( "square" ) ) ) ); //stroke color QDomNodeList strokeColorList = itemElem.elementsByTagName( QStringLiteral( "strokeColor" ) ); if ( !strokeColorList.isEmpty() ) @@ -1104,19 +1110,15 @@ bool QgsCompositionConverter::readScaleBarXml( QgsLayoutItemScaleBar *layoutItem if ( redOk && greenOk && blueOk && alphaOk ) { - layoutItem->setLineColor( QColor( strokeRed, strokeGreen, strokeBlue, strokeAlpha ) ); - QPen p = layoutItem->mSettings.pen(); - p.setColor( layoutItem->mSettings.lineColor() ); - layoutItem->setPen( p ); + lineSymbolLayer->setColor( QColor( strokeRed, strokeGreen, strokeBlue, strokeAlpha ) ); } } else { - layoutItem->setLineColor( QColor( itemElem.attribute( QStringLiteral( "penColor" ), QStringLiteral( "#000000" ) ) ) ); - QPen p = layoutItem->mSettings.pen(); - p.setColor( layoutItem->mSettings.lineColor() ); - layoutItem->setPen( p ); + lineSymbolLayer->setColor( QColor( itemElem.attribute( QStringLiteral( "penColor" ), QStringLiteral( "#000000" ) ) ) ); } + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + layoutItem->setLineSymbol( lineSymbol.release() ); //font color QDomNodeList textColorList = itemElem.elementsByTagName( QStringLiteral( "textColor" ) ); diff --git a/src/core/layout/qgslayoutitemattributetable.cpp b/src/core/layout/qgslayoutitemattributetable.cpp index dbd208984bfb..43c71978f391 100644 --- a/src/core/layout/qgslayoutitemattributetable.cpp +++ b/src/core/layout/qgslayoutitemattributetable.cpp @@ -32,45 +32,6 @@ #include "qgsgeometryengine.h" #include "qgsconditionalstyle.h" -//QgsLayoutAttributeTableCompare - -///@cond PRIVATE - -/** - * Helper class for sorting tables, takes into account sorting column and ascending / descending -*/ -class CORE_EXPORT QgsLayoutAttributeTableCompare -{ - public: - - /** - * Constructor for QgsLayoutAttributeTableCompare. - */ - QgsLayoutAttributeTableCompare() = default; - bool operator()( const QVector< QPair< QVariant, QgsConditionalStyle > > &m1, const QVector< QPair< QVariant, QgsConditionalStyle > > &m2 ) - { - return ( mAscending ? qgsVariantLessThan( m1[mCurrentSortColumn].first, m2[mCurrentSortColumn].first ) - : qgsVariantGreaterThan( m1[mCurrentSortColumn].first, m2[mCurrentSortColumn].first ) ); - } - - /** - * Sets \a column number to sort by. - */ - void setSortColumn( int column ) { mCurrentSortColumn = column; } - - /** - * Sets sort order for column sorting - * Set \a ascending to true to sort in ascending order, false to sort in descending order - */ - void setAscending( bool ascending ) { mAscending = ascending; } - - private: - int mCurrentSortColumn = 0; - bool mAscending = true; -}; - -///@endcond - // // QgsLayoutItemAttributeTable // @@ -519,6 +480,12 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont req.setFilterFid( atlasFeature.id() ); } + QVector< QPair > sortColumns = sortAttributes(); + for ( int i = sortColumns.size() - 1; i >= 0; --i ) + { + req = req.addOrderBy( mColumns.at( sortColumns.at( i ).first )->attribute(), sortColumns.at( i ).second ); + } + QgsFeature f; int counter = 0; QgsFeatureIterator fit = layer->getFeatures( req ); @@ -628,16 +595,6 @@ bool QgsLayoutItemAttributeTable::getTableContents( QgsLayoutTableContents &cont ++counter; } - //sort the list, starting with the last attribute - QgsLayoutAttributeTableCompare c; - QVector< QPair > sortColumns = sortAttributes(); - for ( int i = sortColumns.size() - 1; i >= 0; --i ) - { - c.setSortColumn( sortColumns.at( i ).first ); - c.setAscending( sortColumns.at( i ).second ); - std::stable_sort( tempContents.begin(), tempContents.end(), c ); - } - // build final table contents contents.reserve( tempContents.size() ); mConditionalStyles.reserve( tempContents.size() ); diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index e6cac9940aba..001bf6f16cd9 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -31,6 +31,7 @@ #include "qgssymbollayerutils.h" #include "qgslayertreeutils.h" #include "qgslayoututils.h" +#include "qgsmapthemecollection.h" #include #include #include @@ -678,6 +679,7 @@ void QgsLayoutItemLegend::setupMapConnections( QgsLayoutItemMap *map, bool conne disconnect( map, &QgsLayoutObject::changed, this, &QgsLayoutItemLegend::updateFilterByMapAndRedraw ); disconnect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemLegend::updateFilterByMapAndRedraw ); disconnect( map, &QgsLayoutItemMap::layerStyleOverridesChanged, this, &QgsLayoutItemLegend::mapLayerStyleOverridesChanged ); + disconnect( map, &QgsLayoutItemMap::themeChanged, this, &QgsLayoutItemLegend::mapThemeChanged ); } else { @@ -685,6 +687,7 @@ void QgsLayoutItemLegend::setupMapConnections( QgsLayoutItemMap *map, bool conne connect( map, &QgsLayoutObject::changed, this, &QgsLayoutItemLegend::updateFilterByMapAndRedraw ); connect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemLegend::updateFilterByMapAndRedraw ); connect( map, &QgsLayoutItemMap::layerStyleOverridesChanged, this, &QgsLayoutItemLegend::mapLayerStyleOverridesChanged ); + connect( map, &QgsLayoutItemMap::themeChanged, this, &QgsLayoutItemLegend::mapThemeChanged ); } } @@ -700,10 +703,10 @@ void QgsLayoutItemLegend::setLinkedMap( QgsLayoutItemMap *map ) if ( mMap ) { setupMapConnections( mMap, true ); + mapThemeChanged( mMap->themeToRender( mMap->createExpressionContext() ) ); } updateFilterByMap(); - } void QgsLayoutItemLegend::invalidateCurrentMap() @@ -752,6 +755,15 @@ void QgsLayoutItemLegend::updateFilterByMapAndRedraw() updateFilterByMap( true ); } +void QgsLayoutItemLegend::setModelStyleOverrides( const QMap &overrides ) +{ + mLegendModel->setLayerStyleOverrides( overrides ); + const QList< QgsLayerTreeLayer * > layers = mLegendModel->rootGroup()->findLayers(); + for ( QgsLayerTreeLayer *nodeLayer : layers ) + mLegendModel->refreshLayerLegend( nodeLayer ); + +} + void QgsLayoutItemLegend::mapLayerStyleOverridesChanged() { if ( !mMap ) @@ -766,16 +778,47 @@ void QgsLayoutItemLegend::mapLayerStyleOverridesChanged() } else { - mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() ); - const QList< QgsLayerTreeLayer * > layers = mLegendModel->rootGroup()->findLayers(); - for ( QgsLayerTreeLayer *nodeLayer : layers ) - mLegendModel->refreshLayerLegend( nodeLayer ); + setModelStyleOverrides( mMap->layerStyleOverrides() ); } adjustBoxSize(); + updateFilterByMap( false ); } +void QgsLayoutItemLegend::mapThemeChanged( const QString &theme ) +{ + if ( mThemeName == theme ) + return; + + mThemeName = theme; + + // map's theme has been changed, so make sure to update the legend here + if ( mLegendFilterByMap ) + { + // legend is being filtered by map, so we need to re run the hit test too + // as the style overrides may also have affected the visible symbols + updateFilterByMap( false ); + } + else + { + if ( mThemeName.isEmpty() ) + { + setModelStyleOverrides( QMap() ); + } + else + { + // get style overrides for theme + const QMap overrides = mLayout->project()->mapThemeCollection()->mapThemeStyleOverrides( mThemeName ); + setModelStyleOverrides( overrides ); + } + } + + adjustBoxSize(); + + updateFilterByMap(); +} + void QgsLayoutItemLegend::updateFilterByMap( bool redraw ) { // ask for update @@ -790,7 +833,18 @@ void QgsLayoutItemLegend::updateFilterByMap( bool redraw ) void QgsLayoutItemLegend::doUpdateFilterByMap() { if ( mMap ) - mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() ); + { + if ( !mThemeName.isEmpty() ) + { + // get style overrides for theme + const QMap overrides = mLayout->project()->mapThemeCollection()->mapThemeStyleOverrides( mThemeName ); + mLegendModel->setLayerStyleOverrides( overrides ); + } + else + { + mLegendModel->setLayerStyleOverrides( mMap->layerStyleOverrides() ); + } + } else mLegendModel->setLayerStyleOverrides( QMap() ); @@ -821,6 +875,11 @@ void QgsLayoutItemLegend::doUpdateFilterByMap() mForceResize = true; } +QString QgsLayoutItemLegend::themeName() const +{ + return mThemeName; +} + void QgsLayoutItemLegend::setLegendFilterOutAtlas( bool doFilter ) { mFilterOutAtlas = doFilter; diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index eb3d3b7f87a8..291ee3aa9191 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -476,6 +476,15 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem */ QgsLayoutItemMap *linkedMap() const { return mMap; } + /** + * Returns the name of the theme currently linked to the legend. + * + * This usually equates to the theme rendered in the linkedMap(). + * + * \since QGIS 3.14 + */ + QString themeName() const; + /** * Updates the model and all legend entries. */ @@ -518,6 +527,8 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem //! update legend in case style of associated map has changed void mapLayerStyleOverridesChanged(); + //! update legend in case theme of associated map has changed + void mapThemeChanged( const QString &theme ); //! react to atlas void onAtlasEnded(); @@ -534,6 +545,8 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem void setupMapConnections( QgsLayoutItemMap *map, bool connect = true ); + void setModelStyleOverrides( const QMap &overrides ); + std::unique_ptr< QgsLegendModel > mLegendModel; std::unique_ptr< QgsLayerTreeGroup > mCustomLayerTree; @@ -567,6 +580,9 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem //! Will be TRUE if the legend should be resized automatically to fit contents bool mSizeToContents = true; + //! Name of theme for legend -- usually the theme associated with the linked map. + QString mThemeName; + friend class QgsCompositionConverter; }; diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index 166f91792a09..5959fe19c6e6 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -309,6 +309,26 @@ void QgsLayoutItemMap::storeCurrentLayerStyles() } } +void QgsLayoutItemMap::setFollowVisibilityPreset( bool follow ) +{ + if ( mFollowVisibilityPreset == follow ) + return; + + mFollowVisibilityPreset = follow; + if ( !mFollowVisibilityPresetName.isEmpty() ) + emit themeChanged( mFollowVisibilityPreset ? mFollowVisibilityPresetName : QString() ); +} + +void QgsLayoutItemMap::setFollowVisibilityPresetName( const QString &name ) +{ + if ( name == mFollowVisibilityPresetName ) + return; + + mFollowVisibilityPresetName = name; + if ( mFollowVisibilityPreset ) + emit themeChanged( mFollowVisibilityPresetName ); +} + void QgsLayoutItemMap::moveContent( double dx, double dy ) { mLastRenderedImageOffsetX -= dx; @@ -1718,6 +1738,13 @@ void QgsLayoutItemMap::refreshDataDefinedProperty( const QgsLayoutObject::DataDe { refreshLabelMargin( false ); } + if ( property == QgsLayoutObject::MapStylePreset || property == QgsLayoutObject::AllProperties ) + { + const QString previousTheme = mLastEvaluatedThemeName.isEmpty() ? mFollowVisibilityPresetName : mLastEvaluatedThemeName; + mLastEvaluatedThemeName = mDataDefinedProperties.valueAsString( QgsLayoutObject::MapStylePreset, context, mFollowVisibilityPresetName ); + if ( mLastEvaluatedThemeName != previousTheme ) + emit themeChanged( mLastEvaluatedThemeName ); + } //force redraw mCacheInvalidated = true; diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index 8ccdcebb1800..21049775ed49 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -285,20 +285,24 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem /** * Sets whether the map should follow a map theme. See followVisibilityPreset() for more details. */ - void setFollowVisibilityPreset( bool follow ) { mFollowVisibilityPreset = follow; } + void setFollowVisibilityPreset( bool follow ); /** * Preset name that decides which layers and layer styles are used for map rendering. It is only * used when followVisibilityPreset() returns TRUE. * \see setFollowVisibilityPresetName() + * + * \see themeChanged() */ QString followVisibilityPresetName() const { return mFollowVisibilityPresetName; } /** * Sets preset name for map rendering. See followVisibilityPresetName() for more details. * \see followVisibilityPresetName() + * + * \see themeChanged() */ - void setFollowVisibilityPresetName( const QString &name ) { mFollowVisibilityPresetName = name; } + void setFollowVisibilityPresetName( const QString &name ); void moveContent( double dx, double dy ) override; void setMoveContentPreviewOffset( double dx, double dy ) override; @@ -602,6 +606,16 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem */ void layerStyleOverridesChanged(); + /** + * Emitted when the map's associated \a theme is changed. + * + * \note This signal is not emitted when the definition of the theme changes, only the map + * is linked to a different theme then it previously was. + * + * \since QGIS 3.14 + */ + void themeChanged( const QString &theme ); + public slots: void refresh() override; @@ -716,6 +730,9 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem * is TRUE. May be overridden by data-defined expression. */ QString mFollowVisibilityPresetName; + //! Name of the last data-defined evaluated theme name + QString mLastEvaluatedThemeName; + /** * \brief Draw to paint device * \param painter painter diff --git a/src/core/layout/qgslayoutitemmarker.cpp b/src/core/layout/qgslayoutitemmarker.cpp new file mode 100644 index 000000000000..011fbba021fa --- /dev/null +++ b/src/core/layout/qgslayoutitemmarker.cpp @@ -0,0 +1,254 @@ +/*************************************************************************** + qgslayoutitemmarker.cpp + --------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgslayoutitemmarker.h" +#include "qgslayout.h" +#include "qgslayoututils.h" +#include "qgssymbollayerutils.h" +#include "qgslayoutmodel.h" +#include "qgsstyleentityvisitor.h" +#include "qgslayoutitemmap.h" + +#include + +QgsLayoutItemMarker::QgsLayoutItemMarker( QgsLayout *layout ) + : QgsLayoutItem( layout ) + , mNorthArrowHandler( new QgsLayoutNorthArrowHandler( this ) ) +{ + setBackgroundEnabled( false ); + setFrameEnabled( false ); + setReferencePoint( QgsLayoutItem::Middle ); + QgsStringMap properties; + properties.insert( QStringLiteral( "size" ), QStringLiteral( "4" ) ); + mShapeStyleSymbol.reset( QgsMarkerSymbol::createSimple( properties ) ); + refreshSymbol(); + + connect( this, &QgsLayoutItemMarker::sizePositionChanged, this, [ = ] + { + updateBoundingRect(); + update(); + } ); + + connect( mNorthArrowHandler, &QgsLayoutNorthArrowHandler::arrowRotationChanged, this, &QgsLayoutItemMarker::northArrowRotationChanged ); +} + +QgsLayoutItemMarker::~QgsLayoutItemMarker() = default; + +QgsLayoutItemMarker *QgsLayoutItemMarker::create( QgsLayout *layout ) +{ + return new QgsLayoutItemMarker( layout ); +} + +int QgsLayoutItemMarker::type() const +{ + return QgsLayoutItemRegistry::LayoutMarker; +} + +QIcon QgsLayoutItemMarker::icon() const +{ + return QgsApplication::getThemeIcon( QStringLiteral( "/mLayoutItemMarker.svg" ) ); +} + +void QgsLayoutItemMarker::refreshSymbol() +{ + if ( layout() ) + { + QgsRenderContext rc = QgsLayoutUtils::createRenderContextForLayout( layout(), nullptr, layout()->renderContext().dpi() ); + + std::unique_ptr< QgsMarkerSymbol > sym( mShapeStyleSymbol->clone() ); + sym->setAngle( sym->angle() + mNorthArrowRotation ); + sym->startRender( rc ); + QRectF bounds = sym->bounds( QPointF( 0, 0 ), rc ); + sym->stopRender( rc ); + mPoint = QPointF( -bounds.left() * 25.4 / layout()->renderContext().dpi(), + -bounds.top() * 25.4 / layout()->renderContext().dpi() ); + bounds.translate( mPoint ); + + QgsLayoutSize newSizeMm = QgsLayoutSize( bounds.size() * 25.4 / layout()->renderContext().dpi(), QgsUnitTypes::LayoutMillimeters ); + mFixedSize = mLayout->renderContext().measurementConverter().convert( newSizeMm, sizeWithUnits().units() ); + + attemptResize( mFixedSize ); + } + + updateBoundingRect(); + + update(); + emit frameChanged(); +} + +void QgsLayoutItemMarker::updateBoundingRect() +{ + QRectF rectangle = rect(); + + // add a bit, to account for antialiasing on stroke and miter effects on stroke + rectangle.adjust( -5, -5, 5, 5 ); + if ( rectangle != mCurrentRectangle ) + { + prepareGeometryChange(); + mCurrentRectangle = rectangle; + } +} + +void QgsLayoutItemMarker::northArrowRotationChanged( double rotation ) +{ + mNorthArrowRotation = rotation; + refreshSymbol(); +} + +void QgsLayoutItemMarker::setSymbol( QgsMarkerSymbol *symbol ) +{ + if ( !symbol ) + return; + + mShapeStyleSymbol.reset( symbol ); + refreshSymbol(); +} + +QgsMarkerSymbol *QgsLayoutItemMarker::symbol() +{ + return mShapeStyleSymbol.get(); +} + +void QgsLayoutItemMarker::setLinkedMap( QgsLayoutItemMap *map ) +{ + mNorthArrowHandler->setLinkedMap( map ); +} + +QgsLayoutItemMap *QgsLayoutItemMarker::linkedMap() const +{ + return mNorthArrowHandler->linkedMap(); +} + +QgsLayoutNorthArrowHandler::NorthMode QgsLayoutItemMarker::northMode() const +{ + return mNorthArrowHandler->northMode(); + +} + +void QgsLayoutItemMarker::setNorthMode( QgsLayoutNorthArrowHandler::NorthMode mode ) +{ + mNorthArrowHandler->setNorthMode( mode ); + +} + +double QgsLayoutItemMarker::northOffset() const +{ + return mNorthArrowHandler->northOffset(); +} + +void QgsLayoutItemMarker::setNorthOffset( double offset ) +{ + mNorthArrowHandler->setNorthOffset( offset ); +} + +QRectF QgsLayoutItemMarker::boundingRect() const +{ + return mCurrentRectangle; +} + +QgsLayoutSize QgsLayoutItemMarker::fixedSize() const +{ + return mFixedSize; +} + +bool QgsLayoutItemMarker::accept( QgsStyleEntityVisitorInterface *visitor ) const +{ + if ( mShapeStyleSymbol ) + { + QgsStyleSymbolEntity entity( mShapeStyleSymbol.get() ); + if ( !visitor->visit( QgsStyleEntityVisitorInterface::StyleLeaf( &entity, uuid(), displayName() ) ) ) + return false; + } + + return true; +} + +void QgsLayoutItemMarker::draw( QgsLayoutItemRenderContext &context ) +{ + QPainter *painter = context.renderContext().painter(); + painter->setPen( Qt::NoPen ); + painter->setBrush( Qt::NoBrush ); + + double scale = context.renderContext().convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ); + + QPointF shapePoint = mPoint * scale; + + std::unique_ptr< QgsMarkerSymbol > sym( mShapeStyleSymbol->clone() ); + sym->setAngle( sym->angle() + mNorthArrowRotation ); + sym->startRender( context.renderContext() ); + sym->renderPoint( shapePoint, nullptr, context.renderContext() ); + sym->stopRender( context.renderContext() ); +} + +bool QgsLayoutItemMarker::writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const +{ + QDomElement shapeStyleElem = QgsSymbolLayerUtils::saveSymbol( QString(), mShapeStyleSymbol.get(), document, context ); + element.appendChild( shapeStyleElem ); + + //rotation + element.setAttribute( QStringLiteral( "arrowRotation" ), QString::number( mNorthArrowRotation ) ); + if ( !mNorthArrowHandler->linkedMap() ) + { + element.setAttribute( QStringLiteral( "mapUuid" ), QString() ); + } + else + { + element.setAttribute( QStringLiteral( "mapUuid" ), mNorthArrowHandler->linkedMap()->uuid() ); + } + element.setAttribute( QStringLiteral( "northMode" ), mNorthArrowHandler->northMode() ); + element.setAttribute( QStringLiteral( "northOffset" ), mNorthArrowHandler->northOffset() ); + + return true; +} + +bool QgsLayoutItemMarker::readPropertiesFromElement( const QDomElement &element, const QDomDocument &, const QgsReadWriteContext &context ) +{ + QDomElement shapeStyleSymbolElem = element.firstChildElement( QStringLiteral( "symbol" ) ); + if ( !shapeStyleSymbolElem.isNull() ) + { + mShapeStyleSymbol.reset( QgsSymbolLayerUtils::loadSymbol( shapeStyleSymbolElem, context ) ); + } + + //picture rotation + if ( !qgsDoubleNear( element.attribute( QStringLiteral( "arrowRotation" ), QStringLiteral( "0" ) ).toDouble(), 0.0 ) ) + { + mNorthArrowRotation = element.attribute( QStringLiteral( "arrowRotation" ), QStringLiteral( "0" ) ).toDouble(); + } + + //rotation map + mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( element.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ) ); + mNorthArrowHandler->setNorthOffset( element.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble() ); + + mNorthArrowHandler->setLinkedMap( nullptr ); + mRotationMapUuid = element.attribute( QStringLiteral( "mapUuid" ) ); + + refreshSymbol(); + + return true; +} + +void QgsLayoutItemMarker::finalizeRestoreFromXml() +{ + if ( !mLayout || mRotationMapUuid.isEmpty() ) + { + mNorthArrowHandler->setLinkedMap( nullptr ); + } + else + { + mNorthArrowHandler->setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) ); + } + emit changed(); +} diff --git a/src/core/layout/qgslayoutitemmarker.h b/src/core/layout/qgslayoutitemmarker.h new file mode 100644 index 000000000000..be07d397467b --- /dev/null +++ b/src/core/layout/qgslayoutitemmarker.h @@ -0,0 +1,165 @@ +/*************************************************************************** + qgslayoutitemmarker.h + --------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSLAYOUTITEMMARKER_H +#define QGSLAYOUTITEMMARKER_H + +#include "qgis_core.h" +#include "qgslayoutitem.h" +#include "qgslayoutitemregistry.h" +#include "qgslayoutnortharrowhandler.h" + +class QgsMarkerSymbol; + +/** + * \ingroup core + * \class QgsLayoutItemMarker + * \brief A layout item for showing marker symbols. + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsLayoutItemMarker : public QgsLayoutItem +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsLayoutItemMarker, with the specified parent \a layout. + */ + explicit QgsLayoutItemMarker( QgsLayout *layout ); + ~QgsLayoutItemMarker() override; + + /** + * Returns a new marker item for the specified \a layout. + * + * The caller takes responsibility for deleting the returned object. + */ + static QgsLayoutItemMarker *create( QgsLayout *layout ) SIP_FACTORY; + + + int type() const override; + QIcon icon() const override; + + /** + * Sets the marker \a symbol used to draw the shape. Ownership is transferred. + * \see symbol() + */ + void setSymbol( QgsMarkerSymbol *symbol SIP_TRANSFER ); + + /** + * Returns the marker symbol used to draw the shape. + * \see setSymbol() + */ + QgsMarkerSymbol *symbol(); + + /** + * Sets the \a map object for rotation. + * + * If this is set then the marker will be rotated by the same + * amount as the specified map object. This is useful especially for + * syncing north arrows with a map item. + * + * \see linkedMap() + */ + void setLinkedMap( QgsLayoutItemMap *map ); + + /** + * Returns the linked rotation map, if set. An NULLPTR means map rotation is + * disabled. If this is set then the marker is rotated by the same amount + * as the specified map object. + * \see setLinkedMap() + */ + QgsLayoutItemMap *linkedMap() const; + + /** + * When the marker is linked to a map in north arrow rotation mode, + * returns the current north arrow rotation for the marker. + * + * \see setLinkedMap() + */ + double northArrowRotation() const { return mNorthArrowRotation; } + + /** + * Returns the mode used to align the marker to a map's North. + * \see setNorthMode() + * \see northOffset() + */ + QgsLayoutNorthArrowHandler::NorthMode northMode() const; + + /** + * Sets the \a mode used to align the marker to a map's North. + * \see northMode() + * \see setNorthOffset() + */ + void setNorthMode( QgsLayoutNorthArrowHandler::NorthMode mode ); + + /** + * Returns the offset added to the marker's rotation from a map's North. + * \see setNorthOffset() + * \see northMode() + */ + double northOffset() const; + + /** + * Sets the \a offset added to the marker's rotation from a map's North. + * \see northOffset() + * \see setNorthMode() + */ + void setNorthOffset( double offset ); + + // Depending on the symbol style, the bounding rectangle can be larger than the shape + QRectF boundingRect() const override; + + QgsLayoutSize fixedSize() const override; + + bool accept( QgsStyleEntityVisitorInterface *visitor ) const override; + + protected: + + void draw( QgsLayoutItemRenderContext &context ) override; + + bool writePropertiesToElement( QDomElement &element, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readPropertiesFromElement( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; + + void finalizeRestoreFromXml() override; + private slots: + + /** + * Should be called after the shape's symbol is changed. Redraws the shape and recalculates + * its selection bounds. + */ + void refreshSymbol(); + + //! Updates the bounding rect of this item + void updateBoundingRect(); + + void northArrowRotationChanged( double rotation ); + + private: + + std::unique_ptr< QgsMarkerSymbol > mShapeStyleSymbol; + + QPointF mPoint; + QRectF mCurrentRectangle; + QgsLayoutSize mFixedSize; + + QString mRotationMapUuid; + QgsLayoutNorthArrowHandler *mNorthArrowHandler = nullptr; + double mNorthArrowRotation = 0; +}; + + +#endif //QGSLAYOUTITEMMARKER_H diff --git a/src/core/layout/qgslayoutitempicture.cpp b/src/core/layout/qgslayoutitempicture.cpp index 182a8d3a814a..244ba1ec57d2 100644 --- a/src/core/layout/qgslayoutitempicture.cpp +++ b/src/core/layout/qgslayoutitempicture.cpp @@ -35,6 +35,8 @@ #include "qgsbearingutils.h" #include "qgsmapsettings.h" #include "qgsreadwritecontext.h" +#include "qgsimagecache.h" +#include "qgslayoutnortharrowhandler.h" #include #include @@ -49,6 +51,7 @@ QgsLayoutItemPicture::QgsLayoutItemPicture( QgsLayout *layout ) : QgsLayoutItem( layout ) + , mNorthArrowHandler( new QgsLayoutNorthArrowHandler( this ) ) { //default to no background setBackgroundEnabled( false ); @@ -63,6 +66,7 @@ QgsLayoutItemPicture::QgsLayoutItemPicture( QgsLayout *layout ) connect( &layout->renderContext(), &QgsLayoutRenderContext::dpiChanged, this, &QgsLayoutItemPicture::recalculateSize ); connect( this, &QgsLayoutItem::sizePositionChanged, this, &QgsLayoutItemPicture::shapeChanged ); + connect( mNorthArrowHandler, &QgsLayoutNorthArrowHandler::arrowRotationChanged, this, &QgsLayoutItemPicture::updateNorthArrowRotation ); } int QgsLayoutItemPicture::type() const @@ -344,6 +348,7 @@ void QgsLayoutItemPicture::refreshPicture( const QgsExpressionContext *context ) mHasExpressionError = false; if ( mDataDefinedProperties.isActive( QgsLayoutObject::PictureSource ) ) { + mMode = FormatUnknown; bool ok = false; const QgsProperty &sourceProperty = mDataDefinedProperties.property( QgsLayoutObject::PictureSource ); source = sourceProperty.value( *evalContext, source, &ok ); @@ -439,52 +444,49 @@ void QgsLayoutItemPicture::loadLocalPicture( const QString &path ) } } -void QgsLayoutItemPicture::disconnectMap( QgsLayoutItemMap *map ) +void QgsLayoutItemPicture::loadPictureUsingCache( const QString &path ) { - if ( map ) + switch ( mMode ) { - disconnect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - disconnect( map, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - disconnect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - } -} - -void QgsLayoutItemPicture::updateMapRotation() -{ - if ( !mRotationMap ) - return; - - // take map rotation - double rotation = mRotationMap->mapRotation() + mRotationMap->rotation(); - - // handle true north - switch ( mNorthMode ) - { - case GridNorth: - break; // nothing to do + case FormatUnknown: + break; - case TrueNorth: + case FormatRaster: { - QgsPointXY center = mRotationMap->extent().center(); - QgsCoordinateReferenceSystem crs = mRotationMap->crs(); - QgsCoordinateTransformContext transformContext = mLayout->project()->transformContext(); + bool fitsInCache = false; + mImage = QgsApplication::imageCache()->pathAsImage( path, QSize(), true, 1, fitsInCache, true ); + break; + } - try + case FormatSVG: + { + QgsExpressionContext context = createExpressionContext(); + QColor fillColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgBackgroundColor, context, mSvgFillColor ); + QColor strokeColor = mDataDefinedProperties.valueAsColor( QgsLayoutObject::PictureSvgStrokeColor, context, mSvgStrokeColor ); + double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsLayoutObject::PictureSvgStrokeWidth, context, mSvgStrokeWidth ); + const QByteArray &svgContent = QgsApplication::svgCache()->svgContent( path, rect().width(), fillColor, strokeColor, strokeWidth, + 1.0 ); + mSVG.load( svgContent ); + if ( mSVG.isValid() ) { - double bearing = QgsBearingUtils::bearingTrueNorth( crs, transformContext, center ); - rotation += bearing; + mMode = FormatSVG; + QRect viewBox = mSVG.viewBox(); //take width/height ratio from view box instead of default size + mDefaultSvgSize.setWidth( viewBox.width() ); + mDefaultSvgSize.setHeight( viewBox.height() ); } - catch ( QgsException &e ) + else { - Q_UNUSED( e ) - QgsDebugMsg( QStringLiteral( "Caught exception %1" ).arg( e.what() ) ); + mMode = FormatUnknown; } break; } } +} - rotation += mNorthOffset; - setPictureRotation( ( rotation > 360.0 ) ? rotation - 360.0 : rotation ); +void QgsLayoutItemPicture::updateNorthArrowRotation( double rotation ) +{ + setPictureRotation( rotation ); + emit pictureRotationChanged( rotation ); } void QgsLayoutItemPicture::loadPicture( const QVariant &data ) @@ -493,7 +495,7 @@ void QgsLayoutItemPicture::loadPicture( const QVariant &data ) QVariant imageData( data ); mEvaluatedPath = data.toString(); - if ( mEvaluatedPath.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) ) + if ( mEvaluatedPath.startsWith( QLatin1String( "base64:" ), Qt::CaseInsensitive ) && mMode == FormatUnknown ) { QByteArray base64 = mEvaluatedPath.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix imageData = QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals ); @@ -506,16 +508,21 @@ void QgsLayoutItemPicture::loadPicture( const QVariant &data ) mMode = FormatRaster; } } - else if ( mEvaluatedPath.startsWith( QLatin1String( "http" ) ) ) + else if ( mMode == FormatUnknown && mEvaluatedPath.startsWith( QLatin1String( "http" ) ) ) { - //remote location + //remote location (unsafe way, uses QEventLoop) - for old API/project compatibility only!! loadRemotePicture( mEvaluatedPath ); } - else + else if ( mMode == FormatUnknown ) { - //local location + //local location - for old API/project compatibility only!! loadLocalPicture( mEvaluatedPath ); } + else + { + loadPictureUsingCache( mEvaluatedPath ); + } + if ( mMode != FormatUnknown ) //make sure we start with a new QImage { recalculateSize(); @@ -649,25 +656,7 @@ void QgsLayoutItemPicture::setPictureRotation( double rotation ) void QgsLayoutItemPicture::setLinkedMap( QgsLayoutItemMap *map ) { - if ( mRotationMap ) - { - disconnectMap( mRotationMap ); - } - - if ( !map ) //disable rotation from map - { - mRotationMap = nullptr; - } - else - { - mPictureRotation = map->mapRotation(); - connect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - connect( map, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - connect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - mRotationMap = map; - updateMapRotation(); - emit pictureRotationChanged( mPictureRotation ); - } + mNorthArrowHandler->setLinkedMap( map ); } void QgsLayoutItemPicture::setResizeMode( QgsLayoutItemPicture::ResizeMode mode ) @@ -702,8 +691,9 @@ void QgsLayoutItemPicture::refreshDataDefinedProperty( const QgsLayoutObject::Da QgsLayoutItem::refreshDataDefinedProperty( property ); } -void QgsLayoutItemPicture::setPicturePath( const QString &path ) +void QgsLayoutItemPicture::setPicturePath( const QString &path, Format format ) { + mMode = format; mSourcePath = path; refreshPicture(); } @@ -732,19 +722,20 @@ bool QgsLayoutItemPicture::writePropertiesToElement( QDomElement &elem, QDomDocu elem.setAttribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( mSvgFillColor ) ); elem.setAttribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( mSvgStrokeColor ) ); elem.setAttribute( QStringLiteral( "svgBorderWidth" ), QString::number( mSvgStrokeWidth ) ); + elem.setAttribute( QStringLiteral( "mode" ), mMode ); //rotation elem.setAttribute( QStringLiteral( "pictureRotation" ), QString::number( mPictureRotation ) ); - if ( !mRotationMap ) + if ( !mNorthArrowHandler->linkedMap() ) { elem.setAttribute( QStringLiteral( "mapUuid" ), QString() ); } else { - elem.setAttribute( QStringLiteral( "mapUuid" ), mRotationMap->uuid() ); + elem.setAttribute( QStringLiteral( "mapUuid" ), mNorthArrowHandler->linkedMap()->uuid() ); } - elem.setAttribute( QStringLiteral( "northMode" ), mNorthMode ); - elem.setAttribute( QStringLiteral( "northOffset" ), mNorthOffset ); + elem.setAttribute( QStringLiteral( "northMode" ), mNorthArrowHandler->northMode() ); + elem.setAttribute( QStringLiteral( "northOffset" ), mNorthArrowHandler->northOffset() ); return true; } @@ -759,6 +750,7 @@ bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemEle mSvgFillColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgFillColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 255, 255, 255 ) ) ) ); mSvgStrokeColor = QgsSymbolLayerUtils::decodeColor( itemElem.attribute( QStringLiteral( "svgBorderColor" ), QgsSymbolLayerUtils::encodeColor( QColor( 0, 0, 0 ) ) ) ); mSvgStrokeWidth = itemElem.attribute( QStringLiteral( "svgBorderWidth" ), QStringLiteral( "0.2" ) ).toDouble(); + mMode = static_cast< Format >( itemElem.attribute( QStringLiteral( "mode" ), QString::number( FormatUnknown ) ).toInt() ); QDomNodeList composerItemList = itemElem.elementsByTagName( QStringLiteral( "ComposerItem" ) ); if ( !composerItemList.isEmpty() ) @@ -803,11 +795,10 @@ bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemEle } //rotation map - mNorthMode = static_cast< NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ); - mNorthOffset = itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble(); + mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( itemElem.attribute( QStringLiteral( "northMode" ), QStringLiteral( "0" ) ).toInt() ) ); + mNorthArrowHandler->setNorthOffset( itemElem.attribute( QStringLiteral( "northOffset" ), QStringLiteral( "0" ) ).toDouble() ); - disconnectMap( mRotationMap ); - mRotationMap = nullptr; + mNorthArrowHandler->setLinkedMap( nullptr ); mRotationMapUuid = itemElem.attribute( QStringLiteral( "mapUuid" ) ); return true; @@ -815,19 +806,27 @@ bool QgsLayoutItemPicture::readPropertiesFromElement( const QDomElement &itemEle QgsLayoutItemMap *QgsLayoutItemPicture::linkedMap() const { - return mRotationMap; + return mNorthArrowHandler->linkedMap(); +} + +QgsLayoutItemPicture::NorthMode QgsLayoutItemPicture::northMode() const +{ + return static_cast< QgsLayoutItemPicture::NorthMode >( mNorthArrowHandler->northMode() ); } void QgsLayoutItemPicture::setNorthMode( QgsLayoutItemPicture::NorthMode mode ) { - mNorthMode = mode; - updateMapRotation(); + mNorthArrowHandler->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( mode ) ); +} + +double QgsLayoutItemPicture::northOffset() const +{ + return mNorthArrowHandler->northOffset(); } void QgsLayoutItemPicture::setNorthOffset( double offset ) { - mNorthOffset = offset; - updateMapRotation(); + mNorthArrowHandler->setNorthOffset( offset ); } void QgsLayoutItemPicture::setPictureAnchor( ReferencePoint anchor ) @@ -854,24 +853,24 @@ void QgsLayoutItemPicture::setSvgStrokeWidth( double width ) refreshPicture(); } +void QgsLayoutItemPicture::setMode( QgsLayoutItemPicture::Format mode ) +{ + if ( mMode == mode ) + return; + + mMode = mode; + refreshPicture(); +} + void QgsLayoutItemPicture::finalizeRestoreFromXml() { if ( !mLayout || mRotationMapUuid.isEmpty() ) { - mRotationMap = nullptr; + mNorthArrowHandler->setLinkedMap( nullptr ); } else { - if ( mRotationMap ) - { - disconnectMap( mRotationMap ); - } - if ( ( mRotationMap = qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) ) ) - { - connect( mRotationMap, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - connect( mRotationMap, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - connect( mRotationMap, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutItemPicture::updateMapRotation ); - } + mNorthArrowHandler->setLinkedMap( qobject_cast< QgsLayoutItemMap * >( mLayout->itemByUuid( mRotationMapUuid, true ) ) ); } refreshPicture(); diff --git a/src/core/layout/qgslayoutitempicture.h b/src/core/layout/qgslayoutitempicture.h index ae9a7e7b64e3..1f231fc33cb8 100644 --- a/src/core/layout/qgslayoutitempicture.h +++ b/src/core/layout/qgslayoutitempicture.h @@ -25,6 +25,7 @@ email : nyall dot dawson at gmail dot com class QgsLayoutItemMap; class QgsExpression; +class QgsLayoutNorthArrowHandler; /** * \ingroup core @@ -85,9 +86,12 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem * Sets the source \a path of the image (may be svg or a raster format). Data defined * picture source may override this value. The path can either be a local path * or a remote (http) path. + * + * Ideally, the \a format argument should specify the image format. + * * \see picturePath() */ - void setPicturePath( const QString &path ); + void setPicturePath( const QString &path, Format format = FormatUnknown ); /** * Returns the path of the source image. Data defined picture source may override @@ -131,7 +135,7 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem * \see setNorthMode() * \see northOffset() */ - NorthMode northMode() const { return mNorthMode; } + NorthMode northMode() const; /** * Sets the \a mode used to align the picture to a map's North. @@ -145,7 +149,7 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem * \see setNorthOffset() * \see northMode() */ - double northOffset() const { return mNorthOffset; } + double northOffset() const; /** * Sets the \a offset added to the picture's rotation from a map's North. @@ -226,9 +230,17 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem /** * Returns the current picture mode (image format). + * \see setMode() */ Format mode() const { return mMode; } + /** + * Sets the current picture \a mode (image format). + * \see mode() + * \since QGIS 3.14 + */ + void setMode( Format mode ); + void finalizeRestoreFromXml() override; /** @@ -313,13 +325,6 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem double mPictureRotation = 0; QString mRotationMapUuid; - //! Map that sets the rotation (or NULLPTR if this picture uses map independent rotation) - QPointer< QgsLayoutItemMap > mRotationMap; - - //! Mode used to align to North - NorthMode mNorthMode = GridNorth; - //! Offset for north arrow - double mNorthOffset = 0.0; //! Width of the picture (in mm) double mPictureWidth = 0.0; @@ -339,6 +344,8 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem bool mIsMissingImage = false; QString mEvaluatedPath; + QgsLayoutNorthArrowHandler *mNorthArrowHandler = nullptr; + //! Loads an image file into the picture item and redraws the item void loadPicture( const QVariant &data ); @@ -358,11 +365,11 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem */ void loadLocalPicture( const QString &path ); - void disconnectMap( QgsLayoutItemMap *map ); + void loadPictureUsingCache( const QString &path ); private slots: - void updateMapRotation(); + void updateNorthArrowRotation( double rotation ); void shapeChanged(); diff --git a/src/core/layout/qgslayoutitemregistry.cpp b/src/core/layout/qgslayoutitemregistry.cpp index 5a9af00d7824..486facc236e8 100644 --- a/src/core/layout/qgslayoutitemregistry.cpp +++ b/src/core/layout/qgslayoutitemregistry.cpp @@ -30,6 +30,7 @@ #include "qgslayoutitemmanualtable.h" #include "qgslayoutitemtexttable.h" #include "qgslayoutframe.h" +#include "qgslayoutitemmarker.h" #include "qgsgloweffect.h" #include "qgseffectstack.h" #include "qgsvectorlayer.h" @@ -76,6 +77,7 @@ bool QgsLayoutItemRegistry::populate() shape->setShapeType( QgsLayoutItemShape::Rectangle ); return shape; } ) ); + addLayoutItemType( new QgsLayoutItemMetadata( LayoutMarker, QObject::tr( "Marker" ), QObject::tr( "Markers" ), QgsLayoutItemMarker::create ) ); addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolygon, QObject::tr( "Polygon" ), QObject::tr( "Polygons" ), QgsLayoutItemPolygon::create ) ); addLayoutItemType( new QgsLayoutItemMetadata( LayoutPolyline, QObject::tr( "Polyline" ), QObject::tr( "Polylines" ), QgsLayoutItemPolyline::create ) ); diff --git a/src/core/layout/qgslayoutitemregistry.h b/src/core/layout/qgslayoutitemregistry.h index 8a4779d52606..168b1e2f13e7 100644 --- a/src/core/layout/qgslayoutitemregistry.h +++ b/src/core/layout/qgslayoutitemregistry.h @@ -336,6 +336,7 @@ class CORE_EXPORT QgsLayoutItemRegistry : public QObject Layout3DMap, //!< 3D map item LayoutManualTable, //!< Manual (fixed) table + LayoutMarker, //!< Marker item // item types provided by plugins PluginItem = LayoutTextTable + 10000, //!< Starting point for plugin item types diff --git a/src/core/layout/qgslayoutitemscalebar.cpp b/src/core/layout/qgslayoutitemscalebar.cpp index 1bf0bb655279..7203b9610348 100644 --- a/src/core/layout/qgslayoutitemscalebar.cpp +++ b/src/core/layout/qgslayoutitemscalebar.cpp @@ -16,16 +16,14 @@ #include "qgslayoutitemscalebar.h" #include "qgslayoutitemregistry.h" +#include "qgsscalebarrendererregistry.h" #include "qgslayoutitemmap.h" #include "qgslayout.h" #include "qgslayoututils.h" #include "qgsdistancearea.h" +#include "qgssingleboxscalebarrenderer.h" #include "qgsscalebarrenderer.h" -#include "qgsdoubleboxscalebarrenderer.h" #include "qgsmapsettings.h" -#include "qgsnumericscalebarrenderer.h" -#include "qgssingleboxscalebarrenderer.h" -#include "qgsticksscalebarrenderer.h" #include "qgsrectangle.h" #include "qgsproject.h" #include "qgssymbollayerutils.h" @@ -35,6 +33,8 @@ #include "qgsstyleentityvisitor.h" #include "qgsnumericformat.h" #include "qgsnumericformatregistry.h" +#include "qgslinesymbollayer.h" +#include "qgsfillsymbollayer.h" #include #include @@ -67,7 +67,8 @@ QgsLayoutItemScaleBar *QgsLayoutItemScaleBar::create( QgsLayout *layout ) QgsLayoutSize QgsLayoutItemScaleBar::minimumSize() const { - return QgsLayoutSize( mStyle->calculateBoxSize( mSettings, createScaleContext() ), QgsUnitTypes::LayoutMillimeters ); + QgsRenderContext context = QgsLayoutUtils::createRenderContextForLayout( mLayout, nullptr ); + return QgsLayoutSize( mStyle->calculateBoxSize( context, mSettings, createScaleContext() ), QgsUnitTypes::LayoutMillimeters ); } void QgsLayoutItemScaleBar::draw( QgsLayoutItemRenderContext &context ) @@ -75,6 +76,40 @@ void QgsLayoutItemScaleBar::draw( QgsLayoutItemRenderContext &context ) if ( !mStyle ) return; + if ( dataDefinedProperties().isActive( QgsLayoutObject::ScalebarLineColor ) || dataDefinedProperties().isActive( QgsLayoutObject::ScalebarLineWidth ) ) + { + // compatibility code - ScalebarLineColor and ScalebarLineWidth are deprecated + QgsExpressionContext expContext = createExpressionContext(); + std::unique_ptr< QgsLineSymbol > sym( mSettings.lineSymbol()->clone() ); + Q_NOWARN_DEPRECATED_PUSH + if ( dataDefinedProperties().isActive( QgsLayoutObject::ScalebarLineWidth ) ) + sym->setWidth( mDataDefinedProperties.valueAsDouble( QgsLayoutObject::ScalebarLineWidth, expContext, mSettings.lineWidth() ) ); + if ( dataDefinedProperties().isActive( QgsLayoutObject::ScalebarLineColor ) ) + sym->setColor( mDataDefinedProperties.valueAsColor( QgsLayoutObject::ScalebarLineColor, expContext, mSettings.lineColor() ) ); + Q_NOWARN_DEPRECATED_POP + mSettings.setLineSymbol( sym.release() ); + } + if ( dataDefinedProperties().isActive( QgsLayoutObject::ScalebarFillColor ) ) + { + // compatibility code - ScalebarLineColor and ScalebarLineWidth are deprecated + QgsExpressionContext expContext = createExpressionContext(); + std::unique_ptr< QgsFillSymbol > sym( mSettings.fillSymbol()->clone() ); + Q_NOWARN_DEPRECATED_PUSH + sym->setColor( mDataDefinedProperties.valueAsColor( QgsLayoutObject::ScalebarFillColor, expContext, mSettings.fillColor() ) ); + Q_NOWARN_DEPRECATED_POP + mSettings.setFillSymbol( sym.release() ); + } + if ( dataDefinedProperties().isActive( QgsLayoutObject::ScalebarFillColor2 ) ) + { + // compatibility code - ScalebarLineColor and ScalebarLineWidth are deprecated + QgsExpressionContext expContext = createExpressionContext(); + std::unique_ptr< QgsFillSymbol > sym( mSettings.alternateFillSymbol()->clone() ); + Q_NOWARN_DEPRECATED_PUSH + sym->setColor( mDataDefinedProperties.valueAsColor( QgsLayoutObject::ScalebarFillColor2, expContext, mSettings.fillColor2() ) ); + Q_NOWARN_DEPRECATED_POP + mSettings.setAlternateFillSymbol( sym.release() ); + } + mStyle->draw( context.renderContext(), mSettings, createScaleContext() ); } @@ -149,6 +184,36 @@ void QgsLayoutItemScaleBar::setTextFormat( const QgsTextFormat &format ) emit changed(); } +QgsLineSymbol *QgsLayoutItemScaleBar::lineSymbol() const +{ + return mSettings.lineSymbol(); +} + +void QgsLayoutItemScaleBar::setLineSymbol( QgsLineSymbol *symbol ) +{ + mSettings.setLineSymbol( symbol ); +} + +QgsFillSymbol *QgsLayoutItemScaleBar::fillSymbol() const +{ + return mSettings.fillSymbol(); +} + +void QgsLayoutItemScaleBar::setFillSymbol( QgsFillSymbol *symbol ) +{ + mSettings.setFillSymbol( symbol ); +} + +QgsFillSymbol *QgsLayoutItemScaleBar::alternateFillSymbol() const +{ + return mSettings.alternateFillSymbol(); +} + +void QgsLayoutItemScaleBar::setAlternateFillSymbol( QgsFillSymbol *symbol ) +{ + mSettings.setAlternateFillSymbol( symbol ); +} + void QgsLayoutItemScaleBar::setNumberOfSegmentsLeft( int nSegmentsLeft ) { if ( !mStyle ) @@ -209,30 +274,18 @@ void QgsLayoutItemScaleBar::refreshDataDefinedProperty( const QgsLayoutObject::D //updates data defined properties and redraws item to match if ( property == QgsLayoutObject::ScalebarFillColor || property == QgsLayoutObject::AllProperties ) { - QBrush b = mSettings.brush(); - b.setColor( mDataDefinedProperties.valueAsColor( QgsLayoutObject::ScalebarFillColor, context, mSettings.fillColor() ) ); - mSettings.setBrush( b ); forceUpdate = true; } if ( property == QgsLayoutObject::ScalebarFillColor2 || property == QgsLayoutObject::AllProperties ) { - QBrush b = mSettings.brush2(); - b.setColor( mDataDefinedProperties.valueAsColor( QgsLayoutObject::ScalebarFillColor2, context, mSettings.fillColor2() ) ); - mSettings.setBrush2( b ); forceUpdate = true; } if ( property == QgsLayoutObject::ScalebarLineColor || property == QgsLayoutObject::AllProperties ) { - QPen p = mSettings.pen(); - p.setColor( mDataDefinedProperties.valueAsColor( QgsLayoutObject::ScalebarLineColor, context, mSettings.lineColor() ) ); - mSettings.setPen( p ); forceUpdate = true; } if ( property == QgsLayoutObject::ScalebarLineWidth || property == QgsLayoutObject::AllProperties ) { - QPen p = mSettings.pen(); - p.setWidthF( mDataDefinedProperties.valueAsDouble( QgsLayoutObject::ScalebarLineWidth, context, mSettings.lineWidth() ) ); - mSettings.setPen( p ); forceUpdate = true; } if ( forceUpdate ) @@ -345,26 +398,44 @@ void QgsLayoutItemScaleBar::setUnits( QgsUnitTypes::DistanceUnit u ) emit changed(); } +Qt::PenJoinStyle QgsLayoutItemScaleBar::lineJoinStyle() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.lineJoinStyle(); + Q_NOWARN_DEPRECATED_POP +} + void QgsLayoutItemScaleBar::setLineJoinStyle( Qt::PenJoinStyle style ) { + Q_NOWARN_DEPRECATED_PUSH if ( mSettings.lineJoinStyle() == style ) { //no change return; } mSettings.setLineJoinStyle( style ); + Q_NOWARN_DEPRECATED_POP update(); emit changed(); } +Qt::PenCapStyle QgsLayoutItemScaleBar::lineCapStyle() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.lineCapStyle(); + Q_NOWARN_DEPRECATED_POP +} + void QgsLayoutItemScaleBar::setLineCapStyle( Qt::PenCapStyle style ) { + Q_NOWARN_DEPRECATED_PUSH if ( mSettings.lineCapStyle() == style ) { //no change return; } mSettings.setLineCapStyle( style ); + Q_NOWARN_DEPRECATED_POP update(); emit changed(); } @@ -398,6 +469,11 @@ void QgsLayoutItemScaleBar::applyDefaultSettings() emit changed(); } +bool QgsLayoutItemScaleBar::applyDefaultRendererSettings( QgsScaleBarRenderer *renderer ) +{ + return renderer->applyDefaultSettings( mSettings ); +} + QgsUnitTypes::DistanceUnit QgsLayoutItemScaleBar::guessUnits() const { if ( !mMap ) @@ -479,7 +555,8 @@ void QgsLayoutItemScaleBar::resizeToMinimumWidth() if ( !mStyle ) return; - double widthMM = mStyle->calculateBoxSize( mSettings, createScaleContext() ).width(); + QgsRenderContext context = QgsLayoutUtils::createRenderContextForLayout( mLayout, nullptr ); + double widthMM = mStyle->calculateBoxSize( context, mSettings, createScaleContext() ).width(); QgsLayoutSize currentSize = sizeWithUnits(); currentSize.setWidth( mLayout->renderContext().measurementConverter().convert( QgsLayoutMeasurement( widthMM, QgsUnitTypes::LayoutMillimeters ), currentSize.units() ).length() ); attemptResize( currentSize ); @@ -490,7 +567,7 @@ void QgsLayoutItemScaleBar::resizeToMinimumWidth() void QgsLayoutItemScaleBar::update() { //Don't adjust box size for numeric scale bars: - if ( mStyle && mStyle->name() != QLatin1String( "Numeric" ) ) + if ( mStyle && mStyle->id() != QLatin1String( "Numeric" ) ) { refreshItemSize(); } @@ -507,34 +584,10 @@ void QgsLayoutItemScaleBar::updateScale() void QgsLayoutItemScaleBar::setStyle( const QString &styleName ) { //switch depending on style name - if ( styleName == QLatin1String( "Single Box" ) ) - { - mStyle = qgis::make_unique< QgsSingleBoxScaleBarRenderer >(); - } - else if ( styleName == QLatin1String( "Double Box" ) ) - { - mStyle = qgis::make_unique< QgsDoubleBoxScaleBarRenderer >(); - } - else if ( styleName == QLatin1String( "Line Ticks Middle" ) || styleName == QLatin1String( "Line Ticks Down" ) || styleName == QLatin1String( "Line Ticks Up" ) ) + std::unique_ptr< QgsScaleBarRenderer> renderer( QgsApplication::scaleBarRendererRegistry()->renderer( styleName ) ); + if ( renderer ) { - std::unique_ptr< QgsTicksScaleBarRenderer > tickStyle = qgis::make_unique< QgsTicksScaleBarRenderer >(); - if ( styleName == QLatin1String( "Line Ticks Middle" ) ) - { - tickStyle->setTickPosition( QgsTicksScaleBarRenderer::TicksMiddle ); - } - else if ( styleName == QLatin1String( "Line Ticks Down" ) ) - { - tickStyle->setTickPosition( QgsTicksScaleBarRenderer::TicksDown ); - } - else if ( styleName == QLatin1String( "Line Ticks Up" ) ) - { - tickStyle->setTickPosition( QgsTicksScaleBarRenderer::TicksUp ); - } - mStyle = std::move( tickStyle ); - } - else if ( styleName == QLatin1String( "Numeric" ) ) - { - mStyle = qgis::make_unique< QgsNumericScaleBarRenderer >(); + mStyle = std::move( renderer ); } refreshItemSize(); emit changed(); @@ -544,7 +597,7 @@ QString QgsLayoutItemScaleBar::style() const { if ( mStyle ) { - return mStyle->name(); + return mStyle->id(); } else { @@ -589,6 +642,83 @@ void QgsLayoutItemScaleBar::setFontColor( const QColor &color ) mSettings.textFormat().setOpacity( color.alphaF() ); } +QColor QgsLayoutItemScaleBar::fillColor() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.fillColor(); + Q_NOWARN_DEPRECATED_POP +} + +void QgsLayoutItemScaleBar::setFillColor( const QColor &color ) +{ + Q_NOWARN_DEPRECATED_PUSH + mSettings.setFillColor( color ); + Q_NOWARN_DEPRECATED_POP +} + +QColor QgsLayoutItemScaleBar::fillColor2() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.fillColor2(); + Q_NOWARN_DEPRECATED_POP +} + +void QgsLayoutItemScaleBar::setFillColor2( const QColor &color ) +{ + Q_NOWARN_DEPRECATED_PUSH + mSettings.setFillColor2( color ); + Q_NOWARN_DEPRECATED_POP +} + +QColor QgsLayoutItemScaleBar::lineColor() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.lineColor(); + Q_NOWARN_DEPRECATED_POP +} + +void QgsLayoutItemScaleBar::setLineColor( const QColor &color ) +{ + Q_NOWARN_DEPRECATED_PUSH + mSettings.setLineColor( color ); + Q_NOWARN_DEPRECATED_POP +} + +double QgsLayoutItemScaleBar::lineWidth() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.lineWidth(); + Q_NOWARN_DEPRECATED_POP +} + +void QgsLayoutItemScaleBar::setLineWidth( double width ) +{ + Q_NOWARN_DEPRECATED_PUSH + mSettings.setLineWidth( width ); + Q_NOWARN_DEPRECATED_POP +} + +QPen QgsLayoutItemScaleBar::pen() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.pen(); + Q_NOWARN_DEPRECATED_POP +} + +QBrush QgsLayoutItemScaleBar::brush() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.brush(); + Q_NOWARN_DEPRECATED_POP +} + +QBrush QgsLayoutItemScaleBar::brush2() const +{ + Q_NOWARN_DEPRECATED_PUSH + return mSettings.brush2(); + Q_NOWARN_DEPRECATED_POP +} + bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScaleBarElem, QDomDocument &doc, const QgsReadWriteContext &rwContext ) const { composerScaleBarElem.setAttribute( QStringLiteral( "height" ), QString::number( mSettings.height() ) ); @@ -606,11 +736,22 @@ bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScale QDomElement textElem = mSettings.textFormat().writeXml( doc, rwContext ); composerScaleBarElem.appendChild( textElem ); + Q_NOWARN_DEPRECATED_PUSH + // kept just for allowing projects to open in QGIS < 3.14, remove for 4.0 composerScaleBarElem.setAttribute( QStringLiteral( "outlineWidth" ), QString::number( mSettings.lineWidth() ) ); - composerScaleBarElem.setAttribute( QStringLiteral( "unitLabel" ), mSettings.unitLabel() ); - composerScaleBarElem.setAttribute( QStringLiteral( "unitType" ), QgsUnitTypes::encodeUnit( mSettings.units() ) ); composerScaleBarElem.setAttribute( QStringLiteral( "lineJoinStyle" ), QgsSymbolLayerUtils::encodePenJoinStyle( mSettings.lineJoinStyle() ) ); composerScaleBarElem.setAttribute( QStringLiteral( "lineCapStyle" ), QgsSymbolLayerUtils::encodePenCapStyle( mSettings.lineCapStyle() ) ); + //pen color + QDomElement strokeColorElem = doc.createElement( QStringLiteral( "strokeColor" ) ); + strokeColorElem.setAttribute( QStringLiteral( "red" ), QString::number( mSettings.lineColor().red() ) ); + strokeColorElem.setAttribute( QStringLiteral( "green" ), QString::number( mSettings.lineColor().green() ) ); + strokeColorElem.setAttribute( QStringLiteral( "blue" ), QString::number( mSettings.lineColor().blue() ) ); + strokeColorElem.setAttribute( QStringLiteral( "alpha" ), QString::number( mSettings.lineColor().alpha() ) ); + composerScaleBarElem.appendChild( strokeColorElem ); + Q_NOWARN_DEPRECATED_POP + + composerScaleBarElem.setAttribute( QStringLiteral( "unitLabel" ), mSettings.unitLabel() ); + composerScaleBarElem.setAttribute( QStringLiteral( "unitType" ), QgsUnitTypes::encodeUnit( mSettings.units() ) ); QDomElement numericFormatElem = doc.createElement( QStringLiteral( "numericFormat" ) ); mSettings.numericFormat()->writeXml( numericFormatElem, doc, rwContext ); @@ -619,7 +760,7 @@ bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScale //style if ( mStyle ) { - composerScaleBarElem.setAttribute( QStringLiteral( "style" ), mStyle->name() ); + composerScaleBarElem.setAttribute( QStringLiteral( "style" ), mStyle->id() ); } //map id @@ -630,6 +771,9 @@ bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScale //colors + Q_NOWARN_DEPRECATED_PUSH + // kept just for allowing projects to open in QGIS < 3.14, remove for 4.0 + //fill color QDomElement fillColorElem = doc.createElement( QStringLiteral( "fillColor" ) ); fillColorElem.setAttribute( QStringLiteral( "red" ), QString::number( mSettings.fillColor().red() ) ); @@ -646,13 +790,7 @@ bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScale fillColor2Elem.setAttribute( QStringLiteral( "alpha" ), QString::number( mSettings.fillColor2().alpha() ) ); composerScaleBarElem.appendChild( fillColor2Elem ); - //pen color - QDomElement strokeColorElem = doc.createElement( QStringLiteral( "strokeColor" ) ); - strokeColorElem.setAttribute( QStringLiteral( "red" ), QString::number( mSettings.lineColor().red() ) ); - strokeColorElem.setAttribute( QStringLiteral( "green" ), QString::number( mSettings.lineColor().green() ) ); - strokeColorElem.setAttribute( QStringLiteral( "blue" ), QString::number( mSettings.lineColor().blue() ) ); - strokeColorElem.setAttribute( QStringLiteral( "alpha" ), QString::number( mSettings.lineColor().alpha() ) ); - composerScaleBarElem.appendChild( strokeColorElem ); + Q_NOWARN_DEPRECATED_POP //label vertical/horizontal placement composerScaleBarElem.setAttribute( QStringLiteral( "labelVerticalPlacement" ), QString::number( static_cast< int >( mSettings.labelVerticalPlacement() ) ) ); @@ -661,6 +799,30 @@ bool QgsLayoutItemScaleBar::writePropertiesToElement( QDomElement &composerScale //alignment composerScaleBarElem.setAttribute( QStringLiteral( "alignment" ), QString::number( static_cast< int >( mSettings.alignment() ) ) ); + QDomElement lineSymbol = doc.createElement( QStringLiteral( "lineSymbol" ) ); + const QDomElement symbolElem = QgsSymbolLayerUtils::saveSymbol( QString(), + mSettings.lineSymbol(), + doc, + rwContext ); + lineSymbol.appendChild( symbolElem ); + composerScaleBarElem.appendChild( lineSymbol ); + + QDomElement fillSymbol1Elem = doc.createElement( QStringLiteral( "fillSymbol1" ) ); + const QDomElement symbol1Elem = QgsSymbolLayerUtils::saveSymbol( QString(), + mSettings.fillSymbol(), + doc, + rwContext ); + fillSymbol1Elem.appendChild( symbol1Elem ); + composerScaleBarElem.appendChild( fillSymbol1Elem ); + + QDomElement fillSymbol2Elem = doc.createElement( QStringLiteral( "fillSymbol2" ) ); + const QDomElement symbol2Elem = QgsSymbolLayerUtils::saveSymbol( QString(), + mSettings.alternateFillSymbol(), + doc, + rwContext ); + fillSymbol2Elem.appendChild( symbol2Elem ); + composerScaleBarElem.appendChild( fillSymbol2Elem ); + return true; } @@ -677,10 +839,64 @@ bool QgsLayoutItemScaleBar::readPropertiesFromElement( const QDomElement &itemEl mSettings.setMaximumBarWidth( itemElem.attribute( QStringLiteral( "maxBarWidth" ), QStringLiteral( "150" ) ).toDouble() ); mSegmentMillimeters = itemElem.attribute( QStringLiteral( "segmentMillimeters" ), QStringLiteral( "0.0" ) ).toDouble(); mSettings.setMapUnitsPerScaleBarUnit( itemElem.attribute( QStringLiteral( "numMapUnitsPerScaleBarUnit" ), QStringLiteral( "1.0" ) ).toDouble() ); - mSettings.setLineWidth( itemElem.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "0.3" ) ).toDouble() ); + + QDomElement lineSymbolElem = itemElem.firstChildElement( QStringLiteral( "lineSymbol" ) ); + bool foundLineSymbol = false; + if ( !lineSymbolElem.isNull() ) + { + QDomElement symbolElem = lineSymbolElem.firstChildElement( QStringLiteral( "symbol" ) ); + std::unique_ptr< QgsLineSymbol > lineSymbol( QgsSymbolLayerUtils::loadSymbol( symbolElem, context ) ); + if ( lineSymbol ) + { + mSettings.setLineSymbol( lineSymbol.release() ); + foundLineSymbol = true; + } + } + if ( !foundLineSymbol ) + { + // old project compatibility + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( itemElem.attribute( QStringLiteral( "outlineWidth" ), QStringLiteral( "0.3" ) ).toDouble() ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setPenJoinStyle( QgsSymbolLayerUtils::decodePenJoinStyle( itemElem.attribute( QStringLiteral( "lineJoinStyle" ), QStringLiteral( "miter" ) ) ) ); + lineSymbolLayer->setPenCapStyle( QgsSymbolLayerUtils::decodePenCapStyle( itemElem.attribute( QStringLiteral( "lineCapStyle" ), QStringLiteral( "square" ) ) ) ); + + //stroke color + QDomNodeList strokeColorList = itemElem.elementsByTagName( QStringLiteral( "strokeColor" ) ); + if ( !strokeColorList.isEmpty() ) + { + QDomElement strokeColorElem = strokeColorList.at( 0 ).toElement(); + bool redOk, greenOk, blueOk, alphaOk; + int strokeRed, strokeGreen, strokeBlue, strokeAlpha; + + strokeRed = strokeColorElem.attribute( QStringLiteral( "red" ) ).toDouble( &redOk ); + strokeGreen = strokeColorElem.attribute( QStringLiteral( "green" ) ).toDouble( &greenOk ); + strokeBlue = strokeColorElem.attribute( QStringLiteral( "blue" ) ).toDouble( &blueOk ); + strokeAlpha = strokeColorElem.attribute( QStringLiteral( "alpha" ) ).toDouble( &alphaOk ); + + if ( redOk && greenOk && blueOk && alphaOk ) + { + lineSymbolLayer->setColor( QColor( strokeRed, strokeGreen, strokeBlue, strokeAlpha ) ); + } + } + else + { + lineSymbolLayer->setColor( QColor( itemElem.attribute( QStringLiteral( "penColor" ), QStringLiteral( "#000000" ) ) ) ); + } + + // need to translate the deprecated ScalebarLineWidth and ScalebarLineColor properties to symbol properties, + // and then remove them from the scalebar so they don't interfere and apply to other compatibility workarounds + lineSymbolLayer->setDataDefinedProperty( QgsSymbolLayer::PropertyStrokeWidth, dataDefinedProperties().property( QgsLayoutObject::ScalebarLineWidth ) ); + dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarLineWidth, QgsProperty() ); + lineSymbolLayer->setDataDefinedProperty( QgsSymbolLayer::PropertyStrokeColor, dataDefinedProperties().property( QgsLayoutObject::ScalebarLineColor ) ); + dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarLineColor, QgsProperty() ); + + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + mSettings.setLineSymbol( lineSymbol.release() ); + } + mSettings.setUnitLabel( itemElem.attribute( QStringLiteral( "unitLabel" ) ) ); - mSettings.setLineJoinStyle( QgsSymbolLayerUtils::decodePenJoinStyle( itemElem.attribute( QStringLiteral( "lineJoinStyle" ), QStringLiteral( "miter" ) ) ) ); - mSettings.setLineCapStyle( QgsSymbolLayerUtils::decodePenCapStyle( itemElem.attribute( QStringLiteral( "lineCapStyle" ), QStringLiteral( "square" ) ) ) ); QDomNodeList textFormatNodeList = itemElem.elementsByTagName( QStringLiteral( "text-style" ) ); if ( !textFormatNodeList.isEmpty() ) @@ -715,80 +931,108 @@ bool QgsLayoutItemScaleBar::readPropertiesFromElement( const QDomElement &itemEl mSettings.setNumericFormat( QgsApplication::numericFormatRegistry()->createFromXml( numericFormatElem, context ) ); } - //colors - //fill color - QDomNodeList fillColorList = itemElem.elementsByTagName( QStringLiteral( "fillColor" ) ); - if ( !fillColorList.isEmpty() ) + QDomElement fillSymbol1Elem = itemElem.firstChildElement( QStringLiteral( "fillSymbol1" ) ); + bool foundFillSymbol1 = false; + if ( !fillSymbol1Elem.isNull() ) { - QDomElement fillColorElem = fillColorList.at( 0 ).toElement(); - bool redOk, greenOk, blueOk, alphaOk; - int fillRed, fillGreen, fillBlue, fillAlpha; - - fillRed = fillColorElem.attribute( QStringLiteral( "red" ) ).toDouble( &redOk ); - fillGreen = fillColorElem.attribute( QStringLiteral( "green" ) ).toDouble( &greenOk ); - fillBlue = fillColorElem.attribute( QStringLiteral( "blue" ) ).toDouble( &blueOk ); - fillAlpha = fillColorElem.attribute( QStringLiteral( "alpha" ) ).toDouble( &alphaOk ); - - if ( redOk && greenOk && blueOk && alphaOk ) + QDomElement symbolElem = fillSymbol1Elem.firstChildElement( QStringLiteral( "symbol" ) ); + std::unique_ptr< QgsFillSymbol > fillSymbol( QgsSymbolLayerUtils::loadSymbol( symbolElem, context ) ); + if ( fillSymbol ) { - mSettings.setFillColor( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); + mSettings.setFillSymbol( fillSymbol.release() ); + foundFillSymbol1 = true; } } - else - { - mSettings.setFillColor( QColor( itemElem.attribute( QStringLiteral( "brushColor" ), QStringLiteral( "#000000" ) ) ) ); - } - - //fill color 2 - QDomNodeList fillColor2List = itemElem.elementsByTagName( QStringLiteral( "fillColor2" ) ); - if ( !fillColor2List.isEmpty() ) + if ( !foundFillSymbol1 ) { - QDomElement fillColor2Elem = fillColor2List.at( 0 ).toElement(); - bool redOk, greenOk, blueOk, alphaOk; - int fillRed, fillGreen, fillBlue, fillAlpha; + // old project compatibility + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsSimpleFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsSimpleFillSymbolLayer >(); + fillSymbolLayer->setStrokeStyle( Qt::NoPen ); + + //fill color + QDomNodeList fillColorList = itemElem.elementsByTagName( QStringLiteral( "fillColor" ) ); + if ( !fillColorList.isEmpty() ) + { + QDomElement fillColorElem = fillColorList.at( 0 ).toElement(); + bool redOk, greenOk, blueOk, alphaOk; + int fillRed, fillGreen, fillBlue, fillAlpha; - fillRed = fillColor2Elem.attribute( QStringLiteral( "red" ) ).toDouble( &redOk ); - fillGreen = fillColor2Elem.attribute( QStringLiteral( "green" ) ).toDouble( &greenOk ); - fillBlue = fillColor2Elem.attribute( QStringLiteral( "blue" ) ).toDouble( &blueOk ); - fillAlpha = fillColor2Elem.attribute( QStringLiteral( "alpha" ) ).toDouble( &alphaOk ); + fillRed = fillColorElem.attribute( QStringLiteral( "red" ) ).toDouble( &redOk ); + fillGreen = fillColorElem.attribute( QStringLiteral( "green" ) ).toDouble( &greenOk ); + fillBlue = fillColorElem.attribute( QStringLiteral( "blue" ) ).toDouble( &blueOk ); + fillAlpha = fillColorElem.attribute( QStringLiteral( "alpha" ) ).toDouble( &alphaOk ); - if ( redOk && greenOk && blueOk && alphaOk ) + if ( redOk && greenOk && blueOk && alphaOk ) + { + fillSymbolLayer->setColor( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); + } + } + else { - mSettings.setFillColor2( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); + fillSymbolLayer->setColor( QColor( itemElem.attribute( QStringLiteral( "brushColor" ), QStringLiteral( "#000000" ) ) ) ); } + + // need to translate the deprecated ScalebarFillColor property to symbol properties, + // and then remove them from the scalebar so they don't interfere and apply to other compatibility workarounds + fillSymbolLayer->setDataDefinedProperty( QgsSymbolLayer::PropertyFillColor, dataDefinedProperties().property( QgsLayoutObject::ScalebarFillColor ) ); + dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarFillColor, QgsProperty() ); + + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + mSettings.setFillSymbol( fillSymbol.release() ); } - else + + QDomElement fillSymbol2Elem = itemElem.firstChildElement( QStringLiteral( "fillSymbol2" ) ); + bool foundFillSymbol2 = false; + if ( !fillSymbol2Elem.isNull() ) { - mSettings.setFillColor2( QColor( itemElem.attribute( QStringLiteral( "brush2Color" ), QStringLiteral( "#ffffff" ) ) ) ); + QDomElement symbolElem = fillSymbol2Elem.firstChildElement( QStringLiteral( "symbol" ) ); + std::unique_ptr< QgsFillSymbol > fillSymbol( QgsSymbolLayerUtils::loadSymbol( symbolElem, context ) ); + if ( fillSymbol ) + { + mSettings.setAlternateFillSymbol( fillSymbol.release() ); + foundFillSymbol2 = true; + } } - - //stroke color - QDomNodeList strokeColorList = itemElem.elementsByTagName( QStringLiteral( "strokeColor" ) ); - if ( !strokeColorList.isEmpty() ) + if ( !foundFillSymbol2 ) { - QDomElement strokeColorElem = strokeColorList.at( 0 ).toElement(); - bool redOk, greenOk, blueOk, alphaOk; - int strokeRed, strokeGreen, strokeBlue, strokeAlpha; + // old project compatibility + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsSimpleFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsSimpleFillSymbolLayer >(); + fillSymbolLayer->setStrokeStyle( Qt::NoPen ); - strokeRed = strokeColorElem.attribute( QStringLiteral( "red" ) ).toDouble( &redOk ); - strokeGreen = strokeColorElem.attribute( QStringLiteral( "green" ) ).toDouble( &greenOk ); - strokeBlue = strokeColorElem.attribute( QStringLiteral( "blue" ) ).toDouble( &blueOk ); - strokeAlpha = strokeColorElem.attribute( QStringLiteral( "alpha" ) ).toDouble( &alphaOk ); + //fill color 2 - if ( redOk && greenOk && blueOk && alphaOk ) + QDomNodeList fillColor2List = itemElem.elementsByTagName( QStringLiteral( "fillColor2" ) ); + if ( !fillColor2List.isEmpty() ) { - mSettings.setLineColor( QColor( strokeRed, strokeGreen, strokeBlue, strokeAlpha ) ); - QPen p = mSettings.pen(); - p.setColor( mSettings.lineColor() ); - mSettings.setPen( p ); + QDomElement fillColor2Elem = fillColor2List.at( 0 ).toElement(); + bool redOk, greenOk, blueOk, alphaOk; + int fillRed, fillGreen, fillBlue, fillAlpha; + + fillRed = fillColor2Elem.attribute( QStringLiteral( "red" ) ).toDouble( &redOk ); + fillGreen = fillColor2Elem.attribute( QStringLiteral( "green" ) ).toDouble( &greenOk ); + fillBlue = fillColor2Elem.attribute( QStringLiteral( "blue" ) ).toDouble( &blueOk ); + fillAlpha = fillColor2Elem.attribute( QStringLiteral( "alpha" ) ).toDouble( &alphaOk ); + + if ( redOk && greenOk && blueOk && alphaOk ) + { + fillSymbolLayer->setColor( QColor( fillRed, fillGreen, fillBlue, fillAlpha ) ); + } } - } - else - { - mSettings.setLineColor( QColor( itemElem.attribute( QStringLiteral( "penColor" ), QStringLiteral( "#000000" ) ) ) ); - QPen p = mSettings.pen(); - p.setColor( mSettings.lineColor() ); - mSettings.setPen( p ); + else + { + fillSymbolLayer->setColor( QColor( itemElem.attribute( QStringLiteral( "brush2Color" ), QStringLiteral( "#ffffff" ) ) ) ); + } + + // need to translate the deprecated ScalebarFillColor2 property to symbol properties, + // and then remove them from the scalebar so they don't interfere and apply to other compatibility workarounds + fillSymbolLayer->setDataDefinedProperty( QgsSymbolLayer::PropertyFillColor, dataDefinedProperties().property( QgsLayoutObject::ScalebarFillColor2 ) ); + dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarFillColor2, QgsProperty() ); + + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + mSettings.setAlternateFillSymbol( fillSymbol.release() ); + } //font color @@ -817,8 +1061,7 @@ bool QgsLayoutItemScaleBar::readPropertiesFromElement( const QDomElement &itemEl } //style - QString styleString = itemElem.attribute( QStringLiteral( "style" ), QString() ); - setStyle( styleString.toLocal8Bit().data() ); + setStyle( itemElem.attribute( QStringLiteral( "style" ), QString() ) ); if ( itemElem.attribute( QStringLiteral( "unitType" ) ).isEmpty() ) { diff --git a/src/core/layout/qgslayoutitemscalebar.h b/src/core/layout/qgslayoutitemscalebar.h index 354bc040c12e..bc4c7fd01f02 100644 --- a/src/core/layout/qgslayoutitemscalebar.h +++ b/src/core/layout/qgslayoutitemscalebar.h @@ -188,6 +188,67 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem */ void setTextFormat( const QgsTextFormat &format ); + /** + * Returns the line symbol used to render the scalebar (only used for some scalebar types). + * + * Ownership is not transferred. + * + * \see setLineSymbol() + * \since QGIS 3.14 + */ + QgsLineSymbol *lineSymbol() const; + + /** + * Sets the line \a symbol used to render the scalebar (only used for some scalebar types). Ownership of \a symbol is + * transferred to the scalebar. + * + * \see lineSymbol() + * \since QGIS 3.14 + */ + void setLineSymbol( QgsLineSymbol *symbol SIP_TRANSFER ); + + /** + * Returns the primary fill symbol used to render the scalebar (only used for some scalebar types). + * + * Ownership is not transferred. + * + * \see setFillSymbol() + * \see alternateFillSymbol() + * \since QGIS 3.14 + */ + QgsFillSymbol *fillSymbol() const; + + /** + * Sets the primary fill \a symbol used to render the scalebar (only used for some scalebar types). Ownership of \a symbol is + * transferred to the scalebar. + * + * \see fillSymbol() + * \see setAlternateFillSymbol() + * \since QGIS 3.14 + */ + void setFillSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + + /** + * Returns the secondary fill symbol used to render the scalebar (only used for some scalebar types). + * + * Ownership is not transferred. + * + * \see setAlternateFillSymbol() + * \see fillSymbol() + * \since QGIS 3.14 + */ + QgsFillSymbol *alternateFillSymbol() const; + + /** + * Sets the secondary fill \a symbol used to render the scalebar (only used for some scalebar types). Ownership of \a symbol is + * transferred to the scalebar. + * + * \see alternateFillSymbol() + * \see setFillSymbol() + * \since QGIS 3.14 + */ + void setAlternateFillSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + /** * Returns the font used for drawing text in the scalebar. * \see setFont() @@ -222,75 +283,86 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem * Returns the color used for fills in the scalebar. * \see setFillColor() * \see fillColor2() + * \deprecated use fillSymbol() instead */ - QColor fillColor() const { return mSettings.fillColor(); } + Q_DECL_DEPRECATED QColor fillColor() const SIP_DEPRECATED; /** * Sets the \a color used for fills in the scalebar. * \see fillColor() * \see setFillColor2() + * \deprecated use setFillSymbol() instead */ - void setFillColor( const QColor &color ) { mSettings.setFillColor( color ); } + Q_DECL_DEPRECATED void setFillColor( const QColor &color ) SIP_DEPRECATED; /** * Returns the secondary color used for fills in the scalebar. * \see setFillColor2() * \see fillColor() + * \deprecated use alternateFillSymbol() instead */ - QColor fillColor2() const { return mSettings.fillColor2(); } + Q_DECL_DEPRECATED QColor fillColor2() const SIP_DEPRECATED; /** * Sets the secondary \a color used for fills in the scalebar. * \see fillColor2() * \see setFillColor2() + * \deprecated use setAlternateFillSymbol() instead */ - void setFillColor2( const QColor &color ) { mSettings.setFillColor2( color ); } + Q_DECL_DEPRECATED void setFillColor2( const QColor &color ) SIP_DEPRECATED; /** * Returns the color used for lines in the scalebar. * \see setLineColor() + * \deprecated use lineSymbol() instead */ - QColor lineColor() const { return mSettings.lineColor(); } + Q_DECL_DEPRECATED QColor lineColor() const SIP_DEPRECATED; /** * Sets the \a color used for lines in the scalebar. * \see lineColor() + * \deprecated use setLineSymbol() instead */ - void setLineColor( const QColor &color ) { mSettings.setLineColor( color ); } + Q_DECL_DEPRECATED void setLineColor( const QColor &color ) SIP_DEPRECATED; /** * Returns the line width in millimeters for lines in the scalebar. * \see setLineWidth() + * \deprecated use lineSymbol() instead */ - double lineWidth() const { return mSettings.lineWidth(); } + Q_DECL_DEPRECATED double lineWidth() const SIP_DEPRECATED; /** * Sets the line \a width in millimeters for lines in the scalebar. * \see lineWidth() + * \deprecated use setLineSymbol() instead */ - void setLineWidth( double width ) { mSettings.setLineWidth( width ); } + Q_DECL_DEPRECATED void setLineWidth( double width ) SIP_DEPRECATED; /** * Returns the pen used for drawing outlines in the scalebar. * \see brush() + * \deprecated use lineSymbol() instead */ - QPen pen() const { return mSettings.pen(); } + Q_DECL_DEPRECATED QPen pen() const SIP_DEPRECATED; /** * Returns the primary brush for the scalebar. * \returns QBrush used for filling the scalebar * \see brush2 * \see pen + * \deprecated use fillSymbol() instead */ - QBrush brush() const {return mSettings.brush();} + Q_DECL_DEPRECATED QBrush brush() const SIP_DEPRECATED; /** * Returns the secondary brush for the scalebar. This is used for alternating color style scalebars, such * as single and double box styles. * \returns QBrush used for secondary color areas * \see brush + * \deprecated use alternateFillSymbol() instead */ - QBrush brush2() const {return mSettings.brush2(); } + Q_DECL_DEPRECATED QBrush brush2() const SIP_DEPRECATED; /** * Returns the scalebar height (in millimeters). @@ -395,26 +467,30 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem /** * Returns the join style used for drawing lines in the scalebar. * \see setLineJoinStyle() + * \deprecated use lineSymbol() instead */ - Qt::PenJoinStyle lineJoinStyle() const { return mSettings.lineJoinStyle(); } + Q_DECL_DEPRECATED Qt::PenJoinStyle lineJoinStyle() const SIP_DEPRECATED; /** * Sets the join \a style used when drawing the lines in the scalebar * \see lineJoinStyle() + * \deprecated use setLineSymbol() instead */ - void setLineJoinStyle( Qt::PenJoinStyle style ); + Q_DECL_DEPRECATED void setLineJoinStyle( Qt::PenJoinStyle style ) SIP_DEPRECATED; /** * Returns the cap style used for drawing lines in the scalebar. * \see setLineCapStyle() + * \deprecated use lineSymbol() instead */ - Qt::PenCapStyle lineCapStyle() const { return mSettings.lineCapStyle(); } + Q_DECL_DEPRECATED Qt::PenCapStyle lineCapStyle() const SIP_DEPRECATED; /** * Sets the cap \a style used when drawing the lines in the scalebar. * \see lineCapStyle() + * \deprecated use setLineSymbol() instead */ - void setLineCapStyle( Qt::PenCapStyle style ); + Q_DECL_DEPRECATED void setLineCapStyle( Qt::PenCapStyle style ) SIP_DEPRECATED; /** * Applies the default scalebar settings to the scale bar. @@ -422,6 +498,15 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem */ void applyDefaultSettings(); + /** + * Applies any default settings relating to the specified \a renderer to the item. + * + * Returns TRUE if settings were applied. + * + * \since QGIS 3.14 + */ + bool applyDefaultRendererSettings( QgsScaleBarRenderer *renderer ); + /** * Attempts to guess the most reasonable unit choice for the scalebar, given * the current linked map's scale. diff --git a/src/core/layout/qgslayoutnortharrowhandler.cpp b/src/core/layout/qgslayoutnortharrowhandler.cpp new file mode 100644 index 000000000000..4c4305197a65 --- /dev/null +++ b/src/core/layout/qgslayoutnortharrowhandler.cpp @@ -0,0 +1,122 @@ +/*************************************************************************** + qgslayoutnortharrowhandler.cpp + ------------------- +begin : April 2020 +copyright : (C) 2020 by Nyall Dawson +email : nyall dot dawson 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 "qgslayoutnortharrowhandler.h" +#include "qgslayoutitemmap.h" +#include "qgslayout.h" +#include "qgsbearingutils.h" +#include "qgslogger.h" + +QgsLayoutNorthArrowHandler::QgsLayoutNorthArrowHandler( QObject *parent ) + : QObject( parent ) +{ + +} + +void QgsLayoutNorthArrowHandler::disconnectMap( QgsLayoutItemMap *map ) +{ + if ( map ) + { + disconnect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutNorthArrowHandler::updateMapRotation ); + disconnect( map, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutNorthArrowHandler::updateMapRotation ); + disconnect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutNorthArrowHandler::updateMapRotation ); + } +} + +void QgsLayoutNorthArrowHandler::updateMapRotation() +{ + if ( !mRotationMap ) + return; + + // take map rotation + double rotation = mRotationMap->mapRotation() + mRotationMap->rotation(); + + // handle true north + switch ( mNorthMode ) + { + case GridNorth: + break; // nothing to do + + case TrueNorth: + { + QgsPointXY center = mRotationMap->extent().center(); + QgsCoordinateReferenceSystem crs = mRotationMap->crs(); + QgsCoordinateTransformContext transformContext = mRotationMap->layout()->project()->transformContext(); + + try + { + double bearing = QgsBearingUtils::bearingTrueNorth( crs, transformContext, center ); + rotation += bearing; + } + catch ( QgsException &e ) + { + Q_UNUSED( e ) + QgsDebugMsg( QStringLiteral( "Caught exception %1" ).arg( e.what() ) ); + } + break; + } + } + + rotation += mNorthOffset; + const double oldRotation = mArrowRotation; + mArrowRotation = ( rotation > 360.0 ) ? rotation - 360.0 : rotation ; + if ( mArrowRotation != oldRotation ) + emit arrowRotationChanged( mArrowRotation ); +} + +void QgsLayoutNorthArrowHandler::setLinkedMap( QgsLayoutItemMap *map ) +{ + if ( mRotationMap ) + { + disconnectMap( mRotationMap ); + } + + if ( !map ) //disable rotation from map + { + mRotationMap = nullptr; + if ( mArrowRotation != 0 ) + { + mArrowRotation = 0; + emit arrowRotationChanged( mArrowRotation ); + } + } + else + { + connect( map, &QgsLayoutItemMap::mapRotationChanged, this, &QgsLayoutNorthArrowHandler::updateMapRotation ); + connect( map, &QgsLayoutItemMap::rotationChanged, this, &QgsLayoutNorthArrowHandler::updateMapRotation ); + connect( map, &QgsLayoutItemMap::extentChanged, this, &QgsLayoutNorthArrowHandler::updateMapRotation ); + mRotationMap = map; + updateMapRotation(); + } +} + +QgsLayoutItemMap *QgsLayoutNorthArrowHandler::linkedMap() const +{ + return mRotationMap; +} + +void QgsLayoutNorthArrowHandler::setNorthMode( QgsLayoutNorthArrowHandler::NorthMode mode ) +{ + mNorthMode = mode; + updateMapRotation(); +} + +void QgsLayoutNorthArrowHandler::setNorthOffset( double offset ) +{ + mNorthOffset = offset; + updateMapRotation(); +} diff --git a/src/core/layout/qgslayoutnortharrowhandler.h b/src/core/layout/qgslayoutnortharrowhandler.h new file mode 100644 index 000000000000..556e3144442b --- /dev/null +++ b/src/core/layout/qgslayoutnortharrowhandler.h @@ -0,0 +1,124 @@ +/*************************************************************************** + qgslayoutnortharrowhandler.h + ------------------- +begin : April 2020 +copyright : (C) 2020 by Nyall Dawson +email : nyall dot dawson 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 QGSLAYOUTNORTHARROWHANDLER_H +#define QGSLAYOUTNORTHARROWHANDLER_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include +#include + +class QgsLayoutItemMap; + +/** + * \ingroup core + * An object which handles north-arrow type behavior for layout items. + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsLayoutNorthArrowHandler: public QObject +{ + Q_OBJECT + public: + + //! Method for syncing rotation to a map's North direction + enum NorthMode + { + GridNorth = 0, //!< Align to grid north + TrueNorth, //!< Align to true north + }; + + /** + * Constructor for QgsLayoutNorthArrowHandler, with the specified parent \a object. + */ + QgsLayoutNorthArrowHandler( QObject *parent SIP_TRANSFERTHIS ); + + /** + * Returns the rotation to be used for the arrow, in degrees clockwise. + */ + double arrowRotation() const { return mArrowRotation; } + + /** + * Sets the linked \a map item. + * + * \see linkedMap() + */ + void setLinkedMap( QgsLayoutItemMap *map ); + + /** + * Returns the linked rotation map, if set. An NULLPTR means arrow calculation is + * disabled. + * + * \see setLinkedMap() + */ + QgsLayoutItemMap *linkedMap() const; + + /** + * Returns the mode used to calculate the arrow rotation. + * \see setNorthMode() + * \see northOffset() + */ + NorthMode northMode() const { return mNorthMode; } + + /** + * Sets the \a mode used to calculate the arrow rotation. + * \see northMode() + * \see setNorthOffset() + */ + void setNorthMode( NorthMode mode ); + + /** + * Returns the offset added to the arrows's rotation from a map's North. + * \see setNorthOffset() + * \see northMode() + */ + double northOffset() const { return mNorthOffset; } + + /** + * Sets the \a offset added to the arrows's rotation from a map's North. + * \see northOffset() + * \see setNorthMode() + */ + void setNorthOffset( double offset ); + + signals: + //! Emitted on arrow rotation change + void arrowRotationChanged( double newRotation ); + + private: + + //! Arrow rotation + double mArrowRotation = 0; + + QString mRotationMapUuid; + //! Map that sets the rotation (or NULLPTR if this picture uses map independent rotation) + QPointer< QgsLayoutItemMap > mRotationMap; + + //! Mode used to align to North + NorthMode mNorthMode = GridNorth; + //! Offset for north arrow + double mNorthOffset = 0.0; + + void disconnectMap( QgsLayoutItemMap *map ); + + private slots: + + void updateMapRotation(); + + +}; + +#endif // QGSLAYOUTNORTHARROWHANDLER_H diff --git a/src/core/layout/qgslayoutobject.h b/src/core/layout/qgslayoutobject.h index 3896439bd920..0b645111cad9 100644 --- a/src/core/layout/qgslayoutobject.h +++ b/src/core/layout/qgslayoutobject.h @@ -185,10 +185,10 @@ class CORE_EXPORT QgsLayoutObject: public QObject, public QgsExpressionContextGe LegendTitle, //!< Legend title LegendColumnCount, //!< Legend column count //scalebar item - ScalebarFillColor, //!< Scalebar fill color - ScalebarFillColor2, //!< Scalebar secondary fill color - ScalebarLineColor, //!< Scalebar line color - ScalebarLineWidth, //!< Scalebar line width, + ScalebarFillColor, //!< Scalebar fill color (deprecated, use data defined properties on scalebar fill symbol 1 instead) + ScalebarFillColor2, //!< Scalebar secondary fill color (deprecated, use data defined properties on scalebar fill symbol 2 instead) + ScalebarLineColor, //!< Scalebar line color (deprecated, use data defined properties on scalebar line symbol instead) + ScalebarLineWidth, //!< Scalebar line width (deprecated, use data defined properties on scalebar line symbol instead) //table item AttributeTableSourceLayer, //!< Attribute table source layer MapCrs, //!< Map CRS diff --git a/src/core/layout/qgslayouttable.cpp b/src/core/layout/qgslayouttable.cpp index 384bbe363e53..2c3219f10920 100644 --- a/src/core/layout/qgslayouttable.cpp +++ b/src/core/layout/qgslayouttable.cpp @@ -301,7 +301,7 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF & double gridSizeX = mShowGrid && mVerticalGrid ? mGridStrokeWidth : 0; double gridSizeY = mShowGrid && mHorizontalGrid ? mGridStrokeWidth : 0; - double cellHeaderHeight = QgsLayoutUtils::fontAscentMM( mHeaderFont ) + 2 * mCellMargin; + double cellHeaderHeight = mMaxRowHeightMap[0] + 2 * mCellMargin;//QgsLayoutUtils::fontAscentMM( mHeaderFont ) + 2 * mCellMargin; double cellBodyHeight = QgsLayoutUtils::fontAscentMM( mContentFont ) + 2 * mCellMargin; QRectF cell; @@ -352,15 +352,6 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF & currentX += mCellMargin; - Qt::TextFlag textFlag = static_cast< Qt::TextFlag >( 0 ); - if ( column->width() <= 0 ) - { - //automatic column width, so we use the Qt::TextDontClip flag when drawing contents, as this works nicer for italicised text - //which may slightly exceed the calculated width - //if column size was manually set then we do apply text clipping, to avoid painting text outside of columns width - textFlag = Qt::TextDontClip; - } - cell = QRectF( currentX, currentY, mMaxColumnWidthMap[col], cellHeaderHeight ); //calculate alignment of header @@ -381,7 +372,15 @@ void QgsLayoutTable::render( QgsLayoutItemRenderContext &context, const QRectF & break; } - QgsLayoutUtils::drawText( p, cell, column->heading(), mHeaderFont, mHeaderFontColor, headerAlign, Qt::AlignVCenter, textFlag ); + // disable text clipping to target text rectangle, because we manually clip to the full cell bounds below + // and it's ok if text overlaps into the margin (e.g. extenders or italicized text) + QString str = column->heading(); + Qt::TextFlag textFlag = static_cast< Qt::TextFlag >( Qt::TextDontClip ); + if ( ( mWrapBehavior != TruncateText || column->width() > 0 ) && textRequiresWrapping( str, column->width(), mHeaderFont ) ) + { + str = wrappedText( str, column->width(), mHeaderFont ); + } + QgsLayoutUtils::drawText( p, cell, str, mHeaderFont, mHeaderFontColor, headerAlign, Qt::AlignVCenter, textFlag ); currentX += mMaxColumnWidthMap[ col ]; currentX += mCellMargin; @@ -869,6 +868,8 @@ bool QgsLayoutTable::calculateMaxColumnWidths() int cells = cols * ( mTableContents.count() + 1 ); QVector< double > widths( cells ); + double currentCellTextWidth; + //first, go through all the column headers and calculate the sizes QgsLayoutTableColumns::const_iterator columnIt = mColumns.constBegin(); int col = 0; @@ -881,7 +882,15 @@ bool QgsLayoutTable::calculateMaxColumnWidths() } else if ( mHeaderMode != QgsLayoutTable::NoHeaders ) { - widths[col] = QgsLayoutUtils::textWidthMM( mHeaderFont, ( *columnIt )->heading() ); + //column width set to automatic, so check content size + QStringList multiLineSplit = ( *columnIt )->heading().split( '\n' ); + currentCellTextWidth = 0; + const auto constMultiLineSplit = multiLineSplit; + for ( const QString &line : constMultiLineSplit ) + { + currentCellTextWidth = std::max( currentCellTextWidth, QgsLayoutUtils::textWidthMM( mHeaderFont, line ) ); + } + widths[col] = currentCellTextWidth; } else { @@ -892,7 +901,6 @@ bool QgsLayoutTable::calculateMaxColumnWidths() //next, go through all the table contents and calculate the sizes QgsLayoutTableContents::const_iterator rowIt = mTableContents.constBegin(); - double currentCellTextWidth; int row = 1; for ( ; rowIt != mTableContents.constEnd(); ++rowIt ) { @@ -951,7 +959,19 @@ bool QgsLayoutTable::calculateMaxRowHeights() for ( ; columnIt != mColumns.constEnd(); ++columnIt ) { //height - heights[col] = mHeaderMode != QgsLayoutTable::NoHeaders ? QgsLayoutUtils::textHeightMM( mHeaderFont, ( *columnIt )->heading() ) : 0; + if ( mHeaderMode == QgsLayoutTable::NoHeaders ) + { + heights[col] = 0; + } + else if ( textRequiresWrapping( ( *columnIt )->heading(), mColumns.at( col )->width(), mHeaderFont ) ) + { + //contents too wide for cell, need to wrap + heights[col] = QgsLayoutUtils::textHeightMM( mHeaderFont, wrappedText( ( *columnIt )->heading(), mColumns.at( col )->width(), mHeaderFont ) ); + } + else + { + heights[col] = QgsLayoutUtils::textHeightMM( mHeaderFont, ( *columnIt )->heading() ); + } col++; } @@ -1103,7 +1123,7 @@ void QgsLayoutTable::drawHorizontalGridLines( QPainter *painter, int firstRow, i { painter->drawLine( QPointF( halfGridStrokeWidth, currentY ), QPointF( mTableSize.width() - halfGridStrokeWidth, currentY ) ); currentY += ( mShowGrid ? mGridStrokeWidth : 0 ); - currentY += ( QgsLayoutUtils::fontAscentMM( mHeaderFont ) + 2 * mCellMargin ); + currentY += mMaxRowHeightMap[0] + 2 * mCellMargin; } for ( int row = firstRow; row < lastRow; ++row ) { @@ -1251,7 +1271,7 @@ void QgsLayoutTable::drawVerticalGridLines( QPainter *painter, const QMapisRunning() ) { // can't do anything while a query is running, and can't block diff --git a/src/core/mesh/qgsmeshdataprovider.cpp b/src/core/mesh/qgsmeshdataprovider.cpp index a0f9e672455e..b701856c6815 100644 --- a/src/core/mesh/qgsmeshdataprovider.cpp +++ b/src/core/mesh/qgsmeshdataprovider.cpp @@ -15,180 +15,32 @@ * * ***************************************************************************/ +#include "qgis.h" #include "qgsmeshdataprovider.h" +#include "qgsmeshdataprovidertemporalcapabilities.h" #include "qgsrectangle.h" -#include "qgis.h" - - - -QgsMeshDatasetIndex::QgsMeshDatasetIndex( int group, int dataset ) - : mGroupIndex( group ), mDatasetIndex( dataset ) -{} - -int QgsMeshDatasetIndex::group() const -{ - return mGroupIndex; -} - -int QgsMeshDatasetIndex::dataset() const -{ - return mDatasetIndex; -} - -bool QgsMeshDatasetIndex::isValid() const -{ - return ( group() > -1 ) && ( dataset() > -1 ); -} - -bool QgsMeshDatasetIndex::operator ==( QgsMeshDatasetIndex other ) const -{ - if ( isValid() && other.isValid() ) - return other.group() == group() && other.dataset() == dataset(); - else - return isValid() == other.isValid(); -} - -bool QgsMeshDatasetIndex::operator !=( QgsMeshDatasetIndex other ) const -{ - return !( operator==( other ) ); -} QgsMeshDataProvider::QgsMeshDataProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options ) - : QgsDataProvider( uri, options ) -{ -} - -QgsMeshDatasetValue::QgsMeshDatasetValue( double x, double y ) - : mX( x ), mY( y ) -{} - -QgsMeshDatasetValue::QgsMeshDatasetValue( double scalar ) - : mX( scalar ) -{} - -double QgsMeshDatasetValue::scalar() const -{ - if ( std::isnan( mY ) ) - { - return mX; - } - else if ( std::isnan( mX ) ) - { - return std::numeric_limits::quiet_NaN(); - } - else - { - return std::sqrt( ( mX ) * ( mX ) + ( mY ) * ( mY ) ); - } -} - -void QgsMeshDatasetValue::set( double scalar ) -{ - setX( scalar ); -} - -void QgsMeshDatasetValue::setX( double x ) -{ - mX = x; -} - -void QgsMeshDatasetValue::setY( double y ) -{ - mY = y; -} - -double QgsMeshDatasetValue::x() const + : QgsDataProvider( uri, options ), mTemporalCapabilities( qgis::make_unique() ) { - return mX; } -double QgsMeshDatasetValue::y() const +QgsMeshDataProviderTemporalCapabilities *QgsMeshDataProvider::temporalCapabilities() { - return mY; + return mTemporalCapabilities.get(); } -bool QgsMeshDatasetValue::operator==( const QgsMeshDatasetValue other ) const +const QgsMeshDataProviderTemporalCapabilities *QgsMeshDataProvider::temporalCapabilities() const { - bool equal = std::isnan( mX ) == std::isnan( other.x() ); - equal &= std::isnan( mY ) == std::isnan( other.y() ); - - if ( equal ) - { - if ( std::isnan( mY ) ) - { - equal &= qgsDoubleNear( other.x(), mX, 1E-8 ); - } - else - { - equal &= qgsDoubleNear( other.x(), mX, 1E-8 ); - equal &= qgsDoubleNear( other.y(), mY, 1E-8 ); - } - } - return equal; + return mTemporalCapabilities.get(); } -QgsMeshDatasetGroupMetadata::QgsMeshDatasetGroupMetadata( const QString &name, - bool isScalar, - DataType dataType, - double minimum, - double maximum, - int maximumVerticalLevels, - const QDateTime &referenceTime, - const QMap &extraOptions ) - : mName( name ) - , mIsScalar( isScalar ) - , mDataType( dataType ) - , mMinimumValue( minimum ) - , mMaximumValue( maximum ) - , mExtraOptions( extraOptions ) - , mMaximumVerticalLevelsCount( maximumVerticalLevels ) - , mReferenceTime( referenceTime ) +void QgsMeshDataProvider::setTemporalUnit( QgsUnitTypes::TemporalUnit unit ) { -} - -QMap QgsMeshDatasetGroupMetadata::extraOptions() const -{ - return mExtraOptions; -} - -bool QgsMeshDatasetGroupMetadata::isVector() const -{ - return !mIsScalar; -} - -bool QgsMeshDatasetGroupMetadata::isScalar() const -{ - return mIsScalar; -} - -QString QgsMeshDatasetGroupMetadata::name() const -{ - return mName; -} - -QgsMeshDatasetGroupMetadata::DataType QgsMeshDatasetGroupMetadata::dataType() const -{ - return mDataType; -} - -double QgsMeshDatasetGroupMetadata::minimum() const -{ - return mMinimumValue; -} - -double QgsMeshDatasetGroupMetadata::maximum() const -{ - return mMaximumValue; -} - -int QgsMeshDatasetGroupMetadata::maximumVerticalLevelsCount() const -{ - return mMaximumVerticalLevelsCount; -} - -QDateTime QgsMeshDatasetGroupMetadata::referenceTime() const -{ - return mReferenceTime; + QgsUnitTypes::TemporalUnit oldUnit = mTemporalCapabilities->temporalUnit(); + mTemporalCapabilities->setTemporalUnit( unit ); + if ( oldUnit != unit ) + reloadData(); } int QgsMeshDatasetSourceInterface::datasetCount( QgsMeshDatasetIndex index ) const @@ -201,136 +53,6 @@ QgsMeshDatasetGroupMetadata QgsMeshDatasetSourceInterface::datasetGroupMetadata( return datasetGroupMetadata( index.group() ); } -QgsMeshDatasetMetadata::QgsMeshDatasetMetadata( - double time, - bool isValid, - double minimum, - double maximum, - int maximumVerticalLevels ) - : mTime( time ) - , mIsValid( isValid ) - , mMinimumValue( minimum ) - , mMaximumValue( maximum ) - , mMaximumVerticalLevelsCount( maximumVerticalLevels ) -{ -} - -double QgsMeshDatasetMetadata::time() const -{ - return mTime; -} - -bool QgsMeshDatasetMetadata::isValid() const -{ - return mIsValid; -} - -double QgsMeshDatasetMetadata::minimum() const -{ - return mMinimumValue; -} - -double QgsMeshDatasetMetadata::maximum() const -{ - return mMaximumValue; -} - -int QgsMeshDatasetMetadata::maximumVerticalLevelsCount() const -{ - return mMaximumVerticalLevelsCount; -} - -QgsMeshDataBlock::QgsMeshDataBlock() - : mType( ActiveFlagInteger ) -{ -} - -QgsMeshDataBlock::QgsMeshDataBlock( QgsMeshDataBlock::DataType type, int count ) - : mType( type ), - mSize( count ) -{ -} - -QgsMeshDataBlock::DataType QgsMeshDataBlock::type() const -{ - return mType; -} - -int QgsMeshDataBlock::count() const -{ - return mSize; -} - -bool QgsMeshDataBlock::isValid() const -{ - return ( count() > 0 ) && ( mIsValid ); -} - -QgsMeshDatasetValue QgsMeshDataBlock::value( int index ) const -{ - if ( !isValid() ) - return QgsMeshDatasetValue(); - - Q_ASSERT( mType != ActiveFlagInteger ); - - if ( mType == ScalarDouble ) - return QgsMeshDatasetValue( mDoubleBuffer[index] ); - - return QgsMeshDatasetValue( - mDoubleBuffer[2 * index], - mDoubleBuffer[2 * index + 1] - ); -} - -bool QgsMeshDataBlock::active( int index ) const -{ - if ( !isValid() ) - return false; - - Q_ASSERT( mType == ActiveFlagInteger ); - - if ( mIntegerBuffer.empty() ) - return true; - else - return bool( mIntegerBuffer[index] ); -} - -void QgsMeshDataBlock::setActive( const QVector &vals ) -{ - Q_ASSERT( mType == ActiveFlagInteger ); - Q_ASSERT( vals.size() == count() ); - - mIntegerBuffer = vals; - setValid( true ); -} - -QVector QgsMeshDataBlock::active() const -{ - Q_ASSERT( mType == ActiveFlagInteger ); - return mIntegerBuffer; -} - -QVector QgsMeshDataBlock::values() const -{ - Q_ASSERT( mType != ActiveFlagInteger ); - - return mDoubleBuffer; -} - -void QgsMeshDataBlock::setValues( const QVector &vals ) -{ - Q_ASSERT( mType != ActiveFlagInteger ); - Q_ASSERT( mType == ScalarDouble ? vals.size() == count() : vals.size() == 2 * count() ); - - mDoubleBuffer = vals; - setValid( true ); -} - -void QgsMeshDataBlock::setValid( bool valid ) -{ - mIsValid = valid; -} - QgsMeshVertex QgsMesh::vertex( int index ) const { if ( index < vertices.size() && index >= 0 ) @@ -388,119 +110,6 @@ int QgsMesh::edgeCount() const return edges.size(); } -QgsMesh3dDataBlock::QgsMesh3dDataBlock() = default; - -QgsMesh3dDataBlock::~QgsMesh3dDataBlock() {}; - -QgsMesh3dDataBlock::QgsMesh3dDataBlock( int count, bool isVector ) - : mSize( count ) - , mIsVector( isVector ) -{ -} - -bool QgsMesh3dDataBlock::isValid() const -{ - return mIsValid; -} - -bool QgsMesh3dDataBlock::isVector() const -{ - return mIsVector; -} - -int QgsMesh3dDataBlock::count() const -{ - return mSize; -} - -int QgsMesh3dDataBlock::firstVolumeIndex() const -{ - if ( mFaceToVolumeIndex.empty() ) - return -1; - return mFaceToVolumeIndex[0]; -} - -int QgsMesh3dDataBlock::lastVolumeIndex() const -{ - if ( mFaceToVolumeIndex.empty() || mVerticalLevelsCount.empty() ) - return -1; - const int lastVolumeStartIndex = mFaceToVolumeIndex[mFaceToVolumeIndex.size() - 1]; - const int volumesCountInLastRow = mVerticalLevelsCount[mVerticalLevelsCount.size() - 1]; - return lastVolumeStartIndex + volumesCountInLastRow; -} - -int QgsMesh3dDataBlock::volumesCount() const -{ - return lastVolumeIndex() - firstVolumeIndex(); -} - -QVector QgsMesh3dDataBlock::verticalLevelsCount() const -{ - Q_ASSERT( isValid() ); - return mVerticalLevelsCount; -} - -void QgsMesh3dDataBlock::setFaceToVolumeIndex( const QVector &faceToVolumeIndex ) -{ - Q_ASSERT( faceToVolumeIndex.size() == count() ); - mFaceToVolumeIndex = faceToVolumeIndex; -} - -void QgsMesh3dDataBlock::setVerticalLevelsCount( const QVector &verticalLevelsCount ) -{ - Q_ASSERT( verticalLevelsCount.size() == count() ); - mVerticalLevelsCount = verticalLevelsCount; -} - -QVector QgsMesh3dDataBlock::verticalLevels() const -{ - Q_ASSERT( isValid() ); - return mVerticalLevels; -} - -void QgsMesh3dDataBlock::setVerticalLevels( const QVector &verticalLevels ) -{ - Q_ASSERT( verticalLevels.size() == volumesCount() + count() ); - mVerticalLevels = verticalLevels; -} - -QVector QgsMesh3dDataBlock::faceToVolumeIndex() const -{ - Q_ASSERT( isValid() ); - return mFaceToVolumeIndex; -} - -QVector QgsMesh3dDataBlock::values() const -{ - Q_ASSERT( isValid() ); - return mDoubleBuffer; -} - -QgsMeshDatasetValue QgsMesh3dDataBlock::value( int volumeIndex ) const -{ - if ( !isValid() ) - return QgsMeshDatasetValue(); - - if ( !mIsVector ) - return QgsMeshDatasetValue( mDoubleBuffer[volumeIndex] ); - - return QgsMeshDatasetValue( - mDoubleBuffer[2 * volumeIndex], - mDoubleBuffer[2 * volumeIndex + 1] - ); -} - -void QgsMesh3dDataBlock::setValues( const QVector &doubleBuffer ) -{ - Q_ASSERT( doubleBuffer.size() == ( isVector() ? 2 * volumesCount() : volumesCount() ) ); - mDoubleBuffer = doubleBuffer; -} - -void QgsMesh3dDataBlock::setValid( bool valid ) -{ - mIsValid = valid; -} - bool QgsMeshDataSourceInterface::contains( const QgsMesh::ElementType &type ) const { switch ( type ) diff --git a/src/core/mesh/qgsmeshdataprovider.h b/src/core/mesh/qgsmeshdataprovider.h index dd50abf6a98a..028edc4ec1c4 100644 --- a/src/core/mesh/qgsmeshdataprovider.h +++ b/src/core/mesh/qgsmeshdataprovider.h @@ -28,38 +28,11 @@ #include "qgis_core.h" #include "qgspoint.h" #include "qgsdataprovider.h" +#include "qgsmeshdataset.h" +#include "qgsmeshdataprovidertemporalcapabilities.h" -class QgsRectangle; -/** - * \ingroup core - * - * QgsMeshDatasetIndex is index that identifies the dataset group (e.g. wind speed) - * and a dataset in this group (e.g. magnitude of wind speed in particular time) - * - * \note The API is considered EXPERIMENTAL and can be changed without a notice - * - * \since QGIS 3.4 - */ -class CORE_EXPORT QgsMeshDatasetIndex -{ - public: - //! Creates an index. -1 represents invalid group/dataset - QgsMeshDatasetIndex( int group = -1, int dataset = -1 ); - //! Returns a group index - int group() const; - //! Returns a dataset index within group() - int dataset() const; - //! Returns whether index is valid, ie at least groups is set - bool isValid() const; - //! Equality operator - bool operator == ( QgsMeshDatasetIndex other ) const; - //! Inequality operator - bool operator != ( QgsMeshDatasetIndex other ) const; - private: - int mGroupIndex = -1; - int mDatasetIndex = -1; -}; +class QgsRectangle; //! xyz coords of vertex typedef QgsPoint QgsMeshVertex; @@ -134,455 +107,6 @@ struct CORE_EXPORT QgsMesh QVector faces SIP_SKIP; }; -/** - * \ingroup core - * - * QgsMeshDatasetValue represents single dataset value - * - * could be scalar or vector. Nodata values are represented by NaNs. - * - * \note The API is considered EXPERIMENTAL and can be changed without a notice - * - * \since QGIS 3.2 - */ -class CORE_EXPORT QgsMeshDatasetValue -{ - public: - //! Constructor for vector value - QgsMeshDatasetValue( double x, - double y ); - - //! Constructor for scalar value - QgsMeshDatasetValue( double scalar ); - - //! Default Ctor, initialize to NaN - QgsMeshDatasetValue() = default; - - //! Dtor - ~QgsMeshDatasetValue() = default; - - //! Sets scalar value - void set( double scalar ); - - //! Sets X value - void setX( double x ); - - //! Sets Y value - void setY( double y ) ; - - //! Returns magnitude of vector for vector data or scalar value for scalar data - double scalar() const; - - //! Returns x value - double x() const; - - //! Returns y value - double y() const; - - bool operator==( QgsMeshDatasetValue other ) const; - - private: - double mX = std::numeric_limits::quiet_NaN(); - double mY = std::numeric_limits::quiet_NaN(); -}; - -/** - * \ingroup core - * - * QgsMeshDataBlock is a block of integers/doubles that can be used - * to retrieve: - * active flags (e.g. face's active integer flag) - * scalars (e.g. scalar dataset double values) - * vectors (e.g. vector dataset doubles x,y values) - * - * data are implicitly shared, so the class can be quickly copied - * std::numeric_limits::quiet_NaN() represents NODATA value - * - * Data can be accessed all at once with values() (faster) or - * value by value (slower) with active() or value() - * - * \since QGIS 3.6 - */ -class CORE_EXPORT QgsMeshDataBlock -{ - public: - //! Type of data stored in the block - enum DataType - { - ActiveFlagInteger, //!< Integer boolean flag whether face is active - ScalarDouble, //!< Scalar double values - Vector2DDouble, //!< Vector double pairs (x1, y1, x2, y2, ... ) - }; - - //! Constructs an invalid block - QgsMeshDataBlock(); - - //! Constructs a new block - QgsMeshDataBlock( DataType type, int count ); - - //! Type of data stored in the block - DataType type() const; - - //! Number of items stored in the block - int count() const; - - //! Whether the block is valid - bool isValid() const; - - /** - * Returns a value represented by the index - * For active flag the behavior is undefined - */ - QgsMeshDatasetValue value( int index ) const; - - /** - * Returns a value for active flag by the index - * For scalar and vector 2d the behavior is undefined - */ - bool active( int index ) const; - - /** - * Sets active flag values. - * - * If the data provider/datasets does not have active - * flag capability (== all values are valid), just - * set block validity by setValid( true ) - * - * \param vals value vector with size count() - * - * For scalar and vector 2d the behavior is undefined - * - * \since QGIS 3.12 - */ - void setActive( const QVector &vals ); - - /** - * Returns active flag array - * - * Even for active flag valid dataset, the returned array could be empty. - * This means that the data provider/dataset does not support active flag - * capability, so all faces are active by default. - * - * For scalar and vector 2d the behavior is undefined - * - * \since QGIS 3.12 - */ - QVector active() const; - - /** - * Returns buffer to the array with values - * For vector it is pairs (x1, y1, x2, y2, ... ) - * - * \since QGIS 3.12 - */ - QVector values() const; - - /** - * Sets values - * - * For scalar datasets, it must have size count() - * For vector datasets, it must have size 2 * count() - * For active flag the behavior is undefined - * - * \since QGIS 3.12 - */ - void setValues( const QVector &vals ); - - //! Sets block validity - void setValid( bool valid ); - - private: - QVector mDoubleBuffer; - QVector mIntegerBuffer; - DataType mType; - int mSize = 0; - bool mIsValid = false; -}; - -/** - * \ingroup core - * - * QgsMesh3dDataBlock is a block of 3d stacked mesh data related N - * faces defined on base mesh frame. - * - * data are implicitly shared, so the class can be quickly copied - * std::numeric_limits::quiet_NaN() represents NODATA value - * - * \note The API is considered EXPERIMENTAL and can be changed without a notice - * - * \since QGIS 3.12 - */ -class CORE_EXPORT QgsMesh3dDataBlock -{ - public: - //! Constructs an invalid block - QgsMesh3dDataBlock(); - - //! Dtor - ~QgsMesh3dDataBlock(); - - //! Constructs a new block for count faces - QgsMesh3dDataBlock( int count, bool isVector ); - - //! Sets block validity - void setValid( bool valid ); - - //! Whether the block is valid - bool isValid() const; - - //! Whether we store vector values - bool isVector() const; - - //! Number of 2d faces for which the volume data is stored in the block - int count() const; - - //! Index of the first volume stored in the buffer (absolute) - int firstVolumeIndex() const; - - //! Index of the last volume stored in the buffer (absolute) - int lastVolumeIndex() const; - - //! Returns number of volumes stored in the buffer - int volumesCount() const; - - /** - * Returns number of vertical level above 2d faces - */ - QVector verticalLevelsCount() const; - - /** - * Sets the vertical level counts - */ - void setVerticalLevelsCount( const QVector &verticalLevelsCount ); - - /** - * Returns the vertical levels height - */ - QVector verticalLevels() const; - - /** - * Sets the vertical levels height - */ - void setVerticalLevels( const QVector &verticalLevels ); - - /** - * Returns the indexing between faces and volumes - */ - QVector faceToVolumeIndex() const; - - /** - * Sets the indexing between faces and volumes - */ - void setFaceToVolumeIndex( const QVector &faceToVolumeIndex ); - - /** - * Returns the values at volume centers - * - * For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) - */ - QVector values() const; - - /** - * Returns the value at volume centers - * - * \param volumeIndex volume index relative to firstVolumeIndex() - * \returns value (scalar or vector) - */ - QgsMeshDatasetValue value( int volumeIndex ) const; - - /** - * Sets the values at volume centers - * - * For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) - */ - void setValues( const QVector &doubleBuffer ); - - private: - int mSize = 0; - bool mIsValid = false; - bool mIsVector = false; - QVector mVerticalLevelsCount; - QVector mVerticalLevels; - QVector mFaceToVolumeIndex; - QVector mDoubleBuffer; // for scalar/vector values -}; - -/** - * \ingroup core - * - * QgsMeshDatasetGroupMetadata is a collection of dataset group metadata - * such as whether the data is vector or scalar, name - * - * \note The API is considered EXPERIMENTAL and can be changed without a notice - * - * \since QGIS 3.4 - */ -class CORE_EXPORT QgsMeshDatasetGroupMetadata -{ - public: - - //! Location of where data is specified for datasets in the dataset group - enum DataType - { - DataOnFaces = 0, //!< Data is defined on faces - DataOnVertices, //!< Data is defined on vertices - DataOnVolumes, //!< Data is defined on volumes \since QGIS 3.12 - DataOnEdges //!< Data is defined on edges \since QGIS 3.14 - }; - - //! Constructs an empty metadata object - QgsMeshDatasetGroupMetadata() = default; - - /** - * Constructs a valid metadata object - * - * \param name name of the dataset group - * \param isScalar dataset contains scalar data, specifically the y-value of QgsMeshDatasetValue is NaN - * \param dataType where the data are defined on (vertices, faces or volumes) - * \param minimum minimum value (magnitude for vectors) present among all group's dataset values - * \param maximum maximum value (magnitude for vectors) present among all group's dataset values - * \param maximumVerticalLevels maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes - * \param referenceTime reference time of the dataset group - * \param extraOptions dataset's extra options stored by the provider. Usually contains the name, time value, time units, data file vendor, ... - */ - QgsMeshDatasetGroupMetadata( const QString &name, - bool isScalar, - DataType dataType, - double minimum, - double maximum, - int maximumVerticalLevels, - const QDateTime &referenceTime, - const QMap &extraOptions ); - - /** - * Returns name of the dataset group - */ - QString name() const; - - /** - * Returns extra metadata options, for example description - */ - QMap extraOptions() const; - - /** - * \brief Returns whether dataset group has vector data - */ - bool isVector() const; - - /** - * \brief Returns whether dataset group has scalar data - */ - bool isScalar() const; - - /** - * Returns whether dataset group data is defined on vertices or faces or volumes - * - * \since QGIS 3.12 - */ - DataType dataType() const; - - /** - * \brief Returns minimum scalar value/vector magnitude present for whole dataset group - */ - double minimum() const; - - /** - * \brief Returns maximum scalar value/vector magnitude present for whole dataset group - */ - double maximum() const; - - /** - * Returns maximum number of vertical levels for 3d stacked meshes - * - * \since QGIS 3.12 - */ - int maximumVerticalLevelsCount() const; - - /** - * Returns the reference time - * - * \since QGIS 3.12 - */ - QDateTime referenceTime() const; - - private: - QString mName; - bool mIsScalar = false; - DataType mDataType = DataType::DataOnFaces; - double mMinimumValue = std::numeric_limits::quiet_NaN(); - double mMaximumValue = std::numeric_limits::quiet_NaN(); - QMap mExtraOptions; - int mMaximumVerticalLevelsCount = 0; // for 3d stacked meshes - QDateTime mReferenceTime; -}; - -/** - * \ingroup core - * - * QgsMeshDatasetMetadata is a collection of mesh dataset metadata such - * as whether the data is valid or associated time for the dataset - * - * \note The API is considered EXPERIMENTAL and can be changed without a notice - * - * \since QGIS 3.2 - */ -class CORE_EXPORT QgsMeshDatasetMetadata -{ - public: - //! Constructs an empty metadata object - QgsMeshDatasetMetadata() = default; - - /** - * Constructs a valid metadata object - * - * \param time a time which this dataset represents in the dataset group - * \param isValid dataset is loadad and valid for fetching the data - * \param minimum minimum value (magnitude for vectors) present among dataset values - * \param maximum maximum value (magnitude for vectors) present among dataset values - * \param maximumVerticalLevels maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes - */ - QgsMeshDatasetMetadata( double time, - bool isValid, - double minimum, - double maximum, - int maximumVerticalLevels - ); - - /** - * Returns the time value for this dataset - */ - double time() const; - - /** - * Returns whether dataset is valid - */ - bool isValid() const; - - /** - * Returns minimum scalar value/vector magnitude present for the dataset - */ - double minimum() const; - - /** - * Returns maximum scalar value/vector magnitude present for the dataset - */ - double maximum() const; - - /** - * Returns maximum number of vertical levels for 3d stacked meshes - * - * \since QGIS 3.12 - */ - int maximumVerticalLevelsCount() const; - - private: - double mTime = std::numeric_limits::quiet_NaN(); - bool mIsValid = false; - double mMinimumValue = std::numeric_limits::quiet_NaN(); - double mMaximumValue = std::numeric_limits::quiet_NaN(); - int mMaximumVerticalLevelsCount = 0; // for 3d stacked meshes -}; - /** * \ingroup core * @@ -803,9 +327,24 @@ class CORE_EXPORT QgsMeshDataProvider: public QgsDataProvider, public QgsMeshDat //! Ctor QgsMeshDataProvider( const QString &uri, const QgsDataProvider::ProviderOptions &providerOptions ); + QgsMeshDataProviderTemporalCapabilities *temporalCapabilities() override; + const QgsMeshDataProviderTemporalCapabilities *temporalCapabilities() const override SIP_SKIP; + + /** + * Sets the temporal unit of the provider and reload data if it changes. + * + * \param unit the temporal unit + * + * \since QGIS 3.14 + */ + void setTemporalUnit( QgsUnitTypes::TemporalUnit unit ); + signals: //! Emitted when some new dataset groups have been added void datasetGroupsAdded( int count ); + + private: + std::unique_ptr mTemporalCapabilities; }; #endif // QGSMESHDATAPROVIDER_H diff --git a/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.cpp b/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.cpp new file mode 100644 index 000000000000..28cf516ed804 --- /dev/null +++ b/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.cpp @@ -0,0 +1,153 @@ +/*************************************************************************** + qgsmeshdataprovidertemporalcapabilities.cpp + ----------------------- + begin : March 2020 + copyright : (C) 2020 by Vincent Cloarec + email : vcloarec 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 "qgsmeshdataprovidertemporalcapabilities.h" + + +QgsMeshDataProviderTemporalCapabilities::QgsMeshDataProviderTemporalCapabilities(): QgsDataProviderTemporalCapabilities() +{} + +QgsMeshDatasetIndex QgsMeshDataProviderTemporalCapabilities::datasetIndexFromRelativeTimeRange( int group, qint64 startTimeSinceGlobalReference, qint64 endTimeSinceGlobalReference ) const +{ + // No time --> non temporal dataset, so return the dataset that has to be the only one + const QList &datasetTimes = mDatasetTimeSinceGroupReference[group]; + if ( datasetTimes.isEmpty() ) + return QgsMeshDatasetIndex( group, 0 ); + const QDateTime groupReference = mGroupsReferenceDateTime[group]; + const qint64 startTimeSinceGroupReference = + startTimeSinceGlobalReference - mGlobalReferenceDateTime.msecsTo( groupReference ); + const qint64 endTimeSinceGroupReference = + endTimeSinceGlobalReference - mGlobalReferenceDateTime.msecsTo( groupReference ); + + if ( startTimeSinceGroupReference >= datasetTimes.last() ) + return QgsMeshDatasetIndex(); + + for ( int i = 0; i < datasetTimes.count(); ++i ) + { + qint64 time = datasetTimes.at( i ); + if ( startTimeSinceGroupReference <= time ) + { + if ( endTimeSinceGroupReference <= time ) + return QgsMeshDatasetIndex( group, i - 1 ); // invalid if i=0 + else + return QgsMeshDatasetIndex( group, i ); + } + } + + // if we are here (normally, this could no happen), return invalid dataset index + return QgsMeshDatasetIndex(); +} + +void QgsMeshDataProviderTemporalCapabilities::addGroupReferenceDateTime( int group, const QDateTime &reference ) +{ + if ( ( !mGlobalReferenceDateTime.isValid() && reference.isValid() ) || + ( reference.isValid() && mGlobalReferenceDateTime.isValid() && reference < mGlobalReferenceDateTime ) ) + mGlobalReferenceDateTime = reference; + + mGroupsReferenceDateTime[group] = reference; +} + +void QgsMeshDataProviderTemporalCapabilities::addDatasetTimeInMilliseconds( int group, qint64 time ) +{ + QList &datasetTimes = mDatasetTimeSinceGroupReference[group]; + datasetTimes.append( time ); +} + +void QgsMeshDataProviderTemporalCapabilities::addDatasetTime( int group, double time ) +{ + qint64 unitTimeFactor = QgsUnitTypes::fromUnitToUnitFactor( mTemporalUnit, QgsUnitTypes::TemporalMilliseconds ); + addDatasetTimeInMilliseconds( group, time * unitTimeFactor ); +} + +bool QgsMeshDataProviderTemporalCapabilities::hasReferenceTime() const +{ + return mGlobalReferenceDateTime.isValid(); +} + +QDateTime QgsMeshDataProviderTemporalCapabilities::referenceTime() const +{ + return mGlobalReferenceDateTime; +} + +QgsDateTimeRange QgsMeshDataProviderTemporalCapabilities::timeExtent() const +{ + + return timeExtent( mGlobalReferenceDateTime ); +} + +QgsDateTimeRange QgsMeshDataProviderTemporalCapabilities::timeExtent( const QDateTime &reference ) const +{ + QDateTime end; + QDateTime begin; + for ( QHash::const_iterator it = mGroupsReferenceDateTime.constBegin() ; + it != mGroupsReferenceDateTime.constEnd(); ++it ) + { + QDateTime groupReference = it.value(); + if ( !groupReference.isValid() ) //the dataset group has not a valid reference time -->take global reference + groupReference = mGlobalReferenceDateTime; + + + if ( !groupReference.isValid() ) + groupReference = reference; + + const QList times = mDatasetTimeSinceGroupReference[it.key()]; + qint64 durationSinceFirst = groupReference.msecsTo( reference ); + qint64 durationSinceLast = groupReference.msecsTo( reference ); + if ( !times.isEmpty() ) + { + durationSinceFirst += times.first(); + durationSinceLast += times.last(); + } + + if ( !end.isValid() || groupReference.addMSecs( durationSinceLast ) > end ) + end = groupReference.addMSecs( durationSinceLast ); + + if ( !begin.isValid() || groupReference.addMSecs( durationSinceFirst ) > begin ) + begin = groupReference.addMSecs( durationSinceFirst ); + } + + return QgsDateTimeRange( begin, end ); +} + +void QgsMeshDataProviderTemporalCapabilities::setTemporalUnit( QgsUnitTypes::TemporalUnit timeUnit ) +{ + mTemporalUnit = timeUnit; +} + +QgsUnitTypes::TemporalUnit QgsMeshDataProviderTemporalCapabilities::temporalUnit() const +{ + return mTemporalUnit; +} + +qint64 QgsMeshDataProviderTemporalCapabilities::datasetTime( const QgsMeshDatasetIndex &index ) const +{ + if ( !index.isValid() ) + return -999999; + + const QList ×List = mDatasetTimeSinceGroupReference[index.group()]; + if ( index.dataset() < timesList.count() ) + return timesList.at( index.dataset() ); + else + return -999999; +} + +void QgsMeshDataProviderTemporalCapabilities::clear() +{ + mGlobalReferenceDateTime = QDateTime(); + mGroupsReferenceDateTime.clear(); + mDatasetTimeSinceGroupReference.clear(); +} diff --git a/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h b/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h new file mode 100644 index 000000000000..e2b3b5a05305 --- /dev/null +++ b/src/core/mesh/qgsmeshdataprovidertemporalcapabilities.h @@ -0,0 +1,139 @@ +/*************************************************************************** + qgsmeshdataprovidertemporalcapabilities.h + ----------------------- + begin : March 2020 + copyright : (C) 2020 by Vincent Cloarec + email : vcloarec 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 QGSMESHDATAPROVIDERTEMPORALCAPABILITIES_H +#define QGSMESHDATAPROVIDERTEMPORALCAPABILITIES_H + +#include "qgsdataprovidertemporalcapabilities.h" +#include "qgsrange.h" +#include "qgsmeshdataset.h" + + +/** + * \class QgsMeshDataProviderTemporalCapabilities + * \ingroup core + * Class for handling properties relating to a mesh data provider's temporal capabilities. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsMeshDataProviderTemporalCapabilities: public QgsDataProviderTemporalCapabilities +{ + public: + + /** + * Constructor for QgsMeshDataProviderTemporalCapabilities + */ + QgsMeshDataProviderTemporalCapabilities(); + + /** + * Returns the first dataset that are include in the range [\a startTimeSinceGlobalReference,\a endTimeSinceGlobalReference[ (in milliseconds) + * from the dataset \a group. If no dataset is present in this range return the last dataset before this range if it not the last one + * of whole the dataset group + * + * Returns invalid dataset index if there is no data set in the range + * + * \note for non temporal dataset group, the range is not used and the unique dataset is returned + */ + QgsMeshDatasetIndex datasetIndexFromRelativeTimeRange( int group, qint64 startTimeSinceGlobalReference, qint64 endTimeSinceGlobalReference ) const; + + /** + * Adds a \a reference date/time from a dataset \a group + * + * \note must be used only by the mesh data provider + */ + void addGroupReferenceDateTime( int group, const QDateTime &reference ) SIP_SKIP; + + /** + * Adds a \a time (in milliseconds) from a dataset contained in \a group + * + * \note must be used only by the mesh data provider, + * all dataset need to be added one after one + */ + void addDatasetTimeInMilliseconds( int group, qint64 time ) SIP_SKIP; + + /** + * Adds a \a time (in provider unit) from a dataset contained in \a group + * + * \note must be used only by the mesh data provider, + * all dataset need to be added one after one + */ + void addDatasetTime( int group, double time ) SIP_SKIP; + + /** + * Returns whether the reference time is set + */ + bool hasReferenceTime() const; + + /** + * Returns the reference time + */ + QDateTime referenceTime() const; + + /** + * Returns the time extent using the internal reference time + * and the first and last times available from the all the dataset + */ + QgsDateTimeRange timeExtent() const; + + /** + * Returns the time extent using an external \a reference date time + * and the first and last times available from the all the dataset + */ + QgsDateTimeRange timeExtent( const QDateTime &reference ) const; + + /** + * Sets the temporal unit (\a temporalUnit) used to read data by the data provider + * + * Temporal units supported are milliseconds, seconds, minutes, hors, days and weeks + */ + void setTemporalUnit( QgsUnitTypes::TemporalUnit temporalUnit ); + + /** + * Returns the temporal unit used to read data by the data provider + */ + QgsUnitTypes::TemporalUnit temporalUnit() const; + + /** + * Returns the relative time in milliseconds of the dataset + */ + qint64 datasetTime( const QgsMeshDatasetIndex &index ) const; + + /** + * Clears alls stored reference times and dataset times + */ + void clear(); + + private: + + //! Holds the global reference date/time value if exists (min of all groups), otherwise it is invalid + QDateTime mGlobalReferenceDateTime; + + //! Holds the reference time of each dataset groups + QHash mGroupsReferenceDateTime; + + /** + * Holds the time of each dataset in milliseconds. + * The times are from the dataset groups reference time if any, + * otherwise from 0 + * Non Temporal dataset (static) have empty list + */ + QHash> mDatasetTimeSinceGroupReference; + + QgsUnitTypes::TemporalUnit mTemporalUnit = QgsUnitTypes::TemporalHours; +}; + +#endif // QGSMESHDATAPROVIDERTEMPORALCAPABILITIES_H diff --git a/src/core/mesh/qgsmeshdataset.cpp b/src/core/mesh/qgsmeshdataset.cpp new file mode 100644 index 000000000000..fce759dc6943 --- /dev/null +++ b/src/core/mesh/qgsmeshdataset.cpp @@ -0,0 +1,437 @@ +/*************************************************************************** + qgsmeshdataset.cpp + ----------------------- + begin : April 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv 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 "qgsmeshdataset.h" +#include "qgsrectangle.h" +#include "qgis.h" + +QgsMeshDatasetIndex::QgsMeshDatasetIndex( int group, int dataset ) + : mGroupIndex( group ), mDatasetIndex( dataset ) +{} + +int QgsMeshDatasetIndex::group() const +{ + return mGroupIndex; +} + +int QgsMeshDatasetIndex::dataset() const +{ + return mDatasetIndex; +} + +bool QgsMeshDatasetIndex::isValid() const +{ + return ( group() > -1 ) && ( dataset() > -1 ); +} + +bool QgsMeshDatasetIndex::operator ==( QgsMeshDatasetIndex other ) const +{ + if ( isValid() && other.isValid() ) + return other.group() == group() && other.dataset() == dataset(); + else + return isValid() == other.isValid(); +} + +bool QgsMeshDatasetIndex::operator !=( QgsMeshDatasetIndex other ) const +{ + return !( operator==( other ) ); +} + +QgsMeshDatasetValue::QgsMeshDatasetValue( double x, double y ) + : mX( x ), mY( y ) +{} + +QgsMeshDatasetValue::QgsMeshDatasetValue( double scalar ) + : mX( scalar ) +{} + +double QgsMeshDatasetValue::scalar() const +{ + if ( std::isnan( mY ) ) + { + return mX; + } + else if ( std::isnan( mX ) ) + { + return std::numeric_limits::quiet_NaN(); + } + else + { + return std::sqrt( ( mX ) * ( mX ) + ( mY ) * ( mY ) ); + } +} + +void QgsMeshDatasetValue::set( double scalar ) +{ + setX( scalar ); +} + +void QgsMeshDatasetValue::setX( double x ) +{ + mX = x; +} + +void QgsMeshDatasetValue::setY( double y ) +{ + mY = y; +} + +double QgsMeshDatasetValue::x() const +{ + return mX; +} + +double QgsMeshDatasetValue::y() const +{ + return mY; +} + +bool QgsMeshDatasetValue::operator==( const QgsMeshDatasetValue other ) const +{ + bool equal = std::isnan( mX ) == std::isnan( other.x() ); + equal &= std::isnan( mY ) == std::isnan( other.y() ); + + if ( equal ) + { + if ( std::isnan( mY ) ) + { + equal &= qgsDoubleNear( other.x(), mX, 1E-8 ); + } + else + { + equal &= qgsDoubleNear( other.x(), mX, 1E-8 ); + equal &= qgsDoubleNear( other.y(), mY, 1E-8 ); + } + } + return equal; +} + +QgsMeshDatasetGroupMetadata::QgsMeshDatasetGroupMetadata( const QString &name, + bool isScalar, + DataType dataType, + double minimum, + double maximum, + int maximumVerticalLevels, + const QDateTime &referenceTime, + bool isTemporal, + const QMap &extraOptions ) + : mName( name ) + , mIsScalar( isScalar ) + , mDataType( dataType ) + , mMinimumValue( minimum ) + , mMaximumValue( maximum ) + , mExtraOptions( extraOptions ) + , mMaximumVerticalLevelsCount( maximumVerticalLevels ) + , mReferenceTime( referenceTime ) + , mIsTemporal( isTemporal ) + +{ +} + +QMap QgsMeshDatasetGroupMetadata::extraOptions() const +{ + return mExtraOptions; +} + +bool QgsMeshDatasetGroupMetadata::isVector() const +{ + return !mIsScalar; +} + +bool QgsMeshDatasetGroupMetadata::isScalar() const +{ + return mIsScalar; +} + +bool QgsMeshDatasetGroupMetadata::isTemporal() const +{ + return mIsTemporal; +} + +QString QgsMeshDatasetGroupMetadata::name() const +{ + return mName; +} + +QgsMeshDatasetGroupMetadata::DataType QgsMeshDatasetGroupMetadata::dataType() const +{ + return mDataType; +} + +double QgsMeshDatasetGroupMetadata::minimum() const +{ + return mMinimumValue; +} + +double QgsMeshDatasetGroupMetadata::maximum() const +{ + return mMaximumValue; +} + +int QgsMeshDatasetGroupMetadata::maximumVerticalLevelsCount() const +{ + return mMaximumVerticalLevelsCount; +} + +QDateTime QgsMeshDatasetGroupMetadata::referenceTime() const +{ + return mReferenceTime; +} + +QgsMeshDatasetMetadata::QgsMeshDatasetMetadata( + double time, + bool isValid, + double minimum, + double maximum, + int maximumVerticalLevels ) + : mTime( time ) + , mIsValid( isValid ) + , mMinimumValue( minimum ) + , mMaximumValue( maximum ) + , mMaximumVerticalLevelsCount( maximumVerticalLevels ) +{ +} + +double QgsMeshDatasetMetadata::time() const +{ + return mTime; +} + +bool QgsMeshDatasetMetadata::isValid() const +{ + return mIsValid; +} + +double QgsMeshDatasetMetadata::minimum() const +{ + return mMinimumValue; +} + +double QgsMeshDatasetMetadata::maximum() const +{ + return mMaximumValue; +} + +int QgsMeshDatasetMetadata::maximumVerticalLevelsCount() const +{ + return mMaximumVerticalLevelsCount; +} + +QgsMeshDataBlock::QgsMeshDataBlock() + : mType( ActiveFlagInteger ) +{ +} + +QgsMeshDataBlock::QgsMeshDataBlock( QgsMeshDataBlock::DataType type, int count ) + : mType( type ), + mSize( count ) +{ +} + +QgsMeshDataBlock::DataType QgsMeshDataBlock::type() const +{ + return mType; +} + +int QgsMeshDataBlock::count() const +{ + return mSize; +} + +bool QgsMeshDataBlock::isValid() const +{ + return ( count() > 0 ) && ( mIsValid ); +} + +QgsMeshDatasetValue QgsMeshDataBlock::value( int index ) const +{ + if ( !isValid() ) + return QgsMeshDatasetValue(); + + Q_ASSERT( mType != ActiveFlagInteger ); + + if ( mType == ScalarDouble ) + return QgsMeshDatasetValue( mDoubleBuffer[index] ); + + return QgsMeshDatasetValue( + mDoubleBuffer[2 * index], + mDoubleBuffer[2 * index + 1] + ); +} + +bool QgsMeshDataBlock::active( int index ) const +{ + if ( !isValid() ) + return false; + + Q_ASSERT( mType == ActiveFlagInteger ); + + if ( mIntegerBuffer.empty() ) + return true; + else + return bool( mIntegerBuffer[index] ); +} + +void QgsMeshDataBlock::setActive( const QVector &vals ) +{ + Q_ASSERT( mType == ActiveFlagInteger ); + Q_ASSERT( vals.size() == count() ); + + mIntegerBuffer = vals; + setValid( true ); +} + +QVector QgsMeshDataBlock::active() const +{ + Q_ASSERT( mType == ActiveFlagInteger ); + return mIntegerBuffer; +} + +QVector QgsMeshDataBlock::values() const +{ + Q_ASSERT( mType != ActiveFlagInteger ); + + return mDoubleBuffer; +} + +void QgsMeshDataBlock::setValues( const QVector &vals ) +{ + Q_ASSERT( mType != ActiveFlagInteger ); + Q_ASSERT( mType == ScalarDouble ? vals.size() == count() : vals.size() == 2 * count() ); + + mDoubleBuffer = vals; + setValid( true ); +} + +void QgsMeshDataBlock::setValid( bool valid ) +{ + mIsValid = valid; +} + +QgsMesh3dDataBlock::QgsMesh3dDataBlock() = default; + +QgsMesh3dDataBlock::~QgsMesh3dDataBlock() {}; + +QgsMesh3dDataBlock::QgsMesh3dDataBlock( int count, bool isVector ) + : mSize( count ) + , mIsVector( isVector ) +{ +} + +bool QgsMesh3dDataBlock::isValid() const +{ + return mIsValid; +} + +bool QgsMesh3dDataBlock::isVector() const +{ + return mIsVector; +} + +int QgsMesh3dDataBlock::count() const +{ + return mSize; +} + +int QgsMesh3dDataBlock::firstVolumeIndex() const +{ + if ( mFaceToVolumeIndex.empty() ) + return -1; + return mFaceToVolumeIndex[0]; +} + +int QgsMesh3dDataBlock::lastVolumeIndex() const +{ + if ( mFaceToVolumeIndex.empty() || mVerticalLevelsCount.empty() ) + return -1; + const int lastVolumeStartIndex = mFaceToVolumeIndex[mFaceToVolumeIndex.size() - 1]; + const int volumesCountInLastRow = mVerticalLevelsCount[mVerticalLevelsCount.size() - 1]; + return lastVolumeStartIndex + volumesCountInLastRow; +} + +int QgsMesh3dDataBlock::volumesCount() const +{ + return lastVolumeIndex() - firstVolumeIndex(); +} + +QVector QgsMesh3dDataBlock::verticalLevelsCount() const +{ + Q_ASSERT( isValid() ); + return mVerticalLevelsCount; +} + +void QgsMesh3dDataBlock::setFaceToVolumeIndex( const QVector &faceToVolumeIndex ) +{ + Q_ASSERT( faceToVolumeIndex.size() == count() ); + mFaceToVolumeIndex = faceToVolumeIndex; +} + +void QgsMesh3dDataBlock::setVerticalLevelsCount( const QVector &verticalLevelsCount ) +{ + Q_ASSERT( verticalLevelsCount.size() == count() ); + mVerticalLevelsCount = verticalLevelsCount; +} + +QVector QgsMesh3dDataBlock::verticalLevels() const +{ + Q_ASSERT( isValid() ); + return mVerticalLevels; +} + +void QgsMesh3dDataBlock::setVerticalLevels( const QVector &verticalLevels ) +{ + Q_ASSERT( verticalLevels.size() == volumesCount() + count() ); + mVerticalLevels = verticalLevels; +} + +QVector QgsMesh3dDataBlock::faceToVolumeIndex() const +{ + Q_ASSERT( isValid() ); + return mFaceToVolumeIndex; +} + +QVector QgsMesh3dDataBlock::values() const +{ + Q_ASSERT( isValid() ); + return mDoubleBuffer; +} + +QgsMeshDatasetValue QgsMesh3dDataBlock::value( int volumeIndex ) const +{ + if ( !isValid() ) + return QgsMeshDatasetValue(); + + if ( !mIsVector ) + return QgsMeshDatasetValue( mDoubleBuffer[volumeIndex] ); + + return QgsMeshDatasetValue( + mDoubleBuffer[2 * volumeIndex], + mDoubleBuffer[2 * volumeIndex + 1] + ); +} + +void QgsMesh3dDataBlock::setValues( const QVector &doubleBuffer ) +{ + Q_ASSERT( doubleBuffer.size() == ( isVector() ? 2 * volumesCount() : volumesCount() ) ); + mDoubleBuffer = doubleBuffer; +} + +void QgsMesh3dDataBlock::setValid( bool valid ) +{ + mIsValid = valid; +} + diff --git a/src/core/mesh/qgsmeshdataset.h b/src/core/mesh/qgsmeshdataset.h new file mode 100644 index 000000000000..37db333eae1b --- /dev/null +++ b/src/core/mesh/qgsmeshdataset.h @@ -0,0 +1,524 @@ +/*************************************************************************** + qgsmeshdataset.h + --------------------- + begin : April 2018 + copyright : (C) 2018 by Peter Petrik + email : zilolv 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 QGSMESHDATASET_H +#define QGSMESHDATASET_H + +#include +#include +#include +#include + +#include + +#include "qgis_core.h" +#include "qgspoint.h" +#include "qgsdataprovider.h" + + +class QgsRectangle; + +/** + * \ingroup core + * + * QgsMeshDatasetIndex is index that identifies the dataset group (e.g. wind speed) + * and a dataset in this group (e.g. magnitude of wind speed in particular time) + * + * \note The API is considered EXPERIMENTAL and can be changed without a notice + * + * \since QGIS 3.4 + */ +class CORE_EXPORT QgsMeshDatasetIndex +{ + public: + //! Creates an index. -1 represents invalid group/dataset + QgsMeshDatasetIndex( int group = -1, int dataset = -1 ); + //! Returns a group index + int group() const; + //! Returns a dataset index within group() + int dataset() const; + //! Returns whether index is valid, ie at least groups is set + bool isValid() const; + //! Equality operator + bool operator == ( QgsMeshDatasetIndex other ) const; + //! Inequality operator + bool operator != ( QgsMeshDatasetIndex other ) const; + private: + int mGroupIndex = -1; + int mDatasetIndex = -1; +}; + +/** + * \ingroup core + * + * QgsMeshDatasetValue represents single dataset value + * + * could be scalar or vector. Nodata values are represented by NaNs. + * + * \note The API is considered EXPERIMENTAL and can be changed without a notice + * + * \since QGIS 3.2 + */ +class CORE_EXPORT QgsMeshDatasetValue +{ + public: + //! Constructor for vector value + QgsMeshDatasetValue( double x, + double y ); + + //! Constructor for scalar value + QgsMeshDatasetValue( double scalar ); + + //! Default Ctor, initialize to NaN + QgsMeshDatasetValue() = default; + + //! Dtor + ~QgsMeshDatasetValue() = default; + + //! Sets scalar value + void set( double scalar ); + + //! Sets X value + void setX( double x ); + + //! Sets Y value + void setY( double y ) ; + + //! Returns magnitude of vector for vector data or scalar value for scalar data + double scalar() const; + + //! Returns x value + double x() const; + + //! Returns y value + double y() const; + + bool operator==( QgsMeshDatasetValue other ) const; + + private: + double mX = std::numeric_limits::quiet_NaN(); + double mY = std::numeric_limits::quiet_NaN(); +}; + +/** + * \ingroup core + * + * QgsMeshDataBlock is a block of integers/doubles that can be used + * to retrieve: + * active flags (e.g. face's active integer flag) + * scalars (e.g. scalar dataset double values) + * vectors (e.g. vector dataset doubles x,y values) + * + * data are implicitly shared, so the class can be quickly copied + * std::numeric_limits::quiet_NaN() represents NODATA value + * + * Data can be accessed all at once with values() (faster) or + * value by value (slower) with active() or value() + * + * \since QGIS 3.6 + */ +class CORE_EXPORT QgsMeshDataBlock +{ + public: + //! Type of data stored in the block + enum DataType + { + ActiveFlagInteger, //!< Integer boolean flag whether face is active + ScalarDouble, //!< Scalar double values + Vector2DDouble, //!< Vector double pairs (x1, y1, x2, y2, ... ) + }; + + //! Constructs an invalid block + QgsMeshDataBlock(); + + //! Constructs a new block + QgsMeshDataBlock( DataType type, int count ); + + //! Type of data stored in the block + DataType type() const; + + //! Number of items stored in the block + int count() const; + + //! Whether the block is valid + bool isValid() const; + + /** + * Returns a value represented by the index + * For active flag the behavior is undefined + */ + QgsMeshDatasetValue value( int index ) const; + + /** + * Returns a value for active flag by the index + * For scalar and vector 2d the behavior is undefined + */ + bool active( int index ) const; + + /** + * Sets active flag values. + * + * If the data provider/datasets does not have active + * flag capability (== all values are valid), just + * set block validity by setValid( true ) + * + * \param vals value vector with size count() + * + * For scalar and vector 2d the behavior is undefined + * + * \since QGIS 3.12 + */ + void setActive( const QVector &vals ); + + /** + * Returns active flag array + * + * Even for active flag valid dataset, the returned array could be empty. + * This means that the data provider/dataset does not support active flag + * capability, so all faces are active by default. + * + * For scalar and vector 2d the behavior is undefined + * + * \since QGIS 3.12 + */ + QVector active() const; + + /** + * Returns buffer to the array with values + * For vector it is pairs (x1, y1, x2, y2, ... ) + * + * \since QGIS 3.12 + */ + QVector values() const; + + /** + * Sets values + * + * For scalar datasets, it must have size count() + * For vector datasets, it must have size 2 * count() + * For active flag the behavior is undefined + * + * \since QGIS 3.12 + */ + void setValues( const QVector &vals ); + + //! Sets block validity + void setValid( bool valid ); + + private: + QVector mDoubleBuffer; + QVector mIntegerBuffer; + DataType mType; + int mSize = 0; + bool mIsValid = false; +}; + +/** + * \ingroup core + * + * QgsMesh3dDataBlock is a block of 3d stacked mesh data related N + * faces defined on base mesh frame. + * + * data are implicitly shared, so the class can be quickly copied + * std::numeric_limits::quiet_NaN() represents NODATA value + * + * \note The API is considered EXPERIMENTAL and can be changed without a notice + * + * \since QGIS 3.12 + */ +class CORE_EXPORT QgsMesh3dDataBlock +{ + public: + //! Constructs an invalid block + QgsMesh3dDataBlock(); + + //! Dtor + ~QgsMesh3dDataBlock(); + + //! Constructs a new block for count faces + QgsMesh3dDataBlock( int count, bool isVector ); + + //! Sets block validity + void setValid( bool valid ); + + //! Whether the block is valid + bool isValid() const; + + //! Whether we store vector values + bool isVector() const; + + //! Number of 2d faces for which the volume data is stored in the block + int count() const; + + //! Index of the first volume stored in the buffer (absolute) + int firstVolumeIndex() const; + + //! Index of the last volume stored in the buffer (absolute) + int lastVolumeIndex() const; + + //! Returns number of volumes stored in the buffer + int volumesCount() const; + + /** + * Returns number of vertical level above 2d faces + */ + QVector verticalLevelsCount() const; + + /** + * Sets the vertical level counts + */ + void setVerticalLevelsCount( const QVector &verticalLevelsCount ); + + /** + * Returns the vertical levels height + */ + QVector verticalLevels() const; + + /** + * Sets the vertical levels height + */ + void setVerticalLevels( const QVector &verticalLevels ); + + /** + * Returns the indexing between faces and volumes + */ + QVector faceToVolumeIndex() const; + + /** + * Sets the indexing between faces and volumes + */ + void setFaceToVolumeIndex( const QVector &faceToVolumeIndex ); + + /** + * Returns the values at volume centers + * + * For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) + */ + QVector values() const; + + /** + * Returns the value at volume centers + * + * \param volumeIndex volume index relative to firstVolumeIndex() + * \returns value (scalar or vector) + */ + QgsMeshDatasetValue value( int volumeIndex ) const; + + /** + * Sets the values at volume centers + * + * For vector datasets the number of values is doubled (x1, y1, x2, y2, ... ) + */ + void setValues( const QVector &doubleBuffer ); + + private: + int mSize = 0; + bool mIsValid = false; + bool mIsVector = false; + QVector mVerticalLevelsCount; + QVector mVerticalLevels; + QVector mFaceToVolumeIndex; + QVector mDoubleBuffer; // for scalar/vector values +}; + +/** + * \ingroup core + * + * QgsMeshDatasetGroupMetadata is a collection of dataset group metadata + * such as whether the data is vector or scalar, name + * + * \note The API is considered EXPERIMENTAL and can be changed without a notice + * + * \since QGIS 3.4 + */ +class CORE_EXPORT QgsMeshDatasetGroupMetadata +{ + public: + + //! Location of where data is specified for datasets in the dataset group + enum DataType + { + DataOnFaces = 0, //!< Data is defined on faces + DataOnVertices, //!< Data is defined on vertices + DataOnVolumes, //!< Data is defined on volumes \since QGIS 3.12 + DataOnEdges //!< Data is defined on edges \since QGIS 3.14 + }; + + //! Constructs an empty metadata object + QgsMeshDatasetGroupMetadata() = default; + + /** + * Constructs a valid metadata object + * + * \param name name of the dataset group + * \param isScalar dataset contains scalar data, specifically the y-value of QgsMeshDatasetValue is NaN + * \param dataType where the data are defined on (vertices, faces or volumes) + * \param minimum minimum value (magnitude for vectors) present among all group's dataset values + * \param maximum maximum value (magnitude for vectors) present among all group's dataset values + * \param maximumVerticalLevels maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes + * \param referenceTime reference time of the dataset group + * \param isTemporal weither the dataset group is temporal (contains time-related dataset) + * \param extraOptions dataset's extra options stored by the provider. Usually contains the name, time value, time units, data file vendor, ... + */ + QgsMeshDatasetGroupMetadata( const QString &name, + bool isScalar, + DataType dataType, + double minimum, + double maximum, + int maximumVerticalLevels, + const QDateTime &referenceTime, + bool isTemporal, + const QMap &extraOptions ); + + /** + * Returns name of the dataset group + */ + QString name() const; + + /** + * Returns extra metadata options, for example description + */ + QMap extraOptions() const; + + /** + * \brief Returns whether dataset group has vector data + */ + bool isVector() const; + + /** + * \brief Returns whether dataset group has scalar data + */ + bool isScalar() const; + + + /** + * \brief Returns whether the dataset group is temporal (contains time-related dataset) + */ + bool isTemporal() const; + + + /** + * Returns whether dataset group data is defined on vertices or faces or volumes + * + * \since QGIS 3.12 + */ + DataType dataType() const; + + /** + * \brief Returns minimum scalar value/vector magnitude present for whole dataset group + */ + double minimum() const; + + /** + * \brief Returns maximum scalar value/vector magnitude present for whole dataset group + */ + double maximum() const; + + /** + * Returns maximum number of vertical levels for 3d stacked meshes + * + * \since QGIS 3.12 + */ + int maximumVerticalLevelsCount() const; + + /** + * Returns the reference time + * + * \since QGIS 3.12 + */ + QDateTime referenceTime() const; + + private: + QString mName; + bool mIsScalar = false; + DataType mDataType = DataType::DataOnFaces; + double mMinimumValue = std::numeric_limits::quiet_NaN(); + double mMaximumValue = std::numeric_limits::quiet_NaN(); + QMap mExtraOptions; + int mMaximumVerticalLevelsCount = 0; // for 3d stacked meshes + QDateTime mReferenceTime; + bool mIsTemporal = false; +}; + +/** + * \ingroup core + * + * QgsMeshDatasetMetadata is a collection of mesh dataset metadata such + * as whether the data is valid or associated time for the dataset + * + * \note The API is considered EXPERIMENTAL and can be changed without a notice + * + * \since QGIS 3.2 + */ +class CORE_EXPORT QgsMeshDatasetMetadata +{ + public: + //! Constructs an empty metadata object + QgsMeshDatasetMetadata() = default; + + /** + * Constructs a valid metadata object + * + * \param time a time which this dataset represents in the dataset group + * \param isValid dataset is loadad and valid for fetching the data + * \param minimum minimum value (magnitude for vectors) present among dataset values + * \param maximum maximum value (magnitude for vectors) present among dataset values + * \param maximumVerticalLevels maximum number of vertical levels for 3d stacked meshes, 0 for 2d meshes + */ + QgsMeshDatasetMetadata( double time, + bool isValid, + double minimum, + double maximum, + int maximumVerticalLevels + ); + + /** + * Returns the time value for this dataset + */ + double time() const; + + /** + * Returns whether dataset is valid + */ + bool isValid() const; + + /** + * Returns minimum scalar value/vector magnitude present for the dataset + */ + double minimum() const; + + /** + * Returns maximum scalar value/vector magnitude present for the dataset + */ + double maximum() const; + + /** + * Returns maximum number of vertical levels for 3d stacked meshes + * + * \since QGIS 3.12 + */ + int maximumVerticalLevelsCount() const; + + private: + double mTime = std::numeric_limits::quiet_NaN(); + bool mIsValid = false; + double mMinimumValue = std::numeric_limits::quiet_NaN(); + double mMaximumValue = std::numeric_limits::quiet_NaN(); + int mMaximumVerticalLevelsCount = 0; // for 3d stacked meshes +}; + +#endif // QGSMESHDATASET_H diff --git a/src/core/mesh/qgsmeshlayer.cpp b/src/core/mesh/qgsmeshlayer.cpp index 4797a4222316..06c8bb3318b7 100644 --- a/src/core/mesh/qgsmeshlayer.cpp +++ b/src/core/mesh/qgsmeshlayer.cpp @@ -26,6 +26,7 @@ #include "qgsmeshdataprovider.h" #include "qgsmeshlayer.h" #include "qgsmeshlayerrenderer.h" +#include "qgsmeshlayertemporalproperties.h" #include "qgsmeshlayerutils.h" #include "qgsmeshtimesettings.h" #include "qgspainting.h" @@ -53,6 +54,10 @@ QgsMeshLayer::QgsMeshLayer( const QString &meshLayerPath, setLegend( QgsMapLayerLegend::defaultMeshLegend( this ) ); setDefaultRendererSettings(); + + mTemporalProperties = new QgsMeshLayerTemporalProperties( this ); + if ( mDataProvider ) + mTemporalProperties->setDefaultsFromDataProviderTemporalCapabilities( mDataProvider->temporalCapabilities() ); } @@ -61,7 +66,7 @@ void QgsMeshLayer::setDefaultRendererSettings() if ( mDataProvider && mDataProvider->datasetGroupCount() > 0 ) { // show data from the first dataset group - mRendererSettings.setActiveScalarDataset( QgsMeshDatasetIndex( 0, 0 ) ); + mRendererSettings.setActiveScalarDatasetGroup( 0 ); } else { @@ -70,6 +75,29 @@ void QgsMeshLayer::setDefaultRendererSettings() meshSettings.setEnabled( true ); mRendererSettings.setNativeMeshSettings( meshSettings ); } + + // Sets default resample method for scalar dataset + if ( !mDataProvider ) + return; + for ( int i = 0; i < mDataProvider->datasetGroupCount(); ++i ) + { + QgsMeshDatasetGroupMetadata meta = mDataProvider->datasetGroupMetadata( i ); + QgsMeshRendererScalarSettings scalarSettings = mRendererSettings.scalarSettings( i ); + switch ( meta.dataType() ) + { + case QgsMeshDatasetGroupMetadata::DataOnFaces: + case QgsMeshDatasetGroupMetadata::DataOnVolumes: // data on volumes are averaged to 2D data on faces + scalarSettings.setDataResamplingMethod( QgsMeshRendererScalarSettings::NeighbourAverage ); + break; + case QgsMeshDatasetGroupMetadata::DataOnVertices: + scalarSettings.setDataResamplingMethod( QgsMeshRendererScalarSettings::None ); + break; + case QgsMeshDatasetGroupMetadata::DataOnEdges: + break; + } + mRendererSettings.setScalarSettings( i, scalarSettings ); + } + } void QgsMeshLayer::createSimplifiedMeshes() @@ -212,7 +240,10 @@ void QgsMeshLayer::setTimeSettings( const QgsMeshTimeSettings &settings ) QString QgsMeshLayer::formatTime( double hours ) { - return QgsMeshLayerUtils::formatTime( hours, mTimeSettings ); + if ( dataProvider() && dataProvider()->temporalCapabilities()->hasReferenceTime() ) + return QgsMeshLayerUtils::formatTime( hours, temporalProperties()->referenceTime(), mTimeSettings ); + else + return QgsMeshLayerUtils::formatTime( hours, QDateTime(), mTimeSettings ); } QgsMeshDatasetValue QgsMeshLayer::datasetValue( const QgsMeshDatasetIndex &index, const QgsPointXY &point ) const @@ -314,6 +345,34 @@ void QgsMeshLayer::setTransformContext( const QgsCoordinateTransformContext &tra mDataProvider->setTransformContext( transformContext ); } +QgsMeshDatasetIndex QgsMeshLayer::datasetIndexAtTime( const QgsDateTimeRange &timeRange, int datasetGroupIndex ) const +{ + const QDateTime layerReferenceTime = mTemporalProperties->referenceTime(); + qint64 startTime = layerReferenceTime.msecsTo( timeRange.begin() ); + qint64 endTime = layerReferenceTime.msecsTo( timeRange.end() ); + + if ( dataProvider() ) + return dataProvider()->temporalCapabilities()->datasetIndexFromRelativeTimeRange( datasetGroupIndex, startTime, endTime ); + else + return QgsMeshDatasetIndex(); +} + +QgsMeshDatasetIndex QgsMeshLayer::activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const +{ + if ( mTemporalProperties->isActive() ) + return datasetIndexAtTime( timeRange, mRendererSettings.activeScalarDatasetGroup() ); + else + return mStaticScalarDatasetIndex; +} + +QgsMeshDatasetIndex QgsMeshLayer::activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const +{ + if ( mTemporalProperties->isActive() ) + return datasetIndexAtTime( timeRange, mRendererSettings.activeVectorDatasetGroup() ); + else + return mStaticVectorDatasetIndex; +} + void QgsMeshLayer::fillNativeMesh() { Q_ASSERT( !mNativeMesh ); @@ -332,6 +391,42 @@ void QgsMeshLayer::onDatasetGroupsAdded( int count ) int newDatasetGroupCount = mDataProvider->datasetGroupCount(); for ( int i = newDatasetGroupCount - count; i < newDatasetGroupCount; ++i ) assignDefaultStyleToDatasetGroup( i ); + + if ( mDataProvider ) + temporalProperties()->setIsActive( mDataProvider->temporalCapabilities()->hasTemporalCapabilities() ); + +} + +QgsMeshDatasetIndex QgsMeshLayer::staticVectorDatasetIndex() const +{ + return mStaticVectorDatasetIndex; +} + +void QgsMeshLayer::setReferenceTime( const QDateTime &referenceTime ) +{ + if ( dataProvider() ) + temporalProperties()->setReferenceTime( referenceTime, dataProvider()->temporalCapabilities() ); + else + temporalProperties()->setReferenceTime( referenceTime, nullptr ); +} + +QgsMeshDatasetIndex QgsMeshLayer::staticScalarDatasetIndex() const +{ + return mStaticScalarDatasetIndex; +} + +void QgsMeshLayer::setStaticVectorDatasetIndex( const QgsMeshDatasetIndex &staticVectorDatasetIndex ) +{ + mStaticVectorDatasetIndex = staticVectorDatasetIndex; + if ( !temporalProperties()->isActive() ) + mRendererSettings.setActiveVectorDatasetGroup( staticVectorDatasetIndex.group() ); +} + +void QgsMeshLayer::setStaticScalarDatasetIndex( const QgsMeshDatasetIndex &staticScalarDatasetIndex ) +{ + mStaticScalarDatasetIndex = staticScalarDatasetIndex; + if ( !temporalProperties()->isActive() ) + mRendererSettings.setActiveScalarDatasetGroup( staticScalarDatasetIndex.group() ); } QgsMeshSimplificationSettings QgsMeshLayer::meshSimplificationSettings() const @@ -381,6 +476,13 @@ void QgsMeshLayer::assignDefaultStyleToDatasetGroup( int groupIndex ) scalarSettings.setClassificationMinimumMaximum( groupMin, groupMax ); scalarSettings.setColorRampShader( fcn ); mRendererSettings.setScalarSettings( groupIndex, scalarSettings ); + + if ( metadata.isVector() ) + { + QgsMeshRendererVectorSettings vectorSettings; + vectorSettings.setColorRampShader( fcn ); + mRendererSettings.setVectorSettings( groupIndex, vectorSettings ); + } } QgsMapLayerRenderer *QgsMeshLayer::createMapRenderer( QgsRenderContext &rendererContext ) @@ -412,10 +514,6 @@ bool QgsMeshLayer::readSymbology( const QDomNode &node, QString &errorMessage, if ( !elemRendererSettings.isNull() ) mRendererSettings.readXml( elemRendererSettings ); - QDomElement elemTimeSettings = elem.firstChildElement( "mesh-time-settings" ); - if ( !elemTimeSettings.isNull() ) - mTimeSettings.readXml( elemTimeSettings, context ); - QDomElement elemSimplifySettings = elem.firstChildElement( "mesh-simplify-settings" ); if ( !elemSimplifySettings.isNull() ) mSimplificationSettings.readXml( elemSimplifySettings, context ); @@ -444,9 +542,6 @@ bool QgsMeshLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString &e QDomElement elemRendererSettings = mRendererSettings.writeXml( doc ); elem.appendChild( elemRendererSettings ); - QDomElement elemTimeSettings = mTimeSettings.writeXml( doc, context ); - elem.appendChild( elemTimeSettings ); - QDomElement elemSimplifySettings = mSimplificationSettings.writeXml( doc, context ); elem.appendChild( elemSimplifySettings ); @@ -459,6 +554,16 @@ bool QgsMeshLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString &e return true; } +bool QgsMeshLayer::writeStyle( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) const +{ + return writeSymbology( node, doc, errorMessage, context, categories ); +} + +bool QgsMeshLayer::readStyle( const QDomNode &node, QString &errorMessage, QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) +{ + return readSymbology( node, errorMessage, context, categories ); +} + QString QgsMeshLayer::decodedSource( const QString &source, const QString &provider, const QgsReadWriteContext &context ) const { QString src( source ); @@ -526,9 +631,34 @@ bool QgsMeshLayer::readXml( const QDomNode &layer_node, QgsReadWriteContext &con } } + if ( mDataProvider && pkeyNode.toElement().hasAttribute( QStringLiteral( "time-unit" ) ) ) + mDataProvider->setTemporalUnit( + static_cast( pkeyNode.toElement().attribute( QStringLiteral( "time-unit" ) ).toInt() ) ); + QString errorMsg; readSymbology( layer_node, errorMsg, context ); + // read temporal + temporalProperties()->readXml( layer_node.toElement(), context ); + if ( !temporalProperties()->timeExtent().begin().isValid() ) + temporalProperties()->setDefaultsFromDataProviderTemporalCapabilities( dataProvider()->temporalCapabilities() ); + + + // read static dataset + QDomElement elemStaticDataset = layer_node.firstChildElement( QStringLiteral( "static-active-dataset" ) ); + if ( elemStaticDataset.hasAttribute( QStringLiteral( "scalar" ) ) ) + { + QStringList lst = elemStaticDataset.attribute( QStringLiteral( "scalar" ) ).split( QChar( ',' ) ); + if ( lst.count() == 2 ) + mStaticScalarDatasetIndex = QgsMeshDatasetIndex( lst[0].toInt(), lst[1].toInt() ); + } + if ( elemStaticDataset.hasAttribute( QStringLiteral( "vector" ) ) ) + { + QStringList lst = elemStaticDataset.attribute( QStringLiteral( "vector" ) ).split( QChar( ',' ) ); + if ( lst.count() == 2 ) + mStaticVectorDatasetIndex = QgsMeshDatasetIndex( lst[0].toInt(), lst[1].toInt() ); + } + return mValid; // should be true if read successfully } @@ -552,6 +682,7 @@ bool QgsMeshLayer::writeXml( QDomNode &layer_node, QDomDocument &document, const QDomText providerText = document.createTextNode( providerType() ); provider.appendChild( providerText ); layer_node.appendChild( provider ); + provider.setAttribute( QStringLiteral( "time-unit" ), mDataProvider->temporalCapabilities()->temporalUnit() ); const QStringList extraDatasetUris = mDataProvider->extraDatasets(); QDomElement elemExtraDatasets = document.createElement( QStringLiteral( "extra-datasets" ) ); @@ -564,6 +695,15 @@ bool QgsMeshLayer::writeXml( QDomNode &layer_node, QDomDocument &document, const } layer_node.appendChild( elemExtraDatasets ); } + // write temporal + mTemporalProperties->writeXml( mapLayerNode, document, context ); + + QDomElement elemStaticDataset = document.createElement( QStringLiteral( "static-active-dataset" ) ); + if ( mStaticScalarDatasetIndex.isValid() ) + elemStaticDataset.setAttribute( QStringLiteral( "scalar" ), QStringLiteral( "%1,%2" ).arg( mStaticScalarDatasetIndex.group() ).arg( mStaticScalarDatasetIndex.dataset() ) ); + if ( mStaticVectorDatasetIndex.isValid() ) + elemStaticDataset.setAttribute( QStringLiteral( "vector" ), QStringLiteral( "%1,%2" ).arg( mStaticVectorDatasetIndex.group() ).arg( mStaticVectorDatasetIndex.dataset() ) ); + layer_node.appendChild( elemStaticDataset ); // renderer specific settings QString errorMsg; @@ -622,13 +762,6 @@ bool QgsMeshLayer::setDataProvider( QString const &provider, const QgsDataProvid mDataSource = mDataSource + QStringLiteral( "&uid=%1" ).arg( QUuid::createUuid().toString() ); } - QDateTime referenceTime = QgsMeshLayerUtils::firstReferenceTime( this ); - if ( referenceTime.isValid() ) - { - mTimeSettings.setAbsoluteTimeReferenceTime( referenceTime ); - mTimeSettings.setUseAbsoluteTime( true ); - } - for ( int i = 0; i < mDataProvider->datasetGroupCount(); ++i ) assignDefaultStyleToDatasetGroup( i ); @@ -637,3 +770,8 @@ bool QgsMeshLayer::setDataProvider( QString const &provider, const QgsDataProvid return true; } + +QgsMeshLayerTemporalProperties *QgsMeshLayer::temporalProperties() +{ + return mTemporalProperties; +} diff --git a/src/core/mesh/qgsmeshlayer.h b/src/core/mesh/qgsmeshlayer.h index d7bb9b622183..8add5a430d32 100644 --- a/src/core/mesh/qgsmeshlayer.h +++ b/src/core/mesh/qgsmeshlayer.h @@ -26,6 +26,7 @@ #include "qgsmeshrenderersettings.h" #include "qgsmeshtimesettings.h" #include "qgsmeshsimplificationsettings.h" +#include "qgsmeshlayertemporalproperties.h" class QgsMapLayerRenderer; struct QgsMeshLayerRendererCache; @@ -68,11 +69,11 @@ class QgsMesh3dAveragingMethod; * "3.0, 2.0 \n" \ * "2.0, 3.0 \n" \ * "1.0, 3.0 \n" \ - * "---" + * "---" \ * "0, 1, 3, 4 \n" \ * "1, 2, 3 \n" * ); - * QgsMeshLayer *scratchLayer = new QgsMeshLayer(uri, "My Scratch layer", "memory_mesh"); + * QgsMeshLayer *scratchLayer = new QgsMeshLayer(uri, "My Scratch layer", "mesh_memory"); * \endcode * * \subsection mdal MDAL data provider (mdal) @@ -158,10 +159,13 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories = QgsMapLayer::AllStyleCategories ) override; bool writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories = QgsMapLayer::AllStyleCategories ) const override; + bool writeStyle( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ) const override; + bool readStyle( const QDomNode &node, QString &errorMessage, QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ) override; QString encodedSource( const QString &source, const QgsReadWriteContext &context ) const override; QString decodedSource( const QString &source, const QString &provider, const QgsReadWriteContext &context ) const override; bool readXml( const QDomNode &layer_node, QgsReadWriteContext &context ) override; bool writeXml( QDomNode &layer_node, QDomDocument &doc, const QgsReadWriteContext &context ) const override; + QgsMeshLayerTemporalProperties *temporalProperties() override; void reload() override; @@ -285,6 +289,69 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer */ QgsMesh3dDataBlock dataset3dValue( const QgsMeshDatasetIndex &index, const QgsPointXY &point ) const; + /** + * Returns dataset index from active scalar group depending on the time range. + * If the temporal properties is not active, return the static dataset + * + * \param timeRange the time range + * \returns dataset index + * + * \since QGIS 3.14 + */ + QgsMeshDatasetIndex activeScalarDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + + /** + * Returns dataset index from active vector group depending on the time range + * If the temporal properties is not active, return the static dataset + * + * \param timeRange the time range + * \returns dataset index + * + * \since QGIS 3.14 + */ + QgsMeshDatasetIndex activeVectorDatasetAtTime( const QgsDateTimeRange &timeRange ) const; + + /** + * Sets the static scalar dataset index that is rendered if the temporal properties is not active + * + * \param staticScalarDatasetIndex the scalar data set index + * + * \since QGIS 3.14 + */ + void setStaticScalarDatasetIndex( const QgsMeshDatasetIndex &staticScalarDatasetIndex ) SIP_SKIP; + + /** + * Sets the static vector dataset index that is rendered if the temporal properties is not active + * + * \param staticVectorDatasetIndex the vector data set index + * + * \since QGIS 3.14 + */ + void setStaticVectorDatasetIndex( const QgsMeshDatasetIndex &staticVectorDatasetIndex ) SIP_SKIP; + + /** + * Returns the static scalar dataset index that is rendered if the temporal properties is not active + * + * \since QGIS 3.14 + */ + QgsMeshDatasetIndex staticScalarDatasetIndex() const; + + /** + * Returns the static vector dataset index that is rendered if the temporal properties is not active + * + * \since QGIS 3.14 + */ + QgsMeshDatasetIndex staticVectorDatasetIndex() const; + + /** + * Sets the reference time of the layer + * + * \param referenceTime the reference time + * + * \since QGIS 3.14 + */ + void setReferenceTime( const QDateTime &referenceTime ); + public slots: /** @@ -297,18 +364,18 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer signals: /** - * Emitted when active scalar dataset is changed + * Emitted when active scalar group dataset is changed * - * \since QGIS 3.4 + * \since QGIS 3.14 */ - void activeScalarDatasetChanged( const QgsMeshDatasetIndex &index ); + void activeScalarDatasetGroupChanged( int index ); /** - * Emitted when active vector dataset is changed + * Emitted when active vector group dataset is changed * - * \since QGIS 3.4 + * \since QGIS 3.14 */ - void activeVectorDatasetChanged( const QgsMeshDatasetIndex &index ); + void activeVectorDatasetGroupChanged( int index ); /** * Emitted when time format is changed @@ -344,6 +411,8 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer bool hasSimplifiedMeshes() const; + QgsMeshDatasetIndex datasetIndexAtTime( const QgsDateTimeRange &timeRange, int datasetGroupIndex ) const; + private slots: void onDatasetGroupsAdded( int count ); @@ -368,6 +437,11 @@ class CORE_EXPORT QgsMeshLayer : public QgsMapLayer //! Simplify mesh configuration QgsMeshSimplificationSettings mSimplificationSettings; + + QgsMeshLayerTemporalProperties *mTemporalProperties; + + QgsMeshDatasetIndex mStaticScalarDatasetIndex; + QgsMeshDatasetIndex mStaticVectorDatasetIndex; }; #endif //QGSMESHLAYER_H diff --git a/src/core/mesh/qgsmeshlayerrenderer.cpp b/src/core/mesh/qgsmeshlayerrenderer.cpp index 2f8fab0d7db2..eef347a1dcba 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.cpp +++ b/src/core/mesh/qgsmeshlayerrenderer.cpp @@ -38,6 +38,7 @@ #include "qgsfillsymbollayer.h" #include "qgssettings.h" #include "qgsstyle.h" +#include "qgsmeshdataprovidertemporalcapabilities.h" QgsMeshLayerRenderer::QgsMeshLayerRenderer( QgsMeshLayer *layer, @@ -102,11 +103,15 @@ void QgsMeshLayerRenderer::calculateOutputSize() void QgsMeshLayerRenderer::copyScalarDatasetValues( QgsMeshLayer *layer ) { - const QgsMeshDatasetIndex datasetIndex = mRendererSettings.activeScalarDataset(); + QgsMeshDatasetIndex datasetIndex; + if ( renderContext()->isTemporal() ) + datasetIndex = layer->activeScalarDatasetAtTime( renderContext()->temporalRange() ); + else + datasetIndex = layer->staticScalarDatasetIndex(); // Find out if we can use cache up to date. If yes, use it and return const int datasetGroupCount = layer->dataProvider()->datasetGroupCount(); - const QgsMeshRendererScalarSettings::DataInterpolationMethod method = mRendererSettings.scalarSettings( datasetIndex.group() ).dataInterpolationMethod(); + const QgsMeshRendererScalarSettings::DataResamplingMethod method = mRendererSettings.scalarSettings( datasetIndex.group() ).dataResamplingMethod(); QgsMeshLayerRendererCache *cache = layer->rendererCache(); if ( ( cache->mDatasetGroupsCount == datasetGroupCount ) && ( cache->mActiveScalarDatasetIndex == datasetIndex ) && @@ -125,7 +130,7 @@ void QgsMeshLayerRenderer::copyScalarDatasetValues( QgsMeshLayer *layer ) // Cache is not up-to-date, gather data if ( datasetIndex.isValid() ) { - const QgsMeshDatasetGroupMetadata metadata = layer->dataProvider()->datasetGroupMetadata( datasetIndex ); + const QgsMeshDatasetGroupMetadata metadata = layer->dataProvider()->datasetGroupMetadata( datasetIndex.group() ); mScalarDataType = QgsMeshLayerUtils::datasetValuesType( metadata.dataType() ); // populate scalar values @@ -153,16 +158,31 @@ void QgsMeshLayerRenderer::copyScalarDatasetValues( QgsMeshLayer *layer ) mNativeMesh.faces.count() ); // for data on faces, there could be request to interpolate the data to vertices - if ( ( mScalarDataType == QgsMeshDatasetGroupMetadata::DataType::DataOnFaces ) && ( method != QgsMeshRendererScalarSettings::None ) ) + if ( method != QgsMeshRendererScalarSettings::None ) { - mScalarDataType = QgsMeshDatasetGroupMetadata::DataType::DataOnVertices; - mScalarDatasetValues = QgsMeshLayerUtils::interpolateFromFacesData( - mScalarDatasetValues, - &mNativeMesh, - &mTriangularMesh, - &mScalarActiveFaceFlagValues, - method - ); + if ( mScalarDataType == QgsMeshDatasetGroupMetadata::DataType::DataOnFaces ) + { + mScalarDataType = QgsMeshDatasetGroupMetadata::DataType::DataOnVertices; + mScalarDatasetValues = QgsMeshLayerUtils::interpolateFromFacesData( + mScalarDatasetValues, + &mNativeMesh, + &mTriangularMesh, + &mScalarActiveFaceFlagValues, + method + ); + } + else if ( mScalarDataType == QgsMeshDatasetGroupMetadata::DataType::DataOnVertices ) + { + mScalarDataType = QgsMeshDatasetGroupMetadata::DataType::DataOnFaces; + mScalarDatasetValues = QgsMeshLayerUtils::resampleFromVerticesToFaces( + mScalarDatasetValues, + &mNativeMesh, + &mTriangularMesh, + &mScalarActiveFaceFlagValues, + method + ); + } + } const QgsMeshDatasetMetadata datasetMetadata = layer->dataProvider()->datasetMetadata( datasetIndex ); @@ -182,9 +202,14 @@ void QgsMeshLayerRenderer::copyScalarDatasetValues( QgsMeshLayer *layer ) cache->mScalarAveragingMethod.reset( mRendererSettings.averagingMethod() ? mRendererSettings.averagingMethod()->clone() : nullptr ); } + void QgsMeshLayerRenderer::copyVectorDatasetValues( QgsMeshLayer *layer ) { - const QgsMeshDatasetIndex datasetIndex = mRendererSettings.activeVectorDataset(); + QgsMeshDatasetIndex datasetIndex; + if ( renderContext()->isTemporal() ) + datasetIndex = layer->activeVectorDatasetAtTime( renderContext()->temporalRange() ); + else + datasetIndex = layer->staticVectorDatasetIndex(); // Find out if we can use cache up to date. If yes, use it and return const int datasetGroupCount = layer->dataProvider()->datasetGroupCount(); @@ -409,11 +434,11 @@ void QgsMeshLayerRenderer::renderScalarDataset() if ( std::isnan( mScalarDatasetMinimum ) || std::isnan( mScalarDatasetMaximum ) ) return; // only NODATA values - QgsMeshDatasetIndex index = mRendererSettings.activeScalarDataset(); - if ( !index.isValid() ) + int groupIndex = mRendererSettings.activeScalarDatasetGroup(); + if ( groupIndex < 0 ) return; // no shader - const QgsMeshRendererScalarSettings scalarSettings = mRendererSettings.scalarSettings( index.group() ); + const QgsMeshRendererScalarSettings scalarSettings = mRendererSettings.scalarSettings( groupIndex ); if ( ( mTriangularMesh.contains( QgsMesh::ElementType::Face ) ) && ( mScalarDataType != QgsMeshDatasetGroupMetadata::DataType::DataOnEdges ) ) @@ -625,8 +650,8 @@ void QgsMeshLayerRenderer::renderScalarDatasetOnFaces( const QgsMeshRendererScal void QgsMeshLayerRenderer::renderVectorDataset() { - QgsMeshDatasetIndex index = mRendererSettings.activeVectorDataset(); - if ( !index.isValid() ) + int groupIndex = mRendererSettings.activeVectorDatasetGroup(); + if ( groupIndex < 0 ) return; if ( !mVectorDatasetValues.isValid() ) @@ -643,7 +668,7 @@ void QgsMeshLayerRenderer::renderVectorDataset() mVectorDatasetMagMaximum, mVectorDatasetMagMinimum, mVectorDataType, - mRendererSettings.vectorSettings( index.group() ), + mRendererSettings.vectorSettings( groupIndex ), *renderContext(), mLayerExtent, mOutputSize ) ); diff --git a/src/core/mesh/qgsmeshlayerrenderer.h b/src/core/mesh/qgsmeshlayerrenderer.h index 0934f6d03ee4..a9b691df91bf 100644 --- a/src/core/mesh/qgsmeshlayerrenderer.h +++ b/src/core/mesh/qgsmeshlayerrenderer.h @@ -63,7 +63,7 @@ struct CORE_NO_EXPORT QgsMeshLayerRendererCache QgsMeshDatasetGroupMetadata::DataType mScalarDataType = QgsMeshDatasetGroupMetadata::DataType::DataOnVertices; double mScalarDatasetMinimum = std::numeric_limits::quiet_NaN(); double mScalarDatasetMaximum = std::numeric_limits::quiet_NaN(); - QgsMeshRendererScalarSettings::DataInterpolationMethod mDataInterpolationMethod = QgsMeshRendererScalarSettings::None; + QgsMeshRendererScalarSettings::DataResamplingMethod mDataInterpolationMethod = QgsMeshRendererScalarSettings::None; std::unique_ptr mScalarAveragingMethod; // vector dataset @@ -112,6 +112,7 @@ class QgsMeshLayerRenderer : public QgsMapLayerRenderer QgsPointXY fractionPoint( const QgsPointXY &p1, const QgsPointXY &p2, double fraction ) const; bool mIsMeshSimplificationActive = false; QColor colorAt( QgsColorRampShader *shader, double val ) const; + protected: //! feedback class for cancellation std::unique_ptr mFeedback; diff --git a/src/core/mesh/qgsmeshlayertemporalproperties.cpp b/src/core/mesh/qgsmeshlayertemporalproperties.cpp new file mode 100644 index 000000000000..92d618e4e005 --- /dev/null +++ b/src/core/mesh/qgsmeshlayertemporalproperties.cpp @@ -0,0 +1,95 @@ +/*************************************************************************** + qgsmeshlayertemporalproperties.cpp + ----------------------- + begin : March 2020 + copyright : (C) 2020 by Vincent Cloarec + email : vcloarec 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 "qgsmeshlayertemporalproperties.h" +#include "qgsmeshdataprovidertemporalcapabilities.h" +#include "qgsproject.h" +#include "qgsprojecttimesettings.h" + +QgsMeshLayerTemporalProperties::QgsMeshLayerTemporalProperties( QObject *parent, bool enabled ): + QgsMapLayerTemporalProperties( parent, enabled ) +{} + +QDomElement QgsMeshLayerTemporalProperties::writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) +{ + Q_UNUSED( context ); + + QDomElement temporalElement = doc.createElement( QStringLiteral( "temporal" ) ); + temporalElement.setAttribute( QStringLiteral( "temporal-active" ), isActive() ? true : false ); + temporalElement.setAttribute( QStringLiteral( "reference-time" ), mReferenceTime.toTimeSpec( Qt::UTC ).toString( Qt::ISODate ) ); + temporalElement.setAttribute( QStringLiteral( "start-time-extent" ), mTimeExtent.begin().toTimeSpec( Qt::UTC ).toString( Qt::ISODate ) ); + temporalElement.setAttribute( QStringLiteral( "end-time-extent" ), mTimeExtent.end().toTimeSpec( Qt::UTC ).toString( Qt::ISODate ) ); + + element.appendChild( temporalElement ); + + return element; +} + +bool QgsMeshLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) +{ + Q_UNUSED( context ); + + QDomElement temporalElement = element.firstChildElement( QStringLiteral( "temporal" ) ); + bool active = temporalElement.attribute( QStringLiteral( "temporal-active" ) ).toInt(); + setIsActive( active ); + + mReferenceTime = QDateTime::fromString( temporalElement.attribute( QStringLiteral( "reference-time" ) ), Qt::ISODate ); + + if ( temporalElement.hasAttribute( QStringLiteral( "start-time-extent" ) ) + && temporalElement.hasAttribute( QStringLiteral( "end-time-extent" ) ) ) + { + QDateTime start = QDateTime::fromString( temporalElement.attribute( QStringLiteral( "start-time-extent" ) ), Qt::ISODate ); + QDateTime end = QDateTime::fromString( temporalElement.attribute( QStringLiteral( "end-time-extent" ) ), Qt::ISODate ); + mTimeExtent = QgsDateTimeRange( start, end ); + } + + return true; +} + +void QgsMeshLayerTemporalProperties::setDefaultsFromDataProviderTemporalCapabilities( const QgsDataProviderTemporalCapabilities *capabilities ) +{ + const QgsMeshDataProviderTemporalCapabilities *temporalCapabilities = + static_cast( capabilities ); + + setIsActive( temporalCapabilities->hasTemporalCapabilities() ); + mReferenceTime = temporalCapabilities->referenceTime(); + + if ( mReferenceTime.isValid() ) + mTimeExtent = temporalCapabilities->timeExtent(); +} + +QgsDateTimeRange QgsMeshLayerTemporalProperties::timeExtent() const +{ + return mTimeExtent; +} + +QDateTime QgsMeshLayerTemporalProperties::referenceTime() const +{ + return mReferenceTime; +} + +void QgsMeshLayerTemporalProperties::setReferenceTime( const QDateTime &referenceTime, const QgsDataProviderTemporalCapabilities *capabilities ) +{ + mReferenceTime = referenceTime; + if ( capabilities ) + { + const QgsMeshDataProviderTemporalCapabilities *tempCap = static_cast( capabilities ); + mTimeExtent = tempCap->timeExtent( referenceTime ); + } + else + mTimeExtent = QgsDateTimeRange( referenceTime, referenceTime ); +} diff --git a/src/core/mesh/qgsmeshlayertemporalproperties.h b/src/core/mesh/qgsmeshlayertemporalproperties.h new file mode 100644 index 000000000000..974778e1077c --- /dev/null +++ b/src/core/mesh/qgsmeshlayertemporalproperties.h @@ -0,0 +1,92 @@ +/*************************************************************************** + qgsmeshlayertemporalproperties.h + ----------------------- + begin : March 2020 + copyright : (C) 2020 by Vincent Cloarec + email : vcloarec 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 QGSMESHLAYERTEMPORALPROPERTIES_H +#define QGSMESHLAYERTEMPORALPROPERTIES_H + +#include "qgsmaplayertemporalproperties.h" + + +/** + * \class QgsMeshLayerTemporalProperties + * \ingroup core + * Implementation of map layer temporal properties for mesh layers. + * + * + * The time in a mesh layer is defined by : + * - a reference time provided by the data, the project or the user + * - each dataset is associated with a relative times + * - time extent is defined by the first time and the last time of all dataset + * + * \code{.unparsed} + * Reference time : AT + * Dataset 1 time o-----RT------RT-----RT-----------RT + * Dataset 2 time o---------RT------RT--------RT + * Dataset 3 time o------------------------------RT-------RT----------RT + * Time extent of layer o-----<---------------------------------------------> + * + * AT : absolute time (QDateTime) + * RT : relative time (qint64) + * \endcode + * + * \since QGIS 3.14 + */ + +class CORE_EXPORT QgsMeshLayerTemporalProperties : public QgsMapLayerTemporalProperties +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsMeshLayerTemporalProperties + * + * \param parent pointer to the parent object + * \param enabled argument specifies whether the temporal properties are initially enabled or not (see isActive()). + */ + QgsMeshLayerTemporalProperties( QObject *parent SIP_TRANSFERTHIS = nullptr, bool enabled = true ); + + public: + QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; + bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; + void setDefaultsFromDataProviderTemporalCapabilities( const QgsDataProviderTemporalCapabilities *capabilities ) override; + + /** + * Returns the time extent + */ + QgsDateTimeRange timeExtent() const; + + /** + * Returns the reference time + */ + QDateTime referenceTime() const; + + /** + * Sets the reference time and update the time extent from the temporal capabilities, + * if the temporal capabilities is null, set a void time extent (reference time to reference time) + * + * \param referenceTime the reference time + * \param capabilities the temporal capabilities of the data provider + */ + void setReferenceTime( const QDateTime &referenceTime, const QgsDataProviderTemporalCapabilities *capabilities ); + + private: + QDateTime mReferenceTime; + QgsDateTimeRange mTimeExtent; +}; + +#endif // QGSMESHLAYERTEMPORALPROPERTIES_H diff --git a/src/core/mesh/qgsmeshlayerutils.cpp b/src/core/mesh/qgsmeshlayerutils.cpp index 0d52db22f1bc..7c33a9ab1369 100644 --- a/src/core/mesh/qgsmeshlayerutils.cpp +++ b/src/core/mesh/qgsmeshlayerutils.cpp @@ -119,7 +119,7 @@ QVector QgsMeshLayerUtils::griddedVectorValues( const QgsMeshLayer *m return vectors; // extract vector dataset - bool vectorDataOnVertices = meta.DataOnVertices; + bool vectorDataOnVertices = meta.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices; int datacount = vectorDataOnVertices ? nativeMesh->vertices.count() : nativeMesh->faces.count(); const QgsMeshDataBlock vals = QgsMeshLayerUtils::datasetValues( meshLayer, index, 0, datacount ); @@ -305,11 +305,8 @@ QVector QgsMeshLayerUtils::interpolateFromFacesData( const QgsMesh *nativeMesh, const QgsTriangularMesh *triangularMesh, QgsMeshDataBlock *active, - QgsMeshRendererScalarSettings::DataInterpolationMethod method ) + QgsMeshRendererScalarSettings::DataResamplingMethod method ) { - Q_UNUSED( triangularMesh ) - Q_UNUSED( method ) - assert( nativeMesh ); assert( method == QgsMeshRendererScalarSettings::NeighbourAverage ); @@ -355,10 +352,42 @@ QVector QgsMeshLayerUtils::interpolateFromFacesData( return res; } +QVector QgsMeshLayerUtils::resampleFromVerticesToFaces( + const QVector valuesOnVertices, + const QgsMesh *nativeMesh, + const QgsTriangularMesh *triangularMesh, + const QgsMeshDataBlock *active, + QgsMeshRendererScalarSettings::DataResamplingMethod method ) +{ + assert( nativeMesh ); + assert( method == QgsMeshRendererScalarSettings::NeighbourAverage ); + + // assuming that native vertex count = triangular vertex count + assert( nativeMesh->vertices.size() == triangularMesh->vertices().size() ); + + QVector ret( nativeMesh->faceCount(), std::numeric_limits::quiet_NaN() ); + + for ( int i = 0; i < nativeMesh->faces.size(); ++i ) + { + const QgsMeshFace face = nativeMesh->face( i ); + if ( active->active( i ) && face.count() > 2 ) + { + double value = 0; + for ( int j = 0; j < face.count(); ++j ) + { + value += valuesOnVertices.at( face.at( j ) ); + } + ret[i] = value / face.count(); + } + } + + return ret; +} + QVector QgsMeshLayerUtils::calculateMagnitudeOnVertices( const QgsMeshLayer *meshLayer, const QgsMeshDatasetIndex index, QgsMeshDataBlock *activeFaceFlagValues, - const QgsMeshRendererScalarSettings::DataInterpolationMethod method ) + const QgsMeshRendererScalarSettings::DataResamplingMethod method ) { QVector ret; @@ -423,29 +452,14 @@ QgsRectangle QgsMeshLayerUtils::triangleBoundingBox( const QgsPointXY &p1, const return bbox; } -QString QgsMeshLayerUtils::formatTime( double hours, const QgsMeshTimeSettings &settings ) +QString QgsMeshLayerUtils::formatTime( double hours, const QDateTime &referenceTime, const QgsMeshTimeSettings &settings ) { QString ret; - switch ( settings.providerTimeUnit() ) - { - case QgsMeshTimeSettings::seconds: - hours = hours / 3600.0; - break; - case QgsMeshTimeSettings::minutes: - hours = hours / 60.0; - break; - case QgsMeshTimeSettings::hours: - break; - case QgsMeshTimeSettings::days: - hours = hours * 24.0; - break; - } - - if ( settings.useAbsoluteTime() ) + if ( referenceTime.isValid() ) { QString format( settings.absoluteTimeFormat() ); - QDateTime dateTime( settings.absoluteTimeReferenceTime() ); + QDateTime dateTime( referenceTime ); qint64 seconds = static_cast( hours * 3600.0 ); dateTime = dateTime.addSecs( seconds ); ret = dateTime.toString( format ); @@ -456,7 +470,6 @@ QString QgsMeshLayerUtils::formatTime( double hours, const QgsMeshTimeSettings & { QString format( settings.relativeTimeFormat() ); format = format.trimmed(); - hours = hours + settings.relativeTimeOffsetHours(); int totalHours = static_cast( hours ); if ( format == QStringLiteral( "hh:mm:ss.zzz" ) ) @@ -528,27 +541,6 @@ QString QgsMeshLayerUtils::formatTime( double hours, const QgsMeshTimeSettings & return ret; } -QDateTime QgsMeshLayerUtils::firstReferenceTime( QgsMeshLayer *meshLayer ) -{ - if ( !meshLayer ) - return QDateTime(); - - QgsMeshDataProvider *provider = meshLayer->dataProvider(); - - if ( !provider ) - return QDateTime(); - - // Searches for the first valid reference time in the dataset groups - for ( int i = 0; i < provider->datasetGroupCount(); ++i ) - { - QgsMeshDatasetGroupMetadata meta = provider->datasetGroupMetadata( i ); - if ( meta.referenceTime().isValid() ) - return meta.referenceTime(); - } - - return QDateTime(); -} - QVector QgsMeshLayerUtils::calculateNormals( const QgsTriangularMesh &triangularMesh, const QVector &verticalMagnitude, bool isRelative ) { QVector normals( triangularMesh.vertices().count() ); diff --git a/src/core/mesh/qgsmeshlayerutils.h b/src/core/mesh/qgsmeshlayerutils.h index 5ff2639c5bbc..9c88b4cfdfb4 100644 --- a/src/core/mesh/qgsmeshlayerutils.h +++ b/src/core/mesh/qgsmeshlayerutils.h @@ -209,7 +209,20 @@ class CORE_EXPORT QgsMeshLayerUtils const QgsMesh *nativeMesh, const QgsTriangularMesh *triangularMesh, QgsMeshDataBlock *active, - QgsMeshRendererScalarSettings::DataInterpolationMethod method + QgsMeshRendererScalarSettings::DataResamplingMethod method + ); + + /** + * Resamples values on vertices to values on faces + * + * \since QGIS 3.14 + */ + static QVector resampleFromVerticesToFaces( + const QVector valuesOnVertices, + const QgsMesh *nativeMesh, + const QgsTriangularMesh *triangularMesh, + const QgsMeshDataBlock *active, + QgsMeshRendererScalarSettings::DataResamplingMethod method ); /** @@ -226,7 +239,7 @@ class CORE_EXPORT QgsMeshLayerUtils const QgsMeshLayer *meshLayer, const QgsMeshDatasetIndex index, QgsMeshDataBlock *activeFaceFlagValues, - const QgsMeshRendererScalarSettings::DataInterpolationMethod method = QgsMeshRendererScalarSettings::NeighbourAverage ); + const QgsMeshRendererScalarSettings::DataResamplingMethod method = QgsMeshRendererScalarSettings::NeighbourAverage ); /** * Calculates the bounding box of the triangle @@ -238,17 +251,14 @@ class CORE_EXPORT QgsMeshLayerUtils static QgsRectangle triangleBoundingBox( const QgsPointXY &p1, const QgsPointXY &p2, const QgsPointXY &p3 ); /** - * Formats hours in human readable string based on settings + * Formats hours in human readable string based on settings and reference time + * If reference time is invalid, return relative time + * \param hours time in hours from reference time + * \param referenceTime the reference time + * \param settings the time settings + * \return the formatted time */ - static QString formatTime( double hours, const QgsMeshTimeSettings &settings ); - - /** - * Searches and returns the first valid reference time in layer's dataset group - * \param meshLayer mesh layer to parse - * - * \since QGIS 3.12 - */ - static QDateTime firstReferenceTime( QgsMeshLayer *meshLayer ); + static QString formatTime( double hours, const QDateTime &referenceTime, const QgsMeshTimeSettings &settings ); /** * Calculates the normals on the vertices using vertical magnitudes instead Z value of vertices @@ -262,8 +272,6 @@ class CORE_EXPORT QgsMeshLayerUtils const QgsTriangularMesh &triangularMesh, const QVector &verticalMagnitude, bool isRelative ); - - }; ///@endcond diff --git a/src/core/mesh/qgsmeshrenderersettings.cpp b/src/core/mesh/qgsmeshrenderersettings.cpp index 291b2f077961..1b7b88382415 100644 --- a/src/core/mesh/qgsmeshrenderersettings.cpp +++ b/src/core/mesh/qgsmeshrenderersettings.cpp @@ -102,14 +102,14 @@ double QgsMeshRendererScalarSettings::opacity() const { return mOpacity; } void QgsMeshRendererScalarSettings::setOpacity( double opacity ) { mOpacity = opacity; } -QgsMeshRendererScalarSettings::DataInterpolationMethod QgsMeshRendererScalarSettings::dataInterpolationMethod() const +QgsMeshRendererScalarSettings::DataResamplingMethod QgsMeshRendererScalarSettings::dataResamplingMethod() const { - return mDataInterpolationMethod; + return mDataResamplingMethod; } -void QgsMeshRendererScalarSettings::setDataInterpolationMethod( const QgsMeshRendererScalarSettings::DataInterpolationMethod &dataInterpolationMethod ) +void QgsMeshRendererScalarSettings::setDataResamplingMethod( const QgsMeshRendererScalarSettings::DataResamplingMethod &dataInterpolationMethod ) { - mDataInterpolationMethod = dataInterpolationMethod; + mDataResamplingMethod = dataInterpolationMethod; } QDomElement QgsMeshRendererScalarSettings::writeXml( QDomDocument &doc ) const @@ -122,7 +122,7 @@ QDomElement QgsMeshRendererScalarSettings::writeXml( QDomDocument &doc ) const elem.setAttribute( QStringLiteral( "edge-width-unit" ), QgsUnitTypes::encodeUnit( mEdgeWidthUnit ) ); QString methodTxt; - switch ( mDataInterpolationMethod ) + switch ( mDataResamplingMethod ) { case None: methodTxt = QStringLiteral( "none" ); @@ -148,11 +148,11 @@ void QgsMeshRendererScalarSettings::readXml( const QDomElement &elem ) QString methodTxt = elem.attribute( QStringLiteral( "interpolation-method" ) ); if ( QStringLiteral( "neighbour-average" ) == methodTxt ) { - mDataInterpolationMethod = DataInterpolationMethod::NeighbourAverage; + mDataResamplingMethod = DataResamplingMethod::NeighbourAverage; } else { - mDataInterpolationMethod = DataInterpolationMethod::None; + mDataResamplingMethod = DataResamplingMethod::None; } QDomElement elemShader = elem.firstChildElement( QStringLiteral( "colorrampshader" ) ); mColorRampShader.readXml( elemShader ); @@ -400,12 +400,10 @@ QDomElement QgsMeshRendererSettings::writeXml( QDomDocument &doc ) const { QDomElement elem = doc.createElement( QStringLiteral( "mesh-renderer-settings" ) ); - QDomElement elemActiveDataset = doc.createElement( QStringLiteral( "active-dataset" ) ); - if ( mActiveScalarDataset.isValid() ) - elemActiveDataset.setAttribute( QStringLiteral( "scalar" ), QStringLiteral( "%1,%2" ).arg( mActiveScalarDataset.group() ).arg( mActiveScalarDataset.dataset() ) ); - if ( mActiveVectorDataset.isValid() ) - elemActiveDataset.setAttribute( QStringLiteral( "vector" ), QStringLiteral( "%1,%2" ).arg( mActiveVectorDataset.group() ).arg( mActiveVectorDataset.dataset() ) ); - elem.appendChild( elemActiveDataset ); + QDomElement elemActiveDatasetGroup = doc.createElement( QStringLiteral( "active-dataset-group" ) ); + elemActiveDatasetGroup.setAttribute( QStringLiteral( "scalar" ), mActiveScalarDatasetGroup ); + elemActiveDatasetGroup.setAttribute( QStringLiteral( "vector" ), mActiveVectorDatasetGroup ); + elem.appendChild( elemActiveDatasetGroup ); for ( int groupIndex : mRendererScalarSettings.keys() ) { @@ -453,19 +451,12 @@ void QgsMeshRendererSettings::readXml( const QDomElement &elem ) mRendererVectorSettings.clear(); mAveragingMethod.reset(); - QDomElement elemActiveDataset = elem.firstChildElement( QStringLiteral( "active-dataset" ) ); + QDomElement elemActiveDataset = elem.firstChildElement( QStringLiteral( "active-dataset-group" ) ); if ( elemActiveDataset.hasAttribute( QStringLiteral( "scalar" ) ) ) - { - QStringList lst = elemActiveDataset.attribute( QStringLiteral( "scalar" ) ).split( QChar( ',' ) ); - if ( lst.count() == 2 ) - mActiveScalarDataset = QgsMeshDatasetIndex( lst[0].toInt(), lst[1].toInt() ); - } + mActiveScalarDatasetGroup = elemActiveDataset.attribute( QStringLiteral( "scalar" ) ).toInt(); + if ( elemActiveDataset.hasAttribute( QStringLiteral( "vector" ) ) ) - { - QStringList lst = elemActiveDataset.attribute( QStringLiteral( "vector" ) ).split( QChar( ',' ) ); - if ( lst.count() == 2 ) - mActiveVectorDataset = QgsMeshDatasetIndex( lst[0].toInt(), lst[1].toInt() ); - } + mActiveVectorDatasetGroup = elemActiveDataset.attribute( QStringLiteral( "vector" ) ).toInt(); QDomElement elemScalar = elem.firstChildElement( QStringLiteral( "scalar-settings" ) ); while ( !elemScalar.isNull() ) @@ -505,6 +496,26 @@ void QgsMeshRendererSettings::readXml( const QDomElement &elem ) } } +int QgsMeshRendererSettings::activeScalarDatasetGroup() const +{ + return mActiveScalarDatasetGroup; +} + +void QgsMeshRendererSettings::setActiveScalarDatasetGroup( int activeScalarDatasetGroup ) +{ + mActiveScalarDatasetGroup = activeScalarDatasetGroup; +} + +int QgsMeshRendererSettings::activeVectorDatasetGroup() const +{ + return mActiveVectorDatasetGroup; +} + +void QgsMeshRendererSettings::setActiveVectorDatasetGroup( int activeVectorDatasetGroup ) +{ + mActiveVectorDatasetGroup = activeVectorDatasetGroup; +} + QgsMeshRendererVectorStreamlineSettings::SeedingStartPointsMethod QgsMeshRendererVectorStreamlineSettings::seedingMethod() const { return mSeedingMethod; @@ -579,7 +590,10 @@ QDomElement QgsMeshRendererVectorSettings::writeXml( QDomDocument &doc ) const elem.setAttribute( QStringLiteral( "symbology" ), mDisplayingMethod ); elem.setAttribute( QStringLiteral( "line-width" ), mLineWidth ); + elem.setAttribute( QStringLiteral( "coloring-method" ), coloringMethod() ); elem.setAttribute( QStringLiteral( "color" ), QgsSymbolLayerUtils::encodeColor( mColor ) ); + QDomElement elemShader = mColorRampShader.writeXml( doc ); + elem.appendChild( elemShader ); elem.setAttribute( QStringLiteral( "filter-min" ), mFilterMin ); elem.setAttribute( QStringLiteral( "filter-max" ), mFilterMax ); @@ -600,7 +614,10 @@ void QgsMeshRendererVectorSettings::readXml( const QDomElement &elem ) elem.attribute( QStringLiteral( "symbology" ) ).toInt() ); mLineWidth = elem.attribute( QStringLiteral( "line-width" ) ).toDouble(); + mColoringMethod = static_cast( + elem.attribute( QStringLiteral( "coloring-method" ) ).toInt() ); mColor = QgsSymbolLayerUtils::decodeColor( elem.attribute( QStringLiteral( "color" ) ) ); + mColorRampShader.readXml( elem.firstChildElement( "colorrampshader" ) ); mFilterMin = elem.attribute( QStringLiteral( "filter-min" ) ).toDouble(); mFilterMax = elem.attribute( QStringLiteral( "filter-max" ) ).toDouble(); @@ -621,6 +638,26 @@ void QgsMeshRendererVectorSettings::readXml( const QDomElement &elem ) mTracesSettings.readXml( elemTraces ); } +QgsMeshRendererVectorSettings::ColoringMethod QgsMeshRendererVectorSettings::coloringMethod() const +{ + return mColoringMethod; +} + +void QgsMeshRendererVectorSettings::setColoringMethod( const QgsMeshRendererVectorSettings::ColoringMethod &coloringMethod ) +{ + mColoringMethod = coloringMethod; +} + +QgsColorRampShader QgsMeshRendererVectorSettings::colorRampShader() const +{ + return mColorRampShader; +} + +void QgsMeshRendererVectorSettings::setColorRampShader( const QgsColorRampShader &colorRampShader ) +{ + mColorRampShader = colorRampShader; +} + QgsMeshRendererVectorTracesSettings QgsMeshRendererVectorSettings::tracesSettings() const { return mTracesSettings; diff --git a/src/core/mesh/qgsmeshrenderersettings.h b/src/core/mesh/qgsmeshrenderersettings.h index 22cf7b262af0..a18cf80083e1 100644 --- a/src/core/mesh/qgsmeshrenderersettings.h +++ b/src/core/mesh/qgsmeshrenderersettings.h @@ -93,18 +93,24 @@ class CORE_EXPORT QgsMeshRendererMeshSettings class CORE_EXPORT QgsMeshRendererScalarSettings { public: - //! Interpolation of value defined on vertices from datasets with data defined on faces - enum DataInterpolationMethod + + /** + * Resampling of value from dataset + * + * - for vertices : does a resampling from values defined on surrounding faces + * - for faces : does a resampling from values defined on surrounding vertices + * - for edges : not supported. + */ + enum DataResamplingMethod { /** - * Use data defined on face centers, do not interpolate to vertices + * Does not use resampling */ None = 0, /** - * For each vertex does a simple average of values defined for all faces that contains - * given vertex + * Does a simple average of values defined for all surrounding faces/vertices */ NeighbourAverage, }; @@ -133,14 +139,14 @@ class CORE_EXPORT QgsMeshRendererScalarSettings * * \since QGIS 3.12 */ - DataInterpolationMethod dataInterpolationMethod() const; + DataResamplingMethod dataResamplingMethod() const; /** * Sets data interpolation method * * \since QGIS 3.12 */ - void setDataInterpolationMethod( const DataInterpolationMethod &dataInterpolationMethod ); + void setDataResamplingMethod( const DataResamplingMethod &dataResamplingMethod ); //! Writes configuration to a new DOM element QDomElement writeXml( QDomDocument &doc ) const; @@ -177,7 +183,7 @@ class CORE_EXPORT QgsMeshRendererScalarSettings private: QgsColorRampShader mColorRampShader; - DataInterpolationMethod mDataInterpolationMethod = DataInterpolationMethod::None; + DataResamplingMethod mDataResamplingMethod = DataResamplingMethod::None; double mClassificationMinimum = 0; double mClassificationMaximum = 0; double mOpacity = 1; @@ -329,7 +335,7 @@ class CORE_EXPORT QgsMeshRendererVectorStreamlineSettings /** * Seeds start points randomly on the mesh */ - Random, + Random }; //! Returns the method used for seeding start points of strealines @@ -418,6 +424,18 @@ class CORE_EXPORT QgsMeshRendererVectorSettings Traces }; + /** + * Defines the how the color of vector is defined + * \since QGIS 3.14 + */ + enum ColoringMethod + { + //! Render the vector with a single color + SingleColor = 0, + //! Render the vector with a color ramp + ColorRamp + }; + //! Returns line width of the arrow (in millimeters) double lineWidth() const; @@ -482,6 +500,30 @@ class CORE_EXPORT QgsMeshRendererVectorSettings */ void setSymbology( const Symbology &symbology ); + /** + * Returns the coloring method used to render vector datasets + * \since QGIS 3.14 + */ + ColoringMethod coloringMethod() const; + + /** + * Sets the coloring method used to render vector datasets + * \since QGIS 3.14 + */ + void setColoringMethod( const ColoringMethod &coloringMethod ); + + /** + * Sets the color ramp shader used to render vector datasets + * \since QGIS 3.14 + */ + QgsColorRampShader colorRampShader() const; + + /** + * Returns the color ramp shader used to render vector datasets + * \since QGIS 3.14 + */ + void setColorRampShader( const QgsColorRampShader &colorRampShader ); + /** * Returns settings for vector rendered with arrows * \since QGIS 3.12 @@ -523,13 +565,14 @@ class CORE_EXPORT QgsMeshRendererVectorSettings //! Reads configuration from the given DOM element void readXml( const QDomElement &elem ); - private: Symbology mDisplayingMethod = Arrows; double mLineWidth = DEFAULT_LINE_WIDTH; //in millimeters + QgsColorRampShader mColorRampShader; QColor mColor = Qt::black; + ColoringMethod mColoringMethod = SingleColor; double mFilterMin = -1; //disabled double mFilterMax = -1; //disabled int mUserGridCellWidth = 10; // in pixels @@ -607,21 +650,35 @@ class CORE_EXPORT QgsMeshRendererSettings */ void setAveragingMethod( QgsMesh3dAveragingMethod *method ); - //! Returns active scalar dataset - QgsMeshDatasetIndex activeScalarDataset() const { return mActiveScalarDataset; } - //! Sets active scalar dataset for rendering - void setActiveScalarDataset( QgsMeshDatasetIndex index = QgsMeshDatasetIndex() ) { mActiveScalarDataset = index; } - - //! Returns active vector dataset - QgsMeshDatasetIndex activeVectorDataset() const { return mActiveVectorDataset; } - //! Sets active vector dataset for rendering. - void setActiveVectorDataset( QgsMeshDatasetIndex index = QgsMeshDatasetIndex() ) { mActiveVectorDataset = index; } - //! Writes configuration to a new DOM element QDomElement writeXml( QDomDocument &doc ) const; //! Reads configuration from the given DOM element void readXml( const QDomElement &elem ); + /** + * Returns the active scalar dataset group + * \since QGIS 3.14 + */ + int activeScalarDatasetGroup() const; + + /** + * Sets the active scalar dataset group + * \since QGIS 3.14 + */ + void setActiveScalarDatasetGroup( int activeScalarDatasetGroup ); + + /** + * Returns the active vector dataset group + * \since QGIS 3.14 + */ + int activeVectorDatasetGroup() const; + + /** + * Sets the active vector dataset group + * \since QGIS 3.14 + */ + void setActiveVectorDatasetGroup( int activeVectorDatasetGroup ); + private: QgsMeshRendererMeshSettings mRendererNativeMeshSettings; QgsMeshRendererMeshSettings mRendererTriangularMeshSettings; @@ -630,11 +687,11 @@ class CORE_EXPORT QgsMeshRendererSettings QHash mRendererScalarSettings; //!< Per-group scalar settings QHash mRendererVectorSettings; //!< Per-group vector settings - //! index of active scalar dataset - QgsMeshDatasetIndex mActiveScalarDataset; + //! index of active scalar dataset group + int mActiveScalarDatasetGroup = -1; - //! index of active vector dataset - QgsMeshDatasetIndex mActiveVectorDataset; + //! index of active vector dataset group + int mActiveVectorDatasetGroup = -1; //! Averaging method to get 2D datasets from 3D stacked mesh datasets std::shared_ptr mAveragingMethod; diff --git a/src/core/mesh/qgsmeshtimesettings.cpp b/src/core/mesh/qgsmeshtimesettings.cpp index f3f825180150..312db3101128 100644 --- a/src/core/mesh/qgsmeshtimesettings.cpp +++ b/src/core/mesh/qgsmeshtimesettings.cpp @@ -19,71 +19,21 @@ QgsMeshTimeSettings::QgsMeshTimeSettings() = default; -QgsMeshTimeSettings::QgsMeshTimeSettings( double relativeTimeOffsetHours, const QString &relativeTimeFormat ) - : mUseAbsoluteTime( false ) - , mRelativeTimeOffsetHours( relativeTimeOffsetHours ) - , mRelativeTimeFormat( relativeTimeFormat ) -{} - -QgsMeshTimeSettings::QgsMeshTimeSettings( const QDateTime &absoluteTimeReferenceTime, const QString &absoluteTimeFormat ) - : mUseAbsoluteTime( true ) - , mAbsoluteTimeReferenceTime( absoluteTimeReferenceTime ) - , mAbsoluteTimeFormat( absoluteTimeFormat ) -{} QDomElement QgsMeshTimeSettings::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const { Q_UNUSED( context ) QDomElement elem = doc.createElement( QStringLiteral( "mesh-time-settings" ) ); - elem.setAttribute( QStringLiteral( "use-absolute-time" ), mUseAbsoluteTime ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); - elem.setAttribute( QStringLiteral( "relative-time-offset-hours" ), mRelativeTimeOffsetHours ); elem.setAttribute( QStringLiteral( "relative-time-format" ), mRelativeTimeFormat ); - elem.setAttribute( QStringLiteral( "absolute-time-reference-time" ), mAbsoluteTimeReferenceTime.toString() ); elem.setAttribute( QStringLiteral( "absolute-time-format" ), mAbsoluteTimeFormat ); - elem.setAttribute( QStringLiteral( "provider-time-unit" ), mProviderTimeUnit ); return elem; } void QgsMeshTimeSettings::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) { Q_UNUSED( context ) - mUseAbsoluteTime = elem.attribute( QStringLiteral( "use-absolute-time" ) ).toInt(); //bool - mRelativeTimeOffsetHours = elem.attribute( QStringLiteral( "relative-time-offset-hours" ) ).toDouble(); mRelativeTimeFormat = elem.attribute( QStringLiteral( "relative-time-format" ) ); - mAbsoluteTimeReferenceTime = QDateTime::fromString( elem.attribute( QStringLiteral( "absolute-time-reference-time" ) ) ); mAbsoluteTimeFormat = elem.attribute( QStringLiteral( "absolute-time-format" ) ); - mProviderTimeUnit = static_cast( elem.attribute( QStringLiteral( "provider-time-unit" ) ).toInt() ); -} - -bool QgsMeshTimeSettings::useAbsoluteTime() const -{ - return mUseAbsoluteTime; -} - -void QgsMeshTimeSettings::setUseAbsoluteTime( bool useAbsoluteTime ) -{ - mUseAbsoluteTime = useAbsoluteTime; -} - - -double QgsMeshTimeSettings::relativeTimeOffsetHours() const -{ - return mRelativeTimeOffsetHours; -} - -void QgsMeshTimeSettings::setRelativeTimeOffsetHours( double relativeTimeOffsetHours ) -{ - mRelativeTimeOffsetHours = relativeTimeOffsetHours; -} - -double QgsMeshTimeSettings::datasetPlaybackInterval() const -{ - return mDatasetPlaybackIntervalSec; -} - -void QgsMeshTimeSettings::setDatasetPlaybackInterval( double seconds ) -{ - mDatasetPlaybackIntervalSec = seconds; } QString QgsMeshTimeSettings::relativeTimeFormat() const @@ -96,16 +46,6 @@ void QgsMeshTimeSettings::setRelativeTimeFormat( const QString &relativeTimeForm mRelativeTimeFormat = relativeTimeFormat; } -QDateTime QgsMeshTimeSettings::absoluteTimeReferenceTime() const -{ - return mAbsoluteTimeReferenceTime; -} - -void QgsMeshTimeSettings::setAbsoluteTimeReferenceTime( const QDateTime &absoluteTimeReferenceTime ) -{ - mAbsoluteTimeReferenceTime = absoluteTimeReferenceTime; -} - QString QgsMeshTimeSettings::absoluteTimeFormat() const { return mAbsoluteTimeFormat; @@ -115,13 +55,3 @@ void QgsMeshTimeSettings::setAbsoluteTimeFormat( const QString &absoluteTimeForm { mAbsoluteTimeFormat = absoluteTimeFormat; } - -QgsMeshTimeSettings::TimeUnit QgsMeshTimeSettings::providerTimeUnit() const -{ - return mProviderTimeUnit; -} - -void QgsMeshTimeSettings::setProviderTimeUnit( const QgsMeshTimeSettings::TimeUnit &providerTimeUnit ) -{ - mProviderTimeUnit = providerTimeUnit; -} diff --git a/src/core/mesh/qgsmeshtimesettings.h b/src/core/mesh/qgsmeshtimesettings.h index db95b3081dbb..b74bcfe2fc09 100644 --- a/src/core/mesh/qgsmeshtimesettings.h +++ b/src/core/mesh/qgsmeshtimesettings.h @@ -53,76 +53,26 @@ class CORE_EXPORT QgsMeshTimeSettings }; QgsMeshTimeSettings(); - //! Constructs relative time format settings with defined offset in hours - QgsMeshTimeSettings( double relativeTimeOffsetHours, const QString &relativeTimeFormat ); - //! Constructs absolute time format settings with defined reference time - QgsMeshTimeSettings( const QDateTime &absoluteTimeReferenceTime, const QString &absoluteTimeFormat ); //! Writes configuration to a new DOM element QDomElement writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const; //! Reads configuration from the given DOM element void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); - //! Returns whether to use absolute time format - bool useAbsoluteTime() const; - //! Sets use absolute time flag - void setUseAbsoluteTime( bool useAbsoluteTime ); - - //! Returns number of offset hours for relative time formatting - double relativeTimeOffsetHours() const; - //! Sets number of offset hours for relative time formatting - void setRelativeTimeOffsetHours( double relativeTimeOffsetHours ); - - /** - * Returns number of seconds used as interval for dataset playback - * \since QGIS 3.12 - */ - double datasetPlaybackInterval() const; SIP_SKIP - - /** - * Sets number of seconds used as interval for dataset playback - * \since QGIS 3.12 - */ - void setDatasetPlaybackInterval( double seconds ); SIP_SKIP - //! Returns format used for relative time QString relativeTimeFormat() const; //! Sets format used for relative time void setRelativeTimeFormat( const QString &relativeTimeFormat ); - //! Returns reference time used for absolute time format - QDateTime absoluteTimeReferenceTime() const; - //! Sets reference time used for absolute time format - void setAbsoluteTimeReferenceTime( const QDateTime &absoluteTimeReferenceTime ); - //! Returns format used for absolute time QString absoluteTimeFormat() const; //! Sets format used for absolute time void setAbsoluteTimeFormat( const QString &absoluteTimeFormat ); - /** - * Returns the provider time unit - * \since QGIS 3.12 - */ - TimeUnit providerTimeUnit() const; - - /** - * Sets the provider time unit - * \since QGIS 3.12 - */ - void setProviderTimeUnit( const TimeUnit &providerTimeUnit ); - private: - bool mUseAbsoluteTime = false; - double mRelativeTimeOffsetHours = 0; - double mDatasetPlaybackIntervalSec = 3; QString mRelativeTimeFormat = QStringLiteral( "d hh:mm:ss" ); - - QDateTime mAbsoluteTimeReferenceTime; QString mAbsoluteTimeFormat = QStringLiteral( "dd.MM.yyyy hh:mm:ss" ); - - TimeUnit mProviderTimeUnit = TimeUnit::hours; }; Q_DECLARE_METATYPE( QgsMeshTimeSettings ); diff --git a/src/core/mesh/qgsmeshtracerenderer.cpp b/src/core/mesh/qgsmeshtracerenderer.cpp index 6a6cd080acca..1852aa2f8486 100644 --- a/src/core/mesh/qgsmeshtracerenderer.cpp +++ b/src/core/mesh/qgsmeshtracerenderer.cpp @@ -189,8 +189,17 @@ QgsPointXY QgsMeshStreamField::positionToMapCoordinates( const QPoint &pixelPosi return mapPoint; } -QgsMeshStreamField::QgsMeshStreamField( const QgsTriangularMesh &triangularMesh, const QgsMeshDataBlock &dataSetVectorValues, const QgsMeshDataBlock &scalarActiveFaceFlagValues, const QgsRectangle &layerExtent, double magnitudeMaximum, bool dataIsOnVertices, const QgsRenderContext &rendererContext, int resolution ): +QgsMeshStreamField::QgsMeshStreamField( + const QgsTriangularMesh &triangularMesh, + const QgsMeshDataBlock &dataSetVectorValues, + const QgsMeshDataBlock &scalarActiveFaceFlagValues, + const QgsRectangle &layerExtent, + double magnitudeMaximum, bool dataIsOnVertices, + const QgsRenderContext &rendererContext, + const QgsMeshVectorColoring &vectorColoring, + int resolution ): mFieldResolution( resolution ), + mVectorColoring( vectorColoring ), mLayerExtent( layerExtent ), mMaximumMagnitude( magnitudeMaximum ), mRenderContext( rendererContext ) @@ -223,6 +232,7 @@ QgsMeshStreamField::QgsMeshStreamField( const QgsMeshStreamField &other ): mPen( other.mPen ), mTraceImage( other.mTraceImage ), mMapToFieldPixel( other.mMapToFieldPixel ), + mVectorColoring( other.mVectorColoring ), mPixelFillingCount( other.mPixelFillingCount ), mMaxPixelFillingCount( other.mMaxPixelFillingCount ), mLayerExtent( other.mLayerExtent ), @@ -636,8 +646,16 @@ QgsMeshStreamlinesField::QgsMeshStreamlinesField( const QgsTriangularMesh &trian const QgsRectangle &layerExtent, double magMax, bool dataIsOnVertices, - QgsRenderContext &rendererContext ): - QgsMeshStreamField( triangularMesh, datasetVectorValues, scalarActiveFaceFlagValues, layerExtent, magMax, dataIsOnVertices, rendererContext ) + QgsRenderContext &rendererContext, + const QgsMeshVectorColoring vectorColoring ): + QgsMeshStreamField( triangularMesh, + datasetVectorValues, + scalarActiveFaceFlagValues, + layerExtent, + magMax, + dataIsOnVertices, + rendererContext, + vectorColoring ) {} QgsMeshStreamlinesField::QgsMeshStreamlinesField( const QgsMeshStreamlinesField &other ): @@ -680,8 +698,16 @@ void QgsMeshStreamlinesField::drawChunkTrace( const std::listpen(); + pen.setColor( mVectorColoring.color( ( mag1 + mag2 ) / 2 ) ); + mPainter->setPen( pen ); mPainter->drawLine( fieldToDevice( ( *p1 ).first ), fieldToDevice( ( *p2 ).first ) ); + } + p1++; p2++; } @@ -858,14 +884,24 @@ QgsVector QgsMeshVectorValueInterpolatorFromFace::interpolatedValuePrivate( int point ); } -QgsMeshVectorStreamlineRenderer::QgsMeshVectorStreamlineRenderer( const QgsTriangularMesh &triangularMesh, const QgsMeshDataBlock &dataSetVectorValues, const QgsMeshDataBlock &scalarActiveFaceFlagValues, bool dataIsOnVertices, const QgsMeshRendererVectorSettings &settings, QgsRenderContext &rendererContext, const QgsRectangle &layerExtent, double magMax ): +QgsMeshVectorStreamlineRenderer::QgsMeshVectorStreamlineRenderer( + const QgsTriangularMesh &triangularMesh, + const QgsMeshDataBlock &dataSetVectorValues, + const QgsMeshDataBlock &scalarActiveFaceFlagValues, + bool dataIsOnVertices, + const QgsMeshRendererVectorSettings &settings, + QgsRenderContext &rendererContext, + const QgsRectangle &layerExtent, double magMax ): mRendererContext( rendererContext ) { mStreamlineField.reset( new QgsMeshStreamlinesField( triangularMesh, dataSetVectorValues, scalarActiveFaceFlagValues, layerExtent, - magMax, dataIsOnVertices, rendererContext ) ); + magMax, + dataIsOnVertices, + rendererContext, + QgsMeshVectorColoring( settings ) ) ); mStreamlineField->updateSize( rendererContext ); mStreamlineField->setPixelFillingDensity( settings.streamLinesSettings().seedingDensity() ); @@ -902,8 +938,16 @@ QgsMeshParticleTracesField::QgsMeshParticleTracesField( const QgsTriangularMesh const QgsRectangle &layerExtent, double magMax, bool dataIsOnVertices, - const QgsRenderContext &rendererContext ): - QgsMeshStreamField( triangularMesh, datasetVectorValues, scalarActiveFaceFlagValues, layerExtent, magMax, dataIsOnVertices, rendererContext ) + const QgsRenderContext &rendererContext, + const QgsMeshVectorColoring vectorColoring ): + QgsMeshStreamField( triangularMesh, + datasetVectorValues, + scalarActiveFaceFlagValues, + layerExtent, + magMax, + dataIsOnVertices, + rendererContext, + vectorColoring ) { std::srand( uint( ::time( nullptr ) ) ); mPen.setCapStyle( Qt::RoundCap ); @@ -1036,6 +1080,7 @@ void QgsMeshParticleTracesField::storeInField( const QPair( d ); + mMagnitudeField[j * mFieldSize.width() + i] = pixelData.second.magnitude; } } @@ -1043,6 +1088,7 @@ void QgsMeshParticleTracesField::initField() { mTimeField = QVector( mFieldSize.width() * mFieldSize.height(), -1 ); mDirectionField = QVector( mFieldSize.width() * mFieldSize.height(), static_cast( int( 0 ) ) ); + mMagnitudeField = QVector( mFieldSize.width() * mFieldSize.height(), 0 ); initImage(); mStumpImage = QImage( mFieldSize * mFieldResolution, QImage::Format_ARGB32 ); mStumpImage.fill( QColor( 0, 0, 0, mStumpFactor ) ); //alpha=0 -> no persitence, alpha=255 -> total persistence @@ -1065,6 +1111,11 @@ void QgsMeshParticleTracesField::setStumpParticleWithLifeTime( bool stumpParticl mStumpParticleWithLifeTime = stumpParticleWithLifeTime; } +void QgsMeshParticleTracesField::setParticlesColor( const QColor &c ) +{ + mVectorColoring.setColor( c ); +} + void QgsMeshParticleTracesField::setMinTailLength( int minTailLength ) { mMinTailLength = minTailLength; @@ -1098,11 +1149,6 @@ void QgsMeshParticleTracesField::setParticleSize( double particleSize ) mParticleSize = particleSize; } -void QgsMeshParticleTracesField::setParticleColor( const QColor &particleColor ) -{ - mParticleColor = particleColor; -} - void QgsMeshParticleTracesField::setTimeStep( double timeStep ) { mTimeStep = timeStep; @@ -1157,6 +1203,17 @@ float QgsMeshParticleTracesField::time( QPoint position ) const return -1; } +float QgsMeshParticleTracesField::magnitude( QPoint position ) const +{ + int i = position.x(); + int j = position.y(); + if ( i >= 0 && i < mFieldSize.width() && j >= 0 && j < mFieldSize.height() ) + { + return mMagnitudeField[j * mFieldSize.width() + i]; + } + return -1; +} + void QgsMeshParticleTracesField::drawParticleTrace( const QgsMeshTraceParticle &particle ) { const std::list &tail = particle.tail; @@ -1167,15 +1224,9 @@ void QgsMeshParticleTracesField::drawParticleTrace( const QgsMeshTraceParticle & size_t pixelCount = tail.size(); + double transparency = 1; if ( mStumpParticleWithLifeTime ) - { - QColor traceColor = mParticleColor; - double transparency = sin( M_PI * particle.lifeTime / mParticlesLifeTime ); - traceColor.setAlphaF( transparency ); - mPen.setColor( traceColor ); - } - else - mPen.setColor( mParticleColor ); + transparency = sin( M_PI * particle.lifeTime / mParticlesLifeTime ); double dw; if ( pixelCount > 1 ) @@ -1190,6 +1241,9 @@ void QgsMeshParticleTracesField::drawParticleTrace( const QgsMeshTraceParticle & { QPointF p1 = fieldToDevice( ( *ip1 ) ); QPointF p2 = fieldToDevice( ( *ip2 ) ); + QColor traceColor = mVectorColoring.color( magnitude( *ip1 ) ); + traceColor.setAlphaF( traceColor.alphaF()*transparency ); + mPen.setColor( traceColor ); mPen.setWidthF( iniWidth - i * dw ); mPainter->setPen( mPen ); mPainter->drawLine( p1, p2 ); @@ -1204,13 +1258,14 @@ void QgsMeshParticleTracesField::setParticlesCount( int particlesCount ) mParticlesCount = particlesCount; } -QgsMeshVectorTraceAnimationGenerator::QgsMeshVectorTraceAnimationGenerator( - const QgsTriangularMesh &triangularMesh, - const QgsMeshDataBlock &dataSetVectorValues, - const QgsMeshDataBlock &scalarActiveFaceFlagValues, - bool dataIsOnVertices, - const QgsRenderContext &rendererContext, - const QgsRectangle &layerExtent, double magMax ): +QgsMeshVectorTraceAnimationGenerator::QgsMeshVectorTraceAnimationGenerator( const QgsTriangularMesh &triangularMesh, + const QgsMeshDataBlock &dataSetVectorValues, + const QgsMeshDataBlock &scalarActiveFaceFlagValues, + bool dataIsOnVertices, + const QgsRenderContext &rendererContext, + const QgsRectangle &layerExtent, + double magMax, + const QgsMeshRendererVectorSettings &vectorSettings ): mRendererContext( rendererContext ) { mParticleField = std::unique_ptr( new QgsMeshParticleTracesField( triangularMesh, @@ -1219,7 +1274,8 @@ QgsMeshVectorTraceAnimationGenerator::QgsMeshVectorTraceAnimationGenerator( layerExtent, magMax, dataIsOnVertices, - rendererContext ) ) ; + rendererContext, + QgsMeshVectorColoring( vectorSettings ) ) ) ; mParticleField->updateSize( rendererContext ) ; } @@ -1234,9 +1290,11 @@ QgsMeshVectorTraceAnimationGenerator::QgsMeshVectorTraceAnimationGenerator( QgsM bool vectorDataOnVertices; double magMax; + QgsMeshDatasetIndex datasetIndex = layer->activeVectorDatasetAtTime( rendererContext.temporalRange() ); + // Find out if we can use cache up to date. If yes, use it and return - const int datasetGroupCount = layer->dataProvider()->datasetGroupCount(); - QgsMeshDatasetIndex datasetIndex = layer->rendererSettings().activeVectorDataset(); + int datasetGroupCount = layer->dataProvider()->datasetGroupCount(); + const QgsMeshRendererVectorSettings vectorSettings = layer->rendererSettings().vectorSettings( datasetIndex.group() ); QgsMeshLayerRendererCache *cache = layer->rendererCache(); if ( ( cache->mDatasetGroupsCount == datasetGroupCount ) && @@ -1250,7 +1308,7 @@ QgsMeshVectorTraceAnimationGenerator::QgsMeshVectorTraceAnimationGenerator( QgsM else { const QgsMeshDatasetGroupMetadata metadata = - layer->dataProvider()->datasetGroupMetadata( layer->rendererSettings().activeVectorDataset() ); + layer->dataProvider()->datasetGroupMetadata( datasetIndex.group() ); magMax = metadata.maximum(); vectorDataOnVertices = metadata.dataType() == QgsMeshDatasetGroupMetadata::DataOnVertices; @@ -1277,7 +1335,8 @@ QgsMeshVectorTraceAnimationGenerator::QgsMeshVectorTraceAnimationGenerator( QgsM layer->extent(), magMax, vectorDataOnVertices, - rendererContext ) ) ; + rendererContext, + QgsMeshVectorColoring( vectorSettings ) ) ) ; mParticleField->setMinimizeFieldSize( false ); mParticleField->updateSize( mRendererContext ); @@ -1330,7 +1389,7 @@ void QgsMeshVectorTraceAnimationGenerator::setParticlesLifeTime( double particle void QgsMeshVectorTraceAnimationGenerator::setParticlesColor( const QColor &c ) { - mParticleField->setParticleColor( c ); + mParticleField->setParticlesColor( c ); } void QgsMeshVectorTraceAnimationGenerator::setParticlesSize( double width ) @@ -1376,7 +1435,15 @@ void QgsMeshVectorTraceAnimationGenerator::updateFieldParameter() mParticleField->setParticlesLifeTime( fieldLifeTime ); } -QgsMeshVectorTraceRenderer::QgsMeshVectorTraceRenderer( const QgsTriangularMesh &triangularMesh, const QgsMeshDataBlock &dataSetVectorValues, const QgsMeshDataBlock &scalarActiveFaceFlagValues, bool dataIsOnVertices, const QgsMeshRendererVectorSettings &settings, QgsRenderContext &rendererContext, const QgsRectangle &layerExtent, double magMax ): +QgsMeshVectorTraceRenderer::QgsMeshVectorTraceRenderer( + const QgsTriangularMesh &triangularMesh, + const QgsMeshDataBlock &dataSetVectorValues, + const QgsMeshDataBlock &scalarActiveFaceFlagValues, + bool dataIsOnVertices, + const QgsMeshRendererVectorSettings &settings, + QgsRenderContext &rendererContext, + const QgsRectangle &layerExtent, + double magMax ): mRendererContext( rendererContext ) { mParticleField = std::unique_ptr( new QgsMeshParticleTracesField( triangularMesh, @@ -1385,10 +1452,10 @@ QgsMeshVectorTraceRenderer::QgsMeshVectorTraceRenderer( const QgsTriangularMesh layerExtent, magMax, dataIsOnVertices, - rendererContext ) ) ; + rendererContext, + QgsMeshVectorColoring( settings ) ) ) ; mParticleField->updateSize( rendererContext ) ; - mParticleField->setParticleColor( settings.color() ); mParticleField->setParticleSize( rendererContext.convertToPainterUnits( settings.lineWidth(), QgsUnitTypes::RenderUnit::RenderMillimeters ) ); mParticleField->setParticlesCount( settings.tracesSettings().particlesCount() ); diff --git a/src/core/mesh/qgsmeshtracerenderer.h b/src/core/mesh/qgsmeshtracerenderer.h index 07581ca9c14c..11fe3dea9a64 100644 --- a/src/core/mesh/qgsmeshtracerenderer.h +++ b/src/core/mesh/qgsmeshtracerenderer.h @@ -179,6 +179,7 @@ class QgsMeshStreamField double magnitudeMaximum, bool dataIsOnVertices, const QgsRenderContext &rendererContext, + const QgsMeshVectorColoring &vectorColoring, int resolution = 1 ); //! Copy constructor @@ -287,6 +288,7 @@ class QgsMeshStreamField QImage mTraceImage; QgsMapToPixel mMapToFieldPixel; + QgsMeshVectorColoring mVectorColoring; private: int mPixelFillingCount = 0; @@ -320,7 +322,10 @@ class QgsMeshStreamlinesField: public QgsMeshStreamField const QgsMeshDataBlock &datasetVectorValues, const QgsMeshDataBlock &scalarActiveFaceFlagValues, const QgsRectangle &layerExtent, - double magMax, bool dataIsOnVertices, QgsRenderContext &rendererContext ); + double magMax, + bool dataIsOnVertices, + QgsRenderContext &rendererContext, + const QgsMeshVectorColoring vectorColoring ); //! Copy constructor QgsMeshStreamlinesField( const QgsMeshStreamlinesField &other ); @@ -374,7 +379,8 @@ class QgsMeshParticleTracesField: public QgsMeshStreamField const QgsRectangle &layerExtent, double magMax, bool dataIsOnVertices, - const QgsRenderContext &rendererContext ); + const QgsRenderContext &rendererContext, + const QgsMeshVectorColoring vectorColoring ); //! Copy constructor QgsMeshParticleTracesField( const QgsMeshParticleTracesField &other ); @@ -413,9 +419,6 @@ class QgsMeshParticleTracesField: public QgsMeshStreamField //! Sets the time step void setTimeStep( double timeStep ); - //! Sets tihe color of the particles - void setParticleColor( const QColor &particleColor ); - //! Sets particles size (in px) void setParticleSize( double particleSize ); @@ -431,10 +434,13 @@ class QgsMeshParticleTracesField: public QgsMeshStreamField //! Sets if the particle has to be stumped dependiong on liketime void setStumpParticleWithLifeTime( bool stumpParticleWithLifeTime ); + //! Sets the color of the particles, overwrite the color provided by vector settings + void setParticlesColor( const QColor &c ); private: QPoint direction( QPoint position ) const; float time( QPoint position ) const; + float magnitude( QPoint position ) const; void drawParticleTrace( const QgsMeshTraceParticle &particle ); @@ -451,6 +457,7 @@ class QgsMeshParticleTracesField: public QgsMeshStreamField * */ QVector mTimeField; + QVector mMagnitudeField; /*the direction for a pixel is defined with a char value * @@ -563,7 +570,8 @@ class CORE_EXPORT QgsMeshVectorTraceAnimationGenerator bool dataIsOnVertices, const QgsRenderContext &rendererContext, const QgsRectangle &layerExtent, - double magMax ) SIP_SKIP; + double magMax, + const QgsMeshRendererVectorSettings &vectorSettings ) SIP_SKIP; //!Constructor to use with Python binding QgsMeshVectorTraceAnimationGenerator( QgsMeshLayer *layer, const QgsRenderContext &rendererContext ); diff --git a/src/core/mesh/qgsmeshvectorrenderer.cpp b/src/core/mesh/qgsmeshvectorrenderer.cpp index 514068af0478..819345f10aa1 100644 --- a/src/core/mesh/qgsmeshvectorrenderer.cpp +++ b/src/core/mesh/qgsmeshvectorrenderer.cpp @@ -81,6 +81,8 @@ QgsMeshVectorArrowRenderer::QgsMeshVectorArrowRenderer( mBufferedExtent.setXMaximum( mBufferedExtent.xMaximum() + extension ); mBufferedExtent.setYMinimum( mBufferedExtent.yMinimum() - extension ); mBufferedExtent.setYMaximum( mBufferedExtent.yMaximum() + extension ); + + mVectorColoring.reset( new QgsMeshVectorColoring( settings ) ); } QgsMeshVectorArrowRenderer::~QgsMeshVectorArrowRenderer() = default; @@ -100,7 +102,6 @@ void QgsMeshVectorArrowRenderer::draw() double penWidth = mContext.convertToPainterUnits( mCfg.lineWidth(), QgsUnitTypes::RenderUnit::RenderMillimeters ); pen.setWidthF( penWidth ); - pen.setColor( mCfg.color() ); painter->setPen( pen ); if ( mCfg.isOnUserDefinedGrid() ) @@ -453,6 +454,9 @@ void QgsMeshVectorArrowRenderer::drawVectorArrow( const QgsPointXY &lineStart, d } // Now actually draw the vector + QPen pen( mContext.painter()->pen() ); + pen.setColor( mVectorColoring->color( magnitude ) ); + mContext.painter()->setPen( pen ); mContext.painter()->drawLine( lineStart.toQPointF(), lineEnd.toQPointF() ); mContext.painter()->drawPolygon( finalVectorHeadPoints ); } @@ -514,4 +518,47 @@ QgsMeshVectorRenderer *QgsMeshVectorRenderer::makeVectorRenderer( return renderer; } +QgsMeshVectorColoring::QgsMeshVectorColoring( const QgsMeshRendererVectorSettings &settings ) +{ + switch ( settings.coloringMethod() ) + { + case QgsMeshRendererVectorSettings::SingleColor: + setColor( settings.color() ); + break; + case QgsMeshRendererVectorSettings::ColorRamp: + setColor( settings.colorRampShader() ); + break; + } +} + +void QgsMeshVectorColoring::setColor( const QgsColorRampShader &colorRampShader ) +{ + mColorRampShader = colorRampShader; +} + +void QgsMeshVectorColoring::setColor( const QColor &color ) +{ + mColorRampShader = QgsColorRampShader(); + mSingleColor = color; +} + +QColor QgsMeshVectorColoring::color( double magnitude ) const +{ + if ( mColorRampShader.sourceColorRamp() ) + { + if ( mColorRampShader.isEmpty() ) + return mColorRampShader.sourceColorRamp()->color( 0 ); + + int r, g, b, a; + if ( mColorRampShader.shade( magnitude, &r, &g, &b, &a ) ) + return QColor( r, g, b, a ); + else + return QColor( 0, 0, 0, 0 ); + } + else + { + return mSingleColor; + } +} + ///@endcond diff --git a/src/core/mesh/qgsmeshvectorrenderer.h b/src/core/mesh/qgsmeshvectorrenderer.h index 88e594dfe2f1..a3b42e4a3b38 100644 --- a/src/core/mesh/qgsmeshvectorrenderer.h +++ b/src/core/mesh/qgsmeshvectorrenderer.h @@ -1,7 +1,7 @@ /*************************************************************************** - qgstriangularmesh.h + qgsmeshvectorrenderer.h ------------------- - begin : April 2018 + begin : May 2018 copyright : (C) 2018 by Peter Petrik email : zilolv at gmail dot com ***************************************************************************/ @@ -31,7 +31,7 @@ #include "qgspointxy.h" class QgsRenderContext; - +class QgsMeshVectorColoring; ///@cond PRIVATE @@ -133,6 +133,43 @@ class QgsMeshVectorArrowRenderer : public QgsMeshVectorRenderer QgsMeshDatasetGroupMetadata::DataType mDataType = QgsMeshDatasetGroupMetadata::DataType::DataOnVertices; QSize mOutputSize; QgsRectangle mBufferedExtent; + QPen mPen; + + std::unique_ptr mVectorColoring; + +}; + +/** + * \ingroup core + * + * Class for coloring vector datasets + * + * \note not available in Python bindings + * \since QGIS 3.14 + */ + +class QgsMeshVectorColoring +{ + public: + //! Default constructor + QgsMeshVectorColoring() = default; + //! Constructor + QgsMeshVectorColoring( const QgsMeshRendererVectorSettings &settings ); + + //! Sets the color ramp to define the coloring + void setColor( const QgsColorRampShader &colorRampShader ); + + //! Sets the single color to define the coloring + void setColor( const QColor &color ); + + //! Returns the color corresponding to the magnitude + QColor color( double magnitude ) const; + + private: + + QgsColorRampShader mColorRampShader; + QColor mSingleColor = Qt::black; + }; ///@endcond diff --git a/src/core/numericformats/qgsfractionnumericformat.cpp b/src/core/numericformats/qgsfractionnumericformat.cpp new file mode 100644 index 000000000000..cd33f5ba5191 --- /dev/null +++ b/src/core/numericformats/qgsfractionnumericformat.cpp @@ -0,0 +1,293 @@ +/*************************************************************************** + qgsfractionnumericformat.cpp + ---------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsfractionnumericformat.h" +#include "qgis.h" +#include +#include +#include +#include +#include +#include + +struct formatter : std::numpunct +{ + formatter( QChar thousands, bool showThousands, QChar decimal ) + : mThousands( thousands.unicode() ) + , mDecimal( decimal.unicode() ) + , mShowThousands( showThousands ) + {} + wchar_t do_decimal_point() const override { return mDecimal; } + wchar_t do_thousands_sep() const override { return mThousands; } + std::string do_grouping() const override { return mShowThousands ? "\3" : "\0"; } + + wchar_t mThousands; + wchar_t mDecimal; + bool mShowThousands = true; +}; + +QgsFractionNumericFormat::QgsFractionNumericFormat() +{ +} + +QString QgsFractionNumericFormat::id() const +{ + return QStringLiteral( "fraction" ); +} + +QString QgsFractionNumericFormat::visibleName() const +{ + return QObject::tr( "Fraction" ); +} + +int QgsFractionNumericFormat::sortKey() +{ + return 100; +} + +QString QgsFractionNumericFormat::formatDouble( double value, const QgsNumericFormatContext &context ) const +{ + std::basic_stringstream os; + os.imbue( std::locale( os.getloc(), new formatter( mThousandsSeparator.isNull() ? context.thousandsSeparator() : mThousandsSeparator, + mShowThousandsSeparator, + context.decimalSeparator() ) ) ); + + unsigned long long num; + unsigned long long den; + int sign; + + QString res; + + const double fixed = std::floor( std::fabs( value ) ); + bool success = doubleToVulgarFraction( std::fabs( value ) - fixed, num, den, sign ); + if ( success ) + { + if ( mUseDedicatedUnicode && num == 1 && den == 2 ) + res = QChar( 0xBD ); //½ + else if ( mUseDedicatedUnicode && num == 1 && den == 3 ) + res = QChar( 0x2153 ); //⅓ + else if ( mUseDedicatedUnicode && num == 2 && den == 3 ) + res = QChar( 0x2154 ); //⅔ + else if ( mUseDedicatedUnicode && num == 1 && den == 4 ) + res = QChar( 0xBC ); //¼ + else if ( mUseDedicatedUnicode && num == 3 && den == 4 ) + res = QChar( 0xBE ); //¾ + else if ( mUseDedicatedUnicode && num == 1 && den == 5 ) + res = QChar( 0x2155 ); //⅕ + else if ( mUseDedicatedUnicode && num == 2 && den == 5 ) + res = QChar( 0x2156 ); //⅖ + else if ( mUseDedicatedUnicode && num == 3 && den == 5 ) + res = QChar( 0x2157 ); //⅗ + else if ( mUseDedicatedUnicode && num == 4 && den == 5 ) + res = QChar( 0x2158 ); //⅘ + else if ( mUseDedicatedUnicode && num == 1 && den == 6 ) + res = QChar( 0x2159 ); //⅙ + else if ( mUseDedicatedUnicode && num == 5 && den == 6 ) + res = QChar( 0x215A ); //⅚ + else if ( mUseDedicatedUnicode && num == 1 && den == 7 ) + res = QChar( 0x2150 ); //⅐ + else if ( mUseDedicatedUnicode && num == 1 && den == 8 ) + res = QChar( 0x215B ); //⅛ + else if ( mUseDedicatedUnicode && num == 3 && den == 8 ) + res = QChar( 0x215C ); //⅜ + else if ( mUseDedicatedUnicode && num == 5 && den == 8 ) + res = QChar( 0x215D ); //⅝ + else if ( mUseDedicatedUnicode && num == 7 && den == 8 ) + res = QChar( 0x215E ); //⅞ + else if ( mUseDedicatedUnicode && num == 1 && den == 9 ) + res = QChar( 0x2151 ); //⅑ + else if ( mUseDedicatedUnicode && num == 1 && den == 10 ) + res = QChar( 0x2152 ); //⅒ + else if ( mUseUnicodeSuperSubscript ) + res = num == 0 ? QString() : QStringLiteral( "%1%2%3" ).arg( toUnicodeSuperscript( QString::number( num ) ), + QChar( 0x002F ), // "SOLIDUS" character + toUnicodeSubscript( QString::number( den ) ) ); + else + res = num == 0 ? QString() : QStringLiteral( "%2/%3" ).arg( num ).arg( den ); + if ( fixed ) + { + os << std::fixed << std::setprecision( 0 ); + os << fixed; + res.prepend( QString::fromStdWString( os.str() ) + ' ' ); + res = res.trimmed(); + } + if ( res.isEmpty() ) + res = QString::number( 0 ); + + if ( value < 0 ) + res.prepend( context.negativeSign() ); + } + else + { + os << std::fixed << std::setprecision( 10 ); + os << value; + res = QString::fromStdWString( os.str() ); + } + + if ( value > 0 && mShowPlusSign ) + { + res.prepend( context.positiveSign() ); + } + + return res; +} + +QgsNumericFormat *QgsFractionNumericFormat::clone() const +{ + return new QgsFractionNumericFormat( *this ); +} + +QgsNumericFormat *QgsFractionNumericFormat::create( const QVariantMap &configuration, const QgsReadWriteContext &context ) const +{ + std::unique_ptr< QgsFractionNumericFormat > res = qgis::make_unique< QgsFractionNumericFormat >(); + res->setConfiguration( configuration, context ); + return res.release(); +} + +QVariantMap QgsFractionNumericFormat::configuration( const QgsReadWriteContext & ) const +{ + QVariantMap res; + res.insert( QStringLiteral( "show_thousand_separator" ), mShowThousandsSeparator ); + res.insert( QStringLiteral( "show_plus" ), mShowPlusSign ); + res.insert( QStringLiteral( "thousand_separator" ), mThousandsSeparator ); + res.insert( QStringLiteral( "use_dedicated_unicode" ), mUseDedicatedUnicode ); + res.insert( QStringLiteral( "use_unicode_supersubscript" ), mUseUnicodeSuperSubscript ); + return res; +} + +double QgsFractionNumericFormat::suggestSampleValue() const +{ + return 1234.75; +} + +bool QgsFractionNumericFormat::useDedicatedUnicodeCharacters() const +{ + return mUseDedicatedUnicode; +} + +void QgsFractionNumericFormat::setUseDedicatedUnicodeCharacters( bool enabled ) +{ + mUseDedicatedUnicode = enabled; +} + +bool QgsFractionNumericFormat::useUnicodeSuperSubscript() const +{ + return mUseUnicodeSuperSubscript; +} + +void QgsFractionNumericFormat::setUseUnicodeSuperSubscript( bool enabled ) +{ + mUseUnicodeSuperSubscript = enabled; +} + +void QgsFractionNumericFormat::setConfiguration( const QVariantMap &configuration, const QgsReadWriteContext & ) +{ + mShowThousandsSeparator = configuration.value( QStringLiteral( "show_thousand_separator" ), true ).toBool(); + mShowPlusSign = configuration.value( QStringLiteral( "show_plus" ), false ).toBool(); + mThousandsSeparator = configuration.value( QStringLiteral( "thousand_separator" ), QChar() ).toChar(); + mUseDedicatedUnicode = configuration.value( QStringLiteral( "use_dedicated_unicode" ), false ).toBool(); + mUseUnicodeSuperSubscript = configuration.value( QStringLiteral( "use_unicode_supersubscript" ), true ).toBool(); +} + +bool QgsFractionNumericFormat::showThousandsSeparator() const +{ + return mShowThousandsSeparator; +} + +void QgsFractionNumericFormat::setShowThousandsSeparator( bool showThousandsSeparator ) +{ + mShowThousandsSeparator = showThousandsSeparator; +} + +bool QgsFractionNumericFormat::showPlusSign() const +{ + return mShowPlusSign; +} + +void QgsFractionNumericFormat::setShowPlusSign( bool showPlusSign ) +{ + mShowPlusSign = showPlusSign; +} + +QChar QgsFractionNumericFormat::thousandsSeparator() const +{ + return mThousandsSeparator; +} + +void QgsFractionNumericFormat::setThousandsSeparator( QChar character ) +{ + mThousandsSeparator = character; +} + +QString QgsFractionNumericFormat::toUnicodeSuperscript( const QString &input ) +{ + QString res = input; + for ( int i = 0; i < input.size(); ++i ) + { + QChar c = input.at( i ); + if ( c == '0' ) + res[i] = QChar( 0x2070 ); //⁰ + else if ( c == '1' ) + res[i] = QChar( 0x00B9 ); //¹ + else if ( c == '2' ) + res[i] = QChar( 0x00B2 ); //² + else if ( c == '3' ) + res[i] = QChar( 0x00B3 ); //³ + else if ( c == '4' ) + res[i] = QChar( 0x2074 ); //⁴ + else if ( c == '5' ) + res[i] = QChar( 0x2075 ); //⁵ + else if ( c == '6' ) + res[i] = QChar( 0x2076 ); //⁶ + else if ( c == '7' ) + res[i] = QChar( 0x2077 ); //⁷ + else if ( c == '8' ) + res[i] = QChar( 0x2078 ); //⁸ + else if ( c == '9' ) + res[i] = QChar( 0x2079 ); //⁹ + } + return res; +} + +QString QgsFractionNumericFormat::toUnicodeSubscript( const QString &input ) +{ + QString res = input; + for ( int i = 0; i < input.size(); ++i ) + { + QChar c = input.at( i ); + if ( c == '0' ) + res[i] = QChar( 0x2080 ); //₀ + else if ( c == '1' ) + res[i] = QChar( 0x2081 ); //₁ + else if ( c == '2' ) + res[i] = QChar( 0x2082 ); //₂ + else if ( c == '3' ) + res[i] = QChar( 0x2083 ); //₃ + else if ( c == '4' ) + res[i] = QChar( 0x2084 ); //₄ + else if ( c == '5' ) + res[i] = QChar( 0x2085 ); //₅ + else if ( c == '6' ) + res[i] = QChar( 0x2086 ); //₆ + else if ( c == '7' ) + res[i] = QChar( 0x2087 ); //₇ + else if ( c == '8' ) + res[i] = QChar( 0x2088 ); //₈ + else if ( c == '9' ) + res[i] = QChar( 0x2089 ); //₉ + } + return res; +} diff --git a/src/core/numericformats/qgsfractionnumericformat.h b/src/core/numericformats/qgsfractionnumericformat.h new file mode 100644 index 000000000000..1c7d72a06b28 --- /dev/null +++ b/src/core/numericformats/qgsfractionnumericformat.h @@ -0,0 +1,188 @@ +/*************************************************************************** + qgsfractionnumericformat.h + -------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSFRACTIONNUMERICFORMAT_H +#define QGSFRACTIONNUMERICFORMAT_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgis.h" +#include "qgsnumericformat.h" +#include + +/** + * \ingroup core + * A numeric formatter which returns a vulgar fractional representation of a decimal value (e.g. "1/2" instead of 0.5). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsFractionNumericFormat : public QgsNumericFormat +{ + public: + + /** + * Default constructor + */ + QgsFractionNumericFormat(); + + QString id() const override; + QString visibleName() const override; + int sortKey() override; + QString formatDouble( double value, const QgsNumericFormatContext &context ) const override; + QgsNumericFormat *clone() const override SIP_FACTORY; + QgsNumericFormat *create( const QVariantMap &configuration, const QgsReadWriteContext &context ) const override SIP_FACTORY; + QVariantMap configuration( const QgsReadWriteContext &context ) const override; + double suggestSampleValue() const override; + + /** + * Returns TRUE if dedicated unicode characters should be used, when the are available for the + * particular fraction (e.g. ½, ¼). + * \see setUseDedicatedUnicodeCharacters() + * \see useUnicodeSuperSubscript() + */ + bool useDedicatedUnicodeCharacters() const; + + /** + * Sets whether dedicated unicode characters should be used, when the are available for the + * particular fraction (e.g. ½, ¼). + * \see useDedicatedUnicodeCharacters() + * \see setUseUnicodeSuperSubscript() + */ + void setUseDedicatedUnicodeCharacters( bool enabled ); + + /** + * Returns TRUE if unicode superscript and subscript characters should be used, (e.g. "⁶/₇"). + * \see setUseUnicodeSuperSubscript() + * \see useDedicatedUnicodeCharacters() + */ + bool useUnicodeSuperSubscript() const; + + /** + * Sets whether unicode superscript and subscript characters should be used, (e.g. "⁶/₇"). + * \see useUnicodeSuperSubscript() + * \see setUseDedicatedUnicodeCharacters() + */ + void setUseUnicodeSuperSubscript( bool enabled ); + + /** + * Returns TRUE if the thousands grouping separator will be shown. + * \see setShowThousandsSeparator() + */ + bool showThousandsSeparator() const; + + /** + * Sets whether the thousands grouping separator will be shown. + * \see showThousandsSeparator() + */ + void setShowThousandsSeparator( bool show ); + + /** + * Returns TRUE if a leading plus sign will be shown for positive values. + * \see setShowPlusSign() + */ + bool showPlusSign() const; + + /** + * Sets whether a leading plus sign will be shown for positive values. + * \see showPlusSign() + */ + void setShowPlusSign( bool show ); + + /** + * Returns any override for the thousands separator character. If an invalid QChar is returned, + * then the QGIS locale separator is used instead. + * + * \see setThousandsSeparator() + */ + QChar thousandsSeparator() const; + + /** + * Sets an override \a character for the thousands separator character. If an invalid QChar is set, + * then the QGIS locale separator is used instead. + * + * \see thousandsSeparator() + */ + void setThousandsSeparator( QChar character ); + + /** + * Converts a double \a value to a vulgar fraction (e.g. ⅓, ¼, etc) by attempting to calculate + * the corresponding \a numerator and \a denominator, within the specified \a tolerance. + * + * This method is based of Richard's algorithm (1981) from "Continued Fractions without Tears" (University of Minnesota). + * + * \param value input value to convert + * \param numerator will be set to calculated fraction numerator + * \param denominator will be set to the calculated fraction denominator + * \param sign will be set to the sign of the result (as -1 or +1 values) + * \param tolerance acceptable tolerance. Larger values will give "nicer" fractions. + * \returns TRUE if \a value was successfully converted to a fraction + */ + static bool doubleToVulgarFraction( const double value, unsigned long long &numerator SIP_OUT, unsigned long long &denominator SIP_OUT, int &sign SIP_OUT, const double tolerance = 1e-10 ) + { + sign = value < 0 ? -1 : 1; + double g = std::fabs( value ); + unsigned long long a = 0; + unsigned long long b = 1; + unsigned long long c = 1; + unsigned long long d = 0; + unsigned long long s; + unsigned int iteration = 0; + do + { + s = std::floor( g ); + numerator = a + s * c; + denominator = b + s * d; + a = c; + b = d; + c = numerator; + d = denominator; + g = 1.0 / ( g - s ); + if ( qgsDoubleNear( static_cast< double >( sign )*static_cast< double >( numerator ) / denominator, value, tolerance ) ) + { + return true; + } + } + while ( iteration++ < 100 ); // limit to 100 iterations, should be sufficient for realistic purposes + return false; + } + + /** + * Converts numbers in an \a input string to unicode superscript equivalents. + * \see toUnicodeSubscript() + */ + static QString toUnicodeSuperscript( const QString &input ); + + /** + * Converts numbers in an \a input string to unicode subscript equivalents. + * \see toUnicodeSuperscript() + */ + static QString toUnicodeSubscript( const QString &input ); + + protected: + + /** + * Sets the format's \a configuration. + */ + virtual void setConfiguration( const QVariantMap &configuration, const QgsReadWriteContext &context ); + + private: + + bool mUseDedicatedUnicode = false; + bool mUseUnicodeSuperSubscript = true; + bool mShowThousandsSeparator = true; + bool mShowPlusSign = false; + QChar mThousandsSeparator; +}; + +#endif // QGSFRACTIONNUMERICFORMAT_H diff --git a/src/core/numericformats/qgsnumericformat.h b/src/core/numericformats/qgsnumericformat.h index ed2ba4341b90..db393123a5b2 100644 --- a/src/core/numericformats/qgsnumericformat.h +++ b/src/core/numericformats/qgsnumericformat.h @@ -231,6 +231,8 @@ class CORE_EXPORT QgsNumericFormat sipType = sipType_QgsCurrencyNumericFormat; else if ( dynamic_cast< QgsBasicNumericFormat * >( sipCpp ) ) sipType = sipType_QgsBasicNumericFormat; + else if ( dynamic_cast< QgsFractionNumericFormat * >( sipCpp ) ) + sipType = sipType_QgsFractionNumericFormat; else sipType = NULL; SIP_END diff --git a/src/core/numericformats/qgsnumericformatregistry.cpp b/src/core/numericformats/qgsnumericformatregistry.cpp index 4334fa81784c..a568022676a0 100644 --- a/src/core/numericformats/qgsnumericformatregistry.cpp +++ b/src/core/numericformats/qgsnumericformatregistry.cpp @@ -21,6 +21,7 @@ #include "qgscurrencynumericformat.h" #include "qgspercentagenumericformat.h" #include "qgsscientificnumericformat.h" +#include "qgsfractionnumericformat.h" #include "qgsxmlutils.h" QgsNumericFormatRegistry::QgsNumericFormatRegistry() @@ -31,6 +32,7 @@ QgsNumericFormatRegistry::QgsNumericFormatRegistry() addFormat( new QgsCurrencyNumericFormat() ); addFormat( new QgsPercentageNumericFormat() ); addFormat( new QgsScientificNumericFormat() ); + addFormat( new QgsFractionNumericFormat() ); } QgsNumericFormatRegistry::~QgsNumericFormatRegistry() diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp index 0ea8c40177ac..b54a8c75d89a 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.cpp @@ -27,6 +27,7 @@ #include "qgsapplication.h" #include "qgsprocessingparametertype.h" #include "qgsexpressioncontextutils.h" +#include "qgsprocessingmodelgroupbox.h" #include #include @@ -99,62 +100,70 @@ QgsProcessingAlgorithm::Flags QgsProcessingModelAlgorithm::flags() const QVariantMap QgsProcessingModelAlgorithm::parametersForChildAlgorithm( const QgsProcessingModelChildAlgorithm &child, const QVariantMap &modelParameters, const QVariantMap &results, const QgsExpressionContext &expressionContext ) const { - QVariantMap childParams; - const auto constParameterDefinitions = child.algorithm()->parameterDefinitions(); - for ( const QgsProcessingParameterDefinition *def : constParameterDefinitions ) + auto evaluateSources = [ = ]( const QgsProcessingParameterDefinition * def )->QVariant { - if ( !def->isDestination() ) - { - if ( !child.parameterSources().contains( def->name() ) ) - continue; // use default value + const QgsProcessingModelChildParameterSources paramSources = child.parameterSources().value( def->name() ); - QgsProcessingModelChildParameterSources paramSources = child.parameterSources().value( def->name() ); - - QString expressionText; - QVariantList paramParts; - const auto constParamSources = paramSources; - for ( const QgsProcessingModelChildParameterSource &source : constParamSources ) + QString expressionText; + QVariantList paramParts; + for ( const QgsProcessingModelChildParameterSource &source : paramSources ) + { + switch ( source.source() ) { - switch ( source.source() ) - { - case QgsProcessingModelChildParameterSource::StaticValue: - paramParts << source.staticValue(); - break; + case QgsProcessingModelChildParameterSource::StaticValue: + paramParts << source.staticValue(); + break; - case QgsProcessingModelChildParameterSource::ModelParameter: - paramParts << modelParameters.value( source.parameterName() ); - break; + case QgsProcessingModelChildParameterSource::ModelParameter: + paramParts << modelParameters.value( source.parameterName() ); + break; - case QgsProcessingModelChildParameterSource::ChildOutput: - { - QVariantMap linkedChildResults = results.value( source.outputChildId() ).toMap(); - paramParts << linkedChildResults.value( source.outputName() ); - break; - } + case QgsProcessingModelChildParameterSource::ChildOutput: + { + QVariantMap linkedChildResults = results.value( source.outputChildId() ).toMap(); + paramParts << linkedChildResults.value( source.outputName() ); + break; + } - case QgsProcessingModelChildParameterSource::Expression: - { - QgsExpression exp( source.expression() ); - paramParts << exp.evaluate( &expressionContext ); - break; - } - case QgsProcessingModelChildParameterSource::ExpressionText: - { - expressionText = QgsExpression::replaceExpressionText( source.expressionText(), &expressionContext ); - break; - } + case QgsProcessingModelChildParameterSource::Expression: + { + QgsExpression exp( source.expression() ); + paramParts << exp.evaluate( &expressionContext ); + break; + } + case QgsProcessingModelChildParameterSource::ExpressionText: + { + expressionText = QgsExpression::replaceExpressionText( source.expressionText(), &expressionContext ); + break; } - } - if ( ! expressionText.isEmpty() ) - { - childParams.insert( def->name(), expressionText ); + case QgsProcessingModelChildParameterSource::ModelOutput: + break; } - else if ( paramParts.count() == 1 ) - childParams.insert( def->name(), paramParts.at( 0 ) ); - else - childParams.insert( def->name(), paramParts ); + } + + if ( ! expressionText.isEmpty() ) + { + return expressionText; + } + else if ( paramParts.count() == 1 ) + return paramParts.at( 0 ); + else + return paramParts; + }; + + QVariantMap childParams; + const auto constParameterDefinitions = child.algorithm()->parameterDefinitions(); + for ( const QgsProcessingParameterDefinition *def : constParameterDefinitions ) + { + if ( !def->isDestination() ) + { + if ( !child.parameterSources().contains( def->name() ) ) + continue; // use default value + + const QVariant value = evaluateSources( def ); + childParams.insert( def->name(), value ); } else { @@ -187,7 +196,19 @@ QVariantMap QgsProcessingModelAlgorithm::parametersForChildAlgorithm( const QgsP } } - if ( !isFinalOutput ) + bool hasExplicitDefinition = false; + if ( !isFinalOutput && child.parameterSources().contains( def->name() ) ) + { + // explicitly defined source for output + const QVariant value = evaluateSources( def ); + if ( value.isValid() ) + { + childParams.insert( def->name(), value ); + hasExplicitDefinition = true; + } + } + + if ( !isFinalOutput && !hasExplicitDefinition ) { // output is temporary @@ -253,14 +274,15 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa QgsExpressionContext baseContext = createExpressionContext( parameters, context ); QVariantMap childResults; + QVariantMap childInputs; + QVariantMap finalResults; QSet< QString > executed; bool executedAlg = true; while ( executedAlg && executed.count() < toExecute.count() ) { executedAlg = false; - const auto constToExecute = toExecute; - for ( const QString &childId : constToExecute ) + for ( const QString &childId : qgis::as_const( toExecute ) ) { if ( feedback && feedback->isCanceled() ) break; @@ -269,8 +291,8 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa continue; bool canExecute = true; - const auto constDependsOnChildAlgorithms = dependsOnChildAlgorithms( childId ); - for ( const QString &dependency : constDependsOnChildAlgorithms ) + const QSet< QString > dependencies = dependsOnChildAlgorithms( childId ); + for ( const QString &dependency : dependencies ) { if ( !executed.contains( dependency ) ) { @@ -283,10 +305,13 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa continue; executedAlg = true; - if ( feedback ) - feedback->pushDebugInfo( QObject::tr( "Prepare algorithm: %1" ).arg( childId ) ); const QgsProcessingModelChildAlgorithm &child = mChildAlgorithms[ childId ]; + std::unique_ptr< QgsProcessingAlgorithm > childAlg( child.algorithm()->create( child.configuration() ) ); + + const bool skipGenericLogging = childAlg->flags() & QgsProcessingAlgorithm::FlagSkipGenericModelLogging; + if ( feedback && !skipGenericLogging ) + feedback->pushDebugInfo( QObject::tr( "Prepare algorithm: %1" ).arg( childId ) ); QgsExpressionContext expContext = baseContext; expContext << QgsExpressionContextUtils::processingAlgorithmScope( child.algorithm(), parameters, context ) @@ -294,9 +319,10 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa context.setExpressionContext( expContext ); QVariantMap childParams = parametersForChildAlgorithm( child, parameters, childResults, expContext ); - if ( feedback ) + if ( feedback && !skipGenericLogging ) feedback->setProgressText( QObject::tr( "Running %1 [%2/%3]" ).arg( child.description() ).arg( executed.count() + 1 ).arg( toExecute.count() ) ); + childInputs.insert( childId, childParams ); QStringList params; for ( auto childParamIt = childParams.constBegin(); childParamIt != childParams.constEnd(); ++childParamIt ) { @@ -304,7 +330,7 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa child.algorithm()->parameterDefinition( childParamIt.key() )->valueAsPythonString( childParamIt.value(), context ) ); } - if ( feedback ) + if ( feedback && !skipGenericLogging ) { feedback->pushInfo( QObject::tr( "Input Parameters:" ) ); feedback->pushCommandInfo( QStringLiteral( "{ %1 }" ).arg( params.join( QStringLiteral( ", " ) ) ) ); @@ -314,14 +340,12 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa childTime.start(); bool ok = false; - std::unique_ptr< QgsProcessingAlgorithm > childAlg( child.algorithm()->create( child.configuration() ) ); QVariantMap results = childAlg->run( childParams, context, &modelFeedback, &ok, child.configuration() ); if ( !ok ) { const QString error = childAlg->flags() & QgsProcessingAlgorithm::FlagCustomException ? QString() : QObject::tr( "Error encountered while running %1" ).arg( child.description() ); throw QgsProcessingException( error ); } - childAlg.reset( nullptr ); childResults.insert( childId, results ); // look through child alg's outputs to determine whether any of these should be copied @@ -334,8 +358,64 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa } executed.insert( childId ); + + std::function< void( const QString & )> pruneAlgorithmBranchRecursive; + pruneAlgorithmBranchRecursive = [&]( const QString & id ) + { + const QSet toPrune = dependentChildAlgorithms( id ); + for ( const QString &targetId : toPrune ) + { + if ( executed.contains( targetId ) ) + continue; + + executed.insert( targetId ); + pruneAlgorithmBranchRecursive( targetId ); + } + }; + + if ( childAlg->flags() & QgsProcessingAlgorithm::FlagPruneModelBranchesBasedOnAlgorithmResults ) + { + // check if any dependent algorithms should be canceled based on the outputs of this algorithm run + // first find all direct dependencies of this algorithm by looking through all remaining child algorithms + for ( const QString &candidateId : qgis::as_const( toExecute ) ) + { + if ( executed.contains( candidateId ) ) + continue; + + // a pending algorithm was found..., check it's parameter sources to see if it links to any of the current + // algorithm's outputs + const QgsProcessingModelChildAlgorithm &candidate = mChildAlgorithms[ candidateId ]; + const QMap candidateParams = candidate.parameterSources(); + QMap::const_iterator paramIt = candidateParams.constBegin(); + bool pruned = false; + for ( ; paramIt != candidateParams.constEnd(); ++paramIt ) + { + for ( const QgsProcessingModelChildParameterSource &source : paramIt.value() ) + { + if ( source.source() == QgsProcessingModelChildParameterSource::ChildOutput && source.outputChildId() == childId ) + { + // ok, this one is dependent on the current alg. Did we get a value for it? + if ( !results.contains( source.outputName() ) ) + { + // oh no, nothing returned for this parameter. Gotta trim the branch back! + pruned = true; + // skip the dependent alg.. + executed.insert( candidateId ); + //... and everything which depends on it + pruneAlgorithmBranchRecursive( candidateId ); + break; + } + } + } + if ( pruned ) + break; + } + } + } + + childAlg.reset( nullptr ); modelFeedback.setCurrentStep( executed.count() ); - if ( feedback ) + if ( feedback && !skipGenericLogging ) feedback->pushInfo( QObject::tr( "OK. Execution took %1 s (%2 outputs)." ).arg( childTime.elapsed() / 1000.0 ).arg( results.count() ) ); } @@ -346,6 +426,8 @@ QVariantMap QgsProcessingModelAlgorithm::processAlgorithm( const QVariantMap &pa feedback->pushDebugInfo( QObject::tr( "Model processed OK. Executed %1 algorithms total in %2 s." ).arg( executed.count() ).arg( totalTime.elapsed() / 1000.0 ) ); mResults = finalResults; + mResults.insert( QStringLiteral( "CHILD_RESULTS" ), childResults ); + mResults.insert( QStringLiteral( "CHILD_INPUTS" ), childInputs ); return mResults; } @@ -777,6 +859,7 @@ QMap QgsProcessingMode case QgsProcessingModelChildParameterSource::Expression: case QgsProcessingModelChildParameterSource::ExpressionText: case QgsProcessingModelChildParameterSource::StaticValue: + case QgsProcessingModelChildParameterSource::ModelOutput: continue; }; variables.insert( safeName( name ), VariableDefinition( value, source, description ) ); @@ -822,6 +905,7 @@ QMap QgsProcessingMode case QgsProcessingModelChildParameterSource::Expression: case QgsProcessingModelChildParameterSource::ExpressionText: case QgsProcessingModelChildParameterSource::StaticValue: + case QgsProcessingModelChildParameterSource::ModelOutput: continue; }; @@ -880,6 +964,7 @@ QMap QgsProcessingMode case QgsProcessingModelChildParameterSource::Expression: case QgsProcessingModelChildParameterSource::ExpressionText: case QgsProcessingModelChildParameterSource::StaticValue: + case QgsProcessingModelChildParameterSource::ModelOutput: continue; }; @@ -1129,6 +1214,21 @@ void QgsProcessingModelAlgorithm::updateDestinationParameters() } } +void QgsProcessingModelAlgorithm::addGroupBox( const QgsProcessingModelGroupBox &groupBox ) +{ + mGroupBoxes.insert( groupBox.uuid(), groupBox ); +} + +QList QgsProcessingModelAlgorithm::groupBoxes() const +{ + return mGroupBoxes.values(); +} + +void QgsProcessingModelAlgorithm::removeGroupBox( const QString &uuid ) +{ + mGroupBoxes.remove( uuid ); +} + QVariant QgsProcessingModelAlgorithm::toVariant() const { QVariantMap map; @@ -1153,13 +1253,19 @@ QVariant QgsProcessingModelAlgorithm::toVariant() const map.insert( QStringLiteral( "parameters" ), paramMap ); QVariantMap paramDefMap; - const auto constMParameters = mParameters; - for ( const QgsProcessingParameterDefinition *def : constMParameters ) + for ( const QgsProcessingParameterDefinition *def : mParameters ) { paramDefMap.insert( def->name(), def->toVariantMap() ); } map.insert( QStringLiteral( "parameterDefinitions" ), paramDefMap ); + QVariantList groupBoxDefs; + for ( auto it = mGroupBoxes.constBegin(); it != mGroupBoxes.constEnd(); ++it ) + { + groupBoxDefs.append( it.value().toVariant() ); + } + map.insert( QStringLiteral( "groupBoxes" ), groupBoxDefs ); + map.insert( QStringLiteral( "modelVariables" ), mVariables ); map.insert( QStringLiteral( "designerParameterValues" ), mDesignerParameterValues ); @@ -1228,6 +1334,15 @@ bool QgsProcessingModelAlgorithm::loadVariant( const QVariant &model ) } } + mGroupBoxes.clear(); + const QVariantList groupBoxList = map.value( QStringLiteral( "groupBoxes" ) ).toList(); + for ( const QVariant &groupBoxDef : groupBoxList ) + { + QgsProcessingModelGroupBox groupBox; + groupBox.loadVariant( groupBoxDef.toMap() ); + mGroupBoxes.insert( groupBox.uuid(), groupBox ); + } + updateDestinationParameters(); return true; diff --git a/src/core/processing/models/qgsprocessingmodelalgorithm.h b/src/core/processing/models/qgsprocessingmodelalgorithm.h index 7218d2312eb1..8a0042cb305c 100644 --- a/src/core/processing/models/qgsprocessingmodelalgorithm.h +++ b/src/core/processing/models/qgsprocessingmodelalgorithm.h @@ -24,6 +24,7 @@ #include "qgsprocessingalgorithm.h" #include "qgsprocessingmodelparameter.h" #include "qgsprocessingmodelchildparametersource.h" +#include "qgsprocessingmodelgroupbox.h" ///@cond NOT_STABLE @@ -241,6 +242,33 @@ class CORE_EXPORT QgsProcessingModelAlgorithm : public QgsProcessingAlgorithm */ void updateDestinationParameters(); + /** + * Adds a new group \a box to the model. + * + * If an existing group box with the same uuid already exists then its definition will be replaced. + * + * \see groupBoxes() + * \since QGIS 3.14 + */ + void addGroupBox( const QgsProcessingModelGroupBox &groupBox ); + + /** + * Returns a list of the group boxes within the model. + * + * \see addGroupBox() + * \since QGIS 3.14 + */ + QList< QgsProcessingModelGroupBox > groupBoxes() const; + + /** + * Removes the group box with matching \a uuid from the model. + * + * \see addGroupBox() + * \see groupBoxes() + * \since QGIS 3.14 + */ + void removeGroupBox( const QString &uuid ); + /** * Writes the model to a file, at the specified \a path. * \see fromFile() @@ -452,6 +480,8 @@ class CORE_EXPORT QgsProcessingModelAlgorithm : public QgsProcessingAlgorithm QVariantMap mDesignerParameterValues; + QMap< QString, QgsProcessingModelGroupBox > mGroupBoxes; + void dependsOnChildAlgorithmsRecursive( const QString &childId, QSet &depends ) const; void dependentChildAlgorithmsRecursive( const QString &childId, QSet &depends ) const; diff --git a/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp b/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp index ab2b1c50dfb8..9dcd2d67d0b0 100644 --- a/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp +++ b/src/core/processing/models/qgsprocessingmodelchildalgorithm.cpp @@ -68,7 +68,10 @@ void QgsProcessingModelChildAlgorithm::copyNonDefinitionPropertiesFromModel( Qgs for ( auto it = mModelOutputs.begin(); it != mModelOutputs.end(); ++it ) { if ( !existingChild.modelOutputs().value( it.key() ).position().isNull() ) + { it.value().setPosition( existingChild.modelOutputs().value( it.key() ).position() ); + it.value().setSize( existingChild.modelOutputs().value( it.key() ).size() ); + } else it.value().setPosition( position() + QPointF( size().width(), ( i + 1.5 ) * size().height() ) ); @@ -79,6 +82,7 @@ void QgsProcessingModelChildAlgorithm::copyNonDefinitionPropertiesFromModel( Qgs comment->setDescription( existingComment->description() ); comment->setSize( existingComment->size() ); comment->setPosition( existingComment->position() ); + comment->setColor( existingComment->color() ); } } i++; diff --git a/src/core/processing/models/qgsprocessingmodelchildparametersource.cpp b/src/core/processing/models/qgsprocessingmodelchildparametersource.cpp index abf50449b4c3..aa6a866bc258 100644 --- a/src/core/processing/models/qgsprocessingmodelchildparametersource.cpp +++ b/src/core/processing/models/qgsprocessingmodelchildparametersource.cpp @@ -18,6 +18,7 @@ #include "qgsprocessingmodelchildparametersource.h" #include "qgsprocessingparameters.h" #include "qgsprocessingcontext.h" +#include "qgsprocessingmodelalgorithm.h" ///@cond NOT_STABLE @@ -38,6 +39,8 @@ bool QgsProcessingModelChildParameterSource::operator==( const QgsProcessingMode return mExpression == other.mExpression; case ExpressionText: return mExpressionText == other.mExpressionText; + case ModelOutput: + return true; } return false; } @@ -88,6 +91,11 @@ QgsProcessingModelChildParameterSource::Source QgsProcessingModelChildParameterS return mSource; } +void QgsProcessingModelChildParameterSource::setSource( QgsProcessingModelChildParameterSource::Source source ) +{ + mSource = source; +} + QVariant QgsProcessingModelChildParameterSource::toVariant() const { QVariantMap map; @@ -114,6 +122,9 @@ QVariant QgsProcessingModelChildParameterSource::toVariant() const case ExpressionText: map.insert( QStringLiteral( "expression_text" ), mExpressionText ); break; + + case ModelOutput: + break; } return map; } @@ -143,6 +154,9 @@ bool QgsProcessingModelChildParameterSource::loadVariant( const QVariantMap &map case ExpressionText: mExpressionText = map.value( QStringLiteral( "expression_text" ) ).toString(); break; + + case ModelOutput: + break; } return true; } @@ -173,8 +187,91 @@ QString QgsProcessingModelChildParameterSource::asPythonCode( const QgsProcessin case ExpressionText: return mExpressionText; + + case ModelOutput: + return QString(); } return QString(); } +QString QgsProcessingModelChildParameterSource::friendlyIdentifier( QgsProcessingModelAlgorithm *model ) const +{ + switch ( mSource ) + { + case ModelParameter: + return model ? model->parameterDefinition( mParameterName )->description() : mParameterName; + + case ChildOutput: + { + if ( model ) + { + const QgsProcessingModelChildAlgorithm &alg = model->childAlgorithm( mChildId ); + QString outputName = alg.algorithm() && alg.algorithm()->outputDefinition( mOutputName ) ? alg.algorithm()->outputDefinition( mOutputName )->description() : mOutputName; + // see if this output has been named by the model designer -- if so, we use that friendly name + const QMap outputs = alg.modelOutputs(); + for ( auto it = outputs.constBegin(); it != outputs.constEnd(); ++it ) + { + if ( it.value().childOutputName() == mOutputName ) + { + outputName = it.key(); + break; + } + } + return QObject::tr( "'%1' from algorithm '%2'" ).arg( outputName, alg.description() ); + } + else + { + return QObject::tr( "'%1' from algorithm '%2'" ).arg( mOutputName, mChildId ); + } + } + + case StaticValue: + return mStaticValue.toString(); + + case Expression: + return mExpression; + + case ExpressionText: + return mExpressionText; + + case ModelOutput: + return QString(); + } + return QString(); +} + +QDataStream &operator<<( QDataStream &out, const QgsProcessingModelChildParameterSource &source ) +{ + out << source.source(); + out << source.staticValue(); + out << source.parameterName(); + out << source.outputChildId(); + out << source.outputName(); + out << source.expression(); + out << source.expressionText(); + return out; +} + +QDataStream &operator>>( QDataStream &in, QgsProcessingModelChildParameterSource &source ) +{ + int sourceType; + QVariant staticValue; + QString parameterName; + QString outputChildId; + QString outputName; + QString expression; + QString expressionText; + + in >> sourceType >> staticValue >> parameterName >> outputChildId >> outputName >> expression >> expressionText; + + source.setStaticValue( staticValue ); + source.setParameterName( parameterName ); + source.setOutputChildId( outputChildId ); + source.setOutputName( outputName ); + source.setExpression( expression ); + source.setExpressionText( expressionText ); + source.setSource( static_cast( sourceType ) ); + return in; +} + ///@endcond diff --git a/src/core/processing/models/qgsprocessingmodelchildparametersource.h b/src/core/processing/models/qgsprocessingmodelchildparametersource.h index b1f0e711747c..261abe4d0fd9 100644 --- a/src/core/processing/models/qgsprocessingmodelchildparametersource.h +++ b/src/core/processing/models/qgsprocessingmodelchildparametersource.h @@ -22,6 +22,7 @@ #include "qgis.h" #include "qgsprocessing.h" class QgsProcessingParameterDefinition; +class QgsProcessingModelAlgorithm; ///@cond NOT_STABLE @@ -42,6 +43,7 @@ class CORE_EXPORT QgsProcessingModelChildParameterSource StaticValue, //!< Parameter value is a static value Expression, //!< Parameter value is taken from an expression, evaluated just before the algorithm runs ExpressionText, //!< Parameter value is taken from a text with expressions, evaluated just before the algorithm runs + ModelOutput, //!< Parameter value is linked to an output parameter for the model }; /** @@ -115,6 +117,13 @@ class CORE_EXPORT QgsProcessingModelChildParameterSource */ Source source() const; + /** + * Sets the parameter's source. + * + * \since QGIS 3.14 + */ + void setSource( Source source ); + /** * Returns the source's static value. This is only used when the source() is StaticValue. * \see setStaticValue() @@ -220,6 +229,12 @@ class CORE_EXPORT QgsProcessingModelChildParameterSource */ QString asPythonCode( QgsProcessing::PythonOutputType outputType, const QgsProcessingParameterDefinition *definition, const QMap< QString, QString > &friendlydChildNames ) const; + /** + * Returns a user-friendly identifier for this source, given the context of the specified \a model. + * \since QGIS 3.14 + */ + QString friendlyIdentifier( QgsProcessingModelAlgorithm *model ) const; + private: Source mSource = StaticValue; @@ -233,6 +248,8 @@ class CORE_EXPORT QgsProcessingModelChildParameterSource }; Q_DECLARE_METATYPE( QgsProcessingModelChildParameterSource ); +CORE_EXPORT QDataStream &operator<<( QDataStream &out, const QgsProcessingModelChildParameterSource &source ); +CORE_EXPORT QDataStream &operator>>( QDataStream &in, QgsProcessingModelChildParameterSource &source ); #ifndef SIP_RUN //! List of child parameter sources diff --git a/src/core/processing/models/qgsprocessingmodelcomponent.cpp b/src/core/processing/models/qgsprocessingmodelcomponent.cpp index bd835ff6165d..6f3e4bd97dcd 100644 --- a/src/core/processing/models/qgsprocessingmodelcomponent.cpp +++ b/src/core/processing/models/qgsprocessingmodelcomponent.cpp @@ -17,6 +17,7 @@ #include "qgsprocessingmodelcomponent.h" #include "qgsprocessingmodelcomment.h" +#include "qgssymbollayerutils.h" ///@cond NOT_STABLE @@ -54,6 +55,16 @@ void QgsProcessingModelComponent::setSize( QSizeF size ) mSize = size; } +QColor QgsProcessingModelComponent::color() const +{ + return mColor; +} + +void QgsProcessingModelComponent::setColor( const QColor &color ) +{ + mColor = color; +} + bool QgsProcessingModelComponent::linksCollapsed( Qt::Edge edge ) const { switch ( edge ) @@ -103,6 +114,7 @@ void QgsProcessingModelComponent::saveCommonProperties( QVariantMap &map ) const map.insert( QStringLiteral( "component_height" ), mSize.height() ); map.insert( QStringLiteral( "parameters_collapsed" ), mTopEdgeLinksCollapsed ); map.insert( QStringLiteral( "outputs_collapsed" ), mBottomEdgeLinksCollapsed ); + map.insert( QStringLiteral( "color" ), mColor.isValid() ? QgsSymbolLayerUtils::encodeColor( mColor ) : QString() ); if ( comment() ) map.insert( QStringLiteral( "comment" ), comment()->toVariant() ); } @@ -116,6 +128,7 @@ void QgsProcessingModelComponent::restoreCommonProperties( const QVariantMap &ma mDescription = map.value( QStringLiteral( "component_description" ) ).toString(); mSize.setWidth( map.value( QStringLiteral( "component_width" ), QString::number( DEFAULT_COMPONENT_WIDTH ) ).toDouble() ); mSize.setHeight( map.value( QStringLiteral( "component_height" ), QString::number( DEFAULT_COMPONENT_HEIGHT ) ).toDouble() ); + mColor = map.value( QStringLiteral( "color" ) ).toString().isEmpty() ? QColor() : QgsSymbolLayerUtils::decodeColor( map.value( QStringLiteral( "color" ) ).toString() ); mTopEdgeLinksCollapsed = map.value( QStringLiteral( "parameters_collapsed" ) ).toBool(); mBottomEdgeLinksCollapsed = map.value( QStringLiteral( "outputs_collapsed" ) ).toBool(); if ( comment() ) diff --git a/src/core/processing/models/qgsprocessingmodelcomponent.h b/src/core/processing/models/qgsprocessingmodelcomponent.h index 5b709195d968..b2b8425145bf 100644 --- a/src/core/processing/models/qgsprocessingmodelcomponent.h +++ b/src/core/processing/models/qgsprocessingmodelcomponent.h @@ -22,6 +22,7 @@ #include "qgis.h" #include #include +#include class QgsProcessingModelComment; @@ -76,6 +77,25 @@ class CORE_EXPORT QgsProcessingModelComponent */ void setSize( QSizeF size ); + /** + * Returns the color of the model component within the graphical modeler. + * + * An invalid color indicates that the default color for the component should be used. + * + * \see setColor() + * \since QGIS 3.14 + */ + QColor color() const; + + /** + * Sets the \a color of the model component within the graphical modeler. An invalid \a color + * indicates that the default color for the component should be used. + * + * \see color() + * \since QGIS 3.14 + */ + void setColor( const QColor &color ); + /** * Returns TRUE if the link points for the specified \a edge should be shown collapsed or not. * \see setLinksCollapsed() @@ -158,6 +178,7 @@ class CORE_EXPORT QgsProcessingModelComponent QString mDescription; QSizeF mSize = QSizeF( DEFAULT_COMPONENT_WIDTH, DEFAULT_COMPONENT_HEIGHT ); + QColor mColor; bool mTopEdgeLinksCollapsed = true; bool mBottomEdgeLinksCollapsed = true; diff --git a/src/core/processing/models/qgsprocessingmodelgroupbox.cpp b/src/core/processing/models/qgsprocessingmodelgroupbox.cpp new file mode 100644 index 000000000000..dd4bf7428aff --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelgroupbox.cpp @@ -0,0 +1,55 @@ +/*************************************************************************** + qgsprocessingmodelgroupbox.cpp + -------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsprocessingmodelgroupbox.h" + +///@cond NOT_STABLE + +QgsProcessingModelGroupBox::QgsProcessingModelGroupBox( const QString &description ) + : QgsProcessingModelComponent( description ) + , mUuid( QUuid::createUuid().toString() ) +{ + setSize( QSizeF( 400, 360 ) ); +} + +QgsProcessingModelGroupBox *QgsProcessingModelGroupBox::clone() const +{ + return new QgsProcessingModelGroupBox( *this ); +} + +QVariant QgsProcessingModelGroupBox::toVariant() const +{ + QVariantMap map; + map.insert( QStringLiteral( "uuid" ), mUuid ); + saveCommonProperties( map ); + return map; +} + +bool QgsProcessingModelGroupBox::loadVariant( const QVariantMap &map ) +{ + restoreCommonProperties( map ); + mUuid = map.value( QStringLiteral( "uuid" ) ).toString(); + return true; +} + +QString QgsProcessingModelGroupBox::uuid() const +{ + return mUuid; +} + + +///@endcond diff --git a/src/core/processing/models/qgsprocessingmodelgroupbox.h b/src/core/processing/models/qgsprocessingmodelgroupbox.h new file mode 100644 index 000000000000..a5d194e2555e --- /dev/null +++ b/src/core/processing/models/qgsprocessingmodelgroupbox.h @@ -0,0 +1,68 @@ +/*************************************************************************** + qgsprocessingmodelgroupbox.h + -------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSPROCESSINGMODELGROUPBOX_H +#define QGSPROCESSINGMODELGROUPBOX_H + +#include "qgis_core.h" +#include "qgis.h" +#include "qgsprocessingmodelcomponent.h" +#include "qgsprocessingparameters.h" + +///@cond NOT_STABLE + +/** + * Represents a group box in a model. + * \ingroup core + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsProcessingModelGroupBox : public QgsProcessingModelComponent +{ + public: + + /** + * Constructor for QgsProcessingModelGroupBox with the specified \a description. + */ + QgsProcessingModelGroupBox( const QString &description = QString() ); + + QgsProcessingModelGroupBox *clone() const override SIP_FACTORY; + + /** + * Saves this group box to a QVariant. + * \see loadVariant() + */ + QVariant toVariant() const; + + /** + * Loads this group box from a QVariantMap. + * \see toVariant() + */ + bool loadVariant( const QVariantMap &map ); + + /** + * Returns the unique ID associated with this group box. + */ + QString uuid() const; + + private: + + QString mUuid; +}; + +///@endcond + +#endif // QGSPROCESSINGMODELGROUPBOX_H diff --git a/src/core/processing/qgsprocessingalgorithm.cpp b/src/core/processing/qgsprocessingalgorithm.cpp index cfdcb44fe9f8..228e7f16b6bc 100644 --- a/src/core/processing/qgsprocessingalgorithm.cpp +++ b/src/core/processing/qgsprocessingalgorithm.cpp @@ -418,6 +418,11 @@ bool QgsProcessingAlgorithm::hasHtmlOutputs() const return false; } +QgsProcessingAlgorithm::VectorProperties QgsProcessingAlgorithm::sinkProperties( const QString &, const QVariantMap &, QgsProcessingContext &, const QMap & ) const +{ + return VectorProperties(); +} + QVariantMap QgsProcessingAlgorithm::run( const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingFeedback *feedback, bool *ok, const QVariantMap &configuration ) const { std::unique_ptr< QgsProcessingAlgorithm > alg( create( configuration ) ); @@ -599,6 +604,9 @@ bool QgsProcessingAlgorithm::parameterAsBoolean( const QVariantMap ¶meters, QgsFeatureSink *QgsProcessingAlgorithm::parameterAsSink( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context, QString &destinationIdentifier, const QgsFields &fields, QgsWkbTypes::Type geometryType, const QgsCoordinateReferenceSystem &crs, QgsFeatureSink::SinkFlags sinkFlags ) const { + if ( !parameterDefinition( name ) ) + throw QgsProcessingException( QObject::tr( "No parameter definition for the sink '%1'" ).arg( name ) ); + return QgsProcessingParameters::parameterAsSink( parameterDefinition( name ), parameters, fields, geometryType, crs, context, destinationIdentifier, sinkFlags ); } @@ -732,6 +740,16 @@ QDateTime QgsProcessingAlgorithm::parameterAsDateTime( const QVariantMap ¶me return QgsProcessingParameters::parameterAsDateTime( parameterDefinition( name ), parameters, context ); } +QString QgsProcessingAlgorithm::parameterAsSchema( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ) +{ + return QgsProcessingParameters::parameterAsSchema( parameterDefinition( name ), parameters, context ); +} + +QString QgsProcessingAlgorithm::parameterAsDatabaseTableName( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ) +{ + return QgsProcessingParameters::parameterAsDatabaseTableName( parameterDefinition( name ), parameters, context ); +} + QString QgsProcessingAlgorithm::invalidSourceError( const QVariantMap ¶meters, const QString &name ) { if ( !parameters.contains( name ) ) @@ -857,7 +875,7 @@ void QgsProcessingFeatureBasedAlgorithm::initAlgorithm( const QVariantMap &confi { addParameter( new QgsProcessingParameterFeatureSource( inputParameterName(), inputParameterDescription(), inputLayerTypes() ) ); initParameters( config ); - addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), outputName(), outputLayerType() ) ); + addParameter( new QgsProcessingParameterFeatureSink( QStringLiteral( "OUTPUT" ), outputName(), outputLayerType(), QVariant(), false, true, true ) ); } QString QgsProcessingFeatureBasedAlgorithm::inputParameterName() const @@ -1013,3 +1031,34 @@ void QgsProcessingFeatureBasedAlgorithm::prepareSource( const QVariantMap ¶m } } + +QgsProcessingAlgorithm::VectorProperties QgsProcessingFeatureBasedAlgorithm::sinkProperties( const QString &sink, const QVariantMap ¶meters, QgsProcessingContext &context, const QMap &sourceProperties ) const +{ + QgsProcessingAlgorithm::VectorProperties result; + if ( sink == QStringLiteral( "OUTPUT" ) ) + { + if ( sourceProperties.value( QStringLiteral( "INPUT" ) ).availability == QgsProcessingAlgorithm::Available ) + { + const VectorProperties inputProps = sourceProperties.value( QStringLiteral( "INPUT" ) ); + result.fields = outputFields( inputProps.fields ); + result.crs = outputCrs( inputProps.crs ); + result.wkbType = outputWkbType( inputProps.wkbType ); + result.availability = Available; + return result; + } + else + { + std::unique_ptr< QgsProcessingFeatureSource > source( parameterAsSource( parameters, QStringLiteral( "INPUT" ), context ) ); + if ( source ) + { + result.fields = outputFields( source->fields() ); + result.crs = outputCrs( source->sourceCrs() ); + result.wkbType = outputWkbType( source->wkbType() ); + result.availability = Available; + return result; + } + } + } + return result; +} + diff --git a/src/core/processing/qgsprocessingalgorithm.h b/src/core/processing/qgsprocessingalgorithm.h index 334ae95b8c2c..aa2f8d5676a6 100644 --- a/src/core/processing/qgsprocessingalgorithm.h +++ b/src/core/processing/qgsprocessingalgorithm.h @@ -77,6 +77,8 @@ class CORE_EXPORT QgsProcessingAlgorithm FlagSupportsInPlaceEdits = 1 << 8, //!< Algorithm supports in-place editing FlagKnownIssues = 1 << 9, //!< Algorithm has known issues FlagCustomException = 1 << 10, //!< Algorithm raises custom exception notices, don't use the standard ones + FlagPruneModelBranchesBasedOnAlgorithmResults = 1 << 11, //!< Algorithm results will cause remaining model branches to be pruned based on the results of running the algorithm + FlagSkipGenericModelLogging = 1 << 12, //!< When running as part of a model, the generic algorithm setup and results logging should be skipped FlagDeprecated = FlagHideFromToolbox | FlagHideFromModeler, //!< Algorithm is deprecated }; Q_DECLARE_FLAGS( Flags, Flag ) @@ -313,6 +315,56 @@ class CORE_EXPORT QgsProcessingAlgorithm */ bool hasHtmlOutputs() const; + /** + * Property availability, used for QgsProcessingAlgorithm::VectorProperties + * in order to determine if properties are available or not + */ + enum PropertyAvailability + { + NotAvailable, //!< Properties are not available + Available, //!< Properties are available + }; + + /** + * Properties of a vector source or sink used in an algorithm. + * + * \since QGIS 3.14 + */ + struct VectorProperties + { + //! Fields + QgsFields fields; + + //! Geometry (WKB) type + QgsWkbTypes::Type wkbType = QgsWkbTypes::Unknown; + + //! Coordinate Reference System + QgsCoordinateReferenceSystem crs; + + //! Availability of the properties. By default properties are not available. + QgsProcessingAlgorithm::PropertyAvailability availability = QgsProcessingAlgorithm::NotAvailable; + }; + + /** + * Returns the vector properties which will be used for the \a sink with matching name. + * + * The \a parameters argument specifies the values of all parameters which would be used to generate + * the sink. These can be used alongside the provided \a context in order to pre-evaluate inputs + * when required in order to determine the sink's properties. + * + * The \a sourceProperties map will contain the vector properties of the various sources used + * as inputs to the algorithm. These will only be available in certain circumstances (e.g. when the + * algorithm is used within a model), so implementations will need to be adaptable to circumstances + * when either \a sourceParameters is empty or \a parameters is empty, and use whatever information + * is passed in order to make a best guess determination of the output properties. + * + * \since QGIS 3.14 + */ + virtual QgsProcessingAlgorithm::VectorProperties sinkProperties( const QString &sink, + const QVariantMap ¶meters, + QgsProcessingContext &context, + const QMap< QString, QgsProcessingAlgorithm::VectorProperties > &sourceProperties ) const; + /** * Executes the algorithm using the specified \a parameters. This method internally * creates a copy of the algorithm before running it, so it is safe to call @@ -628,9 +680,11 @@ class CORE_EXPORT QgsProcessingAlgorithm * to the sink, e.g. via calling QgsProcessingUtils::mapLayerFromString(). * * This function creates a new object and the caller takes responsibility for deleting the returned object. + * + * \throws QgsProcessingException */ QgsFeatureSink *parameterAsSink( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context, QString &destinationIdentifier SIP_OUT, - const QgsFields &fields, QgsWkbTypes::Type geometryType = QgsWkbTypes::NoGeometry, const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem(), QgsFeatureSink::SinkFlags sinkFlags = nullptr ) const SIP_FACTORY; + const QgsFields &fields, QgsWkbTypes::Type geometryType = QgsWkbTypes::NoGeometry, const QgsCoordinateReferenceSystem &crs = QgsCoordinateReferenceSystem(), QgsFeatureSink::SinkFlags sinkFlags = nullptr ) const SIP_THROW( QgsProcessingException ) SIP_FACTORY; /** * Evaluates the parameter with matching \a name to a feature source. @@ -862,6 +916,20 @@ class CORE_EXPORT QgsProcessingAlgorithm */ QString parameterAsConnectionName( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ); + /** + * Evaluates the parameter with matching \a name to a database schema name string. + * + * \since QGIS 3.14 + */ + QString parameterAsSchema( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ); + + /** + * Evaluates the parameter with matching \a name to a database table name string. + * + * \since QGIS 3.14 + */ + QString parameterAsDatabaseTableName( const QVariantMap ¶meters, const QString &name, QgsProcessingContext &context ); + /** * Evaluates the parameter with matching \a name to a DateTime, or returns an invalid date time if the parameter was not set. * @@ -1131,6 +1199,11 @@ class CORE_EXPORT QgsProcessingFeatureBasedAlgorithm : public QgsProcessingAlgor */ void prepareSource( const QVariantMap ¶meters, QgsProcessingContext &context ); + QgsProcessingAlgorithm::VectorProperties sinkProperties( const QString &sink, + const QVariantMap ¶meters, + QgsProcessingContext &context, + const QMap< QString, QgsProcessingAlgorithm::VectorProperties > &sourceProperties ) const override; + private: std::unique_ptr< QgsProcessingFeatureSource > mSource; diff --git a/src/core/processing/qgsprocessingcontext.cpp b/src/core/processing/qgsprocessingcontext.cpp index a6adb6b1459c..8fe4af0fe50c 100644 --- a/src/core/processing/qgsprocessingcontext.cpp +++ b/src/core/processing/qgsprocessingcontext.cpp @@ -61,8 +61,12 @@ void QgsProcessingContext::addLayerToLoadOnCompletion( const QString &layer, con void QgsProcessingContext::setInvalidGeometryCheck( QgsFeatureRequest::InvalidGeometryCheck check ) { mInvalidGeometryCheck = check; + mInvalidGeometryCallback = defaultInvalidGeometryCallbackForCheck( check ); +} - switch ( mInvalidGeometryCheck ) +std::function QgsProcessingContext::defaultInvalidGeometryCallbackForCheck( QgsFeatureRequest::InvalidGeometryCheck check ) const +{ + switch ( check ) { case QgsFeatureRequest::GeometryAbortOnInvalid: { @@ -70,8 +74,7 @@ void QgsProcessingContext::setInvalidGeometryCheck( QgsFeatureRequest::InvalidGe { throw QgsProcessingException( QObject::tr( "Feature (%1) has invalid geometry. Please fix the geometry or change the Processing setting to the \"Ignore invalid input features\" option." ).arg( feature.id() ) ); }; - mInvalidGeometryCallback = callback; - break; + return callback; } case QgsFeatureRequest::GeometrySkipInvalid: @@ -81,13 +84,13 @@ void QgsProcessingContext::setInvalidGeometryCheck( QgsFeatureRequest::InvalidGe if ( mFeedback ) mFeedback->reportError( QObject::tr( "Feature (%1) has invalid geometry and has been skipped. Please fix the geometry or change the Processing setting to the \"Ignore invalid input features\" option." ).arg( feature.id() ) ); }; - mInvalidGeometryCallback = callback; - break; + return callback; } - default: - break; + case QgsFeatureRequest::GeometryNoCheck: + return nullptr; } + return nullptr; } void QgsProcessingContext::takeResultsFrom( QgsProcessingContext &context ) diff --git a/src/core/processing/qgsprocessingcontext.h b/src/core/processing/qgsprocessingcontext.h index d59112ec53fa..58c53b97f11f 100644 --- a/src/core/processing/qgsprocessingcontext.h +++ b/src/core/processing/qgsprocessingcontext.h @@ -337,6 +337,13 @@ class CORE_EXPORT QgsProcessingContext */ SIP_SKIP std::function< void( const QgsFeature & ) > invalidGeometryCallback() const { return mInvalidGeometryCallback; } + /** + * Returns the default callback function to use for a particular invalid geometry \a check + * \note not available in Python bindings + * \since QGIS 3.14 + */ + SIP_SKIP std::function< void( const QgsFeature & ) > defaultInvalidGeometryCallbackForCheck( QgsFeatureRequest::InvalidGeometryCheck check ) const; + /** * Sets a callback function to use when encountering a transform error when iterating * features. This function will be diff --git a/src/core/processing/qgsprocessingfeedback.cpp b/src/core/processing/qgsprocessingfeedback.cpp index 389520424e70..de92034864ca 100644 --- a/src/core/processing/qgsprocessingfeedback.cpp +++ b/src/core/processing/qgsprocessingfeedback.cpp @@ -26,33 +26,59 @@ #include #endif +QgsProcessingFeedback::QgsProcessingFeedback( bool logFeedback ) + : mLogFeedback( logFeedback ) +{ + +} + void QgsProcessingFeedback::setProgressText( const QString & ) { } void QgsProcessingFeedback::reportError( const QString &error, bool ) { - QgsMessageLog::logMessage( error, tr( "Processing" ), Qgis::Critical ); + if ( mLogFeedback ) + QgsMessageLog::logMessage( error, tr( "Processing" ), Qgis::Critical ); + + mHtmlLog.append( QStringLiteral( "%1
    " ).arg( error.toHtmlEscaped() ).replace( '\n', QStringLiteral( "
    " ) ) ); + mTextLog.append( error + '\n' ); } void QgsProcessingFeedback::pushInfo( const QString &info ) { - QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + if ( mLogFeedback ) + QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + + mHtmlLog.append( info.toHtmlEscaped().replace( '\n', QStringLiteral( "
    " ) ) + QStringLiteral( "
    " ) ); + mTextLog.append( info + '\n' ); } void QgsProcessingFeedback::pushCommandInfo( const QString &info ) { - QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + if ( mLogFeedback ) + QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + + mHtmlLog.append( QStringLiteral( "%1
    " ).arg( info.toHtmlEscaped().replace( '\n', QStringLiteral( "
    " ) ) ) ); + mTextLog.append( info + '\n' ); } void QgsProcessingFeedback::pushDebugInfo( const QString &info ) { - QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + if ( mLogFeedback ) + QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + + mHtmlLog.append( QStringLiteral( "%1
    " ).arg( info.toHtmlEscaped().replace( '\n', QStringLiteral( "
    " ) ) ) ); + mTextLog.append( info + '\n' ); } void QgsProcessingFeedback::pushConsoleInfo( const QString &info ) { - QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + if ( mLogFeedback ) + QgsMessageLog::logMessage( info, tr( "Processing" ), Qgis::Info ); + + mHtmlLog.append( QStringLiteral( "%1
    " ).arg( info.toHtmlEscaped().replace( '\n', QStringLiteral( "
    " ) ) ) ); + mTextLog.append( info + '\n' ); } void QgsProcessingFeedback::pushVersionInfo( const QgsProcessingProvider *provider ) @@ -78,6 +104,16 @@ void QgsProcessingFeedback::pushVersionInfo( const QgsProcessingProvider *provid } } +QString QgsProcessingFeedback::htmlLog() const +{ + return mHtmlLog; +} + +QString QgsProcessingFeedback::textLog() const +{ + return mTextLog; +} + QgsProcessingMultiStepFeedback::QgsProcessingMultiStepFeedback( int childAlgorithmCount, QgsProcessingFeedback *feedback ) : mChildSteps( childAlgorithmCount ) @@ -123,6 +159,16 @@ void QgsProcessingMultiStepFeedback::pushConsoleInfo( const QString &info ) mFeedback->pushConsoleInfo( info ); } +QString QgsProcessingMultiStepFeedback::htmlLog() const +{ + return mFeedback->htmlLog(); +} + +QString QgsProcessingMultiStepFeedback::textLog() const +{ + return mFeedback->textLog(); +} + void QgsProcessingMultiStepFeedback::updateOverallProgress( double progress ) { double baseProgress = 100.0 * static_cast< double >( mCurrentStep ) / mChildSteps; diff --git a/src/core/processing/qgsprocessingfeedback.h b/src/core/processing/qgsprocessingfeedback.h index 1bd3f0af1023..40d67f91de57 100644 --- a/src/core/processing/qgsprocessingfeedback.h +++ b/src/core/processing/qgsprocessingfeedback.h @@ -40,6 +40,14 @@ class CORE_EXPORT QgsProcessingFeedback : public QgsFeedback public: + /** + * Constructor for QgsProcessingFeedback. + * + * If \a logFeedback is TRUE, then all feedback received will be directed + * to QgsMessageLog. + */ + QgsProcessingFeedback( bool logFeedback = true ); + /** * Sets a progress report text string. This can be used in conjunction with * setProgress() to provide detailed progress reports, such as "Transformed @@ -99,6 +107,27 @@ class CORE_EXPORT QgsProcessingFeedback : public QgsFeedback */ void pushVersionInfo( const QgsProcessingProvider *provider = nullptr ); + /** + * Returns the HTML formatted contents of the log, which contains all messages pushed to the feedback object. + * + * \see textLog() + * \since QGIS 3.14 + */ + virtual QString htmlLog() const; + + /** + * Returns the plain text contents of the log, which contains all messages pushed to the feedback object. + * + * \see htmlLog() + * \since QGIS 3.14 + */ + virtual QString textLog() const; + + private: + bool mLogFeedback = true; + QString mHtmlLog; + QString mTextLog; + }; @@ -139,7 +168,8 @@ class CORE_EXPORT QgsProcessingMultiStepFeedback : public QgsProcessingFeedback void pushCommandInfo( const QString &info ) override; void pushDebugInfo( const QString &info ) override; void pushConsoleInfo( const QString &info ) override; - + QString htmlLog() const override; + QString textLog() const override; private slots: void updateOverallProgress( double progress ); diff --git a/src/core/processing/qgsprocessingparameters.cpp b/src/core/processing/qgsprocessingparameters.cpp index 52ab96d63c9d..6ef7fc5815de 100644 --- a/src/core/processing/qgsprocessingparameters.cpp +++ b/src/core/processing/qgsprocessingparameters.cpp @@ -35,14 +35,49 @@ #include "qgsprintlayout.h" #include "qgssymbollayerutils.h" #include "qgsfileutils.h" +#include "qgsproviderregistry.h" #include +QVariant QgsProcessingFeatureSourceDefinition::toVariant() const +{ + QVariantMap map; + map.insert( QStringLiteral( "source" ), source.toVariant() ); + map.insert( QStringLiteral( "selected_only" ), selectedFeaturesOnly ); + map.insert( QStringLiteral( "feature_limit" ), featureLimit ); + map.insert( QStringLiteral( "flags" ), static_cast< int >( flags ) ); + map.insert( QStringLiteral( "geometry_check" ), static_cast< int >( geometryCheck ) ); + return map; +} + +bool QgsProcessingFeatureSourceDefinition::loadVariant( const QVariantMap &map ) +{ + source.loadVariant( map.value( QStringLiteral( "source" ) ) ); + selectedFeaturesOnly = map.value( QStringLiteral( "selected_only" ), false ).toBool(); + featureLimit = map.value( QStringLiteral( "feature_limit" ), -1 ).toLongLong(); + flags = static_cast< Flags >( map.value( QStringLiteral( "flags" ), 0 ).toInt() ); + geometryCheck = static_cast< QgsFeatureRequest::InvalidGeometryCheck >( map.value( QStringLiteral( "geometry_check" ), QgsFeatureRequest::GeometryAbortOnInvalid ).toInt() ); + return true; +} + + +// +// QgsProcessingOutputLayerDefinition +// + +void QgsProcessingOutputLayerDefinition::setRemappingDefinition( const QgsRemappingSinkDefinition &definition ) +{ + mUseRemapping = true; + mRemappingDefinition = definition; +} + QVariant QgsProcessingOutputLayerDefinition::toVariant() const { QVariantMap map; map.insert( QStringLiteral( "sink" ), sink.toVariant() ); map.insert( QStringLiteral( "create_options" ), createOptions ); + if ( mUseRemapping ) + map.insert( QStringLiteral( "remapping" ), QVariant::fromValue( mRemappingDefinition ) ); return map; } @@ -50,9 +85,29 @@ bool QgsProcessingOutputLayerDefinition::loadVariant( const QVariantMap &map ) { sink.loadVariant( map.value( QStringLiteral( "sink" ) ) ); createOptions = map.value( QStringLiteral( "create_options" ) ).toMap(); + if ( map.contains( QStringLiteral( "remapping" ) ) ) + { + mUseRemapping = true; + mRemappingDefinition = map.value( QStringLiteral( "remapping" ) ).value< QgsRemappingSinkDefinition >(); + } + else + { + mUseRemapping = false; + } return true; } +bool QgsProcessingOutputLayerDefinition::operator==( const QgsProcessingOutputLayerDefinition &other ) const +{ + return sink == other.sink && destinationProject == other.destinationProject && destinationName == other.destinationName && createOptions == other.createOptions + && mUseRemapping == other.mUseRemapping && mRemappingDefinition == other.mRemappingDefinition; +} + +bool QgsProcessingOutputLayerDefinition::operator!=( const QgsProcessingOutputLayerDefinition &other ) const +{ + return !( *this == other ); +} + bool QgsProcessingParameters::isDynamic( const QVariantMap ¶meters, const QString &name ) { QVariant val = parameters.value( name ); @@ -524,6 +579,8 @@ QgsFeatureSink *QgsProcessingParameters::parameterAsSink( const QgsProcessingPar QgsProject *destinationProject = nullptr; QString destName; QVariantMap createOptions; + QgsRemappingSinkDefinition remapDefinition; + bool useRemapDefinition = false; if ( val.canConvert() ) { // input is a QgsProcessingOutputLayerDefinition - get extra properties from it @@ -533,6 +590,11 @@ QgsFeatureSink *QgsProcessingParameters::parameterAsSink( const QgsProcessingPar val = fromVar.sink; destName = fromVar.destinationName; + if ( fromVar.useRemapping() ) + { + useRemapDefinition = true; + remapDefinition = fromVar.remappingDefinition(); + } } QString dest; @@ -548,6 +610,10 @@ QgsFeatureSink *QgsProcessingParameters::parameterAsSink( const QgsProcessingPar return nullptr; } // fall back to default + if ( !definition ) + { + throw QgsProcessingException( QObject::tr( "No parameter definition for the sink" ) ); + } dest = definition->defaultValue().toString(); } else @@ -563,7 +629,7 @@ QgsFeatureSink *QgsProcessingParameters::parameterAsSink( const QgsProcessingPar if ( dest.isEmpty() ) return nullptr; - std::unique_ptr< QgsFeatureSink > sink( QgsProcessingUtils::createFeatureSink( dest, context, fields, geometryType, crs, createOptions, sinkFlags ) ); + std::unique_ptr< QgsFeatureSink > sink( QgsProcessingUtils::createFeatureSink( dest, context, fields, geometryType, crs, createOptions, sinkFlags, useRemapDefinition ? &remapDefinition : nullptr ) ); destinationIdentifier = dest; if ( destinationProject ) @@ -605,11 +671,13 @@ QString parameterAsCompatibleSourceLayerPathInternal( const QgsProcessingParamet QVariant val = parameters.value( definition->name() ); bool selectedFeaturesOnly = false; + long long featureLimit = -1; if ( val.canConvert() ) { // input is a QgsProcessingFeatureSourceDefinition - get extra properties from it QgsProcessingFeatureSourceDefinition fromVar = qvariant_cast( val ); selectedFeaturesOnly = fromVar.selectedFeaturesOnly; + featureLimit = fromVar.featureLimit; val = fromVar.source; } else if ( val.canConvert() ) @@ -663,10 +731,10 @@ QString parameterAsCompatibleSourceLayerPathInternal( const QgsProcessingParamet if ( layerName ) return QgsProcessingUtils::convertToCompatibleFormatAndLayerName( vl, selectedFeaturesOnly, definition->name(), - compatibleFormats, preferredFormat, context, feedback, *layerName ); + compatibleFormats, preferredFormat, context, feedback, *layerName, featureLimit ); else return QgsProcessingUtils::convertToCompatibleFormat( vl, selectedFeaturesOnly, definition->name(), - compatibleFormats, preferredFormat, context, feedback ); + compatibleFormats, preferredFormat, context, feedback, featureLimit ); } QString QgsProcessingParameters::parameterAsCompatibleSourceLayerPath( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingFeedback *feedback ) @@ -687,15 +755,15 @@ QString QgsProcessingParameters::parameterAsCompatibleSourceLayerPathAndLayerNam } -QgsMapLayer *QgsProcessingParameters::parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) +QgsMapLayer *QgsProcessingParameters::parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingUtils::LayerHint layerHint ) { if ( !definition ) return nullptr; - return parameterAsLayer( definition, parameters.value( definition->name() ), context ); + return parameterAsLayer( definition, parameters.value( definition->name() ), context, layerHint ); } -QgsMapLayer *QgsProcessingParameters::parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ) +QgsMapLayer *QgsProcessingParameters::parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context, QgsProcessingUtils::LayerHint layerHint ) { if ( !definition ) return nullptr; @@ -741,27 +809,27 @@ QgsMapLayer *QgsProcessingParameters::parameterAsLayer( const QgsProcessingParam if ( layerRef.isEmpty() ) return nullptr; - return QgsProcessingUtils::mapLayerFromString( layerRef, context ); + return QgsProcessingUtils::mapLayerFromString( layerRef, context, true, layerHint ); } QgsRasterLayer *QgsProcessingParameters::parameterAsRasterLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) { - return qobject_cast< QgsRasterLayer *>( parameterAsLayer( definition, parameters, context ) ); + return qobject_cast< QgsRasterLayer *>( parameterAsLayer( definition, parameters, context, QgsProcessingUtils::LayerHint::Raster ) ); } QgsRasterLayer *QgsProcessingParameters::parameterAsRasterLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ) { - return qobject_cast< QgsRasterLayer *>( parameterAsLayer( definition, value, context ) ); + return qobject_cast< QgsRasterLayer *>( parameterAsLayer( definition, value, context, QgsProcessingUtils::LayerHint::Raster ) ); } QgsMeshLayer *QgsProcessingParameters::parameterAsMeshLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) { - return qobject_cast< QgsMeshLayer *>( parameterAsLayer( definition, parameters, context ) ); + return qobject_cast< QgsMeshLayer *>( parameterAsLayer( definition, parameters, context, QgsProcessingUtils::LayerHint::Mesh ) ); } QgsMeshLayer *QgsProcessingParameters::parameterAsMeshLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ) { - return qobject_cast< QgsMeshLayer *>( parameterAsLayer( definition, value, context ) ); + return qobject_cast< QgsMeshLayer *>( parameterAsLayer( definition, value, context, QgsProcessingUtils::LayerHint::Mesh ) ); } QString QgsProcessingParameters::parameterAsOutputLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) @@ -779,14 +847,12 @@ QString QgsProcessingParameters::parameterAsOutputLayer( const QgsProcessingPara QVariant val = value; QgsProject *destinationProject = nullptr; - QVariantMap createOptions; QString destName; if ( val.canConvert() ) { // input is a QgsProcessingOutputLayerDefinition - get extra properties from it QgsProcessingOutputLayerDefinition fromVar = qvariant_cast( val ); destinationProject = fromVar.destinationProject; - createOptions = fromVar.createOptions; val = fromVar.sink; destName = fromVar.destinationName; } @@ -878,12 +944,12 @@ QString QgsProcessingParameters::parameterAsFileOutput( const QgsProcessingParam QgsVectorLayer *QgsProcessingParameters::parameterAsVectorLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) { - return qobject_cast< QgsVectorLayer *>( parameterAsLayer( definition, parameters, context ) ); + return qobject_cast< QgsVectorLayer *>( parameterAsLayer( definition, parameters, context, QgsProcessingUtils::LayerHint::Vector ) ); } QgsVectorLayer *QgsProcessingParameters::parameterAsVectorLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ) { - return qobject_cast< QgsVectorLayer *>( parameterAsLayer( definition, value, context ) ); + return qobject_cast< QgsVectorLayer *>( parameterAsLayer( definition, value, context, QgsProcessingUtils::LayerHint::Vector ) ); } QgsCoordinateReferenceSystem QgsProcessingParameters::parameterAsCrs( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) @@ -922,6 +988,12 @@ QgsRectangle QgsProcessingParameters::parameterAsExtent( const QgsProcessingPara { return val.value(); } + if ( val.canConvert< QgsGeometry >() ) + { + const QgsGeometry geom = val.value(); + if ( !geom.isNull() ) + return geom.boundingBox(); + } if ( val.canConvert< QgsReferencedRectangle >() ) { QgsReferencedRectangle rr = val.value(); @@ -1147,7 +1219,12 @@ QgsGeometry QgsProcessingParameters::parameterAsExtentGeometry( const QgsProcess QgsCoordinateReferenceSystem QgsProcessingParameters::parameterAsExtentCrs( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ) { QVariant val = parameters.value( definition->name() ); + return parameterAsExtentCrs( definition, val, context ); +} +QgsCoordinateReferenceSystem QgsProcessingParameters::parameterAsExtentCrs( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ) +{ + QVariant val = value; if ( val.canConvert< QgsReferencedRectangle >() ) { QgsReferencedRectangle rr = val.value(); @@ -1785,6 +1862,36 @@ QString QgsProcessingParameters::parameterAsConnectionName( const QgsProcessingP return parameterAsString( definition, value, context ); } +QString QgsProcessingParameters::parameterAsSchema( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, const QgsProcessingContext &context ) +{ + if ( !definition ) + return QString(); + + return parameterAsSchema( definition, parameters.value( definition->name() ), context ); +} + +QString QgsProcessingParameters::parameterAsSchema( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ) +{ + // for now it's just treated identical to strings, but in future we may want flexibility to amend this (e.g. if we want to embed connection details into the schema + // parameter values, such as via a delimiter separated string) + return parameterAsString( definition, value, context ); +} + +QString QgsProcessingParameters::parameterAsDatabaseTableName( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, const QgsProcessingContext &context ) +{ + if ( !definition ) + return QString(); + + return parameterAsDatabaseTableName( definition, parameters.value( definition->name() ), context ); +} + +QString QgsProcessingParameters::parameterAsDatabaseTableName( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ) +{ + // for now it's just treated identical to strings, but in future we may want flexibility to amend this (e.g. if we want to embed connection details into the table name + // parameter values, such as via a delimiter separated string) + return parameterAsString( definition, value, context ); +} + QgsProcessingParameterDefinition *QgsProcessingParameters::parameterFromVariantMap( const QVariantMap &map ) { QString type = map.value( QStringLiteral( "parameter_type" ) ).toString(); @@ -1953,6 +2060,10 @@ QgsProcessingParameterDefinition *QgsProcessingParameters::parameterFromScriptCo return QgsProcessingParameterDateTime::fromScriptCode( name, description, isOptional, definition ); else if ( type == QStringLiteral( "providerconnection" ) ) return QgsProcessingParameterProviderConnection::fromScriptCode( name, description, isOptional, definition ); + else if ( type == QStringLiteral( "databaseschema" ) ) + return QgsProcessingParameterDatabaseSchema::fromScriptCode( name, description, isOptional, definition ); + else if ( type == QStringLiteral( "databasetable" ) ) + return QgsProcessingParameterDatabaseTable::fromScriptCode( name, description, isOptional, definition ); return nullptr; } @@ -2208,8 +2319,9 @@ QgsProcessingParameterCrs *QgsProcessingParameterCrs::fromScriptCode( const QStr return new QgsProcessingParameterCrs( name, description, definition.compare( QLatin1String( "none" ), Qt::CaseInsensitive ) == 0 ? QVariant() : definition, isOptional ); } -QgsProcessingParameterMapLayer::QgsProcessingParameterMapLayer( const QString &name, const QString &description, const QVariant &defaultValue, bool optional ) +QgsProcessingParameterMapLayer::QgsProcessingParameterMapLayer( const QString &name, const QString &description, const QVariant &defaultValue, bool optional, const QList &types ) : QgsProcessingParameterDefinition( name, description, defaultValue, optional ) + , QgsProcessingParameterLimitedDataTypes( types ) { } @@ -2265,9 +2377,175 @@ QString QgsProcessingParameterMapLayer::valueAsPythonString( const QVariant &val : QgsProcessingUtils::stringToPythonLiteral( val.toString() ); } +QString createAllMapLayerFileFilter() +{ + QStringList vectors = QgsProviderRegistry::instance()->fileVectorFilters().split( QStringLiteral( ";;" ) ); + QStringList rasters = QgsProviderRegistry::instance()->fileRasterFilters().split( QStringLiteral( ";;" ) ); + for ( const QString &raster : rasters ) + { + if ( !vectors.contains( raster ) ) + vectors << raster; + } + QStringList meshFilters = QgsProviderRegistry::instance()->fileMeshFilters().split( QStringLiteral( ";;" ) ); + for ( const QString &mesh : meshFilters ) + { + if ( !vectors.contains( mesh ) ) + vectors << mesh; + } + vectors.removeAll( QObject::tr( "All files (*.*)" ) ); + std::sort( vectors.begin(), vectors.end() ); + + return QObject::tr( "All files (*.*)" ) + QStringLiteral( ";;" ) + vectors.join( QStringLiteral( ";;" ) ); +} + +QString QgsProcessingParameterMapLayer::createFileFilter() const +{ + return createAllMapLayerFileFilter(); +} + +QString QgsProcessingParameterMapLayer::asScriptCode() const +{ + QString code = QStringLiteral( "##%1=" ).arg( mName ); + if ( mFlags & FlagOptional ) + code += QStringLiteral( "optional " ); + code += QStringLiteral( "layer " ); + + for ( int type : mDataTypes ) + { + switch ( type ) + { + case QgsProcessing::TypeVectorAnyGeometry: + code += QStringLiteral( "hasgeometry " ); + break; + + case QgsProcessing::TypeVectorPoint: + code += QStringLiteral( "point " ); + break; + + case QgsProcessing::TypeVectorLine: + code += QStringLiteral( "line " ); + break; + + case QgsProcessing::TypeVectorPolygon: + code += QStringLiteral( "polygon " ); + break; + + case QgsProcessing::TypeRaster: + code += QStringLiteral( "raster " ); + break; + + case QgsProcessing::TypeMesh: + code += QStringLiteral( "mesh " ); + break; + } + } + + code += mDefault.toString(); + return code.trimmed(); +} + QgsProcessingParameterMapLayer *QgsProcessingParameterMapLayer::fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) { - return new QgsProcessingParameterMapLayer( name, description, definition, isOptional ); + QList< int > types; + QString def = definition; + while ( true ) + { + if ( def.startsWith( QLatin1String( "hasgeometry" ), Qt::CaseInsensitive ) ) + { + types << QgsProcessing::TypeVectorAnyGeometry; + def = def.mid( 12 ); + continue; + } + else if ( def.startsWith( QLatin1String( "point" ), Qt::CaseInsensitive ) ) + { + types << QgsProcessing::TypeVectorPoint; + def = def.mid( 6 ); + continue; + } + else if ( def.startsWith( QLatin1String( "line" ), Qt::CaseInsensitive ) ) + { + types << QgsProcessing::TypeVectorLine; + def = def.mid( 5 ); + continue; + } + else if ( def.startsWith( QLatin1String( "polygon" ), Qt::CaseInsensitive ) ) + { + types << QgsProcessing::TypeVectorPolygon; + def = def.mid( 8 ); + continue; + } + else if ( def.startsWith( QLatin1String( "raster" ), Qt::CaseInsensitive ) ) + { + types << QgsProcessing::TypeRaster; + def = def.mid( 7 ); + continue; + } + else if ( def.startsWith( QLatin1String( "mesh" ), Qt::CaseInsensitive ) ) + { + types << QgsProcessing::TypeMesh; + def = def.mid( 5 ); + continue; + } + break; + } + + return new QgsProcessingParameterMapLayer( name, description, def, isOptional, types ); +} + +QString QgsProcessingParameterMapLayer::asPythonString( const QgsProcessing::PythonOutputType outputType ) const +{ + switch ( outputType ) + { + case QgsProcessing::PythonQgsProcessingAlgorithmSubclass: + { + QString code = QStringLiteral( "QgsProcessingParameterMapLayer('%1', '%2'" ).arg( name(), description() ); + if ( mFlags & FlagOptional ) + code += QStringLiteral( ", optional=True" ); + + QgsProcessingContext c; + code += QStringLiteral( ", defaultValue=%1" ).arg( valueAsPythonString( mDefault, c ) ); + + if ( !mDataTypes.empty() ) + { + QStringList options; + options.reserve( mDataTypes.size() ); + for ( int t : mDataTypes ) + options << QStringLiteral( "QgsProcessing.%1" ).arg( QgsProcessing::sourceTypeToString( static_cast< QgsProcessing::SourceType >( t ) ) ); + code += QStringLiteral( ", types=[%1])" ).arg( options.join( ',' ) ); + } + else + { + code += QStringLiteral( ")" ); + } + + return code; + } + } + return QString(); +} + +QVariantMap QgsProcessingParameterMapLayer::toVariantMap() const +{ + QVariantMap map = QgsProcessingParameterDefinition::toVariantMap(); + QVariantList types; + for ( int type : mDataTypes ) + { + types << type; + } + map.insert( QStringLiteral( "data_types" ), types ); + return map; +} + +bool QgsProcessingParameterMapLayer::fromVariantMap( const QVariantMap &map ) +{ + QgsProcessingParameterDefinition::fromVariantMap( map ); + mDataTypes.clear(); + const QVariantList values = map.value( QStringLiteral( "data_types" ) ).toList(); + for ( const QVariant &val : values ) + { + mDataTypes << val.toInt(); + } + return true; } QgsProcessingParameterExtent::QgsProcessingParameterExtent( const QString &name, const QString &description, const QVariant &defaultValue, bool optional ) @@ -2305,6 +2583,10 @@ bool QgsProcessingParameterExtent::checkValueIsAcceptable( const QVariant &input QgsRectangle r = input.value(); return !r.isNull(); } + if ( input.canConvert< QgsGeometry >() ) + { + return true; + } if ( input.canConvert< QgsReferencedRectangle >() ) { QgsReferencedRectangle r = input.value(); @@ -2360,7 +2642,7 @@ QString QgsProcessingParameterExtent::valueAsPythonString( const QVariant &value qgsDoubleToString( r.xMaximum() ), qgsDoubleToString( r.yMaximum() ) ); } - if ( value.canConvert< QgsReferencedRectangle >() ) + else if ( value.canConvert< QgsReferencedRectangle >() ) { QgsReferencedRectangle r = value.value(); return QStringLiteral( "'%1, %3, %2, %4 [%5]'" ).arg( qgsDoubleToString( r.xMinimum() ), @@ -2368,6 +2650,15 @@ QString QgsProcessingParameterExtent::valueAsPythonString( const QVariant &value qgsDoubleToString( r.xMaximum() ), qgsDoubleToString( r.yMaximum() ), r.crs().authid() ); } + else if ( value.canConvert< QgsGeometry >() ) + { + const QgsGeometry g = value.value(); + if ( !g.isNull() ) + { + const QString wkt = g.asWkt(); + return QStringLiteral( "QgsGeometry.fromWkt('%1')" ).arg( wkt ); + } + } QVariantMap p; p.insert( name(), value ); @@ -2968,6 +3259,33 @@ QString QgsProcessingParameterMultipleLayers::asPythonString( const QgsProcessin return QString(); } +QString QgsProcessingParameterMultipleLayers::createFileFilter() const +{ + QStringList exts; + switch ( mLayerType ) + { + case QgsProcessing::TypeFile: + return QObject::tr( "All files (*.*)" ); + + case QgsProcessing::TypeRaster: + return QgsProviderRegistry::instance()->fileRasterFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); + + case QgsProcessing::TypeVector: + case QgsProcessing::TypeVectorAnyGeometry: + case QgsProcessing::TypeVectorPoint: + case QgsProcessing::TypeVectorLine: + case QgsProcessing::TypeVectorPolygon: + return QgsProviderRegistry::instance()->fileVectorFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); + + case QgsProcessing::TypeMesh: + return QgsProviderRegistry::instance()->fileMeshFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); + + case QgsProcessing::TypeMapLayer: + return createAllMapLayerFileFilter(); + } + return QString(); +} + QgsProcessing::SourceType QgsProcessingParameterMultipleLayers::layerType() const { return mLayerType; @@ -3350,6 +3668,11 @@ QString QgsProcessingParameterRasterLayer::valueAsPythonString( const QVariant & : QgsProcessingUtils::stringToPythonLiteral( val.toString() ); } +QString QgsProcessingParameterRasterLayer::createFileFilter() const +{ + return QgsProviderRegistry::instance()->fileRasterFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); +} + QgsProcessingParameterRasterLayer *QgsProcessingParameterRasterLayer::fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) { return new QgsProcessingParameterRasterLayer( name, description, definition.isEmpty() ? QVariant() : definition, isOptional ); @@ -3898,6 +4221,11 @@ QString QgsProcessingParameterVectorLayer::asPythonString( const QgsProcessing:: return QString(); } +QString QgsProcessingParameterVectorLayer::createFileFilter() const +{ + return QgsProviderRegistry::instance()->fileVectorFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); +} + QList QgsProcessingParameterLimitedDataTypes::dataTypes() const { return mDataTypes; @@ -3912,8 +4240,7 @@ QVariantMap QgsProcessingParameterVectorLayer::toVariantMap() const { QVariantMap map = QgsProcessingParameterDefinition::toVariantMap(); QVariantList types; - const auto constMDataTypes = mDataTypes; - for ( int type : constMDataTypes ) + for ( int type : mDataTypes ) { types << type; } @@ -3925,9 +4252,8 @@ bool QgsProcessingParameterVectorLayer::fromVariantMap( const QVariantMap &map ) { QgsProcessingParameterDefinition::fromVariantMap( map ); mDataTypes.clear(); - QVariantList values = map.value( QStringLiteral( "data_types" ) ).toList(); - const auto constValues = values; - for ( const QVariant &val : constValues ) + const QVariantList values = map.value( QStringLiteral( "data_types" ) ).toList(); + for ( const QVariant &val : values ) { mDataTypes << val.toInt(); } @@ -4007,6 +4333,11 @@ QString QgsProcessingParameterMeshLayer::valueAsPythonString( const QVariant &va : QgsProcessingUtils::stringToPythonLiteral( val.toString() ); } +QString QgsProcessingParameterMeshLayer::createFileFilter() const +{ + return QgsProviderRegistry::instance()->fileMeshFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); +} + QgsProcessingParameterMeshLayer *QgsProcessingParameterMeshLayer::fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) { return new QgsProcessingParameterMeshLayer( name, description, definition.isEmpty() ? QVariant() : definition, isOptional ); @@ -4373,26 +4704,61 @@ QString QgsProcessingParameterFeatureSource::valueAsPythonString( const QVariant if ( value.canConvert() ) { QgsProcessingFeatureSourceDefinition fromVar = qvariant_cast( value ); + QString geometryCheckString; + switch ( fromVar.geometryCheck ) + { + case QgsFeatureRequest::GeometryNoCheck: + geometryCheckString = QStringLiteral( "QgsFeatureRequest.GeometryNoCheck" ); + break; + + case QgsFeatureRequest::GeometrySkipInvalid: + geometryCheckString = QStringLiteral( "QgsFeatureRequest.GeometrySkipInvalid" ); + break; + + case QgsFeatureRequest::GeometryAbortOnInvalid: + geometryCheckString = QStringLiteral( "QgsFeatureRequest.GeometryAbortOnInvalid" ); + break; + } + + QStringList flags; + QString flagString; + if ( fromVar.flags & QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ) + flags << QStringLiteral( "QgsProcessingFeatureSourceDefinition.FlagOverrideDefaultGeometryCheck" ); + if ( fromVar.flags & QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature ) + flags << QStringLiteral( "QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature" ); + if ( !flags.empty() ) + flagString = flags.join( QStringLiteral( " | " ) ); + if ( fromVar.source.propertyType() == QgsProperty::StaticProperty ) { - if ( fromVar.selectedFeaturesOnly ) + QString layerString = fromVar.source.staticValue().toString(); + // prefer to use layer source instead of id if possible (since it's persistent) + if ( QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( QgsProcessingUtils::mapLayerFromString( layerString, context, true, QgsProcessingUtils::LayerHint::Vector ) ) ) + layerString = layer->source(); + + if ( fromVar.selectedFeaturesOnly || fromVar.featureLimit != -1 || fromVar.flags ) { - return QStringLiteral( "QgsProcessingFeatureSourceDefinition('%1', True)" ).arg( fromVar.source.staticValue().toString() ); + return QStringLiteral( "QgsProcessingFeatureSourceDefinition('%1', selectedFeaturesOnly=%2, featureLimit=%3%4, geometryCheck=%5)" ).arg( layerString, + fromVar.selectedFeaturesOnly ? QStringLiteral( "True" ) : QStringLiteral( "False" ), + QString::number( fromVar.featureLimit ), + flagString.isEmpty() ? QString() : ( QStringLiteral( ", flags=%1" ).arg( flagString ) ), + geometryCheckString ); } else { - QString layerString = fromVar.source.staticValue().toString(); - // prefer to use layer source instead of id if possible (since it's persistent) - if ( QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( QgsProcessingUtils::mapLayerFromString( layerString, context, true, QgsProcessingUtils::LayerHint::Vector ) ) ) - layerString = layer->source(); return QgsProcessingUtils::stringToPythonLiteral( layerString ); } } else { - if ( fromVar.selectedFeaturesOnly ) + if ( fromVar.selectedFeaturesOnly || fromVar.featureLimit != -1 || fromVar.flags ) { - return QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('%1'), True)" ).arg( fromVar.source.asExpression() ); + return QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('%1'), selectedFeaturesOnly=%2, featureLimit=%3%4, geometryCheck=%5)" ) + .arg( fromVar.source.asExpression(), + fromVar.selectedFeaturesOnly ? QStringLiteral( "True" ) : QStringLiteral( "False" ), + QString::number( fromVar.featureLimit ), + flagString.isEmpty() ? QString() : ( QStringLiteral( ", flags=%1" ).arg( flagString ) ), + geometryCheckString ); } else { @@ -4409,7 +4775,7 @@ QString QgsProcessingParameterFeatureSource::valueAsPythonString( const QVariant // prefer to use layer source if possible (since it's persistent) if ( QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( QgsProcessingUtils::mapLayerFromString( layerString, context, true, QgsProcessingUtils::LayerHint::Vector ) ) ) - layerString = layer->source(); + layerString = layer->providerType() != QLatin1String( "ogr" ) && layer->providerType() != QLatin1String( "gdal" ) && layer->providerType() != QLatin1String( "mdal" ) ? QgsProcessingUtils::encodeProviderKeyAndUri( layer->providerType(), layer->source() ) : layer->source(); return QgsProcessingUtils::stringToPythonLiteral( layerString ); } @@ -4421,8 +4787,7 @@ QString QgsProcessingParameterFeatureSource::asScriptCode() const code += QStringLiteral( "optional " ); code += QStringLiteral( "source " ); - const auto constMDataTypes = mDataTypes; - for ( int type : constMDataTypes ) + for ( int type : mDataTypes ) { switch ( type ) { @@ -4472,6 +4837,11 @@ QString QgsProcessingParameterFeatureSource::asPythonString( const QgsProcessing return QString(); } +QString QgsProcessingParameterFeatureSource::createFileFilter() const +{ + return QgsProviderRegistry::instance()->fileVectorFilters() + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); +} + QgsProcessingParameterLimitedDataTypes::QgsProcessingParameterLimitedDataTypes( const QList &types ) : mDataTypes( types ) { @@ -4482,8 +4852,7 @@ QVariantMap QgsProcessingParameterFeatureSource::toVariantMap() const { QVariantMap map = QgsProcessingParameterDefinition::toVariantMap(); QVariantList types; - const auto constMDataTypes = mDataTypes; - for ( int type : constMDataTypes ) + for ( int type : mDataTypes ) { types << type; } @@ -4495,9 +4864,8 @@ bool QgsProcessingParameterFeatureSource::fromVariantMap( const QVariantMap &map { QgsProcessingParameterDefinition::fromVariantMap( map ); mDataTypes.clear(); - QVariantList values = map.value( QStringLiteral( "data_types" ) ).toList(); - const auto constValues = values; - for ( const QVariant &val : constValues ) + const QVariantList values = map.value( QStringLiteral( "data_types" ) ).toList(); + for ( const QVariant &val : values ) { mDataTypes << val.toInt(); } @@ -4534,9 +4902,10 @@ QgsProcessingParameterFeatureSource *QgsProcessingParameterFeatureSource::fromSc return new QgsProcessingParameterFeatureSource( name, description, types, def, isOptional ); } -QgsProcessingParameterFeatureSink::QgsProcessingParameterFeatureSink( const QString &name, const QString &description, QgsProcessing::SourceType type, const QVariant &defaultValue, bool optional, bool createByDefault ) +QgsProcessingParameterFeatureSink::QgsProcessingParameterFeatureSink( const QString &name, const QString &description, QgsProcessing::SourceType type, const QVariant &defaultValue, bool optional, bool createByDefault, bool supportsAppend ) : QgsProcessingDestinationParameter( name, description, defaultValue, optional, createByDefault ) , mDataType( type ) + , mSupportsAppend( supportsAppend ) { } @@ -4677,6 +5046,8 @@ QString QgsProcessingParameterFeatureSink::asPythonString( const QgsProcessing:: code += QStringLiteral( ", type=QgsProcessing.%1" ).arg( QgsProcessing::sourceTypeToString( mDataType ) ); code += QStringLiteral( ", createByDefault=%1" ).arg( createByDefault() ? QStringLiteral( "True" ) : QStringLiteral( "False" ) ); + if ( mSupportsAppend ) + code += QStringLiteral( ", supportsAppend=True" ); QgsProcessingContext c; code += QStringLiteral( ", defaultValue=%1)" ).arg( valueAsPythonString( mDefault, c ) ); @@ -4686,6 +5057,18 @@ QString QgsProcessingParameterFeatureSink::asPythonString( const QgsProcessing:: return QString(); } +QString QgsProcessingParameterFeatureSink::createFileFilter() const +{ + const QStringList exts = supportedOutputVectorLayerExtensions(); + QStringList filters; + for ( const QString &ext : exts ) + { + filters << QObject::tr( "%1 files (*.%2)" ).arg( ext.toUpper(), ext.toLower() ); + } + return filters.join( QStringLiteral( ";;" ) ) + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); + +} + QStringList QgsProcessingParameterFeatureSink::supportedOutputVectorLayerExtensions() const { if ( originalProvider() ) @@ -4742,6 +5125,7 @@ QVariantMap QgsProcessingParameterFeatureSink::toVariantMap() const { QVariantMap map = QgsProcessingDestinationParameter::toVariantMap(); map.insert( QStringLiteral( "data_type" ), mDataType ); + map.insert( QStringLiteral( "supports_append" ), mSupportsAppend ); return map; } @@ -4749,6 +5133,7 @@ bool QgsProcessingParameterFeatureSink::fromVariantMap( const QVariantMap &map ) { QgsProcessingDestinationParameter::fromVariantMap( map ); mDataType = static_cast< QgsProcessing::SourceType >( map.value( QStringLiteral( "data_type" ) ).toInt() ); + mSupportsAppend = map.value( QStringLiteral( "supports_append" ), false ).toBool(); return true; } @@ -4788,6 +5173,16 @@ QgsProcessingParameterFeatureSink *QgsProcessingParameterFeatureSink::fromScript return new QgsProcessingParameterFeatureSink( name, description, type, definition, isOptional ); } +bool QgsProcessingParameterFeatureSink::supportsAppend() const +{ + return mSupportsAppend; +} + +void QgsProcessingParameterFeatureSink::setSupportsAppend( bool supportsAppend ) +{ + mSupportsAppend = supportsAppend; +} + QgsProcessingParameterRasterDestination::QgsProcessingParameterRasterDestination( const QString &name, const QString &description, const QVariant &defaultValue, bool optional, bool createByDefault ) : QgsProcessingDestinationParameter( name, description, defaultValue, optional, createByDefault ) { @@ -4877,6 +5272,17 @@ QString QgsProcessingParameterRasterDestination::defaultFileExtension() const } } +QString QgsProcessingParameterRasterDestination::createFileFilter() const +{ + const QStringList exts = supportedOutputRasterLayerExtensions(); + QStringList filters; + for ( const QString &ext : exts ) + { + filters << QObject::tr( "%1 files (*.%2)" ).arg( ext.toUpper(), ext.toLower() ); + } + return filters.join( QStringLiteral( ";;" ) ) + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); +} + QStringList QgsProcessingParameterRasterDestination::supportedOutputRasterLayerExtensions() const { if ( originalProvider() ) @@ -5019,6 +5425,11 @@ QString QgsProcessingParameterFileDestination::asPythonString( const QgsProcessi return QString(); } +QString QgsProcessingParameterFileDestination::createFileFilter() const +{ + return ( fileFilter().isEmpty() ? QString() : fileFilter() + QStringLiteral( ";;" ) ) + QObject::tr( "All files (*.*)" ); +} + QString QgsProcessingParameterFileDestination::fileFilter() const { return mFileFilter; @@ -5150,6 +5561,11 @@ QString QgsProcessingDestinationParameter::asPythonString( const QgsProcessing:: return QString(); } +QString QgsProcessingDestinationParameter::createFileFilter() const +{ + return QObject::tr( "Default extension" ) + QStringLiteral( " (*." ) + defaultFileExtension() + ')'; +} + QString QgsProcessingDestinationParameter::generateTemporaryDestination() const { if ( defaultFileExtension().isEmpty() ) @@ -5321,6 +5737,17 @@ QString QgsProcessingParameterVectorDestination::asPythonString( const QgsProces return QString(); } +QString QgsProcessingParameterVectorDestination::createFileFilter() const +{ + const QStringList exts = supportedOutputVectorLayerExtensions(); + QStringList filters; + for ( const QString &ext : exts ) + { + filters << QObject::tr( "%1 files (*.%2)" ).arg( ext.toUpper(), ext.toLower() ); + } + return filters.join( QStringLiteral( ";;" ) ) + QStringLiteral( ";;" ) + QObject::tr( "All files (*.*)" ); +} + QStringList QgsProcessingParameterVectorDestination::supportedOutputVectorLayerExtensions() const { if ( originalProvider() ) @@ -6624,3 +7051,296 @@ QgsProcessingParameterProviderConnection *QgsProcessingParameterProviderConnecti return new QgsProcessingParameterProviderConnection( name, description, provider, defaultValue, isOptional ); } + +// +// QgsProcessingParameterDatabaseSchema +// + +QgsProcessingParameterDatabaseSchema::QgsProcessingParameterDatabaseSchema( const QString &name, const QString &description, const QString &parentLayerParameterName, const QVariant &defaultValue, bool optional ) + : QgsProcessingParameterDefinition( name, description, defaultValue, optional ) + , mParentConnectionParameterName( parentLayerParameterName ) +{ + +} + + +QgsProcessingParameterDefinition *QgsProcessingParameterDatabaseSchema::clone() const +{ + return new QgsProcessingParameterDatabaseSchema( *this ); +} + +bool QgsProcessingParameterDatabaseSchema::checkValueIsAcceptable( const QVariant &input, QgsProcessingContext * ) const +{ + if ( !input.isValid() && !mDefault.isValid() ) + return mFlags & FlagOptional; + + if ( ( input.type() == QVariant::String && input.toString().isEmpty() ) + || ( !input.isValid() && mDefault.type() == QVariant::String && mDefault.toString().isEmpty() ) ) + return mFlags & FlagOptional; + + return true; +} + +QString QgsProcessingParameterDatabaseSchema::valueAsPythonString( const QVariant &value, QgsProcessingContext & ) const +{ + if ( !value.isValid() ) + return QStringLiteral( "None" ); + + if ( value.canConvert() ) + return QStringLiteral( "QgsProperty.fromExpression('%1')" ).arg( value.value< QgsProperty >().asExpression() ); + + return QgsProcessingUtils::stringToPythonLiteral( value.toString() ); +} + +QString QgsProcessingParameterDatabaseSchema::asScriptCode() const +{ + QString code = QStringLiteral( "##%1=" ).arg( mName ); + if ( mFlags & FlagOptional ) + code += QStringLiteral( "optional " ); + code += QStringLiteral( "databaseschema " ); + + code += mParentConnectionParameterName + ' '; + + code += mDefault.toString(); + return code.trimmed(); +} + +QString QgsProcessingParameterDatabaseSchema::asPythonString( const QgsProcessing::PythonOutputType outputType ) const +{ + switch ( outputType ) + { + case QgsProcessing::PythonQgsProcessingAlgorithmSubclass: + { + QString code = QStringLiteral( "QgsProcessingParameterDatabaseSchema('%1', '%2'" ).arg( name(), description() ); + if ( mFlags & FlagOptional ) + code += QStringLiteral( ", optional=True" ); + + code += QStringLiteral( ", connectionParameterName='%1'" ).arg( mParentConnectionParameterName ); + QgsProcessingContext c; + code += QStringLiteral( ", defaultValue=%1" ).arg( valueAsPythonString( mDefault, c ) ); + + code += ')'; + + return code; + } + } + return QString(); +} + +QStringList QgsProcessingParameterDatabaseSchema::dependsOnOtherParameters() const +{ + QStringList depends; + if ( !mParentConnectionParameterName.isEmpty() ) + depends << mParentConnectionParameterName; + return depends; +} + +QString QgsProcessingParameterDatabaseSchema::parentConnectionParameterName() const +{ + return mParentConnectionParameterName; +} + +void QgsProcessingParameterDatabaseSchema::setParentConnectionParameterName( const QString &name ) +{ + mParentConnectionParameterName = name; +} + +QVariantMap QgsProcessingParameterDatabaseSchema::toVariantMap() const +{ + QVariantMap map = QgsProcessingParameterDefinition::toVariantMap(); + map.insert( QStringLiteral( "mParentConnectionParameterName" ), mParentConnectionParameterName ); + return map; +} + +bool QgsProcessingParameterDatabaseSchema::fromVariantMap( const QVariantMap &map ) +{ + QgsProcessingParameterDefinition::fromVariantMap( map ); + mParentConnectionParameterName = map.value( QStringLiteral( "mParentConnectionParameterName" ) ).toString(); + return true; +} + +QgsProcessingParameterDatabaseSchema *QgsProcessingParameterDatabaseSchema::fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) +{ + QString parent; + QString def = definition; + + QRegularExpression re( QStringLiteral( "(.*?)\\s+(.*)$" ) ); + QRegularExpressionMatch m = re.match( def ); + if ( m.hasMatch() ) + { + parent = m.captured( 1 ).trimmed(); + def = m.captured( 2 ); + } + else + { + parent = def; + def.clear(); + } + + return new QgsProcessingParameterDatabaseSchema( name, description, parent, def.isEmpty() ? QVariant() : def, isOptional ); +} + +// +// QgsProcessingParameterDatabaseTable +// + +QgsProcessingParameterDatabaseTable::QgsProcessingParameterDatabaseTable( const QString &name, const QString &description, + const QString &connectionParameterName, + const QString &schemaParameterName, + const QVariant &defaultValue, bool optional, bool allowNewTableNames ) + : QgsProcessingParameterDefinition( name, description, defaultValue, optional ) + , mParentConnectionParameterName( connectionParameterName ) + , mParentSchemaParameterName( schemaParameterName ) + , mAllowNewTableNames( allowNewTableNames ) +{ + +} + + +QgsProcessingParameterDefinition *QgsProcessingParameterDatabaseTable::clone() const +{ + return new QgsProcessingParameterDatabaseTable( *this ); +} + +bool QgsProcessingParameterDatabaseTable::checkValueIsAcceptable( const QVariant &input, QgsProcessingContext * ) const +{ + if ( !input.isValid() && !mDefault.isValid() ) + return mFlags & FlagOptional; + + if ( ( input.type() == QVariant::String && input.toString().isEmpty() ) + || ( !input.isValid() && mDefault.type() == QVariant::String && mDefault.toString().isEmpty() ) ) + return mFlags & FlagOptional; + + return true; +} + +QString QgsProcessingParameterDatabaseTable::valueAsPythonString( const QVariant &value, QgsProcessingContext & ) const +{ + if ( !value.isValid() ) + return QStringLiteral( "None" ); + + if ( value.canConvert() ) + return QStringLiteral( "QgsProperty.fromExpression('%1')" ).arg( value.value< QgsProperty >().asExpression() ); + + return QgsProcessingUtils::stringToPythonLiteral( value.toString() ); +} + +QString QgsProcessingParameterDatabaseTable::asScriptCode() const +{ + QString code = QStringLiteral( "##%1=" ).arg( mName ); + if ( mFlags & FlagOptional ) + code += QStringLiteral( "optional " ); + code += QStringLiteral( "databasetable " ); + + code += ( mParentConnectionParameterName.isEmpty() ? QStringLiteral( "none" ) : mParentConnectionParameterName ) + ' '; + code += ( mParentSchemaParameterName.isEmpty() ? QStringLiteral( "none" ) : mParentSchemaParameterName ) + ' '; + + code += mDefault.toString(); + return code.trimmed(); +} + +QString QgsProcessingParameterDatabaseTable::asPythonString( const QgsProcessing::PythonOutputType outputType ) const +{ + switch ( outputType ) + { + case QgsProcessing::PythonQgsProcessingAlgorithmSubclass: + { + QString code = QStringLiteral( "QgsProcessingParameterDatabaseTable('%1', '%2'" ).arg( name(), description() ); + if ( mFlags & FlagOptional ) + code += QStringLiteral( ", optional=True" ); + + if ( mAllowNewTableNames ) + code += QStringLiteral( ", allowNewTableNames=True" ); + + code += QStringLiteral( ", connectionParameterName='%1'" ).arg( mParentConnectionParameterName ); + code += QStringLiteral( ", schemaParameterName='%1'" ).arg( mParentSchemaParameterName ); + QgsProcessingContext c; + code += QStringLiteral( ", defaultValue=%1" ).arg( valueAsPythonString( mDefault, c ) ); + + code += ')'; + + return code; + } + } + return QString(); +} + +QStringList QgsProcessingParameterDatabaseTable::dependsOnOtherParameters() const +{ + QStringList depends; + if ( !mParentConnectionParameterName.isEmpty() ) + depends << mParentConnectionParameterName; + if ( !mParentSchemaParameterName.isEmpty() ) + depends << mParentSchemaParameterName; + return depends; +} + +QString QgsProcessingParameterDatabaseTable::parentConnectionParameterName() const +{ + return mParentConnectionParameterName; +} + +void QgsProcessingParameterDatabaseTable::setParentConnectionParameterName( const QString &name ) +{ + mParentConnectionParameterName = name; +} + +QString QgsProcessingParameterDatabaseTable::parentSchemaParameterName() const +{ + return mParentSchemaParameterName; +} + +void QgsProcessingParameterDatabaseTable::setParentSchemaParameterName( const QString &name ) +{ + mParentSchemaParameterName = name; +} + +QVariantMap QgsProcessingParameterDatabaseTable::toVariantMap() const +{ + QVariantMap map = QgsProcessingParameterDefinition::toVariantMap(); + map.insert( QStringLiteral( "mParentConnectionParameterName" ), mParentConnectionParameterName ); + map.insert( QStringLiteral( "mParentSchemaParameterName" ), mParentSchemaParameterName ); + map.insert( QStringLiteral( "mAllowNewTableNames" ), mAllowNewTableNames ); + return map; +} + +bool QgsProcessingParameterDatabaseTable::fromVariantMap( const QVariantMap &map ) +{ + QgsProcessingParameterDefinition::fromVariantMap( map ); + mParentConnectionParameterName = map.value( QStringLiteral( "mParentConnectionParameterName" ) ).toString(); + mParentSchemaParameterName = map.value( QStringLiteral( "mParentSchemaParameterName" ) ).toString(); + mAllowNewTableNames = map.value( QStringLiteral( "mAllowNewTableNames" ), false ).toBool(); + return true; +} + +QgsProcessingParameterDatabaseTable *QgsProcessingParameterDatabaseTable::fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) +{ + QString connection; + QString schema; + QString def = definition; + + QRegularExpression re( QStringLiteral( "(.*?)\\s+(.*+)\\b\\s*(.*)$" ) ); + QRegularExpressionMatch m = re.match( def ); + if ( m.hasMatch() ) + { + connection = m.captured( 1 ).trimmed(); + if ( connection == QLatin1String( "none" ) ) + connection.clear(); + schema = m.captured( 2 ).trimmed(); + if ( schema == QLatin1String( "none" ) ) + schema.clear(); + def = m.captured( 3 ); + } + + return new QgsProcessingParameterDatabaseTable( name, description, connection, schema, def.isEmpty() ? QVariant() : def, isOptional ); +} + +bool QgsProcessingParameterDatabaseTable::allowNewTableNames() const +{ + return mAllowNewTableNames; +} + +void QgsProcessingParameterDatabaseTable::setAllowNewTableNames( bool allowNewTableNames ) +{ + mAllowNewTableNames = allowNewTableNames; +} diff --git a/src/core/processing/qgsprocessingparameters.h b/src/core/processing/qgsprocessingparameters.h index 83904a7da361..e7da79a770cc 100644 --- a/src/core/processing/qgsprocessingparameters.h +++ b/src/core/processing/qgsprocessingparameters.h @@ -25,6 +25,8 @@ #include "qgscoordinatereferencesystem.h" #include "qgsfeaturesource.h" #include "qgsprocessingutils.h" +#include "qgsfilefiltergenerator.h" +#include "qgsremappingproxyfeaturesink.h" #include #include @@ -55,19 +57,58 @@ class CORE_EXPORT QgsProcessingFeatureSourceDefinition public: /** - * Constructor for QgsProcessingFeatureSourceDefinition, accepting a static string source. + * Flags which control source behavior. + * \since QGIS 3.14 + */ + enum Flag + { + FlagOverrideDefaultGeometryCheck = 1 << 0, //!< If set, the default geometry check method (as dictated by QgsProcessingContext) will be overridden for this source + FlagCreateIndividualOutputPerInputFeature = 1 << 1, //!< If set, every feature processed from this source will be placed into its own individually created output destination. Support for this flag depends on how an algorithm is executed. + }; + Q_DECLARE_FLAGS( Flags, Flag ) + + /** + * Constructor for QgsProcessingFeatureSourceDefinition, accepting a static string \a source. + * + * If \a selectedFeaturesOnly is TRUE, then only selected features from the source will be used. + * + * The optional \a featureLimit can be set to a value > 0 to place a hard limit on the maximum number + * of features which will be read from the source. + * + * The \a flags argument can be used to specify flags which dictate the source behavior. + * + * If the QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck is set in \a flags, then the value of \a geometryCheck will override + * the default geometry check method (as dictated by QgsProcessingContext) for this source. */ - QgsProcessingFeatureSourceDefinition( const QString &source = QString(), bool selectedFeaturesOnly = false ) + QgsProcessingFeatureSourceDefinition( const QString &source = QString(), bool selectedFeaturesOnly = false, long long featureLimit = -1, + QgsProcessingFeatureSourceDefinition::Flags flags = nullptr, QgsFeatureRequest::InvalidGeometryCheck geometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid ) : source( QgsProperty::fromValue( source ) ) , selectedFeaturesOnly( selectedFeaturesOnly ) + , featureLimit( featureLimit ) + , flags( flags ) + , geometryCheck( geometryCheck ) {} /** * Constructor for QgsProcessingFeatureSourceDefinition, accepting a QgsProperty source. + * + * If \a selectedFeaturesOnly is TRUE, then only selected features from the source will be used. + * + * The optional \a featureLimit can be set to a value > 0 to place a hard limit on the maximum number + * of features which will be read from the source. + * + * The \a flags argument can be used to specify flags which dictate the source behavior. + * + * If the QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck is set in \a flags, then the value of \a geometryCheck will override + * the default geometry check method (as dictated by QgsProcessingContext) for this source. */ - QgsProcessingFeatureSourceDefinition( const QgsProperty &source, bool selectedFeaturesOnly = false ) + QgsProcessingFeatureSourceDefinition( const QgsProperty &source, bool selectedFeaturesOnly = false, long long featureLimit = -1, + QgsProcessingFeatureSourceDefinition::Flags flags = nullptr, QgsFeatureRequest::InvalidGeometryCheck geometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid ) : source( source ) , selectedFeaturesOnly( selectedFeaturesOnly ) + , featureLimit( featureLimit ) + , flags( flags ) + , geometryCheck( geometryCheck ) {} /** @@ -80,9 +121,54 @@ class CORE_EXPORT QgsProcessingFeatureSourceDefinition */ bool selectedFeaturesOnly; + /** + * If set to a value > 0, places a limit on the maximum number of features which will be + * read from the source. + * + * \since QGIS 3.14 + */ + long long featureLimit = -1; + + /** + * Flags which dictate source behavior. + * + * \since QGIS 3.14 + */ + Flags flags = nullptr; + + /** + * Geometry check method to apply to this source. This setting is only + * utilized if the QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature is + * set in QgsProcessingFeatureSourceDefinition::flags. + * + * \see overrideDefaultGeometryCheck + * \since QGIS 3.14 + */ + QgsFeatureRequest::InvalidGeometryCheck geometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid; + + /** + * Saves this source definition to a QVariantMap, wrapped in a QVariant. + * You can use QgsXmlUtils::writeVariant to save it to an XML document. + * \see loadVariant() + * \since QGIS 3.14 + */ + QVariant toVariant() const; + + /** + * Loads this source definition from a QVariantMap, wrapped in a QVariant. + * You can use QgsXmlUtils::readVariant to load it from an XML document. + * \see toVariant() + * \since QGIS 3.14 + */ + bool loadVariant( const QVariantMap &map ); + bool operator==( const QgsProcessingFeatureSourceDefinition &other ) { - return source == other.source && selectedFeaturesOnly == other.selectedFeaturesOnly; + return source == other.source + && selectedFeaturesOnly == other.selectedFeaturesOnly + && featureLimit == other.featureLimit + && flags == other.flags + && geometryCheck == other.geometryCheck; } bool operator!=( const QgsProcessingFeatureSourceDefinition &other ) @@ -99,6 +185,7 @@ class CORE_EXPORT QgsProcessingFeatureSourceDefinition }; Q_DECLARE_METATYPE( QgsProcessingFeatureSourceDefinition ) +Q_DECLARE_OPERATORS_FOR_FLAGS( QgsProcessingFeatureSourceDefinition::Flags ) /** * \class QgsProcessingOutputLayerDefinition @@ -158,6 +245,35 @@ class CORE_EXPORT QgsProcessingOutputLayerDefinition */ QVariantMap createOptions; + /** + * Returns TRUE if the output uses a remapping definition. + * + * \see remappingDefinition() + * \since QGIS 3.14 + */ + bool useRemapping() const { return mUseRemapping; } + + /** + * Returns the output remapping definition, if useRemapping() is TRUE. + * + * \see useRemapping() + * \see setRemappingDefinition() + * \since QGIS 3.14 + */ + QgsRemappingSinkDefinition remappingDefinition() const { return mRemappingDefinition; } + + /** + * Sets the remapping \a definition to use when adding features to the output layer. + * + * Calling this method will set useRemapping() to TRUE. + * + * \see remappingDefinition() + * \see useRemapping() + * + * \since QGIS 3.14 + */ + void setRemappingDefinition( const QgsRemappingSinkDefinition &definition ); + /** * Saves this output layer definition to a QVariantMap, wrapped in a QVariant. * You can use QgsXmlUtils::writeVariant to save it to an XML document. @@ -180,6 +296,14 @@ class CORE_EXPORT QgsProcessingOutputLayerDefinition return QVariant::fromValue( *this ); } + bool operator==( const QgsProcessingOutputLayerDefinition &other ) const; + bool operator!=( const QgsProcessingOutputLayerDefinition &other ) const; + + private: + + bool mUseRemapping = false; + QgsRemappingSinkDefinition mRemappingDefinition; + }; Q_DECLARE_METATYPE( QgsProcessingOutputLayerDefinition ) @@ -276,6 +400,10 @@ class CORE_EXPORT QgsProcessingParameterDefinition sipType = sipType_QgsProcessingParameterDateTime; else if ( sipCpp->type() == QgsProcessingParameterProviderConnection::typeName() ) sipType = sipType_QgsProcessingParameterProviderConnection; + else if ( sipCpp->type() == QgsProcessingParameterDatabaseSchema::typeName() ) + sipType = sipType_QgsProcessingParameterDatabaseSchema; + else if ( sipCpp->type() == QgsProcessingParameterDatabaseTable::typeName() ) + sipType = sipType_QgsProcessingParameterDatabaseTable; else sipType = nullptr; SIP_END @@ -821,12 +949,12 @@ class CORE_EXPORT QgsProcessingParameters * to the sink, e.g. via calling QgsProcessingUtils::mapLayerFromString(). * * This function creates a new object and the caller takes responsibility for deleting the returned object. - * + * \throws QgsProcessingException * \since QGIS 3.4 */ static QgsFeatureSink *parameterAsSink( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsFields &fields, QgsWkbTypes::Type geometryType, const QgsCoordinateReferenceSystem &crs, - QgsProcessingContext &context, QString &destinationIdentifier SIP_OUT, QgsFeatureSink::SinkFlags sinkFlags = nullptr ) SIP_FACTORY; + QgsProcessingContext &context, QString &destinationIdentifier SIP_OUT, QgsFeatureSink::SinkFlags sinkFlags = nullptr ) SIP_THROW( QgsProcessingException ) SIP_FACTORY; /** * Evaluates the parameter with matching \a definition to a feature source. @@ -907,7 +1035,7 @@ class CORE_EXPORT QgsProcessingParameters * sources and stored temporarily in the \a context. In either case, callers do not * need to handle deletion of the returned layer. */ - static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ); + static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context, QgsProcessingUtils::LayerHint layerHint = QgsProcessingUtils::LayerHint::UnknownType ); /** * Evaluates the parameter with matching \a definition and \a value to a map layer. @@ -918,7 +1046,7 @@ class CORE_EXPORT QgsProcessingParameters * * \since QGIS 3.4 */ - static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ); + static QgsMapLayer *parameterAsLayer( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context, QgsProcessingUtils::LayerHint layerHint = QgsProcessingUtils::LayerHint::UnknownType ); /** * Evaluates the parameter with matching \a definition to a raster layer. @@ -1063,6 +1191,14 @@ class CORE_EXPORT QgsProcessingParameters */ static QgsCoordinateReferenceSystem parameterAsExtentCrs( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, QgsProcessingContext &context ); + /** + * Returns the coordinate reference system associated with an extent parameter value. + * + * \see parameterAsExtent() + */ + static QgsCoordinateReferenceSystem parameterAsExtentCrs( const QgsProcessingParameterDefinition *definition, const QVariant &value, QgsProcessingContext &context ); + + /** * Evaluates the parameter with matching \a definition to a point. * @@ -1238,6 +1374,34 @@ class CORE_EXPORT QgsProcessingParameters */ static QString parameterAsConnectionName( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ); + /** + * Evaluates the parameter with matching \a definition to a database schema name. + * + * \since QGIS 3.14 + */ + static QString parameterAsSchema( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, const QgsProcessingContext &context ); + + /** + * Evaluates the parameter with matching \a definition and \a value to a database schema name. + * + * \since QGIS 3.14 + */ + static QString parameterAsSchema( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ); + + /** + * Evaluates the parameter with matching \a definition to a database table name. + * + * \since QGIS 3.14 + */ + static QString parameterAsDatabaseTableName( const QgsProcessingParameterDefinition *definition, const QVariantMap ¶meters, const QgsProcessingContext &context ); + + /** + * Evaluates the parameter with matching \a definition and \a value to a database table name. + * + * \since QGIS 3.14 + */ + static QString parameterAsDatabaseTableName( const QgsProcessingParameterDefinition *definition, const QVariant &value, const QgsProcessingContext &context ); + /** * Creates a new QgsProcessingParameterDefinition using the configuration from a * supplied variant \a map. @@ -1327,38 +1491,6 @@ class CORE_EXPORT QgsProcessingParameterCrs : public QgsProcessingParameterDefin }; -/** - * \class QgsProcessingParameterMapLayer - * \ingroup core - * A map layer parameter for processing algorithms. - * \since QGIS 3.0 - */ -class CORE_EXPORT QgsProcessingParameterMapLayer : public QgsProcessingParameterDefinition -{ - public: - - /** - * Constructor for QgsProcessingParameterMapLayer. - */ - QgsProcessingParameterMapLayer( const QString &name, const QString &description = QString(), const QVariant &defaultValue = QVariant(), - bool optional = false ); - - /** - * Returns the type name for the parameter class. - */ - static QString typeName() { return QStringLiteral( "layer" ); } - QgsProcessingParameterDefinition *clone() const override SIP_FACTORY; - QString type() const override { return typeName(); } - bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; - QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; - - /** - * Creates a new parameter using the definition from a script code. - */ - static QgsProcessingParameterMapLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) SIP_FACTORY; - -}; - /** * \class QgsProcessingParameterExtent * \ingroup core @@ -1619,7 +1751,7 @@ class CORE_EXPORT QgsProcessingParameterMatrix : public QgsProcessingParameterDe * A parameter for processing algorithms which accepts multiple map layers. * \since QGIS 3.0 */ -class CORE_EXPORT QgsProcessingParameterMultipleLayers : public QgsProcessingParameterDefinition +class CORE_EXPORT QgsProcessingParameterMultipleLayers : public QgsProcessingParameterDefinition, public QgsFileFilterGenerator { public: @@ -1640,6 +1772,7 @@ class CORE_EXPORT QgsProcessingParameterMultipleLayers : public QgsProcessingPar QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; QString asScriptCode() const override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; /** * Returns the layer type for layers acceptable by the parameter. @@ -1963,7 +2096,7 @@ class CORE_EXPORT QgsProcessingParameterRange : public QgsProcessingParameterDef * A raster layer parameter for processing algorithms. * \since QGIS 3.0 */ -class CORE_EXPORT QgsProcessingParameterRasterLayer : public QgsProcessingParameterDefinition +class CORE_EXPORT QgsProcessingParameterRasterLayer : public QgsProcessingParameterDefinition, public QgsFileFilterGenerator { public: @@ -1981,6 +2114,7 @@ class CORE_EXPORT QgsProcessingParameterRasterLayer : public QgsProcessingParame QString type() const override { return typeName(); } bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; + QString createFileFilter() const override; /** * Creates a new parameter using the definition from a script code. @@ -2243,7 +2377,7 @@ class CORE_EXPORT QgsProcessingParameterLimitedDataTypes * the more versatile QgsProcessingParameterFeatureSource wherever possible. * \since QGIS 3.0 */ -class CORE_EXPORT QgsProcessingParameterVectorLayer : public QgsProcessingParameterDefinition, public QgsProcessingParameterLimitedDataTypes +class CORE_EXPORT QgsProcessingParameterVectorLayer : public QgsProcessingParameterDefinition, public QgsProcessingParameterLimitedDataTypes, public QgsFileFilterGenerator { public: @@ -2265,6 +2399,7 @@ class CORE_EXPORT QgsProcessingParameterVectorLayer : public QgsProcessingParame bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; QVariantMap toVariantMap() const override; bool fromVariantMap( const QVariantMap &map ) override; @@ -2282,7 +2417,7 @@ class CORE_EXPORT QgsProcessingParameterVectorLayer : public QgsProcessingParame * A mesh layer parameter for processing algorithms. * \since QGIS 3.6 */ -class CORE_EXPORT QgsProcessingParameterMeshLayer : public QgsProcessingParameterDefinition +class CORE_EXPORT QgsProcessingParameterMeshLayer : public QgsProcessingParameterDefinition, public QgsFileFilterGenerator { public: @@ -2302,6 +2437,7 @@ class CORE_EXPORT QgsProcessingParameterMeshLayer : public QgsProcessingParamete QString type() const override { return typeName(); } bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; + QString createFileFilter() const override; /** * Creates a new parameter using the definition from a script code. @@ -2309,6 +2445,45 @@ class CORE_EXPORT QgsProcessingParameterMeshLayer : public QgsProcessingParamete static QgsProcessingParameterMeshLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) SIP_FACTORY; }; +/** + * \class QgsProcessingParameterMapLayer + * \ingroup core + * A map layer parameter for processing algorithms. + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsProcessingParameterMapLayer : public QgsProcessingParameterDefinition, public QgsProcessingParameterLimitedDataTypes, public QgsFileFilterGenerator +{ + public: + + /** + * Constructor for QgsProcessingParameterMapLayer. + */ + QgsProcessingParameterMapLayer( const QString &name, const QString &description = QString(), const QVariant &defaultValue = QVariant(), + bool optional = false, + const QList< int > &types = QList< int >() ); + + /** + * Returns the type name for the parameter class. + */ + static QString typeName() { return QStringLiteral( "layer" ); } + QgsProcessingParameterDefinition *clone() const override SIP_FACTORY; + QString type() const override { return typeName(); } + bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; + QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; + QString asScriptCode() const override; + QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; + + QVariantMap toVariantMap() const override; + bool fromVariantMap( const QVariantMap &map ) override; + + /** + * Creates a new parameter using the definition from a script code. + */ + static QgsProcessingParameterMapLayer *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) SIP_FACTORY; + +}; + /** * \class QgsProcessingParameterField * \ingroup core @@ -2432,7 +2607,7 @@ class CORE_EXPORT QgsProcessingParameterField : public QgsProcessingParameterDef * An input feature source (such as vector layers) parameter for processing algorithms. * \since QGIS 3.0 */ -class CORE_EXPORT QgsProcessingParameterFeatureSource : public QgsProcessingParameterDefinition, public QgsProcessingParameterLimitedDataTypes +class CORE_EXPORT QgsProcessingParameterFeatureSource : public QgsProcessingParameterDefinition, public QgsProcessingParameterLimitedDataTypes, public QgsFileFilterGenerator { public: @@ -2453,6 +2628,7 @@ class CORE_EXPORT QgsProcessingParameterFeatureSource : public QgsProcessingPara QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; QString asScriptCode() const override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; QVariantMap toVariantMap() const override; bool fromVariantMap( const QVariantMap &map ) override; @@ -2471,7 +2647,7 @@ class CORE_EXPORT QgsProcessingParameterFeatureSource : public QgsProcessingPara * which are used for the destination for layers output by an algorithm. * \since QGIS 3.0 */ -class CORE_EXPORT QgsProcessingDestinationParameter : public QgsProcessingParameterDefinition +class CORE_EXPORT QgsProcessingDestinationParameter : public QgsProcessingParameterDefinition, public QgsFileFilterGenerator { public: @@ -2488,6 +2664,7 @@ class CORE_EXPORT QgsProcessingDestinationParameter : public QgsProcessingParame QVariantMap toVariantMap() const override; bool fromVariantMap( const QVariantMap &map ) override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; /** * Returns a new QgsProcessingOutputDefinition corresponding to the definition of the destination @@ -2586,7 +2763,7 @@ class CORE_EXPORT QgsProcessingParameterFeatureSink : public QgsProcessingDestin * output will not be created by default. */ QgsProcessingParameterFeatureSink( const QString &name, const QString &description = QString(), QgsProcessing::SourceType type = QgsProcessing::TypeVectorAnyGeometry, const QVariant &defaultValue = QVariant(), - bool optional = false, bool createByDefault = true ); + bool optional = false, bool createByDefault = true, bool supportsAppend = false ); /** * Returns the type name for the parameter class. @@ -2600,6 +2777,7 @@ class CORE_EXPORT QgsProcessingParameterFeatureSink : public QgsProcessingDestin QgsProcessingOutputDefinition *toOutputDefinition() const override SIP_FACTORY; QString defaultFileExtension() const override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; /** * Returns a list of the vector format file extensions supported by this parameter. @@ -2626,6 +2804,26 @@ class CORE_EXPORT QgsProcessingParameterFeatureSink : public QgsProcessingDestin */ void setDataType( QgsProcessing::SourceType type ); + /** + * Returns TRUE if the sink supports appending features to an existing table. + * + * A sink only supports appending if the algorithm implements QgsProcessingAlgorithm::sinkProperties for the sink parameter. + * + * \see setSupportsAppend() + * \since QGIS 3.14 + */ + bool supportsAppend() const; + + /** + * Sets whether the sink supports appending features to an existing table. + * + * \warning A sink only supports appending if the algorithm implements QgsProcessingAlgorithm::sinkProperties for the sink parameter. + * + * \see supportsAppend() + * \since QGIS 3.14 + */ + void setSupportsAppend( bool supportsAppend ); + QVariantMap toVariantMap() const override; bool fromVariantMap( const QVariantMap &map ) override; QString generateTemporaryDestination() const override; @@ -2638,6 +2836,7 @@ class CORE_EXPORT QgsProcessingParameterFeatureSink : public QgsProcessingDestin private: QgsProcessing::SourceType mDataType = QgsProcessing::TypeVectorAnyGeometry; + bool mSupportsAppend = false; }; @@ -2676,6 +2875,7 @@ class CORE_EXPORT QgsProcessingParameterVectorDestination : public QgsProcessing QgsProcessingOutputDefinition *toOutputDefinition() const override SIP_FACTORY; QString defaultFileExtension() const override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; /** * Returns a list of the vector format file extensions supported by this parameter. @@ -2748,6 +2948,7 @@ class CORE_EXPORT QgsProcessingParameterRasterDestination : public QgsProcessing QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; QgsProcessingOutputDefinition *toOutputDefinition() const override SIP_FACTORY; QString defaultFileExtension() const override; + QString createFileFilter() const override; /** * Returns a list of the raster format file extensions supported for this parameter. @@ -2796,6 +2997,7 @@ class CORE_EXPORT QgsProcessingParameterFileDestination : public QgsProcessingDe QgsProcessingOutputDefinition *toOutputDefinition() const override SIP_FACTORY; QString defaultFileExtension() const override; QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QString createFileFilter() const override; /** * Returns the file filter string for file destinations compatible with this parameter. @@ -3431,6 +3633,165 @@ class CORE_EXPORT QgsProcessingParameterProviderConnection : public QgsProcessin }; +/** + * \class QgsProcessingParameterDatabaseSchema + * \ingroup core + * A database schema parameter for processing algorithms, allowing users to select from existing schemas + * on a registered database connection. + * + * QgsProcessingParameterDatabaseSchema should be evaluated by calling QgsProcessingAlgorithm::parameterAsSchema(). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsProcessingParameterDatabaseSchema : public QgsProcessingParameterDefinition +{ + public: + + /** + * Constructor for QgsProcessingParameterDatabaseSchema. + * + * The \a connectionParameterName specifies the name of the parent QgsProcessingParameterProviderConnection parameter. + * + * \warning The provider must support the connection API methods in its QgsProviderMetadata implementation + * in order for the model to work correctly. This is only implemented for a subset of current data providers. + */ + QgsProcessingParameterDatabaseSchema( const QString &name, const QString &description, const QString &connectionParameterName = QString(), const QVariant &defaultValue = QVariant(), + bool optional = false ); + + /** + * Returns the type name for the parameter class. + */ + static QString typeName() { return QStringLiteral( "databaseschema" ); } + QgsProcessingParameterDefinition *clone() const override SIP_FACTORY; + QString type() const override { return typeName(); } + bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; + QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; + QString asScriptCode() const override; + QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QVariantMap toVariantMap() const override; + bool fromVariantMap( const QVariantMap &map ) override; + QStringList dependsOnOtherParameters() const override; + + /** + * Returns the name of the parent connection parameter, or an empty string if this is not set. + * \see setParentConnectionParameterName() + */ + QString parentConnectionParameterName() const; + + /** + * Sets the \a name of the parent connection parameter. Use an empty string if this is not required. + * \see parentConnectionParameterName() + */ + void setParentConnectionParameterName( const QString &name ); + + /** + * Creates a new parameter using the definition from a script code. + */ + static QgsProcessingParameterDatabaseSchema *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) SIP_FACTORY; + + private: + + QString mParentConnectionParameterName; +}; + + +/** + * \class QgsProcessingParameterDatabaseTable + * \ingroup core + * A database table name parameter for processing algorithms, allowing users to select from existing database tables + * on a registered database connection (or optionally to enter a new table name). + * + * QgsProcessingParameterDatabaseTable should be evaluated by calling QgsProcessingAlgorithm::parameterAsDatabaseTableName(). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsProcessingParameterDatabaseTable : public QgsProcessingParameterDefinition +{ + public: + + /** + * Constructor for QgsProcessingParameterDatabaseTable. + * + * The \a connectionParameterName specifies the name of the parent QgsProcessingParameterProviderConnection parameter. + * The \a schemaParameterName specifies the name of the parent QgsProcessingParameterDatabaseSchema parameter. + * + * \warning The provider must support the connection API methods in its QgsProviderMetadata implementation + * in order for the model to work correctly. This is only implemented for a subset of current data providers. + */ + QgsProcessingParameterDatabaseTable( const QString &name, const QString &description, + const QString &connectionParameterName = QString(), + const QString &schemaParameterName = QString(), + const QVariant &defaultValue = QVariant(), + bool optional = false, + bool allowNewTableNames = false ); + + /** + * Returns the type name for the parameter class. + */ + static QString typeName() { return QStringLiteral( "databasetable" ); } + QgsProcessingParameterDefinition *clone() const override SIP_FACTORY; + QString type() const override { return typeName(); } + bool checkValueIsAcceptable( const QVariant &input, QgsProcessingContext *context = nullptr ) const override; + QString valueAsPythonString( const QVariant &value, QgsProcessingContext &context ) const override; + QString asScriptCode() const override; + QString asPythonString( QgsProcessing::PythonOutputType outputType = QgsProcessing::PythonQgsProcessingAlgorithmSubclass ) const override; + QVariantMap toVariantMap() const override; + bool fromVariantMap( const QVariantMap &map ) override; + QStringList dependsOnOtherParameters() const override; + + /** + * Returns the name of the parent connection parameter, or an empty string if this is not set. + * \see setParentConnectionParameterName() + */ + QString parentConnectionParameterName() const; + + /** + * Sets the \a name of the parent connection parameter. Use an empty string if this is not required. + * \see parentConnectionParameterName() + */ + void setParentConnectionParameterName( const QString &name ); + + /** + * Returns the name of the parent schema parameter, or an empty string if this is not set. + * \see setParentSchemaParameterName() + */ + QString parentSchemaParameterName() const; + + /** + * Sets the \a name of the parent schema parameter. Use an empty string if this is not required. + * \see parentSchemaParameterName() + */ + void setParentSchemaParameterName( const QString &name ); + + /** + * Creates a new parameter using the definition from a script code. + */ + static QgsProcessingParameterDatabaseTable *fromScriptCode( const QString &name, const QString &description, bool isOptional, const QString &definition ) SIP_FACTORY; + + /** + * Returns TRUE if the parameter allows users to enter names for + * a new (non-existing) tables. + * + * \see setAllowNewTableNames() + */ + bool allowNewTableNames() const; + + /** + * Sets whether the parameter allows users to enter names for + * a new (non-existing) tables. + * + * \see allowNewTableNames() + */ + void setAllowNewTableNames( bool allowed ); + + private: + + QString mParentConnectionParameterName; + QString mParentSchemaParameterName; + bool mAllowNewTableNames = false; +}; + + // clazy:excludeall=qstring-allocations #endif // QGSPROCESSINGPARAMETERS_H diff --git a/src/core/processing/qgsprocessingparametertypeimpl.h b/src/core/processing/qgsprocessingparametertypeimpl.h index 15ca96dfbb28..548443cc8852 100644 --- a/src/core/processing/qgsprocessingparametertypeimpl.h +++ b/src/core/processing/qgsprocessingparametertypeimpl.h @@ -620,7 +620,8 @@ class CORE_EXPORT QgsProcessingParameterTypeExtent : public QgsProcessingParamet << QObject::tr( "QgsProcessingFeatureSourceDefinition: Extent of source is used" ) << QStringLiteral( "QgsProperty" ) << QStringLiteral( "QgsRectangle" ) - << QStringLiteral( "QgsReferencedRectangle" ); + << QStringLiteral( "QgsReferencedRectangle" ) + << QStringLiteral( "QgsGeometry: bounding box of geometry is used" );; } QStringList acceptedStringValues() const override @@ -1890,5 +1891,110 @@ class CORE_EXPORT QgsProcessingParameterTypeProviderConnection : public QgsProce return QStringList() << QObject::tr( "Name of registered database connection" ); } +}; + +/** + * A database schema name parameter for processing algorithms. + * + * \ingroup core + * \note No Python bindings available. Get your copy from QgsApplication.processingRegistry().parameterType('databaseschema') + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsProcessingParameterTypeDatabaseSchema : public QgsProcessingParameterType +{ + QgsProcessingParameterDefinition *create( const QString &name ) const override SIP_FACTORY + { + return new QgsProcessingParameterDatabaseSchema( name, QString(), QString() ); + } + + QString description() const override + { + return QCoreApplication::translate( "Processing", "A database schema parameter." ); + } + + QString name() const override + { + return QCoreApplication::translate( "Processing", "Database Schema" ); + } + + QString id() const override + { + return QStringLiteral( "databaseschema" ); + } + + QString pythonImportString() const override + { + return QStringLiteral( "from qgis.core import QgsProcessingParameterDatabaseSchema" ); + } + + QString className() const override + { + return QStringLiteral( "QgsProcessingParameterDatabaseSchema" ); + } + + QStringList acceptedPythonTypes() const override + { + return QStringList() << QStringLiteral( "str" ) + << QStringLiteral( "QgsProperty" ); + } + + QStringList acceptedStringValues() const override + { + return QStringList() << QObject::tr( "Name of existing database schema" ); + } + +}; + + +/** + * A database table name parameter for processing algorithms. + * + * \ingroup core + * \note No Python bindings available. Get your copy from QgsApplication.processingRegistry().parameterType('databasetable') + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsProcessingParameterTypeDatabaseTable: public QgsProcessingParameterType +{ + QgsProcessingParameterDefinition *create( const QString &name ) const override SIP_FACTORY + { + return new QgsProcessingParameterDatabaseTable( name, QString(), QString(), QString() ); + } + + QString description() const override + { + return QCoreApplication::translate( "Processing", "A database table parameter." ); + } + + QString name() const override + { + return QCoreApplication::translate( "Processing", "Database Table" ); + } + + QString id() const override + { + return QStringLiteral( "databasetable" ); + } + + QString pythonImportString() const override + { + return QStringLiteral( "from qgis.core import QgsProcessingParameterDatabaseTable" ); + } + + QString className() const override + { + return QStringLiteral( "QgsProcessingParameterDatabaseTable" ); + } + + QStringList acceptedPythonTypes() const override + { + return QStringList() << QStringLiteral( "str" ) + << QStringLiteral( "QgsProperty" ); + } + + QStringList acceptedStringValues() const override + { + return QStringList() << QObject::tr( "Name of existing database table" ); + } + }; #endif // QGSPROCESSINGPARAMETERTYPEIMPL_H diff --git a/src/core/processing/qgsprocessingprovider.cpp b/src/core/processing/qgsprocessingprovider.cpp index 2579c4bee4ef..e7fea3863b8b 100644 --- a/src/core/processing/qgsprocessingprovider.cpp +++ b/src/core/processing/qgsprocessingprovider.cpp @@ -41,6 +41,11 @@ QString QgsProcessingProvider::svgIconPath() const return QgsApplication::iconPath( QStringLiteral( "processingAlgorithm.svg" ) ); } +QgsProcessingProvider::Flags QgsProcessingProvider::flags() const +{ + return nullptr; +} + QString QgsProcessingProvider::helpId() const { return QString(); diff --git a/src/core/processing/qgsprocessingprovider.h b/src/core/processing/qgsprocessingprovider.h index ab5d9f8140a6..efc880ce20b0 100644 --- a/src/core/processing/qgsprocessingprovider.h +++ b/src/core/processing/qgsprocessingprovider.h @@ -37,6 +37,16 @@ class CORE_EXPORT QgsProcessingProvider : public QObject public: + /** + * Flags indicating how and when an provider operates and should be exposed to users + * \since QGIS 3.14 + */ + enum Flag + { + FlagDeemphasiseSearchResults = 1 << 1, //!< Algorithms should be de-emphasised in the search results when searching for algorithms. Use for low-priority providers or those with substantial known issues. + }; + Q_DECLARE_FLAGS( Flags, Flag ) + /** * Constructor for QgsProcessingProvider. */ @@ -61,6 +71,13 @@ class CORE_EXPORT QgsProcessingProvider : public QObject */ virtual QString svgIconPath() const; + /** + * Returns the flags indicating how and when the provider operates and should be exposed to users. + * Default is no flags. + * \since QGIS 3.14 + */ + virtual Flags flags() const; + /** * Returns the unique provider id, used for identifying the provider. This string * should be a unique, short, character only string, eg "qgis" or "gdal". This @@ -281,6 +298,8 @@ class CORE_EXPORT QgsProcessingProvider : public QObject #endif }; +Q_DECLARE_OPERATORS_FOR_FLAGS( QgsProcessingProvider::Flags ) + #endif // QGSPROCESSINGPROVIDER_H diff --git a/src/core/processing/qgsprocessingregistry.cpp b/src/core/processing/qgsprocessingregistry.cpp index aa6e32f803e2..af614da59503 100644 --- a/src/core/processing/qgsprocessingregistry.cpp +++ b/src/core/processing/qgsprocessingregistry.cpp @@ -58,6 +58,8 @@ QgsProcessingRegistry::QgsProcessingRegistry( QObject *parent SIP_TRANSFERTHIS ) addParameterType( new QgsProcessingParameterTypeMapTheme() ); addParameterType( new QgsProcessingParameterTypeDateTime() ); addParameterType( new QgsProcessingParameterTypeProviderConnection() ); + addParameterType( new QgsProcessingParameterTypeDatabaseSchema() ); + addParameterType( new QgsProcessingParameterTypeDatabaseTable() ); } QgsProcessingRegistry::~QgsProcessingRegistry() diff --git a/src/core/processing/qgsprocessingutils.cpp b/src/core/processing/qgsprocessingutils.cpp index fe7056ad88c0..77b6cb600f09 100644 --- a/src/core/processing/qgsprocessingutils.cpp +++ b/src/core/processing/qgsprocessingutils.cpp @@ -33,6 +33,7 @@ #include "qgsmeshlayer.h" #include "qgsreferencedgeometry.h" #include "qgsrasterfilewriter.h" +#include "qgsvectortilelayer.h" QList QgsProcessingUtils::compatibleRasterLayers( QgsProject *project, bool sort ) { @@ -133,6 +134,25 @@ QList QgsProcessingUtils::compatibleLayers( QgsProject *project, return layers; } +QString QgsProcessingUtils::encodeProviderKeyAndUri( const QString &providerKey, const QString &uri ) +{ + return QStringLiteral( "%1://%2" ).arg( providerKey, uri ); +} + +bool QgsProcessingUtils::decodeProviderKeyAndUri( const QString &string, QString &providerKey, QString &uri ) +{ + QRegularExpression re( QStringLiteral( "^(\\w+?):\\/\\/(.+)$" ) ); + const QRegularExpressionMatch match = re.match( string ); + if ( !match.hasMatch() ) + return false; + + providerKey = match.captured( 1 ); + uri = match.captured( 2 ); + + // double check that provider is valid + return QgsProviderRegistry::instance()->providerMetadata( providerKey ); +} + QgsMapLayer *QgsProcessingUtils::mapLayerFromStore( const QString &string, QgsMapLayerStore *store, QgsProcessingUtils::LayerHint typeHint ) { if ( !store || string.isEmpty() ) @@ -152,6 +172,8 @@ QgsMapLayer *QgsProcessingUtils::mapLayerFromStore( const QString &string, QgsMa return true; case QgsMapLayerType::MeshLayer: return !canUseLayer( qobject_cast< QgsMeshLayer * >( layer ) ); + case QgsMapLayerType::VectorTileLayer: + return !canUseLayer( qobject_cast< QgsVectorTileLayer * >( layer ) ); } return true; } ), layers.end() ); @@ -195,19 +217,33 @@ QgsMapLayer *QgsProcessingUtils::mapLayerFromStore( const QString &string, QgsMa QgsMapLayer *QgsProcessingUtils::loadMapLayerFromString( const QString &string, const QgsCoordinateTransformContext &transformContext, LayerHint typeHint ) { - QStringList components = string.split( '|' ); - if ( components.isEmpty() ) - return nullptr; - - QFileInfo fi; - if ( QFileInfo::exists( string ) ) - fi = QFileInfo( string ); - else if ( QFileInfo::exists( components.at( 0 ) ) ) - fi = QFileInfo( components.at( 0 ) ); + QString provider; + QString uri; + const bool useProvider = decodeProviderKeyAndUri( string, provider, uri ); + if ( !useProvider ) + uri = string; + + QString name; + // for disk based sources, we use the filename to determine a layer name + if ( !useProvider || ( provider == QLatin1String( "ogr" ) || provider == QLatin1String( "gdal" ) || provider == QLatin1String( "mdal" ) ) ) + { + QStringList components = uri.split( '|' ); + if ( components.isEmpty() ) + return nullptr; + + QFileInfo fi; + if ( QFileInfo::exists( uri ) ) + fi = QFileInfo( uri ); + else if ( QFileInfo::exists( components.at( 0 ) ) ) + fi = QFileInfo( components.at( 0 ) ); + else + return nullptr; + name = fi.baseName(); + } else - return nullptr; - - QString name = fi.baseName(); + { + name = QgsDataSourceUri( uri ).table(); + } // brute force attempt to load a matching layer if ( typeHint == LayerHint::UnknownType || typeHint == LayerHint::Vector ) @@ -215,7 +251,17 @@ QgsMapLayer *QgsProcessingUtils::loadMapLayerFromString( const QString &string, QgsVectorLayer::LayerOptions options { transformContext }; options.loadDefaultStyle = false; options.skipCrsValidation = true; - std::unique_ptr< QgsVectorLayer > layer = qgis::make_unique( string, name, QStringLiteral( "ogr" ), options ); + + std::unique_ptr< QgsVectorLayer > layer; + if ( useProvider ) + { + layer = qgis::make_unique( uri, name, provider, options ); + } + else + { + // fallback to ogr + layer = qgis::make_unique( uri, name, QStringLiteral( "ogr" ), options ); + } if ( layer->isValid() ) { return layer.release(); @@ -226,7 +272,18 @@ QgsMapLayer *QgsProcessingUtils::loadMapLayerFromString( const QString &string, QgsRasterLayer::LayerOptions rasterOptions; rasterOptions.loadDefaultStyle = false; rasterOptions.skipCrsValidation = true; - std::unique_ptr< QgsRasterLayer > rasterLayer( new QgsRasterLayer( string, name, QStringLiteral( "gdal" ), rasterOptions ) ); + + std::unique_ptr< QgsRasterLayer > rasterLayer; + if ( useProvider ) + { + rasterLayer = qgis::make_unique< QgsRasterLayer >( uri, name, provider, rasterOptions ); + } + else + { + // fallback to gdal + rasterLayer = qgis::make_unique< QgsRasterLayer >( uri, name, QStringLiteral( "gdal" ), rasterOptions ); + } + if ( rasterLayer->isValid() ) { return rasterLayer.release(); @@ -236,7 +293,16 @@ QgsMapLayer *QgsProcessingUtils::loadMapLayerFromString( const QString &string, { QgsMeshLayer::LayerOptions meshOptions; meshOptions.skipCrsValidation = true; - std::unique_ptr< QgsMeshLayer > meshLayer( new QgsMeshLayer( string, name, QStringLiteral( "mdal" ), meshOptions ) ); + + std::unique_ptr< QgsMeshLayer > meshLayer; + if ( useProvider ) + { + meshLayer = qgis::make_unique< QgsMeshLayer >( uri, name, provider, meshOptions ); + } + else + { + meshLayer = qgis::make_unique< QgsMeshLayer >( uri, name, QStringLiteral( "mdal" ), meshOptions ); + } if ( meshLayer->isValid() ) { return meshLayer.release(); @@ -282,12 +348,18 @@ QgsProcessingFeatureSource *QgsProcessingUtils::variantToSource( const QVariant { QVariant val = value; bool selectedFeaturesOnly = false; + long long featureLimit = -1; + bool overrideGeometryCheck = false; + QgsFeatureRequest::InvalidGeometryCheck geometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid; if ( val.canConvert() ) { // input is a QgsProcessingFeatureSourceDefinition - get extra properties from it QgsProcessingFeatureSourceDefinition fromVar = qvariant_cast( val ); selectedFeaturesOnly = fromVar.selectedFeaturesOnly; + featureLimit = fromVar.featureLimit; val = fromVar.source; + overrideGeometryCheck = fromVar.flags & QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck; + geometryCheck = fromVar.geometryCheck; } else if ( val.canConvert() ) { @@ -298,7 +370,10 @@ QgsProcessingFeatureSource *QgsProcessingUtils::variantToSource( const QVariant if ( QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( qvariant_cast( val ) ) ) { - return new QgsProcessingFeatureSource( layer, context ); + std::unique_ptr< QgsProcessingFeatureSource> source = qgis::make_unique< QgsProcessingFeatureSource >( layer, context, false, featureLimit ); + if ( overrideGeometryCheck ) + source->setInvalidGeometryCheck( geometryCheck ); + return source.release(); } QString layerRef; @@ -311,7 +386,10 @@ QgsProcessingFeatureSource *QgsProcessingUtils::variantToSource( const QVariant // fall back to default if ( QgsVectorLayer *layer = qobject_cast< QgsVectorLayer * >( qvariant_cast( fallbackValue ) ) ) { - return new QgsProcessingFeatureSource( layer, context ); + std::unique_ptr< QgsProcessingFeatureSource> source = qgis::make_unique< QgsProcessingFeatureSource >( layer, context, false, featureLimit ); + if ( overrideGeometryCheck ) + source->setInvalidGeometryCheck( geometryCheck ); + return source.release(); } layerRef = fallbackValue.toString(); @@ -328,14 +406,19 @@ QgsProcessingFeatureSource *QgsProcessingUtils::variantToSource( const QVariant if ( !vl ) return nullptr; + std::unique_ptr< QgsProcessingFeatureSource> source; if ( selectedFeaturesOnly ) { - return new QgsProcessingFeatureSource( new QgsVectorLayerSelectedFeatureSource( vl ), context, true ); + source = qgis::make_unique< QgsProcessingFeatureSource>( new QgsVectorLayerSelectedFeatureSource( vl ), context, true, featureLimit ); } else { - return new QgsProcessingFeatureSource( vl, context ); + source = qgis::make_unique< QgsProcessingFeatureSource >( vl, context, false, featureLimit ); } + + if ( overrideGeometryCheck ) + source->setInvalidGeometryCheck( geometryCheck ); + return source.release(); } QgsCoordinateReferenceSystem QgsProcessingUtils::variantToCrs( const QVariant &value, QgsProcessingContext &context, const QVariant &fallbackValue ) @@ -404,6 +487,11 @@ bool QgsProcessingUtils::canUseLayer( const QgsMeshLayer *layer ) return layer && layer->dataProvider(); } +bool QgsProcessingUtils::canUseLayer( const QgsVectorTileLayer *layer ) +{ + return layer && layer->isValid(); +} + bool QgsProcessingUtils::canUseLayer( const QgsRasterLayer *layer ) { return layer && layer->isValid(); @@ -522,16 +610,26 @@ QString QgsProcessingUtils::stringToPythonLiteral( const QString &string ) void QgsProcessingUtils::parseDestinationString( QString &destination, QString &providerKey, QString &uri, QString &layerName, QString &format, QMap &options, bool &useWriter, QString &extension ) { extension.clear(); - QRegularExpression splitRx( QStringLiteral( "^(.{3,}?):(.*)$" ) ); - QRegularExpressionMatch match = splitRx.match( destination ); - if ( match.hasMatch() ) + bool matched = decodeProviderKeyAndUri( destination, providerKey, uri ); + + if ( !matched ) + { + QRegularExpression splitRx( QStringLiteral( "^(.{3,}?):(.*)$" ) ); + QRegularExpressionMatch match = splitRx.match( destination ); + if ( match.hasMatch() ) + { + providerKey = match.captured( 1 ); + uri = match.captured( 2 ); + matched = true; + } + } + + if ( matched ) { - providerKey = match.captured( 1 ); if ( providerKey == QStringLiteral( "postgis" ) ) // older processing used "postgis" instead of "postgres" { providerKey = QStringLiteral( "postgres" ); } - uri = match.captured( 2 ); if ( providerKey == QLatin1String( "ogr" ) ) { QgsDataSourceUri dsUri( uri ); @@ -577,7 +675,7 @@ void QgsProcessingUtils::parseDestinationString( QString &destination, QString & } } -QgsFeatureSink *QgsProcessingUtils::createFeatureSink( QString &destination, QgsProcessingContext &context, const QgsFields &fields, QgsWkbTypes::Type geometryType, const QgsCoordinateReferenceSystem &crs, const QVariantMap &createOptions, QgsFeatureSink::SinkFlags sinkFlags ) +QgsFeatureSink *QgsProcessingUtils::createFeatureSink( QString &destination, QgsProcessingContext &context, const QgsFields &fields, QgsWkbTypes::Type geometryType, const QgsCoordinateReferenceSystem &crs, const QVariantMap &createOptions, QgsFeatureSink::SinkFlags sinkFlags, QgsRemappingSinkDefinition *remappingDefinition ) { QVariantMap options = createOptions; if ( !options.contains( QStringLiteral( "fileEncoding" ) ) ) @@ -633,34 +731,87 @@ QgsFeatureSink *QgsProcessingUtils::createFeatureSink( QString &destination, Qgs saveOptions.datasourceOptions = QgsVectorFileWriter::defaultDatasetOptions( format ); saveOptions.layerOptions = QgsVectorFileWriter::defaultLayerOptions( format ); saveOptions.symbologyExport = QgsVectorFileWriter::NoSymbology; - saveOptions.actionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; + if ( remappingDefinition ) + { + saveOptions.actionOnExistingFile = QgsVectorFileWriter::AppendToLayerNoNewFields; + // sniff destination file to get correct wkb type and crs + std::unique_ptr< QgsVectorLayer > vl = qgis::make_unique< QgsVectorLayer >( destination ); + if ( vl->isValid() ) + { + remappingDefinition->setDestinationWkbType( vl->wkbType() ); + remappingDefinition->setDestinationCrs( vl->crs() ); + newFields = vl->fields(); + remappingDefinition->setDestinationFields( newFields ); + } + context.expressionContext().setFields( fields ); + } + else + { + saveOptions.actionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; + } std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( destination, newFields, geometryType, crs, context.transformContext(), saveOptions, sinkFlags, &finalFileName ) ); if ( writer->hasError() ) { throw QgsProcessingException( QObject::tr( "Could not create layer %1: %2" ).arg( destination, writer->errorMessage() ) ); } destination = finalFileName; - return new QgsProcessingFeatureSink( writer.release(), destination, context, true ); + if ( remappingDefinition ) + { + std::unique_ptr< QgsRemappingProxyFeatureSink > remapSink = qgis::make_unique< QgsRemappingProxyFeatureSink >( *remappingDefinition, writer.release(), true ); + remapSink->setExpressionContext( context.expressionContext() ); + remapSink->setTransformContext( context.transformContext() ); + return new QgsProcessingFeatureSink( remapSink.release(), destination, context, true ); + } + else + return new QgsProcessingFeatureSink( writer.release(), destination, context, true ); } else { - //create empty layer const QgsVectorLayer::LayerOptions layerOptions { context.transformContext() }; - std::unique_ptr< QgsVectorLayerExporter > exporter = qgis::make_unique( uri, providerKey, newFields, geometryType, crs, true, options, sinkFlags ); - if ( exporter->errorCode() ) + if ( remappingDefinition ) { - throw QgsProcessingException( QObject::tr( "Could not create layer %1: %2" ).arg( destination, exporter->errorMessage() ) ); + //write to existing layer + + // use destination string as layer name (eg "postgis:..." ) + if ( !layerName.isEmpty() ) + uri += QStringLiteral( "|layername=%1" ).arg( layerName ); + + std::unique_ptr< QgsVectorLayer > layer = qgis::make_unique( uri, destination, providerKey, layerOptions ); + // update destination to layer ID + destination = layer->id(); + if ( layer->isValid() ) + { + remappingDefinition->setDestinationWkbType( layer->wkbType() ); + remappingDefinition->setDestinationCrs( layer->crs() ); + remappingDefinition->setDestinationFields( layer->fields() ); + } + + std::unique_ptr< QgsRemappingProxyFeatureSink > remapSink = qgis::make_unique< QgsRemappingProxyFeatureSink >( *remappingDefinition, layer->dataProvider(), false ); + context.temporaryLayerStore()->addMapLayer( layer.release() ); + remapSink->setExpressionContext( context.expressionContext() ); + remapSink->setTransformContext( context.transformContext() ); + context.expressionContext().setFields( fields ); + return new QgsProcessingFeatureSink( remapSink.release(), destination, context, true ); } + else + { + //create empty layer + std::unique_ptr< QgsVectorLayerExporter > exporter = qgis::make_unique( uri, providerKey, newFields, geometryType, crs, true, options, sinkFlags ); + if ( exporter->errorCode() ) + { + throw QgsProcessingException( QObject::tr( "Could not create layer %1: %2" ).arg( destination, exporter->errorMessage() ) ); + } - // use destination string as layer name (eg "postgis:..." ) - if ( !layerName.isEmpty() ) - uri += QStringLiteral( "|layername=%1" ).arg( layerName ); - std::unique_ptr< QgsVectorLayer > layer = qgis::make_unique( uri, destination, providerKey, layerOptions ); - // update destination to layer ID - destination = layer->id(); + // use destination string as layer name (eg "postgis:..." ) + if ( !layerName.isEmpty() ) + uri += QStringLiteral( "|layername=%1" ).arg( layerName ); + std::unique_ptr< QgsVectorLayer > layer = qgis::make_unique( uri, destination, providerKey, layerOptions ); + // update destination to layer ID + destination = layer->id(); - context.temporaryLayerStore()->addMapLayer( layer.release() ); - return new QgsProcessingFeatureSink( exporter.release(), destination, context, true ); + context.temporaryLayerStore()->addMapLayer( layer.release() ); + return new QgsProcessingFeatureSink( exporter.release(), destination, context, true ); + } } } } @@ -841,7 +992,8 @@ QString QgsProcessingUtils::formatHelpMapAsHtml( const QVariantMap &map, const Q return s; } -QString convertToCompatibleFormatInternal( const QgsVectorLayer *vl, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, QString *layerName ) +QString convertToCompatibleFormatInternal( const QgsVectorLayer *vl, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, QString *layerName, + long long featureLimit ) { bool requiresTranslation = false; @@ -849,6 +1001,9 @@ QString convertToCompatibleFormatInternal( const QgsVectorLayer *vl, bool select // as we need to subset only selected features, a concept which doesn't exist outside QGIS! requiresTranslation = requiresTranslation || selectedFeaturesOnly; + // if we are limiting the feature count, we better export + requiresTranslation = requiresTranslation || featureLimit != -1; + // if the data provider is NOT ogr, then we HAVE to convert. Otherwise we run into // issues with data providers like spatialite, delimited text where the format can be // opened outside of QGIS, but with potentially very different behavior! @@ -903,10 +1058,20 @@ QString convertToCompatibleFormatInternal( const QgsVectorLayer *vl, bool select std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( temp, vl->fields(), vl->wkbType(), vl->crs(), context.transformContext(), saveOptions ) ); QgsFeature f; QgsFeatureIterator it; - if ( selectedFeaturesOnly ) - it = vl->getSelectedFeatures(); + if ( featureLimit != -1 ) + { + if ( selectedFeaturesOnly ) + it = vl->getSelectedFeatures( QgsFeatureRequest().setLimit( featureLimit ) ); + else + it = vl->getFeatures( QgsFeatureRequest().setLimit( featureLimit ) ); + } else - it = vl->getFeatures(); + { + if ( selectedFeaturesOnly ) + it = vl->getSelectedFeatures( QgsFeatureRequest().setLimit( featureLimit ) ); + else + it = vl->getFeatures(); + } while ( it.nextFeature( f ) ) { @@ -922,15 +1087,15 @@ QString convertToCompatibleFormatInternal( const QgsVectorLayer *vl, bool select } } -QString QgsProcessingUtils::convertToCompatibleFormat( const QgsVectorLayer *vl, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback ) +QString QgsProcessingUtils::convertToCompatibleFormat( const QgsVectorLayer *vl, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, long long featureLimit ) { - return convertToCompatibleFormatInternal( vl, selectedFeaturesOnly, baseName, compatibleFormats, preferredFormat, context, feedback, nullptr ); + return convertToCompatibleFormatInternal( vl, selectedFeaturesOnly, baseName, compatibleFormats, preferredFormat, context, feedback, nullptr, featureLimit ); } -QString QgsProcessingUtils::convertToCompatibleFormatAndLayerName( const QgsVectorLayer *layer, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, QString &layerName ) +QString QgsProcessingUtils::convertToCompatibleFormatAndLayerName( const QgsVectorLayer *layer, bool selectedFeaturesOnly, const QString &baseName, const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, QString &layerName, long long featureLimit ) { layerName.clear(); - return convertToCompatibleFormatInternal( layer, selectedFeaturesOnly, baseName, compatibleFormats, preferredFormat, context, feedback, &layerName ); + return convertToCompatibleFormatInternal( layer, selectedFeaturesOnly, baseName, compatibleFormats, preferredFormat, context, feedback, &layerName, featureLimit ); } QgsFields QgsProcessingUtils::combineFields( const QgsFields &fieldsA, const QgsFields &fieldsB, const QString &fieldsBPrefix ) @@ -1022,7 +1187,7 @@ QString QgsProcessingUtils::defaultRasterExtension() // QgsProcessingFeatureSource // -QgsProcessingFeatureSource::QgsProcessingFeatureSource( QgsFeatureSource *originalSource, const QgsProcessingContext &context, bool ownsOriginalSource ) +QgsProcessingFeatureSource::QgsProcessingFeatureSource( QgsFeatureSource *originalSource, const QgsProcessingContext &context, bool ownsOriginalSource, long long featureLimit ) : mSource( originalSource ) , mOwnsSource( ownsOriginalSource ) , mInvalidGeometryCheck( QgsWkbTypes::geometryType( mSource->wkbType() ) == QgsWkbTypes::PointGeometry @@ -1030,6 +1195,9 @@ QgsProcessingFeatureSource::QgsProcessingFeatureSource( QgsFeatureSource *origin : context.invalidGeometryCheck() ) , mInvalidGeometryCallback( context.invalidGeometryCallback() ) , mTransformErrorCallback( context.transformErrorCallback() ) + , mInvalidGeometryCallbackSkip( context.defaultInvalidGeometryCallbackForCheck( QgsFeatureRequest::GeometrySkipInvalid ) ) + , mInvalidGeometryCallbackAbort( context.defaultInvalidGeometryCallbackForCheck( QgsFeatureRequest::GeometryAbortOnInvalid ) ) + , mFeatureLimit( featureLimit ) {} QgsProcessingFeatureSource::~QgsProcessingFeatureSource() @@ -1051,6 +1219,11 @@ QgsFeatureIterator QgsProcessingFeatureSource::getFeatures( const QgsFeatureRequ req.setInvalidGeometryCallback( mInvalidGeometryCallback ); } + if ( mFeatureLimit != -1 && req.limit() != -1 ) + req.setLimit( std::min( static_cast< long long >( req.limit() ), mFeatureLimit ) ); + else if ( mFeatureLimit != -1 ) + req.setLimit( mFeatureLimit ); + return mSource->getFeatures( req ); } @@ -1072,6 +1245,12 @@ QgsFeatureIterator QgsProcessingFeatureSource::getFeatures( const QgsFeatureRequ req.setInvalidGeometryCheck( mInvalidGeometryCheck ); req.setInvalidGeometryCallback( mInvalidGeometryCallback ); req.setTransformErrorCallback( mTransformErrorCallback ); + + if ( mFeatureLimit != -1 && req.limit() != -1 ) + req.setLimit( std::min( static_cast< long long >( req.limit() ), mFeatureLimit ) ); + else if ( mFeatureLimit != -1 ) + req.setLimit( mFeatureLimit ); + return mSource->getFeatures( req ); } @@ -1092,7 +1271,10 @@ QgsWkbTypes::Type QgsProcessingFeatureSource::wkbType() const long QgsProcessingFeatureSource::featureCount() const { - return mSource->featureCount(); + if ( mFeatureLimit == -1 ) + return mSource->featureCount(); + else + return std::min( mFeatureLimit, static_cast< long long >( mSource->featureCount() ) ); } QString QgsProcessingFeatureSource::sourceName() const @@ -1142,6 +1324,26 @@ QgsExpressionContextScope *QgsProcessingFeatureSource::createExpressionContextSc return expressionContextScope; } +void QgsProcessingFeatureSource::setInvalidGeometryCheck( QgsFeatureRequest::InvalidGeometryCheck method ) +{ + mInvalidGeometryCheck = method; + switch ( mInvalidGeometryCheck ) + { + case QgsFeatureRequest::GeometryNoCheck: + mInvalidGeometryCallback = nullptr; + break; + + case QgsFeatureRequest::GeometrySkipInvalid: + mInvalidGeometryCallback = mInvalidGeometryCallbackSkip; + break; + + case QgsFeatureRequest::GeometryAbortOnInvalid: + mInvalidGeometryCallback = mInvalidGeometryCallbackAbort; + break; + + } +} + // // QgsProcessingFeatureSink diff --git a/src/core/processing/qgsprocessingutils.h b/src/core/processing/qgsprocessingutils.h index e8eb2a09a81c..383c5ce49e9e 100644 --- a/src/core/processing/qgsprocessingutils.h +++ b/src/core/processing/qgsprocessingutils.h @@ -26,6 +26,8 @@ #include "qgsprocessing.h" #include "qgsfeaturesink.h" #include "qgsfeaturesource.h" +#include "qgsproxyfeaturesink.h" +#include "qgsremappingproxyfeaturesink.h" class QgsMeshLayer; class QgsProject; @@ -34,6 +36,7 @@ class QgsMapLayerStore; class QgsProcessingFeedback; class QgsProcessingFeatureSource; class QgsProcessingAlgorithm; +class QgsVectorTileLayer; #include #include @@ -105,6 +108,27 @@ class CORE_EXPORT QgsProcessingUtils */ static QList< QgsMapLayer * > compatibleLayers( QgsProject *project, bool sort = true ); + /** + * Encodes a provider key and layer \a uri to a single string, for use with + * decodeProviderKeyAndUri() + * + * \since QGIS 3.14 + */ + static QString encodeProviderKeyAndUri( const QString &providerKey, const QString &uri ); + + /** + * Decodes a provider key and layer \a uri from an encoded string, for use with + * encodeProviderKeyAndUri() + * + * \param string encoded string, as returned by encodeProviderKeyAndUri() + * \param providerKey ID key for corresponding data provider + * \param uri decoded layer uri + * \returns TRUE if \a string was successfully decoded + * + * \since QGIS 3.14 + */ + static bool decodeProviderKeyAndUri( const QString &string, QString &providerKey SIP_OUT, QString &uri SIP_OUT ); + /** * Layer type hints. * \since QGIS 3.4 @@ -199,7 +223,8 @@ class CORE_EXPORT QgsProcessingUtils QgsWkbTypes::Type geometryType, const QgsCoordinateReferenceSystem &crs, const QVariantMap &createOptions = QVariantMap(), - QgsFeatureSink::SinkFlags sinkFlags = nullptr ) SIP_FACTORY; + QgsFeatureSink::SinkFlags sinkFlags = nullptr, + QgsRemappingSinkDefinition *remappingDefinition = nullptr ) SIP_FACTORY; #endif /** @@ -279,6 +304,8 @@ class CORE_EXPORT QgsProcessingUtils * The \a preferredFormat argument is used to specify to desired file extension to use when a temporary * layer export is required. This defaults to shapefiles. * + * The \a featureLimit argument can be used to specify a limit on the number of features read from the layer. + * * When an algorithm is capable of handling multi-layer input files (such as Geopackage), it is preferable * to use convertToCompatibleFormatAndLayerName() which may avoid conversion in more situations. * @@ -290,7 +317,7 @@ class CORE_EXPORT QgsProcessingUtils const QStringList &compatibleFormats, const QString &preferredFormat, QgsProcessingContext &context, - QgsProcessingFeedback *feedback ); + QgsProcessingFeedback *feedback, long long featureLimit = -1 ); /** * Converts a source vector \a layer to a file path and layer name of a vector layer of compatible format. @@ -301,6 +328,8 @@ class CORE_EXPORT QgsProcessingUtils * * \a compatibleFormats should consist entirely of lowercase file extensions, e.g. 'shp'. * + * The \a featureLimit argument can be used to specify a limit on the number of features read from the layer. + * * The \a preferredFormat argument is used to specify to desired file extension to use when a temporary * layer export is required. This defaults to shapefiles. * @@ -316,6 +345,7 @@ class CORE_EXPORT QgsProcessingUtils * \param context processing context * \param feedback feedback object * \param layerName will be set to the target layer name for multi-layer sources (e.g. Geopackage) + * \param featureLimit can be used to place a limit on the maximum number of features read from the layer * * \returns path to source layer, or nearly converted compatible layer * @@ -329,7 +359,7 @@ class CORE_EXPORT QgsProcessingUtils const QString &preferredFormat, QgsProcessingContext &context, QgsProcessingFeedback *feedback, - QString &layerName SIP_OUT ); + QString &layerName SIP_OUT, long long featureLimit = -1 ); /** * Combines two field lists, avoiding duplicate field names (in a case-insensitive manner). @@ -383,6 +413,7 @@ class CORE_EXPORT QgsProcessingUtils private: static bool canUseLayer( const QgsRasterLayer *layer ); static bool canUseLayer( const QgsMeshLayer *layer ); + static bool canUseLayer( const QgsVectorTileLayer *layer ); static bool canUseLayer( const QgsVectorLayer *layer, const QList< int > &sourceTypes = QList< int >() ); @@ -451,8 +482,12 @@ class CORE_EXPORT QgsProcessingFeatureSource : public QgsFeatureSource * Ownership of \a originalSource is dictated by \a ownsOriginalSource. If \a ownsOriginalSource is FALSE, * ownership is not transferred, and callers must ensure that \a originalSource exists for the lifetime of this object. * If \a ownsOriginalSource is TRUE, then this object will take ownership of \a originalSource. + * + * If \a featureLimit is set to a value > 0, then a limit is placed on the maximum number of features which will be + * read from the source. */ - QgsProcessingFeatureSource( QgsFeatureSource *originalSource, const QgsProcessingContext &context, bool ownsOriginalSource = false ); + QgsProcessingFeatureSource( QgsFeatureSource *originalSource, const QgsProcessingContext &context, bool ownsOriginalSource = false, + long long featureLimit = -1 ); ~QgsProcessingFeatureSource() override; @@ -483,6 +518,13 @@ class CORE_EXPORT QgsProcessingFeatureSource : public QgsFeatureSource */ QgsExpressionContextScope *createExpressionContextScope() const SIP_FACTORY; + /** + * Overrides the default geometry check method for the source. + * + * \since QGIS 3.14 + */ + void setInvalidGeometryCheck( QgsFeatureRequest::InvalidGeometryCheck method ); + private: QgsFeatureSource *mSource = nullptr; @@ -491,6 +533,11 @@ class CORE_EXPORT QgsProcessingFeatureSource : public QgsFeatureSource std::function< void( const QgsFeature & ) > mInvalidGeometryCallback; std::function< void( const QgsFeature & ) > mTransformErrorCallback; + std::function< void( const QgsFeature & ) > mInvalidGeometryCallbackSkip; + std::function< void( const QgsFeature & ) > mInvalidGeometryCallbackAbort; + + long long mFeatureLimit = -1; + }; #ifndef SIP_RUN diff --git a/src/core/providers/gdal/qgsgdaldataitems.cpp b/src/core/providers/gdal/qgsgdaldataitems.cpp index 2c86b4e9c8ef..c37a1922b914 100644 --- a/src/core/providers/gdal/qgsgdaldataitems.cpp +++ b/src/core/providers/gdal/qgsgdaldataitems.cpp @@ -18,10 +18,12 @@ ///@cond PRIVATE #include "qgsgdalprovider.h" #include "qgslogger.h" +#include "qgsmbtilesreader.h" #include "qgssettings.h" #include "qgsogrutils.h" #include "qgsproject.h" #include "qgsgdalutils.h" +#include "qgsvectortiledataitems.h" #include "symbology/qgsstyle.h" #include @@ -263,14 +265,30 @@ QgsDataItem *QgsGdalDataItemProvider::createDataItem( const QString &pathIn, Qgs if ( suffix == QStringLiteral( "mbtiles" ) ) { - // handled by WMS provider - QUrlQuery uq; - uq.addQueryItem( "type", "mbtiles" ); - uq.addQueryItem( "url", QUrl::fromLocalFile( path ).toString() ); - QString encodedUri = uq.toString(); - QgsLayerItem *item = new QgsLayerItem( parentItem, name, path, encodedUri, QgsLayerItem::Raster, QStringLiteral( "wms" ) ); - item->setState( QgsDataItem::Populated ); - return item; + QgsMBTilesReader reader( path ); + if ( reader.open() ) + { + if ( reader.metadataValue( "format" ) == QStringLiteral( "pbf" ) ) + { + // these are vector tiles + QUrlQuery uq; + uq.addQueryItem( QStringLiteral( "type" ), QStringLiteral( "mbtiles" ) ); + uq.addQueryItem( QStringLiteral( "url" ), path ); + QString encodedUri = uq.toString(); + return new QgsVectorTileLayerItem( parentItem, name, path, encodedUri ); + } + else + { + // handled by WMS provider + QUrlQuery uq; + uq.addQueryItem( QStringLiteral( "type" ), QStringLiteral( "mbtiles" ) ); + uq.addQueryItem( QStringLiteral( "url" ), QUrl::fromLocalFile( path ).toString() ); + QString encodedUri = uq.toString(); + QgsLayerItem *item = new QgsLayerItem( parentItem, name, path, encodedUri, QgsLayerItem::Raster, QStringLiteral( "wms" ) ); + item->setState( QgsDataItem::Populated ); + return item; + } + } } // Filters out the OGR/GDAL supported formats that can contain multiple layers diff --git a/src/core/providers/gdal/qgsgdalprovider.cpp b/src/core/providers/gdal/qgsgdalprovider.cpp index 06f5b0cfca9d..a1aba8869507 100644 --- a/src/core/providers/gdal/qgsgdalprovider.cpp +++ b/src/core/providers/gdal/qgsgdalprovider.cpp @@ -691,9 +691,6 @@ bool QgsGdalProvider::readBlock( int bandNo, int xBlock, int yBlock, void *data // TODO!!!: Check data alignment!!! May it happen that nearest value which // is not nearest is assigned to an output cell??? - - //QgsDebugMsg( "yBlock = " + QString::number( yBlock ) ); - GDALRasterBandH myGdalBand = getBand( bandNo ); //GDALReadBlock( myGdalBand, xBlock, yBlock, block ); @@ -1120,8 +1117,6 @@ QgsRasterIdentifyResult QgsGdalProvider::identify( const QgsPointXY &point, QgsR QgsDebugMsgLevel( QStringLiteral( "row = %1 col = %2" ).arg( row ).arg( col ), 3 ); - // QgsDebugMsg( "row = " + QString::number( row ) + " col = " + QString::number( col ) ); - int r = 0; int c = 0; int w = 1; @@ -1453,7 +1448,7 @@ bool QgsGdalProvider::hasHistogram( int bandNo, // If not cached, check if supported by GDAL if ( myHistogram.extent != extent() ) { - QgsDebugMsg( QStringLiteral( "Not supported by GDAL." ) ); + QgsDebugMsgLevel( QStringLiteral( "Not supported by GDAL." ), 2 ); return false; } @@ -1487,7 +1482,7 @@ bool QgsGdalProvider::hasHistogram( int bandNo, // if there was any error/warning assume the histogram is not valid or non-existent if ( myError != CE_None ) { - QgsDebugMsg( QStringLiteral( "Cannot get default GDAL histogram" ) ); + QgsDebugMsgLevel( QStringLiteral( "Cannot get default GDAL histogram" ), 2 ); return false; } @@ -1504,11 +1499,11 @@ bool QgsGdalProvider::hasHistogram( int bandNo, std::fabs( myMinVal - myExpectedMinVal ) > std::fabs( myExpectedMinVal ) / 10e6 || std::fabs( myMaxVal - myExpectedMaxVal ) > std::fabs( myExpectedMaxVal ) / 10e6 ) { - QgsDebugMsg( QStringLiteral( "Params do not match binCount: %1 x %2, minVal: %3 x %4, maxVal: %5 x %6" ).arg( myBinCount ).arg( myHistogram.binCount ).arg( myMinVal ).arg( myExpectedMinVal ).arg( myMaxVal ).arg( myExpectedMaxVal ) ); + QgsDebugMsgLevel( QStringLiteral( "Params do not match binCount: %1 x %2, minVal: %3 x %4, maxVal: %5 x %6" ).arg( myBinCount ).arg( myHistogram.binCount ).arg( myMinVal ).arg( myExpectedMinVal ).arg( myMaxVal ).arg( myExpectedMaxVal ), 2 ); return false; } - QgsDebugMsg( QStringLiteral( "GDAL has cached histogram" ) ); + QgsDebugMsgLevel( QStringLiteral( "GDAL has cached histogram" ), 2 ); // This should be enough, possible call to histogram() should retrieve the histogram cached in GDAL @@ -1526,7 +1521,7 @@ QgsRasterHistogram QgsGdalProvider::histogram( int bandNo, if ( !initIfNeeded() ) return QgsRasterHistogram(); - QgsDebugMsg( QStringLiteral( "theBandNo = %1 binCount = %2 minimum = %3 maximum = %4 sampleSize = %5" ).arg( bandNo ).arg( binCount ).arg( minimum ).arg( maximum ).arg( sampleSize ) ); + QgsDebugMsgLevel( QStringLiteral( "theBandNo = %1 binCount = %2 minimum = %3 maximum = %4 sampleSize = %5" ).arg( bandNo ).arg( binCount ).arg( minimum ).arg( maximum ).arg( sampleSize ), 2 ); QgsRasterHistogram myHistogram; initHistogram( myHistogram, bandNo, binCount, minimum, maximum, boundingBox, sampleSize, includeOutOfRange ); @@ -1537,7 +1532,7 @@ QgsRasterHistogram QgsGdalProvider::histogram( int bandNo, { if ( histogram == myHistogram ) { - QgsDebugMsg( QStringLiteral( "Using cached histogram." ) ); + QgsDebugMsgLevel( QStringLiteral( "Using cached histogram." ), 2 ); return histogram; } } @@ -1545,17 +1540,17 @@ QgsRasterHistogram QgsGdalProvider::histogram( int bandNo, if ( ( sourceHasNoDataValue( bandNo ) && !useSourceNoDataValue( bandNo ) ) || !userNoDataValues( bandNo ).isEmpty() ) { - QgsDebugMsg( QStringLiteral( "Custom no data values, using generic histogram." ) ); + QgsDebugMsgLevel( QStringLiteral( "Custom no data values, using generic histogram." ), 2 ); return QgsRasterDataProvider::histogram( bandNo, binCount, minimum, maximum, boundingBox, sampleSize, includeOutOfRange, feedback ); } if ( myHistogram.extent != extent() ) { - QgsDebugMsg( QStringLiteral( "Not full extent, using generic histogram." ) ); + QgsDebugMsgLevel( QStringLiteral( "Not full extent, using generic histogram." ), 2 ); return QgsRasterDataProvider::histogram( bandNo, binCount, minimum, maximum, boundingBox, sampleSize, includeOutOfRange, feedback ); } - QgsDebugMsg( QStringLiteral( "Computing GDAL histogram" ) ); + QgsDebugMsgLevel( QStringLiteral( "Computing GDAL histogram" ), 2 ); GDALRasterBandH myGdalBand = getBand( bandNo ); @@ -1565,12 +1560,12 @@ QgsRasterHistogram QgsGdalProvider::histogram( int bandNo, // cast to double, integer could overflow if ( ( static_cast( xSize() ) * static_cast( ySize() ) / sampleSize ) > 2 ) // not perfect { - QgsDebugMsg( QStringLiteral( "Approx" ) ); + QgsDebugMsgLevel( QStringLiteral( "Approx" ), 2 ); bApproxOK = true; } } - QgsDebugMsg( QStringLiteral( "xSize() = %1 ySize() = %2 sampleSize = %3 bApproxOK = %4" ).arg( xSize() ).arg( ySize() ).arg( sampleSize ).arg( bApproxOK ) ); + QgsDebugMsgLevel( QStringLiteral( "xSize() = %1 ySize() = %2 sampleSize = %3 bApproxOK = %4" ).arg( xSize() ).arg( ySize() ).arg( sampleSize ).arg( bApproxOK ), 2 ); QgsGdalProgress myProg; myProg.type = QgsRaster::ProgressHistogram; @@ -1642,7 +1637,7 @@ QgsRasterHistogram QgsGdalProvider::histogram( int bandNo, if ( myError != CE_None || ( feedback && feedback->isCanceled() ) ) { - QgsDebugMsg( QStringLiteral( "Cannot get histogram" ) ); + QgsDebugMsgLevel( QStringLiteral( "Cannot get histogram" ), 2 ); delete [] myHistogramArray; return myHistogram; } @@ -1653,14 +1648,13 @@ QgsRasterHistogram QgsGdalProvider::histogram( int bandNo, { myHistogram.histogramVector.push_back( myHistogramArray[myBin] ); myHistogram.nonNullCount += myHistogramArray[myBin]; - // QgsDebugMsg( "Added " + QString::number( myHistogramArray[myBin] ) + " to histogram vector" ); } myHistogram.valid = true; delete [] myHistogramArray; - QgsDebugMsg( ">>>>> Histogram vector now contains " + QString::number( myHistogram.histogramVector.size() ) + " elements" ); + QgsDebugMsgLevel( ">>>>> Histogram vector now contains " + QString::number( myHistogram.histogramVector.size() ) + " elements", 3 ); mHistograms.append( myHistogram ); return myHistogram; @@ -1763,7 +1757,7 @@ QString QgsGdalProvider::buildPyramids( const QList &rasterPyr myConfigOptionsOld[ opt[0] ] = QString( CPLGetConfigOption( key.data(), nullptr ) ); // set temp. value CPLSetConfigOption( key.data(), value.data() ); - QgsDebugMsg( QStringLiteral( "set option %1=%2" ).arg( key.data(), value.data() ) ); + QgsDebugMsgLevel( QStringLiteral( "set option %1=%2" ).arg( key.data(), value.data() ), 2 ); } else { @@ -1785,15 +1779,15 @@ QString QgsGdalProvider::buildPyramids( const QList &rasterPyr ++myRasterPyramidIterator ) { #ifdef QGISDEBUG - QgsDebugMsg( QStringLiteral( "Build pyramids:: Level %1" ).arg( myRasterPyramidIterator->level ) ); - QgsDebugMsg( QStringLiteral( "x:%1" ).arg( myRasterPyramidIterator->xDim ) ); - QgsDebugMsg( QStringLiteral( "y:%1" ).arg( myRasterPyramidIterator->yDim ) ); - QgsDebugMsg( QStringLiteral( "exists : %1" ).arg( myRasterPyramidIterator->exists ) ); + QgsDebugMsgLevel( QStringLiteral( "Build pyramids:: Level %1" ).arg( myRasterPyramidIterator->level ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "x:%1" ).arg( myRasterPyramidIterator->xDim ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "y:%1" ).arg( myRasterPyramidIterator->yDim ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "exists : %1" ).arg( myRasterPyramidIterator->exists ), 2 ); #endif if ( myRasterPyramidIterator->build ) { - QgsDebugMsg( QStringLiteral( "adding overview at level %1 to list" - ).arg( myRasterPyramidIterator->level ) ); + QgsDebugMsgLevel( QStringLiteral( "adding overview at level %1 to list" + ).arg( myRasterPyramidIterator->level ), 2 ); myOverviewLevelsVector.append( myRasterPyramidIterator->level ); } } @@ -1814,8 +1808,8 @@ QString QgsGdalProvider::buildPyramids( const QList &rasterPyr const char *method = ba.data(); //build the pyramid and show progress to console - QgsDebugMsg( QStringLiteral( "Building overviews at %1 levels using resampling method %2" - ).arg( myOverviewLevelsVector.size() ).arg( method ) ); + QgsDebugMsgLevel( QStringLiteral( "Building overviews at %1 levels using resampling method %2" + ).arg( myOverviewLevelsVector.size() ).arg( method ), 2 ); try { //build the pyramid and show progress to console @@ -1855,7 +1849,7 @@ QString QgsGdalProvider::buildPyramids( const QList &rasterPyr } else { - QgsDebugMsg( QStringLiteral( "Building pyramids finished OK" ) ); + QgsDebugMsgLevel( QStringLiteral( "Building pyramids finished OK" ), 2 ); //make sure the raster knows it has pyramids mHasPyramids = true; } @@ -1874,7 +1868,7 @@ QString QgsGdalProvider::buildPyramids( const QList &rasterPyr CPLSetConfigOption( key.data(), value.data() ); } - QgsDebugMsg( QStringLiteral( "Pyramid overviews built" ) ); + QgsDebugMsgLevel( QStringLiteral( "Pyramid overviews built" ), 2 ); // Observed problem: if a *.rrd file exists and GDALBuildOverviews() is called, // the *.rrd is deleted and no overviews are created, if GDALBuildOverviews() @@ -1883,7 +1877,7 @@ QString QgsGdalProvider::buildPyramids( const QList &rasterPyr // Crash can be avoided if dataset is reopened, fixed in GDAL 1.9.2 if ( format == QgsRaster::PyramidsInternal ) { - QgsDebugMsg( QStringLiteral( "Reopening dataset ..." ) ); + QgsDebugMsgLevel( QStringLiteral( "Reopening dataset ..." ), 2 ); //close the gdal dataset and reopen it in read only mode GDALClose( mGdalBaseDataset ); mGdalBaseDataset = gdalOpen( dataSourceUri( true ).toUtf8().constData(), mUpdate ? GA_Update : GA_ReadOnly ); @@ -1988,7 +1982,7 @@ QList QgsGdalProvider::buildPyramidList( QList overviewLi { int myDivisor = 2; - QgsDebugMsg( QStringLiteral( "Building initial pyramid list" ) ); + QgsDebugMsgLevel( QStringLiteral( "Building initial pyramid list" ), 2 ); while ( ( myWidth / myDivisor > 32 ) && ( ( myHeight / myDivisor ) > 32 ) ) { @@ -2012,7 +2006,7 @@ QList QgsGdalProvider::buildPyramidList( QList overviewLi myRasterPyramid.yDim = ( int )( 0.5 + ( myHeight / static_cast( myDivisor ) ) ); // NOLINT myRasterPyramid.exists = false; - QgsDebugMsg( QStringLiteral( "Pyramid %1 xDim %2 yDim %3" ).arg( myRasterPyramid.level ).arg( myRasterPyramid.xDim ).arg( myRasterPyramid.yDim ) ); + QgsDebugMsgLevel( QStringLiteral( "Pyramid %1 xDim %2 yDim %3" ).arg( myRasterPyramid.level ).arg( myRasterPyramid.xDim ).arg( myRasterPyramid.yDim ), 2 ); // // Now we check if it actually exists in the raster layer @@ -2035,9 +2029,9 @@ QList QgsGdalProvider::buildPyramidList( QList overviewLi // here is where we check if its a near match: // we will see if its within 5 cells either side of // - QgsDebugMsg( "Checking whether " + QString::number( myRasterPyramid.xDim ) + " x " + - QString::number( myRasterPyramid.yDim ) + " matches " + - QString::number( myOverviewXDim ) + " x " + QString::number( myOverviewYDim ) ); + QgsDebugMsgLevel( "Checking whether " + QString::number( myRasterPyramid.xDim ) + " x " + + QString::number( myRasterPyramid.yDim ) + " matches " + + QString::number( myOverviewXDim ) + " x " + QString::number( myOverviewYDim ), 2 ); if ( ( myOverviewXDim <= ( myRasterPyramid.xDim + myNearMatchLimit ) ) && @@ -2049,12 +2043,12 @@ QList QgsGdalProvider::buildPyramidList( QList overviewLi myRasterPyramid.xDim = myOverviewXDim; myRasterPyramid.yDim = myOverviewYDim; myRasterPyramid.exists = true; - QgsDebugMsg( QStringLiteral( ".....YES!" ) ); + QgsDebugMsgLevel( QStringLiteral( ".....YES!" ), 2 ); } else { //no match - QgsDebugMsg( QStringLiteral( ".....no." ) ); + QgsDebugMsgLevel( QStringLiteral( ".....no." ), 2 ); } } } @@ -2239,9 +2233,6 @@ void buildSupportedRasterFileFilterAndExtensions( QString &fileFiltersString, QS fileFiltersString += createFileFilter_( myGdalDriverLongName, glob ); } - - //QgsDebugMsg(QString("got driver Desc=%1 LongName=%2").arg(myGdalDriverDescription).arg(myGdalDriverLongName)); - if ( myGdalDriverExtensions.isEmpty() && !myGdalDriverLongName.isEmpty() ) { // Then what we have here is a driver with no corresponding @@ -2323,7 +2314,7 @@ bool QgsGdalProvider::isValidRasterFileName( QString const &fileNameQString, QSt { if ( !fileName.startsWith( vsiPrefix ) ) fileName = vsiPrefix + fileName; - QgsDebugMsg( QStringLiteral( "Trying %1 syntax, fileName= %2" ).arg( vsiPrefix, fileName ) ); + QgsDebugMsgLevel( QStringLiteral( "Trying %1 syntax, fileName= %2" ).arg( vsiPrefix, fileName ), 2 ); } //open the file using gdal making sure we have handled locale properly @@ -2360,7 +2351,7 @@ bool QgsGdalProvider::hasStatistics( int bandNo, if ( !initIfNeeded() ) return false; - QgsDebugMsg( QStringLiteral( "theBandNo = %1 sampleSize = %2" ).arg( bandNo ).arg( sampleSize ) ); + QgsDebugMsgLevel( QStringLiteral( "theBandNo = %1 sampleSize = %2" ).arg( bandNo ).arg( sampleSize ), 2 ); // First check if cached in mStatistics if ( QgsRasterDataProvider::hasStatistics( bandNo, stats, boundingBox, sampleSize ) ) @@ -2374,7 +2365,7 @@ bool QgsGdalProvider::hasStatistics( int bandNo, if ( ( sourceHasNoDataValue( bandNo ) && !useSourceNoDataValue( bandNo ) ) || !userNoDataValues( bandNo ).isEmpty() ) { - QgsDebugMsg( QStringLiteral( "Custom no data values -> GDAL statistics not sufficient." ) ); + QgsDebugMsgLevel( QStringLiteral( "Custom no data values -> GDAL statistics not sufficient." ), 2 ); return false; } @@ -2390,7 +2381,7 @@ bool QgsGdalProvider::hasStatistics( int bandNo, return false; } - QgsDebugMsg( QStringLiteral( "Looking for GDAL statistics" ) ); + QgsDebugMsgLevel( QStringLiteral( "Looking for GDAL statistics" ), 2 ); GDALRasterBandH myGdalBand = getBand( bandNo ); if ( ! myGdalBand ) @@ -2433,7 +2424,7 @@ bool QgsGdalProvider::hasStatistics( int bandNo, if ( CE_None == myerval ) // CE_Warning if cached not found { - QgsDebugMsg( QStringLiteral( "GDAL has cached statistics" ) ); + QgsDebugMsgLevel( QStringLiteral( "GDAL has cached statistics" ), 2 ); return true; } @@ -2446,7 +2437,7 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const if ( !initIfNeeded() ) return QgsRasterBandStats(); - QgsDebugMsg( QStringLiteral( "theBandNo = %1 sampleSize = %2" ).arg( bandNo ).arg( sampleSize ) ); + QgsDebugMsgLevel( QStringLiteral( "theBandNo = %1 sampleSize = %2" ).arg( bandNo ).arg( sampleSize ), 2 ); // TODO: null values set on raster layer!!! @@ -2463,7 +2454,7 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const { if ( stats.contains( myRasterBandStats ) ) { - QgsDebugMsg( QStringLiteral( "Using cached statistics." ) ); + QgsDebugMsgLevel( QStringLiteral( "Using cached statistics." ), 2 ); return stats; } } @@ -2473,7 +2464,7 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const if ( ( sourceHasNoDataValue( bandNo ) && !useSourceNoDataValue( bandNo ) ) || !userNoDataValues( bandNo ).isEmpty() ) { - QgsDebugMsg( QStringLiteral( "Custom no data values, using generic statistics." ) ); + QgsDebugMsgLevel( QStringLiteral( "Custom no data values, using generic statistics." ), 2 ); return QgsRasterDataProvider::bandStatistics( bandNo, stats, boundingBox, sampleSize, feedback ); } @@ -2481,16 +2472,16 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const | QgsRasterBandStats::Range | QgsRasterBandStats::Mean | QgsRasterBandStats::StdDev; - QgsDebugMsg( QStringLiteral( "theStats = %1 supportedStats = %2" ).arg( stats, 0, 2 ).arg( supportedStats, 0, 2 ) ); + QgsDebugMsgLevel( QStringLiteral( "theStats = %1 supportedStats = %2" ).arg( stats, 0, 2 ).arg( supportedStats, 0, 2 ), 2 ); if ( myRasterBandStats.extent != extent() || ( stats & ( ~supportedStats ) ) ) { - QgsDebugMsg( QStringLiteral( "Statistics not supported by provider, using generic statistics." ) ); + QgsDebugMsgLevel( QStringLiteral( "Statistics not supported by provider, using generic statistics." ), 2 ); return QgsRasterDataProvider::bandStatistics( bandNo, stats, boundingBox, sampleSize, feedback ); } - QgsDebugMsg( QStringLiteral( "Using GDAL statistics." ) ); + QgsDebugMsgLevel( QStringLiteral( "Using GDAL statistics." ), 2 ); GDALRasterBandH myGdalBand = getBand( bandNo ); //int bApproxOK = false; //as we asked for stats, don't get approx values @@ -2506,7 +2497,7 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const } } - QgsDebugMsg( QStringLiteral( "bApproxOK = %1" ).arg( bApproxOK ) ); + QgsDebugMsgLevel( QStringLiteral( "bApproxOK = %1" ).arg( bApproxOK ), 2 ); double pdfMin; double pdfMax; @@ -2525,12 +2516,12 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const CPLErr myerval = GDALGetRasterStatistics( myGdalBand, bApproxOK, true, &pdfMin, &pdfMax, &pdfMean, &pdfStdDev ); - QgsDebugMsg( QStringLiteral( "myerval = %1" ).arg( myerval ) ); + QgsDebugMsgLevel( QStringLiteral( "myerval = %1" ).arg( myerval ), 2 ); // if cached stats are not found, compute them if ( !bApproxOK || CE_None != myerval ) { - QgsDebugMsg( QStringLiteral( "Calculating statistics by GDAL" ) ); + QgsDebugMsgLevel( QStringLiteral( "Calculating statistics by GDAL" ), 2 ); myerval = GDALComputeRasterStatistics( myGdalBand, bApproxOK, &pdfMin, &pdfMax, &pdfMean, &pdfStdDev, progressCallback, &myProg ); @@ -2538,7 +2529,7 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const } else { - QgsDebugMsg( QStringLiteral( "Using GDAL cached statistics" ) ); + QgsDebugMsgLevel( QStringLiteral( "Using GDAL cached statistics" ), 2 ); } if ( feedback && feedback->isCanceled() ) @@ -2593,12 +2584,12 @@ QgsRasterBandStats QgsGdalProvider::bandStatistics( int bandNo, int stats, const } #ifdef QGISDEBUG - QgsDebugMsg( QStringLiteral( "************ STATS **************" ) ); - QgsDebugMsg( QStringLiteral( "MIN %1" ).arg( myRasterBandStats.minimumValue ) ); - QgsDebugMsg( QStringLiteral( "MAX %1" ).arg( myRasterBandStats.maximumValue ) ); - QgsDebugMsg( QStringLiteral( "RANGE %1" ).arg( myRasterBandStats.range ) ); - QgsDebugMsg( QStringLiteral( "MEAN %1" ).arg( myRasterBandStats.mean ) ); - QgsDebugMsg( QStringLiteral( "STDDEV %1" ).arg( myRasterBandStats.stdDev ) ); + QgsDebugMsgLevel( QStringLiteral( "************ STATS **************" ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "MIN %1" ).arg( myRasterBandStats.minimumValue ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "MAX %1" ).arg( myRasterBandStats.maximumValue ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "RANGE %1" ).arg( myRasterBandStats.range ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "MEAN %1" ).arg( myRasterBandStats.mean ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "STDDEV %1" ).arg( myRasterBandStats.stdDev ), 2 ); #endif } @@ -2622,7 +2613,7 @@ bool QgsGdalProvider::initIfNeeded() { if ( !gdalUri.startsWith( vsiPrefix ) ) setDataSourceUri( vsiPrefix + gdalUri ); - QgsDebugMsg( QStringLiteral( "Trying %1 syntax, uri= %2" ).arg( vsiPrefix, dataSourceUri() ) ); + QgsDebugMsgLevel( QStringLiteral( "Trying %1 syntax, uri= %2" ).arg( vsiPrefix, dataSourceUri() ), 2 ); } gdalUri = dataSourceUri( true ); @@ -2765,14 +2756,13 @@ void QgsGdalProvider::initBaseDataset() } else { - QgsDebugMsg( QStringLiteral( "No valid CRS identified" ) ); + QgsDebugMsgLevel( QStringLiteral( "No valid CRS identified" ), 2 ); } } //set up the coordinat transform - in the case of raster this is mainly used to convert //the inverese projection of the map extents of the canvas when zooming in etc. so //that they match the coordinate system of this layer - //QgsDebugMsg( "Layer registry has " + QString::number( QgsProject::instance()->count() ) + "layers" ); //metadata(); @@ -2827,12 +2817,12 @@ void QgsGdalProvider::initBaseDataset() // the min/max bounds, it would be cast to 0 by representableValue(). if ( isValid && !QgsRaster::isRepresentableValue( myNoDataValue, dataTypeFromGdal( myGdalDataType ) ) ) { - QgsDebugMsg( QStringLiteral( "GDALGetRasterNoDataValue = %1 is not representable in data type, so ignoring it" ).arg( myNoDataValue ) ); + QgsDebugMsgLevel( QStringLiteral( "GDALGetRasterNoDataValue = %1 is not representable in data type, so ignoring it" ).arg( myNoDataValue ), 2 ); isValid = false; } if ( isValid ) { - QgsDebugMsg( QStringLiteral( "GDALGetRasterNoDataValue = %1" ).arg( myNoDataValue ) ); + QgsDebugMsgLevel( QStringLiteral( "GDALGetRasterNoDataValue = %1" ).arg( myNoDataValue ), 2 ); // The no data value double may be non representable by data type, it can result // in problems if that value is used to represent additional user defined no data // see #3840 @@ -2917,7 +2907,6 @@ void QgsGdalProvider::initBaseDataset() mGdalDataType.append( myGdalDataType ); //mInternalNoDataValue.append( myInternalNoDataValue ); - //QgsDebugMsg( QStringLiteral( "mInternalNoDataValue[%1] = %2" ).arg( i - 1 ).arg( mInternalNoDataValue[i-1] ) ); } if ( mMaskBandExposedAsAlpha ) @@ -2947,7 +2936,7 @@ QgsGdalProvider *QgsGdalProviderMetadata::createRasterDataProvider( return new QgsGdalProvider( uri, error ); } - QgsDebugMsg( "create options: " + createOptions.join( " " ) ); + QgsDebugMsgLevel( "create options: " + createOptions.join( " " ), 2 ); //create dataset CPLErrorReset(); @@ -3019,7 +3008,7 @@ bool QgsGdalProvider::remove() while ( *mpRefCounter != 1 ) { - QgsDebugMsg( QStringLiteral( "Waiting for ref counter for %1 to drop to 1" ).arg( dataSourceUri() ) ); + QgsDebugMsgLevel( QStringLiteral( "Waiting for ref counter for %1 to drop to 1" ).arg( dataSourceUri() ), 2 ); QThread::msleep( 100 ); } @@ -3039,7 +3028,7 @@ bool QgsGdalProvider::remove() QgsDebugMsg( "RasterIO error: " + QString::fromUtf8( CPLGetLastErrorMsg() ) ); return false; } - QgsDebugMsg( QStringLiteral( "Raster dataset dataSourceUri() successfully deleted" ) ); + QgsDebugMsgLevel( QStringLiteral( "Raster dataset dataSourceUri() successfully deleted" ), 2 ); return true; } return false; @@ -3095,7 +3084,7 @@ QString QgsGdalProvider::validateCreationOptions( const QStringList &createOptio { QStringList opt = option.split( '=' ); optionsMap[ opt[0].toUpper()] = opt[1]; - QgsDebugMsg( "option: " + option ); + QgsDebugMsgLevel( "option: " + option, 2 ); } // gtiff files - validate PREDICTOR option @@ -3105,7 +3094,7 @@ QString QgsGdalProvider::validateCreationOptions( const QStringList &createOptio QString value = optionsMap.value( QStringLiteral( "PREDICTOR" ) ); GDALDataType nDataType = ( !mGdalDataType.isEmpty() ) ? ( GDALDataType ) mGdalDataType.at( 0 ) : GDT_Unknown; int nBitsPerSample = nDataType != GDT_Unknown ? GDALGetDataTypeSize( nDataType ) : 0; - QgsDebugMsg( QStringLiteral( "PREDICTOR: %1 nbits: %2 type: %3" ).arg( value ).arg( nBitsPerSample ).arg( ( GDALDataType ) mGdalDataType.at( 0 ) ) ); + QgsDebugMsgLevel( QStringLiteral( "PREDICTOR: %1 nbits: %2 type: %3" ).arg( value ).arg( nBitsPerSample ).arg( ( GDALDataType ) mGdalDataType.at( 0 ) ), 2 ); // PREDICTOR=2 only valid for 8/16/32 bits per sample // TODO check for NBITS option (see geotiff.cpp) if ( value == QLatin1String( "2" ) ) @@ -3182,7 +3171,7 @@ bool QgsGdalProvider::setEditable( bool enabled ) while ( *mpRefCounter != 1 ) { - QgsDebugMsg( QStringLiteral( "Waiting for ref counter for %1 to drop to 1" ).arg( dataSourceUri() ) ); + QgsDebugMsgLevel( QStringLiteral( "Waiting for ref counter for %1 to drop to 1" ).arg( dataSourceUri() ), 2 ); QThread::msleep( 100 ); } diff --git a/src/core/providers/gdal/qgsgdalproviderbase.cpp b/src/core/providers/gdal/qgsgdalproviderbase.cpp index 6ca71bedaca8..5dda088cdfa8 100644 --- a/src/core/providers/gdal/qgsgdalproviderbase.cpp +++ b/src/core/providers/gdal/qgsgdalproviderbase.cpp @@ -56,7 +56,7 @@ QList QgsGdalProviderBase::colorTable( GDALDa if ( myGdalColorTable ) { - QgsDebugMsg( QStringLiteral( "Color table found" ) ); + QgsDebugMsgLevel( QStringLiteral( "Color table found" ), 2 ); // load category labels char **categoryNames = GDALGetRasterCategoryNames( myGdalBand ); @@ -73,9 +73,9 @@ QList QgsGdalProviderBase::colorTable( GDALDa int myEntryCount = GDALGetColorEntryCount( myGdalColorTable ); GDALColorInterp myColorInterpretation = GDALGetRasterColorInterpretation( myGdalBand ); - QgsDebugMsg( "Color Interpretation: " + QString::number( static_cast< int >( myColorInterpretation ) ) ); + QgsDebugMsgLevel( "Color Interpretation: " + QString::number( static_cast< int >( myColorInterpretation ) ), 2 ); GDALPaletteInterp myPaletteInterpretation = GDALGetPaletteInterpretation( myGdalColorTable ); - QgsDebugMsg( "Palette Interpretation: " + QString::number( static_cast< int >( myPaletteInterpretation ) ) ); + QgsDebugMsgLevel( "Palette Interpretation: " + QString::number( static_cast< int >( myPaletteInterpretation ) ), 2 ); const GDALColorEntry *myColorEntry = nullptr; for ( int myIterator = 0; myIterator < myEntryCount; myIterator++ ) @@ -128,7 +128,7 @@ QList QgsGdalProviderBase::colorTable( GDALDa } else { - QgsDebugMsg( QStringLiteral( "Color interpretation type not supported yet" ) ); + QgsDebugMsgLevel( QStringLiteral( "Color interpretation type not supported yet" ), 2 ); return ct; } } @@ -136,11 +136,11 @@ QList QgsGdalProviderBase::colorTable( GDALDa } else { - QgsDebugMsg( "No color table found for band " + QString::number( bandNumber ) ); + QgsDebugMsgLevel( "No color table found for band " + QString::number( bandNumber ), 2 ); return ct; } - QgsDebugMsg( QStringLiteral( "Color table loaded successfully" ) ); + QgsDebugMsgLevel( QStringLiteral( "Color table loaded successfully" ), 2 ); return ct; } diff --git a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp index 933f16049e6d..27c1c601e352 100644 --- a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp +++ b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.cpp @@ -17,6 +17,7 @@ ///@cond PRIVATE #include "qgsmeshmemorydataprovider.h" +#include "qgsmeshdataprovidertemporalcapabilities.h" #include "qgsmeshlayerutils.h" #include "qgstriangularmesh.h" #include @@ -54,6 +55,8 @@ QgsMeshMemoryDataProvider::QgsMeshMemoryDataProvider( const QString &uri, const data = uri.split( "&uid=" )[0]; } mIsValid = splitMeshSections( data ); + + temporalCapabilities()->setTemporalUnit( QgsUnitTypes::TemporalHours ); } QString QgsMeshMemoryDataProvider::providerKey() @@ -341,6 +344,21 @@ bool QgsMeshMemoryDataProvider::checkVertexId( int vertexIndex ) return true; } +void QgsMeshMemoryDataProvider::addGroupToTemporalCapabilities( int groupIndex, const QgsMeshMemoryDatasetGroup &group ) +{ + QgsMeshDataProviderTemporalCapabilities *tempCap = temporalCapabilities(); + if ( !tempCap ) + return; + + if ( group.datasetCount() > 1 ) //non temporal dataset groups (count=1) have no time in the capabilities + { + for ( int i = 0; i < group.datasets.count(); ++i ) + if ( group.datasets.at( i ) ) + tempCap->addDatasetTime( groupIndex, group.datasets.at( i )->time ); + } + +} + int QgsMeshMemoryDataProvider::vertexCount() const { return mVertices.size(); @@ -388,10 +406,12 @@ bool QgsMeshMemoryDataProvider::addDataset( const QString &uri ) calculateMinMaxForDatasetGroup( group ); mDatasetGroups.push_back( group ); + addGroupToTemporalCapabilities( mDatasetGroups.count() - 1, group ); if ( valid ) { mExtraDatasetUris << uri; + temporalCapabilities()->setHasTemporalCapabilities( true ); emit datasetGroupsAdded( 1 ); emit dataChanged(); } @@ -439,6 +459,7 @@ QgsMeshDatasetGroupMetadata QgsMeshMemoryDatasetGroup::groupMetadata() const maximum, 0, QDateTime(), + datasetCount() > 1, metadata ); } diff --git a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h index 473cfc581aa7..a3f1f27d9de4 100644 --- a/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h +++ b/src/core/providers/meshmemory/qgsmeshmemorydataprovider.h @@ -202,6 +202,8 @@ class CORE_EXPORT QgsMeshMemoryDataProvider final: public QgsMeshDataProvider bool checkDatasetValidity( std::shared_ptr &dataset, QgsMeshDatasetGroupMetadata::DataType dataType ); bool checkVertexId( int vertex_id ); + void addGroupToTemporalCapabilities( int groupIndex, const QgsMeshMemoryDatasetGroup &group ); + QVector mVertices; QVector mFaces; QVector mEdges; diff --git a/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp b/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp index 2522930c3da0..bfb97357d676 100644 --- a/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp +++ b/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp @@ -179,6 +179,39 @@ void QgsGeoPackageProviderConnection::vacuum( const QString &schema, const QStri executeGdalSqlPrivate( QStringLiteral( "VACUUM" ) ); } +void QgsGeoPackageProviderConnection::createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options ) const +{ + checkCapability( Capability::CreateSpatialIndex ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by GPKG, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + executeGdalSqlPrivate( QStringLiteral( "SELECT CreateSpatialIndex(%1, %2)" ).arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( ( options.geometryColumnName ) ) ) ); +} + +bool QgsGeoPackageProviderConnection::spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const +{ + checkCapability( Capability::CreateSpatialIndex ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by GPKG, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + const QList res = executeGdalSqlPrivate( QStringLiteral( "SELECT HasSpatialIndex(%1, %2)" ).arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( geometryColumn ) ) ); + return !res.isEmpty() && !res.at( 0 ).isEmpty() && res.at( 0 ).at( 0 ).toBool(); +} + +void QgsGeoPackageProviderConnection::deleteSpatialIndex( const QString &schema, const QString &name, const QString &geometryColumn ) const +{ + checkCapability( Capability::DeleteSpatialIndex ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by GPKG, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + executeGdalSqlPrivate( QStringLiteral( "SELECT DisableSpatialIndex(%1, %2)" ).arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( geometryColumn ) ) ); +} QList QgsGeoPackageProviderConnection::tables( const QString &schema, const TableFlags &flags ) const { @@ -280,6 +313,9 @@ void QgsGeoPackageProviderConnection::setDefaultCapabilities() Capability::Spatial, Capability::TableExists, Capability::ExecuteSql, + Capability::CreateSpatialIndex, + Capability::SpatialIndexExists, + Capability::DeleteSpatialIndex }; #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(2,4,0) mCapabilities |= Capability::DropRasterTable; diff --git a/src/core/providers/ogr/qgsgeopackageproviderconnection.h b/src/core/providers/ogr/qgsgeopackageproviderconnection.h index ad53ea0bafad..652a7676917b 100644 --- a/src/core/providers/ogr/qgsgeopackageproviderconnection.h +++ b/src/core/providers/ogr/qgsgeopackageproviderconnection.h @@ -40,6 +40,9 @@ class QgsGeoPackageProviderConnection : public QgsAbstractDatabaseProviderConnec void renameVectorTable( const QString &schema, const QString &name, const QString &newName ) const override; QList> executeSql( const QString &sql ) const override; void vacuum( const QString &schema, const QString &name ) const override; + void createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options = QgsAbstractDatabaseProviderConnection::SpatialIndexOptions() ) const override; + bool spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const override; + void deleteSpatialIndex( const QString &schema, const QString &name, const QString &geometryColumn ) const override; QList tables( const QString &schema = QString(), const TableFlags &flags = nullptr ) const override; QIcon icon() const override; diff --git a/src/core/providers/ogr/qgsogrdataitems.cpp b/src/core/providers/ogr/qgsogrdataitems.cpp index 3e18aa0d03b5..3a2fc3f8eb49 100644 --- a/src/core/providers/ogr/qgsogrdataitems.cpp +++ b/src/core/providers/ogr/qgsogrdataitems.cpp @@ -563,7 +563,9 @@ QgsDataItem *QgsOgrDataItemProvider::createDataItem( const QString &pathIn, QgsD QStringLiteral( "sqlite" ), QStringLiteral( "db" ), QStringLiteral( "gdb" ), - QStringLiteral( "kml" ) }; + QStringLiteral( "kml" ), + QStringLiteral( "osm" ), + QStringLiteral( "pbf" ) }; static QStringList sOgrSupportedDbDriverNames { QStringLiteral( "GPKG" ), QStringLiteral( "db" ), QStringLiteral( "gdb" ) }; diff --git a/src/core/qgsabstractcontentcache.h b/src/core/qgsabstractcontentcache.h index 8ae49e371c41..d8fe836f6cff 100644 --- a/src/core/qgsabstractcontentcache.h +++ b/src/core/qgsabstractcontentcache.h @@ -402,7 +402,12 @@ class CORE_EXPORT QgsAbstractContentCache : public QgsAbstractContentCacheBase if ( ok ) { // read the content data - mRemoteContentCache.insert( path, new QByteArray( reply->readAll() ) ); + const QByteArray ba = reply->readAll(); + + // because of the fragility listed below in waitForTaskFinished, this slot may get called twice. In that case + // the second time will have an empty reply (we've already read it all...) + if ( !ba.isEmpty() ) + mRemoteContentCache.insert( path, new QByteArray( ba ) ); } QMetaObject::invokeMethod( const_cast< QgsAbstractContentCacheBase * >( qobject_cast< const QgsAbstractContentCacheBase * >( this ) ), "onRemoteContentFetched", Qt::QueuedConnection, Q_ARG( QString, path ), Q_ARG( bool, true ) ); } ); @@ -447,7 +452,7 @@ class CORE_EXPORT QgsAbstractContentCache : public QgsAbstractContentCacheBase } /** - * Blocks the current thread until the \a task finishes or an arbitrary setting maximum wait to 5 seconds + * Blocks the current thread until the \a task finishes (or user's preset network timeout expires) * * \warning this method must NEVER be used from GUI based applications (like the main QGIS application) * or crashes will result. Only for use in external scripts or QGIS server. @@ -458,23 +463,16 @@ class CORE_EXPORT QgsAbstractContentCache : public QgsAbstractContentCacheBase */ bool waitForTaskFinished( QgsNetworkContentFetcherTask *task ) const { - // First step, waiting for task running - if ( task->status() != QgsTask::Running ) - { - QEventLoop loop; - connect( task, &QgsNetworkContentFetcherTask::begun, &loop, &QEventLoop::quit ); - if ( task->status() != QgsTask::Running ) - loop.exec(); - } - - // Second step, wait 5 seconds for task finished - if ( task->waitForFinished( 5000 ) ) + // Wait up to timeout seconds for task finished + if ( task->waitForFinished( QgsNetworkAccessManager::timeout() ) ) { // The wait did not time out // Third step, check status as complete if ( task->status() == QgsTask::Complete ) { // Fourth step, force the signal fetched to be sure reply has been checked + + // ARGH this is BAD BAD BAD. The connection will get called twice as a result!!! task->fetched(); return true; } diff --git a/src/core/qgsabstractdatabaseproviderconnection.cpp b/src/core/qgsabstractdatabaseproviderconnection.cpp index 4b31b44464fa..8e429ec7c1a8 100644 --- a/src/core/qgsabstractdatabaseproviderconnection.cpp +++ b/src/core/qgsabstractdatabaseproviderconnection.cpp @@ -132,6 +132,22 @@ void QgsAbstractDatabaseProviderConnection::vacuum( const QString &, const QStri checkCapability( Capability::Vacuum ); } +void QgsAbstractDatabaseProviderConnection::createSpatialIndex( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions & ) const +{ + checkCapability( Capability::CreateSpatialIndex ); +} + +void QgsAbstractDatabaseProviderConnection::deleteSpatialIndex( const QString &, const QString &, const QString & ) const +{ + checkCapability( Capability::DeleteSpatialIndex ); +} + +bool QgsAbstractDatabaseProviderConnection::spatialIndexExists( const QString &, const QString &, const QString & ) const +{ + checkCapability( Capability::SpatialIndexExists ); + return false; +} + QList QgsAbstractDatabaseProviderConnection::tables( const QString &, const QgsAbstractDatabaseProviderConnection::TableFlags & ) const { checkCapability( Capability::Tables ); diff --git a/src/core/qgsabstractdatabaseproviderconnection.h b/src/core/qgsabstractdatabaseproviderconnection.h index b94f36173988..fa9737d00b60 100644 --- a/src/core/qgsabstractdatabaseproviderconnection.h +++ b/src/core/qgsabstractdatabaseproviderconnection.h @@ -290,6 +290,9 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv SqlLayers = 1 << 13, //!< Can create vector layers from SQL SELECT queries TableExists = 1 << 14, //!< Can check if table exists Spatial = 1 << 15, //!< The connection supports spatial tables + CreateSpatialIndex = 1 << 16, //!< The connection can create spatial indices + SpatialIndexExists = 1 << 17, //!< The connection can determine if a spatial index exists + DeleteSpatialIndex = 1 << 18, //!< The connection can delete spatial indices for tables }; Q_ENUM( Capability ) @@ -328,7 +331,7 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv virtual QString tableUri( const QString &schema, const QString &name ) const SIP_THROW( QgsProviderConnectionException ); /** - * Creates an empty table with \a name in the given \a schema (schema is ignored if not supported by the backend). + * Creates an empty table with \a name in the given \a schema (schema is ignored if not supported by the backend). * Raises a QgsProviderConnectionException if any errors are encountered. * \throws QgsProviderConnectionException */ @@ -350,7 +353,7 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv virtual void dropVectorTable( const QString &schema, const QString &name ) const SIP_THROW( QgsProviderConnectionException ); /** - * Drops a raster table with given \a schema (schema is ignored if not supported by the backend) and \a name. + * Drops a raster table with given \a schema (schema is ignored if not supported by the backend) and \a name. * Raises a QgsProviderConnectionException if any errors are encountered. * \note it is responsibility of the caller to handle open layers and registry entries. * \throws QgsProviderConnectionException @@ -358,7 +361,7 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv virtual void dropRasterTable( const QString &schema, const QString &name ) const SIP_THROW( QgsProviderConnectionException ); /** - * Renames a vector or aspatial table with given \a schema (schema is ignored if not supported by the backend) and \a name. + * Renames a vector or aspatial table with given \a schema (schema is ignored if not supported by the backend) and \a name. * Raises a QgsProviderConnectionException if any errors are encountered. * \note it is responsibility of the caller to handle open layers and registry entries. * \throws QgsProviderConnectionException @@ -366,7 +369,7 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv virtual void renameVectorTable( const QString &schema, const QString &name, const QString &newName ) const SIP_THROW( QgsProviderConnectionException ); /** - * Renames a raster table with given \a schema (schema is ignored if not supported by the backend) and \a name. + * Renames a raster table with given \a schema (schema is ignored if not supported by the backend) and \a name. * Raises a QgsProviderConnectionException if any errors are encountered. * \note it is responsibility of the caller to handle open layers and registry entries. * \throws QgsProviderConnectionException @@ -405,12 +408,54 @@ class CORE_EXPORT QgsAbstractDatabaseProviderConnection : public QgsAbstractProv virtual QList> executeSql( const QString &sql ) const SIP_THROW( QgsProviderConnectionException ); /** - * Vacuum the database table with given \a schema and \a name (schema is ignored if not supported by the backend). + * Vacuum the database table with given \a schema and \a name (schema is ignored if not supported by the backend). * Raises a QgsProviderConnectionException if any errors are encountered. * \throws QgsProviderConnectionException */ virtual void vacuum( const QString &schema, const QString &name ) const SIP_THROW( QgsProviderConnectionException ); + /** + * Contains extra options relating to spatial index creation. + * + * \since QGIS 3.14 + */ + struct CORE_EXPORT SpatialIndexOptions + { + //! Specifies the name of the geometry column to create the index for + QString geometryColumnName; + }; + + /** + * Creates a spatial index for the database table with given \a schema and \a name (schema is ignored if not supported by the backend). + * + * The \a options argument can be used to provide extra options controlling the spatial index creation. + * + * Raises a QgsProviderConnectionException if any errors are encountered. + * \throws QgsProviderConnectionException + * \since QGIS 3.14 + */ + virtual void createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options = QgsAbstractDatabaseProviderConnection::SpatialIndexOptions() ) const SIP_THROW( QgsProviderConnectionException ); + + /** + * Determines whether a spatial index exists for the database table with given \a schema, \a name and \a geometryColumn (\a schema and \a geometryColumn are + * ignored if not supported by the backend). + * + * Raises a QgsProviderConnectionException if any errors are encountered. + * \throws QgsProviderConnectionException + * \since QGIS 3.14 + */ + virtual bool spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const SIP_THROW( QgsProviderConnectionException ); + + /** + * Deletes the existing spatial index for the database table with given \a schema, \a name and \a geometryColumn (\a schema and \a geometryColumn are + * ignored if not supported by the backend). + * + * Raises a QgsProviderConnectionException if any errors are encountered. + * \throws QgsProviderConnectionException + * \since QGIS 3.14 + */ + virtual void deleteSpatialIndex( const QString &schema, const QString &name, const QString &geometryColumn ) const SIP_THROW( QgsProviderConnectionException ); + /** * Returns information on the tables in the given schema. * Raises a QgsProviderConnectionException if any errors are encountered. diff --git a/src/core/qgsactionscope.h b/src/core/qgsactionscope.h index 473741ef877d..d5f6e2e6c304 100644 --- a/src/core/qgsactionscope.h +++ b/src/core/qgsactionscope.h @@ -46,6 +46,11 @@ class CORE_EXPORT QgsActionScope { public: +#ifdef SIP_RUN + % TypeCode +#include + % End +#endif /** * Creates a new invalid action scope. @@ -117,6 +122,12 @@ class CORE_EXPORT QgsActionScope * \since QGIS 3.0 */ bool isValid() const; +#ifdef SIP_RUN + long __hash__(); + % MethodCode + sipRes = qHash( *sipCpp ); + % End +#endif private: QString mId; diff --git a/src/core/qgsaggregatecalculator.cpp b/src/core/qgsaggregatecalculator.cpp index f15d3c7103b0..998e0dff2612 100644 --- a/src/core/qgsaggregatecalculator.cpp +++ b/src/core/qgsaggregatecalculator.cpp @@ -331,6 +331,7 @@ QList QgsAggregateCalculator::aggregates( << QVariant::LongLong << QVariant::ULongLong << QVariant::Double + << QVariant::String } << AggregateInfo { @@ -342,6 +343,7 @@ QList QgsAggregateCalculator::aggregates( << QVariant::LongLong << QVariant::ULongLong << QVariant::Double + << QVariant::String } << AggregateInfo { @@ -583,6 +585,10 @@ QgsStringStatisticalSummary::Statistic QgsAggregateCalculator::stringStatFromAgg return QgsStringStatisticalSummary::MinimumLength; case StringMaximumLength: return QgsStringStatisticalSummary::MaximumLength; + case Minority: + return QgsStringStatisticalSummary::Minority; + case Majority: + return QgsStringStatisticalSummary::Majority; case Sum: case Mean: @@ -590,8 +596,6 @@ QgsStringStatisticalSummary::Statistic QgsAggregateCalculator::stringStatFromAgg case StDev: case StDevSample: case Range: - case Minority: - case Majority: case FirstQuartile: case ThirdQuartile: case InterQuartileRange: diff --git a/src/core/qgsaggregatecalculator.h b/src/core/qgsaggregatecalculator.h index e805a8ee56be..1801595d97c9 100644 --- a/src/core/qgsaggregatecalculator.h +++ b/src/core/qgsaggregatecalculator.h @@ -74,8 +74,8 @@ class CORE_EXPORT QgsAggregateCalculator StDev, //!< Standard deviation of values (numeric fields only) StDevSample, //!< Sample standard deviation of values (numeric fields only) Range, //!< Range of values (max - min) (numeric and datetime fields only) - Minority, //!< Minority of values (numeric fields only) - Majority, //!< Majority of values (numeric fields only) + Minority, //!< Minority of values + Majority, //!< Majority of values FirstQuartile, //!< First quartile (numeric fields only) ThirdQuartile, //!< Third quartile (numeric fields only) InterQuartileRange, //!< Inter quartile range (IQR) (numeric fields only) diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index c0a59a72ca8d..d564adaa9b86 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -31,6 +31,7 @@ #include "qgstaskmanager.h" #include "qgsnumericformatregistry.h" #include "qgsfieldformatterregistry.h" +#include "qgsscalebarrendererregistry.h" #include "qgssvgcache.h" #include "qgsimagecache.h" #include "qgscolorschemeregistry.h" @@ -40,9 +41,9 @@ #include "qgsrendererregistry.h" #include "qgssymbollayerregistry.h" #include "qgssymbollayerutils.h" -#include "callouts/qgscalloutsregistry.h" +#include "qgscalloutsregistry.h" #include "qgspluginlayerregistry.h" -#include "classification/qgsclassificationmethodregistry.h" +#include "qgsclassificationmethodregistry.h" #include "qgsmessagelog.h" #include "qgsannotationregistry.h" #include "qgssettings.h" @@ -59,9 +60,12 @@ #include "qgsnewsfeedparser.h" #include "qgsbookmarkmanager.h" #include "qgsstylemodel.h" +#include "qgsconnectionregistry.h" +#include "qgsremappingproxyfeaturesink.h" #include "gps/qgsgpsconnectionregistry.h" #include "processing/qgsprocessingregistry.h" +#include "processing/models/qgsprocessingmodelchildparametersource.h" #include "layout/qgspagesizeregistry.h" @@ -225,6 +229,9 @@ void QgsApplication::init( QString profileFolder ) qRegisterMetaType( "QgsDatumTransform::TransformDetails" ); qRegisterMetaType( "QgsNewsFeedParser::Entry" ); qRegisterMetaType( "QgsRectangle" ); + qRegisterMetaType( "QgsProcessingModelChildParameterSource" ); + qRegisterMetaTypeStreamOperators( "QgsProcessingModelChildParameterSource" ); + qRegisterMetaType( "QgsRemappingSinkDefinition" ); ( void ) resolvePkgPath(); @@ -2178,6 +2185,11 @@ QgsProcessingRegistry *QgsApplication::processingRegistry() return members()->mProcessingRegistry; } +QgsConnectionRegistry *QgsApplication::connectionRegistry() +{ + return members()->mConnectionRegistry; +} + QgsPageSizeRegistry *QgsApplication::pageSizeRegistry() { return members()->mPageSizeRegistry; @@ -2203,6 +2215,11 @@ Qgs3DRendererRegistry *QgsApplication::renderer3DRegistry() return members()->m3DRendererRegistry; } +QgsScaleBarRendererRegistry *QgsApplication::scaleBarRendererRegistry() +{ + return members()->mScaleBarRendererRegistry; +} + QgsProjectStorageRegistry *QgsApplication::projectStorageRegistry() { return members()->mProjectStorageRegistry; @@ -2214,6 +2231,7 @@ QgsApplication::ApplicationMembers::ApplicationMembers() // will need to be careful with the order of creation/destruction mMessageLog = new QgsMessageLog(); mProfiler = new QgsRuntimeProfiler(); + mConnectionRegistry = new QgsConnectionRegistry(); mTaskManager = new QgsTaskManager(); mActionScopeRegistry = new QgsActionScopeRegistry(); mNumericFormatRegistry = new QgsNumericFormatRegistry(); @@ -2239,11 +2257,13 @@ QgsApplication::ApplicationMembers::ApplicationMembers() mValidityCheckRegistry = new QgsValidityCheckRegistry(); mClassificationMethodRegistry = new QgsClassificationMethodRegistry(); mBookmarkManager = new QgsBookmarkManager( nullptr ); + mScaleBarRendererRegistry = new QgsScaleBarRendererRegistry(); } QgsApplication::ApplicationMembers::~ApplicationMembers() { delete mStyleModel; + delete mScaleBarRendererRegistry; delete mValidityCheckRegistry; delete mActionScopeRegistry; delete m3DRendererRegistry; @@ -2270,6 +2290,7 @@ QgsApplication::ApplicationMembers::~ApplicationMembers() delete mClassificationMethodRegistry; delete mNumericFormatRegistry; delete mBookmarkManager; + delete mConnectionRegistry; } QgsApplication::ApplicationMembers *QgsApplication::members() diff --git a/src/core/qgsapplication.h b/src/core/qgsapplication.h index 9993f06d1344..ca1ce0214342 100644 --- a/src/core/qgsapplication.h +++ b/src/core/qgsapplication.h @@ -56,6 +56,8 @@ class QgsCalloutRegistry; class QgsBookmarkManager; class QgsStyleModel; class QgsNumericFormatRegistry; +class QgsConnectionRegistry; +class QgsScaleBarRendererRegistry; /** * \ingroup core @@ -744,6 +746,12 @@ class CORE_EXPORT QgsApplication : public QApplication */ static QgsActionScopeRegistry *actionScopeRegistry() SIP_KEEPREFERENCE; + /** + * Returns the application's connection registry, used for managing saved data provider connections. + * \since QGIS 3.14 + */ + static QgsConnectionRegistry *connectionRegistry(); + /** * Returns the application runtime profiler. * \since QGIS 3.0 @@ -768,6 +776,13 @@ class CORE_EXPORT QgsApplication : public QApplication */ static Qgs3DRendererRegistry *renderer3DRegistry() SIP_KEEPREFERENCE; + /** + * Gets the registry of available scalebar renderers. + * + * \since QGIS 3.14 + */ + static QgsScaleBarRendererRegistry *scaleBarRendererRegistry() SIP_KEEPREFERENCE; + /** * Returns registry of available project storage implementations. * \since QGIS 3.2 @@ -902,12 +917,14 @@ class CORE_EXPORT QgsApplication : public QApplication QgsFieldFormatterRegistry *mFieldFormatterRegistry = nullptr; QgsGpsConnectionRegistry *mGpsConnectionRegistry = nullptr; QgsNetworkContentFetcherRegistry *mNetworkContentFetcherRegistry = nullptr; + QgsScaleBarRendererRegistry *mScaleBarRendererRegistry = nullptr; QgsValidityCheckRegistry *mValidityCheckRegistry = nullptr; QgsMessageLog *mMessageLog = nullptr; QgsPaintEffectRegistry *mPaintEffectRegistry = nullptr; QgsPluginLayerRegistry *mPluginLayerRegistry = nullptr; QgsClassificationMethodRegistry *mClassificationMethodRegistry = nullptr; QgsProcessingRegistry *mProcessingRegistry = nullptr; + QgsConnectionRegistry *mConnectionRegistry = nullptr; QgsProjectStorageRegistry *mProjectStorageRegistry = nullptr; QgsPageSizeRegistry *mPageSizeRegistry = nullptr; QgsRasterRendererRegistry *mRasterRendererRegistry = nullptr; diff --git a/src/core/qgscolorscheme.cpp b/src/core/qgscolorscheme.cpp index b9482e65eee2..3e4313622e17 100644 --- a/src/core/qgscolorscheme.cpp +++ b/src/core/qgscolorscheme.cpp @@ -356,6 +356,12 @@ bool QgsUserColorScheme::erase() return false; } + // if file does not exist, nothing to do on the disk, so we can consider erasing done + if ( ! QFile::exists( filePath ) ) + { + return true; + } + //try to erase gpl file return QFile::remove( filePath ); } diff --git a/src/core/qgsconnectionpool.h b/src/core/qgsconnectionpool.h index 0e49bc2adf80..5b0fa1caa3f5 100644 --- a/src/core/qgsconnectionpool.h +++ b/src/core/qgsconnectionpool.h @@ -285,8 +285,6 @@ class QgsConnectionPool * If \a timeout is a negative value the calling thread will be blocked * until a connection becomes available. This is the default behavior. * - * - * * \returns initialized connection or NULLPTR if unsuccessful */ T acquireConnection( const QString &connInfo, int timeout = -1, bool requestMayBeNested = false ) diff --git a/src/core/qgsconnectionregistry.cpp b/src/core/qgsconnectionregistry.cpp new file mode 100644 index 000000000000..ffbe238e7f24 --- /dev/null +++ b/src/core/qgsconnectionregistry.cpp @@ -0,0 +1,42 @@ +/*************************************************************************** + qgsconnectionregistry.cpp + -------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsconnectionregistry.h" +#include "qgsproviderregistry.h" +#include "qgsprovidermetadata.h" + +QgsConnectionRegistry::QgsConnectionRegistry( QObject *parent SIP_TRANSFERTHIS ) + : QObject( parent ) +{ +} + +QgsAbstractProviderConnection *QgsConnectionRegistry::createConnection( const QString &id ) +{ + QRegularExpressionMatch m = QRegularExpression( QStringLiteral( "(.*?)\\://(.*)" ) ).match( id ); + if ( !m.hasMatch() ) + throw QgsProviderConnectionException( QObject::tr( "Invalid connection id" ) ); + + const QString providerKey = m.captured( 1 ); + const QString name = m.captured( 2 ); + + QgsProviderMetadata *md = QgsProviderRegistry::instance()->providerMetadata( providerKey ); + + if ( !md ) + throw QgsProviderConnectionException( QObject::tr( "Invalid provider key: %1" ).arg( providerKey ) ); + + return md->createConnection( name ); +} diff --git a/src/core/qgsconnectionregistry.h b/src/core/qgsconnectionregistry.h new file mode 100644 index 000000000000..20c933d5400a --- /dev/null +++ b/src/core/qgsconnectionregistry.h @@ -0,0 +1,75 @@ +/*************************************************************************** + qgsconnectionregistry.h + ------------------------ + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSCONNECTIONREGISTRY_H +#define QGSCONNECTIONREGISTRY_H + +#include "qgis_core.h" +#include "qgis.h" +#include + +class QgsAbstractProviderConnection; + + +/** + * \class QgsConnectionRegistry + * \ingroup core + * A registry for saved data provider connections, allowing retrieval of + * saved connections by name and provider type. + * + * QgsConnectionRegistry is not usually directly created, but rather accessed through + * QgsApplication::connectionRegistry(). + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsConnectionRegistry : public QObject +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsConnectionRegistry. + */ + QgsConnectionRegistry( QObject *parent SIP_TRANSFERTHIS = nullptr ); + + //! Registry cannot be copied + QgsConnectionRegistry( const QgsConnectionRegistry &other ) = delete; + //! Registry cannot be copied + QgsConnectionRegistry &operator=( const QgsConnectionRegistry &other ) = delete; + + /** + * Creates a new connection by loading the connection with the given \a id from the settings. + * + * The \a id string must be of the format "provider://connection_name", e.g. "postgres://my_connection" for + * the PostgreSQL connection saved as "my_connection". + * + * Ownership is transferred to the caller. + * + * \throws QgsProviderConnectionException + */ + QgsAbstractProviderConnection *createConnection( const QString &name ) SIP_THROW( QgsProviderConnectionException ) SIP_FACTORY; + + private: + +#ifdef SIP_RUN + QgsConnectionRegistry( const QgsConnectionRegistry &other ); +#endif +}; + +#endif // QGSCONNECTIONREGISTRY_H + + diff --git a/src/core/qgscoordinatereferencesystem.cpp b/src/core/qgscoordinatereferencesystem.cpp index be916f020fd6..31bf2d2e1f42 100644 --- a/src/core/qgscoordinatereferencesystem.cpp +++ b/src/core/qgscoordinatereferencesystem.cpp @@ -2983,7 +2983,7 @@ int QgsCoordinateReferenceSystem::syncDatabase() if ( name.isEmpty() ) name = QObject::tr( "Imported from GDAL" ); - bool deprecated = name.contains( QLatin1Literal( "(deprecated)" ) ); + bool deprecated = name.contains( QLatin1String( "(deprecated)" ) ); sql = QStringLiteral( "SELECT parameters,description,deprecated,noupdate FROM tbl_srs WHERE auth_name='EPSG' AND auth_id='%1'" ).arg( it.key() ); statement = database.prepare( sql, result ); diff --git a/src/core/qgscoordinatetransform_p.cpp b/src/core/qgscoordinatetransform_p.cpp index e200334fd0bc..28239cb907a5 100644 --- a/src/core/qgscoordinatetransform_p.cpp +++ b/src/core/qgscoordinatetransform_p.cpp @@ -84,7 +84,8 @@ QgsCoordinateTransformPrivate::QgsCoordinateTransformPrivate( const QgsCoordinat : mSourceCRS( source ) , mDestCRS( destination ) { - calculateTransforms( context ); + if ( mSourceCRS != mDestCRS ) + calculateTransforms( context ); } Q_NOWARN_DEPRECATED_POP @@ -169,6 +170,14 @@ bool QgsCoordinateTransformPrivate::initialize() mIsValid = true; + if ( mSourceCRS == mDestCRS ) + { + // If the source and destination projection are the same, set the short + // circuit flag (no transform takes place) + mShortCircuit = true; + return true; + } + // init the projections (destination and source) freeProj(); @@ -245,22 +254,9 @@ bool QgsCoordinateTransformPrivate::initialize() } #endif - //XXX todo overload == operator for QgsCoordinateReferenceSystem - //at the moment srs.parameters contains the whole proj def...soon it won't... - //if (mSourceCRS->toProj() == mDestCRS->toProj()) - if ( mSourceCRS == mDestCRS ) - { - // If the source and destination projection are the same, set the short - // circuit flag (no transform takes place) - mShortCircuit = true; - QgsDebugMsgLevel( QStringLiteral( "Source/Dest CRS equal, shortcircuit is set." ), 3 ); - } - else - { - // Transform must take place - mShortCircuit = false; - QgsDebugMsgLevel( QStringLiteral( "Source/Dest CRS not equal, shortcircuit is not set." ), 3 ); - } + // Transform must take place + mShortCircuit = false; + return mIsValid; } @@ -268,9 +264,18 @@ void QgsCoordinateTransformPrivate::calculateTransforms( const QgsCoordinateTran { // recalculate datum transforms from context #if PROJ_VERSION_MAJOR >= 6 - mProjCoordinateOperation = context.calculateCoordinateOperation( mSourceCRS, mDestCRS ); - mShouldReverseCoordinateOperation = context.mustReverseCoordinateOperation( mSourceCRS, mDestCRS ); - mAllowFallbackTransforms = context.allowFallbackTransform( mSourceCRS, mDestCRS ); + if ( mSourceCRS.isValid() && mDestCRS.isValid() ) + { + mProjCoordinateOperation = context.calculateCoordinateOperation( mSourceCRS, mDestCRS ); + mShouldReverseCoordinateOperation = context.mustReverseCoordinateOperation( mSourceCRS, mDestCRS ); + mAllowFallbackTransforms = context.allowFallbackTransform( mSourceCRS, mDestCRS ); + } + else + { + mProjCoordinateOperation.clear(); + mShouldReverseCoordinateOperation = false; + mAllowFallbackTransforms = false; + } #else Q_NOWARN_DEPRECATED_PUSH QgsDatumTransform::TransformPair transforms = context.calculateDatumTransforms( mSourceCRS, mDestCRS ); diff --git a/src/core/qgscoordinatetransformcontext.cpp b/src/core/qgscoordinatetransformcontext.cpp index f8afa9977fdd..226f7353c71e 100644 --- a/src/core/qgscoordinatetransformcontext.cpp +++ b/src/core/qgscoordinatetransformcontext.cpp @@ -21,6 +21,19 @@ #include "qgssettings.h" #include "qgsprojutils.h" +QString crsToKey( const QgsCoordinateReferenceSystem &crs ) +{ + return crs.authid().isEmpty() ? crs.toWkt( QgsCoordinateReferenceSystem::WKT2_2018 ) : crs.authid(); +} + +template<> +bool qMapLessThanKey>( const QPair &key1, + const QPair &key2 ) +{ + const QPair< QString, QString > key1String = qMakePair( crsToKey( key1.first ), crsToKey( key1.second ) ); + const QPair< QString, QString > key2String = qMakePair( crsToKey( key2.first ), crsToKey( key2.second ) ); + return key1String < key2String; +} QgsCoordinateTransformContext::QgsCoordinateTransformContext() : d( new QgsCoordinateTransformContextPrivate() ) @@ -28,11 +41,11 @@ QgsCoordinateTransformContext::QgsCoordinateTransformContext() QgsCoordinateTransformContext::~QgsCoordinateTransformContext() = default; -QgsCoordinateTransformContext::QgsCoordinateTransformContext( const QgsCoordinateTransformContext &rhs ) //NOLINT +QgsCoordinateTransformContext::QgsCoordinateTransformContext( const QgsCoordinateTransformContext &rhs ) //NOLINT : d( rhs.d ) {} -QgsCoordinateTransformContext &QgsCoordinateTransformContext::operator=( const QgsCoordinateTransformContext &rhs ) //NOLINT +QgsCoordinateTransformContext &QgsCoordinateTransformContext::operator=( const QgsCoordinateTransformContext &rhs ) //NOLINT { d = rhs.d; return *this; @@ -82,7 +95,7 @@ QMap, QString> QgsCoordinateTransformContext::coordinate d->mLock.unlock(); QMap, QString> results; for ( auto it = res.constBegin(); it != res.constEnd(); ++it ) - results.insert( it.key(), it.value().operation ); + results.insert( qMakePair( it.key().first.authid(), it.key().second.authid() ), it.value().operation ); return results; #else @@ -117,7 +130,7 @@ bool QgsCoordinateTransformContext::addCoordinateOperation( const QgsCoordinateR QgsCoordinateTransformContextPrivate::OperationDetails details; details.operation = coordinateOperationProjString; details.allowFallback = allowFallback; - d->mSourceDestDatumTransforms.insert( qMakePair( sourceCrs.authid(), destinationCrs.authid() ), details ); + d->mSourceDestDatumTransforms.insert( qMakePair( sourceCrs, destinationCrs ), details ); d->mLock.unlock(); return true; #else @@ -134,7 +147,11 @@ void QgsCoordinateTransformContext::removeSourceDestinationDatumTransform( const void QgsCoordinateTransformContext::removeCoordinateOperation( const QgsCoordinateReferenceSystem &sourceCrs, const QgsCoordinateReferenceSystem &destinationCrs ) { +#if PROJ_VERSION_MAJOR>=6 + d->mSourceDestDatumTransforms.remove( qMakePair( sourceCrs, destinationCrs ) ); +#else d->mSourceDestDatumTransforms.remove( qMakePair( sourceCrs.authid(), destinationCrs.authid() ) ); +#endif } bool QgsCoordinateTransformContext::hasTransform( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination ) const @@ -178,15 +195,15 @@ QgsDatumTransform::TransformPair QgsCoordinateTransformContext::calculateDatumTr QString QgsCoordinateTransformContext::calculateCoordinateOperation( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination ) const { #if PROJ_VERSION_MAJOR>=6 - const QString srcKey = source.authid(); - const QString destKey = destination.authid(); + if ( !source.isValid() || !destination.isValid() ) + return QString(); d->mLock.lockForRead(); - QgsCoordinateTransformContextPrivate::OperationDetails res = d->mSourceDestDatumTransforms.value( qMakePair( srcKey, destKey ), QgsCoordinateTransformContextPrivate::OperationDetails() ); + QgsCoordinateTransformContextPrivate::OperationDetails res = d->mSourceDestDatumTransforms.value( qMakePair( source, destination ), QgsCoordinateTransformContextPrivate::OperationDetails() ); if ( res.operation.isEmpty() ) { // try to reverse - res = d->mSourceDestDatumTransforms.value( qMakePair( destKey, srcKey ), QgsCoordinateTransformContextPrivate::OperationDetails() ); + res = d->mSourceDestDatumTransforms.value( qMakePair( destination, source ), QgsCoordinateTransformContextPrivate::OperationDetails() ); } d->mLock.unlock(); return res.operation; @@ -200,15 +217,15 @@ QString QgsCoordinateTransformContext::calculateCoordinateOperation( const QgsCo bool QgsCoordinateTransformContext::allowFallbackTransform( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination ) const { #if PROJ_VERSION_MAJOR>=6 - const QString srcKey = source.authid(); - const QString destKey = destination.authid(); + if ( !source.isValid() || !destination.isValid() ) + return false; d->mLock.lockForRead(); - QgsCoordinateTransformContextPrivate::OperationDetails res = d->mSourceDestDatumTransforms.value( qMakePair( srcKey, destKey ), QgsCoordinateTransformContextPrivate::OperationDetails() ); + QgsCoordinateTransformContextPrivate::OperationDetails res = d->mSourceDestDatumTransforms.value( qMakePair( source, destination ), QgsCoordinateTransformContextPrivate::OperationDetails() ); if ( res.operation.isEmpty() ) { // try to reverse - res = d->mSourceDestDatumTransforms.value( qMakePair( destKey, srcKey ), QgsCoordinateTransformContextPrivate::OperationDetails() ); + res = d->mSourceDestDatumTransforms.value( qMakePair( destination, source ), QgsCoordinateTransformContextPrivate::OperationDetails() ); } d->mLock.unlock(); return res.allowFallback; @@ -222,18 +239,18 @@ bool QgsCoordinateTransformContext::allowFallbackTransform( const QgsCoordinateR bool QgsCoordinateTransformContext::mustReverseCoordinateOperation( const QgsCoordinateReferenceSystem &source, const QgsCoordinateReferenceSystem &destination ) const { #if PROJ_VERSION_MAJOR>=6 - const QString srcKey = source.authid(); - const QString destKey = destination.authid(); + if ( !source.isValid() || !destination.isValid() ) + return false; d->mLock.lockForRead(); - QgsCoordinateTransformContextPrivate::OperationDetails res = d->mSourceDestDatumTransforms.value( qMakePair( srcKey, destKey ), QgsCoordinateTransformContextPrivate::OperationDetails() ); + QgsCoordinateTransformContextPrivate::OperationDetails res = d->mSourceDestDatumTransforms.value( qMakePair( source, destination ), QgsCoordinateTransformContextPrivate::OperationDetails() ); if ( !res.operation.isEmpty() ) { d->mLock.unlock(); return false; } // see if the reverse operation is present - res = d->mSourceDestDatumTransforms.value( qMakePair( destKey, srcKey ), QgsCoordinateTransformContextPrivate::OperationDetails() ); + res = d->mSourceDestDatumTransforms.value( qMakePair( destination, source ), QgsCoordinateTransformContextPrivate::OperationDetails() ); if ( !res.operation.isEmpty() ) { d->mLock.unlock(); @@ -273,10 +290,30 @@ bool QgsCoordinateTransformContext::readXml( const QDomElement &element, const Q for ( int i = 0; i < srcDestNodes.size(); ++i ) { const QDomElement transformElem = srcDestNodes.at( i ).toElement(); - const QString key1 = transformElem.attribute( QStringLiteral( "source" ) ); - const QString key2 = transformElem.attribute( QStringLiteral( "dest" ) ); #if PROJ_VERSION_MAJOR>=6 + const QDomElement srcElem = transformElem.firstChildElement( QStringLiteral( "src" ) ); + const QDomElement destElem = transformElem.firstChildElement( QStringLiteral( "dest" ) ); + + QgsCoordinateReferenceSystem srcCrs; + QgsCoordinateReferenceSystem destCrs; + if ( !srcElem.isNull() && !destElem.isNull() ) + { + srcCrs.readXml( srcElem ); + destCrs.readXml( destElem ); + } + else + { + // for older project compatibility + const QString key1 = transformElem.attribute( QStringLiteral( "source" ) ); + const QString key2 = transformElem.attribute( QStringLiteral( "dest" ) ); + srcCrs = QgsCoordinateReferenceSystem( key1 ); + destCrs = QgsCoordinateReferenceSystem( key2 ); + } + + if ( !srcCrs.isValid() || !destCrs.isValid() ) + continue; + const QString coordinateOp = transformElem.attribute( QStringLiteral( "coordinateOp" ) ); const bool allowFallback = transformElem.attribute( QStringLiteral( "allowFallback" ), QStringLiteral( "1" ) ).toInt(); @@ -291,8 +328,11 @@ bool QgsCoordinateTransformContext::readXml( const QDomElement &element, const Q QgsCoordinateTransformContextPrivate::OperationDetails deets; deets.operation = coordinateOp; deets.allowFallback = allowFallback; - d->mSourceDestDatumTransforms.insert( qMakePair( key1, key2 ), deets ); + d->mSourceDestDatumTransforms.insert( qMakePair( srcCrs, destCrs ), deets ); #else + const QString key1 = transformElem.attribute( QStringLiteral( "source" ) ); + const QString key2 = transformElem.attribute( QStringLiteral( "dest" ) ); + QString value1 = transformElem.attribute( QStringLiteral( "sourceTransform" ) ); QString value2 = transformElem.attribute( QStringLiteral( "destTransform" ) ); @@ -332,18 +372,30 @@ void QgsCoordinateTransformContext::writeXml( QDomElement &element, const QgsRea { d->mLock.lockForRead(); - QDomElement contextElem = element.ownerDocument().createElement( QStringLiteral( "transformContext" ) ); + QDomDocument doc = element.ownerDocument(); + + QDomElement contextElem = doc.createElement( QStringLiteral( "transformContext" ) ); //src/dest transforms for ( auto it = d->mSourceDestDatumTransforms.constBegin(); it != d->mSourceDestDatumTransforms.constEnd(); ++ it ) { - QDomElement transformElem = element.ownerDocument().createElement( QStringLiteral( "srcDest" ) ); - transformElem.setAttribute( QStringLiteral( "source" ), it.key().first ); - transformElem.setAttribute( QStringLiteral( "dest" ), it.key().second ); + QDomElement transformElem = doc.createElement( QStringLiteral( "srcDest" ) ); #if PROJ_VERSION_MAJOR>=6 + QDomElement srcElem = doc.createElement( QStringLiteral( "src" ) ); + QDomElement destElem = doc.createElement( QStringLiteral( "dest" ) ); + + it.key().first.writeXml( srcElem, doc ); + it.key().second.writeXml( destElem, doc ); + + transformElem.appendChild( srcElem ); + transformElem.appendChild( destElem ); + transformElem.setAttribute( QStringLiteral( "coordinateOp" ), it.value().operation ); transformElem.setAttribute( QStringLiteral( "allowFallback" ), it.value().allowFallback ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); #else + transformElem.setAttribute( QStringLiteral( "source" ), it.key().first ); + transformElem.setAttribute( QStringLiteral( "dest" ), it.key().second ); + Q_NOWARN_DEPRECATED_PUSH transformElem.setAttribute( QStringLiteral( "sourceTransform" ), it.value().sourceTransformId < 0 ? QString() : QgsDatumTransform::datumTransformToProj( it.value().sourceTransformId ) ); transformElem.setAttribute( QStringLiteral( "destTransform" ), it.value().destinationTransformId < 0 ? QString() : QgsDatumTransform::datumTransformToProj( it.value().destinationTransformId ) ); @@ -369,7 +421,7 @@ void QgsCoordinateTransformContext::readSettings() //collect src and dest entries that belong together #if PROJ_VERSION_MAJOR>=6 - QMap< QPair< QString, QString >, QgsCoordinateTransformContextPrivate::OperationDetails > transforms; + QMap< QPair< QgsCoordinateReferenceSystem, QgsCoordinateReferenceSystem >, QgsCoordinateTransformContextPrivate::OperationDetails > transforms; #else QMap< QPair< QString, QString >, QPair< int, int > > transforms; #endif @@ -390,12 +442,15 @@ void QgsCoordinateTransformContext::readSettings() destAuthId = split.at( 1 ).split( '_' ).at( 0 ); } + if ( srcAuthId.isEmpty() || destAuthId.isEmpty() ) + continue; + const QString proj = settings.value( *pkeyIt ).toString(); const bool allowFallback = settings.value( QStringLiteral( "%1//%2_allowFallback" ).arg( srcAuthId, destAuthId ) ).toBool(); QgsCoordinateTransformContextPrivate::OperationDetails deets; deets.operation = proj; deets.allowFallback = allowFallback; - transforms[ qMakePair( srcAuthId, destAuthId )] = deets; + transforms[ qMakePair( QgsCoordinateReferenceSystem( srcAuthId ), QgsCoordinateReferenceSystem( destAuthId ) )] = deets; } #else if ( pkeyIt->contains( QLatin1String( "srcTransform" ) ) || pkeyIt->contains( QLatin1String( "destTransform" ) ) ) @@ -458,8 +513,16 @@ void QgsCoordinateTransformContext::writeSettings() for ( auto transformIt = d->mSourceDestDatumTransforms.constBegin(); transformIt != d->mSourceDestDatumTransforms.constEnd(); ++transformIt ) { +#if PROJ_VERSION_MAJOR>=6 + const QString srcAuthId = transformIt.key().first.authid(); + const QString destAuthId = transformIt.key().second.authid(); +#else const QString srcAuthId = transformIt.key().first; const QString destAuthId = transformIt.key().second; +#endif + + if ( srcAuthId.isEmpty() || destAuthId.isEmpty() ) + continue; // not so nice, but alternative would be to shove whole CRS wkt into the settings values... #if PROJ_VERSION_MAJOR>=6 const QString proj = transformIt.value().operation; diff --git a/src/core/qgscoordinatetransformcontext_p.h b/src/core/qgscoordinatetransformcontext_p.h index 5274fa55eba4..0a5431177049 100644 --- a/src/core/qgscoordinatetransformcontext_p.h +++ b/src/core/qgscoordinatetransformcontext_p.h @@ -70,7 +70,7 @@ class QgsCoordinateTransformContextPrivate : public QSharedData return operation == other.operation && allowFallback == other.allowFallback; } }; - QMap< QPair< QString, QString >, OperationDetails > mSourceDestDatumTransforms; + QMap< QPair< QgsCoordinateReferenceSystem, QgsCoordinateReferenceSystem >, OperationDetails > mSourceDestDatumTransforms; #else QMap< QPair< QString, QString >, QgsDatumTransform::TransformPair > mSourceDestDatumTransforms; #endif diff --git a/src/core/qgsdatabaseschemamodel.cpp b/src/core/qgsdatabaseschemamodel.cpp index 20165410970e..78e8db9a6ddb 100644 --- a/src/core/qgsdatabaseschemamodel.cpp +++ b/src/core/qgsdatabaseschemamodel.cpp @@ -54,7 +54,7 @@ int QgsDatabaseSchemaModel::rowCount( const QModelIndex &parent ) const if ( parent.isValid() ) return 0; - return mSchemas.count(); + return mSchemas.count() + ( mAllowEmpty ? 1 : 0 ); } int QgsDatabaseSchemaModel::columnCount( const QModelIndex &parent ) const @@ -69,10 +69,22 @@ QVariant QgsDatabaseSchemaModel::data( const QModelIndex &index, int role ) cons if ( !index.isValid() ) return QVariant(); - const QString schemaName = mSchemas.value( index.row() ); + if ( index.row() == 0 && mAllowEmpty ) + { + if ( role == RoleEmpty ) + return true; + + return QVariant(); + } + + const QString schemaName = mSchemas.value( index.row() - ( mAllowEmpty ? 1 : 0 ) ); switch ( role ) { + case RoleEmpty: + return false; + case Qt::DisplayRole: + case Qt::EditRole: case Qt::ToolTipRole: { return schemaName; @@ -92,6 +104,25 @@ QModelIndex QgsDatabaseSchemaModel::index( int row, int column, const QModelInde return QModelIndex(); } +void QgsDatabaseSchemaModel::setAllowEmptySchema( bool allowEmpty ) +{ + if ( allowEmpty == mAllowEmpty ) + return; + + if ( allowEmpty ) + { + beginInsertRows( QModelIndex(), 0, 0 ); + mAllowEmpty = true; + endInsertRows(); + } + else + { + beginRemoveRows( QModelIndex(), 0, 0 ); + mAllowEmpty = false; + endRemoveRows(); + } +} + void QgsDatabaseSchemaModel::refresh() { const QStringList newSchemas = mConnection->schemas(); @@ -101,8 +132,8 @@ void QgsDatabaseSchemaModel::refresh() { if ( !newSchemas.contains( oldSchema ) ) { - int r = mSchemas.indexOf( oldSchema ); - beginRemoveRows( QModelIndex(), r, r ); + int r = mSchemas.indexOf( oldSchema ) ; + beginRemoveRows( QModelIndex(), r + ( mAllowEmpty ? 1 : 0 ), r + ( mAllowEmpty ? 1 : 0 ) ); mSchemas.removeAt( r ); endRemoveRows(); } @@ -112,7 +143,7 @@ void QgsDatabaseSchemaModel::refresh() { if ( !mSchemas.contains( newSchema ) ) { - beginInsertRows( QModelIndex(), mSchemas.count(), mSchemas.count() ); + beginInsertRows( QModelIndex(), mSchemas.count() + ( mAllowEmpty ? 1 : 0 ), mSchemas.count() + ( mAllowEmpty ? 1 : 0 ) ); mSchemas.append( newSchema ); endInsertRows(); } diff --git a/src/core/qgsdatabaseschemamodel.h b/src/core/qgsdatabaseschemamodel.h index bcf5f2d376fa..c22664f9a6c1 100644 --- a/src/core/qgsdatabaseschemamodel.h +++ b/src/core/qgsdatabaseschemamodel.h @@ -45,6 +45,12 @@ class CORE_EXPORT QgsDatabaseSchemaModel : public QAbstractItemModel public: + //! Model roles + enum Role + { + RoleEmpty = Qt::UserRole, //!< Entry is an empty entry + }; + /** * Constructor for QgsDatabaseSchemaModel, for the specified \a provider and \a connection name. * @@ -67,6 +73,18 @@ class CORE_EXPORT QgsDatabaseSchemaModel : public QAbstractItemModel QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; QModelIndex index( int row, int column, const QModelIndex &parent ) const override; + /** + * Sets whether an optional empty schema ("not set") option is present in the model. + * \see allowEmptySchema() + */ + void setAllowEmptySchema( bool allowEmpty ); + + /** + * Returns TRUE if the model allows the empty schema ("not set") choice. + * \see setAllowEmptySchema() + */ + bool allowEmptySchema() const { return mAllowEmpty; } + public slots: /** @@ -78,6 +96,7 @@ class CORE_EXPORT QgsDatabaseSchemaModel : public QAbstractItemModel void init(); std::unique_ptr< QgsAbstractDatabaseProviderConnection > mConnection; QStringList mSchemas; + bool mAllowEmpty = false; }; #endif // QGSDATABASESCHEMAMODEL_H diff --git a/src/core/qgsdatabasetablemodel.cpp b/src/core/qgsdatabasetablemodel.cpp index c21dca9bf900..3230f69b2d87 100644 --- a/src/core/qgsdatabasetablemodel.cpp +++ b/src/core/qgsdatabasetablemodel.cpp @@ -57,7 +57,7 @@ int QgsDatabaseTableModel::rowCount( const QModelIndex &parent ) const if ( parent.isValid() ) return 0; - return mTables.count(); + return mTables.count() + ( mAllowEmpty ? 1 : 0 ); } int QgsDatabaseTableModel::columnCount( const QModelIndex &parent ) const @@ -72,14 +72,26 @@ QVariant QgsDatabaseTableModel::data( const QModelIndex &index, int role ) const if ( !index.isValid() ) return QVariant(); - if ( index.row() >= mTables.count() ) + if ( index.row() == 0 && mAllowEmpty ) + { + if ( role == RoleEmpty ) + return true; + return QVariant(); + } - const QgsAbstractDatabaseProviderConnection::TableProperty &table = mTables[ index.row() ]; + if ( index.row() - ( mAllowEmpty ? 1 : 0 ) >= mTables.count() ) + return QVariant(); + + const QgsAbstractDatabaseProviderConnection::TableProperty &table = mTables[ index.row() - ( mAllowEmpty ? 1 : 0 ) ]; switch ( role ) { + case RoleEmpty: + return false; + case Qt::DisplayRole: case Qt::ToolTipRole: + case Qt::EditRole: { return mSchema.isEmpty() && !table.schema().isEmpty() ? QStringLiteral( "%1.%2" ).arg( table.schema(), table.tableName() ) : table.tableName(); } @@ -158,6 +170,25 @@ QModelIndex QgsDatabaseTableModel::index( int row, int column, const QModelIndex return QModelIndex(); } +void QgsDatabaseTableModel::setAllowEmptyTable( bool allowEmpty ) +{ + if ( allowEmpty == mAllowEmpty ) + return; + + if ( allowEmpty ) + { + beginInsertRows( QModelIndex(), 0, 0 ); + mAllowEmpty = true; + endInsertRows(); + } + else + { + beginRemoveRows( QModelIndex(), 0, 0 ); + mAllowEmpty = false; + endRemoveRows(); + } +} + void QgsDatabaseTableModel::refresh() { const QList< QgsAbstractDatabaseProviderConnection::TableProperty > newTables = mConnection->tables( mSchema ); @@ -168,7 +199,7 @@ void QgsDatabaseTableModel::refresh() if ( !newTables.contains( oldTable ) ) { int r = mTables.indexOf( oldTable ); - beginRemoveRows( QModelIndex(), r, r ); + beginRemoveRows( QModelIndex(), r + ( mAllowEmpty ? 1 : 0 ), r + ( mAllowEmpty ? 1 : 0 ) ); mTables.removeAt( r ); endRemoveRows(); } @@ -178,7 +209,7 @@ void QgsDatabaseTableModel::refresh() { if ( !mTables.contains( newTable ) ) { - beginInsertRows( QModelIndex(), mTables.count(), mTables.count() ); + beginInsertRows( QModelIndex(), mTables.count() + ( mAllowEmpty ? 1 : 0 ), mTables.count() + ( mAllowEmpty ? 1 : 0 ) ); mTables.append( newTable ); endInsertRows(); } diff --git a/src/core/qgsdatabasetablemodel.h b/src/core/qgsdatabasetablemodel.h index ede5b05d2113..6a6c2e3621fd 100644 --- a/src/core/qgsdatabasetablemodel.h +++ b/src/core/qgsdatabasetablemodel.h @@ -52,6 +52,7 @@ class CORE_EXPORT QgsDatabaseTableModel : public QAbstractItemModel RoleCustomInfo, //!< Custom info variant map role RoleWkbType, //!< WKB type for primary (first) geometry column in table RoleCrs, //!< CRS for primary (first) geometry column in table + RoleEmpty, //!< Entry is an empty entry }; /** @@ -80,6 +81,18 @@ class CORE_EXPORT QgsDatabaseTableModel : public QAbstractItemModel QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; QModelIndex index( int row, int column, const QModelIndex &parent ) const override; + /** + * Sets whether an optional empty table ("not set") option is present in the model. + * \see allowEmptyTable() + */ + void setAllowEmptyTable( bool allowEmpty ); + + /** + * Returns TRUE if the model allows the empty table ("not set") choice. + * \see setAllowEmptyTable() + */ + bool allowEmptyTable() const { return mAllowEmpty; } + public slots: /** @@ -92,6 +105,7 @@ class CORE_EXPORT QgsDatabaseTableModel : public QAbstractItemModel std::unique_ptr< QgsAbstractDatabaseProviderConnection > mConnection; QString mSchema; QList mTables; + bool mAllowEmpty = false; }; #endif // QGSDATABASETABLEMODEL_H diff --git a/src/core/qgsdataitem.cpp b/src/core/qgsdataitem.cpp index 8e4b80c5f3b4..85d3de6394a9 100644 --- a/src/core/qgsdataitem.cpp +++ b/src/core/qgsdataitem.cpp @@ -80,6 +80,11 @@ QIcon QgsLayerItem::iconMesh() return QgsApplication::getThemeIcon( QStringLiteral( "/mIconMeshLayer.svg" ) ); } +QIcon QgsLayerItem::iconVectorTile() +{ + return QgsApplication::getThemeIcon( QStringLiteral( "/mIconVectorTileLayer.svg" ) ); +} + QIcon QgsLayerItem::iconDefault() { return QgsApplication::getThemeIcon( QStringLiteral( "/mIconLayer.png" ) ); @@ -643,6 +648,9 @@ QgsMapLayerType QgsLayerItem::mapLayerType() const case QgsLayerItem::Mesh: return QgsMapLayerType::MeshLayer; + case QgsLayerItem::VectorTile: + return QgsMapLayerType::VectorTileLayer; + case QgsLayerItem::Plugin: return QgsMapLayerType::PluginLayer; @@ -693,6 +701,8 @@ QgsLayerItem::LayerType QgsLayerItem::typeFromMapLayer( QgsMapLayer *layer ) return Plugin; case QgsMapLayerType::MeshLayer: return Mesh; + case QgsMapLayerType::VectorTileLayer: + return VectorTile; } return Vector; // no warnings } @@ -778,6 +788,7 @@ QgsMimeDataUtils::Uri QgsLayerItem::mimeUri() const case Raster: case Plugin: case Mesh: + case VectorTile: break; } break; @@ -787,6 +798,9 @@ QgsMimeDataUtils::Uri QgsLayerItem::mimeUri() const case QgsMapLayerType::MeshLayer: u.layerType = QStringLiteral( "mesh" ); break; + case QgsMapLayerType::VectorTileLayer: + u.layerType = QStringLiteral( "vector-tile" ); + break; case QgsMapLayerType::PluginLayer: u.layerType = QStringLiteral( "plugin" ); break; diff --git a/src/core/qgsdataitem.h b/src/core/qgsdataitem.h index 637f8d3cb1c3..e697ecfdf151 100644 --- a/src/core/qgsdataitem.h +++ b/src/core/qgsdataitem.h @@ -506,7 +506,8 @@ class CORE_EXPORT QgsLayerItem : public QgsDataItem Database, Table, Plugin, //!< Added in 2.10 - Mesh //!< Added in 3.2 + Mesh, //!< Added in 3.2 + VectorTile //!< Added in 3.14 }; Q_ENUM( LayerType ) @@ -595,6 +596,8 @@ class CORE_EXPORT QgsLayerItem : public QgsDataItem static QIcon iconDefault(); //! Returns icon for mesh layer type static QIcon iconMesh(); + //! Returns icon for vector tile layer + static QIcon iconVectorTile(); //! \returns the layer name virtual QString layerName() const { return name(); } diff --git a/src/core/qgsdataprovider.cpp b/src/core/qgsdataprovider.cpp index 0de547f3b85a..b0d6ad0748b1 100644 --- a/src/core/qgsdataprovider.cpp +++ b/src/core/qgsdataprovider.cpp @@ -30,6 +30,11 @@ QgsDataProviderTemporalCapabilities *QgsDataProvider::temporalCapabilities() return nullptr; } +const QgsDataProviderTemporalCapabilities *QgsDataProvider::temporalCapabilities() const +{ + return nullptr; +} + void QgsDataProvider::reloadData() { reloadProviderData(); diff --git a/src/core/qgsdataprovider.h b/src/core/qgsdataprovider.h index 7a4ae513b04f..765682b24b8c 100644 --- a/src/core/qgsdataprovider.h +++ b/src/core/qgsdataprovider.h @@ -143,7 +143,7 @@ class CORE_EXPORT QgsDataProvider : public QObject { if ( expandAuthConfig && mDataSourceURI.contains( QLatin1String( "authcfg" ) ) ) { - QgsDataSourceUri uri( mDataSourceURI ); + const QgsDataSourceUri uri( mDataSourceURI ); return uri.uri( expandAuthConfig ); } else @@ -152,6 +152,16 @@ class CORE_EXPORT QgsDataProvider : public QObject } } + /** + * Returns a short comment for the data that this provider is + * providing access to (e.g. the comment for postgres table). + * + * \note The default implementation returns an empty string. + * \since QGIS 3.14 + */ + virtual QString dataComment() const { return QString(); }; + + /** * Set the data source specification. * @@ -181,6 +191,15 @@ class CORE_EXPORT QgsDataProvider : public QObject */ virtual QgsDataProviderTemporalCapabilities *temporalCapabilities(); + /** + * Returns the provider's temporal capabilities. + * + * This may be NULLPTR, depending on the data provider. + * + * \since QGIS 3.14 + */ + virtual const QgsDataProviderTemporalCapabilities *temporalCapabilities() const SIP_SKIP; + /** * Returns the extent of the layer * \returns QgsRectangle containing the extent of the layer diff --git a/src/core/qgsdataprovidertemporalcapabilities.h b/src/core/qgsdataprovidertemporalcapabilities.h index de4bb193c213..1499d63d23f0 100644 --- a/src/core/qgsdataprovidertemporalcapabilities.h +++ b/src/core/qgsdataprovidertemporalcapabilities.h @@ -43,6 +43,10 @@ class CORE_EXPORT QgsDataProviderTemporalCapabilities { sipType = sipType_QgsVectorDataProviderTemporalCapabilities; } + else if ( dynamic_cast < QgsMeshDataProviderTemporalCapabilities * >( sipCpp ) ) + { + sipType = sipType_QgsMeshDataProviderTemporalCapabilities; + } else { sipType = 0; diff --git a/src/core/qgsfeaturefiltermodel.cpp b/src/core/qgsfeaturefiltermodel.cpp index e3278395d018..88d1c3c3cd55 100644 --- a/src/core/qgsfeaturefiltermodel.cpp +++ b/src/core/qgsfeaturefiltermodel.cpp @@ -63,6 +63,8 @@ void QgsFeatureFilterModel::setSourceLayer( QgsVectorLayer *sourceLayer ) mExpressionContext = sourceLayer->createExpressionContext(); reload(); emit sourceLayerChanged(); + + setDisplayExpression( sourceLayer->displayExpression() ); } QString QgsFeatureFilterModel::displayExpression() const @@ -225,9 +227,11 @@ QVariant QgsFeatureFilterModel::data( const QModelIndex &index, int role ) const void QgsFeatureFilterModel::updateCompleter() { emit beginUpdate(); - if ( !mGatherer ) + + QgsFieldExpressionValuesGatherer *gatherer = qobject_cast( sender() ); + if ( gatherer->wasCanceled() ) { - emit endUpdate(); + delete gatherer; return; } @@ -239,7 +243,8 @@ void QgsFeatureFilterModel::updateCompleter() } // Only reloading the current entry? - if ( mGatherer->data().toBool() ) + bool reloadCurrentFeatureOnly = mGatherer->data().toBool(); + if ( reloadCurrentFeatureOnly ) { if ( !entries.isEmpty() ) { @@ -270,31 +275,30 @@ void QgsFeatureFilterModel::updateCompleter() const int newEntriesSize = entries.size(); - // Find the index of the extra entry in the new list + // fixed entry is either NULL or extra value + const int nbFixedEntry = ( mExtraValueDoesNotExist ? 1 : 0 ) + ( mAllowNull ? 1 : 0 ); + + // Find the index of the current entry in the new list int currentEntryInNewList = -1; - if ( mExtraIdentifierValueIndex != -1 ) + if ( mExtraIdentifierValueIndex != -1 && mExtraIdentifierValueIndex < mEntries.count() ) { for ( int i = 0; i < newEntriesSize; ++i ) { - if ( qVariantListCompare( entries.at( i ).identifierValues, mExtraIdentifierValues ) ) + if ( qVariantListCompare( entries.at( i ).identifierValues, mEntries.at( mExtraIdentifierValueIndex ).identifierValues ) ) { - currentEntryInNewList = i; mEntries.replace( mExtraIdentifierValueIndex, entries.at( i ) ); - emit dataChanged( index( mExtraIdentifierValueIndex, 0, QModelIndex() ), index( mExtraIdentifierValueIndex, 0, QModelIndex() ) ); + currentEntryInNewList = i; setExtraValueDoesNotExist( false ); break; } } } - else - { - Q_ASSERT_X( false, "QgsFeatureFilterModel::updateCompleter", "No extra identifier value generated. Should not get here." ); - } int firstRow = 0; - // Move the extra entry to the first position - if ( mExtraIdentifierValueIndex != -1 && currentEntryInNewList != -1 ) + // Move current entry to the first position if this is a fixed entry or because + // the entry exists in the new list + if ( mExtraIdentifierValueIndex > -1 && ( mExtraIdentifierValueIndex < nbFixedEntry || currentEntryInNewList != -1 ) ) { if ( mExtraIdentifierValueIndex != 0 ) { @@ -309,19 +313,24 @@ void QgsFeatureFilterModel::updateCompleter() beginRemoveRows( QModelIndex(), firstRow, mEntries.size() - firstRow ); mEntries.remove( firstRow, mEntries.size() - firstRow ); - // we need to reset mExtraIdentifierValueIndex variable if we remove all rows - // before endRemoveRows, if not setExtraIdentifierValuesUnguarded will be called - // and a null value will be added to mEntries - mExtraIdentifierValueIndex = firstRow > 0 ? mExtraIdentifierValueIndex : 0; + // if we remove all rows before endRemoveRows, setExtraIdentifierValuesUnguarded will be called + // and a null value will be added to mEntries, so we block setExtraIdentifierValuesUnguarded call + mIsSettingExtraIdentifierValue = true; endRemoveRows(); + mIsSettingExtraIdentifierValue = false; + if ( currentEntryInNewList == -1 ) { - beginInsertRows( QModelIndex(), 1, entries.size() + 1 ); + beginInsertRows( QModelIndex(), firstRow, entries.size() + 1 ); mEntries += entries; endInsertRows(); - setExtraIdentifierValuesIndex( mAllowNull && !mEntries.isEmpty() ? 1 : 0 ); + + // if all entries have been cleaned (firstRow == 0) + // and there is a value in entries, prefer this value over NULL + // else chose the first one (the previous one) + setExtraIdentifierValuesIndex( firstRow == 0 && mAllowNull && !entries.isEmpty() ? 1 : 0, firstRow == 0 ); } else { @@ -336,7 +345,11 @@ void QgsFeatureFilterModel::updateCompleter() mEntries.replace( 0, entries.at( 0 ) ); } - emit dataChanged( index( currentEntryInNewList, 0, QModelIndex() ), index( currentEntryInNewList, 0, QModelIndex() ) ); + // don't notify for a change if it's a fixed entry + if ( currentEntryInNewList >= nbFixedEntry ) + { + emit dataChanged( index( currentEntryInNewList, 0, QModelIndex() ), index( currentEntryInNewList, 0, QModelIndex() ) ); + } beginInsertRows( QModelIndex(), currentEntryInNewList + 1, newEntriesSize - currentEntryInNewList - 1 ); mEntries += entries.mid( currentEntryInNewList + 1 ); @@ -348,13 +361,10 @@ void QgsFeatureFilterModel::updateCompleter() } emit endUpdate(); - gathererThreadFinished(); -} -void QgsFeatureFilterModel::gathererThreadFinished() -{ - // It's possible that gatherer run method is not completely over, so we wait - mGatherer->wait(); + // scheduleReload and updateCompleter lives in the same thread so if the gatherer hasn't been stopped + // (checked before), mGatherer still references the current gatherer + Q_ASSERT( gatherer == mGatherer ); delete mGatherer; mGatherer = nullptr; emit isLoadingChanged(); @@ -369,10 +379,6 @@ void QgsFeatureFilterModel::scheduledReload() if ( mGatherer ) { - // Send the gatherer thread to the graveyard: - // forget about it, tell it to stop and delete when finished - disconnect( mGatherer, &QgsFieldExpressionValuesGatherer::collectedValues, this, &QgsFeatureFilterModel::updateCompleter ); - connect( mGatherer, &QgsFieldExpressionValuesGatherer::finished, mGatherer, &QgsFieldExpressionValuesGatherer::deleteLater ); mGatherer->stop(); wasLoading = true; } @@ -421,8 +427,8 @@ void QgsFeatureFilterModel::scheduledReload() mGatherer = new QgsFieldExpressionValuesGatherer( mSourceLayer, mDisplayExpression, mIdentifierFields, request ); mGatherer->setData( mShouldReloadCurrentFeature ); + connect( mGatherer, &QgsFieldExpressionValuesGatherer::finished, this, &QgsFeatureFilterModel::updateCompleter ); - connect( mGatherer, &QgsFieldExpressionValuesGatherer::collectedValues, this, &QgsFeatureFilterModel::updateCompleter ); mGatherer->start(); if ( !wasLoading ) @@ -489,24 +495,26 @@ void QgsFeatureFilterModel::setExtraIdentifierValuesUnguarded( const QVariantLis // Value not found in current entries if ( mExtraIdentifierValueIndex != index ) { - beginInsertRows( QModelIndex(), 0, 0 ); - if ( extraIdentifierValues.isEmpty() ) - { - mEntries.prepend( nullEntry() ); - } - else + if ( !extraIdentifierValues.isEmpty() || mAllowNull ) { - QStringList values; - for ( const QVariant &v : qgis::as_const( extraIdentifierValues ) ) - values << QStringLiteral( "(%1)" ).arg( v.toString() ); - - mEntries.prepend( Entry( extraIdentifierValues, values.join( QStringLiteral( " " ) ), QgsFeature() ) ); - } - endInsertRows(); + beginInsertRows( QModelIndex(), 0, 0 ); + if ( !extraIdentifierValues.isEmpty() ) + { + QStringList values; + for ( const QVariant &v : qgis::as_const( extraIdentifierValues ) ) + values << QStringLiteral( "(%1)" ).arg( v.toString() ); - setExtraIdentifierValuesIndex( 0, true ); + mEntries.prepend( Entry( extraIdentifierValues, values.join( QStringLiteral( " " ) ), QgsFeature() ) ); + reloadCurrentFeature(); + } + else + { + mEntries.prepend( nullEntry() ); + } + endInsertRows(); - reloadCurrentFeature(); + setExtraIdentifierValuesIndex( 0, true ); + } } } @@ -617,7 +625,7 @@ QVariantList QgsFeatureFilterModel::extraIdentifierValues() const { QVariantList nullValues; for ( int i = 0; i < mIdentifierFields.count(); i++ ) - nullValues << QVariant(); + nullValues << QVariant( QVariant::Int ); return nullValues; } return mExtraIdentifierValues; diff --git a/src/core/qgsfeaturefiltermodel.h b/src/core/qgsfeaturefiltermodel.h index 0035530b09c5..ec01b175be6a 100644 --- a/src/core/qgsfeaturefiltermodel.h +++ b/src/core/qgsfeaturefiltermodel.h @@ -296,7 +296,6 @@ class CORE_EXPORT QgsFeatureFilterModel : public QAbstractItemModel private slots: void updateCompleter(); - void gathererThreadFinished(); void scheduledReload(); private: diff --git a/src/core/qgsfeaturefiltermodel_p.h b/src/core/qgsfeaturefiltermodel_p.h index 0959e5e7d748..33da75becbbc 100644 --- a/src/core/qgsfeaturefiltermodel_p.h +++ b/src/core/qgsfeaturefiltermodel_p.h @@ -16,6 +16,7 @@ #define QGSFEATUREFILTERMODEL_P_H #include +#include #include "qgsfeaturefiltermodel.h" #include "qgslogger.h" #include "qgsvectorlayerfeatureiterator.h" @@ -68,21 +69,25 @@ class QgsFieldExpressionValuesGatherer: public QThread attributes << feat.attribute( idx ); mEntries.append( QgsFeatureFilterModel::Entry( attributes, mDisplayExpression.evaluate( &mExpressionContext ).toString(), feat ) ); + QMutexLocker locker( &mCancelMutex ); if ( mWasCanceled ) return; } - - emit collectedValues(); } //! Informs the gatherer to immediately stop collecting values void stop() { + QMutexLocker locker( &mCancelMutex ); mWasCanceled = true; } //! Returns TRUE if collection was canceled before completion - bool wasCanceled() const { return mWasCanceled; } + bool wasCanceled() const + { + QMutexLocker locker( &mCancelMutex ); + return mWasCanceled; + } QVector entries() const { @@ -110,14 +115,6 @@ class QgsFieldExpressionValuesGatherer: public QThread mData = data; } - signals: - - /** - * Emitted when values have been collected - * \param values list of unique matching string values - */ - void collectedValues(); - private: std::unique_ptr mSource; @@ -126,6 +123,7 @@ class QgsFieldExpressionValuesGatherer: public QThread QgsFeatureRequest mRequest; QgsFeatureIterator mIterator; bool mWasCanceled = false; + mutable QMutex mCancelMutex; QVector mEntries; QStringList mIdentifierFields; QVariant mData; diff --git a/src/core/qgsfeaturesink.cpp b/src/core/qgsfeaturesink.cpp index 6316ada45643..f4aaea81db52 100644 --- a/src/core/qgsfeaturesink.cpp +++ b/src/core/qgsfeaturesink.cpp @@ -42,7 +42,3 @@ bool QgsFeatureSink::addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink:: return result; } - -QgsProxyFeatureSink::QgsProxyFeatureSink( QgsFeatureSink *sink ) - : mSink( sink ) -{} diff --git a/src/core/qgsfeaturesink.h b/src/core/qgsfeaturesink.h index c8f61b805979..015052b7ba6b 100644 --- a/src/core/qgsfeaturesink.h +++ b/src/core/qgsfeaturesink.h @@ -102,42 +102,6 @@ class CORE_EXPORT QgsFeatureSink Q_DECLARE_OPERATORS_FOR_FLAGS( QgsFeatureSink::Flags ) - -/** - * \class QgsProxyFeatureSink - * \ingroup core - * A simple feature sink which proxies feature addition on to another feature sink. - * - * This class is designed to allow factory methods which always return new QgsFeatureSink - * objects. Since it is not always possible to create an entirely new QgsFeatureSink - * (e.g. if the feature sink is a layer's data provider), a new QgsProxyFeatureSink - * can instead be returned which forwards features on to the destination sink. The - * proxy sink can be safely deleted without affecting the destination sink. - * - * \since QGIS 3.0 - */ -class CORE_EXPORT QgsProxyFeatureSink : public QgsFeatureSink -{ - public: - - /** - * Constructs a new QgsProxyFeatureSink which forwards features onto a destination \a sink. - */ - QgsProxyFeatureSink( QgsFeatureSink *sink ); - bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = nullptr ) override { return mSink->addFeature( feature, flags ); } - bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = nullptr ) override { return mSink->addFeatures( features, flags ); } - bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = nullptr ) override { return mSink->addFeatures( iterator, flags ); } - - /** - * Returns the destination QgsFeatureSink which the proxy will forward features to. - */ - QgsFeatureSink *destinationSink() { return mSink; } - - private: - - QgsFeatureSink *mSink = nullptr; -}; - Q_DECLARE_METATYPE( QgsFeatureSink * ) #endif // QGSFEATURESINK_H diff --git a/src/core/qgsfield.cpp b/src/core/qgsfield.cpp index 4dd97e48bfe0..c3f259958818 100644 --- a/src/core/qgsfield.cpp +++ b/src/core/qgsfield.cpp @@ -103,6 +103,29 @@ QString QgsField::displayNameWithAlias() const return QStringLiteral( "%1 (%2)" ).arg( name() ).arg( alias() ); } +QString QgsField::displayType( const bool showConstraints ) const +{ + QString typeStr = typeName(); + + if ( length() > 0 && precision() > 0 ) + typeStr += QStringLiteral( "(%1, %2)" ).arg( length() ).arg( precision() ); + else if ( length() > 0 ) + typeStr += QStringLiteral( "(%1)" ).arg( length() ); + + if ( showConstraints ) + { + typeStr += constraints().constraints() & QgsFieldConstraints::ConstraintNotNull + ? QStringLiteral( " NOT NULL" ) + : QStringLiteral( " NULL" ); + + typeStr += constraints().constraints() & QgsFieldConstraints::ConstraintUnique + ? QStringLiteral( " UNIQUE" ) + : QString(); + } + + return typeStr; +} + QVariant::Type QgsField::type() const { return d->type; diff --git a/src/core/qgsfield.h b/src/core/qgsfield.h index 5e7736b66849..8b01bcf18225 100644 --- a/src/core/qgsfield.h +++ b/src/core/qgsfield.h @@ -130,6 +130,17 @@ class CORE_EXPORT QgsField */ QString displayNameWithAlias() const; + + /** + * Returns the type to use when displaying this field, including the length and precision of the datatype if applicable. + * + * This will be used when the full datatype with details has to displayed to the user. + * + * \see type() + * \since QGIS 3.14 + */ + QString displayType( bool showConstraints = false ) const; + //! Gets variant type of the field as it will be retrieved from data source QVariant::Type type() const; diff --git a/src/core/qgsfieldformatterregistry.cpp b/src/core/qgsfieldformatterregistry.cpp index 0c595752f356..b3696a70f062 100644 --- a/src/core/qgsfieldformatterregistry.cpp +++ b/src/core/qgsfieldformatterregistry.cpp @@ -13,6 +13,7 @@ * (at your option) any later version. * * * ***************************************************************************/ + #include "qgsfieldformatterregistry.h" #include "qgsfieldformatter.h" diff --git a/src/core/qgsfieldformatterregistry.h b/src/core/qgsfieldformatterregistry.h index f89d3f65313c..0795bb72700a 100644 --- a/src/core/qgsfieldformatterregistry.h +++ b/src/core/qgsfieldformatterregistry.h @@ -24,6 +24,7 @@ #include "qgis_core.h" class QgsFieldFormatter; +class QgsVectorLayer; /** * \ingroup core diff --git a/src/core/qgsfieldmodel.cpp b/src/core/qgsfieldmodel.cpp index 551d17f78725..fb4f088af9e3 100644 --- a/src/core/qgsfieldmodel.cpp +++ b/src/core/qgsfieldmodel.cpp @@ -284,7 +284,7 @@ QVariant QgsFieldModel::data( const QModelIndex &index, int role ) const { if ( isEmpty || exprIdx >= 0 ) { - return ""; + return QString(); } QgsField field = mFields.at( index.row() - fieldOffset ); return field.name(); @@ -387,6 +387,12 @@ QVariant QgsFieldModel::data( const QModelIndex &index, int role ) const return QVariant(); } + case FieldIsWidgetEditable: + { + return !( mLayer->editFormConfig().readOnly( index.row() - fieldOffset ) ); + } + + case Qt::DisplayRole: case Qt::EditRole: case Qt::ToolTipRole: @@ -475,23 +481,37 @@ QString QgsFieldModel::fieldToolTip( const QgsField &field ) { toolTip = QStringLiteral( "%1" ).arg( field.name() ); } - QString typeString; - if ( field.length() > 0 ) + + toolTip += QStringLiteral( "
    %3" ).arg( field.displayType( true ) ); + + QString comment = field.comment(); + + if ( ! comment.isEmpty() ) { - if ( field.precision() > 0 ) - { - typeString = QStringLiteral( "%1 (%2, %3)" ).arg( field.typeName() ).arg( field.length() ).arg( field.precision() ); - } - else - { - typeString = QStringLiteral( "%1 (%2)" ).arg( field.typeName() ).arg( field.length() ); - } + toolTip += QStringLiteral( "
    %1" ).arg( comment ); } - else + + return toolTip; +} + +QString QgsFieldModel::fieldToolTipExtended( const QgsField &field, const QgsVectorLayer *layer ) +{ + QString toolTip = QgsFieldModel::fieldToolTip( field ); + const QgsFields fields = layer->fields(); + int fieldIdx = fields.indexOf( field.name() ); + + if ( fieldIdx < 0 ) + return QString(); + + QString expressionString = fields.fieldOrigin( fieldIdx ) == QgsFields::OriginExpression + ? layer->expressionField( fieldIdx ) + : QString(); + + if ( !expressionString.isEmpty() ) { - typeString = field.typeName(); + toolTip += QStringLiteral( "
    %3" ).arg( expressionString ); } - toolTip += QStringLiteral( "

    %1

    " ).arg( typeString ); + return toolTip; } diff --git a/src/core/qgsfieldmodel.h b/src/core/qgsfieldmodel.h index bee874bc19b3..1ee85061e6f4 100644 --- a/src/core/qgsfieldmodel.h +++ b/src/core/qgsfieldmodel.h @@ -29,7 +29,8 @@ class QgsVectorLayer; /** * \ingroup core - * \brief The QgsFieldModel class is a model to display the list of fields of a layer in widgets. + * \brief The QgsFieldModel class is a model to display the list of fields in widgets + * (optionally associated with a vector layer). * If allowed, expressions might be added to the end of the model. * It can be associated with a QgsMapLayerModel to dynamically display a layer and its fields. * \since QGIS 2.3 @@ -57,6 +58,7 @@ class CORE_EXPORT QgsFieldModel : public QAbstractItemModel IsEmptyRole = Qt::UserRole + 8, //!< Return if the index corresponds to the empty value EditorWidgetType = Qt::UserRole + 9, //!< Editor widget type JoinedFieldIsEditable = Qt::UserRole + 10, //!< TRUE if a joined field is editable (returns QVariant if not a joined field) + FieldIsWidgetEditable = Qt::UserRole + 11, //!< TRUE if a is editable from the widget }; /** @@ -137,6 +139,13 @@ class CORE_EXPORT QgsFieldModel : public QAbstractItemModel */ static QString fieldToolTip( const QgsField &field ); + /** + * Returns a HTML formatted tooltip string for a \a field, containing details + * like the field name, alias, type and expression. + * \since QGIS 3.14 + */ + static QString fieldToolTipExtended( const QgsField &field, const QgsVectorLayer *layer ); + /** * Manually sets the \a fields to use for the model. * diff --git a/src/core/qgsfieldproxymodel.cpp b/src/core/qgsfieldproxymodel.cpp index c9b9cba151ee..94f3ef7769bf 100644 --- a/src/core/qgsfieldproxymodel.cpp +++ b/src/core/qgsfieldproxymodel.cpp @@ -62,8 +62,18 @@ bool QgsFieldProxyModel::isReadOnly( const QModelIndex &index ) const case QgsFields::OriginEdit: case QgsFields::OriginProvider: - //not read only - return false; + { + if ( !sourceModel()->data( index, QgsFieldModel::FieldIsWidgetEditable ).toBool() ) + { + return true; + } + else + { + //not read only + return false; + } + } + } return false; // avoid warnings } diff --git a/src/core/qgsfilefiltergenerator.h b/src/core/qgsfilefiltergenerator.h new file mode 100644 index 000000000000..7c22593a5dd2 --- /dev/null +++ b/src/core/qgsfilefiltergenerator.h @@ -0,0 +1,48 @@ +/*************************************************************************** + qgsfilefiltergenerator.h + ------------------------ + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSFILEFILTERGENERATOR_H +#define QGSFILEFILTERGENERATOR_H + +#include "qgis_core.h" +#include + +/** + * \ingroup core + * Abstract interface for classes which generate a file filter string. + * + * This interface can be inherited by classes which can generate a file filter + * string, for use in file open or file save dialogs. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsFileFilterGenerator +{ + public: + virtual ~QgsFileFilterGenerator() = default; + + /** + * This method needs to be reimplemented in all classes which implement this interface + * and return a file filter. + */ + virtual QString createFileFilter() const = 0; + +}; + +#endif // QGSPROCESSINGUTILS_H + + diff --git a/src/core/qgslayerdefinition.cpp b/src/core/qgslayerdefinition.cpp index 94520bc98df0..82be65d6ff74 100644 --- a/src/core/qgslayerdefinition.cpp +++ b/src/core/qgslayerdefinition.cpp @@ -210,7 +210,8 @@ bool QgsLayerDefinition::exportLayerDefinition( QString path, const QListreadBoolEntry( QStringLiteral( "Paths" ), QStringLiteral( "/Absolute" ), false ); + context.setPathResolver( QgsPathResolver( writeAbsolutePath ? QString() : path ) ); QDomDocument doc( QStringLiteral( "qgis-layer-definition" ) ); if ( !exportLayerDefinition( doc, selectedTreeNodes, errorMessage, context ) ) diff --git a/src/core/qgslegendrenderer.cpp b/src/core/qgslegendrenderer.cpp index 053c0837f1c4..7b81d4be55d6 100644 --- a/src/core/qgslegendrenderer.cpp +++ b/src/core/qgslegendrenderer.cpp @@ -651,6 +651,9 @@ QgsLegendRenderer::LegendComponent QgsLegendRenderer::drawSymbolItemInternal( Qg ctx.columnRight = columnContext.right; ctx.maxSiblingSymbolWidth = maxSiblingSymbolWidth; + if ( const QgsSymbolLegendNode *symbolNode = dynamic_cast< const QgsSymbolLegendNode * >( symbolItem ) ) + ctx.patchShape = symbolNode->patchShape(); + QgsLayerTreeModelLegendNode::ItemMetrics im = symbolItem->draw( mSettings, context ? &ctx : ( painter ? &ctx : nullptr ) ); diff --git a/src/core/qgsmaplayer.cpp b/src/core/qgsmaplayer.cpp index 8d34cdf9933e..38fd32159604 100644 --- a/src/core/qgsmaplayer.cpp +++ b/src/core/qgsmaplayer.cpp @@ -1699,6 +1699,11 @@ void QgsMapLayer::setCustomProperties( const QgsObjectCustomProperties &properti mCustomProperties = properties; } +const QgsObjectCustomProperties &QgsMapLayer::customProperties() const +{ + return mCustomProperties; +} + QVariant QgsMapLayer::customProperty( const QString &value, const QVariant &defaultValue ) const { return mCustomProperties.value( value, defaultValue ); diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 1a81bb99ee02..b19cab3a817f 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -69,7 +69,8 @@ enum class QgsMapLayerType SIP_MONKEYPATCH_SCOPEENUM_UNNEST( QgsMapLayer, LayerT VectorLayer, RasterLayer, PluginLayer, - MeshLayer //!< Added in 3.2 + MeshLayer, //!< Added in 3.2 + VectorTileLayer //!< Added in 3.14 }; /** @@ -108,6 +109,9 @@ class CORE_EXPORT QgsMapLayer : public QObject case QgsMapLayerType::MeshLayer: sipType = sipType_QgsMeshLayer; break; + case QgsMapLayerType::VectorTileLayer: + sipType = sipType_QgsVectorTileLayer; + break; default: sipType = nullptr; break; @@ -614,6 +618,13 @@ class CORE_EXPORT QgsMapLayer : public QObject */ void setCustomProperties( const QgsObjectCustomProperties &properties ); + /** + * Read all custom properties from layer. Properties are stored in a map and saved in project file. + * \see setCustomProperties + * \since QGIS 3.14 + */ + const QgsObjectCustomProperties &customProperties() const; + /** * Remove a custom property from layer. Properties are stored in a map and saved in project file. * \see setCustomProperty() @@ -1266,7 +1277,7 @@ class CORE_EXPORT QgsMapLayer : public QObject #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider()->name() ); + QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider() ? sipCpp->dataProvider()->name() : QStringLiteral( "Invalid" ) ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); % End #endif diff --git a/src/core/qgsmaplayerlegend.cpp b/src/core/qgsmaplayerlegend.cpp index c039da7f23d6..7f35b3c48eb0 100644 --- a/src/core/qgsmaplayerlegend.cpp +++ b/src/core/qgsmaplayerlegend.cpp @@ -146,6 +146,27 @@ bool QgsMapLayerLegendUtils::hasLegendNodeUserLabel( QgsLayerTreeLayer *nodeLaye return nodeLayer->customProperties().contains( "legend/label-" + QString::number( originalIndex ) ); } +void QgsMapLayerLegendUtils::setLegendNodePatchShape( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsLegendPatchShape &shape ) +{ + QDomDocument patchDoc; + QDomElement patchElem = patchDoc.createElement( QStringLiteral( "patch" ) ); + shape.writeXml( patchElem, patchDoc, QgsReadWriteContext() ); + patchDoc.appendChild( patchElem ); + nodeLayer->setCustomProperty( "legend/patch-shape-" + QString::number( originalIndex ), patchDoc.toString() ); +} + +QgsLegendPatchShape QgsMapLayerLegendUtils::legendNodePatchShape( QgsLayerTreeLayer *nodeLayer, int originalIndex ) +{ + QString patchDef = nodeLayer->customProperty( "legend/patch-shape-" + QString::number( originalIndex ) ).toString(); + if ( patchDef.isEmpty() ) + return QgsLegendPatchShape(); + + QDomDocument doc( QStringLiteral( "patch" ) ); + doc.setContent( patchDef ); + QgsLegendPatchShape shape; + shape.readXml( doc.documentElement(), QgsReadWriteContext() ); + return shape; +} void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList &nodes ) { @@ -154,9 +175,17 @@ void QgsMapLayerLegendUtils::applyLayerNodeProperties( QgsLayerTreeLayer *nodeLa const auto constNodes = nodes; for ( QgsLayerTreeModelLegendNode *legendNode : constNodes ) { - QString userLabel = QgsMapLayerLegendUtils::legendNodeUserLabel( nodeLayer, i++ ); + QString userLabel = QgsMapLayerLegendUtils::legendNodeUserLabel( nodeLayer, i ); if ( !userLabel.isNull() ) legendNode->setUserLabel( userLabel ); + + if ( QgsSymbolLegendNode *symbolNode = dynamic_cast< QgsSymbolLegendNode * >( legendNode ) ) + { + const QgsLegendPatchShape shape = QgsMapLayerLegendUtils::legendNodePatchShape( nodeLayer, i ); + symbolNode->setPatchShape( shape ); + } + + i++; } // handle user order of nodes @@ -361,16 +390,16 @@ QList QgsDefaultMeshLayerLegend::createLayerTreeM QgsMeshRendererSettings rendererSettings = mLayer->rendererSettings(); - QgsMeshDatasetIndex indexScalar = rendererSettings.activeScalarDataset(); - QgsMeshDatasetIndex indexVector = rendererSettings.activeVectorDataset(); + int indexScalar = rendererSettings.activeScalarDatasetGroup(); + int indexVector = rendererSettings.activeVectorDatasetGroup(); QString name; - if ( indexScalar.isValid() && indexVector.isValid() && indexScalar.group() != indexVector.group() ) - name = QString( "%1 / %2" ).arg( provider->datasetGroupMetadata( indexScalar.group() ).name(), provider->datasetGroupMetadata( indexVector.group() ).name() ); - else if ( indexScalar.isValid() ) - name = provider->datasetGroupMetadata( indexScalar.group() ).name(); - else if ( indexVector.isValid() ) - name = provider->datasetGroupMetadata( indexVector.group() ).name(); + if ( indexScalar > -1 && indexVector > -1 && indexScalar != indexVector ) + name = QString( "%1 / %2" ).arg( provider->datasetGroupMetadata( indexScalar ).name(), provider->datasetGroupMetadata( indexVector ).name() ); + else if ( indexScalar > -1 ) + name = provider->datasetGroupMetadata( indexScalar ).name(); + else if ( indexVector > -1 ) + name = provider->datasetGroupMetadata( indexVector ).name(); else { // neither contours nor vectors get rendered - no legend needed @@ -379,9 +408,9 @@ QList QgsDefaultMeshLayerLegend::createLayerTreeM nodes << new QgsSimpleLegendNode( nodeLayer, name ); - if ( indexScalar.isValid() ) + if ( indexScalar > -1 ) { - QgsMeshRendererScalarSettings settings = rendererSettings.scalarSettings( indexScalar.group() ); + QgsMeshRendererScalarSettings settings = rendererSettings.scalarSettings( indexScalar ); QgsLegendColorList items; settings.colorRampShader().legendSymbologyItems( items ); for ( const QPair< QString, QColor > &item : qgis::as_const( items ) ) diff --git a/src/core/qgsmaplayerlegend.h b/src/core/qgsmaplayerlegend.h index e3baf01bf88a..99741787b605 100644 --- a/src/core/qgsmaplayerlegend.h +++ b/src/core/qgsmaplayerlegend.h @@ -29,6 +29,7 @@ class QgsPluginLayer; class QgsRasterLayer; class QgsReadWriteContext; class QgsVectorLayer; +class QgsLegendPatchShape; #include "qgis_core.h" @@ -102,6 +103,22 @@ class CORE_EXPORT QgsMapLayerLegendUtils static QString legendNodeUserLabel( QgsLayerTreeLayer *nodeLayer, int originalIndex ); static bool hasLegendNodeUserLabel( QgsLayerTreeLayer *nodeLayer, int originalIndex ); + /** + * Sets the legend patch \a shape for the legend node belonging to \a nodeLayer at the specified \a originalIndex. + * + * \see legendNodePatchShape() + * \since QGIS 3.14 + */ + static void setLegendNodePatchShape( QgsLayerTreeLayer *nodeLayer, int originalIndex, const QgsLegendPatchShape &shape ); + + /** + * Returns the legend patch shape for the legend node belonging to \a nodeLayer at the specified \a originalIndex. + * + * \see setLegendNodePatchShape() + * \since QGIS 3.14 + */ + static QgsLegendPatchShape legendNodePatchShape( QgsLayerTreeLayer *nodeLayer, int originalIndex ); + //! update according to layer node's custom properties (order of items, user labels for items) static void applyLayerNodeProperties( QgsLayerTreeLayer *nodeLayer, QList &nodes ); }; diff --git a/src/core/qgsmaplayermodel.cpp b/src/core/qgsmaplayermodel.cpp index a4e2c5a78f07..54ff8e27bf20 100644 --- a/src/core/qgsmaplayermodel.cpp +++ b/src/core/qgsmaplayermodel.cpp @@ -225,6 +225,7 @@ QVariant QgsMapLayerModel::data( const QModelIndex &index, int role ) const switch ( role ) { case Qt::DisplayRole: + case Qt::EditRole: { if ( index.row() == 0 && mAllowEmpty ) return QVariant(); @@ -236,7 +237,7 @@ QVariant QgsMapLayerModel::data( const QModelIndex &index, int role ) const if ( !layer ) return QVariant(); - if ( !mShowCrs || !layer->isSpatial() ) + if ( !mShowCrs || !layer->isSpatial() || role == Qt::EditRole ) { return layer->name(); } @@ -367,6 +368,11 @@ QIcon QgsMapLayerModel::iconForLayer( QgsMapLayer *layer ) return QgsLayerItem::iconMesh(); } + case QgsMapLayerType::VectorTileLayer: + { + return QgsLayerItem::iconVectorTile(); + } + case QgsMapLayerType::VectorLayer: { QgsVectorLayer *vl = qobject_cast( layer ); diff --git a/src/core/qgsmaplayerproxymodel.cpp b/src/core/qgsmaplayerproxymodel.cpp index 4dd20727fa97..485f61695d9f 100644 --- a/src/core/qgsmaplayerproxymodel.cpp +++ b/src/core/qgsmaplayerproxymodel.cpp @@ -116,6 +116,7 @@ bool QgsMapLayerProxyModel::acceptsLayer( QgsMapLayer *layer ) const if ( ( mFilters.testFlag( RasterLayer ) && layer->type() == QgsMapLayerType::RasterLayer ) || ( mFilters.testFlag( VectorLayer ) && layer->type() == QgsMapLayerType::VectorLayer ) || ( mFilters.testFlag( MeshLayer ) && layer->type() == QgsMapLayerType::MeshLayer ) || + ( mFilters.testFlag( VectorTileLayer ) && layer->type() == QgsMapLayerType::VectorTileLayer ) || ( mFilters.testFlag( PluginLayer ) && layer->type() == QgsMapLayerType::PluginLayer ) ) return true; diff --git a/src/core/qgsmaplayerproxymodel.h b/src/core/qgsmaplayerproxymodel.h index 2383bd9a94f0..8261dc911f02 100644 --- a/src/core/qgsmaplayerproxymodel.h +++ b/src/core/qgsmaplayerproxymodel.h @@ -51,7 +51,8 @@ class CORE_EXPORT QgsMapLayerProxyModel : public QSortFilterProxyModel PluginLayer = 32, WritableLayer = 64, MeshLayer = 128, //!< QgsMeshLayer \since QGIS 3.6 - All = RasterLayer | VectorLayer | PluginLayer | MeshLayer + VectorTileLayer = 256, //!< QgsVectorTileLayer \since QGIS 3.14 + All = RasterLayer | VectorLayer | PluginLayer | MeshLayer | VectorTileLayer }; Q_DECLARE_FLAGS( Filters, Filter ) Q_FLAG( Filters ) diff --git a/src/core/qgsmaplayertemporalproperties.cpp b/src/core/qgsmaplayertemporalproperties.cpp index 79daa6836b9d..158deaae1f6f 100644 --- a/src/core/qgsmaplayertemporalproperties.cpp +++ b/src/core/qgsmaplayertemporalproperties.cpp @@ -22,21 +22,7 @@ QgsMapLayerTemporalProperties::QgsMapLayerTemporalProperties( QObject *parent, b { } -void QgsMapLayerTemporalProperties::setTemporalSource( QgsMapLayerTemporalProperties::TemporalSource source ) -{ - if ( mSource != source ) - { - mSource = source; - emit changed(); - } -} - bool QgsMapLayerTemporalProperties::isVisibleInTemporalRange( const QgsDateTimeRange & ) const { return true; } - -QgsMapLayerTemporalProperties::TemporalSource QgsMapLayerTemporalProperties::temporalSource() const -{ - return mSource; -} diff --git a/src/core/qgsmaplayertemporalproperties.h b/src/core/qgsmaplayertemporalproperties.h index 63f67c0781d7..bd405a13de90 100644 --- a/src/core/qgsmaplayertemporalproperties.h +++ b/src/core/qgsmaplayertemporalproperties.h @@ -49,6 +49,10 @@ class CORE_EXPORT QgsMapLayerTemporalProperties : public QgsTemporalProperty { sipType = sipType_QgsRasterLayerTemporalProperties; } + else if ( qobject_cast( sipCpp ) ) + { + sipType = sipType_QgsMeshLayerTemporalProperties; + } else { sipType = 0; @@ -79,29 +83,6 @@ class CORE_EXPORT QgsMapLayerTemporalProperties : public QgsTemporalProperty */ virtual bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) = 0; - /** - * Source of the temporal range of these properties. - */ - enum TemporalSource - { - Layer = 0, //!< Layer's temporal range has been manually defined - Project = 1 //!< Layer should inherit its temporal range from the project's time settings - }; - - /** - * Returns the temporal properties temporal range source, can be layer or project. - * - *\see setTemporalSource() - **/ - TemporalSource temporalSource() const; - - /** - * Sets the temporal properties temporal range \a source. - * - *\see temporalSource() - **/ - void setTemporalSource( TemporalSource source ); - /** * Returns TRUE if the layer should be visible and rendered for the specified time \a range. */ @@ -113,11 +94,6 @@ class CORE_EXPORT QgsMapLayerTemporalProperties : public QgsTemporalProperty */ virtual void setDefaultsFromDataProviderTemporalCapabilities( const QgsDataProviderTemporalCapabilities *capabilities ) = 0; - private: - - //! Source of the properties temporal range - TemporalSource mSource = Layer; - }; #endif // QGSMAPLAYERTEMPORALPROPERTIES_H diff --git a/src/core/qgsmaprendererjob.cpp b/src/core/qgsmaprendererjob.cpp index 68c951a3beed..22aee6907d90 100644 --- a/src/core/qgsmaprendererjob.cpp +++ b/src/core/qgsmaprendererjob.cpp @@ -126,6 +126,11 @@ bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const Qgs { bool split = false; + // we can safely use ballpark transforms without bothering the user here -- at the likely scale of layer extents there + // won't be an appreciable difference, and we aren't actually transforming any rendered points here anyway (just the layer extent) + QgsCoordinateTransform approxTransform = ct; + approxTransform.setBallparkTransformsAreAppropriate( true ); + try { #ifdef QGISDEBUG @@ -140,13 +145,13 @@ bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const Qgs if ( ml->crs().isGeographic() ) { - if ( ml->type() == QgsMapLayerType::VectorLayer && !ct.destinationCrs().isGeographic() ) + if ( ml->type() == QgsMapLayerType::VectorLayer && !approxTransform.destinationCrs().isGeographic() ) { // if we transform from a projected coordinate system check // check if transforming back roughly returns the input // extend - otherwise render the world. - QgsRectangle extent1 = ct.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform ); - QgsRectangle extent2 = ct.transformBoundingBox( extent1, QgsCoordinateTransform::ForwardTransform ); + QgsRectangle extent1 = approxTransform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform ); + QgsRectangle extent2 = approxTransform.transformBoundingBox( extent1, QgsCoordinateTransform::ForwardTransform ); QgsDebugMsgLevel( QStringLiteral( "\n0:%1 %2x%3\n1:%4\n2:%5 %6x%7 (w:%8 h:%9)" ) .arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ) @@ -172,16 +177,16 @@ bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const Qgs else { // Note: ll = lower left point - QgsPointXY ll = ct.transform( extent.xMinimum(), extent.yMinimum(), - QgsCoordinateTransform::ReverseTransform ); + QgsPointXY ll = approxTransform.transform( extent.xMinimum(), extent.yMinimum(), + QgsCoordinateTransform::ReverseTransform ); // and ur = upper right point - QgsPointXY ur = ct.transform( extent.xMaximum(), extent.yMaximum(), - QgsCoordinateTransform::ReverseTransform ); + QgsPointXY ur = approxTransform.transform( extent.xMaximum(), extent.yMaximum(), + QgsCoordinateTransform::ReverseTransform ); QgsDebugMsgLevel( QStringLiteral( "in:%1 (ll:%2 ur:%3)" ).arg( extent.toString(), ll.toString(), ur.toString() ), 4 ); - extent = ct.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform ); + extent = approxTransform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform ); QgsDebugMsgLevel( QStringLiteral( "out:%1 (w:%2 h:%3)" ).arg( extent.toString() ).arg( extent.width() ).arg( extent.height() ), 4 ); @@ -205,7 +210,7 @@ bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const Qgs } else // can't cross 180 { - if ( ct.destinationCrs().isGeographic() && + if ( approxTransform.destinationCrs().isGeographic() && ( extent.xMinimum() <= -180 || extent.xMaximum() >= 180 || extent.yMinimum() <= -90 || extent.yMaximum() >= 90 ) ) // Use unlimited rectangle because otherwise we may end up transforming wrong coordinates. @@ -214,7 +219,7 @@ bool QgsMapRendererJob::reprojectToLayerExtent( const QgsMapLayer *ml, const Qgs // but this seems like a safer choice. extent = QgsRectangle( std::numeric_limits::lowest(), std::numeric_limits::lowest(), std::numeric_limits::max(), std::numeric_limits::max() ); else - extent = ct.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform ); + extent = approxTransform.transformBoundingBox( extent, QgsCoordinateTransform::ReverseTransform ); } } catch ( QgsCsException &cse ) @@ -906,6 +911,7 @@ bool QgsMapRendererJob::needTemporaryImage( QgsMapLayer *ml ) } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/core/qgsmapsettings.cpp b/src/core/qgsmapsettings.cpp index 873921dd8610..bb36fac7a624 100644 --- a/src/core/qgsmapsettings.cpp +++ b/src/core/qgsmapsettings.cpp @@ -459,6 +459,7 @@ QgsRectangle QgsMapSettings::outputExtentToLayerExtent( const QgsMapLayer *layer try { QgsCoordinateTransform ct = layerTransform( layer ); + ct.setBallparkTransformsAreAppropriate( true ); if ( ct.isValid() ) { QgsDebugMsgLevel( QStringLiteral( "sourceCrs = %1" ).arg( ct.sourceCrs().authid() ), 3 ); diff --git a/src/providers/wms/qgsmbtilesreader.cpp b/src/core/qgsmbtilesreader.cpp similarity index 100% rename from src/providers/wms/qgsmbtilesreader.cpp rename to src/core/qgsmbtilesreader.cpp diff --git a/src/providers/wms/qgsmbtilesreader.h b/src/core/qgsmbtilesreader.h similarity index 69% rename from src/providers/wms/qgsmbtilesreader.h rename to src/core/qgsmbtilesreader.h index 279e89a6e3e9..a08ded825d9c 100644 --- a/src/providers/wms/qgsmbtilesreader.h +++ b/src/core/qgsmbtilesreader.h @@ -16,29 +16,44 @@ #ifndef QGSMBTILESREADER_H #define QGSMBTILESREADER_H +#include "qgis_core.h" #include "sqlite3.h" #include "qgssqliteutils.h" +#define SIP_NO_FILE + class QImage; class QgsRectangle; -class QgsMBTilesReader +/** + * \ingroup core + * Utility class for reading MBTiles files (which are SQLite3 databases). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsMBTilesReader { public: + //! Contructs MBTiles reader (but it does not open the file yet) explicit QgsMBTilesReader( const QString &filename ); + //! Tries to open the file, returns true on success bool open(); + //! Returns whether the MBTiles file is currently opened bool isOpen() const; + //! Requests metadata value for the given key QString metadataValue( const QString &key ); - //! given in WGS 84 (if available) + //! Returns bounding box from metadata, given in WGS 84 (if available) QgsRectangle extent(); + //! Returns raw tile data for given tile QByteArray tileData( int z, int x, int y ); + //! Returns tile decoded as a raster image (if stored in a known format like JPG or PNG) QImage tileDataAsImage( int z, int x, int y ); private: diff --git a/src/core/qgsmimedatautils.cpp b/src/core/qgsmimedatautils.cpp index 58fea21faebd..08fba7866d2f 100644 --- a/src/core/qgsmimedatautils.cpp +++ b/src/core/qgsmimedatautils.cpp @@ -91,6 +91,12 @@ QgsMimeDataUtils::Uri::Uri( QgsMapLayer *layer ) break; } + case QgsMapLayerType::VectorTileLayer: + { + layerType = QStringLiteral( "vector-tile" ); + break; + } + case QgsMapLayerType::PluginLayer: { // plugin layers do not have a standard way of storing their URI... diff --git a/src/core/qgsnetworkcontentfetchertask.cpp b/src/core/qgsnetworkcontentfetchertask.cpp index 60eaed050cfa..a8c2fbfdac8c 100644 --- a/src/core/qgsnetworkcontentfetchertask.cpp +++ b/src/core/qgsnetworkcontentfetchertask.cpp @@ -19,13 +19,13 @@ #include "qgsnetworkcontentfetchertask.h" #include "qgsnetworkcontentfetcher.h" -QgsNetworkContentFetcherTask::QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg ) - : QgsNetworkContentFetcherTask( QNetworkRequest( url ), authcfg ) +QgsNetworkContentFetcherTask::QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg, QgsTask::Flags flags ) + : QgsNetworkContentFetcherTask( QNetworkRequest( url ), authcfg, flags ) { } -QgsNetworkContentFetcherTask::QgsNetworkContentFetcherTask( const QNetworkRequest &request, const QString &authcfg ) - : QgsTask( tr( "Fetching %1" ).arg( request.url().toString() ) ) +QgsNetworkContentFetcherTask::QgsNetworkContentFetcherTask( const QNetworkRequest &request, const QString &authcfg, QgsTask::Flags flags ) + : QgsTask( tr( "Fetching %1" ).arg( request.url().toString() ), flags ) , mRequest( request ) , mAuthcfg( authcfg ) { diff --git a/src/core/qgsnetworkcontentfetchertask.h b/src/core/qgsnetworkcontentfetchertask.h index 907441d6ea9d..cfdcd481fe1a 100644 --- a/src/core/qgsnetworkcontentfetchertask.h +++ b/src/core/qgsnetworkcontentfetchertask.h @@ -56,7 +56,7 @@ class CORE_EXPORT QgsNetworkContentFetcherTask : public QgsTask * * Optionally, authentication configuration can be set via the \a authcfg argument. */ - QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg = QString() ); + QgsNetworkContentFetcherTask( const QUrl &url, const QString &authcfg = QString(), QgsTask::Flags flags = QgsTask::CanCancel ); /** * Constructor for a QgsNetworkContentFetcherTask which fetches @@ -64,7 +64,7 @@ class CORE_EXPORT QgsNetworkContentFetcherTask : public QgsTask * * Optionally, authentication configuration can be set via the \a authcfg argument. */ - QgsNetworkContentFetcherTask( const QNetworkRequest &request, const QString &authcfg = QString() ); + QgsNetworkContentFetcherTask( const QNetworkRequest &request, const QString &authcfg = QString(), QgsTask::Flags flags = QgsTask::CanCancel ); ~QgsNetworkContentFetcherTask() override; diff --git a/src/core/qgsnewsfeedparser.cpp b/src/core/qgsnewsfeedparser.cpp index c1b6fa302297..0befccc8c862 100644 --- a/src/core/qgsnewsfeedparser.cpp +++ b/src/core/qgsnewsfeedparser.cpp @@ -140,7 +140,8 @@ void QgsNewsFeedParser::fetch() mFetchStartTime = QDateTime::currentDateTimeUtc().toSecsSinceEpoch(); - QgsNetworkContentFetcherTask *task = new QgsNetworkContentFetcherTask( req, mAuthCfg ); + // allow canceling the news fetching without prompts -- it's not crucial if this gets finished or not + QgsNetworkContentFetcherTask *task = new QgsNetworkContentFetcherTask( req, mAuthCfg, QgsTask::CanCancel | QgsTask::CancelWithoutPrompt ); task->setDescription( tr( "Fetching News Feed" ) ); connect( task, &QgsNetworkContentFetcherTask::fetched, this, [this, task] { diff --git a/src/core/qgsofflineediting.cpp b/src/core/qgsofflineediting.cpp index 74065deaff14..396394f17959 100644 --- a/src/core/qgsofflineediting.cpp +++ b/src/core/qgsofflineediting.cpp @@ -38,6 +38,8 @@ #include "qgsogrutils.h" #include "qgsvectorfilewriter.h" #include "qgsvectorlayer.h" +#include "qgsproviderregistry.h" +#include "qgsprovidermetadata.h" #include #include @@ -135,7 +137,7 @@ bool QgsOfflineEditing::convertToOfflineProject( const QString &offlineDataPath, QgsSnappingConfig snappingConfig = QgsProject::instance()->snappingConfig(); - // copy selected vector layers to SpatiaLite + // copy selected vector layers to offline layer for ( int i = 0; i < layerIds.count(); i++ ) { emit layerProgressUpdated( i + 1, layerIds.count() ); @@ -162,7 +164,7 @@ bool QgsOfflineEditing::convertToOfflineProject( const QString &offlineDataPath, QgsProject::instance()->setSnappingConfig( snappingConfig ); - // restore join info on new SpatiaLite layer + // restore join info on new offline layer QMap::ConstIterator it; for ( it = joinInfoBuffer.constBegin(); it != joinInfoBuffer.constEnd(); ++it ) { @@ -848,6 +850,9 @@ QgsVectorLayer *QgsOfflineEditing::copyVectorLayer( QgsVectorLayer *layer, sqlit showWarning( newLayer->commitErrors().join( QStringLiteral( "\n" ) ) ); } + // copy the custom properties from original layer + newLayer->setCustomProperties( layer->customProperties() ); + // mark as offline layer newLayer->setCustomProperty( CUSTOM_PROPERTY_IS_OFFLINE_EDITABLE, true ); @@ -1453,9 +1458,8 @@ void QgsOfflineEditing::committedAttributesAdded( const QString &qgisLayerId, co int layerId = getOrCreateLayerId( database.get(), qgisLayerId ); int commitNo = getCommitNo( database.get() ); - for ( QList::const_iterator it = addedAttributes.begin(); it != addedAttributes.end(); ++it ) + for ( const QgsField &field : addedAttributes ) { - QgsField field = *it; QString sql = QStringLiteral( "INSERT INTO 'log_added_attrs' VALUES ( %1, %2, '%3', %4, %5, %6, '%7' )" ) .arg( layerId ) .arg( commitNo ) @@ -1481,7 +1485,8 @@ void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, cons // get new feature ids from db QgsMapLayer *layer = QgsProject::instance()->mapLayer( qgisLayerId ); - QgsDataSourceUri uri = QgsDataSourceUri( layer->source() ); + QString dataSourceString = layer->source(); + QgsDataSourceUri uri = QgsDataSourceUri( dataSourceString ); QString offlinePath = QgsProject::instance()->readPath( QgsProject::instance()->readEntry( PROJECT_ENTRY_SCOPE_OFFLINE, PROJECT_ENTRY_KEY_OFFLINE_DB_PATH ) ); QString tableName; @@ -1492,7 +1497,13 @@ void QgsOfflineEditing::committedFeaturesAdded( const QString &qgisLayerId, cons } else { - tableName = uri.param( offlinePath + "|layername" ); + QgsProviderMetadata *ogrProviderMetaData = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) ); + QVariantMap decodedUri = ogrProviderMetaData->decodeUri( dataSourceString ); + tableName = decodedUri.value( QStringLiteral( "layerName" ) ).toString(); + if ( tableName.isEmpty() ) + { + showWarning( tr( "Could not deduce table name from data source %1." ).arg( dataSourceString ) ); + } } // only store feature ids @@ -1516,19 +1527,19 @@ void QgsOfflineEditing::committedFeaturesRemoved( const QString &qgisLayerId, co // insert log int layerId = getOrCreateLayerId( database.get(), qgisLayerId ); - for ( QgsFeatureIds::const_iterator it = deletedFeatureIds.begin(); it != deletedFeatureIds.end(); ++it ) + for ( QgsFeatureId id : deletedFeatureIds ) { - if ( isAddedFeature( database.get(), layerId, *it ) ) + if ( isAddedFeature( database.get(), layerId, id ) ) { // remove from added features log - QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( *it ); + QString sql = QStringLiteral( "DELETE FROM 'log_added_features' WHERE \"layer_id\" = %1 AND \"fid\" = %2" ).arg( layerId ).arg( id ); sqlExec( database.get(), sql ); } else { QString sql = QStringLiteral( "INSERT INTO 'log_removed_features' VALUES ( %1, %2)" ) .arg( layerId ) - .arg( *it ); + .arg( id ); sqlExec( database.get(), sql ); } } diff --git a/src/core/qgsopenclutils.cpp b/src/core/qgsopenclutils.cpp index 003fdf0fc266..fb3bfacc5390 100644 --- a/src/core/qgsopenclutils.cpp +++ b/src/core/qgsopenclutils.cpp @@ -29,9 +29,9 @@ #include #endif -QLatin1String QgsOpenClUtils::SETTINGS_GLOBAL_ENABLED_KEY = QLatin1Literal( "OpenClEnabled" ); -QLatin1String QgsOpenClUtils::SETTINGS_DEFAULT_DEVICE_KEY = QLatin1Literal( "OpenClDefaultDevice" ); -QLatin1String QgsOpenClUtils::LOGMESSAGE_TAG = QLatin1Literal( "OpenCL" ); +QLatin1String QgsOpenClUtils::SETTINGS_GLOBAL_ENABLED_KEY = QLatin1String( "OpenClEnabled" ); +QLatin1String QgsOpenClUtils::SETTINGS_DEFAULT_DEVICE_KEY = QLatin1String( "OpenClDefaultDevice" ); +QLatin1String QgsOpenClUtils::LOGMESSAGE_TAG = QLatin1String( "OpenCL" ); bool QgsOpenClUtils::sAvailable = false; Q_GLOBAL_STATIC( QString, sSourcePath ) diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index 5682f13caff9..390dbe68ba06 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -60,6 +60,7 @@ #include "qgsprojectviewsettings.h" #include "qgsprojectdisplaysettings.h" #include "qgsprojecttimesettings.h" +#include "qgsvectortilelayer.h" #include #include @@ -385,18 +386,23 @@ QgsProject::QgsProject( QObject *parent ) // proxy map layer store signals to this connect( mLayerStore.get(), qgis::overload::of( &QgsMapLayerStore::layersWillBeRemoved ), - this, qgis::overload< const QStringList &>::of( &QgsProject::layersWillBeRemoved ) ); + this, [ = ]( const QStringList & layers ) { mProjectScope.reset(); emit layersWillBeRemoved( layers ); } ); connect( mLayerStore.get(), qgis::overload< const QList & >::of( &QgsMapLayerStore::layersWillBeRemoved ), - this, qgis::overload< const QList & >::of( &QgsProject::layersWillBeRemoved ) ); + this, [ = ]( const QList &layers ) { mProjectScope.reset(); emit layersWillBeRemoved( layers ); } ); connect( mLayerStore.get(), qgis::overload< const QString & >::of( &QgsMapLayerStore::layerWillBeRemoved ), - this, qgis::overload< const QString & >::of( &QgsProject::layerWillBeRemoved ) ); + this, [ = ]( const QString & layer ) { mProjectScope.reset(); emit layerWillBeRemoved( layer ); } ); connect( mLayerStore.get(), qgis::overload< QgsMapLayer * >::of( &QgsMapLayerStore::layerWillBeRemoved ), - this, qgis::overload< QgsMapLayer * >::of( &QgsProject::layerWillBeRemoved ) ); - connect( mLayerStore.get(), qgis::overload::of( &QgsMapLayerStore::layersRemoved ), this, &QgsProject::layersRemoved ); - connect( mLayerStore.get(), &QgsMapLayerStore::layerRemoved, this, &QgsProject::layerRemoved ); - connect( mLayerStore.get(), &QgsMapLayerStore::allLayersRemoved, this, &QgsProject::removeAll ); - connect( mLayerStore.get(), &QgsMapLayerStore::layersAdded, this, &QgsProject::layersAdded ); - connect( mLayerStore.get(), &QgsMapLayerStore::layerWasAdded, this, &QgsProject::layerWasAdded ); + this, [ = ]( QgsMapLayer * layer ) { mProjectScope.reset(); emit layerWillBeRemoved( layer ); } ); + connect( mLayerStore.get(), qgis::overload::of( &QgsMapLayerStore::layersRemoved ), this, + [ = ]( const QStringList & layers ) { mProjectScope.reset(); emit layersRemoved( layers ); } ); + connect( mLayerStore.get(), &QgsMapLayerStore::layerRemoved, this, + [ = ]( const QString & layer ) { mProjectScope.reset(); emit layerRemoved( layer ); } ); + connect( mLayerStore.get(), &QgsMapLayerStore::allLayersRemoved, this, + [ = ]() { mProjectScope.reset(); emit removeAll(); } ); + connect( mLayerStore.get(), &QgsMapLayerStore::layersAdded, this, + [ = ]( const QList< QgsMapLayer * > &layers ) { mProjectScope.reset(); emit layersAdded( layers ); } ); + connect( mLayerStore.get(), &QgsMapLayerStore::layerWasAdded, this, + [ = ]( QgsMapLayer * layer ) { mProjectScope.reset(); emit layerWasAdded( layer ); } ); if ( QgsApplication::instance() ) { @@ -768,6 +774,7 @@ void QgsProject::clear() mDisplaySettings->reset(); mSnappingConfig.reset(); emit snappingConfigChanged( mSnappingConfig ); + emit avoidIntersectionsModeChanged(); emit topologicalEditingChanged(); mMapThemeCollection.reset( new QgsMapThemeCollection( this ) ); @@ -889,7 +896,7 @@ static void _getTitle( const QDomDocument &doc, QString &title ) if ( !nl.count() ) { - QgsDebugMsg( QStringLiteral( "unable to find title element" ) ); + QgsDebugMsgLevel( QStringLiteral( "unable to find title element" ), 2 ); return; } @@ -897,7 +904,7 @@ static void _getTitle( const QDomDocument &doc, QString &title ) if ( !titleNode.hasChildNodes() ) // if not, then there's no actual text { - QgsDebugMsg( QStringLiteral( "unable to find title element" ) ); + QgsDebugMsgLevel( QStringLiteral( "unable to find title element" ), 2 ); return; } @@ -905,7 +912,7 @@ static void _getTitle( const QDomDocument &doc, QString &title ) if ( !titleTextNode.isText() ) { - QgsDebugMsg( QStringLiteral( "unable to find title element" ) ); + QgsDebugMsgLevel( QStringLiteral( "unable to find title element" ), 2 ); return; } @@ -966,6 +973,15 @@ void QgsProject::setSnappingConfig( const QgsSnappingConfig &snappingConfig ) emit snappingConfigChanged( mSnappingConfig ); } +void QgsProject::setAvoidIntersectionsMode( const AvoidIntersectionsMode mode ) +{ + if ( mAvoidIntersectionsMode == mode ) + return; + + mAvoidIntersectionsMode = mode; + emit avoidIntersectionsModeChanged(); +} + bool QgsProject::_getMapLayers( const QDomDocument &doc, QList &brokenNodes, QgsProject::ReadFlags flags ) { // Layer order is set by the restoring the legend settings from project file. @@ -1057,6 +1073,10 @@ bool QgsProject::addLayer( const QDomElement &layerElem, QList &broken { mapLayer = qgis::make_unique(); } + else if ( type == QLatin1String( "vector-tile" ) ) + { + mapLayer = qgis::make_unique(); + } else if ( type == QLatin1String( "plugin" ) ) { QString typeName = layerElem.attribute( QStringLiteral( "name" ) ); @@ -1519,6 +1539,7 @@ bool QgsProject::readProjectFile( const QString &filename, QgsProject::ReadFlags } mSnappingConfig.readProject( *doc ); + mAvoidIntersectionsMode = static_cast( readNumEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/AvoidIntersectionsMode" ), static_cast( AvoidIntersectionsMode::AvoidIntersectionsLayers ) ) ); // restore older project scales settings mViewSettings->setUseProjectScales( readBoolEntry( QStringLiteral( "Scales" ), QStringLiteral( "/useProjectScales" ) ) ); @@ -1559,6 +1580,7 @@ bool QgsProject::readProjectFile( const QString &filename, QgsProject::ReadFlags emit readProject( *doc ); emit readProjectWithContext( *doc, context ); emit snappingConfigChanged( mSnappingConfig ); + emit avoidIntersectionsModeChanged(); emit topologicalEditingChanged(); emit projectColorsChanged(); @@ -1684,6 +1706,7 @@ const QgsLabelingEngineSettings &QgsProject::labelingEngineSettings() const QgsMapLayerStore *QgsProject::layerStore() { + mProjectScope.reset(); return mLayerStore.get(); } @@ -1763,8 +1786,14 @@ QgsExpressionContextScope *QgsProject::createExpressionContextScope() const QgsCoordinateReferenceSystem projectCrs = crs(); mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs" ), projectCrs.authid(), true, true ) ); mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_definition" ), projectCrs.toProj(), true, true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_description" ), projectCrs.description(), true, true ) ); mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_ellipsoid" ), ellipsoid(), true, true ) ); mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "_project_transform_context" ), QVariant::fromValue( transformContext() ), true, true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_units" ), QgsUnitTypes::toString( projectCrs.mapUnits() ), true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_acronym" ), projectCrs.projectionAcronym(), true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_ellipsoid" ), projectCrs.ellipsoidAcronym(), true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_proj4" ), projectCrs.toProj(), true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_crs_wkt" ), projectCrs.toWkt( QgsCoordinateReferenceSystem::WKT2_2018 ), true ) ); // metadata mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_author" ), metadata().author(), true, true ) ); @@ -1781,6 +1810,20 @@ QgsExpressionContextScope *QgsProject::createExpressionContextScope() const } mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "project_keywords" ), keywords, true, true ) ); + // layers + QVariantList layersIds; + QVariantList layers; + const QMap layersInProject = mLayerStore->mapLayers(); + layersIds.reserve( layersInProject.count() ); + layers.reserve( layersInProject.count() ); + for ( auto it = layersInProject.constBegin(); it != layersInProject.constEnd(); ++it ) + { + layersIds << it.value()->id(); + layers << QVariant::fromValue( QgsWeakMapLayerPointer( it.value() ) ); + } + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layer_ids" ), layersIds, true ) ); + mProjectScope->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "layers" ), layers, true ) ); + mProjectScope->addFunction( QStringLiteral( "project_color" ), new GetNamedProjectColor( this ) ); return createExpressionContextScope(); @@ -2054,6 +2097,7 @@ bool QgsProject::writeProjectFile( const QString &filename ) delete clonedRoot; mSnappingConfig.writeProject( *doc ); + writeEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/AvoidIntersectionsMode" ), static_cast( mAvoidIntersectionsMode ) ); // let map canvas and legend write their information emit writeProject( *doc ); @@ -2815,6 +2859,8 @@ QString QgsProject::homePath() const if ( !mCachedHomePath.isEmpty() ) return mCachedHomePath; + QFileInfo pfi( fileName() ); + if ( !mHomePath.isEmpty() ) { QFileInfo homeInfo( mHomePath ); @@ -2824,8 +2870,13 @@ QString QgsProject::homePath() const return mHomePath; } } + else if ( !fileName().isEmpty() ) + { + mCachedHomePath = pfi.path(); + + return mCachedHomePath; + } - QFileInfo pfi( fileName() ); if ( !pfi.exists() ) { mCachedHomePath = mHomePath; @@ -3174,6 +3225,8 @@ QList QgsProject::addMapLayers( } } + mProjectScope.reset(); + return myResultList; } @@ -3189,31 +3242,37 @@ QgsProject::addMapLayer( QgsMapLayer *layer, void QgsProject::removeMapLayers( const QStringList &layerIds ) { + mProjectScope.reset(); mLayerStore->removeMapLayers( layerIds ); } void QgsProject::removeMapLayers( const QList &layers ) { + mProjectScope.reset(); mLayerStore->removeMapLayers( layers ); } void QgsProject::removeMapLayer( const QString &layerId ) { + mProjectScope.reset(); mLayerStore->removeMapLayer( layerId ); } void QgsProject::removeMapLayer( QgsMapLayer *layer ) { + mProjectScope.reset(); mLayerStore->removeMapLayer( layer ); } QgsMapLayer *QgsProject::takeMapLayer( QgsMapLayer *layer ) { + mProjectScope.reset(); return mLayerStore->takeMapLayer( layer ); } void QgsProject::removeAllMapLayers() { + mProjectScope.reset(); mLayerStore->removeAllMapLayers(); } diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h index fb4ee3e3dad2..80bd15daf144 100644 --- a/src/core/qgsproject.h +++ b/src/core/qgsproject.h @@ -101,10 +101,12 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera Q_PROPERTY( QgsMapThemeCollection *mapThemeCollection READ mapThemeCollection NOTIFY mapThemeCollectionChanged ) Q_PROPERTY( QgsSnappingConfig snappingConfig READ snappingConfig WRITE setSnappingConfig NOTIFY snappingConfigChanged ) Q_PROPERTY( QgsRelationManager *relationManager READ relationManager ) + Q_PROPERTY( AvoidIntersectionsMode avoidIntersectionsMode READ avoidIntersectionsMode WRITE setAvoidIntersectionsMode NOTIFY avoidIntersectionsModeChanged ) Q_PROPERTY( QList avoidIntersectionsLayers READ avoidIntersectionsLayers WRITE setAvoidIntersectionsLayers NOTIFY avoidIntersectionsLayersChanged ) Q_PROPERTY( QgsProjectMetadata metadata READ metadata WRITE setMetadata NOTIFY metadataChanged ) Q_PROPERTY( QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY backgroundColorChanged ) Q_PROPERTY( QColor selectionColor READ selectionColor WRITE setSelectionColor NOTIFY selectionColorChanged ) + Q_PROPERTY( bool topologicalEditing READ topologicalEditing WRITE setTopologicalEditing NOTIFY topologicalEditingChanged ) public: @@ -130,6 +132,18 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera }; Q_ENUM( FileFormat ) + /** + * Flags which control how intersections of pre-existing feature are handled when digitizing new features. + * \since QGIS 3.14 + */ + enum class AvoidIntersectionsMode + { + AllowIntersections, //!< Overlap with any feature allowed when digitizing new features + AvoidIntersectionsCurrentLayer, //!< Overlap with features from the active layer when digitizing new features not allowed + AvoidIntersectionsLayers, //!< Overlap with features from a specified list of layers when digitizing new features not allowed + }; + Q_ENUM( AvoidIntersectionsMode ) + //! Returns the QgsProject singleton instance static QgsProject *instance(); @@ -764,12 +778,27 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera QList avoidIntersectionsLayers() const; /** - * A list of layers with which intersections should be avoided. + * Sets the list of layers with which intersections should be avoided. + * Only used if the avoid intersection mode is set to advanced. * * \since QGIS 3.0 */ void setAvoidIntersectionsLayers( const QList &layers ); + /** + * Sets the avoid intersections mode. + * + * \since QGIS 3.14 + */ + void setAvoidIntersectionsMode( const AvoidIntersectionsMode mode ); + + /** + * Returns the current avoid intersections mode. + * + * \since QGIS 3.14 + */ + AvoidIntersectionsMode avoidIntersectionsMode() const { return mAvoidIntersectionsMode; } + /** * A map of custom project variables. * To get all available variables including generated ones @@ -1411,6 +1440,13 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera */ void snappingConfigChanged( const QgsSnappingConfig &config ); + /** + * Emitted whenever the avoid intersections mode has changed. + * + * \since QGIS 3.14 + */ + void avoidIntersectionsModeChanged(); + /** * Emitted whenever the expression variables stored in the project have been changed. * \since QGIS 3.0 @@ -1798,6 +1834,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera QHash< QString, QPair< QString, bool> > mEmbeddedLayers; QgsSnappingConfig mSnappingConfig; + AvoidIntersectionsMode mAvoidIntersectionsMode = AvoidIntersectionsMode::AllowIntersections; QgsRelationManager *mRelationManager = nullptr; diff --git a/src/core/qgsproperty.h b/src/core/qgsproperty.h index 39e4576cb149..e6a26fad0d34 100644 --- a/src/core/qgsproperty.h +++ b/src/core/qgsproperty.h @@ -489,6 +489,41 @@ class CORE_EXPORT QgsProperty return QVariant::fromValue( *this ); } + +#ifdef SIP_RUN + SIP_PYOBJECT __repr__(); + % MethodCode + QString typeString; + QString definitionString; + switch ( sipCpp->propertyType() ) + { + case QgsProperty::StaticProperty: + typeString = QStringLiteral( "static" ); + definitionString = sipCpp->staticValue().toString(); + break; + + case QgsProperty::FieldBasedProperty: + typeString = QStringLiteral( "field" ); + definitionString = sipCpp->field(); + break; + + case QgsProperty::ExpressionBasedProperty: + typeString = QStringLiteral( "expression" ); + definitionString = sipCpp->expressionString(); + break; + + case QgsProperty::InvalidProperty: + typeString = QStringLiteral( "invalid" ); + break; + } + + QString str = QStringLiteral( "" ).arg( !sipCpp->isActive() && sipCpp->propertyType() != QgsProperty::InvalidProperty ? QStringLiteral( "INACTIVE " ) : QString(), + typeString, + definitionString.isEmpty() ? QString() : QStringLiteral( " (%1)" ).arg( definitionString ) ); + sipRes = PyUnicode_FromString( str.toUtf8().constData() ); + % End +#endif + private: mutable QExplicitlySharedDataPointer d; diff --git a/src/core/qgsproviderconnectionmodel.h b/src/core/qgsproviderconnectionmodel.h index e175b823f8ba..f221aeaf40ce 100644 --- a/src/core/qgsproviderconnectionmodel.h +++ b/src/core/qgsproviderconnectionmodel.h @@ -49,6 +49,7 @@ class CORE_EXPORT QgsProviderConnectionModel : public QAbstractItemModel RoleConfiguration, //!< Connection configuration variant map RoleEmpty, //!< Entry is an empty entry }; + Q_ENUM( Role ) /** * Constructor for QgsProviderConnectionModel, for the specified \a provider. diff --git a/src/core/qgsproviderregistry.cpp b/src/core/qgsproviderregistry.cpp index ef63104e8f28..8231b11ade12 100644 --- a/src/core/qgsproviderregistry.cpp +++ b/src/core/qgsproviderregistry.cpp @@ -30,6 +30,7 @@ #include "qgsmessagelog.h" #include "qgsprovidermetadata.h" #include "qgsvectorlayer.h" +#include "qgsvectortileprovidermetadata.h" #include "qgsproject.h" #include "providers/memory/qgsmemoryprovider.h" #include "providers/gdal/qgsgdalprovider.h" @@ -66,9 +67,10 @@ QgsProviderRegistry *QgsProviderRegistry::instance( const QString &pluginPath ) without accidentally adding a null meta data item to the metadata map. */ static -QgsProviderMetadata *findMetadata_( QgsProviderRegistry::Providers const &metaData, - QString const &providerKey ) +QgsProviderMetadata *findMetadata_( const QgsProviderRegistry::Providers &metaData, + const QString &providerKey ) { + // first do case-sensitive match QgsProviderRegistry::Providers::const_iterator i = metaData.find( providerKey ); @@ -77,8 +79,15 @@ QgsProviderMetadata *findMetadata_( QgsProviderRegistry::Providers const &metaDa return i->second; } + // fallback to case-insensitive match + for ( auto it = metaData.begin(); it != metaData.end(); ++it ) + { + if ( providerKey.compare( it->first, Qt::CaseInsensitive ) == 0 ) + return it->second; + } + return nullptr; -} // findMetadata_ +} QgsProviderRegistry::QgsProviderRegistry( const QString &pluginPath ) { @@ -98,7 +107,6 @@ QgsProviderRegistry::QgsProviderRegistry( const QString &pluginPath ) init(); } - void QgsProviderRegistry::init() { // add static providers @@ -108,6 +116,8 @@ void QgsProviderRegistry::init() Q_NOWARN_DEPRECATED_POP mProviders[ QgsGdalProvider::providerKey() ] = new QgsGdalProviderMetadata(); mProviders[ QgsOgrProvider::providerKey() ] = new QgsOgrProviderMetadata(); + QgsProviderMetadata *vt = new QgsVectorTileProviderMetadata(); + mProviders[ vt->key() ] = vt; #ifdef HAVE_STATIC_PROVIDERS mProviders[ QgsWmsProvider::providerKey() ] = new QgsWmsProviderMetadata(); mProviders[ QgsPostgresProvider::providerKey() ] = new QgsPostgresProviderMetadata(); @@ -568,7 +578,7 @@ QWidget *QgsProviderRegistry::createSelectionWidget( const QString &providerKey, Q_UNUSED( parent ); Q_UNUSED( fl ); Q_UNUSED( widgetMode ); - QgsDebugMsg( "deprecated call - use QgsGui::providerGuiRegistry()->sourceSelectProviders(providerKey)[0]->createDataSourceWidget() instead" ); + QgsDebugMsg( "deprecated call - use QgsGui::sourceSelectProviderRegistry()->createDataSourceWidget() instead" ); return nullptr; } diff --git a/src/core/qgsproviderregistry.h b/src/core/qgsproviderregistry.h index b7f97431cc60..5194261c03e5 100644 --- a/src/core/qgsproviderregistry.h +++ b/src/core/qgsproviderregistry.h @@ -187,7 +187,7 @@ class CORE_EXPORT QgsProviderRegistry * Returns a new widget for selecting layers from a provider. * Either the \a parent widget must be set or the caller becomes * responsible for deleting the returned widget. - * \deprecated QGIS 3.10 - use QgsGui::providerGuiRegistry()->createDataSourceWidget() instead + * \deprecated QGIS 3.10 - use QgsGui::sourceSelectProviderRegistry()->createDataSourceWidget() instead */ Q_DECL_DEPRECATED QWidget *createSelectionWidget( const QString &providerKey, QWidget *parent = nullptr, Qt::WindowFlags fl = Qt::WindowFlags(), QgsProviderRegistry::WidgetMode widgetMode = QgsProviderRegistry::WidgetMode::None ) SIP_DEPRECATED; diff --git a/src/core/qgsproxyfeaturesink.cpp b/src/core/qgsproxyfeaturesink.cpp new file mode 100644 index 000000000000..6fa48841917c --- /dev/null +++ b/src/core/qgsproxyfeaturesink.cpp @@ -0,0 +1,23 @@ +/*************************************************************************** + qgsproxyfeaturesink.cpp + ---------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsproxyfeaturesink.h" + + +QgsProxyFeatureSink::QgsProxyFeatureSink( QgsFeatureSink *sink ) + : mSink( sink ) +{} diff --git a/src/core/qgsproxyfeaturesink.h b/src/core/qgsproxyfeaturesink.h new file mode 100644 index 000000000000..6ebd68734364 --- /dev/null +++ b/src/core/qgsproxyfeaturesink.h @@ -0,0 +1,66 @@ +/*************************************************************************** + qgsproxyfeaturesink.h + ---------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSPROXYFEATURESINK_H +#define QGSPROXYFEATURESINK_H + +#include "qgis_core.h" +#include "qgis.h" +#include "qgsfeaturesink.h" + + +/** + * \class QgsProxyFeatureSink + * \ingroup core + * A simple feature sink which proxies feature addition on to another feature sink. + * + * This class is designed to allow factory methods which always return new QgsFeatureSink + * objects. Since it is not always possible to create an entirely new QgsFeatureSink + * (e.g. if the feature sink is a layer's data provider), a new QgsProxyFeatureSink + * can instead be returned which forwards features on to the destination sink. The + * proxy sink can be safely deleted without affecting the destination sink. + * + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsProxyFeatureSink : public QgsFeatureSink +{ + public: + + /** + * Constructs a new QgsProxyFeatureSink which forwards features onto a destination \a sink. + */ + QgsProxyFeatureSink( QgsFeatureSink *sink ); + bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = nullptr ) override { return mSink->addFeature( feature, flags ); } + bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = nullptr ) override { return mSink->addFeatures( features, flags ); } + bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = nullptr ) override { return mSink->addFeatures( iterator, flags ); } + + /** + * Returns the destination QgsFeatureSink which the proxy will forward features to. + */ + QgsFeatureSink *destinationSink() { return mSink; } + + private: + + QgsFeatureSink *mSink = nullptr; +}; + + +#endif // QGSPROXYFEATURESINK_H + + + + diff --git a/src/core/qgsremappingproxyfeaturesink.cpp b/src/core/qgsremappingproxyfeaturesink.cpp new file mode 100644 index 000000000000..c721c80ae614 --- /dev/null +++ b/src/core/qgsremappingproxyfeaturesink.cpp @@ -0,0 +1,194 @@ +/*************************************************************************** + qgsremappingproxyfeaturesink.cpp + ---------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsremappingproxyfeaturesink.h" +#include "qgslogger.h" + +QgsRemappingProxyFeatureSink::QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink, bool ownsSink ) + : QgsFeatureSink() + , mDefinition( mappingDefinition ) + , mSink( sink ) + , mOwnsSink( ownsSink ) +{} + +QgsRemappingProxyFeatureSink::~QgsRemappingProxyFeatureSink() +{ + if ( mOwnsSink ) + delete mSink; +} + +void QgsRemappingProxyFeatureSink::setExpressionContext( const QgsExpressionContext &context ) +{ + mContext = context; +} + +void QgsRemappingProxyFeatureSink::setTransformContext( const QgsCoordinateTransformContext &context ) +{ + mTransform = QgsCoordinateTransform( mDefinition.sourceCrs(), mDefinition.destinationCrs(), context ); +} + +QgsFeatureList QgsRemappingProxyFeatureSink::remapFeature( const QgsFeature &feature ) const +{ + QgsFeatureList res; + + mContext.setFeature( feature ); + + // remap fields first + QgsFeature f; + f.setFields( mDefinition.destinationFields(), true ); + QgsAttributes attributes; + const QMap< QString, QgsProperty > fieldMap = mDefinition.fieldMap(); + for ( const QgsField &field : mDefinition.destinationFields() ) + { + if ( fieldMap.contains( field.name() ) ) + { + attributes.append( fieldMap.value( field.name() ).value( mContext ) ); + } + else + { + attributes.append( QVariant() ); + } + } + f.setAttributes( attributes ); + + // make geometries compatible, and reproject if necessary + if ( feature.hasGeometry() ) + { + const QVector< QgsGeometry > geometries = feature.geometry().coerceToType( mDefinition.destinationWkbType() ); + if ( !geometries.isEmpty() ) + { + res.reserve( geometries.size() ); + for ( const QgsGeometry &geometry : geometries ) + { + QgsFeature featurePart = f; + + QgsGeometry reproject = geometry; + try + { + reproject.transform( mTransform ); + featurePart.setGeometry( reproject ); + } + catch ( QgsCsException & ) + { + QgsLogger::warning( QObject::tr( "Error reprojecting feature geometry" ) ); + featurePart.clearGeometry(); + } + res << featurePart; + } + } + else + { + f.clearGeometry(); + res << f; + } + } + else + { + res << f; + } + return res; +} + +bool QgsRemappingProxyFeatureSink::addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags ) +{ + QgsFeatureList features = remapFeature( feature ); + return mSink->addFeatures( features, flags ); +} + +bool QgsRemappingProxyFeatureSink::addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags ) +{ + bool res = true; + for ( QgsFeature &f : features ) + { + res = addFeature( f, flags ) && res; + } + return res; +} + +bool QgsRemappingProxyFeatureSink::addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags ) +{ + QgsFeature f; + bool res = true; + while ( iterator.nextFeature( f ) ) + { + res = addFeature( f, flags ) && res; + } + return res; +} + +QVariant QgsRemappingSinkDefinition::toVariant() const +{ + QVariantMap map; + map.insert( QStringLiteral( "wkb_type" ), mDestinationWkbType ); + // we only really care about names here + QVariantList fieldNames; + for ( const QgsField &field : mDestinationFields ) + fieldNames << field.name(); + map.insert( QStringLiteral( "destination_field_names" ), fieldNames ); + map.insert( QStringLiteral( "transform_source" ), mSourceCrs.toWkt( QgsCoordinateReferenceSystem::WKT2_2018 ) ); + map.insert( QStringLiteral( "transform_dest" ), mDestinationCrs.toWkt( QgsCoordinateReferenceSystem::WKT2_2018 ) ); + + QVariantMap fieldMap; + for ( auto it = mFieldMap.constBegin(); it != mFieldMap.constEnd(); ++it ) + { + fieldMap.insert( it.key(), it.value().toVariant() ); + } + map.insert( QStringLiteral( "field_map" ), fieldMap ); + + return map; +} + +bool QgsRemappingSinkDefinition::loadVariant( const QVariantMap &map ) +{ + mDestinationWkbType = static_cast< QgsWkbTypes::Type >( map.value( QStringLiteral( "wkb_type" ), QgsWkbTypes::Unknown ).toInt() ); + + const QVariantList fieldNames = map.value( QStringLiteral( "destination_field_names" ) ).toList(); + QgsFields fields; + for ( const QVariant &field : fieldNames ) + { + fields.append( QgsField( field.toString() ) ); + } + mDestinationFields = fields; + + mSourceCrs = QgsCoordinateReferenceSystem::fromWkt( map.value( QStringLiteral( "transform_source" ) ).toString() ); + mDestinationCrs = QgsCoordinateReferenceSystem::fromWkt( map.value( QStringLiteral( "transform_dest" ) ).toString() ); + + const QVariantMap fieldMap = map.value( QStringLiteral( "field_map" ) ).toMap(); + mFieldMap.clear(); + for ( auto it = fieldMap.constBegin(); it != fieldMap.constEnd(); ++it ) + { + QgsProperty p; + p.loadVariant( it.value() ); + mFieldMap.insert( it.key(), p ); + } + + return true; +} + +bool QgsRemappingSinkDefinition::operator==( const QgsRemappingSinkDefinition &other ) const +{ + return mDestinationWkbType == other.mDestinationWkbType + && mDestinationFields == other.mDestinationFields + && mFieldMap == other.mFieldMap + && mSourceCrs == other.mSourceCrs + && mDestinationCrs == other.mDestinationCrs; +} + +bool QgsRemappingSinkDefinition::operator!=( const QgsRemappingSinkDefinition &other ) const +{ + return !( *this == other ); +} diff --git a/src/core/qgsremappingproxyfeaturesink.h b/src/core/qgsremappingproxyfeaturesink.h new file mode 100644 index 000000000000..df915f806396 --- /dev/null +++ b/src/core/qgsremappingproxyfeaturesink.h @@ -0,0 +1,241 @@ +/*************************************************************************** + qgsremappingproxyfeaturesink.h + ---------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSREMAPPINGPROXYFEATURESINK_H +#define QGSREMAPPINGPROXYFEATURESINK_H + +#include "qgis_core.h" +#include "qgis.h" +#include "qgsfeaturesink.h" +#include "qgsproperty.h" + +/** + * \class QgsRemappingSinkDefinition + * \ingroup core + * Defines the parameters used to remap features when creating a QgsRemappingProxyFeatureSink. + * + * The definition includes parameters required to correctly map incoming features to the structure + * of the destination sink, e.g. information about how to create output field values and how to transform + * geometries to match the destination CRS. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsRemappingSinkDefinition +{ + public: + + /** + * Returns the field mapping, which defines how to map the values from incoming features to destination + * field values. + * + * Field values are mapped using a QgsProperty source object, which allows either direct field value to field value + * mapping or use of QgsExpression expressions to transform values to the destination field. + * + * \see setFieldMap() + * \see addMappedField() + */ + QMap< QString, QgsProperty > fieldMap() const { return mFieldMap; } + + /** + * Sets the field mapping, which defines how to map the values from incoming features to destination + * field values. + * + * Field values are mapped using a QgsProperty source object, which allows either direct field value to field value + * mapping or use of QgsExpression expressions to transform values to the destination field. + * + * \see fieldMap() + * \see addMappedField() + */ + void setFieldMap( const QMap< QString, QgsProperty > &map ) { mFieldMap = map; } + + /** + * Adds a mapping for a destination field. + * + * Field values are mapped using a QgsProperty source object, which allows either direct field value to field value + * mapping or use of QgsExpression expressions to transform values to the destination field. + * + * \see setFieldMap() + * \see fieldMap() + */ + void addMappedField( const QString &destinationField, const QgsProperty &property ) { mFieldMap.insert( destinationField, property ); } + + /** + * Returns the source CRS used for reprojecting incoming features to the sink's destination CRS. + * + * \see setSourceCrs() + */ + QgsCoordinateReferenceSystem sourceCrs() const { return mSourceCrs; } + + /** + * Sets the \a source crs used for reprojecting incoming features to the sink's destination CRS. + * + * \see sourceCrs() + */ + void setSourceCrs( const QgsCoordinateReferenceSystem &source ) { mSourceCrs = source; } + + /** + * Returns the destination CRS used for reprojecting incoming features to the sink's destination CRS. + * + * \see setDestinationCrs() + */ + QgsCoordinateReferenceSystem destinationCrs() const { return mDestinationCrs; } + + /** + * Sets the \a destination crs used for reprojecting incoming features to the sink's destination CRS. + * + * \see destinationCrs() + */ + void setDestinationCrs( const QgsCoordinateReferenceSystem &destination ) { mDestinationCrs = destination; } + + /** + * Returns the WKB geometry type for the destination. + * + * \see setDestinationWkbType() + */ + QgsWkbTypes::Type destinationWkbType() const { return mDestinationWkbType; } + + /** + * Sets the WKB geometry \a type for the destination. + * + * \see setDestinationWkbType() + */ + void setDestinationWkbType( QgsWkbTypes::Type type ) { mDestinationWkbType = type; } + + /** + * Returns the fields for the destination sink. + * + * \see setDestinationFields() + */ + QgsFields destinationFields() const { return mDestinationFields; } + + /** + * Sets the \a fields for the destination sink. + * + * \see destinationFields() + */ + void setDestinationFields( const QgsFields &fields ) { mDestinationFields = fields; } + + /** + * Saves this remapping definition to a QVariantMap, wrapped in a QVariant. + * You can use QgsXmlUtils::writeVariant to save it to an XML document. + * \see loadVariant() + */ + QVariant toVariant() const; + + /** + * Loads this remapping definition from a QVariantMap, wrapped in a QVariant. + * You can use QgsXmlUtils::readVariant to load it from an XML document. + * \see toVariant() + */ + bool loadVariant( const QVariantMap &map ); + + bool operator==( const QgsRemappingSinkDefinition &other ) const; + bool operator!=( const QgsRemappingSinkDefinition &other ) const; + + private: + + QMap< QString, QgsProperty > mFieldMap; + + QgsCoordinateReferenceSystem mSourceCrs; + QgsCoordinateReferenceSystem mDestinationCrs; + + QgsWkbTypes::Type mDestinationWkbType = QgsWkbTypes::Unknown; + + QgsFields mDestinationFields; + +}; + +Q_DECLARE_METATYPE( QgsRemappingSinkDefinition ) + + + +/** + * \class QgsRemappingProxyFeatureSink + * \ingroup core + * A QgsFeatureSink which proxies incoming features to a destination feature sink, after applying + * transformations and field value mappings. + * + * This sink allows for transformation of incoming features to match the requirements of storing + * in an existing destination layer, e.g. by reprojecting the features to the destination's CRS + * and by coercing geometries to the format required by the destination sink. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsRemappingProxyFeatureSink : public QgsFeatureSink +{ + public: + +#ifndef SIP_RUN + + /** + * Constructor for QgsRemappingProxyFeatureSink, using the specified \a mappingDefinition + * to manipulate features before sending them to the destination \a sink. + * + * Ownership of \a sink is dictated by \a ownsSink. If \a ownsSink is FALSE, + * ownership is not transferred, and callers must ensure that \a sink exists for the lifetime of this object. + * If \a ownsSink is TRUE, then this object will take ownership of \a sink. + */ + QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink, bool ownsSink = false ); +#else + + /** + * Constructor for QgsRemappingProxyFeatureSink, using the specified \a mappingDefinition + * to manipulate features before sending them to the destination \a sink. + */ + QgsRemappingProxyFeatureSink( const QgsRemappingSinkDefinition &mappingDefinition, QgsFeatureSink *sink ); +#endif + + ~QgsRemappingProxyFeatureSink() override; + + /** + * Sets the expression \a context to use when evaluating mapped field values. + */ + void setExpressionContext( const QgsExpressionContext &context ); + + /** + * Sets the transform \a context to use when reprojecting features. + */ + void setTransformContext( const QgsCoordinateTransformContext &context ); + + /** + * Remaps a \a feature to a set of features compatible with the destination sink. + */ + QgsFeatureList remapFeature( const QgsFeature &feature ) const; + + bool addFeature( QgsFeature &feature, QgsFeatureSink::Flags flags = nullptr ) override; + bool addFeatures( QgsFeatureList &features, QgsFeatureSink::Flags flags = nullptr ) override; + bool addFeatures( QgsFeatureIterator &iterator, QgsFeatureSink::Flags flags = nullptr ) override; + + /** + * Returns the destination QgsFeatureSink which the proxy will forward features to. + */ + QgsFeatureSink *destinationSink() { return mSink; } + + private: + + QgsRemappingSinkDefinition mDefinition; + QgsCoordinateTransform mTransform; + QgsFeatureSink *mSink = nullptr; + mutable QgsExpressionContext mContext; + bool mOwnsSink = false; +}; + +#endif // QGSREMAPPINGPROXYFEATURESINK_H + + + + diff --git a/src/core/qgssnappingconfig.cpp b/src/core/qgssnappingconfig.cpp index a8ff57edc66d..2095212119da 100644 --- a/src/core/qgssnappingconfig.cpp +++ b/src/core/qgssnappingconfig.cpp @@ -25,12 +25,14 @@ #include "qgsapplication.h" -QgsSnappingConfig::IndividualLayerSettings::IndividualLayerSettings( bool enabled, SnappingTypeFlag type, double tolerance, QgsTolerance::UnitType units ) +QgsSnappingConfig::IndividualLayerSettings::IndividualLayerSettings( bool enabled, SnappingTypeFlag type, double tolerance, QgsTolerance::UnitType units, double minScale, double maxScale ) : mValid( true ) , mEnabled( enabled ) , mType( type ) , mTolerance( tolerance ) , mUnits( units ) + , mMinimumScale( minScale ) + , mMaximumScale( maxScale ) {} QgsSnappingConfig::IndividualLayerSettings::IndividualLayerSettings( bool enabled, SnappingType type, double tolerance, QgsTolerance::UnitType units ) @@ -119,13 +121,35 @@ void QgsSnappingConfig::IndividualLayerSettings::setUnits( QgsTolerance::UnitTyp mUnits = units; } +double QgsSnappingConfig::IndividualLayerSettings::minimumScale() const +{ + return mMinimumScale; +} + +void QgsSnappingConfig::IndividualLayerSettings::setMinimumScale( double minScale ) +{ + mMinimumScale = minScale; +} + +double QgsSnappingConfig::IndividualLayerSettings::maximumScale() const +{ + return mMaximumScale; +} + +void QgsSnappingConfig::IndividualLayerSettings::setMaximumScale( double maxScale ) +{ + mMaximumScale = maxScale; +} + bool QgsSnappingConfig::IndividualLayerSettings::operator !=( const QgsSnappingConfig::IndividualLayerSettings &other ) const { return mValid != other.mValid || mEnabled != other.mEnabled || mType != other.mType || mTolerance != other.mTolerance - || mUnits != other.mUnits; + || mUnits != other.mUnits + || mMinimumScale != other.mMinimumScale + || mMaximumScale != other.mMaximumScale; } bool QgsSnappingConfig::IndividualLayerSettings::operator ==( const QgsSnappingConfig::IndividualLayerSettings &other ) const @@ -134,7 +158,9 @@ bool QgsSnappingConfig::IndividualLayerSettings::operator ==( const QgsSnappingC && mEnabled == other.mEnabled && mType == other.mType && mTolerance == other.mTolerance - && mUnits == other.mUnits; + && mUnits == other.mUnits + && mMinimumScale == other.mMinimumScale + && mMaximumScale == other.mMaximumScale; } @@ -153,7 +179,10 @@ bool QgsSnappingConfig::operator==( const QgsSnappingConfig &other ) const && mTolerance == other.mTolerance && mUnits == other.mUnits && mIntersectionSnapping == other.mIntersectionSnapping - && mIndividualLayerSettings == other.mIndividualLayerSettings; + && mIndividualLayerSettings == other.mIndividualLayerSettings + && mScaleDependencyMode == other.mScaleDependencyMode + && mMinimumScale == other.mMinimumScale + && mMaximumScale == other.mMaximumScale; } void QgsSnappingConfig::reset() @@ -176,6 +205,9 @@ void QgsSnappingConfig::reset() mMode = mode; mType = type; mTolerance = tolerance; + mScaleDependencyMode = Disabled; + mMinimumScale = 0.0; + mMaximumScale = 0.0; // do not allow unit to be "layer" if not in advanced configuration if ( mUnits == QgsTolerance::LayerUnits && mMode != AdvancedConfiguration ) { @@ -197,7 +229,7 @@ void QgsSnappingConfig::reset() QgsVectorLayer *vl = qobject_cast( ml ); if ( vl ) { - mIndividualLayerSettings.insert( vl, IndividualLayerSettings( enabled, type, tolerance, units ) ); + mIndividualLayerSettings.insert( vl, IndividualLayerSettings( enabled, type, tolerance, units, 0.0, 0.0 ) ); } } } @@ -345,7 +377,10 @@ bool QgsSnappingConfig::operator!=( const QgsSnappingConfig &other ) const || mType != other.mType || mTolerance != other.mTolerance || mUnits != other.mUnits - || mIndividualLayerSettings != other.mIndividualLayerSettings; + || mIndividualLayerSettings != other.mIndividualLayerSettings + || mScaleDependencyMode != other.mScaleDependencyMode + || mMinimumScale != other.mMinimumScale + || mMaximumScale != other.mMaximumScale; } void QgsSnappingConfig::readProject( const QDomDocument &doc ) @@ -372,7 +407,7 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc ) if ( versionElem.hasAttribute( QStringLiteral( "version" ) ) ) { version = versionElem.attribute( QStringLiteral( "version" ) ); - QRegularExpression re( "(\\d).(\\d)" ); + QRegularExpression re( "([\\d]+)\\.([\\d]+)" ); QRegularExpressionMatch match = re.match( version ); if ( match.hasMatch() ) { @@ -409,6 +444,15 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc ) if ( snapSettingsElem.hasAttribute( QStringLiteral( "tolerance" ) ) ) mTolerance = snapSettingsElem.attribute( QStringLiteral( "tolerance" ) ).toDouble(); + if ( snapSettingsElem.hasAttribute( QStringLiteral( "scaleDependencyMode" ) ) ) + mScaleDependencyMode = static_cast( snapSettingsElem.attribute( QStringLiteral( "scaleDependencyMode" ) ).toInt() ); + + if ( snapSettingsElem.hasAttribute( QStringLiteral( "minScale" ) ) ) + mMinimumScale = snapSettingsElem.attribute( QStringLiteral( "minScale" ) ).toDouble(); + + if ( snapSettingsElem.hasAttribute( QStringLiteral( "maxScale" ) ) ) + mMaximumScale = snapSettingsElem.attribute( QStringLiteral( "maxScale" ) ).toDouble(); + if ( snapSettingsElem.hasAttribute( QStringLiteral( "unit" ) ) ) mUnits = ( QgsTolerance::UnitType )snapSettingsElem.attribute( QStringLiteral( "unit" ) ).toInt(); @@ -436,6 +480,8 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc ) QgsSnappingConfig::SnappingTypeFlag type = static_cast( settingElement.attribute( QStringLiteral( "type" ) ).toInt() ); double tolerance = settingElement.attribute( QStringLiteral( "tolerance" ) ).toDouble(); QgsTolerance::UnitType units = ( QgsTolerance::UnitType )settingElement.attribute( QStringLiteral( "units" ) ).toInt(); + double minScale = settingElement.attribute( QStringLiteral( "minScale" ) ).toDouble(); + double maxScale = settingElement.attribute( QStringLiteral( "maxScale" ) ).toDouble(); QgsMapLayer *ml = mProject->mapLayer( layerId ); if ( !ml || ml->type() != QgsMapLayerType::VectorLayer ) @@ -443,7 +489,7 @@ void QgsSnappingConfig::readProject( const QDomDocument &doc ) QgsVectorLayer *vl = qobject_cast( ml ); - IndividualLayerSettings setting = IndividualLayerSettings( enabled, type, tolerance, units ); + IndividualLayerSettings setting = IndividualLayerSettings( enabled, type, tolerance, units, minScale, maxScale ); mIndividualLayerSettings.insert( vl, setting ); } } @@ -458,6 +504,9 @@ void QgsSnappingConfig::writeProject( QDomDocument &doc ) snapSettingsElem.setAttribute( QStringLiteral( "tolerance" ), mTolerance ); snapSettingsElem.setAttribute( QStringLiteral( "unit" ), static_cast( mUnits ) ); snapSettingsElem.setAttribute( QStringLiteral( "intersection-snapping" ), QString::number( mIntersectionSnapping ) ); + snapSettingsElem.setAttribute( QStringLiteral( "scaleDependencyMode" ), QString::number( mScaleDependencyMode ) ); + snapSettingsElem.setAttribute( QStringLiteral( "minScale" ), mMinimumScale ); + snapSettingsElem.setAttribute( QStringLiteral( "maxScale" ), mMaximumScale ); QDomElement ilsElement = doc.createElement( QStringLiteral( "individual-layer-settings" ) ); QHash::const_iterator layerIt = mIndividualLayerSettings.constBegin(); @@ -471,6 +520,8 @@ void QgsSnappingConfig::writeProject( QDomDocument &doc ) layerElement.setAttribute( QStringLiteral( "type" ), static_cast( setting.typeFlag() ) ); layerElement.setAttribute( QStringLiteral( "tolerance" ), setting.tolerance() ); layerElement.setAttribute( QStringLiteral( "units" ), static_cast( setting.units() ) ); + layerElement.setAttribute( QStringLiteral( "minScale" ), setting.minimumScale() ); + layerElement.setAttribute( QStringLiteral( "maxScale" ), setting.maximumScale() ); ilsElement.appendChild( layerElement ); } snapSettingsElem.appendChild( ilsElement ); @@ -492,7 +543,7 @@ bool QgsSnappingConfig::addLayers( const QList &layers ) QgsVectorLayer *vl = qobject_cast( ml ); if ( vl && vl->isSpatial() ) { - mIndividualLayerSettings.insert( vl, IndividualLayerSettings( enabled, type, tolerance, units ) ); + mIndividualLayerSettings.insert( vl, IndividualLayerSettings( enabled, type, tolerance, units, 0.0, 0.0 ) ); changed = true; } } @@ -567,7 +618,7 @@ void QgsSnappingConfig::readLegacySettings() ) ); - mIndividualLayerSettings.insert( vlayer, IndividualLayerSettings( *enabledIt == QLatin1String( "enabled" ), t, tolIt->toDouble(), static_cast( tolUnitIt->toInt() ) ) ); + mIndividualLayerSettings.insert( vlayer, IndividualLayerSettings( *enabledIt == QLatin1String( "enabled" ), t, tolIt->toDouble(), static_cast( tolUnitIt->toInt() ), 0.0, 0.0 ) ); } QString snapType = mProject->readEntry( QStringLiteral( "Digitizing" ), QStringLiteral( "/DefaultSnapType" ), QStringLiteral( "off" ) ); @@ -596,3 +647,38 @@ void QgsSnappingConfig::setProject( QgsProject *project ) reset(); } + +double QgsSnappingConfig::minimumScale() const +{ + return mMinimumScale; +} + +void QgsSnappingConfig::setMinimumScale( double minScale ) +{ + mMinimumScale = minScale; +} + +double QgsSnappingConfig::maximumScale() const +{ + return mMaximumScale; +} + +void QgsSnappingConfig::setMaximumScale( double maxScale ) +{ + mMaximumScale = maxScale; +} + +void QgsSnappingConfig::setScaleDependencyMode( QgsSnappingConfig::ScaleDependencyMode mode ) +{ + mScaleDependencyMode = mode; +} + +QgsSnappingConfig::ScaleDependencyMode QgsSnappingConfig::scaleDependencyMode() const +{ + return mScaleDependencyMode; +} + + + + + diff --git a/src/core/qgssnappingconfig.h b/src/core/qgssnappingconfig.h index 1e949edf7f1b..e71ee813f3a0 100644 --- a/src/core/qgssnappingconfig.h +++ b/src/core/qgssnappingconfig.h @@ -80,6 +80,18 @@ class CORE_EXPORT QgsSnappingConfig Q_DECLARE_FLAGS( SnappingTypeFlag, SnappingTypes ) Q_FLAG( SnappingTypeFlag ) + /** + * ScaleDependencyMode the scale dependency mode of snapping + * \since QGIS 3.14 + */ + enum ScaleDependencyMode + { + Disabled = 0,//!< No scale dependency + Global = 1,//!< Scale dependency using global min max range + PerLayer = 2//!< Scale dependency using min max range per layer + }; + Q_ENUM( ScaleDependencyMode ) + /** * Convenient method to returns the translated name of the enum type * QgsSnappingConfig::SnappingTypeFlag @@ -126,9 +138,11 @@ class CORE_EXPORT QgsSnappingConfig * \param type * \param tolerance * \param units + * \param minScale 0.0 disable scale limit + * \param maxScale 0.0 disable scale limit * \since QGIS 3.12 */ - IndividualLayerSettings( bool enabled, SnappingTypeFlag type, double tolerance, QgsTolerance::UnitType units ); + IndividualLayerSettings( bool enabled, SnappingTypeFlag type, double tolerance, QgsTolerance::UnitType units, double minScale = 0.0, double maxScale = 0.0 ); /** * Constructs an invalid setting @@ -180,6 +194,30 @@ class CORE_EXPORT QgsSnappingConfig //! Sets the type of units void setUnits( QgsTolerance::UnitType units ); + /** + * Returns minimum scale on which snapping is limited + * \since QGIS 3.14 + */ + double minimumScale() const; + + /** + * Sets the min scale value on which snapping is used, 0.0 disable scale limit + * \since QGIS 3.14 + */ + void setMinimumScale( double minScale ); + + /** + * Returns max scale on which snapping is limited + * \since QGIS 3.14 + */ + double maximumScale() const; + + /** + * Sets the max scale value on which snapping is used, 0.0 disable scale limit + * \since QGIS 3.14 + */ + void setMaximumScale( double maxScale ); + /** * Compare this configuration to other. */ @@ -193,6 +231,8 @@ class CORE_EXPORT QgsSnappingConfig SnappingTypeFlag mType = VertexFlag; double mTolerance = 0; QgsTolerance::UnitType mUnits = QgsTolerance::Pixels; + double mMinimumScale = 0.0; + double mMaximumScale = 0.0; }; /** @@ -247,6 +287,42 @@ class CORE_EXPORT QgsSnappingConfig //! Sets the tolerance void setTolerance( double tolerance ); + /** + * Returns the min scale + * \since QGIS 3.14 + */ + double minimumScale() const; + + /** + * Sets the min scale on which snapping is enabled, 0.0 disable scale limit + * \since QGIS 3.14 + */ + void setMinimumScale( double minScale ); + + /** + * Returns the max scale + * \since QGIS 3.14 + */ + double maximumScale() const; + + /** + * Set the max scale on which snapping is enabled, 0.0 disable scale limit + * \since QGIS 3.14 + */ + void setMaximumScale( double maxScale ); + + /** + * Set the scale dependency mode + * \since QGIS 3.14 + */ + void setScaleDependencyMode( ScaleDependencyMode mode ); + + /** + * Returns the scale dependency mode + * \since QGIS 3.14 + */ + ScaleDependencyMode scaleDependencyMode() const; + //! Returns the type of units QgsTolerance::UnitType units() const; @@ -377,6 +453,9 @@ class CORE_EXPORT QgsSnappingConfig SnappingMode mMode = ActiveLayer; SnappingTypeFlag mType = VertexFlag; double mTolerance = 0.0; + ScaleDependencyMode mScaleDependencyMode = Disabled; + double mMinimumScale = 0.0; + double mMaximumScale = 0.0; QgsTolerance::UnitType mUnits = QgsTolerance::ProjectUnits; bool mIntersectionSnapping = false; diff --git a/src/core/qgssnappingutils.cpp b/src/core/qgssnappingutils.cpp index 7c8015159ab8..4d5c07799acf 100644 --- a/src/core/qgssnappingutils.cpp +++ b/src/core/qgssnappingutils.cpp @@ -292,10 +292,28 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, else if ( mSnappingConfig.mode() == QgsSnappingConfig::AdvancedConfiguration ) { QList layers; + QList filteredConfigs; + + bool inRangeGlobal = ( mSnappingConfig.minimumScale() <= 0.0 || mMapSettings.scale() >= mSnappingConfig.minimumScale() ) + && ( mSnappingConfig.maximumScale() <= 0.0 || mMapSettings.scale() <= mSnappingConfig.maximumScale() ); + for ( const LayerConfig &layerConfig : qgis::as_const( mLayers ) ) { - double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit ); - layers << qMakePair( layerConfig.layer, _areaOfInterest( pointMap, tolerance ) ); + QgsSnappingConfig::IndividualLayerSettings layerSettings = mSnappingConfig.individualLayerSettings( layerConfig.layer ); + + bool inRangeLayer = ( layerSettings.minimumScale() <= 0.0 || mMapSettings.scale() >= layerSettings.minimumScale() ) + && ( layerSettings.maximumScale() <= 0.0 || mMapSettings.scale() <= layerSettings.maximumScale() ); + + //If limit to scale is disabled, snapping activated on all layer + //If no per layer config is set use the global one, otherwise use the layer config + if ( mSnappingConfig.scaleDependencyMode() == QgsSnappingConfig::Disabled + || ( mSnappingConfig.scaleDependencyMode() == QgsSnappingConfig::Global && inRangeGlobal ) + || ( mSnappingConfig.scaleDependencyMode() == QgsSnappingConfig::PerLayer && inRangeLayer ) ) + { + double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit ); + layers << qMakePair( layerConfig.layer, _areaOfInterest( pointMap, tolerance ) ); + filteredConfigs << layerConfig; + } } prepareIndex( layers, relaxed ); @@ -303,7 +321,7 @@ QgsPointLocator::Match QgsSnappingUtils::snapToMap( const QgsPointXY &pointMap, QgsPointLocator::MatchList edges; // for snap on intersection double maxSnapIntTolerance = 0; - for ( const LayerConfig &layerConfig : qgis::as_const( mLayers ) ) + for ( const LayerConfig &layerConfig : qgis::as_const( filteredConfigs ) ) { double tolerance = QgsTolerance::toleranceInProjectUnits( layerConfig.tolerance, layerConfig.layer, mMapSettings, layerConfig.unit ); if ( QgsPointLocator *loc = locatorForLayerUsingStrategy( layerConfig.layer, pointMap, tolerance ) ) diff --git a/src/core/qgsspatialiteutils.cpp b/src/core/qgsspatialiteutils.cpp index c5f79b2187a9..e9e1950b149d 100644 --- a/src/core/qgsspatialiteutils.cpp +++ b/src/core/qgsspatialiteutils.cpp @@ -22,6 +22,22 @@ #include #include +// Define this variable to print all spatialite SQL statements +#ifdef SPATIALITE_PRINT_ALL_SQL +// Debugging code +#include +#include +static int trace_callback( unsigned, void *ctx, void *p, void * ) +{ + sqlite3_stmt *stmt = ( sqlite3_stmt * )p; + char *sql = sqlite3_expanded_sql( stmt ); + qDebug() << "SPATIALITE" << QThread::currentThreadId() << ( sqlite3 * ) ctx << sql; + sqlite3_free( sql ); + return 0; +} +#endif + + int spatialite_database_unique_ptr::open( const QString &path ) { auto &deleter = get_deleter(); @@ -54,6 +70,16 @@ int spatialite_database_unique_ptr::open_v2( const QString &path, int flags, con if ( result == SQLITE_OK ) spatialite_init_ex( database, deleter.mSpatialiteContext, 0 ); +#ifdef SPATIALITE_PRINT_ALL_SQL + // Log all queries + sqlite3_trace_v2( + database, + SQLITE_TRACE_STMT, + trace_callback, + database + ); +#endif + return result; } diff --git a/src/core/qgssqliteexpressioncompiler.cpp b/src/core/qgssqliteexpressioncompiler.cpp index dc0561425909..248bf6a6fb10 100644 --- a/src/core/qgssqliteexpressioncompiler.cpp +++ b/src/core/qgssqliteexpressioncompiler.cpp @@ -31,12 +31,30 @@ QgsSqlExpressionCompiler::Result QgsSQLiteExpressionCompiler::compileNode( const { case QgsExpressionNode::ntBinaryOperator: { - switch ( static_cast( node )->op() ) + const QgsExpressionNodeBinaryOperator *op = static_cast( node ); + switch ( op->op() ) { case QgsExpressionNodeBinaryOperator::boPow: case QgsExpressionNodeBinaryOperator::boRegexp: return Fail; //not supported by SQLite + case QgsExpressionNodeBinaryOperator::boILike: + case QgsExpressionNodeBinaryOperator::boNotILike: + { + QString opL, opR; + + if ( compileNode( op->opLeft(), opL ) != Complete || + compileNode( op->opRight(), opR ) != Complete ) + return Fail; + + result = QStringLiteral( "lower(%1) %2 lower(%3) ESCAPE '\\'" ) + .arg( opL ) + .arg( op->op() == QgsExpressionNodeBinaryOperator::boILike ? QStringLiteral( "LIKE" ) : QStringLiteral( "NOT LIKE" ) ) + .arg( opR ); + + return Complete; + } + default: //fallback to default handling return QgsSqlExpressionCompiler::compileNode( node, result ); diff --git a/src/core/qgsstatisticalsummary.cpp b/src/core/qgsstatisticalsummary.cpp index 466d230842e7..161d9d9a31e3 100644 --- a/src/core/qgsstatisticalsummary.cpp +++ b/src/core/qgsstatisticalsummary.cpp @@ -229,14 +229,14 @@ void QgsStatisticalSummary::finalize() if ( mStatistics & QgsStatisticalSummary::Minority || mStatistics & QgsStatisticalSummary::Majority ) { QList valueCounts = mValueCount.values(); - std::sort( valueCounts.begin(), valueCounts.end() ); + if ( mStatistics & QgsStatisticalSummary::Minority ) { - mMinority = mValueCount.key( valueCounts.first() ); + mMinority = mValueCount.key( *std::min_element( valueCounts.begin(), valueCounts.end() ) ); } if ( mStatistics & QgsStatisticalSummary::Majority ) { - mMajority = mValueCount.key( valueCounts.last() ); + mMajority = mValueCount.key( *std::max_element( valueCounts.begin(), valueCounts.end() ) ); } } diff --git a/src/core/qgsstatisticalsummary.h b/src/core/qgsstatisticalsummary.h index e4b4a0729dca..8f5965922a54 100644 --- a/src/core/qgsstatisticalsummary.h +++ b/src/core/qgsstatisticalsummary.h @@ -241,19 +241,19 @@ class CORE_EXPORT QgsStatisticalSummary int variety() const { return mValueCount.count(); } /** - * Returns minority of values. The minority is the value with least occurrences in the list + * Returns minority of values. The minority is the value with least occurrences in the list. * This is only calculated if Statistic::Minority has been specified in the constructor - * or via setStatistics. A NaN value may be returned if the minority cannot - * be calculated. + * or via setStatistics. If multiple values match, return the first value relative to the + * initial values order. A NaN value may be returned if the minority cannot be calculated. * \see majority */ double minority() const { return mMinority; } /** - * Returns majority of values. The majority is the value with most occurrences in the list + * Returns majority of values. The majority is the value with most occurrences in the list. * This is only calculated if Statistic::Majority has been specified in the constructor - * or via setStatistics. A NaN value may be returned if the majority cannot - * be calculated. + * or via setStatistics. If multiple values match, return the first value relative to the + * initial values order. A NaN value may be returned if the minority cannot be calculated. * \see minority */ double majority() const { return mMajority; } diff --git a/src/core/qgsstringstatisticalsummary.cpp b/src/core/qgsstringstatisticalsummary.cpp index 1de59f4bd175..450b11b5d435 100644 --- a/src/core/qgsstringstatisticalsummary.cpp +++ b/src/core/qgsstringstatisticalsummary.cpp @@ -44,6 +44,8 @@ void QgsStringStatisticalSummary::reset() mMaxLength = 0; mSumLengths = 0; mMeanLength = 0; + mMinority = QString(); + mMajority = QString(); } void QgsStringStatisticalSummary::calculate( const QStringList &values ) @@ -75,6 +77,20 @@ void QgsStringStatisticalSummary::addValue( const QVariant &value ) void QgsStringStatisticalSummary::finalize() { mMeanLength = mSumLengths / static_cast< double >( mCount ); + + if ( mStatistics & Minority || mStatistics & Majority ) + { + QList valueCounts = mValues.values(); + + if ( mStatistics & Minority ) + { + mMinority = mValues.key( *std::min_element( valueCounts.begin(), valueCounts.end() ) ); + } + if ( mStatistics & Majority ) + { + mMajority = mValues.key( *std::max_element( valueCounts.begin(), valueCounts.end() ) ); + } + } } void QgsStringStatisticalSummary::calculateFromVariants( const QVariantList &values ) @@ -89,6 +105,8 @@ void QgsStringStatisticalSummary::calculateFromVariants( const QVariantList &val testString( variant.toString() ); } } + + finalize(); } void QgsStringStatisticalSummary::testString( const QString &string ) @@ -98,9 +116,9 @@ void QgsStringStatisticalSummary::testString( const QString &string ) if ( string.isEmpty() ) mCountMissing++; - if ( mStatistics & CountDistinct ) + if ( mStatistics & CountDistinct || mStatistics & Majority || mStatistics & Minority ) { - mValues << string; + mValues[string]++; } if ( mStatistics & Min ) { @@ -150,6 +168,10 @@ QVariant QgsStringStatisticalSummary::statistic( QgsStringStatisticalSummary::St return mMaxLength; case MeanLength: return mMeanLength; + case Minority: + return mMinority; + case Majority: + return mMajority; case All: return 0; } @@ -176,6 +198,10 @@ QString QgsStringStatisticalSummary::displayName( QgsStringStatisticalSummary::S return QObject::tr( "Maximum length" ); case MeanLength: return QObject::tr( "Mean length" ); + case Minority: + return QObject::tr( "Minority" ); + case Majority: + return QObject::tr( "Majority" ); case All: return QString(); } diff --git a/src/core/qgsstringstatisticalsummary.h b/src/core/qgsstringstatisticalsummary.h index 3cc29cd7736c..b4669818e1ef 100644 --- a/src/core/qgsstringstatisticalsummary.h +++ b/src/core/qgsstringstatisticalsummary.h @@ -55,7 +55,9 @@ class CORE_EXPORT QgsStringStatisticalSummary MinimumLength = 32, //!< Minimum length of string MaximumLength = 64, //!< Maximum length of string MeanLength = 128, //!< Mean length of strings - All = Count | CountDistinct | CountMissing | Min | Max | MinimumLength | MaximumLength | MeanLength, //!< All statistics + Minority = 256, //!< Minority of strings + Majority = 512, //!< Majority of strings + All = Count | CountDistinct | CountMissing | Min | Max | MinimumLength | MaximumLength | MeanLength | Minority | Majority, //!< All statistics }; Q_DECLARE_FLAGS( Statistics, Statistic ) @@ -156,13 +158,13 @@ class CORE_EXPORT QgsStringStatisticalSummary * Returns the number of distinct string values. * \see distinctValues() */ - int countDistinct() const { return mValues.count(); } + int countDistinct() const { return mValues.keys().count(); } /** * Returns the set of distinct string values. * \see countDistinct() */ - QSet< QString > distinctValues() const { return mValues; } + QSet< QString > distinctValues() const { return QSet::fromList( mValues.keys() ); } /** * Returns the number of missing (null) string values. @@ -195,6 +197,26 @@ class CORE_EXPORT QgsStringStatisticalSummary */ double meanLength() const { return mMeanLength; } + /** + * Returns the least common string. The minority is the value with least occurrences in the list + * This is only calculated if Statistic::Minority has been specified in the constructor + * or via setStatistics. If multiple values match, return the first value relative to the + * initial values order. + * \see majority + * \since QGIS 3.14 + */ + QString minority() const { return mMinority; } + + /** + * Returns the most common string. The majority is the value with most occurrences in the list + * This is only calculated if Statistic::Majority has been specified in the constructor + * or via setStatistics. If multiple values match, return the first value relative to the + * initial values order. + * \see minority + * \since QGIS 3.14 + */ + QString majority() const { return mMajority; } + /** * Returns the friendly display name for a statistic * \param statistic statistic to return name for @@ -206,7 +228,7 @@ class CORE_EXPORT QgsStringStatisticalSummary Statistics mStatistics; int mCount; - QSet< QString > mValues; + QMap< QString, int > mValues; int mCountMissing; QString mMin; QString mMax; @@ -214,6 +236,8 @@ class CORE_EXPORT QgsStringStatisticalSummary int mMaxLength; long mSumLengths; double mMeanLength; + QString mMinority; + QString mMajority; void testString( const QString &string ); }; diff --git a/src/core/qgsstringutils.cpp b/src/core/qgsstringutils.cpp index 9423dd486397..7cbadccd6342 100644 --- a/src/core/qgsstringutils.cpp +++ b/src/core/qgsstringutils.cpp @@ -14,6 +14,7 @@ ***************************************************************************/ #include "qgsstringutils.h" +#include "qgslogger.h" #include #include #include @@ -407,6 +408,111 @@ QString QgsStringUtils::soundex( const QString &string ) return tmp; } + +double QgsStringUtils::fuzzyScore( const QString &candidate, const QString &search ) +{ + QString candidateNormalized = candidate.simplified().normalized( QString:: NormalizationForm_C ).toLower(); + QString searchNormalized = search.simplified().normalized( QString:: NormalizationForm_C ).toLower(); + + int candidateLength = candidateNormalized.length(); + int searchLength = searchNormalized.length(); + int score = 0; + + // if the candidate and the search term are empty, no other option than 0 score + if ( candidateLength == 0 || searchLength == 0 ) + return score; + + int candidateIdx = 0; + int searchIdx = 0; + // there is always at least one word + int maxScore = FUZZY_SCORE_WORD_MATCH; + + bool isPreviousIndexMatching = false; + bool isWordOpen = true; + + // loop trough each candidate char and calculate the potential max score + while ( candidateIdx < candidateLength ) + { + QChar candidateChar = candidateNormalized[ candidateIdx++ ]; + bool isCandidateCharWordEnd = candidateChar == ' ' || candidateChar.isPunct(); + + // the first char is always the default score + if ( candidateIdx == 1 ) + maxScore += FUZZY_SCORE_NEW_MATCH; + // every space character or underscore is a opportunity for a new word + else if ( isCandidateCharWordEnd ) + maxScore += FUZZY_SCORE_WORD_MATCH; + // potentially we can match every other character + else + maxScore += FUZZY_SCORE_CONSECUTIVE_MATCH; + + // we looped through all the characters + if ( searchIdx >= searchLength ) + continue; + + QChar searchChar = searchNormalized[ searchIdx ]; + bool isSearchCharWordEnd = searchChar == ' ' || searchChar.isPunct(); + + // match! + if ( candidateChar == searchChar || ( isCandidateCharWordEnd && isSearchCharWordEnd ) ) + { + searchIdx++; + + // if we have just successfully finished a word, give higher score + if ( isSearchCharWordEnd ) + { + if ( isWordOpen ) + score += FUZZY_SCORE_WORD_MATCH; + else if ( isPreviousIndexMatching ) + score += FUZZY_SCORE_CONSECUTIVE_MATCH; + else + score += FUZZY_SCORE_NEW_MATCH; + + isWordOpen = true; + } + // if we have consecutive characters matching, give higher score + else if ( isPreviousIndexMatching ) + { + score += FUZZY_SCORE_CONSECUTIVE_MATCH; + } + // normal score for new independent character that matches + else + { + score += FUZZY_SCORE_NEW_MATCH; + } + + isPreviousIndexMatching = true; + } + // if the current character does NOT match, we are sure we cannot build a word for now + else + { + isPreviousIndexMatching = false; + isWordOpen = false; + } + + // if the search string is covered, check if the last match is end of word + if ( searchIdx >= searchLength ) + { + bool isEndOfWord = ( candidateIdx >= candidateLength ) + ? true + : candidateNormalized[candidateIdx] == ' ' || candidateNormalized[candidateIdx].isPunct(); + + if ( isEndOfWord ) + score += FUZZY_SCORE_WORD_MATCH; + } + + // QgsLogger::debug( QStringLiteral( "TMP: %1 | %2 | %3 | %4 | %5" ).arg( candidateChar, searchChar, QString::number(score), QString::number(isCandidateCharWordEnd), QString::number(isSearchCharWordEnd) ) + QStringLiteral( __FILE__ ) ); + } + + // QgsLogger::debug( QStringLiteral( "RES: %1 | %2" ).arg( QString::number(maxScore), QString::number(score) ) + QStringLiteral( __FILE__ ) ); + // we didn't loop through all the search chars, it means, that they are not present in the current candidate + if ( searchIdx < searchLength ) + score = 0; + + return static_cast( std::max( score, 0 ) ) / std::max( maxScore, 1 ); +} + + QString QgsStringUtils::insertLinks( const QString &string, bool *foundLinks ) { QString converted = string; @@ -450,7 +556,7 @@ QString QgsStringUtils::insertLinks( const QString &string, bool *foundLinks ) QString QgsStringUtils::htmlToMarkdown( const QString &html ) { - + // Any changes in this function must be copied to qgscrashreport.cpp too QString converted = html; converted.replace( QLatin1String( "
    " ), QLatin1String( "\n" ) ); converted.replace( QLatin1String( "" ), QLatin1String( "**" ) ); @@ -461,7 +567,7 @@ QString QgsStringUtils::htmlToMarkdown( const QString &html ) while ( hrefRegEx.indexIn( converted, offset ) != -1 ) { QString url = hrefRegEx.cap( 1 ).replace( QStringLiteral( "\"" ), QString() ); - url.replace( QStringLiteral( "'" ), QString() ); + url.replace( '\'', QString() ); QString name = hrefRegEx.cap( 2 ); QString anchor = QStringLiteral( "[%1](%2)" ).arg( name, url ); converted.replace( hrefRegEx, anchor ); diff --git a/src/core/qgsstringutils.h b/src/core/qgsstringutils.h index 2b71dafceb50..7f34d170c42e 100644 --- a/src/core/qgsstringutils.h +++ b/src/core/qgsstringutils.h @@ -24,6 +24,9 @@ #ifndef QGSSTRINGUTILS_H #define QGSSTRINGUTILS_H +#define FUZZY_SCORE_WORD_MATCH 5 +#define FUZZY_SCORE_NEW_MATCH 3 +#define FUZZY_SCORE_CONSECUTIVE_MATCH 4 /** * \ingroup core @@ -252,6 +255,17 @@ class CORE_EXPORT QgsStringUtils */ static QString soundex( const QString &string ); + /** + * Tests a \a candidate string to see how likely it is a match for + * a specified \a search string. Values are normalized between 0 and 1. + * \param candidate candidate string + * \param search search term string + * \return Normalized value of how likely is the \a search to be in the \a candidate + * \note Use this function only to calculate the fuzzy score between two strings and later compare these values, but do not depend on the actual numbers. They are implementation detail that may change in a future release. + * \since 3.14 + */ + static double fuzzyScore( const QString &candidate, const QString &search ); + /** * Returns a string with any URL (e.g., http(s)/ftp) and mailto: text converted to valid HTML * links. diff --git a/src/core/qgstaskmanager.cpp b/src/core/qgstaskmanager.cpp index 6b84aeb6570c..bc7ad5543173 100644 --- a/src/core/qgstaskmanager.cpp +++ b/src/core/qgstaskmanager.cpp @@ -18,6 +18,7 @@ #include "qgstaskmanager.h" #include "qgsproject.h" #include "qgsmaplayerlistutils.h" +#include #include @@ -390,8 +391,7 @@ QgsTaskManager::QgsTaskManager( QObject *parent ) : QObject( parent ) , mTaskMutex( new QMutex( QMutex::Recursive ) ) { - connect( QgsProject::instance(), static_cast < void ( QgsProject::* )( const QList< QgsMapLayer * >& ) > ( &QgsProject::layersWillBeRemoved ), - this, &QgsTaskManager::layersWillBeRemoved ); + } QgsTaskManager::~QgsTaskManager() @@ -432,6 +432,15 @@ long QgsTaskManager::addTaskPrivate( QgsTask *task, QgsTaskList dependencies, bo if ( !task ) return 0; + if ( !mInitialized ) + { + mInitialized = true; + // defer connection to project until we actually need it -- we don't want to connect to the project instance in the constructor, + // cos that forces early creation of QgsProject + connect( QgsProject::instance(), static_cast < void ( QgsProject::* )( const QList< QgsMapLayer * >& ) > ( &QgsProject::layersWillBeRemoved ), + this, &QgsTaskManager::layersWillBeRemoved ); + } + long taskId = mNextTaskId++; mTaskMutex->lock(); diff --git a/src/core/qgstaskmanager.h b/src/core/qgstaskmanager.h index 257d87b519ef..272e62b43e55 100644 --- a/src/core/qgstaskmanager.h +++ b/src/core/qgstaskmanager.h @@ -64,11 +64,13 @@ class CORE_EXPORT QgsTask : public QObject Complete, //!< Task successfully completed Terminated, //!< Task was terminated or errored }; + Q_ENUM( TaskStatus ) //! Task flags enum Flag { CanCancel = 1 << 1, //!< Task can be canceled + CancelWithoutPrompt = 1 << 2, //!< Task can be canceled without any users prompts, e.g. when closing a project or QGIS. AllFlags = CanCancel, //!< Task supports all flags }; Q_DECLARE_FLAGS( Flags, Flag ) @@ -591,6 +593,8 @@ class CORE_EXPORT QgsTaskManager : public QObject QgsTaskRunnableWrapper *runnable = nullptr; }; + bool mInitialized = false; + mutable QMutex *mTaskMutex; QMap< long, TaskInfo > mTasks; diff --git a/src/core/qgstemporalnavigationobject.cpp b/src/core/qgstemporalnavigationobject.cpp index 9a03640e2e22..166ec78e7dbe 100644 --- a/src/core/qgstemporalnavigationobject.cpp +++ b/src/core/qgstemporalnavigationobject.cpp @@ -71,25 +71,32 @@ void QgsTemporalNavigationObject::setLooping( bool loopAnimation ) QgsDateTimeRange QgsTemporalNavigationObject::dateTimeRangeForFrameNumber( long long frame ) const { - QDateTime start = mTemporalExtents.begin(); + const QDateTime start = mTemporalExtents.begin(); if ( frame < 0 ) frame = 0; - long long nextFrame = frame + 1; - QDateTime begin = start.addSecs( frame * mFrameDuration.seconds() ); - QDateTime end = start.addSecs( nextFrame * mFrameDuration.seconds() ); + const long long nextFrame = frame + 1; + + const QDateTime begin = start.addSecs( frame * mFrameDuration.seconds() ); + const QDateTime end = start.addSecs( nextFrame * mFrameDuration.seconds() ); if ( end <= mTemporalExtents.end() ) - return QgsDateTimeRange( begin, end ); + return QgsDateTimeRange( begin, end, true, false ); - return QgsDateTimeRange( begin, mTemporalExtents.end() ); + return QgsDateTimeRange( begin, mTemporalExtents.end(), true, false ); } void QgsTemporalNavigationObject::setTemporalExtents( const QgsDateTimeRange &temporalExtents ) { mTemporalExtents = temporalExtents; + int currentFrameNmber = mCurrentFrameNumber; setCurrentFrameNumber( 0 ); + + //Force to emit signal if the current frame number doesn't change + if ( currentFrameNmber == mCurrentFrameNumber ) + emit updateTemporalRange( dateTimeRangeForFrameNumber( 0 ) ); + } QgsDateTimeRange QgsTemporalNavigationObject::temporalExtents() const diff --git a/src/core/qgstemporalutils.cpp b/src/core/qgstemporalutils.cpp new file mode 100644 index 000000000000..8480ea70a801 --- /dev/null +++ b/src/core/qgstemporalutils.cpp @@ -0,0 +1,76 @@ +/*************************************************************************** + qgstemporalutils.cpp + ----------------------- + Date : March 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson 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 "qgstemporalutils.h" +#include "qgsproject.h" +#include "qgsmaplayertemporalproperties.h" +#include "qgsrasterlayer.h" +#include "qgsmeshlayer.h" + +QgsDateTimeRange QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject *project ) +{ + const QMap &mapLayers = project->mapLayers(); + QgsMapLayer *currentLayer = nullptr; + + QDateTime minDate; + QDateTime maxDate; + + for ( QMap::const_iterator it = mapLayers.constBegin(); it != mapLayers.constEnd(); ++it ) + { + currentLayer = it.value(); + + if ( !currentLayer->temporalProperties() || !currentLayer->temporalProperties()->isActive() ) + continue; + QgsDateTimeRange layerRange; + + switch ( currentLayer->type() ) + { + case QgsMapLayerType::RasterLayer: + { + QgsRasterLayer *rasterLayer = qobject_cast( currentLayer ); + + switch ( rasterLayer->temporalProperties()->mode() ) + { + case QgsRasterLayerTemporalProperties::ModeFixedTemporalRange: + layerRange = rasterLayer->temporalProperties()->fixedTemporalRange(); + break; + + case QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider: + layerRange = rasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange(); + break; + } + } + break; + case QgsMapLayerType::MeshLayer: + { + QgsMeshLayer *meshLayer = qobject_cast( currentLayer ); + layerRange = meshLayer->temporalProperties()->timeExtent(); + } + break; + default: + break; + } + + + if ( !minDate.isValid() || layerRange.begin() < minDate ) + minDate = layerRange.begin(); + if ( !maxDate.isValid() || layerRange.end() > maxDate ) + maxDate = layerRange.end(); + + + } + + return QgsDateTimeRange( minDate, maxDate ); +} diff --git a/src/core/qgstemporalutils.h b/src/core/qgstemporalutils.h new file mode 100644 index 000000000000..2f750e83635f --- /dev/null +++ b/src/core/qgstemporalutils.h @@ -0,0 +1,47 @@ +/*************************************************************************** + qgstemporalutils.h + ------------------ + Date : March 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson 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 QGSTEMPORALUTILS_H +#define QGSTEMPORALUTILS_H + +#include "qgis_core.h" +#include "qgsrange.h" + +class QgsProject; + +/** + * \ingroup core + * \class QgsTemporalUtils + * \brief Contains utility methods for working with temporal layers and projects. + * + * \since QGIS 3.14 + */ + +class CORE_EXPORT QgsTemporalUtils +{ + public: + + /** + * Calculates the temporal range for a \a project. + * + * This method considers the temporal range available from layers contained within the project and + * returns the maximal combined temporal extent of these layers. + */ + static QgsDateTimeRange calculateTemporalRangeForProject( QgsProject *project ); + +}; + + +#endif // QGSTEMPORALUTILS_H diff --git a/src/core/qgstiles.cpp b/src/core/qgstiles.cpp new file mode 100644 index 000000000000..3beade14fca8 --- /dev/null +++ b/src/core/qgstiles.cpp @@ -0,0 +1,83 @@ +/*************************************************************************** + qgstiles.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgstiles.h" + +#include "qgslogger.h" + +QgsTileMatrix QgsTileMatrix::fromWebMercator( int zoomLevel ) +{ + int numTiles = static_cast( pow( 2, zoomLevel ) ); // assuming we won't ever go over 30 zoom levels + double z0xMin = -20037508.3427892, z0yMin = -20037508.3427892; + double z0xMax = 20037508.3427892, z0yMax = 20037508.3427892; + double s0 = 559082264.0287178; // scale denominator at zoom level 0 of GoogleCRS84Quad + + QgsTileMatrix tm; + tm.mZoomLevel = zoomLevel; + tm.mMatrixWidth = numTiles; + tm.mMatrixHeight = numTiles; + tm.mTileXSpan = ( z0xMax - z0xMin ) / tm.mMatrixWidth; + tm.mTileYSpan = ( z0yMax - z0yMin ) / tm.mMatrixHeight; + tm.mExtent = QgsRectangle( z0xMin, z0yMin, z0xMax, z0yMax ); + tm.mScaleDenom = s0 / pow( 2, zoomLevel ); + return tm; +} + +QgsRectangle QgsTileMatrix::tileExtent( QgsTileXYZ id ) const +{ + double xMin = mExtent.xMinimum() + mTileXSpan * id.column(); + double xMax = xMin + mTileXSpan; + double yMax = mExtent.yMaximum() - mTileYSpan * id.row(); + double yMin = yMax - mTileYSpan; + return QgsRectangle( xMin, yMin, xMax, yMax ); +} + +QgsPointXY QgsTileMatrix::tileCenter( QgsTileXYZ id ) const +{ + double x = mExtent.xMinimum() + mTileXSpan / 2 * id.column(); + double y = mExtent.yMaximum() - mTileYSpan / 2 * id.row(); + return QgsPointXY( x, y ); +} + +QgsTileRange QgsTileMatrix::tileRangeFromExtent( const QgsRectangle &r ) +{ + double x0 = qBound( mExtent.xMinimum(), r.xMinimum(), mExtent.xMaximum() ); + double y0 = qBound( mExtent.yMinimum(), r.yMinimum(), mExtent.yMaximum() ); + double x1 = qBound( mExtent.xMinimum(), r.xMaximum(), mExtent.xMaximum() ); + double y1 = qBound( mExtent.yMinimum(), r.yMaximum(), mExtent.yMaximum() ); + if ( x0 >= x1 || y0 >= y1 ) + return QgsTileRange(); // nothing to display + + double tileX1 = ( x0 - mExtent.xMinimum() ) / mTileXSpan; + double tileX2 = ( x1 - mExtent.xMinimum() ) / mTileXSpan; + double tileY1 = ( mExtent.yMaximum() - y1 ) / mTileYSpan; + double tileY2 = ( mExtent.yMaximum() - y0 ) / mTileYSpan; + + QgsDebugMsgLevel( QStringLiteral( "Tile range of edges [%1,%2] - [%3,%4]" ).arg( tileX1 ).arg( tileY1 ).arg( tileX2 ).arg( tileY2 ), 2 ); + + // figure out tile range from zoom + int startColumn = qBound( 0, static_cast( floor( tileX1 ) ), mMatrixWidth - 1 ); + int endColumn = qBound( 0, static_cast( floor( tileX2 ) ), mMatrixWidth - 1 ); + int startRow = qBound( 0, static_cast( floor( tileY1 ) ), mMatrixHeight - 1 ); + int endRow = qBound( 0, static_cast( floor( tileY2 ) ), mMatrixHeight - 1 ); + return QgsTileRange( startColumn, endColumn, startRow, endRow ); +} + +QPointF QgsTileMatrix::mapToTileCoordinates( const QgsPointXY &mapPoint ) const +{ + double dx = mapPoint.x() - mExtent.xMinimum(); + double dy = mExtent.yMaximum() - mapPoint.y(); + return QPointF( dx / mTileXSpan, dy / mTileYSpan ); +} diff --git a/src/core/qgstiles.h b/src/core/qgstiles.h new file mode 100644 index 000000000000..86641487ad38 --- /dev/null +++ b/src/core/qgstiles.h @@ -0,0 +1,138 @@ +/*************************************************************************** + qgstiles.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSTILES_H +#define QGSTILES_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +#include "qgsrectangle.h" + +/** + * \ingroup core + * Stores coordinates of a tile in a tile matrix set. Tile matrix is identified + * by the zoomLevel(), and the position within tile matrix is given by column() + * and row(). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsTileXYZ +{ + public: + //! Constructs a tile identifier from given column, row and zoom level indices + QgsTileXYZ( int tc = -1, int tr = -1, int tz = -1 ) + : mColumn( tc ), mRow( tr ), mZoomLevel( tz ) + { + } + + //! Returns tile's column index (X) + int column() const { return mColumn; } + //! Returns tile's row index (Y) + int row() const { return mRow; } + //! Returns tile's zoom level (Z) + int zoomLevel() const { return mZoomLevel; } + + //! Returns tile coordinates in a formatted string + QString toString() const { return QStringLiteral( "X=%1 Y=%2 Z=%3" ).arg( mColumn ).arg( mRow ).arg( mZoomLevel ); } + + private: + int mColumn; + int mRow; + int mZoomLevel; +}; + + +/** + * \ingroup core + * Range of tiles in a tile matrix to be rendered. The selection is rectangular, + * given by start/end row and column numbers. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsTileRange +{ + public: + //! Constructs a range of tiles from given span of columns and rows + QgsTileRange( int c1 = -1, int c2 = -1, int r1 = -1, int r2 = -1 ) + : mStartColumn( c1 ), mEndColumn( c2 ), mStartRow( r1 ), mEndRow( r2 ) {} + + //! Returns whether the range is valid (when all row/column numbers are not negative) + bool isValid() const { return mStartColumn >= 0 && mEndColumn >= 0 && mStartRow >= 0 && mEndRow >= 0; } + + //! Returns index of the first column in the range + int startColumn() const { return mStartColumn; } + //! Returns index of the last column in the range + int endColumn() const { return mEndColumn; } + //! Returns index of the first row in the range + int startRow() const { return mStartRow; } + //! Returns index of the last row in the range + int endRow() const { return mEndRow; } + + private: + int mStartColumn; + int mEndColumn; + int mStartRow; + int mEndRow; +}; + + +/** + * \ingroup core + * Defines a matrix of tiles for a single zoom level: it is defined by its size (width * height) + * and map extent that it covers. + * + * Please note that we follow the XYZ convention of X/Y axes, i.e. top-left tile has [0,0] coordinate + * (which is different from TMS convention where bottom-left tile has [0,0] coordinate). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsTileMatrix +{ + public: + + //! Returns a tile matrix for the usual web mercator + static QgsTileMatrix fromWebMercator( int mZoomLevel ); + + //! Returns extent of the given tile in this matrix + QgsRectangle tileExtent( QgsTileXYZ id ) const; + + //! Returns center of the given tile in this matrix + QgsPointXY tileCenter( QgsTileXYZ id ) const; + + //! Returns tile range that fully covers the given extent + QgsTileRange tileRangeFromExtent( const QgsRectangle &mExtent ); + + //! Returns row/column coordinates (floating point number) from the given point in map coordinates + QPointF mapToTileCoordinates( const QgsPointXY &mapPoint ) const; + + private: + //! Zoom level index associated with the tile matrix + int mZoomLevel; + //! Number of columns of the tile matrix + int mMatrixWidth; + //! Number of rows of the tile matrix + int mMatrixHeight; + //! Matrix extent in map units in the CRS of tile matrix set + QgsRectangle mExtent; + //! Scale denominator of the map scale associated with the tile matrix + double mScaleDenom; + //! Width of a single tile in map units (derived from extent and matrix size) + double mTileXSpan; + //! Height of a single tile in map units (derived from extent and matrix size) + double mTileYSpan; +}; + +#endif // QGSTILES_H diff --git a/src/core/qgsvectordataprovider.cpp b/src/core/qgsvectordataprovider.cpp index 0fd2c38797fe..525447cd2ac8 100644 --- a/src/core/qgsvectordataprovider.cpp +++ b/src/core/qgsvectordataprovider.cpp @@ -846,3 +846,8 @@ QgsVectorDataProviderTemporalCapabilities *QgsVectorDataProvider::temporalCapabi { return mTemporalCapabilities.get(); } + +const QgsVectorDataProviderTemporalCapabilities *QgsVectorDataProvider::temporalCapabilities() const +{ + return mTemporalCapabilities.get(); +} diff --git a/src/core/qgsvectordataprovider.h b/src/core/qgsvectordataprovider.h index 68498c57f30f..f50105aea427 100644 --- a/src/core/qgsvectordataprovider.h +++ b/src/core/qgsvectordataprovider.h @@ -198,7 +198,7 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider, public QgsFeat * Returns a short comment for the data that this provider is * providing access to (e.g. the comment for postgres table). */ - virtual QString dataComment() const; + virtual QString dataComment() const override; /** * Returns the minimum value of an attribute @@ -618,6 +618,7 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider, public QgsFeat virtual void handlePostCloneOperations( QgsVectorDataProvider *source ); QgsVectorDataProviderTemporalCapabilities *temporalCapabilities() override; + const QgsVectorDataProviderTemporalCapabilities *temporalCapabilities() const override SIP_SKIP; signals: diff --git a/src/core/qgsvectorlayer.h b/src/core/qgsvectorlayer.h index cf537a3b4c30..cc70ea2d613c 100644 --- a/src/core/qgsvectorlayer.h +++ b/src/core/qgsvectorlayer.h @@ -2323,7 +2323,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * * \see select(QgsFeatureId) */ - void select( const QgsFeatureIds &featureIds ); + Q_INVOKABLE void select( const QgsFeatureIds &featureIds ); /** * Deselects feature by its ID @@ -2341,7 +2341,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * * \see deselect(const QgsFeatureId) */ - void deselect( const QgsFeatureIds &featureIds ); + Q_INVOKABLE void deselect( const QgsFeatureIds &featureIds ); /** * Clear selection @@ -2349,7 +2349,7 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte * \see selectByIds() * \see reselect() */ - void removeSelection(); + Q_INVOKABLE void removeSelection(); /** * Reselects the previous set of selected features. This is only applicable diff --git a/src/core/qgsvectorlayereditutils.cpp b/src/core/qgsvectorlayereditutils.cpp index a0d56de62c84..7255207cb2b8 100644 --- a/src/core/qgsvectorlayereditutils.cpp +++ b/src/core/qgsvectorlayereditutils.cpp @@ -475,35 +475,27 @@ QgsGeometry::OperationResult QgsVectorLayerEditUtils::splitParts( const QgsPoint fit = mLayer->getFeatures( QgsFeatureRequest().setFilterRect( bBox ).setFlags( QgsFeatureRequest::ExactIntersect ) ); } - QgsGeometry::OperationResult addPartRet = QgsGeometry::OperationResult::Success; - QgsFeature feat; while ( fit.nextFeature( feat ) ) { QVector newGeometries; QgsPointSequence topologyTestPoints; QgsGeometry featureGeom = feat.geometry(); - splitFunctionReturn = featureGeom.splitGeometry( splitLine, newGeometries, topologicalEditing, topologyTestPoints ); - if ( splitFunctionReturn == 0 ) + splitFunctionReturn = featureGeom.splitGeometry( splitLine, newGeometries, topologicalEditing, topologyTestPoints, false ); + + if ( splitFunctionReturn == QgsGeometry::OperationResult::Success && !newGeometries.isEmpty() ) { - //add new parts - if ( !newGeometries.isEmpty() ) - featureGeom.convertToMultiType(); + QgsGeometry newGeom( newGeometries.at( 0 ) ); + newGeom.convertToMultiType(); - for ( int i = 0; i < newGeometries.size(); ++i ) + for ( int i = 1; i < newGeometries.size(); ++i ) { - addPartRet = featureGeom.addPart( newGeometries.at( i ) ); - if ( addPartRet ) - break; + QgsGeometry part = newGeometries.at( i ); + part.convertToSingleType(); + newGeom.addPart( part ); } - // For test only: Exception already thrown here... - // feat.geometry()->asWkb(); - - if ( !addPartRet ) - { - mLayer->changeGeometry( feat.id(), featureGeom ); - } + mLayer->changeGeometry( feat.id(), newGeom ); if ( topologicalEditing ) { diff --git a/src/core/qgsvectorlayerjoininfo.cpp b/src/core/qgsvectorlayerjoininfo.cpp index 3353ebd40643..8743e8755cef 100644 --- a/src/core/qgsvectorlayerjoininfo.cpp +++ b/src/core/qgsvectorlayerjoininfo.cpp @@ -35,6 +35,19 @@ QString QgsVectorLayerJoinInfo::prefixedFieldName( const QgsField &f ) const return name; } +void QgsVectorLayerJoinInfo::setUsingMemoryCache( bool enabled ) +{ + mMemoryCache = enabled; +} + +bool QgsVectorLayerJoinInfo::isUsingMemoryCache() const +{ + if ( mUpsertOnEdit ) + return false; + + return mMemoryCache; +} + void QgsVectorLayerJoinInfo::setEditable( bool enabled ) { mEditable = enabled; diff --git a/src/core/qgsvectorlayerjoininfo.h b/src/core/qgsvectorlayerjoininfo.h index 6b508d723547..3c5be6a1f6c1 100644 --- a/src/core/qgsvectorlayerjoininfo.h +++ b/src/core/qgsvectorlayerjoininfo.h @@ -65,9 +65,13 @@ class CORE_EXPORT QgsVectorLayerJoinInfo QString prefix() const { return mPrefix; } //! Sets whether values from the joined layer should be cached in memory to speed up lookups - void setUsingMemoryCache( bool enabled ) { mMemoryCache = enabled; } - //! Returns whether values from the joined layer should be cached in memory to speed up lookups - bool isUsingMemoryCache() const { return mMemoryCache; } + void setUsingMemoryCache( bool enabled ); + + /** + * Returns whether values from the joined layer should be cached in memory to speed up lookups. + * Will return false if upsertOnEdit is enabled. + */ + bool isUsingMemoryCache() const; /** * Returns whether the form has to be dynamically updated with joined fields diff --git a/src/core/qgsvectorlayerutils.cpp b/src/core/qgsvectorlayerutils.cpp index a4edebb9953f..5c5f20742446 100644 --- a/src/core/qgsvectorlayerutils.cpp +++ b/src/core/qgsvectorlayerutils.cpp @@ -714,7 +714,7 @@ QgsFeatureList QgsVectorLayerUtils::makeFeatureCompatible( const QgsFeature &fea QgsWkbTypes::Type::NoGeometry && inputWkbType != QgsWkbTypes::Type::Unknown; // Drop geometry if layer is geometry-less - if ( newFHasGeom && ! layerHasGeom ) + if ( ( newFHasGeom && !layerHasGeom ) || !newFHasGeom ) { QgsFeature _f = QgsFeature( layer->fields() ); _f.setAttributes( newF.attributes() ); @@ -722,133 +722,26 @@ QgsFeatureList QgsVectorLayerUtils::makeFeatureCompatible( const QgsFeature &fea } else { - // Geometry need fixing - if ( newFHasGeom && layerHasGeom && newF.geometry().wkbType() != inputWkbType ) - { - // Curved -> straight - if ( !QgsWkbTypes::isCurvedType( inputWkbType ) && QgsWkbTypes::isCurvedType( newF.geometry().wkbType() ) ) - { - QgsGeometry newGeom( newF.geometry().constGet()->segmentize() ); - newF.setGeometry( newGeom ); - } + // Geometry need fixing? + const QVector< QgsGeometry > geometries = newF.geometry().coerceToType( inputWkbType ); - // polygon -> line - if ( QgsWkbTypes::geometryType( inputWkbType ) == QgsWkbTypes::LineGeometry && - newF.geometry().type() == QgsWkbTypes::PolygonGeometry ) - { - // boundary gives us a (multi)line string of exterior + interior rings - QgsGeometry newGeom( newF.geometry().constGet()->boundary() ); - newF.setGeometry( newGeom ); - } - // line -> polygon - if ( QgsWkbTypes::geometryType( inputWkbType ) == QgsWkbTypes::PolygonGeometry && - newF.geometry().type() == QgsWkbTypes::LineGeometry ) - { - std::unique_ptr< QgsGeometryCollection > gc( QgsGeometryFactory::createCollectionOfType( inputWkbType ) ); - const QgsGeometry source = newF.geometry(); - for ( auto part = source.const_parts_begin(); part != source.const_parts_end(); ++part ) - { - std::unique_ptr< QgsAbstractGeometry > exterior( ( *part )->clone() ); - if ( QgsCurve *curve = qgsgeometry_cast< QgsCurve * >( exterior.get() ) ) - { - if ( QgsWkbTypes::isCurvedType( inputWkbType ) ) - { - std::unique_ptr< QgsCurvePolygon > cp = qgis::make_unique< QgsCurvePolygon >(); - cp->setExteriorRing( curve ); - exterior.release(); - gc->addGeometry( cp.release() ); - } - else - { - std::unique_ptr< QgsPolygon > p = qgis::make_unique< QgsPolygon >(); - p->setExteriorRing( qgsgeometry_cast< QgsLineString * >( curve ) ); - exterior.release(); - gc->addGeometry( p.release() ); - } - } - } - QgsGeometry newGeom( std::move( gc ) ); - newF.setGeometry( newGeom ); - } - - // line/polygon -> points - if ( QgsWkbTypes::geometryType( inputWkbType ) == QgsWkbTypes::PointGeometry && - ( newF.geometry().type() == QgsWkbTypes::LineGeometry || - newF.geometry().type() == QgsWkbTypes::PolygonGeometry ) ) - { - // lines/polygons to a point layer, extract all vertices - std::unique_ptr< QgsMultiPoint > mp = qgis::make_unique< QgsMultiPoint >(); - const QgsGeometry source = newF.geometry(); - QSet< QgsPoint > added; - for ( auto vertex = source.vertices_begin(); vertex != source.vertices_end(); ++vertex ) - { - if ( added.contains( *vertex ) ) - continue; // avoid duplicate points, e.g. start/end of rings - mp->addGeometry( ( *vertex ).clone() ); - added.insert( *vertex ); - } - QgsGeometry newGeom( std::move( mp ) ); - newF.setGeometry( newGeom ); - } - - // Single -> multi - if ( QgsWkbTypes::isMultiType( inputWkbType ) && ! newF.geometry().isMultipart( ) ) - { - QgsGeometry newGeom( newF.geometry( ) ); - newGeom.convertToMultiType(); - newF.setGeometry( newGeom ); - } - // Drop Z/M - if ( newF.geometry().constGet()->is3D() && ! QgsWkbTypes::hasZ( inputWkbType ) ) - { - QgsGeometry newGeom( newF.geometry( ) ); - newGeom.get()->dropZValue(); - newF.setGeometry( newGeom ); - } - if ( newF.geometry().constGet()->isMeasure() && ! QgsWkbTypes::hasM( inputWkbType ) ) - { - QgsGeometry newGeom( newF.geometry( ) ); - newGeom.get()->dropMValue(); - newF.setGeometry( newGeom ); - } - // Add Z/M back, set to 0 - if ( ! newF.geometry().constGet()->is3D() && QgsWkbTypes::hasZ( inputWkbType ) ) - { - QgsGeometry newGeom( newF.geometry( ) ); - newGeom.get()->addZValue( 0.0 ); - newF.setGeometry( newGeom ); - } - if ( ! newF.geometry().constGet()->isMeasure() && QgsWkbTypes::hasM( inputWkbType ) ) + if ( geometries.count() != 1 ) + { + QgsAttributeMap attrMap; + for ( int j = 0; j < newF.fields().count(); j++ ) { - QgsGeometry newGeom( newF.geometry( ) ); - newGeom.get()->addMValue( 0.0 ); - newF.setGeometry( newGeom ); + attrMap[j] = newF.attribute( j ); } - // Multi -> single - if ( ! QgsWkbTypes::isMultiType( inputWkbType ) && newF.geometry().isMultipart( ) ) - { - QgsGeometry newGeom( newF.geometry( ) ); - const QgsGeometryCollection *parts( static_cast< const QgsGeometryCollection * >( newGeom.constGet() ) ); - QgsAttributeMap attrMap; - for ( int j = 0; j < newF.fields().count(); j++ ) - { - attrMap[j] = newF.attribute( j ); - } - resultFeatures.reserve( parts->partCount() ); - for ( int i = 0; i < parts->partCount( ); i++ ) - { - QgsGeometry g( parts->geometryN( i )->clone() ); - QgsFeature _f( createFeature( layer, g, attrMap ) ); - resultFeatures.append( _f ); - } - } - else + resultFeatures.reserve( geometries.size() ); + for ( const QgsGeometry &geometry : geometries ) { - resultFeatures.append( newF ); + QgsFeature _f( createFeature( layer, geometry, attrMap ) ); + resultFeatures.append( _f ); } } else { + newF.setGeometry( geometries.at( 0 ) ); resultFeatures.append( newF ); } } diff --git a/src/core/qgsvirtuallayertask.cpp b/src/core/qgsvirtuallayertask.cpp index 0b240b2bfa02..90a3ad212477 100644 --- a/src/core/qgsvirtuallayertask.cpp +++ b/src/core/qgsvirtuallayertask.cpp @@ -24,7 +24,7 @@ QgsVirtualLayerTask::QgsVirtualLayerTask( const QgsVirtualLayerDefinition &defin { mDefinition.setLazy( true ); QgsVectorLayer::LayerOptions options { QgsCoordinateTransformContext() }; - mLayer = qgis::make_unique( mDefinition.toString(), QStringLiteral( "layer" ), QLatin1Literal( "virtual" ), options ); + mLayer = qgis::make_unique( mDefinition.toString(), QStringLiteral( "layer" ), QLatin1String( "virtual" ), options ); } bool QgsVirtualLayerTask::run() diff --git a/src/core/qgsxmlutils.cpp b/src/core/qgsxmlutils.cpp index 13d32b594554..2b901158b8ef 100644 --- a/src/core/qgsxmlutils.cpp +++ b/src/core/qgsxmlutils.cpp @@ -21,6 +21,7 @@ #include "qgsproperty.h" #include "qgssymbollayerutils.h" #include "qgsprocessingparameters.h" +#include "qgsremappingproxyfeaturesink.h" QgsUnitTypes::DistanceUnit QgsXmlUtils::readMapUnits( const QDomElement &element ) { @@ -221,6 +222,20 @@ QDomElement QgsXmlUtils::writeVariant( const QVariant &value, QDomDocument &doc element.setAttribute( QStringLiteral( "type" ), QStringLiteral( "QgsProcessingOutputLayerDefinition" ) ); break; } + else if ( value.canConvert< QgsProcessingFeatureSourceDefinition >() ) + { + QDomElement valueElement = writeVariant( value.value< QgsProcessingFeatureSourceDefinition >().toVariant(), doc ); + element.appendChild( valueElement ); + element.setAttribute( QStringLiteral( "type" ), QStringLiteral( "QgsProcessingFeatureSourceDefinition" ) ); + break; + } + else if ( value.canConvert< QgsRemappingSinkDefinition >() ) + { + QDomElement valueElement = writeVariant( value.value< QgsRemappingSinkDefinition >().toVariant(), doc ); + element.appendChild( valueElement ); + element.setAttribute( QStringLiteral( "type" ), QStringLiteral( "QgsRemappingSinkDefinition" ) ); + break; + } Q_ASSERT_X( false, "QgsXmlUtils::writeVariant", QStringLiteral( "unsupported user variant type %1" ).arg( QMetaType::typeName( value.userType() ) ).toLocal8Bit() ); break; } @@ -359,6 +374,30 @@ QVariant QgsXmlUtils::readVariant( const QDomElement &element ) return QVariant(); } + else if ( type == QLatin1String( "QgsProcessingFeatureSourceDefinition" ) ) + { + QgsProcessingFeatureSourceDefinition res; + const QDomNodeList values = element.childNodes(); + if ( values.isEmpty() ) + return QVariant(); + + if ( res.loadVariant( QgsXmlUtils::readVariant( values.at( 0 ).toElement() ).toMap() ) ) + return res; + + return QVariant(); + } + else if ( type == QLatin1String( "QgsRemappingSinkDefinition" ) ) + { + QgsRemappingSinkDefinition res; + const QDomNodeList values = element.childNodes(); + if ( values.isEmpty() ) + return QVariant(); + + if ( res.loadVariant( QgsXmlUtils::readVariant( values.at( 0 ).toElement() ).toMap() ) ) + return QVariant::fromValue( res ); + + return QVariant(); + } else { return QVariant(); diff --git a/src/core/raster/qgscolorrampshader.cpp b/src/core/raster/qgscolorrampshader.cpp index ec3c5ed841ea..abc03975822d 100644 --- a/src/core/raster/qgscolorrampshader.cpp +++ b/src/core/raster/qgscolorrampshader.cpp @@ -500,6 +500,8 @@ QDomElement QgsColorRampShader::writeXml( QDomDocument &doc ) const colorRampShaderElem.setAttribute( QStringLiteral( "colorRampType" ), colorRampTypeAsQString() ); colorRampShaderElem.setAttribute( QStringLiteral( "classificationMode" ), classificationMode() ); colorRampShaderElem.setAttribute( QStringLiteral( "clip" ), clip() ); + colorRampShaderElem.setAttribute( QStringLiteral( "minimumValue" ), mMinimumValue ); + colorRampShaderElem.setAttribute( QStringLiteral( "maximumValue" ), mMaximumValue ); // save source color ramp if ( sourceColorRamp() ) @@ -535,6 +537,8 @@ void QgsColorRampShader::readXml( const QDomElement &colorRampShaderElem ) setColorRampType( colorRampShaderElem.attribute( QStringLiteral( "colorRampType" ), QStringLiteral( "INTERPOLATED" ) ) ); setClassificationMode( static_cast< QgsColorRampShader::ClassificationMode >( colorRampShaderElem.attribute( QStringLiteral( "classificationMode" ), QStringLiteral( "1" ) ).toInt() ) ); setClip( colorRampShaderElem.attribute( QStringLiteral( "clip" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ) ); + setMinimumValue( colorRampShaderElem.attribute( QStringLiteral( "minimumValue" ) ).toDouble() ); + setMaximumValue( colorRampShaderElem.attribute( QStringLiteral( "maximumValue" ) ).toDouble() ); QList itemList; QDomElement itemElem; diff --git a/src/core/raster/qgsrastercontourrenderer.cpp b/src/core/raster/qgsrastercontourrenderer.cpp new file mode 100644 index 000000000000..2452de375aa6 --- /dev/null +++ b/src/core/raster/qgsrastercontourrenderer.cpp @@ -0,0 +1,226 @@ +/*************************************************************************** + qgsrastercontourrenderer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsrastercontourrenderer.h" + +#include "qgslinesymbollayer.h" +#include "qgsreadwritecontext.h" +#include "qgssymbollayerutils.h" + +#include + +QgsRasterContourRenderer::QgsRasterContourRenderer( QgsRasterInterface *input ) + : QgsRasterRenderer( input, QStringLiteral( "contour" ) ) +{ + mContourSymbol.reset( static_cast( QgsLineSymbol::defaultSymbol( QgsWkbTypes::LineGeometry ) ) ); +} + +QgsRasterContourRenderer::~QgsRasterContourRenderer() = default; + +QgsRasterContourRenderer *QgsRasterContourRenderer::clone() const +{ + QgsRasterContourRenderer *renderer = new QgsRasterContourRenderer( nullptr ); + renderer->copyCommonProperties( this ); + renderer->mContourSymbol.reset( mContourSymbol ? mContourSymbol->clone() : nullptr ); + renderer->mContourIndexSymbol.reset( mContourIndexSymbol ? mContourIndexSymbol->clone() : nullptr ); + renderer->mContourInterval = mContourInterval; + renderer->mContourIndexInterval = mContourIndexInterval; + renderer->mInputBand = mInputBand; + renderer->mDownscale = mDownscale; + return renderer; +} + +QgsRasterRenderer *QgsRasterContourRenderer::create( const QDomElement &elem, QgsRasterInterface *input ) +{ + if ( elem.isNull() ) + { + return nullptr; + } + + QgsRasterContourRenderer *r = new QgsRasterContourRenderer( input ); + r->readXml( elem ); + + int inputBand = elem.attribute( QStringLiteral( "band" ), QStringLiteral( "-1" ) ).toInt(); + double contourInterval = elem.attribute( QStringLiteral( "contour-interval" ), QStringLiteral( "100" ) ).toDouble(); + double contourIndexInterval = elem.attribute( QStringLiteral( "contour-index-interval" ), QStringLiteral( "0" ) ).toDouble(); + double downscale = elem.attribute( QStringLiteral( "downscale" ), QStringLiteral( "4" ) ).toDouble(); + + r->setInputBand( inputBand ); + r->setContourInterval( contourInterval ); + r->setContourIndexInterval( contourIndexInterval ); + r->setDownscale( downscale ); + + QDomElement symbolsElem = elem.firstChildElement( QStringLiteral( "symbols" ) ); + if ( !symbolsElem.isNull() ) + { + QgsSymbolMap symbolMap = QgsSymbolLayerUtils::loadSymbols( symbolsElem, QgsReadWriteContext() ); + if ( symbolMap.contains( QStringLiteral( "contour" ) ) ) + { + QgsSymbol *symbol = symbolMap.take( QStringLiteral( "contour" ) ); + if ( symbol->type() == QgsSymbol::Line ) + r->setContourSymbol( static_cast( symbol ) ); + } + if ( symbolMap.contains( QStringLiteral( "index-contour" ) ) ) + { + QgsSymbol *symbol = symbolMap.take( QStringLiteral( "index-contour" ) ); + if ( symbol->type() == QgsSymbol::Line ) + r->setContourIndexSymbol( static_cast( symbol ) ); + } + } + return r; +} + +void QgsRasterContourRenderer::writeXml( QDomDocument &doc, QDomElement &parentElem ) const +{ + if ( parentElem.isNull() ) + { + return; + } + + QDomElement rasterRendererElem = doc.createElement( QStringLiteral( "rasterrenderer" ) ); + _writeXml( doc, rasterRendererElem ); + + rasterRendererElem.setAttribute( QStringLiteral( "band" ), mInputBand ); + rasterRendererElem.setAttribute( QStringLiteral( "contour-interval" ), mContourInterval ); + rasterRendererElem.setAttribute( QStringLiteral( "contour-index-interval" ), mContourIndexInterval ); + rasterRendererElem.setAttribute( QStringLiteral( "downscale" ), mDownscale ); + + QgsSymbolMap symbols; + symbols[QStringLiteral( "contour" )] = mContourSymbol.get(); + if ( mContourIndexSymbol ) + symbols[QStringLiteral( "index-contour" )] = mContourIndexSymbol.get(); + QDomElement symbolsElem = QgsSymbolLayerUtils::saveSymbols( symbols, QStringLiteral( "symbols" ), doc, QgsReadWriteContext() ); + rasterRendererElem.appendChild( symbolsElem ); + + parentElem.appendChild( rasterRendererElem ); +} + +struct ContourWriterData +{ + QPainter *painter; + double scaleX, scaleY; + QgsLineSymbol *symbol; + QgsLineSymbol *indexSymbol; + double indexInterval; + QgsRenderContext *context; +}; + +CPLErr _rasterContourWriter( double dfLevel, int nPoints, double *padfX, double *padfY, void *ptr ) +{ + Q_UNUSED( dfLevel ) + ContourWriterData *crData = static_cast( ptr ); + QPolygonF polygon( nPoints ); + QPointF *d = polygon.data(); + for ( int i = 0; i < nPoints; ++i ) + { + d[i] = QPointF( padfX[i] * crData->scaleX, padfY[i] * crData->scaleY ); + } + + if ( crData->indexSymbol && !qgsDoubleNear( crData->indexInterval, 0 ) && qgsDoubleNear( fmod( dfLevel, crData->indexInterval ), 0 ) ) + crData->indexSymbol->renderPolyline( polygon, nullptr, *crData->context ); + else + crData->symbol->renderPolyline( polygon, nullptr, *crData->context ); + return CE_None; +} + +QgsRasterBlock *QgsRasterContourRenderer::block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback ) +{ + Q_UNUSED( bandNo ) + + std::unique_ptr< QgsRasterBlock > outputBlock( new QgsRasterBlock() ); + if ( !mInput || !mContourSymbol ) + { + return outputBlock.release(); + } + + int inputWidth = static_cast( round( width / mDownscale ) ); + int inputHeight = static_cast( round( height / mDownscale ) ); + + std::unique_ptr< QgsRasterBlock > inputBlock( mInput->block( mInputBand, extent, inputWidth, inputHeight, feedback ) ); + if ( !inputBlock || inputBlock->isEmpty() ) + { + QgsDebugMsg( QStringLiteral( "No raster data!" ) ); + return outputBlock.release(); + } + + if ( !inputBlock->convert( Qgis::Float64 ) ) // contouring algorithm requires double + return outputBlock.release(); + double *scanline = reinterpret_cast( inputBlock->bits() ); + + QImage img( width, height, QImage::Format_ARGB32_Premultiplied ); + img.fill( Qt::transparent ); + + QPainter p( &img ); + p.setRenderHint( QPainter::Antialiasing ); + + QgsRenderContext context = QgsRenderContext::fromQPainter( &p ); + + ContourWriterData crData; + crData.painter = &p; + crData.scaleX = width / double( inputWidth ); + crData.scaleY = height / double( inputHeight ); + crData.symbol = mContourSymbol.get(); + crData.indexSymbol = mContourIndexSymbol.get(); + crData.indexInterval = mContourIndexInterval; + crData.context = &context; + + crData.symbol->startRender( context ); + if ( crData.indexSymbol ) + crData.indexSymbol->startRender( context ); + + double contourBase = 0.; + GDALContourGeneratorH cg = GDAL_CG_Create( inputBlock->width(), inputBlock->height(), + inputBlock->hasNoDataValue(), inputBlock->noDataValue(), + mContourInterval, contourBase, + _rasterContourWriter, static_cast( &crData ) ); + for ( int i = 0; i < inputHeight; ++i ) + { + if ( feedback && feedback->isCanceled() ) + break; + + GDAL_CG_FeedLine( cg, scanline ); + scanline += inputWidth; + } + GDAL_CG_Destroy( cg ); + + crData.symbol->stopRender( context ); + if ( crData.indexSymbol ) + crData.indexSymbol->stopRender( context ); + + p.end(); + + outputBlock->setImage( &img ); + return outputBlock.release(); +} + +QList QgsRasterContourRenderer::usesBands() const +{ + QList bandList; + if ( mInputBand != -1 ) + { + bandList << mInputBand; + } + return bandList; +} + +void QgsRasterContourRenderer::setContourSymbol( QgsLineSymbol *symbol ) +{ + mContourSymbol.reset( symbol ); +} + +void QgsRasterContourRenderer::setContourIndexSymbol( QgsLineSymbol *symbol ) +{ + mContourIndexSymbol.reset( symbol ); +} diff --git a/src/core/raster/qgsrastercontourrenderer.h b/src/core/raster/qgsrastercontourrenderer.h new file mode 100644 index 000000000000..9429918af0de --- /dev/null +++ b/src/core/raster/qgsrastercontourrenderer.h @@ -0,0 +1,110 @@ +/*************************************************************************** + qgsrastercontourrenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSRASTERCONTOURRENDERER_H +#define QGSRASTERCONTOURRENDERER_H + + +#include "qgsrasterrenderer.h" + +class QgsLineSymbol; + +/** + * \ingroup core + * Raster renderer that generates contours on the fly for a source raster band. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsRasterContourRenderer : public QgsRasterRenderer +{ + public: + //! Creates a contour renderer + explicit QgsRasterContourRenderer( QgsRasterInterface *input ); + ~QgsRasterContourRenderer() override; + + //! QgsRasterContourRenderer cannot be copied. Use clone() instead. + QgsRasterContourRenderer( const QgsRasterContourRenderer & ) = delete; + //! QgsRasterContourRenderer cannot be copied. Use clone() instead. + const QgsRasterContourRenderer &operator=( const QgsRasterContourRenderer & ) = delete; + + QgsRasterContourRenderer *clone() const override SIP_FACTORY; + + //! Creates an instance of the renderer based on definition from XML (used by renderer registry) + static QgsRasterRenderer *create( const QDomElement &elem, QgsRasterInterface *input ) SIP_FACTORY; + + void writeXml( QDomDocument &doc, QDomElement &parentElem ) const override; + + QgsRasterBlock *block( int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback = nullptr ) override SIP_FACTORY; + + QList usesBands() const override; + + // + + //! Returns the number of the input raster band + int inputBand() const { return mInputBand; } + //! Sets the number of the input raster band + void setInputBand( int band ) { mInputBand = band; } + + //! Returns the interval of contour lines generation + double contourInterval() const { return mContourInterval; } + //! Sets the interval of contour lines generation + void setContourInterval( double interval ) { mContourInterval = interval; } + + //! Returns the symbol used for contour lines + QgsLineSymbol *contourSymbol() const { return mContourSymbol.get(); } + //! Sets the symbol used for contour lines. Takes ownership of the passed symbol + void setContourSymbol( QgsLineSymbol *symbol SIP_TRANSFER ); + + //! Returns the interval of index contour lines (index contour lines are typical further apart and with a wider line symbol) + double contourIndexInterval() const { return mContourIndexInterval; } + //! Sets the interval of index contour lines (index contour lines are typical further apart and with a wider line symbol) + void setContourIndexInterval( double interval ) { mContourIndexInterval = interval; } + + //! Returns the symbol of index contour lines + QgsLineSymbol *contourIndexSymbol() const { return mContourIndexSymbol.get(); } + //! Sets the symbol of index contour lines + void setContourIndexSymbol( QgsLineSymbol *symbol SIP_TRANSFER ); + + /** + * Returns by how much the renderer will scale down the request to the data provider. + * For example, for a raster block 1000x500 with downscale 10, the renderer will request raster 100x50 from provider. + * Higher downscale makes contour lines more simplified (at the expense of losing some detail). + * The value of one means there will be no downscaling. + */ + double downscale() const { return mDownscale; } + + /** + * Sets by how much the renderer will scale down the request to the data provider. + * \see downscale() + */ + void setDownscale( double scale ) { mDownscale = scale; } + + private: + +#ifdef SIP_RUN + QgsRasterContourRenderer( const QgsRasterContourRenderer & ); + const QgsRasterContourRenderer &operator=( const QgsRasterContourRenderer & ); +#endif + + std::unique_ptr mContourSymbol; // should not be null + std::unique_ptr mContourIndexSymbol; // may be null + double mDownscale = 8.; + double mContourInterval = 100.; + double mContourIndexInterval = 0.; + int mInputBand = 1; +}; + + +#endif // QGSRASTERCONTOURRENDERER_H diff --git a/src/core/raster/qgsrasterdataprovider.cpp b/src/core/raster/qgsrasterdataprovider.cpp index 9b2dc3843727..b2a5290694be 100644 --- a/src/core/raster/qgsrasterdataprovider.cpp +++ b/src/core/raster/qgsrasterdataprovider.cpp @@ -116,7 +116,7 @@ QgsRasterBlock *QgsRasterDataProvider::block( int bandNo, QgsRectangle const &b return block.release(); } - // If lower source resolution is used, the extent must beS aligned to original + // If lower source resolution is used, the extent must be aligned to original // resolution to avoid possible shift due to resampling if ( tmpXRes > xRes ) { @@ -413,6 +413,11 @@ QgsRasterDataProviderTemporalCapabilities *QgsRasterDataProvider::temporalCapabi return mTemporalCapabilities.get(); } +const QgsRasterDataProviderTemporalCapabilities *QgsRasterDataProvider::temporalCapabilities() const +{ + return mTemporalCapabilities.get(); +} + QgsRasterDataProvider *QgsRasterDataProvider::create( const QString &providerKey, const QString &uri, const QString &format, int nBands, diff --git a/src/core/raster/qgsrasterdataprovider.h b/src/core/raster/qgsrasterdataprovider.h index ae3eb7e5b7c0..27ed2c9cbb65 100644 --- a/src/core/raster/qgsrasterdataprovider.h +++ b/src/core/raster/qgsrasterdataprovider.h @@ -33,8 +33,8 @@ #include "qgscolorrampshader.h" #include "qgsdataprovider.h" -#include "qgsfields.h" #include "qgsraster.h" +#include "qgsfields.h" #include "qgsrasterinterface.h" #include "qgsrasterpyramid.h" #include "qgsrasterrange.h" @@ -134,6 +134,13 @@ class CORE_EXPORT QgsRasterDataProvider : public QgsDataProvider, public QgsRast //! Returns data type for the band specified by number Qgis::DataType dataType( int bandNo ) const override = 0; + /** + * Returns the fields of the raster layer for data providers that expose them, + * the default implementation returns an empty list. + * \since QGIS 3.14 + */ + virtual QgsFields fields() const { return QgsFields(); }; + /** * Returns source data type for the band specified by number, * source data type may be shorter than dataType @@ -262,6 +269,7 @@ class CORE_EXPORT QgsRasterDataProvider : public QgsDataProvider, public QgsRast } QgsRasterDataProviderTemporalCapabilities *temporalCapabilities() override; + const QgsRasterDataProviderTemporalCapabilities *temporalCapabilities() const override SIP_SKIP; //! \brief Returns whether the provider supplies a legend graphic virtual bool supportsLegendGraphic() const { return false; } diff --git a/src/core/raster/qgsrasterdataprovidertemporalcapabilities.cpp b/src/core/raster/qgsrasterdataprovidertemporalcapabilities.cpp index 426173cc9691..e9386c040759 100644 --- a/src/core/raster/qgsrasterdataprovidertemporalcapabilities.cpp +++ b/src/core/raster/qgsrasterdataprovidertemporalcapabilities.cpp @@ -35,16 +35,6 @@ const QgsDateTimeRange &QgsRasterDataProviderTemporalCapabilities::availableTemp return mAvailableTemporalRange; } -void QgsRasterDataProviderTemporalCapabilities::setEnableTime( bool enabled ) -{ - mEnableTime = enabled; -} - -bool QgsRasterDataProviderTemporalCapabilities::isTimeEnabled() const -{ - return mEnableTime; -} - void QgsRasterDataProviderTemporalCapabilities::setAvailableReferenceTemporalRange( const QgsDateTimeRange &dateTimeRange ) { if ( !hasTemporalCapabilities() ) @@ -60,8 +50,7 @@ const QgsDateTimeRange &QgsRasterDataProviderTemporalCapabilities::availableRefe void QgsRasterDataProviderTemporalCapabilities::setRequestedTemporalRange( const QgsDateTimeRange &dateTimeRange ) { - if ( mAvailableTemporalRange.contains( dateTimeRange ) ) - mRequestedRange = dateTimeRange; + mRequestedRange = dateTimeRange; } const QgsDateTimeRange &QgsRasterDataProviderTemporalCapabilities::requestedTemporalRange() const @@ -69,27 +58,6 @@ const QgsDateTimeRange &QgsRasterDataProviderTemporalCapabilities::requestedTemp return mRequestedRange; } -void QgsRasterDataProviderTemporalCapabilities::setRequestedReferenceTemporalRange( const QgsDateTimeRange &dateTimeRange ) -{ - if ( mAvailableReferenceRange.contains( dateTimeRange ) ) - mRequestedReferenceRange = dateTimeRange; -} - -const QgsDateTimeRange &QgsRasterDataProviderTemporalCapabilities::requestedReferenceTemporalRange() const -{ - return mRequestedReferenceRange; -} - -void QgsRasterDataProviderTemporalCapabilities::setReferenceEnable( bool enabled ) -{ - mReferenceEnable = enabled; -} - -bool QgsRasterDataProviderTemporalCapabilities::isReferenceEnable() const -{ - return mReferenceEnable; -} - QgsRasterDataProviderTemporalCapabilities::IntervalHandlingMethod QgsRasterDataProviderTemporalCapabilities::intervalHandlingMethod() const { return mIntervalMatchMethod; diff --git a/src/core/raster/qgsrasterdataprovidertemporalcapabilities.h b/src/core/raster/qgsrasterdataprovidertemporalcapabilities.h index 87285f0d1ea8..62c6e35f0706 100644 --- a/src/core/raster/qgsrasterdataprovidertemporalcapabilities.h +++ b/src/core/raster/qgsrasterdataprovidertemporalcapabilities.h @@ -53,8 +53,10 @@ class CORE_EXPORT QgsRasterDataProviderTemporalCapabilities : public QgsDataProv MatchUsingWholeRange, //!< Use an exact match to the whole temporal range MatchExactUsingStartOfRange, //!< Match the start of the temporal range to a corresponding layer or band, and only use exact matching results MatchExactUsingEndOfRange, //!< Match the end of the temporal range to a corresponding layer or band, and only use exact matching results + FindClosestMatchToStartOfRange, //! Match the start of the temporal range to the least previous closest datetime. + FindClosestMatchToEndOfRange //! Match the end of the temporal range to the least previous closest datetime. }; - // TODO -- add other methods, like "FindClosestMatchToStartOfRange", "FindClosestMatchToEndOfRange", etc + // TODO -- add other methods /** * Returns the desired method to use when resolving a temporal interval to matching @@ -108,58 +110,6 @@ class CORE_EXPORT QgsRasterDataProviderTemporalCapabilities : public QgsDataProv */ const QgsDateTimeRange &requestedTemporalRange() const; - /** - * Sets the requested reference temporal \a range to retrieve when - * returning data from the associated data provider. - * - * \note this is not normally manually set, and is intended for use by - * QgsRasterLayerRenderer to automatically set the requested temporal range - * on a clone of the data provider during a render job. - * - * \see requestedReferenceTemporalRange() - */ - void setRequestedReferenceTemporalRange( const QgsDateTimeRange &range ); - - /** - * Returns the requested reference temporal range. - * Intended to be used by the provider in fetching data. - * - * \see setRequestedReferenceTemporalRange() - */ - const QgsDateTimeRange &requestedReferenceTemporalRange() const; - - /** - * Sets the time enabled status. - * This enables whether time part in the temporal range should be - * used when updated the temporal range of these capabilities. - * - * This is useful in some temporal layers who use dates only. - * - * \see isTimeEnabled() - */ - void setEnableTime( bool enabled ); - - /** - * Returns the temporal property status. - * - * \see setEnableTime() - */ - bool isTimeEnabled() const; - - /** - * Sets the usage status of the reference range. - * - * \see isReferenceEnable() - */ - void setReferenceEnable( bool enabled ); - - /** - * Returns the enabled status of the reference range. - * - * \see setReferenceEnable() - */ - bool isReferenceEnable() const; - private: /** @@ -184,31 +134,14 @@ class CORE_EXPORT QgsRasterDataProviderTemporalCapabilities : public QgsDataProv */ QgsDateTimeRange mAvailableTemporalRange; - /** - * If the stored time part in temporal ranges should be taked into account. - * - * This is to enable data providers that use dates only and no datetime, to - * configure their temporal properties to consider their state. - * - * eg. some WMS-T providers only require date with "YYYY-MM-DD" format with - * no time part. - */ - bool mEnableTime = true; - //! Represents the requested temporal range. QgsDateTimeRange mRequestedRange; - //! Represents the requested reference temporal range. - QgsDateTimeRange mRequestedReferenceRange; - /** * Stores the available reference temporal range */ QgsDateTimeRange mAvailableReferenceRange; - //! If reference range has been enabled to be used in these properties - bool mReferenceEnable = false; - //! Interval handling method IntervalHandlingMethod mIntervalMatchMethod = MatchUsingWholeRange; diff --git a/src/core/raster/qgsrasterlayer.cpp b/src/core/raster/qgsrasterlayer.cpp index b6ec07bf4802..07420ab5decb 100644 --- a/src/core/raster/qgsrasterlayer.cpp +++ b/src/core/raster/qgsrasterlayer.cpp @@ -101,6 +101,7 @@ QgsRasterLayer::QgsRasterLayer() : QgsMapLayer( QgsMapLayerType::RasterLayer ) , QSTRING_NOT_SET( QStringLiteral( "Not Set" ) ) , TRSTRING_NOT_SET( tr( "Not Set" ) ) + , mTemporalProperties( new QgsRasterLayerTemporalProperties( this ) ) { init(); @@ -115,6 +116,7 @@ QgsRasterLayer::QgsRasterLayer( const QString &uri, // Constant that signals property not used. , QSTRING_NOT_SET( QStringLiteral( "Not Set" ) ) , TRSTRING_NOT_SET( tr( "Not Set" ) ) + , mTemporalProperties( new QgsRasterLayerTemporalProperties( this ) ) { mShouldValidateCrs = !options.skipCrsValidation; @@ -576,8 +578,6 @@ void QgsRasterLayer::init() //Initialize the last view port structure, should really be a class mLastViewPort.mWidth = 0; mLastViewPort.mHeight = 0; - - mTemporalProperties = new QgsRasterLayerTemporalProperties( this ); } void QgsRasterLayer::setDataProvider( QString const &provider, const QgsDataProvider::ProviderOptions &options ) @@ -866,6 +866,9 @@ void QgsRasterLayer::setDataSource( const QString &dataSource, const QString &ba setDataProvider( provider, options ); + if ( mDataProvider ) + mDataProvider->setDataSourceUri( mDataSource ); + if ( mValid ) { mTemporalProperties->setDefaultsFromDataProviderTemporalCapabilities( mDataProvider->temporalCapabilities() ); @@ -1334,7 +1337,7 @@ bool QgsRasterLayer::defaultContrastEnhancementSettings( if ( key.isEmpty() ) { - QgsDebugMsg( QStringLiteral( "No default contrast enhancement for this drawing style" ) ); + QgsDebugMsgLevel( QStringLiteral( "No default contrast enhancement for this drawing style" ), 2 ); myAlgorithm = QgsContrastEnhancement::contrastEnhancementAlgorithmFromString( QString() ); myLimits = QgsRasterMinMaxOrigin::limitsFromString( QString() ); return false; diff --git a/src/core/raster/qgsrasterlayerrenderer.cpp b/src/core/raster/qgsrasterlayerrenderer.cpp index 5c6934917df3..445607a10bf9 100644 --- a/src/core/raster/qgsrasterlayerrenderer.cpp +++ b/src/core/raster/qgsrasterlayerrenderer.cpp @@ -240,12 +240,16 @@ QgsRasterLayerRenderer::QgsRasterLayerRenderer( QgsRasterLayer *layer, QgsRender if ( mPipe->provider()->temporalCapabilities() ) { mPipe->provider()->temporalCapabilities()->setRequestedTemporalRange( rendererContext.temporalRange() ); - mPipe->provider()->temporalCapabilities()->setRequestedReferenceTemporalRange( layer->temporalProperties()->referenceTemporalRange() ); mPipe->provider()->temporalCapabilities()->setIntervalHandlingMethod( layer->temporalProperties()->intervalHandlingMethod() ); } break; } } + else if ( mPipe->provider()->temporalCapabilities() ) + { + mPipe->provider()->temporalCapabilities()->setRequestedTemporalRange( QgsDateTimeRange() ); + mPipe->provider()->temporalCapabilities()->setIntervalHandlingMethod( layer->temporalProperties()->intervalHandlingMethod() ); + } } QgsRasterLayerRenderer::~QgsRasterLayerRenderer() diff --git a/src/core/raster/qgsrasterlayertemporalproperties.cpp b/src/core/raster/qgsrasterlayertemporalproperties.cpp index cb3eb988cc51..4ea66208225e 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.cpp +++ b/src/core/raster/qgsrasterlayertemporalproperties.cpp @@ -73,46 +73,6 @@ const QgsDateTimeRange &QgsRasterLayerTemporalProperties::fixedTemporalRange() c return mFixedRange; } -void QgsRasterLayerTemporalProperties::setFixedReferenceTemporalRange( const QgsDateTimeRange &range ) -{ - mFixedReferenceRange = range; -} - -const QgsDateTimeRange &QgsRasterLayerTemporalProperties::fixedReferenceTemporalRange() const -{ - return mFixedReferenceRange; -} - -void QgsRasterLayerTemporalProperties::setTemporalRange( const QgsDateTimeRange &dateTimeRange ) -{ - // Don't set temporal range outside fixed temporal range limits, - // instead set equal to the fixed temporal range - - if ( !isActive() ) - setIsActive( true ); - - if ( mFixedRange.contains( dateTimeRange ) ) - mRange = dateTimeRange; - else - mRange = mFixedRange; -} - -const QgsDateTimeRange &QgsRasterLayerTemporalProperties::temporalRange() const -{ - return mRange; -} - -void QgsRasterLayerTemporalProperties::setReferenceTemporalRange( const QgsDateTimeRange &dateTimeRange ) -{ - if ( mFixedReferenceRange.contains( dateTimeRange ) ) - mReferenceRange = dateTimeRange; -} - -const QgsDateTimeRange &QgsRasterLayerTemporalProperties::referenceTemporalRange() const -{ - return mReferenceRange; -} - bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, const QgsReadWriteContext &context ) { Q_UNUSED( context ) @@ -120,37 +80,22 @@ bool QgsRasterLayerTemporalProperties::readXml( const QDomElement &element, cons QDomElement temporalNode = element.firstChildElement( QStringLiteral( "temporal" ) ); + setIsActive( temporalNode.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt() ); + mMode = static_cast< TemporalMode >( temporalNode.attribute( QStringLiteral( "mode" ), QStringLiteral( "0" ) ). toInt() ); mIntervalHandlingMethod = static_cast< QgsRasterDataProviderTemporalCapabilities::IntervalHandlingMethod >( temporalNode.attribute( QStringLiteral( "fetchMode" ), QStringLiteral( "0" ) ). toInt() ); - int sourceIndex = temporalNode.attribute( QStringLiteral( "source" ), QStringLiteral( "0" ) ).toInt(); + QDomNode rangeElement = temporalNode.namedItem( QStringLiteral( "fixedRange" ) ); - if ( sourceIndex == 0 ) - setTemporalSource( TemporalSource::Layer ); - else - setTemporalSource( TemporalSource::Project ); - - for ( QString rangeString : { "fixedRange", "fixedReferenceRange", "normalRange", "referenceRange" } ) - { - QDomNode rangeElement = temporalNode.namedItem( rangeString ); + QDomNode begin = rangeElement.namedItem( QStringLiteral( "start" ) ); + QDomNode end = rangeElement.namedItem( QStringLiteral( "end" ) ); - QDomNode begin = rangeElement.namedItem( QStringLiteral( "start" ) ); - QDomNode end = rangeElement.namedItem( QStringLiteral( "end" ) ); + QDateTime beginDate = QDateTime::fromString( begin.toElement().text(), Qt::ISODate ); + QDateTime endDate = QDateTime::fromString( end.toElement().text(), Qt::ISODate ); - QDateTime beginDate = QDateTime::fromString( begin.toElement().text(), Qt::ISODate ); - QDateTime endDate = QDateTime::fromString( end.toElement().text(), Qt::ISODate ); + QgsDateTimeRange range = QgsDateTimeRange( beginDate, endDate ); + setFixedTemporalRange( range ); - QgsDateTimeRange range = QgsDateTimeRange( beginDate, endDate ); - - if ( rangeString == QLatin1String( "fixedRange" ) ) - setFixedTemporalRange( range ); - if ( rangeString == QLatin1String( "normalRange" ) ) - setTemporalRange( range ); - if ( rangeString == QLatin1String( "fixedReferenceRange" ) ) - setFixedReferenceTemporalRange( range ); - if ( rangeString == QLatin1String( "referenceRange" ) ) - setReferenceTemporalRange( range ); - } return true; } @@ -161,39 +106,24 @@ QDomElement QgsRasterLayerTemporalProperties::writeXml( QDomElement &element, QD return QDomElement(); QDomElement temporalElement = document.createElement( QStringLiteral( "temporal" ) ); + temporalElement.setAttribute( QStringLiteral( "enabled" ), isActive() ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); temporalElement.setAttribute( QStringLiteral( "mode" ), QString::number( mMode ) ); - temporalElement.setAttribute( QStringLiteral( "source" ), QString::number( temporalSource() ) ); temporalElement.setAttribute( QStringLiteral( "fetchMode" ), QString::number( mIntervalHandlingMethod ) ); - for ( QString rangeString : { "fixedRange", "fixedReferenceRange", "normalRange", "referenceRange" } ) - { - QgsDateTimeRange range; - - if ( rangeString == QLatin1String( "fixedRange" ) ) - range = mFixedRange; - if ( rangeString == QLatin1String( "fixedReferenceRange" ) ) - range = mFixedReferenceRange; - if ( rangeString == QLatin1String( "normalRange" ) ) - range = mRange; - if ( rangeString == QLatin1String( "referenceRange" ) ) - range = mReferenceRange; + QDomElement rangeElement = document.createElement( QStringLiteral( "fixedRange" ) ); - QDomElement rangeElement = document.createElement( rangeString ); + QDomElement startElement = document.createElement( QStringLiteral( "start" ) ); + QDomElement endElement = document.createElement( QStringLiteral( "end" ) ); - QDomElement startElement = document.createElement( QStringLiteral( "start" ) ); - QDomElement endElement = document.createElement( QStringLiteral( "end" ) ); + QDomText startText = document.createTextNode( mFixedRange.begin().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); + QDomText endText = document.createTextNode( mFixedRange.end().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); + startElement.appendChild( startText ); + endElement.appendChild( endText ); + rangeElement.appendChild( startElement ); + rangeElement.appendChild( endElement ); - QDomText startText = document.createTextNode( range.begin().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); - QDomText endText = document.createTextNode( range.end().toTimeSpec( Qt::OffsetFromUTC ).toString( Qt::ISODate ) ); + temporalElement.appendChild( rangeElement ); - startElement.appendChild( startText ); - endElement.appendChild( endText ); - - rangeElement.appendChild( startElement ); - rangeElement.appendChild( endElement ); - - temporalElement.appendChild( rangeElement ); - } element.appendChild( temporalElement ); return element; @@ -205,7 +135,6 @@ void QgsRasterLayerTemporalProperties::setDefaultsFromDataProviderTemporalCapabi { setIsActive( rasterCaps->hasTemporalCapabilities() ); setFixedTemporalRange( rasterCaps->availableTemporalRange() ); - setFixedReferenceTemporalRange( rasterCaps->availableReferenceTemporalRange() ); if ( rasterCaps->hasTemporalCapabilities() ) { diff --git a/src/core/raster/qgsrasterlayertemporalproperties.h b/src/core/raster/qgsrasterlayertemporalproperties.h index 3d3e61351a53..208f2ebbe7cb 100644 --- a/src/core/raster/qgsrasterlayertemporalproperties.h +++ b/src/core/raster/qgsrasterlayertemporalproperties.h @@ -108,62 +108,6 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP **/ const QgsDateTimeRange &fixedTemporalRange() const; - /** - * Sets a fixed reference temporal \a range to apply to the whole layer. All bands from - * the raster layer will be rendered whenever the current datetime range of - * a render context intersects the specified \a range. - * - * \warning This setting is only effective when mode() is - * QgsRasterLayerTemporalProperties::ModeFixedTemporalRange - * - * \see fixedReferenceTemporalRange() - */ - void setFixedReferenceTemporalRange( const QgsDateTimeRange &range ); - - /** - * Returns the fixed reference temporal range for the layer. - * - * \warning To be used only when mode() is - * QgsRasterLayerTemporalProperties::ModeFixedTemporalRange - * - * \see setFixedReferenceTemporalRange() - **/ - const QgsDateTimeRange &fixedReferenceTemporalRange() const; - - /** - * Sets the current active datetime range for the temporal properties. - * - * \note This can be set by user, through raster layer properties widget. - * - * \see temporalRange() - */ - void setTemporalRange( const QgsDateTimeRange &dateTimeRange ); - - /** - * Returns the current active datetime range for these temporal properties. - * - * \see setTemporalRange() - */ - const QgsDateTimeRange &temporalRange() const; - - /** - * Sets the current active reference datetime range for the temporal properties. - * - * This will be used by bi-temporal data. - * - * \note This can be set by user, through raster layer properties widget. - * - * \see referenceTemporalRange() - */ - void setReferenceTemporalRange( const QgsDateTimeRange &dateTimeRange ); - - /** - * Returns the current active reference datetime range for these temporal properties. - * - * \see setReferenceTemporalRange() - */ - const QgsDateTimeRange &referenceTemporalRange() const; - QDomElement writeXml( QDomElement &element, QDomDocument &doc, const QgsReadWriteContext &context ) override; bool readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; @@ -180,18 +124,6 @@ class CORE_EXPORT QgsRasterLayerTemporalProperties : public QgsMapLayerTemporalP //! Represents fixed temporal range. QgsDateTimeRange mFixedRange; - - //! Represents fixed reference temporal range member. - QgsDateTimeRange mFixedReferenceRange; - - /** - * Stores reference temporal range - */ - QgsDateTimeRange mReferenceRange; - - //! Represents current active datetime range member. - QgsDateTimeRange mRange; - }; #endif // QGSRASTERLAYERTEMPORALPROPERTIES_H diff --git a/src/core/raster/qgsrasterrendererregistry.cpp b/src/core/raster/qgsrasterrendererregistry.cpp index a1bb213466e4..be8c2739b36b 100644 --- a/src/core/raster/qgsrasterrendererregistry.cpp +++ b/src/core/raster/qgsrasterrendererregistry.cpp @@ -21,6 +21,7 @@ #include "qgsrastertransparency.h" #include "qgsmultibandcolorrenderer.h" #include "qgspalettedrasterrenderer.h" +#include "qgsrastercontourrenderer.h" #include "qgssinglebandcolordatarenderer.h" #include "qgssinglebandgrayrenderer.h" #include "qgssinglebandpseudocolorrenderer.h" @@ -59,6 +60,8 @@ QgsRasterRendererRegistry::QgsRasterRendererRegistry() QgsSingleBandColorDataRenderer::create, nullptr ) ); insert( QgsRasterRendererRegistryEntry( QStringLiteral( "hillshade" ), QObject::tr( "Hillshade" ), QgsHillshadeRenderer::create, nullptr ) ); + insert( QgsRasterRendererRegistryEntry( QStringLiteral( "contour" ), QObject::tr( "Contours" ), + QgsRasterContourRenderer::create, nullptr ) ); } void QgsRasterRendererRegistry::insert( const QgsRasterRendererRegistryEntry &entry ) diff --git a/src/core/scalebar/qgsdoubleboxscalebarrenderer.cpp b/src/core/scalebar/qgsdoubleboxscalebarrenderer.cpp index f8a7cf8ea363..903157fd89cd 100644 --- a/src/core/scalebar/qgsdoubleboxscalebarrenderer.cpp +++ b/src/core/scalebar/qgsdoubleboxscalebarrenderer.cpp @@ -17,9 +17,44 @@ #include "qgsdoubleboxscalebarrenderer.h" #include "qgsscalebarsettings.h" #include "qgslayoututils.h" +#include "qgssymbol.h" #include #include +QString QgsDoubleBoxScaleBarRenderer::id() const +{ + return QStringLiteral( "Double Box" ); +} + +QString QgsDoubleBoxScaleBarRenderer::visibleName() const +{ + return QObject::tr( "Double Box" ); +} + +QgsScaleBarRenderer::Flags QgsDoubleBoxScaleBarRenderer::flags() const +{ + return Flag::FlagUsesLineSymbol | + Flag::FlagUsesFillSymbol | + Flag::FlagUsesAlternateFillSymbol | + Flag::FlagRespectsUnits | + Flag::FlagRespectsMapUnitsPerScaleBarUnit | + Flag::FlagUsesUnitLabel | + Flag::FlagUsesSegments | + Flag::FlagUsesLabelBarSpace | + Flag::FlagUsesLabelVerticalPlacement | + Flag::FlagUsesLabelHorizontalPlacement; +} + +int QgsDoubleBoxScaleBarRenderer::sortKey() const +{ + return 2; +} + +QgsDoubleBoxScaleBarRenderer *QgsDoubleBoxScaleBarRenderer::clone() const +{ + return new QgsDoubleBoxScaleBarRenderer( *this ); +} + void QgsDoubleBoxScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const { if ( !context.painter() ) @@ -28,62 +63,127 @@ void QgsDoubleBoxScaleBarRenderer::draw( QgsRenderContext &context, const QgsSca } QPainter *painter = context.painter(); - double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); - double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); - QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); - double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); - double segmentHeight = context.convertToPainterUnits( settings.height() / 2, QgsUnitTypes::RenderMillimeters ); + const double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); + const double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); + const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); + const double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); + const double segmentHeight = context.convertToPainterUnits( settings.height() / 2, QgsUnitTypes::RenderMillimeters ); painter->save(); if ( context.flags() & QgsRenderContext::Antialiasing ) painter->setRenderHint( QPainter::Antialiasing, true ); - QPen pen = settings.pen(); - pen.setWidthF( context.convertToPainterUnits( pen.widthF(), QgsUnitTypes::RenderMillimeters ) ); - painter->setPen( pen ); + std::unique_ptr< QgsLineSymbol > lineSymbol( settings.lineSymbol()->clone() ); + lineSymbol->startRender( context ); + + std::unique_ptr< QgsFillSymbol > fillSymbol1( settings.fillSymbol()->clone() ); + fillSymbol1->startRender( context ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2( settings.alternateFillSymbol()->clone() ); + fillSymbol2->startRender( context ); + + painter->setPen( Qt::NoPen ); + painter->setBrush( Qt::NoBrush ); bool useColor = true; //alternate brush color/white - double xOffset = firstLabelXOffset( settings, context, scaleContext ); + const double xOffset = firstLabelXOffset( settings, context, scaleContext ); - QList positions = segmentPositions( scaleContext, settings ); - QList widths = segmentWidths( scaleContext, settings ); + const QList positions = segmentPositions( context, scaleContext, settings ); + const QList widths = segmentWidths( scaleContext, settings ); + double minX = 0; + double maxX = 0; + QgsFillSymbol *currentSymbol = nullptr; for ( int i = 0; i < positions.size(); ++i ) { //draw top half if ( useColor ) { - painter->setBrush( settings.brush() ); + currentSymbol = fillSymbol1.get(); } - else //secondary color + else //secondary symbol { - painter->setBrush( settings.brush2() ); + currentSymbol = fillSymbol2.get(); } - QRectF segmentRectTop( context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset, - barTopPosition, - context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ), segmentHeight ); + const double thisX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + const double thisWidth = context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ); + + if ( i == 0 ) + minX = thisX; + if ( i == positions.size() - 1 ) + maxX = thisX + thisWidth; + + const QRectF segmentRectTop( thisX, barTopPosition, thisWidth, segmentHeight ); + currentSymbol->renderPolygon( QPolygonF() + << segmentRectTop.topLeft() + << segmentRectTop.topRight() + << segmentRectTop.bottomRight() + << segmentRectTop.bottomLeft() + << segmentRectTop.topLeft(), + nullptr, nullptr, context ); painter->drawRect( segmentRectTop ); //draw bottom half if ( useColor ) { - //secondary color - painter->setBrush( settings.brush2() ); + //secondary symbol + currentSymbol = fillSymbol2.get(); } - else //primary color + else //primary symbol { - painter->setBrush( settings.brush() ); + currentSymbol = fillSymbol1.get(); ; } - QRectF segmentRectBottom( context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset, - barTopPosition + segmentHeight, - context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ), segmentHeight ); - painter->drawRect( segmentRectBottom ); + const QRectF segmentRectBottom( thisX, barTopPosition + segmentHeight, thisWidth, segmentHeight ); + + currentSymbol->renderPolygon( QPolygonF() + << segmentRectBottom.topLeft() + << segmentRectBottom.topRight() + << segmentRectBottom.bottomRight() + << segmentRectBottom.bottomLeft() + << segmentRectBottom.topLeft(), + nullptr, nullptr, context ); useColor = !useColor; } + + // and then the lines + // note that we do this layer-by-layer, to avoid ugliness where the lines touch the outer rect + for ( int layer = 0; layer < lineSymbol->symbolLayerCount(); ++layer ) + { + // vertical lines + for ( int i = 1; i < positions.size(); ++i ) + { + const double lineX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + lineSymbol->renderPolyline( QPolygonF() + << QPointF( lineX, barTopPosition ) + << QPointF( lineX, barTopPosition + segmentHeight * 2 ), + nullptr, context, layer ); + } + + // middle horizontal line + lineSymbol->renderPolyline( QPolygonF() + << QPointF( minX, barTopPosition + segmentHeight ) + << QPointF( maxX, barTopPosition + segmentHeight ), + nullptr, context, layer ); + + + // outside line + lineSymbol->renderPolyline( QPolygonF() + << QPointF( minX, barTopPosition ) + << QPointF( maxX, barTopPosition ) + << QPointF( maxX, barTopPosition + segmentHeight * 2 ) + << QPointF( minX, barTopPosition + segmentHeight * 2 ) + << QPointF( minX, barTopPosition ), + nullptr, context, layer ); + } + + lineSymbol->stopRender( context ); + fillSymbol1->stopRender( context ); + fillSymbol2->stopRender( context ); + painter->restore(); //draw labels using the default method diff --git a/src/core/scalebar/qgsdoubleboxscalebarrenderer.h b/src/core/scalebar/qgsdoubleboxscalebarrenderer.h index 1971823758a1..7c0f1fa4e3a0 100644 --- a/src/core/scalebar/qgsdoubleboxscalebarrenderer.h +++ b/src/core/scalebar/qgsdoubleboxscalebarrenderer.h @@ -36,7 +36,11 @@ class CORE_EXPORT QgsDoubleBoxScaleBarRenderer: public QgsScaleBarRenderer */ QgsDoubleBoxScaleBarRenderer() = default; - QString name() const override { return QStringLiteral( "Double Box" ); } + QString id() const override; + QString visibleName() const override; + Flags flags() const override; + int sortKey() const override; + QgsDoubleBoxScaleBarRenderer *clone() const override SIP_FACTORY; void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, diff --git a/src/core/scalebar/qgshollowscalebarrenderer.cpp b/src/core/scalebar/qgshollowscalebarrenderer.cpp new file mode 100644 index 000000000000..ec8038911ba0 --- /dev/null +++ b/src/core/scalebar/qgshollowscalebarrenderer.cpp @@ -0,0 +1,197 @@ +/*************************************************************************** + qgshollowscalebarrenderer.cpp + -------------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgshollowscalebarrenderer.h" +#include "qgsscalebarsettings.h" +#include "qgslayoututils.h" +#include "qgssymbol.h" +#include "qgsfillsymbollayer.h" +#include +#include + +QString QgsHollowScaleBarRenderer::id() const +{ + return QStringLiteral( "hollow" ); +} + +QString QgsHollowScaleBarRenderer::visibleName() const +{ + return QObject::tr( "Hollow" ); +} + +QgsScaleBarRenderer::Flags QgsHollowScaleBarRenderer::flags() const +{ + return Flag::FlagUsesLineSymbol | + Flag::FlagUsesFillSymbol | + Flag::FlagUsesAlternateFillSymbol | + Flag::FlagRespectsUnits | + Flag::FlagRespectsMapUnitsPerScaleBarUnit | + Flag::FlagUsesUnitLabel | + Flag::FlagUsesSegments | + Flag::FlagUsesLabelBarSpace | + Flag::FlagUsesLabelVerticalPlacement | + Flag::FlagUsesLabelHorizontalPlacement; +} + +int QgsHollowScaleBarRenderer::sortKey() const +{ + return 8; +} + +QgsHollowScaleBarRenderer *QgsHollowScaleBarRenderer::clone() const +{ + return new QgsHollowScaleBarRenderer( *this ); +} + +void QgsHollowScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const +{ + if ( !context.painter() ) + { + return; + } + QPainter *painter = context.painter(); + + const double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); + const double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); + const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); + const double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); + const double barHeight = context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ); + + painter->save(); + if ( context.flags() & QgsRenderContext::Antialiasing ) + painter->setRenderHint( QPainter::Antialiasing, true ); + + std::unique_ptr< QgsLineSymbol > lineSymbol( settings.lineSymbol()->clone() ); + lineSymbol->startRender( context ); + + std::unique_ptr< QgsFillSymbol > fillSymbol1( settings.fillSymbol()->clone() ); + fillSymbol1->startRender( context ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2( settings.alternateFillSymbol()->clone() ); + fillSymbol2->startRender( context ); + + painter->setPen( Qt::NoPen ); + painter->setBrush( Qt::NoBrush ); + + bool useColor = true; //alternate brush color/white + const double xOffset = firstLabelXOffset( settings, context, scaleContext ); + + const QList positions = segmentPositions( context, scaleContext, settings ); + const QList widths = segmentWidths( scaleContext, settings ); + + // draw the fill + double minX = 0; + double maxX = 0; + QgsFillSymbol *currentSymbol = nullptr; + for ( int i = 0; i < positions.size(); ++i ) + { + if ( useColor ) //alternating colors + { + currentSymbol = fillSymbol1.get(); + } + else //secondary fill + { + currentSymbol = fillSymbol2.get(); + } + + const double thisX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + const double thisWidth = context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ); + + if ( i == 0 ) + minX = thisX; + if ( i == positions.size() - 1 ) + maxX = thisX + thisWidth; + + QRectF segmentRect( thisX, barTopPosition, thisWidth, barHeight ); + currentSymbol->renderPolygon( QPolygonF() + << segmentRect.topLeft() + << segmentRect.topRight() + << segmentRect.bottomRight() + << segmentRect.bottomLeft() + << segmentRect.topLeft(), nullptr, nullptr, context ); + useColor = !useColor; + } + + // and then the lines + // note that we do this layer-by-layer, to avoid ugliness where the lines touch the outer rect + for ( int layer = 0; layer < lineSymbol->symbolLayerCount(); ++layer ) + { + // horizontal lines + bool drawLine = false; + for ( int i = 0; i < positions.size(); ++i ) + { + drawLine = !drawLine; + if ( !drawLine ) + continue; + + const double lineX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + const double lineLength = context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ); + lineSymbol->renderPolyline( QPolygonF() + << QPointF( lineX, barTopPosition + barHeight / 2.0 ) + << QPointF( lineX + lineLength, barTopPosition + barHeight / 2.0 ), + nullptr, context, layer ); + } + + // vertical lines + for ( int i = 1; i < positions.size(); ++i ) + { + const double lineX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + lineSymbol->renderPolyline( QPolygonF() + << QPointF( lineX, barTopPosition ) + << QPointF( lineX, barTopPosition + barHeight ), + nullptr, context, layer ); + } + + // outside line + lineSymbol->renderPolyline( QPolygonF() + << QPointF( minX, barTopPosition ) + << QPointF( maxX, barTopPosition ) + << QPointF( maxX, barTopPosition + barHeight ) + << QPointF( minX, barTopPosition + barHeight ) + << QPointF( minX, barTopPosition ), + nullptr, context, layer ); + } + + lineSymbol->stopRender( context ); + fillSymbol1->stopRender( context ); + fillSymbol2->stopRender( context ); + painter->restore(); + + //draw labels using the default method + drawDefaultLabels( context, settings, scaleContext ); +} + +bool QgsHollowScaleBarRenderer::applyDefaultSettings( QgsScaleBarSettings &settings ) const +{ + // null the fill symbols by default + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsSimpleFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsSimpleFillSymbolLayer >(); + fillSymbolLayer->setColor( QColor( 0, 0, 0 ) ); + fillSymbolLayer->setBrushStyle( Qt::NoBrush ); + fillSymbolLayer->setStrokeStyle( Qt::NoPen ); + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer->clone() ); + settings.setFillSymbol( fillSymbol.release() ); + + fillSymbol = qgis::make_unique< QgsFillSymbol >(); + fillSymbolLayer->setColor( QColor( 255, 255, 255 ) ); + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + settings.setAlternateFillSymbol( fillSymbol.release() ); + + return true; +} + + + diff --git a/src/core/scalebar/qgshollowscalebarrenderer.h b/src/core/scalebar/qgshollowscalebarrenderer.h new file mode 100644 index 000000000000..40a585b197ff --- /dev/null +++ b/src/core/scalebar/qgshollowscalebarrenderer.h @@ -0,0 +1,53 @@ +/*************************************************************************** + qgshollowscalebarrenderer.h + ------------------------------ + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSHOLLOWSCALEBARRENDERER_H +#define QGSHOLLOWSCALEBARRENDERER_H + +#include "qgis_core.h" +#include "qgsscalebarrenderer.h" +#include + +/** + * \class QgsHollowScaleBarRenderer + * \ingroup core + * Scalebar style that draws a single box with alternating color for the segments, with horizontal lines through + * alternating segments. AKA "South African" style. + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsHollowScaleBarRenderer: public QgsScaleBarRenderer +{ + public: + + /** + * Constructor for QgsHollowScaleBarRenderer. + */ + QgsHollowScaleBarRenderer() = default; + + QString id() const override; + QString visibleName() const override; + Flags flags() const override; + int sortKey() const override; + QgsHollowScaleBarRenderer *clone() const override SIP_FACTORY; + + void draw( QgsRenderContext &context, + const QgsScaleBarSettings &settings, + const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const override; + bool applyDefaultSettings( QgsScaleBarSettings &settings ) const override; + +}; + +#endif // QGSHOLLOWSCALEBARRENDERER_H diff --git a/src/core/scalebar/qgsnumericscalebarrenderer.cpp b/src/core/scalebar/qgsnumericscalebarrenderer.cpp index d711d3657916..1c08b87303e5 100644 --- a/src/core/scalebar/qgsnumericscalebarrenderer.cpp +++ b/src/core/scalebar/qgsnumericscalebarrenderer.cpp @@ -21,6 +21,31 @@ #include #include +QString QgsNumericScaleBarRenderer::id() const +{ + return QStringLiteral( "Numeric" ); +} + +QString QgsNumericScaleBarRenderer::visibleName() const +{ + return QObject::tr( "Numeric" ); +} + +int QgsNumericScaleBarRenderer::sortKey() const +{ + return 100; +} + +QgsScaleBarRenderer::Flags QgsNumericScaleBarRenderer::flags() const +{ + return Flag::FlagUsesAlignment; +} + +QgsNumericScaleBarRenderer *QgsNumericScaleBarRenderer::clone() const +{ + return new QgsNumericScaleBarRenderer( *this ); +} + void QgsNumericScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const { if ( !context.painter() ) @@ -58,7 +83,7 @@ void QgsNumericScaleBarRenderer::draw( QgsRenderContext &context, const QgsScale painter->restore(); } -QSizeF QgsNumericScaleBarRenderer::calculateBoxSize( const QgsScaleBarSettings &settings, +QSizeF QgsNumericScaleBarRenderer::calculateBoxSize( QgsRenderContext &, const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const { QFont font = settings.textFormat().toQFont(); @@ -66,7 +91,18 @@ QSizeF QgsNumericScaleBarRenderer::calculateBoxSize( const QgsScaleBarSettings & double textWidth = QgsLayoutUtils::textWidthMM( font, scaleText( scaleContext.scale, settings ) ); double textHeight = QgsLayoutUtils::fontAscentMM( font ); - return QSizeF( 2 * settings.boxContentSpace() + 2 * settings.pen().width() + textWidth, + return QSizeF( 2 * settings.boxContentSpace() + textWidth, + textHeight + 2 * settings.boxContentSpace() ); +} + +QSizeF QgsNumericScaleBarRenderer::calculateBoxSize( const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const +{ + QFont font = settings.textFormat().toQFont(); + + double textWidth = QgsLayoutUtils::textWidthMM( font, scaleText( scaleContext.scale, settings ) ); + double textHeight = QgsLayoutUtils::fontAscentMM( font ); + + return QSizeF( 2 * settings.boxContentSpace() + textWidth, textHeight + 2 * settings.boxContentSpace() ); } diff --git a/src/core/scalebar/qgsnumericscalebarrenderer.h b/src/core/scalebar/qgsnumericscalebarrenderer.h index 1dd7ae6a47a8..48ec801b2647 100644 --- a/src/core/scalebar/qgsnumericscalebarrenderer.h +++ b/src/core/scalebar/qgsnumericscalebarrenderer.h @@ -36,15 +36,22 @@ class CORE_EXPORT QgsNumericScaleBarRenderer: public QgsScaleBarRenderer */ QgsNumericScaleBarRenderer() = default; - QString name() const override { return QStringLiteral( "Numeric" ); } + QString id() const override; + QString visibleName() const override; + int sortKey() const override; + Flags flags() const override; + QgsNumericScaleBarRenderer *clone() const override SIP_FACTORY; void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const override; - QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, + QSizeF calculateBoxSize( QgsRenderContext &context, + const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const override; + Q_DECL_DEPRECATED QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const override SIP_DEPRECATED ; + private: //! Returns the text for the scale bar or an empty string in case of error diff --git a/src/core/scalebar/qgsscalebarrenderer.cpp b/src/core/scalebar/qgsscalebarrenderer.cpp index a225913376b7..b08e8c8a7b74 100644 --- a/src/core/scalebar/qgsscalebarrenderer.cpp +++ b/src/core/scalebar/qgsscalebarrenderer.cpp @@ -20,6 +20,8 @@ #include "qgstextrenderer.h" #include "qgsexpressioncontextutils.h" #include "qgsnumericformat.h" +#include "qgssymbol.h" +#include "qgssymbollayerutils.h" #include #include @@ -53,7 +55,7 @@ void QgsScaleBarRenderer::drawDefaultLabels( QgsRenderContext &context, const Qg int segmentCounter = 0; QString currentNumericLabel; - QList positions = segmentPositions( scaleContext, settings ); + QList positions = segmentPositions( context, scaleContext, settings ); bool drawZero = true; switch ( settings.labelHorizontalPlacement() ) @@ -143,6 +145,16 @@ void QgsScaleBarRenderer::drawDefaultLabels( QgsRenderContext &context, const Qg painter->restore(); } +QgsScaleBarRenderer::Flags QgsScaleBarRenderer::flags() const +{ + return nullptr; +} + +int QgsScaleBarRenderer::sortKey() const +{ + return 100; +} + QSizeF QgsScaleBarRenderer::calculateBoxSize( const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const { @@ -190,12 +202,77 @@ QSizeF QgsScaleBarRenderer::calculateBoxSize( const QgsScaleBarSettings &setting double totalBarLength = scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) ); + // this whole method is deprecated, so we can still call the deprecated settings.pen() getter + Q_NOWARN_DEPRECATED_PUSH double width = firstLabelWidth + totalBarLength + 2 * settings.pen().widthF() + largestLabelWidth + 2 * settings.boxContentSpace(); + Q_NOWARN_DEPRECATED_POP + + double height = settings.height() + settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsLayoutUtils::fontAscentMM( font ); + + return QSizeF( width, height ); +} + +QSizeF QgsScaleBarRenderer::calculateBoxSize( QgsRenderContext &context, const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const +{ + QFont font = settings.textFormat().toQFont(); + + //consider centered first label + double firstLabelWidth = QgsLayoutUtils::textWidthMM( font, firstLabelString( settings ) ); + if ( settings.labelHorizontalPlacement() == QgsScaleBarSettings::LabelCenteredSegment ) + { + if ( firstLabelWidth > scaleContext.segmentWidth ) + { + firstLabelWidth = ( firstLabelWidth - scaleContext.segmentWidth ) / 2; + } + else + { + firstLabelWidth = 0.0; + } + } + else + { + firstLabelWidth = firstLabelWidth / 2; + } + + //consider last number and label + double largestLabelNumber = settings.numberOfSegments() * settings.unitsPerSegment() / settings.mapUnitsPerScaleBarUnit(); + QString largestNumberLabel = settings.numericFormat()->formatDouble( largestLabelNumber, QgsNumericFormatContext() ); + QString largestLabel = largestNumberLabel + ' ' + settings.unitLabel(); + double largestLabelWidth; + if ( settings.labelHorizontalPlacement() == QgsScaleBarSettings::LabelCenteredSegment ) + { + largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel ); + if ( largestLabelWidth > scaleContext.segmentWidth ) + { + largestLabelWidth = ( largestLabelWidth - scaleContext.segmentWidth ) / 2; + } + else + { + largestLabelWidth = 0.0; + } + } + else + { + largestLabelWidth = QgsLayoutUtils::textWidthMM( font, largestLabel ) - QgsLayoutUtils::textWidthMM( font, largestNumberLabel ) / 2; + } + + double totalBarLength = scaleContext.segmentWidth * ( settings.numberOfSegments() + ( settings.numberOfSegmentsLeft() > 0 ? 1 : 0 ) ); + + double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2; + // need to convert to mm + lineWidth /= context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ); + + double width = firstLabelWidth + totalBarLength + 2 * lineWidth + largestLabelWidth + 2 * settings.boxContentSpace(); double height = settings.height() + settings.labelBarSpace() + 2 * settings.boxContentSpace() + QgsLayoutUtils::fontAscentMM( font ); return QSizeF( width, height ); } +bool QgsScaleBarRenderer::applyDefaultSettings( QgsScaleBarSettings & ) const +{ + return false; +} + QString QgsScaleBarRenderer::firstLabelString( const QgsScaleBarSettings &settings ) const { if ( settings.numberOfSegmentsLeft() > 0 ) @@ -242,7 +319,38 @@ QList QgsScaleBarRenderer::segmentPositions( const ScaleBarContext &scal { QList positions; + // this whole method is deprecated, so calling a deprecated function is fine + Q_NOWARN_DEPRECATED_PUSH double currentXCoord = settings.pen().widthF() + settings.boxContentSpace(); + Q_NOWARN_DEPRECATED_POP + + //left segments + double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft(); + positions.reserve( settings.numberOfSegmentsLeft() + settings.numberOfSegments() ); + for ( int i = 0; i < settings.numberOfSegmentsLeft(); ++i ) + { + positions << currentXCoord; + currentXCoord += leftSegmentSize; + } + + //right segments + for ( int i = 0; i < settings.numberOfSegments(); ++i ) + { + positions << currentXCoord; + currentXCoord += scaleContext.segmentWidth; + } + return positions; +} + +QList QgsScaleBarRenderer::segmentPositions( QgsRenderContext &context, const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const +{ + QList positions; + + double lineWidth = QgsSymbolLayerUtils::estimateMaxSymbolBleed( settings.lineSymbol(), context ) * 2.0; + // need to convert to mm + lineWidth /= context.convertToPainterUnits( 1, QgsUnitTypes::RenderMillimeters ); + + double currentXCoord = lineWidth + settings.boxContentSpace(); //left segments double leftSegmentSize = scaleContext.segmentWidth / settings.numberOfSegmentsLeft(); diff --git a/src/core/scalebar/qgsscalebarrenderer.h b/src/core/scalebar/qgsscalebarrenderer.h index 50bc2f133927..f11008b0707a 100644 --- a/src/core/scalebar/qgsscalebarrenderer.h +++ b/src/core/scalebar/qgsscalebarrenderer.h @@ -64,6 +64,26 @@ class CORE_EXPORT QgsScaleBarRenderer }; + /** + * Flags which control scalebar renderer behavior. + * \since QGIS 3.14 + */ + enum class Flag + { + FlagUsesLineSymbol = 1 << 0, //!< Renderer utilizes the scalebar line symbol (see QgsScaleBarSettings::lineSymbol() ) + FlagUsesFillSymbol = 1 << 1, //!< Renderer utilizes the scalebar fill symbol (see QgsScaleBarSettings::fillSymbol() ) + FlagUsesAlternateFillSymbol = 1 << 2, //!< Renderer utilizes the alternate scalebar fill symbol (see QgsScaleBarSettings::alternateFillSymbol() ) + FlagRespectsUnits = 1 << 3, //!< Renderer respects the QgsScaleBarSettings::units() setting + FlagRespectsMapUnitsPerScaleBarUnit = 1 << 4, //!< Renderer respects the QgsScaleBarSettings::mapUnitsPerScaleBarUnit() setting + FlagUsesUnitLabel = 1 << 5, //!< Renderer uses the QgsScaleBarSettings::unitLabel() setting + FlagUsesSegments = 1 << 6, //!< Renderer uses the scalebar segments + FlagUsesLabelBarSpace = 1 << 7, //!< Renderer uses the QgsScaleBarSettings::labelBarSpace() setting + FlagUsesLabelVerticalPlacement = 1 << 8, //!< Renderer uses the QgsScaleBarSettings::labelVerticalPlacement() setting + FlagUsesLabelHorizontalPlacement = 1 << 8, //!< Renderer uses the QgsScaleBarSettings::labelHorizontalPlacement() setting + FlagUsesAlignment = 1 << 9, //!< Renderer uses the QgsScaleBarSettings::alignment() setting + }; + Q_DECLARE_FLAGS( Flags, Flag ) + /** * Constructor for QgsScaleBarRenderer. */ @@ -72,8 +92,40 @@ class CORE_EXPORT QgsScaleBarRenderer /** * Returns the unique name for this style. + * \deprecated use id() instead + */ + Q_DECL_DEPRECATED QString name() const SIP_DEPRECATED { return id(); } + + /** + * Returns the unique ID for this renderer. + * \since QGIS 3.14 + */ + virtual QString id() const = 0; + + /** + * Returns the user friendly, translated name for the renderer. + * \since QGIS 3.14 + */ + virtual QString visibleName() const = 0; + + /** + * Returns the scalebar rendering flags, which dictates the renderer's behavior. + * + * \since QGIS 3.14 + */ + virtual Flags flags() const; + + /** + * Returns a sorting key value, where renderers with a lower sort key will be shown earlier in lists. + * + * Generally, subclasses should return QgsScaleBarRenderer::sortKey() as their sorting key. + */ + virtual int sortKey() const; + + /** + * Returns a clone of the renderer. The caller takes ownership of the returned value. */ - virtual QString name() const = 0; + virtual QgsScaleBarRenderer *clone() const = 0 SIP_FACTORY; /** * Draws the scalebar using the specified \a settings and \a scaleContext to a destination render \a context. @@ -84,10 +136,29 @@ class CORE_EXPORT QgsScaleBarRenderer /** * Calculates the required box size (in millimeters) for a scalebar using the specified \a settings and \a scaleContext. + * \deprecated Use the version with a QgsRenderContext instead. + */ + Q_DECL_DEPRECATED virtual QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, + const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const SIP_DEPRECATED; + + /** + * Calculates the required box size (in millimeters) for a scalebar using the specified \a settings and \a scaleContext. + * + * \since QGIS 3.14 */ - virtual QSizeF calculateBoxSize( const QgsScaleBarSettings &settings, + virtual QSizeF calculateBoxSize( QgsRenderContext &context, + const QgsScaleBarSettings &settings, const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const; + /** + * Applies any default settings relating to the scalebar to the passed \a settings object. + * + * Returns TRUE if settings were applied. + * + * \since QGIS 3.14 + */ + virtual bool applyDefaultSettings( QgsScaleBarSettings &settings ) const; + protected: /** @@ -116,8 +187,15 @@ class CORE_EXPORT QgsScaleBarRenderer /** * Returns a list of positions for each segment within the scalebar. + * \deprecated use the version with a QgsRenderContext instead + */ + Q_DECL_DEPRECATED QList segmentPositions( const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const SIP_DEPRECATED; + + /** + * Returns a list of positions for each segment within the scalebar. + * \since QGIS 3.14 */ - QList segmentPositions( const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const; + QList segmentPositions( QgsRenderContext &context, const QgsScaleBarRenderer::ScaleBarContext &scaleContext, const QgsScaleBarSettings &settings ) const; /** * Returns a list of widths of each segment of the scalebar. @@ -126,4 +204,6 @@ class CORE_EXPORT QgsScaleBarRenderer }; +Q_DECLARE_OPERATORS_FOR_FLAGS( QgsScaleBarRenderer::Flags ) + #endif //QGSSCALEBARRENDERER_H diff --git a/src/core/scalebar/qgsscalebarrendererregistry.cpp b/src/core/scalebar/qgsscalebarrendererregistry.cpp new file mode 100644 index 000000000000..13ef8dab6054 --- /dev/null +++ b/src/core/scalebar/qgsscalebarrendererregistry.cpp @@ -0,0 +1,109 @@ +/*************************************************************************** + qgsscalebarrendererregistry.cpp + ----------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsscalebarrendererregistry.h" +#include "qgsscalebarrenderer.h" +#include "qgsdoubleboxscalebarrenderer.h" +#include "qgsnumericscalebarrenderer.h" +#include "qgssingleboxscalebarrenderer.h" +#include "qgsticksscalebarrenderer.h" +#include "qgssteppedlinescalebarrenderer.h" +#include "qgshollowscalebarrenderer.h" + +QgsScaleBarRendererRegistry::QgsScaleBarRendererRegistry() +{ + addRenderer( new QgsDoubleBoxScaleBarRenderer() ); + addRenderer( new QgsNumericScaleBarRenderer() ); + addRenderer( new QgsSingleBoxScaleBarRenderer() ); + addRenderer( new QgsTicksScaleBarRenderer( QgsTicksScaleBarRenderer::TicksUp ) ); + addRenderer( new QgsTicksScaleBarRenderer( QgsTicksScaleBarRenderer::TicksDown ) ); + addRenderer( new QgsTicksScaleBarRenderer( QgsTicksScaleBarRenderer::TicksMiddle ) ); + addRenderer( new QgsSteppedLineScaleBarRenderer() ); + addRenderer( new QgsHollowScaleBarRenderer() ); +} + +QgsScaleBarRendererRegistry::~QgsScaleBarRendererRegistry() +{ + qDeleteAll( mRenderers ); +} + +QStringList QgsScaleBarRendererRegistry::renderers() const +{ + return mRenderers.keys(); +} + +QStringList QgsScaleBarRendererRegistry::sortedRendererList() const +{ + QStringList ids = mRenderers.keys(); + + std::sort( ids.begin(), ids.end(), [ = ]( const QString & a, const QString & b )->bool + { + if ( sortKey( a ) < sortKey( b ) ) + return true; + else if ( sortKey( a ) > sortKey( b ) ) + return false; + else + { + int res = QString::localeAwareCompare( visibleName( a ), visibleName( b ) ); + if ( res < 0 ) + return true; + else if ( res > 0 ) + return false; + } + return false; + } ); + return ids; +} + +void QgsScaleBarRendererRegistry::addRenderer( QgsScaleBarRenderer *renderer ) +{ + if ( !renderer ) + return; + + mRenderers.insert( renderer->id(), renderer ); +} + +void QgsScaleBarRendererRegistry::removeRenderer( const QString &id ) +{ + if ( QgsScaleBarRenderer *renderer = mRenderers.take( id ) ) + { + delete renderer; + } +} + +QgsScaleBarRenderer *QgsScaleBarRendererRegistry::renderer( const QString &id ) const +{ + if ( mRenderers.contains( id ) ) + return mRenderers.value( id )->clone(); + + return nullptr; +} + +QString QgsScaleBarRendererRegistry::visibleName( const QString &id ) const +{ + if ( mRenderers.contains( id ) ) + return mRenderers.value( id )->visibleName(); + + return QString(); +} + +int QgsScaleBarRendererRegistry::sortKey( const QString &id ) const +{ + if ( mRenderers.contains( id ) ) + return mRenderers.value( id )->sortKey(); + + return 0; +} diff --git a/src/core/scalebar/qgsscalebarrendererregistry.h b/src/core/scalebar/qgsscalebarrendererregistry.h new file mode 100644 index 000000000000..37fb1c7df7f0 --- /dev/null +++ b/src/core/scalebar/qgsscalebarrendererregistry.h @@ -0,0 +1,90 @@ +/*************************************************************************** + QgsScaleBarRendererRegistry.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSSCALEBARRENDERERREGISTRY_H +#define QGSSCALEBARRENDERERREGISTRY_H + +#include "qgis_core.h" +#include "qgis.h" + +class QgsScaleBarRenderer; + +/** + * \ingroup core + * The QgsScaleBarRendererRegistry manages registered scalebar renderers. + * + * A reference to the QgsScaleBarRendererRegistry can be obtained from + * QgsApplication::scalebarRendererRegistry(). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsScaleBarRendererRegistry +{ + + public: + + /** + * You should not normally need to create your own scalebar renderer registry. + * + * Use the one provided by `QgsApplication::scalebarRendererRegistry()` instead. + */ + explicit QgsScaleBarRendererRegistry(); + ~QgsScaleBarRendererRegistry(); + + /** + * Returns a list of the renderer ids currently contained in the registry. + */ + QStringList renderers() const; + + /** + * Returns a list of the renderer ids currently contained in the registry, + * sorted in an order respecting the renderer's sort keys and display strings. + */ + QStringList sortedRendererList() const; + + /** + * Adds a new \a renderer to the registry. + * + * Ownership is transferred to the registry. + */ + void addRenderer( QgsScaleBarRenderer *renderer SIP_TRANSFER ); + + /** + * Removes the renderer with matching \a id from the registry. + */ + void removeRenderer( const QString &id ); + + /** + * Creates a new scalebar renderer by \a id. If there is no such \a id registered, + * NULLPTR will be returned instead. + * + * The caller takes ownership of the returned object. + */ + QgsScaleBarRenderer *renderer( const QString &id ) const SIP_TRANSFERBACK; + + /** + * Returns the translated, user-visible name for the renderer with matching \a id. + */ + QString visibleName( const QString &id ) const; + + /** + * Returns the sorting key for the renderer with matching \a id. + */ + int sortKey( const QString &id ) const; + + private: + QHash mRenderers; +}; +#endif // QGSSCALEBARRENDERERREGISTRY_H diff --git a/src/core/scalebar/qgsscalebarsettings.cpp b/src/core/scalebar/qgsscalebarsettings.cpp index 0f8e02bdf680..696bef2df3ad 100644 --- a/src/core/scalebar/qgsscalebarsettings.cpp +++ b/src/core/scalebar/qgsscalebarsettings.cpp @@ -18,25 +18,40 @@ #include "qgsapplication.h" #include "qgsnumericformat.h" #include "qgsbasicnumericformat.h" +#include "qgslinesymbollayer.h" +#include "qgssymbol.h" +#include "qgsfillsymbollayer.h" QgsScaleBarSettings::QgsScaleBarSettings() { - mPen = QPen( mLineColor ); - mPen.setJoinStyle( mLineJoinStyle ); - mPen.setCapStyle( mLineCapStyle ); - mPen.setWidthF( mLineWidth ); - - mBrush.setColor( mFillColor ); - mBrush.setStyle( Qt::SolidPattern ); - - mBrush2.setColor( mFillColor2 ); - mBrush2.setStyle( Qt::SolidPattern ); - mTextFormat.setSize( 12.0 ); mTextFormat.setSizeUnit( QgsUnitTypes::RenderPoints ); mTextFormat.setColor( QColor( 0, 0, 0 ) ); mNumericFormat = qgis::make_unique< QgsBasicNumericFormat >(); + + mLineSymbol = qgis::make_unique< QgsLineSymbol >(); + mLineSymbol->setColor( QColor( 0, 0, 0 ) ); + mLineSymbol->setWidth( 0.3 ); + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + line->setPenJoinStyle( Qt::MiterJoin ); + line->setPenCapStyle( Qt::SquareCap ); + } + mLineSymbol->setOutputUnit( QgsUnitTypes::RenderMillimeters ); + + mFillSymbol = qgis::make_unique< QgsFillSymbol >(); + mFillSymbol->setColor( QColor( 0, 0, 0 ) ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( mFillSymbol->symbolLayer( 0 ) ) ) + { + fill->setStrokeStyle( Qt::NoPen ); + } + mAlternateFillSymbol = qgis::make_unique< QgsFillSymbol >(); + mAlternateFillSymbol->setColor( QColor( 255, 255, 255 ) ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( mAlternateFillSymbol->symbolLayer( 0 ) ) ) + { + fill->setStrokeStyle( Qt::NoPen ); + } } QgsScaleBarSettings::QgsScaleBarSettings( const QgsScaleBarSettings &other ) @@ -49,22 +64,16 @@ QgsScaleBarSettings::QgsScaleBarSettings( const QgsScaleBarSettings &other ) , mMaxBarWidth( other.mMaxBarWidth ) , mUnitLabeling( other.mUnitLabeling ) , mTextFormat( other.mTextFormat ) - , mFillColor( other.mFillColor ) - , mFillColor2( other.mFillColor2 ) - , mLineColor( other.mLineColor ) - , mLineWidth( other.mLineWidth ) - , mPen( other.mPen ) - , mBrush( other.mBrush ) - , mBrush2( other.mBrush2 ) , mHeight( other.mHeight ) + , mLineSymbol( other.mLineSymbol->clone() ) + , mFillSymbol( other.mFillSymbol->clone() ) + , mAlternateFillSymbol( other.mAlternateFillSymbol->clone() ) , mLabelBarSpace( other.mLabelBarSpace ) , mLabelVerticalPlacement( other.mLabelVerticalPlacement ) , mLabelHorizontalPlacement( other.mLabelHorizontalPlacement ) , mBoxContentSpace( other.mBoxContentSpace ) , mAlignment( other.mAlignment ) , mUnits( other.mUnits ) - , mLineJoinStyle( other.mLineJoinStyle ) - , mLineCapStyle( other.mLineCapStyle ) , mNumericFormat( other.mNumericFormat->clone() ) { @@ -81,13 +90,9 @@ QgsScaleBarSettings &QgsScaleBarSettings::operator=( const QgsScaleBarSettings & mMaxBarWidth = other.mMaxBarWidth; mUnitLabeling = other.mUnitLabeling; mTextFormat = other.mTextFormat; - mFillColor = other.mFillColor; - mFillColor2 = other.mFillColor2; - mLineColor = other.mLineColor; - mLineWidth = other.mLineWidth; - mPen = other.mPen; - mBrush = other.mBrush; - mBrush2 = other.mBrush2; + mLineSymbol.reset( other.mLineSymbol->clone() ); + mFillSymbol.reset( other.mFillSymbol->clone() ); + mAlternateFillSymbol.reset( other.mAlternateFillSymbol->clone() ); mHeight = other.mHeight; mLabelBarSpace = other.mLabelBarSpace; mLabelVerticalPlacement = other.mLabelVerticalPlacement; @@ -95,12 +100,181 @@ QgsScaleBarSettings &QgsScaleBarSettings::operator=( const QgsScaleBarSettings & mBoxContentSpace = other.mBoxContentSpace; mAlignment = other.mAlignment; mUnits = other.mUnits; - mLineJoinStyle = other.mLineJoinStyle; - mLineCapStyle = other.mLineCapStyle; mNumericFormat.reset( other.mNumericFormat->clone() ); return *this; } +QColor QgsScaleBarSettings::fillColor() const +{ + return mFillSymbol->color(); +} + +void QgsScaleBarSettings::setFillColor( const QColor &color ) +{ + mFillSymbol->setColor( color ); +} + +QColor QgsScaleBarSettings::fillColor2() const +{ + return mAlternateFillSymbol->color(); +} + +void QgsScaleBarSettings::setFillColor2( const QColor &color ) +{ + mAlternateFillSymbol->setColor( color ); +} + +QColor QgsScaleBarSettings::lineColor() const +{ + return mLineSymbol->color(); +} + +void QgsScaleBarSettings::setLineColor( const QColor &color ) +{ + mLineSymbol->setColor( color ); +} + +double QgsScaleBarSettings::lineWidth() const +{ + return mLineSymbol->width(); +} + +void QgsScaleBarSettings::setLineWidth( double width ) +{ + mLineSymbol->setWidth( width ); + mLineSymbol->setOutputUnit( QgsUnitTypes::RenderMillimeters ); +} + +QPen QgsScaleBarSettings::pen() const +{ + QPen pen( mLineSymbol->color() ); + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + pen.setJoinStyle( line->penJoinStyle() ); + pen.setCapStyle( line->penCapStyle() ); + } + pen.setWidthF( mLineSymbol->width() ); + return pen; +} + +void QgsScaleBarSettings::setPen( const QPen &pen ) +{ + mLineSymbol->setColor( pen.color() ); + mLineSymbol->setWidth( pen.widthF() ); + mLineSymbol->setOutputUnit( QgsUnitTypes::RenderMillimeters ); + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + line->setPenJoinStyle( pen.joinStyle() ); + line->setPenCapStyle( pen.capStyle() ); + } +} + +QgsLineSymbol *QgsScaleBarSettings::lineSymbol() const +{ + return mLineSymbol.get(); +} + +void QgsScaleBarSettings::setLineSymbol( QgsLineSymbol *symbol ) +{ + mLineSymbol.reset( symbol ); +} + +QgsFillSymbol *QgsScaleBarSettings::fillSymbol() const +{ + return mFillSymbol.get(); +} + +void QgsScaleBarSettings::setFillSymbol( QgsFillSymbol *symbol ) +{ + mFillSymbol.reset( symbol ); +} + +QgsFillSymbol *QgsScaleBarSettings::alternateFillSymbol() const +{ + return mAlternateFillSymbol.get(); +} + +void QgsScaleBarSettings::setAlternateFillSymbol( QgsFillSymbol *symbol ) +{ + mAlternateFillSymbol.reset( symbol ); +} + +QBrush QgsScaleBarSettings::brush() const +{ + QBrush b; + b.setColor( mFillSymbol->color() ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( mFillSymbol->symbolLayer( 0 ) ) ) + { + b.setStyle( fill->brushStyle() ); + } + + return b; +} + +void QgsScaleBarSettings::setBrush( const QBrush &brush ) +{ + mFillSymbol->setColor( brush.color() ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( mFillSymbol->symbolLayer( 0 ) ) ) + { + fill->setBrushStyle( brush.style() ); + } +} + +QBrush QgsScaleBarSettings::brush2() const +{ + QBrush b; + b.setColor( mAlternateFillSymbol->color() ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( mAlternateFillSymbol->symbolLayer( 0 ) ) ) + { + b.setStyle( fill->brushStyle() ); + } + + return b; +} + +void QgsScaleBarSettings::setBrush2( const QBrush &brush ) +{ + mAlternateFillSymbol->setColor( brush.color() ); + if ( QgsSimpleFillSymbolLayer *fill = dynamic_cast< QgsSimpleFillSymbolLayer * >( mAlternateFillSymbol->symbolLayer( 0 ) ) ) + { + fill->setBrushStyle( brush.style() ); + } +} + +Qt::PenJoinStyle QgsScaleBarSettings::lineJoinStyle() const +{ + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + return line->penJoinStyle(); + } + return Qt::MiterJoin; +} + +void QgsScaleBarSettings::setLineJoinStyle( Qt::PenJoinStyle style ) +{ + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + line->setPenJoinStyle( style ); + } +} + +Qt::PenCapStyle QgsScaleBarSettings::lineCapStyle() const +{ + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + return line->penCapStyle(); + } + return Qt::FlatCap; +} + +void QgsScaleBarSettings::setLineCapStyle( Qt::PenCapStyle style ) +{ + if ( QgsSimpleLineSymbolLayer *line = dynamic_cast< QgsSimpleLineSymbolLayer * >( mLineSymbol->symbolLayer( 0 ) ) ) + { + line->setPenCapStyle( style ); + } +} + const QgsNumericFormat *QgsScaleBarSettings::numericFormat() const { return mNumericFormat.get(); diff --git a/src/core/scalebar/qgsscalebarsettings.h b/src/core/scalebar/qgsscalebarsettings.h index d3c4f7d913f3..fbf8d3530e26 100644 --- a/src/core/scalebar/qgsscalebarsettings.h +++ b/src/core/scalebar/qgsscalebarsettings.h @@ -27,7 +27,8 @@ #include class QgsNumericFormat; - +class QgsLineSymbol; +class QgsFillSymbol; /** * \class QgsScaleBarSettings @@ -291,94 +292,170 @@ class CORE_EXPORT QgsScaleBarSettings * Returns the color used for fills in the scalebar. * \see setFillColor() * \see fillColor2() + * \deprecated use fillSymbol() instead. */ - QColor fillColor() const { return mFillColor; } + Q_DECL_DEPRECATED QColor fillColor() const SIP_DEPRECATED; /** * Sets the \a color used for fills in the scalebar. * \see fillColor() * \see setFillColor2() + * \deprecated use setFillSymbol() instead. */ - void setFillColor( const QColor &color ) { mFillColor = color; mBrush.setColor( color ); } + Q_DECL_DEPRECATED void setFillColor( const QColor &color ) SIP_DEPRECATED; /** * Returns the secondary color used for fills in the scalebar. * \see setFillColor2() * \see fillColor() + * \deprecated use alternateFillSymbol() instead */ - QColor fillColor2() const {return mFillColor2;} + Q_DECL_DEPRECATED QColor fillColor2() const SIP_DEPRECATED; /** * Sets the secondary \a color used for fills in the scalebar. * \see fillColor2() * \see setFillColor2() + * \deprecated use setAlternateFillSymbol() instead. */ - void setFillColor2( const QColor &color ) { mFillColor2 = color; mBrush2.setColor( color ); } + Q_DECL_DEPRECATED void setFillColor2( const QColor &color ) SIP_DEPRECATED; /** * Returns the color used for lines in the scalebar. * \see setLineColor() + * \deprecated use lineSymbol() instead. */ - QColor lineColor() const { return mLineColor; } + Q_DECL_DEPRECATED QColor lineColor() const SIP_DEPRECATED; /** * Sets the \a color used for lines in the scalebar. * \see lineColor() + * \deprecated use setLineSymbol() instead. */ - void setLineColor( const QColor &color ) { mLineColor = color; mPen.setColor( mLineColor ); } + Q_DECL_DEPRECATED void setLineColor( const QColor &color ) SIP_DEPRECATED; /** * Returns the line width in millimeters for lines in the scalebar. * \see setLineWidth() + * \deprecated use lineSymbol() instead. */ - double lineWidth() const { return mLineWidth; } + Q_DECL_DEPRECATED double lineWidth() const SIP_DEPRECATED; /** * Sets the line \a width in millimeters for lines in the scalebar. * \see lineWidth() + * \deprecated use setLineSymbol() instead. */ - void setLineWidth( double width ) { mLineWidth = width; mPen.setWidthF( width ); } + Q_DECL_DEPRECATED void setLineWidth( double width ) SIP_DEPRECATED; /** * Returns the pen used for drawing outlines in the scalebar. * \see setPen() * \see brush() + * \deprecated use lineSymbol() instead. */ - QPen pen() const { return mPen; } + Q_DECL_DEPRECATED QPen pen() const SIP_DEPRECATED; /** * Sets the pen used for drawing outlines in the scalebar. * \see pen() + * \deprecated use setLineSymbol() instead. + */ + Q_DECL_DEPRECATED void setPen( const QPen &pen ) SIP_DEPRECATED; + + /** + * Returns the line symbol used to render the scalebar (only used for some scalebar types). + * + * Ownership is not transferred. + * + * \see setLineSymbol() + * \since QGIS 3.14 + */ + QgsLineSymbol *lineSymbol() const; + + /** + * Sets the line \a symbol used to render the scalebar (only used for some scalebar types). Ownership of \a symbol is + * transferred to the scalebar. + * + * \see lineSymbol() + * \since QGIS 3.14 + */ + void setLineSymbol( QgsLineSymbol *symbol SIP_TRANSFER ); + + /** + * Returns the primary fill symbol used to render the scalebar (only used for some scalebar types). + * + * Ownership is not transferred. + * + * \see setFillSymbol() + * \see alternateFillSymbol() + * \since QGIS 3.14 */ - void setPen( const QPen &pen ) { mPen = pen; } + QgsFillSymbol *fillSymbol() const; + + /** + * Sets the primary fill \a symbol used to render the scalebar (only used for some scalebar types). Ownership of \a symbol is + * transferred to the scalebar. + * + * \see fillSymbol() + * \see setAlternateFillSymbol() + * \since QGIS 3.14 + */ + void setFillSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); + + + /** + * Returns the secondary fill symbol used to render the scalebar (only used for some scalebar types). + * + * Ownership is not transferred. + * + * \see setAlternateFillSymbol() + * \see fillSymbol() + * \since QGIS 3.14 + */ + QgsFillSymbol *alternateFillSymbol() const; + + /** + * Sets the secondary fill \a symbol used to render the scalebar (only used for some scalebar types). Ownership of \a symbol is + * transferred to the scalebar. + * + * \see alternateFillSymbol() + * \see setFillSymbol() + * \since QGIS 3.14 + */ + void setAlternateFillSymbol( QgsFillSymbol *symbol SIP_TRANSFER ); /** * Returns the primary brush used for filling the scalebar. * \see setBrush() * \see brush2() * \see pen() + * \deprecated use fillSymbol() instead */ - QBrush brush() const { return mBrush; } + Q_DECL_DEPRECATED QBrush brush() const SIP_DEPRECATED; /** * Sets the primary brush used for filling the scalebar. * \see brush() + * \deprecated use setFillSymbol() instead */ - void setBrush( const QBrush &brush ) { mBrush = brush; } + Q_DECL_DEPRECATED void setBrush( const QBrush &brush ) SIP_DEPRECATED; /** * Returns the secondary brush for the scalebar. This is used for alternating color style scalebars, such * as single and double box styles. * \see setBrush2() * \see brush() + * \deprecated use alternateFillSymbol() instead */ - QBrush brush2() const { return mBrush2; } + Q_DECL_DEPRECATED QBrush brush2() const SIP_DEPRECATED; /** * Sets the secondary brush used for filling the scalebar. * \see brush() + * \deprecated use setAlternateFillSymbol() instead */ - void setBrush2( const QBrush &brush ) { mBrush2 = brush; } + Q_DECL_DEPRECATED void setBrush2( const QBrush &brush ) SIP_DEPRECATED; /** * Returns the scalebar height (in millimeters). @@ -459,26 +536,30 @@ class CORE_EXPORT QgsScaleBarSettings /** * Returns the join style used for drawing lines in the scalebar. * \see setLineJoinStyle() + * \deprecated use lineSymbol() instead */ - Qt::PenJoinStyle lineJoinStyle() const { return mLineJoinStyle; } + Q_DECL_DEPRECATED Qt::PenJoinStyle lineJoinStyle() const SIP_DEPRECATED; /** * Sets the join \a style used when drawing the lines in the scalebar * \see lineJoinStyle() + * \deprecated use setLineSymbol() instead */ - void setLineJoinStyle( Qt::PenJoinStyle style ) { mLineJoinStyle = style; mPen.setJoinStyle( style ); } + Q_DECL_DEPRECATED void setLineJoinStyle( Qt::PenJoinStyle style ) SIP_DEPRECATED; /** * Returns the cap style used for drawing lines in the scalebar. * \see setLineCapStyle() + * \deprecated use lineSymbol() instead */ - Qt::PenCapStyle lineCapStyle() const { return mLineCapStyle; } + Q_DECL_DEPRECATED Qt::PenCapStyle lineCapStyle() const SIP_DEPRECATED; /** * Sets the cap \a style used when drawing the lines in the scalebar. * \see lineCapStyle() + * \deprecated use setLineSymbol() instead */ - void setLineCapStyle( Qt::PenCapStyle style ) { mLineCapStyle = style; mPen.setCapStyle( style ); } + Q_DECL_DEPRECATED void setLineCapStyle( Qt::PenCapStyle style ) SIP_DEPRECATED; /** * Returns the numeric format used for numbers in the scalebar. @@ -521,23 +602,13 @@ class CORE_EXPORT QgsScaleBarSettings //! Text format QgsTextFormat mTextFormat; - //! Fill color - QColor mFillColor = QColor( 0, 0, 0 ); - //! Secondary fill color - QColor mFillColor2 = QColor( 255, 255, 255 ); - //! Line color - QColor mLineColor = QColor( 0, 0, 0 ); - //! Line width - double mLineWidth = 0.3; - //! Stroke - QPen mPen; - //! Fill - QBrush mBrush; - //! Secondary fill - QBrush mBrush2; //! Height of bars/lines double mHeight = 3.0; + std::unique_ptr< QgsLineSymbol > mLineSymbol; + std::unique_ptr< QgsFillSymbol > mFillSymbol; + std::unique_ptr< QgsFillSymbol > mAlternateFillSymbol; + //! Space between bar and Text labels double mLabelBarSpace = 3.0; //! Label's vertical placement @@ -552,8 +623,6 @@ class CORE_EXPORT QgsScaleBarSettings QgsUnitTypes::DistanceUnit mUnits = QgsUnitTypes::DistanceMeters; - Qt::PenJoinStyle mLineJoinStyle = Qt::MiterJoin; - Qt::PenCapStyle mLineCapStyle = Qt::SquareCap; std::unique_ptr< QgsNumericFormat > mNumericFormat; diff --git a/src/core/scalebar/qgssingleboxscalebarrenderer.cpp b/src/core/scalebar/qgssingleboxscalebarrenderer.cpp index 7f9318c2b217..9f17cf36fcc7 100644 --- a/src/core/scalebar/qgssingleboxscalebarrenderer.cpp +++ b/src/core/scalebar/qgssingleboxscalebarrenderer.cpp @@ -17,9 +17,44 @@ #include "qgssingleboxscalebarrenderer.h" #include "qgsscalebarsettings.h" #include "qgslayoututils.h" +#include "qgssymbol.h" #include #include +QString QgsSingleBoxScaleBarRenderer::id() const +{ + return QStringLiteral( "Single Box" ); +} + +QString QgsSingleBoxScaleBarRenderer::visibleName() const +{ + return QObject::tr( "Single Box" ); +} + +int QgsSingleBoxScaleBarRenderer::sortKey() const +{ + return 1; +} + +QgsScaleBarRenderer::Flags QgsSingleBoxScaleBarRenderer::flags() const +{ + return Flag::FlagUsesLineSymbol | + Flag::FlagUsesFillSymbol | + Flag::FlagUsesAlternateFillSymbol | + Flag::FlagRespectsUnits | + Flag::FlagRespectsMapUnitsPerScaleBarUnit | + Flag::FlagUsesUnitLabel | + Flag::FlagUsesSegments | + Flag::FlagUsesLabelBarSpace | + Flag::FlagUsesLabelVerticalPlacement | + Flag::FlagUsesLabelHorizontalPlacement; +} + +QgsSingleBoxScaleBarRenderer *QgsSingleBoxScaleBarRenderer::clone() const +{ + return new QgsSingleBoxScaleBarRenderer( *this ); +} + void QgsSingleBoxScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const { if ( !context.painter() ) @@ -28,43 +63,93 @@ void QgsSingleBoxScaleBarRenderer::draw( QgsRenderContext &context, const QgsSca } QPainter *painter = context.painter(); - double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); - double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); - QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); - double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); + const double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); + const double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); + const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); + const double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); + const double barHeight = context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ); painter->save(); if ( context.flags() & QgsRenderContext::Antialiasing ) painter->setRenderHint( QPainter::Antialiasing, true ); - QPen pen = settings.pen(); - pen.setWidthF( context.convertToPainterUnits( pen.widthF(), QgsUnitTypes::RenderMillimeters ) ); - painter->setPen( pen ); + std::unique_ptr< QgsLineSymbol > lineSymbol( settings.lineSymbol()->clone() ); + lineSymbol->startRender( context ); + + std::unique_ptr< QgsFillSymbol > fillSymbol1( settings.fillSymbol()->clone() ); + fillSymbol1->startRender( context ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2( settings.alternateFillSymbol()->clone() ); + fillSymbol2->startRender( context ); + + painter->setPen( Qt::NoPen ); + painter->setBrush( Qt::NoBrush ); bool useColor = true; //alternate brush color/white - double xOffset = firstLabelXOffset( settings, context, scaleContext ); + const double xOffset = firstLabelXOffset( settings, context, scaleContext ); - QList positions = segmentPositions( scaleContext, settings ); - QList widths = segmentWidths( scaleContext, settings ); + const QList positions = segmentPositions( context, scaleContext, settings ); + const QList widths = segmentWidths( scaleContext, settings ); + // draw the fill + double minX = 0; + double maxX = 0; + QgsFillSymbol *currentSymbol = nullptr; for ( int i = 0; i < positions.size(); ++i ) { if ( useColor ) //alternating colors { - painter->setBrush( settings.brush() ); + currentSymbol = fillSymbol1.get(); } - else //secondary color + else //secondary fill { - painter->setBrush( settings.brush2() ); + currentSymbol = fillSymbol2.get(); } - QRectF segmentRect( context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset, - barTopPosition, context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ), - context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ) ); - painter->drawRect( segmentRect ); + const double thisX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + const double thisWidth = context.convertToPainterUnits( widths.at( i ), QgsUnitTypes::RenderMillimeters ); + + if ( i == 0 ) + minX = thisX; + if ( i == positions.size() - 1 ) + maxX = thisX + thisWidth; + + QRectF segmentRect( thisX, barTopPosition, thisWidth, barHeight ); + currentSymbol->renderPolygon( QPolygonF() + << segmentRect.topLeft() + << segmentRect.topRight() + << segmentRect.bottomRight() + << segmentRect.bottomLeft() + << segmentRect.topLeft(), nullptr, nullptr, context ); useColor = !useColor; } + // and then the lines + // note that we do this layer-by-layer, to avoid ugliness where the lines touch the outer rect + for ( int layer = 0; layer < lineSymbol->symbolLayerCount(); ++layer ) + { + for ( int i = 1; i < positions.size(); ++i ) + { + const double lineX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + lineSymbol->renderPolyline( QPolygonF() + << QPointF( lineX, barTopPosition ) + << QPointF( lineX, barTopPosition + barHeight ), + nullptr, context, layer ); + } + + // outside line + lineSymbol->renderPolyline( QPolygonF() + << QPointF( minX, barTopPosition ) + << QPointF( maxX, barTopPosition ) + << QPointF( maxX, barTopPosition + barHeight ) + << QPointF( minX, barTopPosition + barHeight ) + << QPointF( minX, barTopPosition ), + nullptr, context, layer ); + } + + lineSymbol->stopRender( context ); + fillSymbol1->stopRender( context ); + fillSymbol2->stopRender( context ); painter->restore(); //draw labels using the default method diff --git a/src/core/scalebar/qgssingleboxscalebarrenderer.h b/src/core/scalebar/qgssingleboxscalebarrenderer.h index 72481b2de052..3ee35edfba77 100644 --- a/src/core/scalebar/qgssingleboxscalebarrenderer.h +++ b/src/core/scalebar/qgssingleboxscalebarrenderer.h @@ -37,7 +37,11 @@ class CORE_EXPORT QgsSingleBoxScaleBarRenderer: public QgsScaleBarRenderer */ QgsSingleBoxScaleBarRenderer() = default; - QString name() const override { return QStringLiteral( "Single Box" ); } + QString id() const override; + QString visibleName() const override; + int sortKey() const override; + Flags flags() const override; + QgsSingleBoxScaleBarRenderer *clone() const override SIP_FACTORY; void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, diff --git a/src/core/scalebar/qgssteppedlinescalebarrenderer.cpp b/src/core/scalebar/qgssteppedlinescalebarrenderer.cpp new file mode 100644 index 000000000000..740505a03283 --- /dev/null +++ b/src/core/scalebar/qgssteppedlinescalebarrenderer.cpp @@ -0,0 +1,112 @@ +/*************************************************************************** + qgssteppedlinescalebarrenderer.cpp + -------------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgssteppedlinescalebarrenderer.h" +#include "qgsscalebarsettings.h" +#include "qgslayoututils.h" +#include "qgssymbol.h" +#include +#include + +QString QgsSteppedLineScaleBarRenderer::id() const +{ + return QStringLiteral( "stepped" ); +} + +QString QgsSteppedLineScaleBarRenderer::visibleName() const +{ + return QObject::tr( "Stepped Line" ); +} + +int QgsSteppedLineScaleBarRenderer::sortKey() const +{ + return 7; +} + +QgsScaleBarRenderer::Flags QgsSteppedLineScaleBarRenderer::flags() const +{ + return Flag::FlagUsesLineSymbol | + Flag::FlagRespectsUnits | + Flag::FlagRespectsMapUnitsPerScaleBarUnit | + Flag::FlagUsesUnitLabel | + Flag::FlagUsesSegments | + Flag::FlagUsesLabelBarSpace | + Flag::FlagUsesLabelVerticalPlacement | + Flag::FlagUsesLabelHorizontalPlacement; +} + +QgsSteppedLineScaleBarRenderer *QgsSteppedLineScaleBarRenderer::clone() const +{ + return new QgsSteppedLineScaleBarRenderer( *this ); +} + +void QgsSteppedLineScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const +{ + if ( !context.painter() ) + { + return; + } + QPainter *painter = context.painter(); + + std::unique_ptr< QgsLineSymbol > sym( settings.lineSymbol()->clone() ); + sym->startRender( context ) ; + + double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); + double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); + QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); + double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); + const double barBottomPosition = barTopPosition + context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ); + + painter->save(); + if ( context.flags() & QgsRenderContext::Antialiasing ) + painter->setRenderHint( QPainter::Antialiasing, true ); + + painter->setPen( Qt::NoPen ); + + double xOffset = firstLabelXOffset( settings, context, scaleContext ); + + QList positions = segmentPositions( context, scaleContext, settings ); + QList widths = segmentWidths( scaleContext, settings ); + + QPolygonF points; + + for ( int i = 0; i < positions.size() + 1; ++i ) + { + // we render one extra place, corresponding to the final position + width (i.e. the "end" of the bar) + double x = i < positions.size() ? context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset + : context.convertToPainterUnits( positions.at( i - 1 ), QgsUnitTypes::RenderMillimeters ) + xOffset + context.convertToPainterUnits( widths.at( i - 1 ), QgsUnitTypes::RenderMillimeters ); + if ( i % 2 == 0 ) + { + points << QPointF( x, barBottomPosition ) << QPointF( x, barTopPosition ); + } + else + { + points << QPointF( x, barTopPosition ) << QPointF( x, barBottomPosition ) ; + } + } + + sym->renderPolyline( points, nullptr, context ); + + painter->restore(); + + sym->stopRender( context ); + + //draw labels using the default method + drawDefaultLabels( context, settings, scaleContext ); +} + + + diff --git a/src/core/scalebar/qgssteppedlinescalebarrenderer.h b/src/core/scalebar/qgssteppedlinescalebarrenderer.h new file mode 100644 index 000000000000..3efb231bb9c7 --- /dev/null +++ b/src/core/scalebar/qgssteppedlinescalebarrenderer.h @@ -0,0 +1,51 @@ +/*************************************************************************** + qgssteppedlinescalebarrenderer.h + -------------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSSTEPPEDLINESCALEBARRENDERER_H +#define QGSSTEPPEDLINESCALEBARRENDERER_H + +#include "qgis_core.h" +#include "qgsscalebarrenderer.h" +#include + +/** + * \class QgsSteppedLineScaleBarRenderer + * \ingroup core + * Scalebar style that draws a stepped line representation of a scalebar. + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsSteppedLineScaleBarRenderer: public QgsScaleBarRenderer +{ + public: + + /** + * Constructor for QgsSteppedLineScaleBarRenderer. + */ + QgsSteppedLineScaleBarRenderer() = default; + + QString id() const override; + QString visibleName() const override; + int sortKey() const override; + Flags flags() const override; + QgsSteppedLineScaleBarRenderer *clone() const override SIP_FACTORY; + + void draw( QgsRenderContext &context, + const QgsScaleBarSettings &settings, + const QgsScaleBarRenderer::ScaleBarContext &scaleContext ) const override; + +}; + +#endif // QGSSTEPPEDLINESCALEBARRENDERER_H diff --git a/src/core/scalebar/qgsticksscalebarrenderer.cpp b/src/core/scalebar/qgsticksscalebarrenderer.cpp index 2aaeeaf89e2b..afb6d9e5751e 100644 --- a/src/core/scalebar/qgsticksscalebarrenderer.cpp +++ b/src/core/scalebar/qgsticksscalebarrenderer.cpp @@ -17,9 +17,16 @@ #include "qgsticksscalebarrenderer.h" #include "qgsscalebarsettings.h" #include "qgslayoututils.h" +#include "qgssymbol.h" #include -QString QgsTicksScaleBarRenderer::name() const +QgsTicksScaleBarRenderer::QgsTicksScaleBarRenderer( QgsTicksScaleBarRenderer::TickPosition position ) + : mTickPosition( position ) +{ + +} + +QString QgsTicksScaleBarRenderer::id() const { switch ( mTickPosition ) { @@ -33,6 +40,52 @@ QString QgsTicksScaleBarRenderer::name() const return QString(); // to make gcc happy } +QString QgsTicksScaleBarRenderer::visibleName() const +{ + switch ( mTickPosition ) + { + case TicksUp: + return QObject::tr( "Line Ticks Up" ); + case TicksDown: + return QObject::tr( "Line Ticks Down" ); + case TicksMiddle: + return QObject::tr( "Line Ticks Middle" ); + } + return QString(); // to make gcc happy + +} + +int QgsTicksScaleBarRenderer::sortKey() const +{ + switch ( mTickPosition ) + { + case TicksUp: + return 5; + case TicksDown: + return 4; + case TicksMiddle: + return 3; + } + return 6; +} + +QgsScaleBarRenderer::Flags QgsTicksScaleBarRenderer::flags() const +{ + return Flag::FlagUsesLineSymbol | + Flag::FlagRespectsUnits | + Flag::FlagRespectsMapUnitsPerScaleBarUnit | + Flag::FlagUsesUnitLabel | + Flag::FlagUsesSegments | + Flag::FlagUsesLabelBarSpace | + Flag::FlagUsesLabelVerticalPlacement | + Flag::FlagUsesLabelHorizontalPlacement; +} + +QgsTicksScaleBarRenderer *QgsTicksScaleBarRenderer::clone() const +{ + return new QgsTicksScaleBarRenderer( * this ); +} + void QgsTicksScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, const ScaleBarContext &scaleContext ) const { if ( !context.painter() ) @@ -40,56 +93,65 @@ void QgsTicksScaleBarRenderer::draw( QgsRenderContext &context, const QgsScaleBa QPainter *painter = context.painter(); - double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); - double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); - QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); - double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); - double middlePosition = barTopPosition + context.convertToPainterUnits( settings.height() / 2.0, QgsUnitTypes::RenderMillimeters ); - double bottomPosition = barTopPosition + context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ); + const double scaledLabelBarSpace = context.convertToPainterUnits( settings.labelBarSpace(), QgsUnitTypes::RenderMillimeters ); + const double scaledBoxContentSpace = context.convertToPainterUnits( settings.boxContentSpace(), QgsUnitTypes::RenderMillimeters ); + const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, settings.textFormat() ); + const double barTopPosition = scaledBoxContentSpace + ( settings.labelVerticalPlacement() == QgsScaleBarSettings::LabelAboveSegment ? fontMetrics.ascent() + scaledLabelBarSpace : 0 ); + const double middlePosition = barTopPosition + context.convertToPainterUnits( settings.height() / 2.0, QgsUnitTypes::RenderMillimeters ); + const double bottomPosition = barTopPosition + context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ); - double xOffset = firstLabelXOffset( settings, context, scaleContext ); + const double xOffset = firstLabelXOffset( settings, context, scaleContext ); painter->save(); if ( context.flags() & QgsRenderContext::Antialiasing ) painter->setRenderHint( QPainter::Antialiasing, true ); - QPen pen = settings.pen(); - pen.setWidthF( context.convertToPainterUnits( pen.widthF(), QgsUnitTypes::RenderMillimeters ) ); - painter->setPen( pen ); + std::unique_ptr< QgsLineSymbol > symbol( settings.lineSymbol()->clone() ); + symbol->startRender( context ); - QList positions = segmentPositions( scaleContext, settings ); + const QList positions = segmentPositions( context, scaleContext, settings ); - for ( int i = 0; i < positions.size(); ++i ) + // we render the bar symbol-layer-by-symbol-layer, to avoid ugliness where the lines overlap in multi-layer symbols + for ( int layer = 0; layer < symbol->symbolLayerCount(); ++ layer ) { - painter->drawLine( QLineF( context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset, barTopPosition, - context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset, - barTopPosition + context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ) ) ); - } + // first draw the vertical lines + for ( int i = 0; i < positions.size(); ++i ) + { + const double thisX = context.convertToPainterUnits( positions.at( i ), QgsUnitTypes::RenderMillimeters ) + xOffset; + symbol->renderPolyline( QPolygonF() << QPointF( thisX, barTopPosition ) + << QPointF( thisX, bottomPosition ), nullptr, context, layer ); + } - //draw last tick and horizontal line - if ( !positions.isEmpty() ) - { - double lastTickPositionX = context.convertToPainterUnits( positions.at( positions.size() - 1 ) + scaleContext.segmentWidth, QgsUnitTypes::RenderMillimeters ) + xOffset; - double verticalPos = 0.0; - switch ( mTickPosition ) + //draw last tick and horizontal line + if ( !positions.isEmpty() ) { - case TicksDown: - verticalPos = barTopPosition; - break; - case TicksMiddle: - verticalPos = middlePosition; - break; - case TicksUp: - verticalPos = bottomPosition; - break; + double lastTickPositionX = context.convertToPainterUnits( positions.at( positions.size() - 1 ) + scaleContext.segmentWidth, QgsUnitTypes::RenderMillimeters ) + xOffset; + + //last vertical line + symbol->renderPolyline( QPolygonF() << QPointF( lastTickPositionX, barTopPosition ) + << QPointF( lastTickPositionX, bottomPosition ), + nullptr, context, layer ); + double verticalPos = 0.0; + switch ( mTickPosition ) + { + case TicksDown: + verticalPos = barTopPosition; + break; + case TicksMiddle: + verticalPos = middlePosition; + break; + case TicksUp: + verticalPos = bottomPosition; + break; + } + //horizontal line + symbol->renderPolyline( QPolygonF() << QPointF( xOffset + context.convertToPainterUnits( positions.at( 0 ), QgsUnitTypes::RenderMillimeters ), verticalPos ) + << QPointF( lastTickPositionX, verticalPos ), nullptr, context, layer ); } - //horizontal line - painter->drawLine( QLineF( xOffset + context.convertToPainterUnits( positions.at( 0 ), QgsUnitTypes::RenderMillimeters ), - verticalPos, lastTickPositionX, verticalPos ) ); - //last vertical line - painter->drawLine( QLineF( lastTickPositionX, barTopPosition, lastTickPositionX, barTopPosition + context.convertToPainterUnits( settings.height(), QgsUnitTypes::RenderMillimeters ) ) ); } + symbol->stopRender( context ); + painter->restore(); //draw labels using the default method diff --git a/src/core/scalebar/qgsticksscalebarrenderer.h b/src/core/scalebar/qgsticksscalebarrenderer.h index 9af953bc8cc6..2ae81dda7a5b 100644 --- a/src/core/scalebar/qgsticksscalebarrenderer.h +++ b/src/core/scalebar/qgsticksscalebarrenderer.h @@ -41,9 +41,13 @@ class CORE_EXPORT QgsTicksScaleBarRenderer: public QgsScaleBarRenderer /** * Constructor for QgsTicksScaleBarRenderer. */ - QgsTicksScaleBarRenderer() = default; + QgsTicksScaleBarRenderer( TickPosition position = TicksMiddle ); - QString name() const override; + QString id() const override; + QString visibleName() const override; + int sortKey() const override; + Flags flags() const override; + QgsTicksScaleBarRenderer *clone() const override SIP_FACTORY; void draw( QgsRenderContext &context, const QgsScaleBarSettings &settings, diff --git a/src/core/symbology/qgsellipsesymbollayer.cpp b/src/core/symbology/qgsellipsesymbollayer.cpp index 475797203c0d..b2e6f94682e1 100644 --- a/src/core/symbology/qgsellipsesymbollayer.cpp +++ b/src/core/symbology/qgsellipsesymbollayer.cpp @@ -674,7 +674,7 @@ QRectF QgsEllipseSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext &con if ( !qgsDoubleNear( angle, 0.0 ) ) transform.rotate( angle ); - double penWidth = 0.0; + double penWidth = mStrokeWidth; if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyStrokeWidth ) ) { context.setOriginalValueVariable( mStrokeWidth ); @@ -686,10 +686,12 @@ QRectF QgsEllipseSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext &con double strokeWidth = exprVal.toDouble( &ok ); if ( ok ) { - penWidth = context.renderContext().convertToPainterUnits( strokeWidth, mStrokeWidthUnit, mStrokeWidthMapUnitScale ); + penWidth = strokeWidth; } } } + penWidth = context.renderContext().convertToPainterUnits( penWidth, mStrokeWidthUnit, mStrokeWidthMapUnitScale ); + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyStrokeStyle ) ) { context.setOriginalValueVariable( QgsSymbolLayerUtils::encodePenStyle( mStrokeStyle ) ); @@ -699,6 +701,8 @@ QRectF QgsEllipseSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext &con penWidth = 0.0; } } + else if ( mStrokeStyle == Qt::NoPen ) + penWidth = 0; //antialiasing, add 1 pixel penWidth += 1; diff --git a/src/core/symbology/qgsfillsymbollayer.cpp b/src/core/symbology/qgsfillsymbollayer.cpp index 7b64ed4c378d..7784b6eea500 100644 --- a/src/core/symbology/qgsfillsymbollayer.cpp +++ b/src/core/symbology/qgsfillsymbollayer.cpp @@ -3905,7 +3905,24 @@ void QgsRasterFillSymbolLayer::applyPattern( QBrush &brush, const QString &image QSize size; if ( width > 0 ) { - size.setWidth( context.renderContext().convertToPainterUnits( width, mWidthUnit, mWidthMapUnitScale ) ); + if ( mWidthUnit != QgsUnitTypes::RenderPercentage ) + { + size.setWidth( context.renderContext().convertToPainterUnits( width, mWidthUnit, mWidthMapUnitScale ) ); + } + else + { + // RenderPercentage Unit Type takes original image size + size = QgsApplication::imageCache()->originalSize( imageFilePath ); + if ( size.isEmpty() ) + return; + + size.setWidth( ( width * size.width() ) / 100.0 ); + + // don't render symbols with size below one or above 10,000 pixels + if ( static_cast< int >( size.width() ) < 1 || 10000.0 < size.width() ) + return; + } + size.setHeight( 0 ); } diff --git a/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp b/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp index a47db5f4074d..4e282861b6b7 100644 --- a/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp +++ b/src/core/symbology/qgsgeometrygeneratorsymbollayer.cpp @@ -137,7 +137,7 @@ QgsStringMap QgsGeometryGeneratorSymbolLayer::properties() const void QgsGeometryGeneratorSymbolLayer::drawPreviewIcon( QgsSymbolRenderContext &context, QSize size ) { if ( mSymbol ) - mSymbol->drawPreviewIcon( context.renderContext().painter(), size ); + mSymbol->drawPreviewIcon( context.renderContext().painter(), size, nullptr, false, nullptr, context.patchShape() ); } void QgsGeometryGeneratorSymbolLayer::setGeometryExpression( const QString &exp ) diff --git a/src/core/symbology/qgsmarkersymbollayer.cpp b/src/core/symbology/qgsmarkersymbollayer.cpp index b8f5d082d185..f51122db9649 100644 --- a/src/core/symbology/qgsmarkersymbollayer.cpp +++ b/src/core/symbology/qgsmarkersymbollayer.cpp @@ -19,6 +19,7 @@ #include "qgsdxfexport.h" #include "qgsdxfpaintdevice.h" #include "qgsexpression.h" +#include "qgsfontutils.h" #include "qgsimagecache.h" #include "qgsimageoperation.h" #include "qgsrendercontext.h" @@ -1458,7 +1459,7 @@ QRectF QgsSimpleMarkerSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext QRectF symbolBounds = QgsSimpleMarkerSymbolLayerBase::bounds( point, context ); // need to account for stroke width - double penWidth = 0.0; + double penWidth = mStrokeWidth; bool ok = true; if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyStrokeWidth ) ) { @@ -1466,9 +1467,10 @@ QRectF QgsSimpleMarkerSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext double strokeWidth = mDataDefinedProperties.valueAsDouble( QgsSymbolLayer::PropertyStrokeWidth, context.renderContext().expressionContext(), mStrokeWidth, &ok ); if ( ok ) { - penWidth = context.renderContext().convertToPainterUnits( strokeWidth, mStrokeWidthUnit, mStrokeWidthMapUnitScale ); + penWidth = strokeWidth; } } + penWidth = context.renderContext().convertToPainterUnits( penWidth, mStrokeWidthUnit, mStrokeWidthMapUnitScale ); if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyStrokeStyle ) ) { context.setOriginalValueVariable( QgsSymbolLayerUtils::encodePenStyle( mStrokeStyle ) ); @@ -1478,6 +1480,9 @@ QRectF QgsSimpleMarkerSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext penWidth = 0.0; } } + else if ( mStrokeStyle == Qt::NoPen ) + penWidth = 0; + //antialiasing, add 1 pixel penWidth += 1; @@ -2683,24 +2688,49 @@ void QgsRasterMarkerSymbolLayer::renderPoint( QPointF point, QgsSymbolRenderCont if ( !p ) return; + QString path = mPath; + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyName ) ) + { + context.setOriginalValueVariable( mPath ); + path = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyName, context.renderContext().expressionContext(), mPath ); + } + + if ( path.isEmpty() ) + return; + + double width = 0.0; + double height = 0.0; + bool hasDataDefinedSize = false; double scaledSize = calculateSize( context, hasDataDefinedSize ); - double width = context.renderContext().convertToPainterUnits( scaledSize, mSizeUnit, mSizeMapUnitScale ); + bool hasDataDefinedAspectRatio = false; double aspectRatio = calculateAspectRatio( context, scaledSize, hasDataDefinedAspectRatio ); - double height = width * ( preservedAspectRatio() ? defaultAspectRatio() : aspectRatio ); - //don't render symbols with size below one or above 10,000 pixels - if ( static_cast< int >( width ) < 1 || 10000.0 < width ) + QPointF outputOffset; + double angle = 0.0; + + // RenderPercentage Unit Type takes original image size + if ( mSizeUnit == QgsUnitTypes::RenderPercentage ) { - return; - } + QSize size = QgsApplication::imageCache()->originalSize( path ); + if ( size.isEmpty() ) + return; - QString path = mPath; - if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyName ) ) + width = ( scaledSize * static_cast< double >( size.width() ) ) / 100.0; + height = ( scaledSize * static_cast< double >( size.height() ) ) / 100.0; + + // don't render symbols with size below one or above 10,000 pixels + if ( static_cast< int >( width ) < 1 || 10000.0 < width || static_cast< int >( height ) < 1 || 10000.0 < height ) + return; + + calculateOffsetAndRotation( context, width, height, outputOffset, angle ); + } + else { - context.setOriginalValueVariable( mPath ); - path = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyName, context.renderContext().expressionContext(), mPath ); + width = context.renderContext().convertToPainterUnits( scaledSize, mSizeUnit, mSizeMapUnitScale ); + height = width * ( preservedAspectRatio() ? defaultAspectRatio() : aspectRatio ); + if ( preservedAspectRatio() && path != mPath ) { QSize size = QgsApplication::imageCache()->originalSize( path ); @@ -2709,17 +2739,15 @@ void QgsRasterMarkerSymbolLayer::renderPoint( QPointF point, QgsSymbolRenderCont height = width * ( static_cast< double >( size.height() ) / static_cast< double >( size.width() ) ); } } - } - - if ( path.isEmpty() ) - return; - p->save(); + // don't render symbols with size below one or above 10,000 pixels + if ( static_cast< int >( width ) < 1 || 10000.0 < width ) + return; - QPointF outputOffset; - double angle = 0.0; - calculateOffsetAndRotation( context, scaledSize, scaledSize * ( height / width ), outputOffset, angle ); + calculateOffsetAndRotation( context, scaledSize, scaledSize * ( height / width ), outputOffset, angle ); + } + p->save(); p->translate( point + outputOffset ); bool rotated = !qgsDoubleNear( angle, 0 ); @@ -2958,14 +2986,10 @@ QgsFontMarkerSymbolLayer::QgsFontMarkerSymbolLayer( const QString &fontFamily, Q mPenJoinStyle = DEFAULT_FONTMARKER_JOINSTYLE; } -QgsFontMarkerSymbolLayer::~QgsFontMarkerSymbolLayer() -{ - delete mFontMetrics; -} - QgsSymbolLayer *QgsFontMarkerSymbolLayer::create( const QgsStringMap &props ) { QString fontFamily = DEFAULT_FONTMARKER_FONT; + QString fontStyle = DEFAULT_FONTMARKER_FONT; QString string = DEFAULT_FONTMARKER_CHR; double pointSize = DEFAULT_FONTMARKER_SIZE; QColor color = DEFAULT_FONTMARKER_COLOR; @@ -2984,6 +3008,8 @@ QgsSymbolLayer *QgsFontMarkerSymbolLayer::create( const QgsStringMap &props ) QgsFontMarkerSymbolLayer *m = new QgsFontMarkerSymbolLayer( fontFamily, string, pointSize, color, angle ); + if ( props.contains( QStringLiteral( "font_style" ) ) ) + m->setFontStyle( props[QStringLiteral( "font_style" )] ); if ( props.contains( QStringLiteral( "outline_color" ) ) ) m->setStrokeColor( QgsSymbolLayerUtils::decodeColor( props[QStringLiteral( "outline_color" )] ) ); if ( props.contains( QStringLiteral( "outline_width" ) ) ) @@ -3033,13 +3059,17 @@ void QgsFontMarkerSymbolLayer::startRender( QgsSymbolRenderContext &context ) mPen.setWidthF( context.renderContext().convertToPainterUnits( mStrokeWidth, mStrokeWidthUnit, mStrokeWidthMapUnitScale ) ); mFont = QFont( mFontFamily ); + if ( !mFontStyle.isEmpty() ) + { + mFont.setStyleName( QgsFontUtils::translateNamedStyle( mFontStyle ) ); + } + const double sizePixels = context.renderContext().convertToPainterUnits( mSize, mSizeUnit, mSizeMapUnitScale ); mNonZeroFontSize = !qgsDoubleNear( sizePixels, 0.0 ); // if a non zero, but small pixel size results, round up to 2 pixels so that a "dot" is at least visible // (if we set a <=1 pixel size here Qt will reset the font to a default size, leading to much too large symbols) mFont.setPixelSize( std::max( 2, static_cast< int >( std::round( sizePixels ) ) ) ); - delete mFontMetrics; - mFontMetrics = new QFontMetrics( mFont ); + mFontMetrics.reset( new QFontMetrics( mFont ) ); #if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) mChrWidth = mFontMetrics->width( mString ); #else @@ -3049,7 +3079,9 @@ void QgsFontMarkerSymbolLayer::startRender( QgsSymbolRenderContext &context ) mOrigSize = mSize; // save in case the size would be data defined // use caching only when not using a data defined character - mUseCachedPath = !mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyCharacter ); + mUseCachedPath = !mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyFontFamily ) && + !mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyFontStyle ) && + !mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyCharacter ); if ( mUseCachedPath ) { QPointF chrOffset = mChrOffset; @@ -3221,6 +3253,23 @@ void QgsFontMarkerSymbolLayer::renderPoint( QPointF point, QgsSymbolRenderContex p->setPen( Qt::NoPen ); } + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyFontFamily ) ) + { + context.setOriginalValueVariable( mFontFamily ); + QString fontFamily = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyFontFamily, context.renderContext().expressionContext(), mFontFamily, &ok ); + mFont.setFamily( ok ? fontFamily : mFontFamily ); + } + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyFontStyle ) ) + { + context.setOriginalValueVariable( mFontStyle ); + QString fontStyle = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyFontStyle, context.renderContext().expressionContext(), mFontStyle, &ok ); + mFont.setStyleName( QgsFontUtils::translateNamedStyle( ok ? fontStyle : mFontStyle ) ); + } + if ( mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyFontFamily ) || mDataDefinedProperties.isActive( QgsSymbolLayer::PropertyFontStyle ) ) + { + mFontMetrics.reset( new QFontMetrics( mFont ) ); + } + QPointF chrOffset = mChrOffset; double chrWidth; QString charToRender = characterToRender( context, chrOffset, chrWidth ); @@ -3261,6 +3310,7 @@ QgsStringMap QgsFontMarkerSymbolLayer::properties() const { QgsStringMap props; props[QStringLiteral( "font" )] = mFontFamily; + props[QStringLiteral( "font_style" )] = mFontStyle; props[QStringLiteral( "chr" )] = mString; props[QStringLiteral( "size" )] = QString::number( mSize ); props[QStringLiteral( "size_unit" )] = QgsUnitTypes::encodeUnit( mSizeUnit ); @@ -3283,6 +3333,7 @@ QgsStringMap QgsFontMarkerSymbolLayer::properties() const QgsFontMarkerSymbolLayer *QgsFontMarkerSymbolLayer::clone() const { QgsFontMarkerSymbolLayer *m = new QgsFontMarkerSymbolLayer( mFontFamily, mString, mSize, mColor, mAngle ); + m->setFontStyle( mFontStyle ); m->setStrokeColor( mStrokeColor ); m->setStrokeWidth( mStrokeWidth ); m->setStrokeWidthUnit( mStrokeWidthUnit ); @@ -3338,7 +3389,7 @@ QRectF QgsFontMarkerSymbolLayer::bounds( QPointF point, QgsSymbolRenderContext & ( void )characterToRender( context, chrOffset, chrWidth ); if ( !mFontMetrics ) - mFontMetrics = new QFontMetrics( mFont ); + mFontMetrics.reset( new QFontMetrics( mFont ) ); double scaledSize = calculateSize( context ); if ( !qgsDoubleNear( scaledSize, mOrigSize ) ) diff --git a/src/core/symbology/qgsmarkersymbollayer.h b/src/core/symbology/qgsmarkersymbollayer.h index 61b75fd2d67b..4b720f16e1e8 100644 --- a/src/core/symbology/qgsmarkersymbollayer.h +++ b/src/core/symbology/qgsmarkersymbollayer.h @@ -808,8 +808,6 @@ class CORE_EXPORT QgsFontMarkerSymbolLayer : public QgsMarkerSymbolLayer const QColor &color = DEFAULT_FONTMARKER_COLOR, double angle = DEFAULT_FONTMARKER_ANGLE ); - ~QgsFontMarkerSymbolLayer() override; - // static stuff /** @@ -854,6 +852,22 @@ class CORE_EXPORT QgsFontMarkerSymbolLayer : public QgsMarkerSymbolLayer */ void setFontFamily( const QString &family ) { mFontFamily = family; } + /** + * Returns the font style for the associated font which will be used to render the point. + * + * \see setFontStyle() + * \since QGIS 3.14 + */ + QString fontStyle() const { return mFontStyle; } + + /** + * Sets the font \a style for the font which will be used to render the point. + * + * \see fontStyle() + * \since QGIS 3.14 + */ + void setFontStyle( const QString &style ) { mFontStyle = style; } + /** * Returns the character(s) used when rendering points. * @@ -956,19 +970,19 @@ class CORE_EXPORT QgsFontMarkerSymbolLayer : public QgsMarkerSymbolLayer QRectF bounds( QPointF point, QgsSymbolRenderContext &context ) override; - protected: + private: QString mFontFamily; - QFontMetrics *mFontMetrics = nullptr; + QString mFontStyle; + QFont mFont; + std::unique_ptr< QFontMetrics >mFontMetrics; + QString mString; double mChrWidth = 0; QPointF mChrOffset; - QFont mFont; double mOrigSize; - private: - QColor mStrokeColor; double mStrokeWidth; QgsUnitTypes::RenderUnit mStrokeWidthUnit; diff --git a/src/core/symbology/qgsstyle.cpp b/src/core/symbology/qgsstyle.cpp index 864a6cf48693..2b3389c4d0e6 100644 --- a/src/core/symbology/qgsstyle.cpp +++ b/src/core/symbology/qgsstyle.cpp @@ -110,15 +110,9 @@ void QgsStyle::clear() mSymbols.clear(); mColorRamps.clear(); mTextFormats.clear(); - mCachedColorRampTags.clear(); - mCachedSymbolTags.clear(); - mCachedTextFormatTags.clear(); - mCachedLabelSettingsTags.clear(); - mCachedSymbolFavorites.clear(); - mCachedColorRampFavorites.clear(); - mCachedTextFormatFavorites.clear(); - mCachedLabelSettingsFavorites.clear(); + mCachedTags.clear(); + mCachedFavorites.clear(); } bool QgsStyle::addSymbol( const QString &name, QgsSymbol *symbol, bool update ) @@ -169,11 +163,12 @@ bool QgsStyle::saveSymbol( const QString &name, QgsSymbol *symbol, bool favorite return false; } - mCachedSymbolFavorites[ name ] = favorite; + mCachedFavorites[ SymbolEntity ].insert( name, favorite ); tagSymbol( SymbolEntity, name, tags ); emit symbolSaved( name, symbol ); + emit entityAdded( SymbolEntity, name ); return true; } @@ -204,13 +199,37 @@ bool QgsStyle::removeSymbol( const QString &name ) const bool result = remove( SymbolEntity, symbolid ); if ( result ) { - mCachedSymbolTags.remove( name ); - mCachedSymbolFavorites.remove( name ); + mCachedTags[ SymbolEntity ].remove( name ); + mCachedFavorites[ SymbolEntity ].remove( name ); emit symbolRemoved( name ); + emit entityRemoved( SymbolEntity, name ); } return result; } +bool QgsStyle::renameEntity( QgsStyle::StyleEntity type, const QString &oldName, const QString &newName ) +{ + switch ( type ) + { + case SymbolEntity: + return renameSymbol( oldName, newName ); + + case ColorrampEntity: + return renameColorRamp( oldName, newName ); + + case TextFormatEntity: + return renameTextFormat( oldName, newName ); + + case LabelSettingsEntity: + return renameLabelSettings( oldName, newName ); + + case TagEntity: + case SmartgroupEntity: + return false; + } + return false; +} + QgsSymbol *QgsStyle::symbol( const QString &name ) { const QgsSymbol *symbol = symbolRef( name ); @@ -323,11 +342,12 @@ bool QgsStyle::saveColorRamp( const QString &name, QgsColorRamp *ramp, bool favo return false; } - mCachedColorRampFavorites[ name ] = favorite; + mCachedFavorites[ ColorrampEntity ].insert( name, favorite ); tagSymbol( ColorrampEntity, name, tags ); emit rampAdded( name ); + emit entityAdded( ColorrampEntity, name ); return true; } @@ -345,10 +365,11 @@ bool QgsStyle::removeColorRamp( const QString &name ) return false; } - mCachedColorRampTags.remove( name ); - mCachedColorRampFavorites.remove( name ); + mCachedTags[ ColorrampEntity ].remove( name ); + mCachedFavorites[ ColorrampEntity ].remove( name ); emit rampRemoved( name ); + emit entityRemoved( ColorrampEntity, name ); return true; } @@ -667,12 +688,15 @@ bool QgsStyle::renameSymbol( const QString &oldName, const QString &newName ) return false; } - mCachedSymbolTags.remove( oldName ); - mCachedSymbolFavorites.remove( oldName ); + mCachedTags[ SymbolEntity ].remove( oldName ); + mCachedFavorites[ SymbolEntity ].remove( oldName ); const bool result = rename( SymbolEntity, symbolid, newName ); if ( result ) + { emit symbolRenamed( oldName, newName ); + emit entityRenamed( SymbolEntity, oldName, newName ); + } return result; } @@ -690,8 +714,8 @@ bool QgsStyle::renameColorRamp( const QString &oldName, const QString &newName ) return false; mColorRamps.insert( newName, ramp ); - mCachedColorRampTags.remove( oldName ); - mCachedColorRampFavorites.remove( oldName ); + mCachedTags[ ColorrampEntity ].remove( oldName ); + mCachedFavorites[ ColorrampEntity ].remove( oldName ); int rampid = 0; sqlite3_statement_unique_ptr statement; @@ -704,7 +728,10 @@ bool QgsStyle::renameColorRamp( const QString &oldName, const QString &newName ) } const bool result = rename( ColorrampEntity, rampid, newName ); if ( result ) + { emit rampRenamed( oldName, newName ); + emit entityRenamed( ColorrampEntity, oldName, newName ); + } return result; } @@ -733,11 +760,12 @@ bool QgsStyle::saveTextFormat( const QString &name, const QgsTextFormat &format, return false; } - mCachedTextFormatFavorites[ name ] = favorite; + mCachedFavorites[ TextFormatEntity ].insert( name, favorite ); tagSymbol( TextFormatEntity, name, tags ); emit textFormatAdded( name ); + emit entityAdded( TextFormatEntity, name ); return true; } @@ -756,10 +784,11 @@ bool QgsStyle::removeTextFormat( const QString &name ) return false; } - mCachedTextFormatTags.remove( name ); - mCachedTextFormatFavorites.remove( name ); + mCachedTags[ TextFormatEntity ].remove( name ); + mCachedFavorites[ TextFormatEntity ].remove( name ); emit textFormatRemoved( name ); + emit entityRemoved( TextFormatEntity, name ); return true; @@ -778,8 +807,8 @@ bool QgsStyle::renameTextFormat( const QString &oldName, const QString &newName QgsTextFormat format = mTextFormats.take( oldName ); mTextFormats.insert( newName, format ); - mCachedTextFormatTags.remove( oldName ); - mCachedTextFormatFavorites.remove( oldName ); + mCachedTags[ TextFormatEntity ].remove( oldName ); + mCachedFavorites[ TextFormatEntity ].remove( oldName ); int textFormatId = 0; sqlite3_statement_unique_ptr statement; @@ -792,7 +821,10 @@ bool QgsStyle::renameTextFormat( const QString &oldName, const QString &newName } const bool result = rename( TextFormatEntity, textFormatId, newName ); if ( result ) + { emit textFormatRenamed( oldName, newName ); + emit entityRenamed( TextFormatEntity, oldName, newName ); + } return result; } @@ -821,11 +853,12 @@ bool QgsStyle::saveLabelSettings( const QString &name, const QgsPalLayerSettings return false; } - mCachedLabelSettingsFavorites[ name ] = favorite; + mCachedFavorites[ LabelSettingsEntity ].insert( name, favorite ); tagSymbol( LabelSettingsEntity, name, tags ); emit labelSettingsAdded( name ); + emit entityAdded( LabelSettingsEntity, name ); return true; } @@ -844,10 +877,11 @@ bool QgsStyle::removeLabelSettings( const QString &name ) return false; } - mCachedLabelSettingsTags.remove( name ); - mCachedLabelSettingsFavorites.remove( name ); + mCachedTags[ LabelSettingsEntity ].remove( name ); + mCachedFavorites[ LabelSettingsEntity ].remove( name ); emit labelSettingsRemoved( name ); + emit entityRemoved( LabelSettingsEntity, name ); return true; } @@ -865,8 +899,8 @@ bool QgsStyle::renameLabelSettings( const QString &oldName, const QString &newNa QgsPalLayerSettings settings = mLabelSettings.take( oldName ); mLabelSettings.insert( newName, settings ); - mCachedLabelSettingsTags.remove( oldName ); - mCachedLabelSettingsFavorites.remove( oldName ); + mCachedTags[ LabelSettingsEntity ].remove( oldName ); + mCachedFavorites[ LabelSettingsEntity ].remove( oldName ); int labelSettingsId = 0; sqlite3_statement_unique_ptr statement; @@ -879,7 +913,10 @@ bool QgsStyle::renameLabelSettings( const QString &oldName, const QString &newNa } const bool result = rename( LabelSettingsEntity, labelSettingsId, newName ); if ( result ) + { emit labelSettingsRenamed( oldName, newName ); + emit entityRenamed( LabelSettingsEntity, oldName, newName ); + } return result; } @@ -1080,14 +1117,8 @@ bool QgsStyle::rename( StyleEntity type, int id, const QString &newName ) } else { - mCachedColorRampTags.clear(); - mCachedSymbolTags.clear(); - mCachedTextFormatTags.clear(); - mCachedLabelSettingsTags.clear(); - mCachedSymbolFavorites.clear(); - mCachedColorRampFavorites.clear(); - mCachedTextFormatFavorites.clear(); - mCachedLabelSettingsFavorites.clear(); + mCachedTags.clear(); + mCachedFavorites.clear(); switch ( type ) { @@ -1148,14 +1179,8 @@ bool QgsStyle::remove( StyleEntity type, int id ) } else { - mCachedColorRampTags.clear(); - mCachedSymbolTags.clear(); - mCachedTextFormatTags.clear(); - mCachedLabelSettingsTags.clear(); - mCachedSymbolFavorites.clear(); - mCachedColorRampFavorites.clear(); - mCachedTextFormatFavorites.clear(); - mCachedLabelSettingsFavorites.clear(); + mCachedTags.clear(); + mCachedFavorites.clear(); if ( groupRemoved ) { @@ -1216,21 +1241,13 @@ bool QgsStyle::addFavorite( StyleEntity type, const QString &name ) { switch ( type ) { - case SymbolEntity: - mCachedSymbolFavorites[ name ] = true; - break; - case ColorrampEntity: - mCachedColorRampFavorites[ name ] = true; - break; - case TextFormatEntity: - mCachedTextFormatFavorites[ name ] = true; - break; - case LabelSettingsEntity: - mCachedLabelSettingsFavorites[ name ] = true; - break; case TagEntity: case SmartgroupEntity: break; + + default: + mCachedFavorites[ type ].insert( name, true ); + break; } emit favoritedChanged( type, name, true ); } @@ -1268,21 +1285,13 @@ bool QgsStyle::removeFavorite( StyleEntity type, const QString &name ) { switch ( type ) { - case SymbolEntity: - mCachedSymbolFavorites[ name ] = false; - break; - case ColorrampEntity: - mCachedColorRampFavorites[ name ] = false; - break; - case TextFormatEntity: - mCachedTextFormatFavorites[ name ] = false; - break; - case LabelSettingsEntity: - mCachedLabelSettingsFavorites[ name ] = false; - break; case TagEntity: case SmartgroupEntity: break; + + default: + mCachedFavorites[ type ].insert( name, false ); + break; } emit favoritedChanged( type, name, false ); } @@ -1675,28 +1684,13 @@ QStringList QgsStyle::tagsOfSymbol( StyleEntity type, const QString &symbol ) { switch ( type ) { - case SymbolEntity: - if ( mCachedSymbolTags.contains( symbol ) ) - return mCachedSymbolTags.value( symbol ); - break; - - case ColorrampEntity: - if ( mCachedColorRampTags.contains( symbol ) ) - return mCachedColorRampTags.value( symbol ); - break; - - case TextFormatEntity: - if ( mCachedTextFormatTags.contains( symbol ) ) - return mCachedTextFormatTags.value( symbol ); - break; - - case LabelSettingsEntity: - if ( mCachedLabelSettingsTags.contains( symbol ) ) - return mCachedLabelSettingsTags.value( symbol ); - break; - case TagEntity: case SmartgroupEntity: + return QStringList(); + + default: + if ( mCachedTags[ type ].contains( symbol ) ) + return mCachedTags[ type ].value( symbol ); break; } @@ -1779,25 +1773,13 @@ QStringList QgsStyle::tagsOfSymbol( StyleEntity type, const QString &symbol ) // update cache switch ( type ) { - case SymbolEntity: - mCachedSymbolTags[ symbol ] = tagList; - break; - - case ColorrampEntity: - mCachedColorRampTags[ symbol ] = tagList; - break; - - case TextFormatEntity: - mCachedTextFormatTags[ symbol ] = tagList; - break; - - case LabelSettingsEntity: - mCachedLabelSettingsTags[ symbol ] = tagList; - break; - case TagEntity: case SmartgroupEntity: break; + + default: + mCachedTags[ type ].insert( symbol, tagList ); + break; } return tagList; @@ -1813,29 +1795,14 @@ bool QgsStyle::isFavorite( QgsStyle::StyleEntity type, const QString &name ) switch ( type ) { - case SymbolEntity: - if ( mCachedSymbolFavorites.contains( name ) ) - return mCachedSymbolFavorites.value( name ); - break; - - case ColorrampEntity: - if ( mCachedColorRampFavorites.contains( name ) ) - return mCachedColorRampFavorites.value( name ); - break; - - case TextFormatEntity: - if ( mCachedTextFormatFavorites.contains( name ) ) - return mCachedTextFormatFavorites.value( name ); - break; - - case LabelSettingsEntity: - if ( mCachedLabelSettingsFavorites.contains( name ) ) - return mCachedLabelSettingsFavorites.value( name ); - break; - case TagEntity: case SmartgroupEntity: return false; + + default: + if ( mCachedFavorites[ type ].contains( name ) ) + return mCachedFavorites[ type ].value( name ); + break; } const QStringList names = allNames( type ); @@ -1851,28 +1818,7 @@ bool QgsStyle::isFavorite( QgsStyle::StyleEntity type, const QString &name ) if ( n == name ) res = isFav; - switch ( type ) - { - case SymbolEntity: - mCachedSymbolFavorites[n] = isFav; - break; - - case ColorrampEntity: - mCachedColorRampFavorites[ n ] = isFav; - break; - - case TextFormatEntity: - mCachedTextFormatFavorites[ n ] = isFav; - break; - - case LabelSettingsEntity: - mCachedLabelSettingsFavorites[ n ] = isFav; - break; - - case TagEntity: - case SmartgroupEntity: - return false; - } + mCachedFavorites[ type ].insert( n, isFav ); } return res; } @@ -2852,34 +2798,14 @@ bool QgsStyle::updateSymbol( StyleEntity type, const QString &name ) case SmartgroupEntity: break; } + emit entityChanged( type, name ); } return true; } void QgsStyle::clearCachedTags( QgsStyle::StyleEntity type, const QString &name ) { - switch ( type ) - { - case SymbolEntity: - mCachedSymbolTags.remove( name ); - break; - - case ColorrampEntity: - mCachedColorRampTags.remove( name ); - break; - - case TextFormatEntity: - mCachedTextFormatTags.remove( name ); - break; - - case LabelSettingsEntity: - mCachedLabelSettingsTags.remove( name ); - break; - - case TagEntity: - case SmartgroupEntity: - break; - } + mCachedTags[ type ].remove( name ); } QgsStyle::StyleEntity QgsStyleSymbolEntity::type() const diff --git a/src/core/symbology/qgsstyle.h b/src/core/symbology/qgsstyle.h index 9960361f6dcd..0bbc2fe4535e 100644 --- a/src/core/symbology/qgsstyle.h +++ b/src/core/symbology/qgsstyle.h @@ -412,6 +412,15 @@ class CORE_EXPORT QgsStyle : public QObject //! Removes symbol from style (and delete it) bool removeSymbol( const QString &name ); + /** + * Renames an entity of the specified \a type from \a oldName to \a newName. + * + * Returns TRUE if the entity was successfully renamed. + * + * \since QGIS 3.14 + */ + bool renameEntity( StyleEntity type, const QString &oldName, const QString &newName ); + /** * Renames a symbol from \a oldName to \a newName. * @@ -736,6 +745,35 @@ class CORE_EXPORT QgsStyle : public QObject */ void favoritedChanged( QgsStyle::StyleEntity entity, const QString &name, bool isFavorite ); + /** + * Emitted every time a new entity has been added to the database. + * + * \since QGIS 3.14 + */ + void entityAdded( QgsStyle::StyleEntity entity, const QString &name ); + + /** + * Emitted whenever an entity of the specified type is removed from the style and the database + * has been updated as a result. + * + * \since QGIS 3.14 + */ + void entityRemoved( QgsStyle::StyleEntity entity, const QString &name ); + + /** + * Emitted whenever a entity of the specified type has been renamed from \a oldName to \a newName + * \since QGIS 3.14 + */ + void entityRenamed( QgsStyle::StyleEntity entity, const QString &oldName, const QString &newName ); + + /** + * Emitted whenever an entity's definition is changed. This does not include + * name or tag changes. + * + * \since QGIS 3.14 + */ + void entityChanged( QgsStyle::StyleEntity entity, const QString &name ); + /** * Emitted whenever a symbol has been removed from the style and the database * has been updated as a result. @@ -865,15 +903,8 @@ class CORE_EXPORT QgsStyle : public QObject QgsTextFormatMap mTextFormats; QgsLabelSettingsMap mLabelSettings; - QHash< QString, QStringList > mCachedSymbolTags; - QHash< QString, QStringList > mCachedColorRampTags; - QHash< QString, QStringList > mCachedTextFormatTags; - QHash< QString, QStringList > mCachedLabelSettingsTags; - - QHash< QString, bool > mCachedSymbolFavorites; - QHash< QString, bool > mCachedColorRampFavorites; - QHash< QString, bool > mCachedTextFormatFavorites; - QHash< QString, bool > mCachedLabelSettingsFavorites; + QHash< QgsStyle::StyleEntity, QHash< QString, QStringList > > mCachedTags; + QHash< QgsStyle::StyleEntity, QHash< QString, bool > > mCachedFavorites; QString mErrorString; QString mFileName; diff --git a/src/core/symbology/qgsstylemodel.cpp b/src/core/symbology/qgsstylemodel.cpp index d73e77f4f0d4..e3705cdd36e5 100644 --- a/src/core/symbology/qgsstylemodel.cpp +++ b/src/core/symbology/qgsstylemodel.cpp @@ -25,36 +25,23 @@ const double ICON_PADDING_FACTOR = 0.16; +const auto ENTITIES = { QgsStyle::SymbolEntity, QgsStyle::ColorrampEntity, QgsStyle::TextFormatEntity, QgsStyle::LabelSettingsEntity }; + QgsStyleModel::QgsStyleModel( QgsStyle *style, QObject *parent ) : QAbstractItemModel( parent ) , mStyle( style ) { Q_ASSERT( mStyle ); - mSymbolNames = mStyle->symbolNames(); - mRampNames = mStyle->colorRampNames(); - mTextFormatNames = mStyle->textFormatNames(); - mLabelSettingsNames = mStyle->labelSettingsNames(); - - connect( mStyle, &QgsStyle::symbolSaved, this, &QgsStyleModel::onSymbolAdded ); - connect( mStyle, &QgsStyle::symbolRemoved, this, &QgsStyleModel::onSymbolRemoved ); - connect( mStyle, &QgsStyle::symbolRenamed, this, &QgsStyleModel::onSymbolRename ); - connect( mStyle, &QgsStyle::symbolChanged, this, &QgsStyleModel::onSymbolChanged ); - - connect( mStyle, &QgsStyle::rampAdded, this, &QgsStyleModel::onRampAdded ); - connect( mStyle, &QgsStyle::rampChanged, this, &QgsStyleModel::onRampChanged ); - connect( mStyle, &QgsStyle::rampRemoved, this, &QgsStyleModel::onRampRemoved ); - connect( mStyle, &QgsStyle::rampRenamed, this, &QgsStyleModel::onRampRename ); - - connect( mStyle, &QgsStyle::textFormatAdded, this, &QgsStyleModel::onTextFormatAdded ); - connect( mStyle, &QgsStyle::textFormatChanged, this, &QgsStyleModel::onTextFormatChanged ); - connect( mStyle, &QgsStyle::textFormatRemoved, this, &QgsStyleModel::onTextFormatRemoved ); - connect( mStyle, &QgsStyle::textFormatRenamed, this, &QgsStyleModel::onTextFormatRename ); - - connect( mStyle, &QgsStyle::labelSettingsAdded, this, &QgsStyleModel::onLabelSettingsAdded ); - connect( mStyle, &QgsStyle::labelSettingsChanged, this, &QgsStyleModel::onLabelSettingsChanged ); - connect( mStyle, &QgsStyle::labelSettingsRemoved, this, &QgsStyleModel::onLabelSettingsRemoved ); - connect( mStyle, &QgsStyle::labelSettingsRenamed, this, &QgsStyleModel::onLabelSettingsRename ); + for ( QgsStyle::StyleEntity entity : ENTITIES ) + { + mEntityNames.insert( entity, mStyle->allNames( entity ) ); + } + + connect( mStyle, &QgsStyle::entityAdded, this, &QgsStyleModel::onEntityAdded ); + connect( mStyle, &QgsStyle::entityRemoved, this, &QgsStyleModel::onEntityRemoved ); + connect( mStyle, &QgsStyle::entityRenamed, this, &QgsStyleModel::onEntityRename ); + connect( mStyle, &QgsStyle::entityChanged, this, &QgsStyleModel::onEntityChanged ); connect( mStyle, &QgsStyle::entityTagsChanged, this, &QgsStyleModel::onTagsChanged ); // when a remote svg or image has been fetched, update the model's decorations. @@ -81,25 +68,13 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const QString name; switch ( entityType ) { - case QgsStyle::SymbolEntity: - name = mSymbolNames.value( index.row() ); - break; - - case QgsStyle::ColorrampEntity: - name = mRampNames.value( index.row() - mSymbolNames.size() ); - break; - - case QgsStyle::TextFormatEntity: - name = mTextFormatNames.value( index.row() - mSymbolNames.size() - mRampNames.size() ); - break; - - case QgsStyle::LabelSettingsEntity: - name = mLabelSettingsNames.value( index.row() - mSymbolNames.size() - mRampNames.size() - mTextFormatNames.size() ); - break; - case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: break; + + default: + name = mEntityNames[ entityType ].value( index.row() - offsetForEntity( entityType ) ); + break; } switch ( role ) @@ -217,7 +192,7 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const case QgsStyle::SymbolEntity: { // use cached icon if possible - QIcon icon = mSymbolIconCache.value( name ); + QIcon icon = mIconCache[ entityType ].value( name ); if ( !icon.isNull() ) return icon; @@ -233,13 +208,13 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const } } - mSymbolIconCache.insert( name, icon ); + mIconCache[ entityType ].insert( name, icon ); return icon; } case QgsStyle::ColorrampEntity: { // use cached icon if possible - QIcon icon = mColorRampIconCache.value( name ); + QIcon icon = mIconCache[ entityType ].value( name ); if ( !icon.isNull() ) return icon; @@ -254,14 +229,14 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const } } - mColorRampIconCache.insert( name, icon ); + mIconCache[ entityType ].insert( name, icon ); return icon; } case QgsStyle::TextFormatEntity: { // use cached icon if possible - QIcon icon = mTextFormatIconCache.value( name ); + QIcon icon = mIconCache[ entityType ].value( name ); if ( !icon.isNull() ) return icon; @@ -272,14 +247,14 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const { icon.addPixmap( QgsTextFormat::textFormatPreviewPixmap( format, s, QString(), static_cast< int >( s.width() * ICON_PADDING_FACTOR ) ) ); } - mTextFormatIconCache.insert( name, icon ); + mIconCache[ entityType ].insert( name, icon ); return icon; } case QgsStyle::LabelSettingsEntity: { // use cached icon if possible - QIcon icon = mLabelSettingsIconCache.value( name ); + QIcon icon = mIconCache[ entityType ].value( name ); if ( !icon.isNull() ) return icon; @@ -290,7 +265,7 @@ QVariant QgsStyleModel::data( const QModelIndex &index, int role ) const { icon.addPixmap( QgsPalLayerSettings::labelSettingsPreviewPixmap( settings, s, QString(), static_cast< int >( s.width() * ICON_PADDING_FACTOR ) ) ); } - mLabelSettingsIconCache.insert( name, icon ); + mIconCache[ entityType ].insert( name, icon ); return icon; } @@ -353,48 +328,17 @@ bool QgsStyleModel::setData( const QModelIndex &index, const QVariant &value, in QString name; switch ( entityType ) { - case QgsStyle::SymbolEntity: - name = mSymbolNames.value( index.row() ); - break; - - case QgsStyle::ColorrampEntity: - name = mRampNames.value( index.row() - mSymbolNames.size() ); - break; - - case QgsStyle::TextFormatEntity: - name = mTextFormatNames.value( index.row() - mSymbolNames.size() - mRampNames.size() ); - break; - - case QgsStyle::LabelSettingsEntity: - name = mLabelSettingsNames.value( index.row() - mSymbolNames.size() - mRampNames.size() - mTextFormatNames.size() ); - break; - case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: + return false; + + default: + name = mEntityNames[ entityType ].value( index.row() - offsetForEntity( entityType ) ); break; } const QString newName = value.toString(); - - switch ( entityType ) - { - case QgsStyle::SymbolEntity: - return mStyle->renameSymbol( name, newName ); - - case QgsStyle::ColorrampEntity: - return mStyle->renameColorRamp( name, newName ); - - case QgsStyle::TextFormatEntity: - return mStyle->renameTextFormat( name, newName ); - - case QgsStyle::LabelSettingsEntity: - return mStyle->renameLabelSettings( name, newName ); - - case QgsStyle::TagEntity: - case QgsStyle::SmartgroupEntity: - return false; - } - break; + return mStyle->renameEntity( entityType, name, newName ); } case Tags: @@ -469,7 +413,10 @@ int QgsStyleModel::rowCount( const QModelIndex &parent ) const { if ( !parent.isValid() ) { - return mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count() + mLabelSettingsNames.count(); + int count = 0; + for ( QgsStyle::StyleEntity type : ENTITIES ) + count += mEntityNames[ type ].size(); + return count; } return 0; } @@ -485,57 +432,57 @@ void QgsStyleModel::addDesiredIconSize( QSize size ) return; mAdditionalSizes << size; - mSymbolIconCache.clear(); - mColorRampIconCache.clear(); - mTextFormatIconCache.clear(); - mLabelSettingsIconCache.clear(); + mIconCache.clear(); } -void QgsStyleModel::onSymbolAdded( const QString &name, QgsSymbol * ) +void QgsStyleModel::onEntityAdded( QgsStyle::StyleEntity type, const QString &name ) { - mSymbolIconCache.remove( name ); - const QStringList oldSymbolNames = mSymbolNames; - const QStringList newSymbolNames = mStyle->symbolNames(); + mIconCache[ type ].remove( name ); + const QStringList oldSymbolNames = mEntityNames[ type ]; + const QStringList newSymbolNames = mStyle->allNames( type ); // find index of newly added symbol const int newNameIndex = newSymbolNames.indexOf( name ); if ( newNameIndex < 0 ) return; // shouldn't happen - beginInsertRows( QModelIndex(), newNameIndex, newNameIndex ); - mSymbolNames = newSymbolNames; + const int offset = offsetForEntity( type ); + beginInsertRows( QModelIndex(), newNameIndex + offset, newNameIndex + offset ); + mEntityNames[ type ] = newSymbolNames; endInsertRows(); } -void QgsStyleModel::onSymbolRemoved( const QString &name ) +void QgsStyleModel::onEntityRemoved( QgsStyle::StyleEntity type, const QString &name ) { - mSymbolIconCache.remove( name ); - const QStringList oldSymbolNames = mSymbolNames; - const QStringList newSymbolNames = mStyle->symbolNames(); + mIconCache[ type ].remove( name ); + const QStringList oldSymbolNames = mEntityNames[ type ]; + const QStringList newSymbolNames = mStyle->allNames( type ); // find index of removed symbol const int oldNameIndex = oldSymbolNames.indexOf( name ); if ( oldNameIndex < 0 ) return; // shouldn't happen - beginRemoveRows( QModelIndex(), oldNameIndex, oldNameIndex ); - mSymbolNames = newSymbolNames; + const int offset = offsetForEntity( type ); + beginRemoveRows( QModelIndex(), oldNameIndex + offset, oldNameIndex + offset ); + mEntityNames[ type ] = newSymbolNames; endRemoveRows(); } -void QgsStyleModel::onSymbolChanged( const QString &name ) +void QgsStyleModel::onEntityChanged( QgsStyle::StyleEntity type, const QString &name ) { - mSymbolIconCache.remove( name ); + mIconCache[ type ].remove( name ); - QModelIndex i = index( mSymbolNames.indexOf( name ), Tags ); + const int offset = offsetForEntity( type ); + QModelIndex i = index( offset + mEntityNames[ type ].indexOf( name ), Tags ); emit dataChanged( i, i, QVector< int >() << Qt::DecorationRole ); } -void QgsStyleModel::onSymbolRename( const QString &oldName, const QString &newName ) +void QgsStyleModel::onEntityRename( QgsStyle::StyleEntity type, const QString &oldName, const QString &newName ) { - mSymbolIconCache.remove( oldName ); - const QStringList oldSymbolNames = mSymbolNames; - const QStringList newSymbolNames = mStyle->symbolNames(); + mIconCache[ type ].remove( oldName ); + const QStringList oldSymbolNames = mEntityNames[ type ]; + const QStringList newSymbolNames = mStyle->allNames( type ); // find index of removed symbol const int oldNameIndex = oldSymbolNames.indexOf( oldName ); @@ -549,265 +496,68 @@ void QgsStyleModel::onSymbolRename( const QString &oldName, const QString &newNa if ( newNameIndex == oldNameIndex ) { - mSymbolNames = newSymbolNames; - return; - } - - beginMoveRows( QModelIndex(), oldNameIndex, oldNameIndex, QModelIndex(), newNameIndex > oldNameIndex ? newNameIndex + 1 : newNameIndex ); - mSymbolNames = newSymbolNames; - endMoveRows(); -} - -void QgsStyleModel::onRampAdded( const QString &name ) -{ - mColorRampIconCache.remove( name ); - const QStringList oldRampNames = mRampNames; - const QStringList newRampNames = mStyle->colorRampNames(); - - // find index of newly added symbol - const int newNameIndex = newRampNames.indexOf( name ); - if ( newNameIndex < 0 ) - return; // shouldn't happen - - beginInsertRows( QModelIndex(), newNameIndex + mSymbolNames.count(), newNameIndex + mSymbolNames.count() ); - mRampNames = newRampNames; - endInsertRows(); -} - -void QgsStyleModel::onRampRemoved( const QString &name ) -{ - mColorRampIconCache.remove( name ); - const QStringList oldRampNames = mRampNames; - const QStringList newRampNames = mStyle->colorRampNames(); - - // find index of removed symbol - const int oldNameIndex = oldRampNames.indexOf( name ); - if ( oldNameIndex < 0 ) - return; // shouldn't happen - - beginRemoveRows( QModelIndex(), oldNameIndex + mSymbolNames.count(), oldNameIndex + mSymbolNames.count() ); - mRampNames = newRampNames; - endRemoveRows(); -} - -void QgsStyleModel::onRampChanged( const QString &name ) -{ - mColorRampIconCache.remove( name ); - - QModelIndex i = index( mSymbolNames.count() + mRampNames.indexOf( name ), Tags ); - emit dataChanged( i, i, QVector< int >() << Qt::DecorationRole ); -} - -void QgsStyleModel::onRampRename( const QString &oldName, const QString &newName ) -{ - mColorRampIconCache.remove( oldName ); - const QStringList oldRampNames = mRampNames; - const QStringList newRampNames = mStyle->colorRampNames(); - - // find index of removed ramp - const int oldNameIndex = oldRampNames.indexOf( oldName ); - if ( oldNameIndex < 0 ) - return; // shouldn't happen - - // find index of newly added ramp - const int newNameIndex = newRampNames.indexOf( newName ); - if ( newNameIndex < 0 ) - return; // shouldn't happen - - if ( newNameIndex == oldNameIndex ) - { - mRampNames = newRampNames; - return; - } - - beginMoveRows( QModelIndex(), oldNameIndex + mSymbolNames.count(), oldNameIndex + mSymbolNames.count(), - QModelIndex(), ( newNameIndex > oldNameIndex ? newNameIndex + 1 : newNameIndex ) + mSymbolNames.count() ); - mRampNames = newRampNames; - endMoveRows(); -} - -void QgsStyleModel::onTextFormatAdded( const QString &name ) -{ - mTextFormatIconCache.remove( name ); - const QStringList oldTextFormatNames = mTextFormatNames; - const QStringList newTextFormatNames = mStyle->textFormatNames(); - - // find index of newly added symbol - const int newNameIndex = newTextFormatNames.indexOf( name ); - if ( newNameIndex < 0 ) - return; // shouldn't happen - - beginInsertRows( QModelIndex(), newNameIndex + mSymbolNames.count() + mRampNames.count(), newNameIndex + mSymbolNames.count() + mRampNames.count() ); - mTextFormatNames = newTextFormatNames; - endInsertRows(); -} - -void QgsStyleModel::onTextFormatRemoved( const QString &name ) -{ - mTextFormatIconCache.remove( name ); - const QStringList oldTextFormatNames = mTextFormatNames; - const QStringList newTextFormatNames = mStyle->textFormatNames(); - - // find index of removed symbol - const int oldNameIndex = oldTextFormatNames.indexOf( name ); - if ( oldNameIndex < 0 ) - return; // shouldn't happen - - beginRemoveRows( QModelIndex(), oldNameIndex + mSymbolNames.count() + mRampNames.count(), oldNameIndex + mSymbolNames.count() + mRampNames.count() ); - mTextFormatNames = newTextFormatNames; - endRemoveRows(); -} - -void QgsStyleModel::onTextFormatChanged( const QString &name ) -{ - mTextFormatIconCache.remove( name ); - - QModelIndex i = index( mSymbolNames.count() + mRampNames.count() + mTextFormatNames.indexOf( name ), Tags ); - emit dataChanged( i, i, QVector< int >() << Qt::DecorationRole ); -} - -void QgsStyleModel::onTextFormatRename( const QString &oldName, const QString &newName ) -{ - mTextFormatIconCache.remove( oldName ); - const QStringList oldTextFormatNames = mTextFormatNames; - const QStringList newTextFormatNames = mStyle->textFormatNames(); - - // find index of removed format - const int oldNameIndex = oldTextFormatNames.indexOf( oldName ); - if ( oldNameIndex < 0 ) - return; // shouldn't happen - - // find index of newly added format - const int newNameIndex = newTextFormatNames.indexOf( newName ); - if ( newNameIndex < 0 ) - return; // shouldn't happen - - if ( newNameIndex == oldNameIndex ) - { - mTextFormatNames = newTextFormatNames; + mEntityNames[ type ] = newSymbolNames; return; } - beginMoveRows( QModelIndex(), oldNameIndex + mSymbolNames.count() + mRampNames.count(), oldNameIndex + mSymbolNames.count() + mRampNames.count(), - QModelIndex(), ( newNameIndex > oldNameIndex ? newNameIndex + 1 : newNameIndex ) + mSymbolNames.count() + mRampNames.count() ); - mTextFormatNames = newTextFormatNames; - endMoveRows(); -} - -void QgsStyleModel::onLabelSettingsAdded( const QString &name ) -{ - mLabelSettingsIconCache.remove( name ); - const QStringList oldLabelSettingsNames = mLabelSettingsNames; - const QStringList newLabelSettingsNames = mStyle->labelSettingsNames(); - - // find index of newly added symbol - const int newNameIndex = newLabelSettingsNames.indexOf( name ); - if ( newNameIndex < 0 ) - return; // shouldn't happen - - beginInsertRows( QModelIndex(), newNameIndex + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count(), newNameIndex + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count() ); - mLabelSettingsNames = newLabelSettingsNames; - endInsertRows(); -} - -void QgsStyleModel::onLabelSettingsRemoved( const QString &name ) -{ - mLabelSettingsIconCache.remove( name ); - const QStringList oldLabelSettingsNames = mLabelSettingsNames; - const QStringList newLabelSettingsNames = mStyle->labelSettingsNames(); - - // find index of removed symbol - const int oldNameIndex = oldLabelSettingsNames.indexOf( name ); - if ( oldNameIndex < 0 ) - return; // shouldn't happen - - beginRemoveRows( QModelIndex(), oldNameIndex + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count(), oldNameIndex + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count() ); - mLabelSettingsNames = newLabelSettingsNames; - endRemoveRows(); -} - -void QgsStyleModel::onLabelSettingsChanged( const QString &name ) -{ - mLabelSettingsIconCache.remove( name ); - - QModelIndex i = index( mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count() + mLabelSettingsNames.indexOf( name ), Tags ); - emit dataChanged( i, i, QVector< int >() << Qt::DecorationRole ); -} - -void QgsStyleModel::onLabelSettingsRename( const QString &oldName, const QString &newName ) -{ - mLabelSettingsIconCache.remove( oldName ); - const QStringList oldLabelSettingsNames = mLabelSettingsNames; - const QStringList newLabelSettingsNames = mStyle->labelSettingsNames(); - - // find index of removed format - const int oldNameIndex = oldLabelSettingsNames.indexOf( oldName ); - if ( oldNameIndex < 0 ) - return; // shouldn't happen - - // find index of newly added format - const int newNameIndex = newLabelSettingsNames.indexOf( newName ); - if ( newNameIndex < 0 ) - return; // shouldn't happen - - if ( newNameIndex == oldNameIndex ) - { - mLabelSettingsNames = newLabelSettingsNames; - return; - } - - beginMoveRows( QModelIndex(), oldNameIndex + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count(), oldNameIndex + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count(), - QModelIndex(), ( newNameIndex > oldNameIndex ? newNameIndex + 1 : newNameIndex ) + mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count() ); - mLabelSettingsNames = newLabelSettingsNames; + const int offset = offsetForEntity( type ); + beginMoveRows( QModelIndex(), oldNameIndex + offset, oldNameIndex + offset, QModelIndex(), ( newNameIndex > oldNameIndex ? newNameIndex + 1 : newNameIndex ) + offset ); + mEntityNames[ type ] = newSymbolNames; endMoveRows(); } void QgsStyleModel::onTagsChanged( int entity, const QString &name, const QStringList & ) { + QgsStyle::StyleEntity type = static_cast< QgsStyle::StyleEntity >( entity ); QModelIndex i; + int row = mEntityNames[type].indexOf( name ) + offsetForEntity( type ); switch ( static_cast< QgsStyle::StyleEntity >( entity ) ) { - case QgsStyle::SymbolEntity: - i = index( mSymbolNames.indexOf( name ), Tags ); - break; - - case QgsStyle::ColorrampEntity: - i = index( mSymbolNames.count() + mRampNames.indexOf( name ), Tags ); - break; - - case QgsStyle::TextFormatEntity: - i = index( mSymbolNames.count() + mRampNames.count() + mTextFormatNames.indexOf( name ), Tags ); - break; - - case QgsStyle::LabelSettingsEntity: - i = index( mSymbolNames.count() + mRampNames.count() + mTextFormatNames.count() + mLabelSettingsNames.indexOf( name ), Tags ); - break; - case QgsStyle::TagEntity: case QgsStyle::SmartgroupEntity: return; + + default: + i = index( row, Tags ); } emit dataChanged( i, i ); } void QgsStyleModel::rebuildSymbolIcons() { - mSymbolIconCache.clear(); + mIconCache[ QgsStyle::SymbolEntity ].clear(); mExpressionContext.reset(); - emit dataChanged( index( 0, 0 ), index( mSymbolNames.count() - 1, 0 ), QVector() << Qt::DecorationRole ); + emit dataChanged( index( 0, 0 ), index( mEntityNames[ QgsStyle::SymbolEntity ].count() - 1, 0 ), QVector() << Qt::DecorationRole ); } QgsStyle::StyleEntity QgsStyleModel::entityTypeFromRow( int row ) const { - if ( row >= mStyle->symbolCount() + mStyle->colorRampCount() + + mTextFormatNames.count() ) - return QgsStyle::LabelSettingsEntity; - else if ( row >= mStyle->symbolCount() + mStyle->colorRampCount() ) - return QgsStyle::TextFormatEntity; - else if ( row >= mStyle->symbolCount() ) - return QgsStyle::ColorrampEntity; + int maxRowForEntity = 0; + for ( QgsStyle::StyleEntity type : ENTITIES ) + { + maxRowForEntity += mEntityNames[ type ].size(); + if ( row < maxRowForEntity ) + return type; + } + + // should never happen + Q_ASSERT( false ); return QgsStyle::SymbolEntity; } +int QgsStyleModel::offsetForEntity( QgsStyle::StyleEntity entity ) const +{ + int offset = 0; + for ( QgsStyle::StyleEntity type : ENTITIES ) + { + if ( type == entity ) + return offset; + + offset += mEntityNames[ type ].size(); + } + return 0; +} + // // QgsStyleProxyModel // @@ -984,16 +734,11 @@ void QgsStyleProxyModel::setTagId( int id ) { mTagId = id; + mTaggedSymbolNames.clear(); if ( mTagId >= 0 ) { - mTaggedSymbolNames = mStyle->symbolsWithTag( QgsStyle::SymbolEntity, mTagId ); - mTaggedSymbolNames.append( mStyle->symbolsWithTag( QgsStyle::ColorrampEntity, mTagId ) ); - mTaggedSymbolNames.append( mStyle->symbolsWithTag( QgsStyle::TextFormatEntity, mTagId ) ); - mTaggedSymbolNames.append( mStyle->symbolsWithTag( QgsStyle::LabelSettingsEntity, mTagId ) ); - } - else - { - mTaggedSymbolNames.clear(); + for ( QgsStyle::StyleEntity entity : ENTITIES ) + mTaggedSymbolNames.append( mStyle->symbolsWithTag( entity, mTagId ) ); } invalidateFilter(); @@ -1008,18 +753,12 @@ void QgsStyleProxyModel::setSmartGroupId( int id ) { mSmartGroupId = id; + mSmartGroupSymbolNames.clear(); if ( mSmartGroupId >= 0 ) { - mSmartGroupSymbolNames = mStyle->symbolsOfSmartgroup( QgsStyle::SymbolEntity, mSmartGroupId ); - mSmartGroupSymbolNames.append( mStyle->symbolsOfSmartgroup( QgsStyle::ColorrampEntity, mSmartGroupId ) ); - mSmartGroupSymbolNames.append( mStyle->symbolsOfSmartgroup( QgsStyle::TextFormatEntity, mSmartGroupId ) ); - mSmartGroupSymbolNames.append( mStyle->symbolsOfSmartgroup( QgsStyle::LabelSettingsEntity, mSmartGroupId ) ); + for ( QgsStyle::StyleEntity entity : ENTITIES ) + mSmartGroupSymbolNames.append( mStyle->symbolsOfSmartgroup( entity, mSmartGroupId ) ); } - else - { - mSmartGroupSymbolNames.clear(); - } - invalidateFilter(); } diff --git a/src/core/symbology/qgsstylemodel.h b/src/core/symbology/qgsstylemodel.h index a6e5eb36c896..ca23ed5cd53e 100644 --- a/src/core/symbology/qgsstylemodel.h +++ b/src/core/symbology/qgsstylemodel.h @@ -100,45 +100,28 @@ class CORE_EXPORT QgsStyleModel: public QAbstractItemModel private slots: - void onSymbolAdded( const QString &name, QgsSymbol *symbol ); - void onSymbolRemoved( const QString &name ); - void onSymbolChanged( const QString &name ); - void onSymbolRename( const QString &oldName, const QString &newName ); - void onRampAdded( const QString &name ); - void onRampRemoved( const QString &name ); - void onRampChanged( const QString &name ); - void onRampRename( const QString &oldName, const QString &newName ); - - void onTextFormatAdded( const QString &name ); - void onTextFormatRemoved( const QString &name ); - void onTextFormatChanged( const QString &name ); - void onTextFormatRename( const QString &oldName, const QString &newName ); - - void onLabelSettingsAdded( const QString &name ); - void onLabelSettingsRemoved( const QString &name ); - void onLabelSettingsChanged( const QString &name ); - void onLabelSettingsRename( const QString &oldName, const QString &newName ); - + void onEntityAdded( QgsStyle::StyleEntity type, const QString &name ); + void onEntityRemoved( QgsStyle::StyleEntity type, const QString &name ); + void onEntityChanged( QgsStyle::StyleEntity type, const QString &name ); + void onEntityRename( QgsStyle::StyleEntity type, const QString &oldName, const QString &newName ); void onTagsChanged( int entity, const QString &name, const QStringList &tags ); void rebuildSymbolIcons(); private: QgsStyle *mStyle = nullptr; - QStringList mSymbolNames; - QStringList mRampNames; - QStringList mTextFormatNames; - QStringList mLabelSettingsNames; + + QHash< QgsStyle::StyleEntity, QStringList > mEntityNames; + QList< QSize > mAdditionalSizes; mutable std::unique_ptr< QgsExpressionContext > mExpressionContext; - mutable QHash< QString, QIcon > mSymbolIconCache; - mutable QHash< QString, QIcon > mColorRampIconCache; - mutable QHash< QString, QIcon > mTextFormatIconCache; - mutable QHash< QString, QIcon > mLabelSettingsIconCache; + mutable QHash< QgsStyle::StyleEntity, QHash< QString, QIcon > > mIconCache; QgsStyle::StyleEntity entityTypeFromRow( int row ) const; + int offsetForEntity( QgsStyle::StyleEntity entity ) const; + }; /** diff --git a/src/core/symbology/qgssymbol.cpp b/src/core/symbology/qgssymbol.cpp index 1e4a12d6b80d..ffe6ea265ccf 100644 --- a/src/core/symbology/qgssymbol.cpp +++ b/src/core/symbology/qgssymbol.cpp @@ -50,6 +50,7 @@ #include "qgsapplication.h" #include "qgsexpressioncontextutils.h" #include "qgsrenderedfeaturehandlerinterface.h" +#include "qgslegendpatchshape.h" inline QgsProperty rotateWholeSymbol( double additionalRotation, const QgsProperty &property ) @@ -499,7 +500,7 @@ QColor QgsSymbol::color() const return QColor( 0, 0, 0 ); } -void QgsSymbol::drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext *customContext, bool selected, const QgsExpressionContext *expressionContext ) +void QgsSymbol::drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext *customContext, bool selected, const QgsExpressionContext *expressionContext, const QgsLegendPatchShape *patchShape ) { QgsRenderContext *context = customContext; std::unique_ptr< QgsRenderContext > tempContext; @@ -514,6 +515,8 @@ void QgsSymbol::drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext QgsSymbolRenderContext symbolContext( *context, QgsUnitTypes::RenderUnknownUnit, mOpacity, false, mRenderHints, nullptr ); symbolContext.setSelected( selected ); symbolContext.setOriginalGeometryType( mType == Fill ? QgsWkbTypes::PolygonGeometry : QgsWkbTypes::UnknownGeometry ); + if ( patchShape ) + symbolContext.setPatchShape( *patchShape ); if ( !customContext && expressionContext ) { @@ -539,19 +542,31 @@ void QgsSymbol::drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext QgsLineSymbolLayer *lsl = dynamic_cast( layer ); if ( lsl ) { - // from QgsFillSymbolLayer::drawPreviewIcon() - QPolygonF poly = QRectF( QPointF( 0, 0 ), QPointF( size.width() - 1, size.height() - 1 ) ); + // from QgsFillSymbolLayer::drawPreviewIcon() -- would be nicer to add the + // symbol type to QgsSymbolLayer::drawPreviewIcon so this logic could be avoided! + + // hmm... why was this using size -1 ?? + const QSizeF targetSize = QSizeF( size.width() - 1, size.height() - 1 ); + + const QList< QList< QPolygonF > > polys = patchShape ? patchShape->toQPolygonF( QgsSymbol::Fill, targetSize ) + : QgsLegendPatchShape::defaultPatch( QgsSymbol::Fill, targetSize ); + lsl->startRender( symbolContext ); QgsPaintEffect *effect = lsl->paintEffect(); + + std::unique_ptr< QgsEffectPainter > effectPainter; if ( effect && effect->enabled() ) + effectPainter = qgis::make_unique< QgsEffectPainter >( symbolContext.renderContext(), effect ); + + for ( const QList< QPolygonF > &poly : polys ) { - QgsEffectPainter p( symbolContext.renderContext(), effect ); - lsl->renderPolygonStroke( poly, nullptr, symbolContext ); - } - else - { - lsl->renderPolygonStroke( poly, nullptr, symbolContext ); + QList< QPolygonF > rings; + for ( int i = 1; i < poly.size(); ++i ) + rings << poly.at( i ); + lsl->renderPolygonStroke( poly.value( 0 ), &rings, symbolContext ); } + + effectPainter.reset(); lsl->stopRender( symbolContext ); } } @@ -1248,6 +1263,8 @@ QgsSymbolRenderContext::QgsSymbolRenderContext( QgsRenderContext &c, QgsUnitType { } +QgsSymbolRenderContext::~QgsSymbolRenderContext() = default; + void QgsSymbolRenderContext::setOriginalValueVariable( const QVariant &value ) { mRenderContext.expressionContext().setOriginalValueVariable( value ); @@ -1283,6 +1300,16 @@ void QgsSymbolRenderContext::setExpressionContextScope( QgsExpressionContextScop mExpressionContextScope.reset( contextScope ); } +const QgsLegendPatchShape *QgsSymbolRenderContext::patchShape() const +{ + return mPatchShape.get(); +} + +void QgsSymbolRenderContext::setPatchShape( const QgsLegendPatchShape &patchShape ) +{ + mPatchShape.reset( new QgsLegendPatchShape( patchShape ) ); +} + /////////////////// QgsMarkerSymbol *QgsMarkerSymbol::createSimple( const QgsStringMap &properties ) diff --git a/src/core/symbology/qgssymbol.h b/src/core/symbology/qgssymbol.h index 495131033d09..50bf6046c8a9 100644 --- a/src/core/symbology/qgssymbol.h +++ b/src/core/symbology/qgssymbol.h @@ -50,6 +50,7 @@ class QgsCurve; class QgsPolygon; class QgsExpressionContext; class QgsPoint; +class QgsLegendPatchShape; typedef QList QgsSymbolLayerList; @@ -336,13 +337,15 @@ class CORE_EXPORT QgsSymbol * \param customContext the context in which the rendering happens * \param selected set to TRUE to render the symbol in a selected state * \param expressionContext optional custom expression context + * \param patchShape optional patch shape to use for symbol preview. If not specified a default shape will be used instead. * * \see exportImage() * \see asImage() * \note Parameter selected added in QGIS 3.10 * \since QGIS 2.6 */ - void drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext *customContext = nullptr, bool selected = false, const QgsExpressionContext *expressionContext = nullptr ); + void drawPreviewIcon( QPainter *painter, QSize size, QgsRenderContext *customContext = nullptr, bool selected = false, const QgsExpressionContext *expressionContext = nullptr, + const QgsLegendPatchShape *patchShape = nullptr ); /** * Export the symbol as an image format, to the specified \a path and with the given \a size. @@ -694,6 +697,8 @@ class CORE_EXPORT QgsSymbolRenderContext */ QgsSymbolRenderContext( QgsRenderContext &c, QgsUnitTypes::RenderUnit u, qreal opacity = 1.0, bool selected = false, QgsSymbol::RenderHints renderHints = nullptr, const QgsFeature *f = nullptr, const QgsFields &fields = QgsFields(), const QgsMapUnitScale &mapUnitScale = QgsMapUnitScale() ); + ~QgsSymbolRenderContext(); + //! QgsSymbolRenderContext cannot be copied. QgsSymbolRenderContext( const QgsSymbolRenderContext &rh ) = delete; @@ -861,6 +866,22 @@ class CORE_EXPORT QgsSymbolRenderContext */ void setExpressionContextScope( QgsExpressionContextScope *contextScope SIP_TRANSFER ); + /** + * Returns the symbol patch shape, to use if rendering symbol preview icons. + * + * \see setPatchShape() + * \since QGIS 3.14 + */ + const QgsLegendPatchShape *patchShape() const; + + /** + * Sets the symbol patch \a shape, to use if rendering symbol preview icons. + * + * \see patchShape() + * \since QGIS 3.14 + */ + void setPatchShape( const QgsLegendPatchShape &shape ); + private: #ifdef SIP_RUN @@ -879,6 +900,7 @@ class CORE_EXPORT QgsSymbolRenderContext int mGeometryPartCount; int mGeometryPartNum; QgsWkbTypes::GeometryType mOriginalGeometryType = QgsWkbTypes::UnknownGeometry; + std::unique_ptr< QgsLegendPatchShape > mPatchShape; }; diff --git a/src/core/symbology/qgssymbollayer.cpp b/src/core/symbology/qgssymbollayer.cpp index 7a4bc0ef4d00..46f0a891f33b 100644 --- a/src/core/symbology/qgssymbollayer.cpp +++ b/src/core/symbology/qgssymbollayer.cpp @@ -27,6 +27,8 @@ #include "qgsexpressioncontext.h" #include "qgssymbollayerutils.h" #include "qgsapplication.h" +#include "qgsmultipoint.h" +#include "qgslegendpatchshape.h" #include #include @@ -53,6 +55,8 @@ void QgsSymbolLayer::initPropertyDefinitions() { QgsSymbolLayer::PropertyStrokeStyle, QgsPropertyDefinition( "outlineStyle", QObject::tr( "Symbol stroke style" ), QgsPropertyDefinition::LineStyle, origin )}, { QgsSymbolLayer::PropertyOffset, QgsPropertyDefinition( "offset", QObject::tr( "Symbol offset" ), QgsPropertyDefinition::Offset, origin )}, { QgsSymbolLayer::PropertyCharacter, QgsPropertyDefinition( "char", QObject::tr( "Marker character(s)" ), QgsPropertyDefinition::String, origin )}, + { QgsSymbolLayer::PropertyFontFamily, QgsPropertyDefinition( "fontFamily", QObject::tr( "Font family" ), QgsPropertyDefinition::String, origin )}, + { QgsSymbolLayer::PropertyFontStyle, QgsPropertyDefinition( "fontStyle", QObject::tr( "Font style" ), QgsPropertyDefinition::String, origin )}, { QgsSymbolLayer::PropertyWidth, QgsPropertyDefinition( "width", QObject::tr( "Symbol width" ), QgsPropertyDefinition::DoublePositive, origin )}, { QgsSymbolLayer::PropertyHeight, QgsPropertyDefinition( "height", QObject::tr( "Symbol height" ), QgsPropertyDefinition::DoublePositive, origin )}, { QgsSymbolLayer::PropertyPreserveAspectRatio, QgsPropertyDefinition( "preserveAspectRatio", QObject::tr( "Preserve aspect ratio between width and height" ), QgsPropertyDefinition::Boolean, origin )}, @@ -447,15 +451,19 @@ void QgsMarkerSymbolLayer::drawPreviewIcon( QgsSymbolRenderContext &context, QSi { startRender( context ); QgsPaintEffect *effect = paintEffect(); + + QPolygonF points = context.patchShape() ? context.patchShape()->toQPolygonF( QgsSymbol::Marker, size ).value( 0 ).value( 0 ) + : QgsLegendPatchShape::defaultPatch( QgsSymbol::Marker, size ).value( 0 ).value( 0 ); + + std::unique_ptr< QgsEffectPainter > effectPainter; if ( effect && effect->enabled() ) - { - QgsEffectPainter p( context.renderContext(), effect ); - renderPoint( QPointF( size.width() / 2, size.height() / 2 ), context ); - } - else - { - renderPoint( QPointF( size.width() / 2, size.height() / 2 ), context ); - } + effectPainter = qgis::make_unique< QgsEffectPainter >( context.renderContext(), effect ); + + for ( QPointF point : qgis::as_const( points ) ) + renderPoint( point, context ); + + effectPainter.reset(); + stopRender( context ); } @@ -631,22 +639,20 @@ QgsMapUnitScale QgsLineSymbolLayer::mapUnitScale() const void QgsLineSymbolLayer::drawPreviewIcon( QgsSymbolRenderContext &context, QSize size ) { - QPolygonF points; - // we're adding 0.5 to get rid of blurred preview: - // drawing antialiased lines of width 1 at (x,0)-(x,100) creates 2px line - points << QPointF( 0, int( size.height() / 2 ) + 0.5 ) << QPointF( size.width(), int( size.height() / 2 ) + 0.5 ); - + const QList< QList< QPolygonF > > points = context.patchShape() ? context.patchShape()->toQPolygonF( QgsSymbol::Line, size ) + : QgsLegendPatchShape::defaultPatch( QgsSymbol::Line, size ); startRender( context ); QgsPaintEffect *effect = paintEffect(); + + std::unique_ptr< QgsEffectPainter > effectPainter; if ( effect && effect->enabled() ) - { - QgsEffectPainter p( context.renderContext(), effect ); - renderPolyline( points, context ); - } - else - { - renderPolyline( points, context ); - } + effectPainter = qgis::make_unique< QgsEffectPainter >( context.renderContext(), effect ); + + for ( const QList< QPolygonF > &line : points ) + renderPolyline( line.value( 0 ), context ); + + effectPainter.reset(); + stopRender( context ); } @@ -693,18 +699,26 @@ double QgsLineSymbolLayer::dxfWidth( const QgsDxfExport &e, QgsSymbolRenderConte void QgsFillSymbolLayer::drawPreviewIcon( QgsSymbolRenderContext &context, QSize size ) { - QPolygonF poly = QRectF( QPointF( 0, 0 ), QPointF( size.width(), size.height() ) ); + const QList< QList< QPolygonF > > polys = context.patchShape() ? context.patchShape()->toQPolygonF( QgsSymbol::Fill, size ) + : QgsLegendPatchShape::defaultPatch( QgsSymbol::Fill, size ); + startRender( context ); QgsPaintEffect *effect = paintEffect(); + + std::unique_ptr< QgsEffectPainter > effectPainter; if ( effect && effect->enabled() ) + effectPainter = qgis::make_unique< QgsEffectPainter >( context.renderContext(), effect ); + + for ( const QList< QPolygonF > &poly : polys ) { - QgsEffectPainter p( context.renderContext(), effect ); - renderPolygon( poly, nullptr, context ); - } - else - { - renderPolygon( poly, nullptr, context ); + QList< QPolygonF > rings; + for ( int i = 1; i < poly.size(); ++i ) + rings << poly.at( i ); + renderPolygon( poly.value( 0 ), &rings, context ); } + + effectPainter.reset(); + stopRender( context ); } diff --git a/src/core/symbology/qgssymbollayer.h b/src/core/symbology/qgssymbollayer.h index 85e610b27b31..64ebe2b26427 100644 --- a/src/core/symbology/qgssymbollayer.h +++ b/src/core/symbology/qgssymbollayer.h @@ -185,6 +185,8 @@ class CORE_EXPORT QgsSymbolLayer PropertyRandomSeed, //!< Random number seed PropertyClipPoints, //!< Whether markers should be clipped to polygon boundaries PropertyDensityArea, //clone() : nullptr ); + mEnabled = other.mEnabled; + mExpression = other.mExpression; + mMinZoomLevel = other.mMinZoomLevel; + mMaxZoomLevel = other.mMaxZoomLevel; + return *this; +} + +QgsVectorTileBasicRendererStyle::~QgsVectorTileBasicRendererStyle() = default; + +void QgsVectorTileBasicRendererStyle::setSymbol( QgsSymbol *sym ) +{ + mSymbol.reset( sym ); +} + +void QgsVectorTileBasicRendererStyle::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const +{ + elem.setAttribute( QStringLiteral( "name" ), mStyleName ); + elem.setAttribute( QStringLiteral( "layer" ), mLayerName ); + elem.setAttribute( QStringLiteral( "geometry" ), mGeometryType ); + elem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + elem.setAttribute( QStringLiteral( "expression" ), mExpression ); + elem.setAttribute( QStringLiteral( "min-zoom" ), mMinZoomLevel ); + elem.setAttribute( QStringLiteral( "max-zoom" ), mMaxZoomLevel ); + + QDomDocument doc = elem.ownerDocument(); + QgsSymbolMap symbols; + symbols[QStringLiteral( "0" )] = mSymbol.get(); + QDomElement symbolsElem = QgsSymbolLayerUtils::saveSymbols( symbols, QStringLiteral( "symbols" ), doc, context ); + elem.appendChild( symbolsElem ); +} + +void QgsVectorTileBasicRendererStyle::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) +{ + mStyleName = elem.attribute( QStringLiteral( "name" ) ); + mLayerName = elem.attribute( QStringLiteral( "layer" ) ); + mGeometryType = static_cast( elem.attribute( QStringLiteral( "geometry" ) ).toInt() ); + mEnabled = elem.attribute( QStringLiteral( "enabled" ) ).toInt(); + mExpression = elem.attribute( QStringLiteral( "expression" ) ); + mMinZoomLevel = elem.attribute( QStringLiteral( "min-zoom" ) ).toInt(); + mMaxZoomLevel = elem.attribute( QStringLiteral( "max-zoom" ) ).toInt(); + + mSymbol.reset(); + QDomElement symbolsElem = elem.firstChildElement( QStringLiteral( "symbols" ) ); + if ( !symbolsElem.isNull() ) + { + QgsSymbolMap symbolMap = QgsSymbolLayerUtils::loadSymbols( symbolsElem, context ); + if ( symbolMap.contains( QStringLiteral( "0" ) ) ) + { + mSymbol.reset( symbolMap.take( QStringLiteral( "0" ) ) ); + } + } +} + +//////// + + +QgsVectorTileBasicRenderer::QgsVectorTileBasicRenderer() +{ +} + +QString QgsVectorTileBasicRenderer::type() const +{ + return QStringLiteral( "basic" ); +} + +QgsVectorTileBasicRenderer *QgsVectorTileBasicRenderer::clone() const +{ + QgsVectorTileBasicRenderer *r = new QgsVectorTileBasicRenderer; + r->mStyles = mStyles; + r->mStyles.detach(); // make a deep copy to make sure symbols get cloned + return r; +} + +void QgsVectorTileBasicRenderer::startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) +{ + Q_UNUSED( context ) + Q_UNUSED( tileRange ) + // figure out required fields for different layers + for ( const QgsVectorTileBasicRendererStyle &layerStyle : qgis::as_const( mStyles ) ) + { + if ( layerStyle.isActive( tileZoom ) && !layerStyle.filterExpression().isEmpty() ) + { + QgsExpression expr( layerStyle.filterExpression() ); + mRequiredFields[layerStyle.layerName()].unite( expr.referencedColumns() ); + } + } +} + +QMap > QgsVectorTileBasicRenderer::usedAttributes( const QgsRenderContext & ) +{ + return mRequiredFields; +} + +void QgsVectorTileBasicRenderer::stopRender( QgsRenderContext &context ) +{ + Q_UNUSED( context ) +} + +void QgsVectorTileBasicRenderer::renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) +{ + const QgsVectorTileFeatures tileData = tile.features(); + int zoomLevel = tile.id().zoomLevel(); + + for ( const QgsVectorTileBasicRendererStyle &layerStyle : qgis::as_const( mStyles ) ) + { + if ( !layerStyle.isActive( zoomLevel ) ) + continue; + + QgsFields fields = QgsVectorTileUtils::makeQgisFields( mRequiredFields[layerStyle.layerName()] ); + + QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Layer" ) ); // will be deleted by popper + scope->setFields( fields ); + QgsExpressionContextScopePopper popper( context.expressionContext(), scope ); + + QgsExpression filterExpression( layerStyle.filterExpression() ); + filterExpression.prepare( &context.expressionContext() ); + + QgsSymbol *sym = layerStyle.symbol(); + sym->startRender( context, QgsFields() ); + if ( layerStyle.layerName().isEmpty() ) + { + // matching all layers + for ( QString layerName : tileData.keys() ) + { + for ( const QgsFeature &f : tileData[layerName] ) + { + scope->setFeature( f ); + if ( filterExpression.isValid() && !filterExpression.evaluate( &context.expressionContext() ).toBool() ) + continue; + + if ( QgsWkbTypes::geometryType( f.geometry().wkbType() ) == layerStyle.geometryType() ) + sym->renderFeature( f, context ); + } + } + } + else if ( tileData.contains( layerStyle.layerName() ) ) + { + // matching one particular layer + for ( const QgsFeature &f : tileData[layerStyle.layerName()] ) + { + scope->setFeature( f ); + if ( filterExpression.isValid() && !filterExpression.evaluate( &context.expressionContext() ).toBool() ) + continue; + + if ( QgsWkbTypes::geometryType( f.geometry().wkbType() ) == layerStyle.geometryType() ) + sym->renderFeature( f, context ); + } + } + sym->stopRender( context ); + } +} + +void QgsVectorTileBasicRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const +{ + QDomDocument doc = elem.ownerDocument(); + QDomElement elemStyles = doc.createElement( QStringLiteral( "styles" ) ); + for ( const QgsVectorTileBasicRendererStyle &layerStyle : mStyles ) + { + QDomElement elemStyle = doc.createElement( QStringLiteral( "style" ) ); + layerStyle.writeXml( elemStyle, context ); + elemStyles.appendChild( elemStyle ); + } + elem.appendChild( elemStyles ); +} + +void QgsVectorTileBasicRenderer::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) +{ + mStyles.clear(); + + QDomElement elemStyles = elem.firstChildElement( QStringLiteral( "styles" ) ); + QDomElement elemStyle = elemStyles.firstChildElement( QStringLiteral( "style" ) ); + while ( !elemStyle.isNull() ) + { + QgsVectorTileBasicRendererStyle layerStyle; + layerStyle.readXml( elemStyle, context ); + mStyles.append( layerStyle ); + elemStyle = elemStyle.nextSiblingElement( QStringLiteral( "style" ) ); + } +} + +void QgsVectorTileBasicRenderer::setStyles( const QList &styles ) +{ + mStyles = styles; +} + +QList QgsVectorTileBasicRenderer::styles() const +{ + return mStyles; +} + +QList QgsVectorTileBasicRenderer::simpleStyleWithRandomColors() +{ + QColor polygonFillColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); + QColor polygonStrokeColor = polygonFillColor; + polygonFillColor.setAlpha( 100 ); + double polygonStrokeWidth = DEFAULT_LINE_WIDTH; + + QColor lineStrokeColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); + double lineStrokeWidth = DEFAULT_LINE_WIDTH; + + QColor pointFillColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); + QColor pointStrokeColor = pointFillColor; + pointFillColor.setAlpha( 100 ); + double pointSize = DEFAULT_POINT_SIZE; + + return simpleStyle( polygonFillColor, polygonStrokeColor, polygonStrokeWidth, + lineStrokeColor, lineStrokeWidth, + pointFillColor, pointStrokeColor, pointSize ); +} + +QList QgsVectorTileBasicRenderer::simpleStyle( + const QColor &polygonFillColor, const QColor &polygonStrokeColor, double polygonStrokeWidth, + const QColor &lineStrokeColor, double lineStrokeWidth, + const QColor &pointFillColor, const QColor &pointStrokeColor, double pointSize ) +{ + QgsSimpleFillSymbolLayer *fillSymbolLayer = new QgsSimpleFillSymbolLayer(); + fillSymbolLayer->setFillColor( polygonFillColor ); + fillSymbolLayer->setStrokeColor( polygonStrokeColor ); + fillSymbolLayer->setStrokeWidth( polygonStrokeWidth ); + QgsFillSymbol *fillSymbol = new QgsFillSymbol( QgsSymbolLayerList() << fillSymbolLayer ); + + QgsSimpleLineSymbolLayer *lineSymbolLayer = new QgsSimpleLineSymbolLayer; + lineSymbolLayer->setColor( lineStrokeColor ); + lineSymbolLayer->setWidth( lineStrokeWidth ); + QgsLineSymbol *lineSymbol = new QgsLineSymbol( QgsSymbolLayerList() << lineSymbolLayer ); + + QgsSimpleMarkerSymbolLayer *markerSymbolLayer = new QgsSimpleMarkerSymbolLayer; + markerSymbolLayer->setFillColor( pointFillColor ); + markerSymbolLayer->setStrokeColor( pointStrokeColor ); + markerSymbolLayer->setSize( pointSize ); + QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << markerSymbolLayer ); + + QgsVectorTileBasicRendererStyle st1( QStringLiteral( "Polygons" ), QString(), QgsWkbTypes::PolygonGeometry ); + st1.setSymbol( fillSymbol ); + + QgsVectorTileBasicRendererStyle st2( QStringLiteral( "Lines" ), QString(), QgsWkbTypes::LineGeometry ); + st2.setSymbol( lineSymbol ); + + QgsVectorTileBasicRendererStyle st3( QStringLiteral( "Points" ), QString(), QgsWkbTypes::PointGeometry ); + st3.setSymbol( markerSymbol ); + + QList lst; + lst << st1 << st2 << st3; + return lst; +} diff --git a/src/core/vectortile/qgsvectortilebasicrenderer.h b/src/core/vectortile/qgsvectortilebasicrenderer.h new file mode 100644 index 000000000000..1c576ed1e45a --- /dev/null +++ b/src/core/vectortile/qgsvectortilebasicrenderer.h @@ -0,0 +1,173 @@ +/*************************************************************************** + qgsvectortilebasicrenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEBASICRENDERER_H +#define QGSVECTORTILEBASICRENDERER_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +#include "qgsvectortilerenderer.h" + +class QgsLineSymbol; +class QgsFillSymbol; +class QgsMarkerSymbol; + +class QgsSymbol; + +/** + * \ingroup core + * Definition of map rendering of a subset of vector tile data. The subset of data is defined by: + * 1. sub-layer name + * 2. geometry type (a single sub-layer may have multiple geometry types) + * 3. filter expression + * + * Renering is determined by the associated symbol (QgsSymbol). Symbol has to be of the same + * type as the chosen geometryType() - i.e. QgsMarkerSymbol for points, QgsLineSymbol for linestrings + * and QgsFillSymbol for polygons. + * + * It is possible to further constrain when this style is applied by setting a range of allowed + * zoom levels, or by disabling it. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileBasicRendererStyle +{ + public: + //! Constructs a style object + QgsVectorTileBasicRendererStyle( const QString &stName = QString(), const QString &laName = QString(), QgsWkbTypes::GeometryType geomType = QgsWkbTypes::UnknownGeometry ); + //! Constructs a style object as a copy of another style + QgsVectorTileBasicRendererStyle( const QgsVectorTileBasicRendererStyle &other ); + QgsVectorTileBasicRendererStyle &operator=( const QgsVectorTileBasicRendererStyle &other ); + ~QgsVectorTileBasicRendererStyle(); + + //! Sets human readable name of this style + void setStyleName( const QString &name ) { mStyleName = name; } + //! Returns human readable name of this style + QString styleName() const { return mStyleName; } + + //! Sets name of the sub-layer to render (empty layer means that all layers match) + void setLayerName( const QString &name ) { mLayerName = name; } + //! Returns name of the sub-layer to render (empty layer means that all layers match) + QString layerName() const { return mLayerName; } + + //! Sets type of the geometry that will be used (point / line / polygon) + void setGeometryType( QgsWkbTypes::GeometryType geomType ) { mGeometryType = geomType; } + //! Returns type of the geometry that will be used (point / line / polygon) + QgsWkbTypes::GeometryType geometryType() const { return mGeometryType; } + + //! Sets filter expression (empty filter means that all features match) + void setFilterExpression( const QString &expr ) { mExpression = expr; } + //! Returns filter expression (empty filter means that all features match) + QString filterExpression() const { return mExpression; } + + //! Sets symbol for rendering. Takes ownership of the symbol. + void setSymbol( QgsSymbol *sym SIP_TRANSFER ); + //! Returns symbol for rendering + QgsSymbol *symbol() const { return mSymbol.get(); } + + //! Sets whether this style is enabled (used for rendering) + void setEnabled( bool enabled ) { mEnabled = enabled; } + //! Returns whether this style is enabled (used for rendering) + bool isEnabled() const { return mEnabled; } + + //! Sets minimum zoom level index (negative number means no limit) + void setMinZoomLevel( int minZoom ) { mMinZoomLevel = minZoom; } + //! Returns minimum zoom level index (negative number means no limit) + int minZoomLevel() const { return mMinZoomLevel; } + + //! Sets maximum zoom level index (negative number means no limit) + void setMaxZoomLevel( int maxZoom ) { mMaxZoomLevel = maxZoom; } + //! Returns maxnimum zoom level index (negative number means no limit) + int maxZoomLevel() const { return mMaxZoomLevel; } + + //! Returns whether the style is active at given zoom level (also checks "enabled" flag) + bool isActive( int zoomLevel ) const + { + return mEnabled && ( mMinZoomLevel == -1 || zoomLevel >= mMinZoomLevel ) && ( mMaxZoomLevel == -1 || zoomLevel <= mMaxZoomLevel ); + } + + //! Writes object content to given DOM element + void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const; + //! Reads object content from given DOM element + void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); + + private: + QString mStyleName; + QString mLayerName; + QgsWkbTypes::GeometryType mGeometryType; + std::unique_ptr mSymbol; + bool mEnabled = true; + QString mExpression; + int mMinZoomLevel = -1; + int mMaxZoomLevel = -1; +}; + + +/** + * \ingroup core + * The default vector tile renderer implementation. It has an ordered list of "styles", + * each defines a rendering rule. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileBasicRenderer : public QgsVectorTileRenderer +{ + public: + //! Constructs renderer with no styles + QgsVectorTileBasicRenderer(); + + QString type() const override; + QgsVectorTileBasicRenderer *clone() const override SIP_FACTORY; + void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) override; + QMap > usedAttributes( const QgsRenderContext & ) override SIP_SKIP; + void stopRender( QgsRenderContext &context ) override; + void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) override; + void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const override; + void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) override; + + //! Sets list of styles of the renderer + void setStyles( const QList &styles ); + //! Returns list of styles of the renderer + QList styles() const; + //! Updates style definition at the paricular index of the list (the index must be in interval [0,N-1] otherwise this function does nothing) + void setStyle( int index, const QgsVectorTileBasicRendererStyle &style ) { mStyles[index] = style; } + //! Returns style definition at the particular index + QgsVectorTileBasicRendererStyle style( int index ) const { return mStyles[index]; } + + //! Returns a list of styles to render all layers with the given fill/stroke colors, stroke widths and marker sizes + static QList simpleStyle( + const QColor &polygonFillColor, const QColor &polygonStrokeColor, double polygonStrokeWidth, + const QColor &lineStrokeColor, double lineStrokeWidth, + const QColor &pointFillColor, const QColor &pointStrokeColor, double pointSize ); + + //! Returns a list of styles to render all layers, using random colors + static QList simpleStyleWithRandomColors(); + + private: + void setDefaultStyle(); + + private: + //! List of rendering styles + QList mStyles; + + // temporary bits + + //! Names of required fields for each sub-layer (only valid between startRender/stopRender calls) + QMap > mRequiredFields; + +}; + +#endif // QGSVECTORTILEBASICRENDERER_H diff --git a/src/core/vectortile/qgsvectortileconnection.cpp b/src/core/vectortile/qgsvectortileconnection.cpp new file mode 100644 index 000000000000..0efb84780efb --- /dev/null +++ b/src/core/vectortile/qgsvectortileconnection.cpp @@ -0,0 +1,124 @@ +/*************************************************************************** + qgsvectortileconnection.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Martin Dobias + email : wonder dot sk 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 "qgsvectortileconnection.h" + +#include "qgslogger.h" +#include "qgsdatasourceuri.h" +#include "qgssettings.h" + +///@cond PRIVATE + +QString QgsVectorTileProviderConnection::encodedUri( const QgsVectorTileProviderConnection::Data &conn ) +{ + QgsDataSourceUri uri; + uri.setParam( QStringLiteral( "type" ), QStringLiteral( "xyz" ) ); + uri.setParam( QStringLiteral( "url" ), conn.url ); + if ( conn.zMin != -1 ) + uri.setParam( QStringLiteral( "zmin" ), QString::number( conn.zMin ) ); + if ( conn.zMax != -1 ) + uri.setParam( QStringLiteral( "zmax" ), QString::number( conn.zMax ) ); + return uri.encodedUri(); +} + +QgsVectorTileProviderConnection::Data QgsVectorTileProviderConnection::decodedUri( const QString &uri ) +{ + QgsDataSourceUri dsUri; + dsUri.setEncodedUri( uri ); + + QgsVectorTileProviderConnection::Data conn; + conn.url = dsUri.param( QStringLiteral( "url" ) ); + conn.zMin = dsUri.hasParam( QStringLiteral( "zmin" ) ) ? dsUri.param( QStringLiteral( "zmin" ) ).toInt() : -1; + conn.zMax = dsUri.hasParam( QStringLiteral( "zmax" ) ) ? dsUri.param( QStringLiteral( "zmax" ) ).toInt() : -1; + return conn; +} + +QString QgsVectorTileProviderConnection::encodedLayerUri( const QgsVectorTileProviderConnection::Data &conn ) +{ + // compared to encodedUri() this one also adds type=xyz to the URI + QgsDataSourceUri uri; + uri.setParam( QStringLiteral( "type" ), QStringLiteral( "xyz" ) ); + uri.setParam( QStringLiteral( "url" ), conn.url ); + if ( conn.zMin != -1 ) + uri.setParam( QStringLiteral( "zmin" ), QString::number( conn.zMin ) ); + if ( conn.zMax != -1 ) + uri.setParam( QStringLiteral( "zmax" ), QString::number( conn.zMax ) ); + return uri.encodedUri(); +} + +QStringList QgsVectorTileProviderConnection::connectionList() +{ + QgsSettings settings; + settings.beginGroup( QStringLiteral( "qgis/connections-vector-tile" ) ); + QStringList connList = settings.childGroups(); + + return connList; +} + +QgsVectorTileProviderConnection::Data QgsVectorTileProviderConnection::connection( const QString &name ) +{ + QgsSettings settings; + settings.beginGroup( "qgis/connections-vector-tile/" + name ); + + if ( settings.value( "url" ).toString().isEmpty() ) + return QgsVectorTileProviderConnection::Data(); + + QgsVectorTileProviderConnection::Data conn; + conn.url = settings.value( QStringLiteral( "url" ) ).toString(); + conn.zMin = settings.value( QStringLiteral( "zmin" ), -1 ).toInt(); + conn.zMax = settings.value( QStringLiteral( "zmax" ), -1 ).toInt(); + return conn; +} + +void QgsVectorTileProviderConnection::deleteConnection( const QString &name ) +{ + QgsSettings settings; + settings.remove( "qgis/connections-vector-tile/" + name ); +} + +void QgsVectorTileProviderConnection::addConnection( const QString &name, QgsVectorTileProviderConnection::Data conn ) +{ + QgsSettings settings; + + settings.beginGroup( "qgis/connections-vector-tile/" + name ); + settings.setValue( QStringLiteral( "url" ), conn.url ); + settings.setValue( QStringLiteral( "zmin" ), conn.zMin ); + settings.setValue( QStringLiteral( "zmax" ), conn.zMax ); +} + +// + +QgsVectorTileProviderConnection::QgsVectorTileProviderConnection( const QString &name ) + : QgsAbstractProviderConnection( name ) +{ + setUri( encodedUri( connection( name ) ) ); +} + +QgsVectorTileProviderConnection::QgsVectorTileProviderConnection( const QString &uri, const QVariantMap &configuration ) + : QgsAbstractProviderConnection( uri, configuration ) +{ +} + +void QgsVectorTileProviderConnection::store( const QString &name ) const +{ + addConnection( name, decodedUri( uri() ) ); +} + +void QgsVectorTileProviderConnection::remove( const QString &name ) const +{ + deleteConnection( name ); +} + +///@endcond diff --git a/src/core/vectortile/qgsvectortileconnection.h b/src/core/vectortile/qgsvectortileconnection.h new file mode 100644 index 000000000000..6164c78ec630 --- /dev/null +++ b/src/core/vectortile/qgsvectortileconnection.h @@ -0,0 +1,66 @@ +/*************************************************************************** + qgsvectortileconnection.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Martin Dobias + email : wonder dot sk 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 QGSVECTORTILECONNECTION_H +#define QGSVECTORTILECONNECTION_H + +#include "qgis_core.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +#include + +#include "qgsabstractproviderconnection.h" + +class CORE_EXPORT QgsVectorTileProviderConnection : public QgsAbstractProviderConnection +{ + + public: + QgsVectorTileProviderConnection( const QString &name ); + QgsVectorTileProviderConnection( const QString &uri, const QVariantMap &configuration ); + + virtual void store( const QString &name ) const override; + virtual void remove( const QString &name ) const override; + + //! Represents decoded data of a connection + struct Data + { + QString url; + int zMin = -1; + int zMax = -1; + }; + + //! Returns connection data encoded as a string + static QString encodedUri( const Data &conn ); + //! Decodes connection string to a data structure + static Data decodedUri( const QString &uri ); + + //! Returns connection data encoded as a string containing URI for QgsVectorTileLayer + static QString encodedLayerUri( const Data &conn ); + + //! Returns list of existing connections, unless the hidden ones + static QStringList connectionList(); + //! Returns connection details + static Data connection( const QString &name ); + //! Removes a connection from the list + static void deleteConnection( const QString &name ); + //! Adds a new connection to the list + static void addConnection( const QString &name, Data conn ); +}; + +///@endcond + +#endif // QGSVECTORTILECONNECTION_H diff --git a/src/core/vectortile/qgsvectortiledataitems.cpp b/src/core/vectortile/qgsvectortiledataitems.cpp new file mode 100644 index 000000000000..c5bace855a38 --- /dev/null +++ b/src/core/vectortile/qgsvectortiledataitems.cpp @@ -0,0 +1,78 @@ +/*************************************************************************** + qgsvectortiledataitems.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Martin Dobias + email : wonder dot sk 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 "qgsvectortiledataitems.h" + +#include "qgssettings.h" +#include "qgsvectortileconnection.h" + +///@cond PRIVATE + +QgsVectorTileRootItem::QgsVectorTileRootItem( QgsDataItem *parent, QString name, QString path ) + : QgsDataCollectionItem( parent, name, path, QStringLiteral( "vectortile" ) ) +{ + mCapabilities |= Fast; + mIconName = QStringLiteral( "mIconWms.svg" ); + populate(); +} + +QVector QgsVectorTileRootItem::createChildren() +{ + QVector connections; + const auto connectionList = QgsVectorTileProviderConnection::connectionList(); + for ( const QString &connName : connectionList ) + { + QString uri = QgsVectorTileProviderConnection::encodedLayerUri( QgsVectorTileProviderConnection::connection( connName ) ); + QgsDataItem *conn = new QgsVectorTileLayerItem( this, connName, mPath + '/' + connName, uri ); + connections.append( conn ); + } + return connections; +} + + +// --------------------------------------------------------------------------- + + +QgsVectorTileLayerItem::QgsVectorTileLayerItem( QgsDataItem *parent, QString name, QString path, const QString &encodedUri ) + : QgsLayerItem( parent, name, path, encodedUri, QgsLayerItem::VectorTile, QString() ) +{ + setState( Populated ); +} + + +// --------------------------------------------------------------------------- + +QString QgsVectorTileDataItemProvider::name() +{ + return QStringLiteral( "Vector Tiles" ); +} + +QString QgsVectorTileDataItemProvider::dataProviderKey() const +{ + return QStringLiteral( "vectortile" ); +} + +int QgsVectorTileDataItemProvider::capabilities() const +{ + return QgsDataProvider::Net; +} + +QgsDataItem *QgsVectorTileDataItemProvider::createDataItem( const QString &path, QgsDataItem *parentItem ) +{ + if ( path.isEmpty() ) + return new QgsVectorTileRootItem( parentItem, QStringLiteral( "Vector Tiles" ), QStringLiteral( "vectortile:" ) ); + return nullptr; +} + +///@endcond diff --git a/src/core/vectortile/qgsvectortiledataitems.h b/src/core/vectortile/qgsvectortiledataitems.h new file mode 100644 index 000000000000..365738e5be1f --- /dev/null +++ b/src/core/vectortile/qgsvectortiledataitems.h @@ -0,0 +1,60 @@ +/*************************************************************************** + qgsvectortiledataitems.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Martin Dobias + email : wonder dot sk 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 QGSVECTORTILEDATAITEMS_H +#define QGSVECTORTILEDATAITEMS_H + +#include "qgsdataitem.h" +#include "qgsdataitemprovider.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +//! Root item for XYZ tile layers +class CORE_EXPORT QgsVectorTileRootItem : public QgsDataCollectionItem +{ + Q_OBJECT + public: + QgsVectorTileRootItem( QgsDataItem *parent, QString name, QString path ); + + QVector createChildren() override; + + QVariant sortKey() const override { return 8; } + +}; + +//! Item implementation for XYZ tile layers +class CORE_EXPORT QgsVectorTileLayerItem : public QgsLayerItem +{ + Q_OBJECT + public: + QgsVectorTileLayerItem( QgsDataItem *parent, QString name, QString path, const QString &encodedUri ); + +}; + + +//! Provider for XYZ root data item +class QgsVectorTileDataItemProvider : public QgsDataItemProvider +{ + public: + QString name() override; + QString dataProviderKey() const override; + int capabilities() const override; + + QgsDataItem *createDataItem( const QString &path, QgsDataItem *parentItem ) override; +}; + +///@endcond + +#endif // QGSVECTORTILEDATAITEMS_H diff --git a/src/core/vectortile/qgsvectortilelayer.cpp b/src/core/vectortile/qgsvectortilelayer.cpp new file mode 100644 index 000000000000..717c46b360ea --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayer.cpp @@ -0,0 +1,193 @@ +/*************************************************************************** + qgsvectortilelayer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortilelayer.h" + +#include "qgslogger.h" +#include "qgsvectortilelayerrenderer.h" +#include "qgsmbtilesreader.h" +#include "qgsvectortilebasicrenderer.h" +#include "qgsvectortileloader.h" + +#include "qgsdatasourceuri.h" + + +QgsVectorTileLayer::QgsVectorTileLayer( const QString &uri, const QString &baseName ) + : QgsMapLayer( QgsMapLayerType::VectorTileLayer, baseName ) +{ + mDataSource = uri; + + mValid = loadDataSource(); + + // set a default renderer + QgsVectorTileBasicRenderer *renderer = new QgsVectorTileBasicRenderer; + renderer->setStyles( QgsVectorTileBasicRenderer::simpleStyleWithRandomColors() ); + setRenderer( renderer ); +} + +bool QgsVectorTileLayer::loadDataSource() +{ + QgsDataSourceUri dsUri; + dsUri.setEncodedUri( mDataSource ); + + mSourceType = dsUri.param( QStringLiteral( "type" ) ); + mSourcePath = dsUri.param( QStringLiteral( "url" ) ); + if ( mSourceType == QStringLiteral( "xyz" ) ) + { + // online tiles + mSourceMinZoom = 0; + mSourceMaxZoom = 14; + + if ( dsUri.hasParam( QStringLiteral( "zmin" ) ) ) + mSourceMinZoom = dsUri.param( QStringLiteral( "zmin" ) ).toInt(); + if ( dsUri.hasParam( QStringLiteral( "zmax" ) ) ) + mSourceMaxZoom = dsUri.param( QStringLiteral( "zmax" ) ).toInt(); + + setExtent( QgsRectangle( -20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892 ) ); + } + else if ( mSourceType == QStringLiteral( "mbtiles" ) ) + { + QgsMBTilesReader reader( mSourcePath ); + if ( !reader.open() ) + { + QgsDebugMsg( QStringLiteral( "failed to open MBTiles file: " ) + mSourcePath ); + return false; + } + + QgsDebugMsgLevel( QStringLiteral( "name: " ) + reader.metadataValue( QStringLiteral( "name" ) ), 2 ); + bool minZoomOk, maxZoomOk; + int minZoom = reader.metadataValue( QStringLiteral( "minzoom" ) ).toInt( &minZoomOk ); + int maxZoom = reader.metadataValue( QStringLiteral( "maxzoom" ) ).toInt( &maxZoomOk ); + if ( minZoomOk ) + mSourceMinZoom = minZoom; + if ( maxZoomOk ) + mSourceMaxZoom = maxZoom; + QgsDebugMsgLevel( QStringLiteral( "zoom range: %1 - %2" ).arg( mSourceMinZoom ).arg( mSourceMaxZoom ), 2 ); + + QgsRectangle r = reader.extent(); + QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), + QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ), transformContext() ); + r = ct.transformBoundingBox( r ); + setExtent( r ); + } + else + { + QgsDebugMsg( QStringLiteral( "Unknown source type: " ) + mSourceType ); + return false; + } + + setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ) ); + return true; +} + +QgsVectorTileLayer::~QgsVectorTileLayer() = default; + + +QgsVectorTileLayer *QgsVectorTileLayer::clone() const +{ + QgsVectorTileLayer *layer = new QgsVectorTileLayer( source(), name() ); + layer->setRenderer( renderer() ? renderer()->clone() : nullptr ); + return layer; +} + +QgsMapLayerRenderer *QgsVectorTileLayer::createMapRenderer( QgsRenderContext &rendererContext ) +{ + return new QgsVectorTileLayerRenderer( this, rendererContext ); +} + +bool QgsVectorTileLayer::readXml( const QDomNode &layerNode, QgsReadWriteContext &context ) +{ + mValid = loadDataSource(); + + QString errorMsg; + return readSymbology( layerNode, errorMsg, context ); +} + +bool QgsVectorTileLayer::writeXml( QDomNode &layerNode, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + QDomElement mapLayerNode = layerNode.toElement(); + mapLayerNode.setAttribute( QStringLiteral( "type" ), QStringLiteral( "vector-tile" ) ); + + QString errorMsg; + return writeSymbology( layerNode, doc, errorMsg, context ); +} + +bool QgsVectorTileLayer::readSymbology( const QDomNode &node, QString &errorMessage, QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) +{ + QDomElement elem = node.toElement(); + + readCommonStyle( elem, context, categories ); + + QDomElement elemRenderer = elem.firstChildElement( QStringLiteral( "renderer" ) ); + if ( elemRenderer.isNull() ) + { + errorMessage = tr( "Missing tag" ); + return false; + } + QString rendererType = elemRenderer.attribute( QStringLiteral( "type" ) ); + QgsVectorTileRenderer *r = nullptr; + if ( rendererType == QStringLiteral( "basic" ) ) + r = new QgsVectorTileBasicRenderer; + else + { + errorMessage = tr( "Unknown renderer type: " ) + rendererType; + return false; + } + + r->readXml( elemRenderer, context ); + setRenderer( r ); + return true; +} + +bool QgsVectorTileLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) const +{ + Q_UNUSED( errorMessage ) + QDomElement elem = node.toElement(); + + writeCommonStyle( elem, doc, context, categories ); + + if ( mRenderer ) + { + QDomElement elemRenderer = doc.createElement( QStringLiteral( "renderer" ) ); + elemRenderer.setAttribute( QStringLiteral( "type" ), mRenderer->type() ); + mRenderer->writeXml( elemRenderer, context ); + elem.appendChild( elemRenderer ); + } + return true; +} + +void QgsVectorTileLayer::setTransformContext( const QgsCoordinateTransformContext &transformContext ) +{ + Q_UNUSED( transformContext ) +} + +QByteArray QgsVectorTileLayer::getRawTile( QgsTileXYZ tileID ) +{ + QgsTileRange tileRange( tileID.column(), tileID.column(), tileID.row(), tileID.row() ); + QList rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mSourceType, mSourcePath, tileID.zoomLevel(), QPointF(), tileRange ); + if ( rawTiles.isEmpty() ) + return QByteArray(); + return rawTiles.first().data; +} + +void QgsVectorTileLayer::setRenderer( QgsVectorTileRenderer *r ) +{ + mRenderer.reset( r ); +} + +QgsVectorTileRenderer *QgsVectorTileLayer::renderer() const +{ + return mRenderer.get(); +} diff --git a/src/core/vectortile/qgsvectortilelayer.h b/src/core/vectortile/qgsvectortilelayer.h new file mode 100644 index 000000000000..f685abd941d0 --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayer.h @@ -0,0 +1,160 @@ +/*************************************************************************** + qgsvectortilelayer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILELAYER_H +#define QGSVECTORTILELAYER_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +#include "qgsmaplayer.h" + +class QgsVectorTileRenderer; + +class QgsTileXYZ; + +/** + * \ingroup core + * Implements a map layer that is dedicated to rendering of vector tiles. + * Vector tiles compared to "ordinary" vector layers are pre-processed data + * optimized for fast rendering. A dataset is provided with a series of zoom levels + * for different map scales. Each zoom level has a matrix of tiles that contain + * actual data. A single vector tile may be a a file stored on a local drive, + * requested over HTTP request or retrieved from a database. + * + * Content of a vector tile is divided into one or more named sub-layers. Each such + * sub-layer may contain many features which consist of geometry and attributes. + * Contrary to traditional vector layers, these sub-layers do not need to have a rigid + * schema where geometry type and attributes are the same for all features. A single + * sub-layer may have multiple geometry types in a single tile or have some attributes + * defined only at particular zoom levels. + * + * Vector tile layer currently does not use the concept of data providers that other + * layer types use. The process of rendering of vector tiles looks like this: + * + * +--------+ +------+ +---------+ + * | DATA | | RAW | | DECODED | + * | | --> LOADER --> | | --> DECODER --> | | --> RENDERER + * | SOURCE | | TILE | | TILE | + * +--------+ +------+ +---------+ + * + * Data source is a place from where tiles are fetched from (URL for HTTP access, local + * files, MBTiles file, GeoPackage file or others. Loader (QgsVectorTileLoader) class + * takes care of loading data from the data source. The "raw tile" data is just a blob + * (QByteArray) that is encoded in some way. There are multiple ways how vector tiles + * are encoded just like there are different formats how to store images. For example, + * tiles can be encoded using Mapbox Vector Tiles (MVT) format or in GeoJSON. Decoder + * (QgsVectorTileDecoder) takes care of decoding raw tile data into QgsFeature objects. + * A decoded tile is essentially an array of vector features for each sub-layer found + * in the tile - this is what vector tile renderer (QgsVectorTileRenderer) expects + * and does the map rendering. + * + * To construct a vector tile layer, it is best to use QgsDataSourceUri class and set + * the following parameters to get a valid encoded URI: + * - "type" - what kind of data source will be used + * - "url" - URL or path of the data source (specific to each data source type, see below) + * + * Currently supported data source types: + * - "xyz" - the "url" should be a template like http://example.com/{z}/{x}/{y}.pbf where + * {x},{y},{z} will be replaced by tile coordinates + * - "mbtiles" - tiles read from a MBTiles file (a SQLite database) + * + * Currently supported decoders: + * - MVT - following Mapbox Vector Tiles specification + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileLayer : public QgsMapLayer +{ + Q_OBJECT + + public: + //! Constructs a new vector tile layer + explicit QgsVectorTileLayer( const QString &path = QString(), const QString &baseName = QString() ); + ~QgsVectorTileLayer() override; + + // implementation of virtual functions from QgsMapLayer + + QgsVectorTileLayer *clone() const override SIP_FACTORY; + + virtual QgsMapLayerRenderer *createMapRenderer( QgsRenderContext &rendererContext ) override SIP_FACTORY; + + virtual bool readXml( const QDomNode &layerNode, QgsReadWriteContext &context ) override; + + virtual bool writeXml( QDomNode &layerNode, QDomDocument &doc, const QgsReadWriteContext &context ) const override; + + virtual bool readSymbology( const QDomNode &node, QString &errorMessage, + QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ) override; + + virtual bool writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, + StyleCategories categories = AllStyleCategories ) const override; + + virtual void setTransformContext( const QgsCoordinateTransformContext &transformContext ) override; + + // new methods + + //! Returns type of the data source + QString sourceType() const { return mSourceType; } + //! Returns URL/path of the data source (syntax different to each data source type) + QString sourcePath() const { return mSourcePath; } + + //! Returns minimum zoom level at which source has any valid tiles (negative = unconstrained) + int sourceMinZoom() const { return mSourceMinZoom; } + //! Returns maximum zoom level at which source has any valid tiles (negative = unconstrained) + int sourceMaxZoom() const { return mSourceMaxZoom; } + + /** + * Fetches raw tile data for the give tile coordinates. If failed to fetch tile data, + * it will return an empty byte array. + * + * \note This call may issue a network request (depending on the source type) and will block + * the caller until the request is finished. + */ + QByteArray getRawTile( QgsTileXYZ tileID ) SIP_SKIP; + + /** + * Sets renderer for the map layer. + * \note Takes ownership of the passed renderer + */ + void setRenderer( QgsVectorTileRenderer *r SIP_TRANSFER ); + //! Returns currently assigned renderer + QgsVectorTileRenderer *renderer() const; + + //! Sets whether to render also borders of tiles (useful for debugging) + void setTileBorderRenderingEnabled( bool enabled ) { mTileBorderRendering = enabled; } + //! Returns whether to render also borders of tiles (useful for debugging) + bool isTileBorderRenderingEnabled() const { return mTileBorderRendering; } + + private: + bool loadDataSource(); + + private: + //! Type of the data source + QString mSourceType; + //! URL/Path of the data source + QString mSourcePath; + //! Minimum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMinZoom = -1; + //! Maximum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMaxZoom = -1; + + //! Renderer assigned to the layer to draw map + std::unique_ptr mRenderer; + //! Whether we draw borders of tiles + bool mTileBorderRendering = false; +}; + + +#endif // QGSVECTORTILELAYER_H diff --git a/src/core/vectortile/qgsvectortilelayerrenderer.cpp b/src/core/vectortile/qgsvectortilelayerrenderer.cpp new file mode 100644 index 000000000000..6cdc4f6741eb --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayerrenderer.cpp @@ -0,0 +1,188 @@ +/*************************************************************************** + qgsvectortilelayerrenderer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortilelayerrenderer.h" + +#include + +#include "qgsexpressioncontextutils.h" +#include "qgsfeedback.h" +#include "qgslogger.h" + +#include "qgsvectortilemvtdecoder.h" +#include "qgsvectortilelayer.h" +#include "qgsvectortileloader.h" +#include "qgsvectortileutils.h" + + +QgsVectorTileLayerRenderer::QgsVectorTileLayerRenderer( QgsVectorTileLayer *layer, QgsRenderContext &context ) + : QgsMapLayerRenderer( layer->id(), &context ) + , mSourceType( layer->sourceType() ) + , mSourcePath( layer->sourcePath() ) + , mSourceMinZoom( layer->sourceMinZoom() ) + , mSourceMaxZoom( layer->sourceMaxZoom() ) + , mRenderer( layer->renderer()->clone() ) + , mDrawTileBoundaries( layer->isTileBorderRenderingEnabled() ) + , mFeedback( new QgsFeedback ) +{ +} + +bool QgsVectorTileLayerRenderer::render() +{ + QgsRenderContext &ctx = *renderContext(); + + if ( ctx.renderingStopped() ) + return false; + + QElapsedTimer tTotal; + tTotal.start(); + + QgsDebugMsgLevel( QStringLiteral( "Vector tiles rendering extent: " ) + ctx.extent().toString( -1 ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Vector tiles map scale 1 : %1" ).arg( ctx.rendererScale() ), 2 ); + + mTileZoom = QgsVectorTileUtils::scaleToZoomLevel( ctx.rendererScale(), mSourceMinZoom, mSourceMaxZoom ); + QgsDebugMsgLevel( QStringLiteral( "Vector tiles zoom level: %1" ).arg( mTileZoom ), 2 ); + + mTileMatrix = QgsTileMatrix::fromWebMercator( mTileZoom ); + + mTileRange = mTileMatrix.tileRangeFromExtent( ctx.extent() ); + QgsDebugMsgLevel( QStringLiteral( "Vector tiles range X: %1 - %2 Y: %3 - %4" ) + .arg( mTileRange.startColumn() ).arg( mTileRange.endColumn() ) + .arg( mTileRange.startRow() ).arg( mTileRange.endRow() ), 2 ); + + // view center is used to sort the order of tiles for fetching and rendering + QPointF viewCenter = mTileMatrix.mapToTileCoordinates( ctx.extent().center() ); + + if ( !mTileRange.isValid() ) + { + QgsDebugMsgLevel( QStringLiteral( "Vector tiles - outside of range" ), 2 ); + return true; // nothing to do + } + + bool isAsync = ( mSourceType == QStringLiteral( "xyz" ) ); + + std::unique_ptr asyncLoader; + QList rawTiles; + if ( !isAsync ) + { + QElapsedTimer tFetch; + tFetch.start(); + rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mSourceType, mSourcePath, mTileZoom, viewCenter, mTileRange ); + QgsDebugMsgLevel( QStringLiteral( "Tile fetching time: %1" ).arg( tFetch.elapsed() / 1000. ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Fetched tiles: %1" ).arg( rawTiles.count() ), 2 ); + } + else + { + asyncLoader.reset( new QgsVectorTileLoader( mSourcePath, mTileZoom, mTileRange, viewCenter, mFeedback.get() ) ); + QObject::connect( asyncLoader.get(), &QgsVectorTileLoader::tileRequestFinished, [this]( const QgsVectorTileRawData & rawTile ) + { + QgsDebugMsgLevel( QStringLiteral( "Got tile asynchronously: " ) + rawTile.id.toString(), 2 ); + if ( !rawTile.data.isEmpty() ) + decodeAndDrawTile( rawTile ); + } ); + } + + if ( ctx.renderingStopped() ) + return false; + + mRenderer->startRender( *renderContext(), mTileZoom, mTileRange ); + + QMap > requiredFields = mRenderer->usedAttributes( *renderContext() ); + + QMap perLayerFields; + for ( QString layerName : requiredFields.keys() ) + mPerLayerFields[layerName] = QgsVectorTileUtils::makeQgisFields( requiredFields[layerName] ); + + if ( !isAsync ) + { + for ( QgsVectorTileRawData &rawTile : rawTiles ) + { + if ( ctx.renderingStopped() ) + break; + + decodeAndDrawTile( rawTile ); + } + } + else + { + // Block until tiles are fetched and rendered. If the rendering gets canceled at some point, + // the async loader will catch the signal, abort requests and return from downloadBlocking() + asyncLoader->downloadBlocking(); + } + + mRenderer->stopRender( ctx ); + + ctx.painter()->setClipping( false ); + + QgsDebugMsgLevel( QStringLiteral( "Total time for decoding: %1" ).arg( mTotalDecodeTime / 1000. ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Drawing time: %1" ).arg( mTotalDrawTime / 1000. ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Total time: %1" ).arg( tTotal.elapsed() / 1000. ), 2 ); + + return !ctx.renderingStopped(); +} + +void QgsVectorTileLayerRenderer::decodeAndDrawTile( const QgsVectorTileRawData &rawTile ) +{ + QgsRenderContext &ctx = *renderContext(); + + QgsDebugMsgLevel( QStringLiteral( "Drawing tile " ) + rawTile.id.toString(), 2 ); + + QElapsedTimer tLoad; + tLoad.start(); + + // currently only MVT encoding supported + QgsVectorTileMVTDecoder decoder; + if ( !decoder.decode( rawTile.id, rawTile.data ) ) + { + QgsDebugMsgLevel( QStringLiteral( "Failed to parse raw tile data! " ) + rawTile.id.toString(), 2 ); + return; + } + + if ( ctx.renderingStopped() ) + return; + + QgsCoordinateTransform ct = ctx.coordinateTransform(); + + QgsVectorTileRendererData tile( rawTile.id ); + tile.setFeatures( decoder.layerFeatures( mPerLayerFields, ct ) ); + tile.setTilePolygon( QgsVectorTileUtils::tilePolygon( rawTile.id, ct, mTileMatrix, ctx.mapToPixel() ) ); + + mTotalDecodeTime += tLoad.elapsed(); + + // calculate tile polygon in screen coordinates + + if ( ctx.renderingStopped() ) + return; + + // set up clipping so that rendering does not go behind tile's extent + + ctx.painter()->setClipRegion( QRegion( tile.tilePolygon() ) ); + + QElapsedTimer tDraw; + tDraw.start(); + + mRenderer->renderTile( tile, ctx ); + mTotalDrawTime += tDraw.elapsed(); + + if ( mDrawTileBoundaries ) + { + ctx.painter()->setClipping( false ); + + QPen pen( Qt::red ); + pen.setWidth( 3 ); + ctx.painter()->setPen( pen ); + ctx.painter()->drawPolygon( tile.tilePolygon() ); + } +} diff --git a/src/core/vectortile/qgsvectortilelayerrenderer.h b/src/core/vectortile/qgsvectortilelayerrenderer.h new file mode 100644 index 000000000000..68bbb29d75e3 --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayerrenderer.h @@ -0,0 +1,85 @@ +/*************************************************************************** + qgsvectortilelayerrenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILELAYERRENDERER_H +#define QGSVECTORTILELAYERRENDERER_H + +#define SIP_NO_FILE + +#include "qgsmaplayerrenderer.h" + +class QgsVectorTileLayer; +class QgsVectorTileRawData; + +#include "qgsvectortilerenderer.h" + +/** + * \ingroup core + * This class provides map rendering functionality for vector tile layers. + * In render() function (assumed to be run in a worker thread) it will: + * 1. fetch vector tiles using QgsVectorTileLoader + * 2. decode raw tiles into QgsFeature objects using QgsVectorTileDecoder + * 3. render tiles using a class derived from QgsVectorTileRenderer + * + * \since QGIS 3.14 + */ +class QgsVectorTileLayerRenderer : public QgsMapLayerRenderer +{ + public: + //! Creates the renderer. Always called from main thread, should copy whatever necessary from the layer + QgsVectorTileLayerRenderer( QgsVectorTileLayer *layer, QgsRenderContext &context ); + + virtual bool render() override; + virtual QgsFeedback *feedback() const override { return mFeedback.get(); } + + private: + void decodeAndDrawTile( const QgsVectorTileRawData &rawTile ); + + // data coming from the vector tile layer + + //! Type of the source from which we will be loading tiles (e.g. "xyz" or "mbtiles") + QString mSourceType; + //! Path/URL of the source. Format depends on source type + QString mSourcePath; + //! Minimum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMinZoom = -1; + //! Maximum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMaxZoom = -1; + //! Tile renderer object to do rendering of individual tiles + std::unique_ptr mRenderer; + + //! Whether to draw boundaries of tiles (useful for debugging) + bool mDrawTileBoundaries = false; + + // temporary data used during rendering process + + //! Feedback object that may be used by the caller to cancel the rendering + std::unique_ptr mFeedback; + //! Zoom level at which we will be rendering + int mTileZoom = 0; + //! Definition of the tile matrix for our zoom level + QgsTileMatrix mTileMatrix; + //!< Block of tiles we will be rendering in that zoom level + QgsTileRange mTileRange; + //! Cached QgsFields object for each sub-layer that will be rendered + QMap mPerLayerFields; + //! Counter of total elapsed time to decode tiles (ms) + int mTotalDecodeTime = 0; + //! Counter of total elapsed time to render tiles (ms) + int mTotalDrawTime = 0; +}; + + +#endif // QGSVECTORTILELAYERRENDERER_H diff --git a/src/core/vectortile/qgsvectortileloader.cpp b/src/core/vectortile/qgsvectortileloader.cpp new file mode 100644 index 000000000000..bb951fce6232 --- /dev/null +++ b/src/core/vectortile/qgsvectortileloader.cpp @@ -0,0 +1,270 @@ +/*************************************************************************** + qgsvectortileloader.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortileloader.h" + +#include + +#include + +#include "qgsblockingnetworkrequest.h" +#include "qgslogger.h" +#include "qgsmbtilesreader.h" +#include "qgsnetworkaccessmanager.h" +#include "qgsvectortileutils.h" + +QgsVectorTileLoader::QgsVectorTileLoader( const QString &uri, int zoomLevel, const QgsTileRange &range, const QPointF &viewCenter, QgsFeedback *feedback ) + : mEventLoop( new QEventLoop ) + , mFeedback( feedback ) +{ + if ( feedback ) + { + connect( feedback, &QgsFeedback::canceled, this, &QgsVectorTileLoader::canceled, Qt::QueuedConnection ); + + // rendering could have been canceled before we started to listen to canceled() signal + // so let's check before doing the download and maybe quit prematurely + if ( feedback->isCanceled() ) + return; + } + + QgsDebugMsgLevel( QStringLiteral( "Starting network loader" ), 2 ); + QVector tiles = QgsVectorTileUtils::tilesInRange( range, zoomLevel ); + QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter ); + for ( QgsTileXYZ id : qgis::as_const( tiles ) ) + { + loadFromNetworkAsync( id, uri ); + } +} + +QgsVectorTileLoader::~QgsVectorTileLoader() +{ + QgsDebugMsgLevel( QStringLiteral( "Terminating network loader" ), 2 ); + + if ( !mReplies.isEmpty() ) + { + // this can happen when the loader is terminated without getting requests finalized + // (e.g. downloadBlocking() was not called) + canceled(); + } +} + +void QgsVectorTileLoader::downloadBlocking() +{ + if ( mFeedback && mFeedback->isCanceled() ) + { + QgsDebugMsgLevel( QStringLiteral( "downloadBlocking - not staring event loop - canceled" ), 2 ); + return; // nothing to do + } + + QgsDebugMsgLevel( QStringLiteral( "Starting event loop with %1 requests" ).arg( mReplies.count() ), 2 ); + + mEventLoop->exec( QEventLoop::ExcludeUserInputEvents ); + + QgsDebugMsgLevel( QStringLiteral( "downloadBlocking finished" ), 2 ); + + Q_ASSERT( mReplies.isEmpty() ); +} + +void QgsVectorTileLoader::loadFromNetworkAsync( const QgsTileXYZ &id, const QString &requestUrl ) +{ + QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id ); + QNetworkRequest request( url ); + QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLoader" ) ); + QgsSetRequestInitiatorId( request, id.toString() ); + + request.setAttribute( static_cast( QNetworkRequest::User + 1 ), id.column() ); + request.setAttribute( static_cast( QNetworkRequest::User + 2 ), id.row() ); + request.setAttribute( static_cast( QNetworkRequest::User + 3 ), id.zoomLevel() ); + + request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache ); + request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true ); + + QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request ); + connect( reply, &QNetworkReply::finished, this, &QgsVectorTileLoader::tileReplyFinished ); + + mReplies << reply; +} + +void QgsVectorTileLoader::tileReplyFinished() +{ + QNetworkReply *reply = qobject_cast( sender() ); + + int reqX = reply->request().attribute( static_cast( QNetworkRequest::User + 1 ) ).toInt(); + int reqY = reply->request().attribute( static_cast( QNetworkRequest::User + 2 ) ).toInt(); + int reqZ = reply->request().attribute( static_cast( QNetworkRequest::User + 3 ) ).toInt(); + QgsTileXYZ tileID( reqX, reqY, reqZ ); + + if ( reply->error() == QNetworkReply::NoError ) + { + // TODO: handle redirections? + + QgsDebugMsgLevel( QStringLiteral( "Tile download successful: " ) + tileID.toString(), 2 ); + QByteArray rawData = reply->readAll(); + mReplies.removeOne( reply ); + reply->deleteLater(); + + emit tileRequestFinished( QgsVectorTileRawData( tileID, rawData ) ); + } + else + { + QgsDebugMsg( QStringLiteral( "Tile download failed! " ) + reply->errorString() ); + mReplies.removeOne( reply ); + reply->deleteLater(); + + emit tileRequestFinished( QgsVectorTileRawData( tileID, QByteArray() ) ); + } + + if ( mReplies.isEmpty() ) + { + // exist the event loop + QMetaObject::invokeMethod( mEventLoop.get(), "quit", Qt::QueuedConnection ); + } +} + +void QgsVectorTileLoader::canceled() +{ + QgsDebugMsgLevel( QStringLiteral( "Canceling %1 pending requests" ).arg( mReplies.count() ), 2 ); + const QList replies = mReplies; + for ( QNetworkReply *reply : replies ) + { + reply->abort(); + } +} + +////// + +QList QgsVectorTileLoader::blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, int zoomLevel, const QPointF &viewCenter, const QgsTileRange &range ) +{ + QList rawTiles; + + QgsMBTilesReader mbReader( sourcePath ); + bool isUrl = ( sourceType == QStringLiteral( "xyz" ) ); + if ( !isUrl ) + { + bool res = mbReader.open(); + Q_ASSERT( res ); + } + + QVector tiles = QgsVectorTileUtils::tilesInRange( range, zoomLevel ); + QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter ); + for ( QgsTileXYZ id : qgis::as_const( tiles ) ) + { + QByteArray rawData = isUrl ? loadFromNetwork( id, sourcePath ) : loadFromMBTiles( id, mbReader ); + if ( !rawData.isEmpty() ) + { + rawTiles.append( QgsVectorTileRawData( id, rawData ) ); + } + } + return rawTiles; +} + +QByteArray QgsVectorTileLoader::loadFromNetwork( const QgsTileXYZ &id, const QString &requestUrl ) +{ + QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id ); + QNetworkRequest nr; + nr.setUrl( QUrl( url ) ); + QgsBlockingNetworkRequest req; + QgsDebugMsgLevel( QStringLiteral( "Blocking request: " ) + url, 2 ); + QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr ); + if ( errCode != QgsBlockingNetworkRequest::NoError ) + { + QgsDebugMsg( QStringLiteral( "Request failed: " ) + url ); + return QByteArray(); + } + QgsNetworkReplyContent reply = req.reply(); + QgsDebugMsgLevel( QStringLiteral( "Request successful, content size %1" ).arg( reply.content().size() ), 2 ); + return reply.content(); +} + + +QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMBTilesReader &mbTileReader ) +{ + // MBTiles uses TMS specs with Y starting at the bottom while XYZ uses Y starting at the top + int rowTMS = pow( 2, id.zoomLevel() ) - id.row() - 1; + QByteArray gzippedTileData = mbTileReader.tileData( id.zoomLevel(), id.column(), rowTMS ); + if ( gzippedTileData.isEmpty() ) + { + QgsDebugMsg( QStringLiteral( "Failed to get tile " ) + id.toString() ); + return QByteArray(); + } + + // TODO: check format is "pbf" + + QByteArray data; + if ( !decodeGzip( gzippedTileData, data ) ) + { + QgsDebugMsg( QStringLiteral( "Failed to decompress tile " ) + id.toString() ); + return QByteArray(); + } + + QgsDebugMsgLevel( QStringLiteral( "Tile blob size %1 -> uncompressed size %2" ).arg( gzippedTileData.size() ).arg( data.size() ), 2 ); + return data; +} + + +bool QgsVectorTileLoader::decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut ) +{ + unsigned char *bytesInPtr = reinterpret_cast( const_cast( bytesIn.constData() ) ); + uint bytesInLeft = static_cast( bytesIn.count() ); + + const uint CHUNK = 16384; + unsigned char out[CHUNK]; + const int DEC_MAGIC_NUM_FOR_GZIP = 16; + + // allocate inflate state + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = Z_NULL; + + int ret = inflateInit2( &strm, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP ); + if ( ret != Z_OK ) + return false; + + while ( ret != Z_STREAM_END ) // done when inflate() says it's done + { + // prepare next chunk + uint bytesToProcess = std::min( CHUNK, bytesInLeft ); + strm.next_in = bytesInPtr; + strm.avail_in = bytesToProcess; + bytesInPtr += bytesToProcess; + bytesInLeft -= bytesToProcess; + + if ( bytesToProcess == 0 ) + break; // we end with an error - no more data but inflate() wants more data + + // run inflate() on input until output buffer not full + do + { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate( &strm, Z_NO_FLUSH ); + Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered + if ( ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR ) + { + inflateEnd( &strm ); + return false; + } + unsigned have = CHUNK - strm.avail_out; + bytesOut.append( QByteArray::fromRawData( reinterpret_cast( out ), static_cast( have ) ) ); + } + while ( strm.avail_out == 0 ); + } + + inflateEnd( &strm ); + return ret == Z_STREAM_END; +} diff --git a/src/core/vectortile/qgsvectortileloader.h b/src/core/vectortile/qgsvectortileloader.h new file mode 100644 index 000000000000..e4b0b78a330c --- /dev/null +++ b/src/core/vectortile/qgsvectortileloader.h @@ -0,0 +1,103 @@ +/*************************************************************************** + qgsvectortileloader.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILELOADER_H +#define QGSVECTORTILELOADER_H + +#define SIP_NO_FILE + +class QByteArray; + +#include "qgsvectortilerenderer.h" + +/** + * \ingroup core + * Keeps track of raw tile data that need to be decoded + * + * \since QGIS 3.14 + */ +class QgsVectorTileRawData +{ + public: + //! Constructs a raw tile object + QgsVectorTileRawData( QgsTileXYZ tileID = QgsTileXYZ(), const QByteArray &raw = QByteArray() ) + : id( tileID ), data( raw ) {} + + //! Tile position in tile matrix set + QgsTileXYZ id; + //! Raw tile data + QByteArray data; +}; + + +class QNetworkReply; +class QEventLoop; + +class QgsMBTilesReader; + +/** + * \ingroup core + * The loader class takes care of loading raw vector tile data from a tile source. + * + * \since QGIS 3.14 + */ +class QgsVectorTileLoader : public QObject +{ + Q_OBJECT + public: + + //! Returns raw tile data for the specified range of tiles. Blocks the caller until all tiles are fetched. + static QList blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, int zoomLevel, const QPointF &viewCenter, const QgsTileRange &range ); + + //! Returns raw tile data for a single tile, doing a HTTP request. Block the caller until tile data are downloaded. + static QByteArray loadFromNetwork( const QgsTileXYZ &id, const QString &requestUrl ); + //! Returns raw tile data for a single tile loaded from MBTiles file + static QByteArray loadFromMBTiles( const QgsTileXYZ &id, QgsMBTilesReader &mbTileReader ); + //! Decodes gzip byte stream, returns true on success + static bool decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut ); + + // + // non-static stuff + // + + //! Constructs tile loader for doing asynchronous requests and starts network requests + QgsVectorTileLoader( const QString &uri, int zoomLevel, const QgsTileRange &range, const QPointF &viewCenter, QgsFeedback *feedback ); + ~QgsVectorTileLoader(); + + //! Blocks the caller until all asynchronous requests are finished (with a success or a failure) + void downloadBlocking(); + + private: + void loadFromNetworkAsync( const QgsTileXYZ &id, const QString &requestUrl ); + + private slots: + void tileReplyFinished(); + void canceled(); + + signals: + //! Emitted when a tile request has finished. If a tile request has failed, the returned raw tile byte array is empty. + void tileRequestFinished( const QgsVectorTileRawData &rawTile ); + + private: + //! Event loop used for blocking download + std::unique_ptr mEventLoop; + //! Feedback object that allows cancellation of pending requests + QgsFeedback *mFeedback; + //! Running tile requests + QList mReplies; + +}; + +#endif // QGSVECTORTILELOADER_H diff --git a/src/core/vectortile/qgsvectortilemvtdecoder.cpp b/src/core/vectortile/qgsvectortilemvtdecoder.cpp new file mode 100644 index 000000000000..5e261c3df572 --- /dev/null +++ b/src/core/vectortile/qgsvectortilemvtdecoder.cpp @@ -0,0 +1,338 @@ +/*************************************************************************** + qgsvectortilemvtdecoder.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 + +#include "qgsvectortilemvtdecoder.h" + +#include "qgsvectortilelayerrenderer.h" +#include "qgsvectortileutils.h" + +#include "qgslogger.h" +#include "qgsmultipoint.h" +#include "qgslinestring.h" +#include "qgsmultilinestring.h" +#include "qgsmultipolygon.h" +#include "qgspolygon.h" + + +inline bool _isExteriorRing( const QVector &pts ) +{ + // Exterior rings have POSITIVE area while interior rings have NEGATIVE area + // when calculated with https://en.wikipedia.org/wiki/Shoelace_formula + // The orientation of axes is that X grows to the right and Y grows to the bottom. + // the input data are expected to form a closed ring, i.e. first pt == last pt. + + double total = 0.0; + const QgsPoint *ptsPtr = pts.constData(); + int count = pts.count(); + for ( int i = 0; i < count - 1; i++ ) + { + double val = ( pts[i + 1].x() - ptsPtr[i].x() ) * ( ptsPtr[i + 1].y() + pts[i].y() ); + //double val = ptsPtr[i].x() * (-ptsPtr[i+1].y()) - ptsPtr[i+1].x() * (-ptsPtr[i].y()); // gives the same result + total += val; + } + return total >= 0; +} + + +bool QgsVectorTileMVTDecoder::decode( QgsTileXYZ tileID, const QByteArray &rawTileData ) +{ + if ( !tile.ParseFromArray( rawTileData.constData(), rawTileData.count() ) ) + return false; + + mTileID = tileID; + + mLayerNameToIndex.clear(); + for ( int layerNum = 0; layerNum < tile.layers_size(); layerNum++ ) + { + const ::vector_tile::Tile_Layer &layer = tile.layers( layerNum ); + QString layerName = layer.name().c_str(); + mLayerNameToIndex[layerName] = layerNum; + } + return true; +} + +QStringList QgsVectorTileMVTDecoder::layers() const +{ + QStringList layerNames; + for ( int layerNum = 0; layerNum < tile.layers_size(); layerNum++ ) + { + const ::vector_tile::Tile_Layer &layer = tile.layers( layerNum ); + QString layerName = layer.name().c_str(); + layerNames << layerName; + } + return layerNames; +} + +QStringList QgsVectorTileMVTDecoder::layerFieldNames( const QString &layerName ) const +{ + if ( !mLayerNameToIndex.contains( layerName ) ) + return QStringList(); + + const ::vector_tile::Tile_Layer &layer = tile.layers( mLayerNameToIndex[layerName] ); + QStringList fieldNames; + for ( int i = 0; i < layer.keys_size(); ++i ) + { + QString fieldName = layer.keys( i ).c_str(); + fieldNames << fieldName; + } + return fieldNames; +} + +QgsVectorTileFeatures QgsVectorTileMVTDecoder::layerFeatures( const QMap &perLayerFields, const QgsCoordinateTransform &ct ) const +{ + QgsVectorTileFeatures features; + + int numTiles = static_cast( pow( 2, mTileID.zoomLevel() ) ); // assuming we won't ever go over 30 zoom levels + double z0xMin = -20037508.3427892, z0yMin = -20037508.3427892; + double z0xMax = 20037508.3427892, z0yMax = 20037508.3427892; + double tileDX = ( z0xMax - z0xMin ) / numTiles; + double tileDY = ( z0yMax - z0yMin ) / numTiles; + double tileXMin = z0xMin + mTileID.column() * tileDX; + double tileYMax = z0yMax - mTileID.row() * tileDY; + + for ( int layerNum = 0; layerNum < tile.layers_size(); layerNum++ ) + { + const ::vector_tile::Tile_Layer &layer = tile.layers( layerNum ); + + QString layerName = layer.name().c_str(); + QVector layerFeatures; + QgsFields layerFields = perLayerFields[layerName]; + + // figure out how field indexes in MVT encoding map to field indexes in QgsFields (we may not use all available fields) + QHash tagKeyIndexToFieldIndex; + for ( int i = 0; i < layer.keys_size(); ++i ) + { + int fieldIndex = layerFields.indexOf( layer.keys( i ).c_str() ); + if ( fieldIndex != -1 ) + tagKeyIndexToFieldIndex.insert( i, fieldIndex ); + } + + // go through features of a layer + for ( int featureNum = 0; featureNum < layer.features_size(); featureNum++ ) + { + const ::vector_tile::Tile_Feature &feature = layer.features( featureNum ); + + QgsFeature f( layerFields, static_cast( feature.id() ) ); + + // + // parse attributes + // + + for ( int tagNum = 0; tagNum + 1 < feature.tags_size(); tagNum += 2 ) + { + int keyIndex = static_cast( feature.tags( tagNum ) ); + int fieldIndex = tagKeyIndexToFieldIndex.value( keyIndex, -1 ); + if ( fieldIndex == -1 ) + continue; + + int valueIndex = static_cast( feature.tags( tagNum + 1 ) ); + if ( valueIndex >= layer.values_size() ) + { + QgsDebugMsg( QStringLiteral( "Invalid value index for attribute" ) ); + continue; + } + const ::vector_tile::Tile_Value &value = layer.values( valueIndex ); + + if ( value.has_string_value() ) + f.setAttribute( fieldIndex, QString::fromStdString( value.string_value() ) ); + else if ( value.has_float_value() ) + f.setAttribute( fieldIndex, static_cast( value.float_value() ) ); + else if ( value.has_double_value() ) + f.setAttribute( fieldIndex, value.double_value() ); + else if ( value.has_int_value() ) + f.setAttribute( fieldIndex, static_cast( value.int_value() ) ); + else if ( value.has_uint_value() ) + f.setAttribute( fieldIndex, static_cast( value.uint_value() ) ); + else if ( value.has_sint_value() ) + f.setAttribute( fieldIndex, static_cast( value.sint_value() ) ); + else if ( value.has_bool_value() ) + f.setAttribute( fieldIndex, static_cast( value.bool_value() ) ); + else + { + QgsDebugMsg( QStringLiteral( "Unexpected attribute value" ) ); + } + } + + // + // parse geometry + // + + int extent = static_cast( layer.extent() ); + int cursorx = 0, cursory = 0; + + QVector outputPoints; // for point/multi-point + QVector outputLinestrings; // for linestring/multi-linestring + QVector outputPolygons; + QVector tmpPoints; + + for ( int i = 0; i < feature.geometry_size(); i ++ ) + { + unsigned g = feature.geometry( i ); + unsigned cmdId = g & 0x7; + unsigned cmdCount = g >> 3; + if ( cmdId == 1 ) // MoveTo + { + if ( i + static_cast( cmdCount ) * 2 >= feature.geometry_size() ) + { + QgsDebugMsg( QStringLiteral( "Malformed geometry: invalid cmdCount" ) ); + break; + } + for ( unsigned j = 0; j < cmdCount; j++ ) + { + unsigned v = feature.geometry( i + 1 ); + unsigned w = feature.geometry( i + 2 ); + int dx = ( ( v >> 1 ) ^ ( -( v & 1 ) ) ); + int dy = ( ( w >> 1 ) ^ ( -( w & 1 ) ) ); + cursorx += dx; + cursory += dy; + double px = tileXMin + tileDX * double( cursorx ) / double( extent ); + double py = tileYMax - tileDY * double( cursory ) / double( extent ); + + if ( feature.type() == vector_tile::Tile_GeomType_POINT ) + { + outputPoints.append( new QgsPoint( px, py ) ); + } + else if ( feature.type() == vector_tile::Tile_GeomType_LINESTRING ) + { + if ( tmpPoints.size() > 0 ) + { + outputLinestrings.append( new QgsLineString( tmpPoints ) ); + tmpPoints.clear(); + } + tmpPoints.append( QgsPoint( px, py ) ); + } + else if ( feature.type() == vector_tile::Tile_GeomType_POLYGON ) + { + tmpPoints.append( QgsPoint( px, py ) ); + } + i += 2; + } + } + else if ( cmdId == 2 ) // LineTo + { + if ( i + static_cast( cmdCount ) * 2 >= feature.geometry_size() ) + { + QgsDebugMsg( QStringLiteral( "Malformed geometry: invalid cmdCount" ) ); + break; + } + for ( unsigned j = 0; j < cmdCount; j++ ) + { + unsigned v = feature.geometry( i + 1 ); + unsigned w = feature.geometry( i + 2 ); + int dx = ( ( v >> 1 ) ^ ( -( v & 1 ) ) ); + int dy = ( ( w >> 1 ) ^ ( -( w & 1 ) ) ); + cursorx += dx; + cursory += dy; + double px = tileXMin + tileDX * double( cursorx ) / double( extent ); + double py = tileYMax - tileDY * double( cursory ) / double( extent ); + + tmpPoints.push_back( QgsPoint( px, py ) ); + i += 2; + } + } + else if ( cmdId == 7 ) // ClosePath + { + if ( feature.type() == vector_tile::Tile_GeomType_POLYGON ) + { + tmpPoints.append( tmpPoints.first() ); // close the ring + + if ( _isExteriorRing( tmpPoints ) ) + { + // start a new polygon + QgsPolygon *p = new QgsPolygon; + p->setExteriorRing( new QgsLineString( tmpPoints ) ); + outputPolygons.append( p ); + tmpPoints.clear(); + } + else + { + // interior ring (hole) + if ( outputPolygons.count() != 0 ) + { + outputPolygons[outputPolygons.count() - 1]->addInteriorRing( new QgsLineString( tmpPoints ) ); + } + else + { + QgsDebugMsg( QStringLiteral( "Malformed geometry: first ring of a polygon is interior ring" ) ); + } + tmpPoints.clear(); + } + } + + } + else + { + QgsDebugMsg( QStringLiteral( "Unexpected command ID: %1" ).arg( cmdId ) ); + } + } + + QString geomType; + if ( feature.type() == vector_tile::Tile_GeomType_POINT ) + { + geomType = QStringLiteral( "Point" ); + if ( outputPoints.count() == 1 ) + f.setGeometry( QgsGeometry( outputPoints[0] ) ); + else + { + QgsMultiPoint *mp = new QgsMultiPoint; + for ( int k = 0; k < outputPoints.count(); ++k ) + mp->addGeometry( outputPoints[k] ); + f.setGeometry( QgsGeometry( mp ) ); + } + } + else if ( feature.type() == vector_tile::Tile_GeomType_LINESTRING ) + { + geomType = QStringLiteral( "LineString" ); + + // finish the linestring we have started + outputLinestrings.append( new QgsLineString( tmpPoints ) ); + + if ( outputLinestrings.count() == 1 ) + f.setGeometry( QgsGeometry( outputLinestrings[0] ) ); + else + { + QgsMultiLineString *mls = new QgsMultiLineString; + for ( int k = 0; k < outputLinestrings.count(); ++k ) + mls->addGeometry( outputLinestrings[k] ); + f.setGeometry( QgsGeometry( mls ) ); + } + } + else if ( feature.type() == vector_tile::Tile_GeomType_POLYGON ) + { + geomType = QStringLiteral( "Polygon" ); + + if ( outputPolygons.count() == 1 ) + f.setGeometry( QgsGeometry( outputPolygons[0] ) ); + else + { + QgsMultiPolygon *mpl = new QgsMultiPolygon; + for ( int k = 0; k < outputPolygons.count(); ++k ) + mpl->addGeometry( outputPolygons[k] ); + f.setGeometry( QgsGeometry( mpl ) ); + } + } + + f.setAttribute( QStringLiteral( "_geom_type" ), geomType ); + f.geometry().transform( ct ); + + layerFeatures.append( f ); + } + + features[layerName] = layerFeatures; + } + return features; +} diff --git a/src/core/vectortile/qgsvectortilemvtdecoder.h b/src/core/vectortile/qgsvectortilemvtdecoder.h new file mode 100644 index 000000000000..aff57065d6ef --- /dev/null +++ b/src/core/vectortile/qgsvectortilemvtdecoder.h @@ -0,0 +1,58 @@ +/*************************************************************************** + qgsvectortilemvtdecoder.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEMVTDECODER_H +#define QGSVECTORTILEMVTDECODER_H + +#define SIP_NO_FILE + +class QgsFeature; + +#include +#include + +#include "vector_tile.pb.h" + +#include "qgsvectortilerenderer.h" + +/** + * \ingroup core + * This class is responsible for decoding raw tile data written with Mapbox Vector Tiles encoding. + * + * \since QGIS 3.14 + */ +class QgsVectorTileMVTDecoder +{ + public: + + //! Tries to decode raw tile data, returns true on success + bool decode( QgsTileXYZ tileID, const QByteArray &rawTileData ); + + //! Returns a list of sub-layer names in a tile. It can only be called after a successful decode() + QStringList layers() const; + + //! Returns a list of all field names in a tile. It can only be called after a successful decode() + QStringList layerFieldNames( const QString &layerName ) const; + + //! Returns decoded features grouped by sub-layers. It can only be called after a successful decode() + QgsVectorTileFeatures layerFeatures( const QMap &perLayerFields, const QgsCoordinateTransform &ct ) const; + + private: + vector_tile::Tile tile; + QgsTileXYZ mTileID; + QMap mLayerNameToIndex; +}; + +#endif // QGSVECTORTILEMVTDECODER_H diff --git a/src/core/vectortile/qgsvectortileprovidermetadata.cpp b/src/core/vectortile/qgsvectortileprovidermetadata.cpp new file mode 100644 index 000000000000..9c3dc253b5cd --- /dev/null +++ b/src/core/vectortile/qgsvectortileprovidermetadata.cpp @@ -0,0 +1,58 @@ +/*************************************************************************** + qgsvectortileprovidermetadata.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortileprovidermetadata.h" + +#include "qgsvectortileconnection.h" +#include "qgsvectortiledataitems.h" + +///@cond PRIVATE + +#define PROVIDER_KEY QStringLiteral( "vectortile" ) +#define PROVIDER_DESCRIPTION QStringLiteral( "Vector tile provider" ) + +QgsVectorTileProviderMetadata::QgsVectorTileProviderMetadata() + : QgsProviderMetadata( PROVIDER_KEY, PROVIDER_DESCRIPTION ) +{ +} + +QList QgsVectorTileProviderMetadata::dataItemProviders() const +{ + QList< QgsDataItemProvider * > providers; + providers << new QgsVectorTileDataItemProvider; + return providers; +} + +QMap QgsVectorTileProviderMetadata::connections( bool cached ) +{ + return connectionsProtected( cached ); +} + +QgsAbstractProviderConnection *QgsVectorTileProviderMetadata::createConnection( const QString &name ) +{ + return new QgsVectorTileProviderConnection( name ); +} + +void QgsVectorTileProviderMetadata::deleteConnection( const QString &name ) +{ + deleteConnectionProtected( name ); +} + +void QgsVectorTileProviderMetadata::saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) +{ + saveConnectionProtected( connection, name ); +} + +///@endcond diff --git a/src/core/vectortile/qgsvectortileprovidermetadata.h b/src/core/vectortile/qgsvectortileprovidermetadata.h new file mode 100644 index 000000000000..cc83b7319376 --- /dev/null +++ b/src/core/vectortile/qgsvectortileprovidermetadata.h @@ -0,0 +1,47 @@ +/*************************************************************************** + qgsvectortileprovidermetadata.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEPROVIDERMETADATA_H +#define QGSVECTORTILEPROVIDERMETADATA_H + + +#include "qgsprovidermetadata.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +/** + * This metadata class does not support creation of provider instances, because + * vector tile layer currently does not have a concept of data providers. This class + * is only used to create data item provider (for browser integration). + */ +class QgsVectorTileProviderMetadata : public QgsProviderMetadata +{ + public: + QgsVectorTileProviderMetadata(); + QList< QgsDataItemProvider * > dataItemProviders() const override; + + // handling of stored connections + + QMap connections( bool cached ) override; + QgsAbstractProviderConnection *createConnection( const QString &name ) override; + void deleteConnection( const QString &name ) override; + void saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) override; + +}; + +///@endcond + +#endif // QGSVECTORTILEPROVIDERMETADATA_H diff --git a/src/core/vectortile/qgsvectortilerenderer.h b/src/core/vectortile/qgsvectortilerenderer.h new file mode 100644 index 000000000000..2b42704a938e --- /dev/null +++ b/src/core/vectortile/qgsvectortilerenderer.h @@ -0,0 +1,125 @@ +/*************************************************************************** + qgsvectortilerenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILERENDERER_H +#define QGSVECTORTILERENDERER_H + +#include "qgis_core.h" + +#include "qgsfeature.h" + +#include "qgstiles.h" + +class QgsRenderContext; + +//! Features of a vector tile, grouped by sub-layer names (key of the map) +typedef QMap > QgsVectorTileFeatures SIP_SKIP; + +/** + * \ingroup core + * Contains decoded features of a single vector tile and any other data necessary + * for rendering of it. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileRendererData +{ + public: + //! Constructs the object + explicit QgsVectorTileRendererData( QgsTileXYZ id ): mId( id ) {} + + //! Returns coordinates of the tile + QgsTileXYZ id() const { return mId; } + + //! Sets polygon of the tile + void setTilePolygon( QPolygon polygon ) { mTilePolygon = polygon; } + //! Returns polygon (made out of four corners of the tile) in screen coordinates calculated from render context + QPolygon tilePolygon() const { return mTilePolygon; } + + //! Sets features of the tile + void setFeatures( const QgsVectorTileFeatures &features ) SIP_SKIP { mFeatures = features; } + //! Returns features of the tile grouped by sub-layer names + QgsVectorTileFeatures features() const SIP_SKIP { return mFeatures; } + //! Returns list of layer names present in the tile + QStringList layers() const { return mFeatures.keys(); } + //! Returns list of all features within a single sub-layer + QVector layerFeatures( const QString &layerName ) const { return mFeatures[layerName]; } + + private: + //! Position of the tile in the tile matrix set + QgsTileXYZ mId; + //! Features of the tile grouped into sub-layers + QgsVectorTileFeatures mFeatures; + //! Polygon (made out of four corners of the tile) in screen coordinates calculated from render context + QPolygon mTilePolygon; +}; + +/** + * \ingroup core + * Abstract base class for all vector tile renderer implementations. + * + * For rendering it is expected that client code calls: + * 1. startRender() to prepare renderer + * 2. renderTile() for each tile + * 3. stopRender() to clean up renderer and free resources + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileRenderer +{ + +#ifdef SIP_RUN + SIP_CONVERT_TO_SUBCLASS_CODE + + const QString type = sipCpp->type(); + + if ( type == QStringLiteral( "basic" ) ) + sipType = sipType_QgsVectorTileBasicRenderer; + else + sipType = 0; + SIP_END +#endif + + public: + virtual ~QgsVectorTileRenderer() = default; + + //! Returns unique type name of the renderer implementation + virtual QString type() const = 0; + + //! Returns a clone of the renderer + virtual QgsVectorTileRenderer *clone() const = 0 SIP_FACTORY; + + //! Initializes rendering. It should be paired with a stopRender() call. + virtual void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) = 0; + + //! Returns field names of sub-layers that will be used for rendering. Must be called between startRender/stopRender. + virtual QMap > usedAttributes( const QgsRenderContext & ) SIP_SKIP { return QMap >(); } + + //! Finishes rendering and cleans up any resources + virtual void stopRender( QgsRenderContext &context ) = 0; + + //! Renders given vector tile. Must be called between startRender/stopRender. + virtual void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) = 0; + + //! Writes renderer's properties to given XML element + virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const = 0; + //! Reads renderer's properties from given XML element + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) = 0; + //! Resolves references to other objects - second phase of loading - after readXml() + virtual void resolveReferences( const QgsProject &project ) { Q_UNUSED( project ) } + +}; + +#endif // QGSVECTORTILERENDERER_H diff --git a/src/core/vectortile/qgsvectortileutils.cpp b/src/core/vectortile/qgsvectortileutils.cpp new file mode 100644 index 000000000000..5b4022a95ccc --- /dev/null +++ b/src/core/vectortile/qgsvectortileutils.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + qgsvectortileutils.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortileutils.h" + +#include + +#include + +#include "qgscoordinatetransform.h" +#include "qgsgeometrycollection.h" +#include "qgsfields.h" +#include "qgslogger.h" +#include "qgsmaptopixel.h" +#include "qgsrectangle.h" +#include "qgsvectorlayer.h" + +#include "qgsvectortilemvtdecoder.h" +#include "qgsvectortilelayer.h" +#include "qgsvectortilerenderer.h" + + + +QPolygon QgsVectorTileUtils::tilePolygon( QgsTileXYZ id, const QgsCoordinateTransform &ct, const QgsTileMatrix &tm, const QgsMapToPixel &mtp ) +{ + QgsRectangle r = tm.tileExtent( id ); + QgsPointXY p00a = mtp.transform( ct.transform( r.xMinimum(), r.yMinimum() ) ); + QgsPointXY p11a = mtp.transform( ct.transform( r.xMaximum(), r.yMaximum() ) ); + QgsPointXY p01a = mtp.transform( ct.transform( r.xMinimum(), r.yMaximum() ) ); + QgsPointXY p10a = mtp.transform( ct.transform( r.xMaximum(), r.yMinimum() ) ); + QPolygon path; + path << p00a.toQPointF().toPoint(); + path << p01a.toQPointF().toPoint(); + path << p11a.toQPointF().toPoint(); + path << p10a.toQPointF().toPoint(); + return path; +} + +QgsFields QgsVectorTileUtils::makeQgisFields( QSet flds ) +{ + QgsFields fields; + for ( QString fieldName : flds ) + { + fields.append( QgsField( fieldName, QVariant::String ) ); + } + return fields; +} + + +int QgsVectorTileUtils::scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom ) +{ + double s0 = 559082264.0287178; // scale denominator at zoom level 0 of GoogleCRS84Quad + double tileZoom2 = log( s0 / mapScale ) / log( 2 ); + tileZoom2 -= 1; // TODO: it seems that map scale is double (is that because of high-dpi screen?) + int tileZoom = static_cast( round( tileZoom2 ) ); + + if ( tileZoom < sourceMinZoom ) + tileZoom = sourceMinZoom; + if ( tileZoom > sourceMaxZoom ) + tileZoom = sourceMaxZoom; + + return tileZoom; +} + +QgsVectorLayer *QgsVectorTileUtils::makeVectorLayerForTile( QgsVectorTileLayer *mvt, QgsTileXYZ tileID, const QString &layerName ) +{ + QgsVectorTileMVTDecoder decoder; + decoder.decode( tileID, mvt->getRawTile( tileID ) ); + QSet fieldNames = QSet::fromList( decoder.layerFieldNames( layerName ) ); + fieldNames << QStringLiteral( "_geom_type" ); + QMap perLayerFields; + QgsFields fields = QgsVectorTileUtils::makeQgisFields( fieldNames ); + perLayerFields[layerName] = fields; + QgsVectorTileFeatures data = decoder.layerFeatures( perLayerFields, QgsCoordinateTransform() ); + QgsFeatureList featuresList = data[layerName].toList(); + + // turn all geometries to geom. collections (otherwise they won't be accepted by memory provider) + for ( int i = 0; i < featuresList.count(); ++i ) + { + QgsGeometry g = featuresList[i].geometry(); + QgsGeometryCollection *gc = new QgsGeometryCollection; + const QgsAbstractGeometry *gg = g.constGet(); + if ( const QgsGeometryCollection *ggc = qgsgeometry_cast( gg ) ) + { + for ( int k = 0; k < ggc->numGeometries(); ++k ) + gc->addGeometry( ggc->geometryN( k )->clone() ); + } + else + gc->addGeometry( gg->clone() ); + featuresList[i].setGeometry( QgsGeometry( gc ) ); + } + + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "GeometryCollection" ), layerName, QStringLiteral( "memory" ) ); + vl->dataProvider()->addAttributes( fields.toList() ); + vl->updateFields(); + bool res = vl->dataProvider()->addFeatures( featuresList ); + Q_ASSERT( res ); + Q_ASSERT( featuresList.count() == vl->featureCount() ); + vl->updateExtents(); + QgsDebugMsgLevel( QStringLiteral( "Layer %1 features %2" ).arg( layerName ).arg( vl->featureCount() ), 2 ); + return vl; +} + + +QString QgsVectorTileUtils::formatXYZUrlTemplate( const QString &url, QgsTileXYZ tile ) +{ + QString turl( url ); + + turl.replace( QLatin1String( "{x}" ), QString::number( tile.column() ), Qt::CaseInsensitive ); + // TODO: inverted Y axis +// if ( turl.contains( QLatin1String( "{-y}" ) ) ) +// { +// turl.replace( QLatin1String( "{-y}" ), QString::number( tm.matrixHeight - tile.tileRow - 1 ), Qt::CaseInsensitive ); +// } +// else + { + turl.replace( QLatin1String( "{y}" ), QString::number( tile.row() ), Qt::CaseInsensitive ); + } + turl.replace( QLatin1String( "{z}" ), QString::number( tile.zoomLevel() ), Qt::CaseInsensitive ); + return turl; +} + +//! a helper class for ordering tile requests according to the distance from view center +struct LessThanTileRequest +{ + QPointF center; //!< Center in tile matrix (!) coordinates + bool operator()( const QgsTileXYZ &req1, const QgsTileXYZ &req2 ) + { + QPointF p1( req1.column() + 0.5, req1.row() + 0.5 ); + QPointF p2( req2.column() + 0.5, req2.row() + 0.5 ); + // using chessboard distance (loading order more natural than euclidean/manhattan distance) + double d1 = std::max( std::fabs( center.x() - p1.x() ), std::fabs( center.y() - p1.y() ) ); + double d2 = std::max( std::fabs( center.x() - p2.x() ), std::fabs( center.y() - p2.y() ) ); + return d1 < d2; + } +}; + +QVector QgsVectorTileUtils::tilesInRange( const QgsTileRange &range, int zoomLevel ) +{ + QVector tiles; + for ( int tileRow = range.startRow(); tileRow <= range.endRow(); ++tileRow ) + { + for ( int tileColumn = range.startColumn(); tileColumn <= range.endColumn(); ++tileColumn ) + { + tiles.append( QgsTileXYZ( tileColumn, tileRow, zoomLevel ) ); + } + } + return tiles; +} + +void QgsVectorTileUtils::sortTilesByDistanceFromCenter( QVector &tiles, const QPointF ¢er ) +{ + LessThanTileRequest cmp; + cmp.center = center; + std::sort( tiles.begin(), tiles.end(), cmp ); +} diff --git a/src/core/vectortile/qgsvectortileutils.h b/src/core/vectortile/qgsvectortileutils.h new file mode 100644 index 000000000000..4ceb86dbbdfa --- /dev/null +++ b/src/core/vectortile/qgsvectortileutils.h @@ -0,0 +1,64 @@ +/*************************************************************************** + qgsvectortileutils.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEUTILS_H +#define QGSVECTORTILEUTILS_H + +#define SIP_NO_FILE + +#include + +class QPointF; +class QPolygon; + +class QgsCoordinateTransform; +class QgsFields; +class QgsMapToPixel; +class QgsRectangle; +class QgsVectorLayer; + +class QgsTileMatrix; +class QgsTileRange; +class QgsTileXYZ; +class QgsVectorTileLayer; + +/** + * \ingroup core + * Random utility functions for working with vector tiles + * + * \since QGIS 3.14 + */ +class QgsVectorTileUtils +{ + public: + + //! Returns a list of tiles in the given tile range + static QVector tilesInRange( const QgsTileRange &range, int zoomLevel ); + //! Orders tile requests according to the distance from view center (given in tile matrix coords) + static void sortTilesByDistanceFromCenter( QVector &tiles, const QPointF ¢er ); + + //! Returns polygon (made by four corners of the tile) in screen coordinates + static QPolygon tilePolygon( QgsTileXYZ id, const QgsCoordinateTransform &ct, const QgsTileMatrix &tm, const QgsMapToPixel &mtp ); + //! Returns QgsFields instance based on the set of field names + static QgsFields makeQgisFields( QSet flds ); + //! Finds best fitting zoom level (assuming GoogleCRS84Quad tile matrix set) given map scale denominator and allowed zoom level range + static int scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom ); + //! Returns a temporary vector layer for given sub-layer of tile in vector tile layer + static QgsVectorLayer *makeVectorLayerForTile( QgsVectorTileLayer *mvt, QgsTileXYZ tileID, const QString &layerName ); + //! Returns formatted tile URL string replacing {x}, {y}, {z} placeholders + static QString formatXYZUrlTemplate( const QString &url, QgsTileXYZ tile ); +}; + +#endif // QGSVECTORTILEUTILS_H diff --git a/src/core/vectortile/vector_tile.proto b/src/core/vectortile/vector_tile.proto new file mode 100644 index 000000000000..f9aa8023d786 --- /dev/null +++ b/src/core/vectortile/vector_tile.proto @@ -0,0 +1,82 @@ +syntax = "proto2"; // needed by newer protoc compilers +// The rest of the file is a verbatim copy of MVT 2.1 proto file: +// https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto + +package vector_tile; + +option optimize_for = LITE_RUNTIME; + +message Tile { + + // GeomType is described in section 4.3.4 of the specification + enum GeomType { + UNKNOWN = 0; + POINT = 1; + LINESTRING = 2; + POLYGON = 3; + } + + // Variant type encoding + // The use of values is described in section 4.1 of the specification + message Value { + // Exactly one of these values must be present in a valid message + optional string string_value = 1; + optional float float_value = 2; + optional double double_value = 3; + optional int64 int_value = 4; + optional uint64 uint_value = 5; + optional sint64 sint_value = 6; + optional bool bool_value = 7; + + extensions 8 to max; + } + + // Features are described in section 4.2 of the specification + message Feature { + optional uint64 id = 1 [ default = 0 ]; + + // Tags of this feature are encoded as repeated pairs of + // integers. + // A detailed description of tags is located in sections + // 4.2 and 4.4 of the specification + repeated uint32 tags = 2 [ packed = true ]; + + // The type of geometry stored in this feature. + optional GeomType type = 3 [ default = UNKNOWN ]; + + // Contains a stream of commands and parameters (vertices). + // A detailed description on geometry encoding is located in + // section 4.3 of the specification. + repeated uint32 geometry = 4 [ packed = true ]; + } + + // Layers are described in section 4.1 of the specification + message Layer { + // Any compliant implementation must first read the version + // number encoded in this message and choose the correct + // implementation for this version number before proceeding to + // decode other parts of this message. + required uint32 version = 15 [ default = 1 ]; + + required string name = 1; + + // The actual features in this tile. + repeated Feature features = 2; + + // Dictionary encoding for keys + repeated string keys = 3; + + // Dictionary encoding for values + repeated Value values = 4; + + // Although this is an "optional" field it is required by the specification. + // See https://github.com/mapbox/vector-tile-spec/issues/47 + optional uint32 extent = 5 [ default = 4096 ]; + + extensions 16 to max; + } + + repeated Layer layers = 3; + + extensions 16 to 8191; +} diff --git a/src/crashhandler/qgscrashreport.cpp b/src/crashhandler/qgscrashreport.cpp index 35a3f54aaea1..5655efe5d613 100644 --- a/src/crashhandler/qgscrashreport.cpp +++ b/src/crashhandler/qgscrashreport.cpp @@ -171,6 +171,7 @@ QString QgsCrashReport::crashReportFolder() QString QgsCrashReport::htmlToMarkdown( const QString &html ) { + // Any changes in this function must be copied to qgsstringutils.cpp too QString converted = html; converted.replace( QLatin1String( "
    " ), QLatin1String( "\n" ) ); converted.replace( QLatin1String( "" ), QLatin1String( "**" ) ); @@ -181,6 +182,7 @@ QString QgsCrashReport::htmlToMarkdown( const QString &html ) while ( hrefRegEx.indexIn( converted, offset ) != -1 ) { QString url = hrefRegEx.cap( 1 ).replace( QStringLiteral( "\"" ), QString() ); + url.replace( '\'', QString() ); QString name = hrefRegEx.cap( 2 ); QString anchor = QStringLiteral( "[%1](%2)" ).arg( name, url ); converted.replace( hrefRegEx, anchor ); diff --git a/src/customwidgets/CMakeLists.txt b/src/customwidgets/CMakeLists.txt index 76c3faf92110..54519439e234 100644 --- a/src/customwidgets/CMakeLists.txt +++ b/src/customwidgets/CMakeLists.txt @@ -22,6 +22,7 @@ SET (QGIS_CUSTOMWIDGETS_SRCS qgsexpressionbuilderwidgetplugin.cpp qgsextentgroupboxplugin.cpp qgsexternalresourcewidgetplugin.cpp + qgsfeaturelistcomboboxplugin.cpp qgsfieldcomboboxplugin.cpp qgsfieldexpressionwidgetplugin.cpp qgsfilewidgetplugin.cpp @@ -54,6 +55,7 @@ SET (QGIS_CUSTOMWIDGETS_MOC_HDRS qgsexpressionbuilderwidgetplugin.h qgsextentgroupboxplugin.h qgsexternalresourcewidgetplugin.h + qgsfeaturelistcomboboxplugin.h qgsfieldcomboboxplugin.h qgsfieldexpressionwidgetplugin.h qgsfilewidgetplugin.h diff --git a/src/customwidgets/qgiscustomwidgets.cpp b/src/customwidgets/qgiscustomwidgets.cpp index 6f7d463767d5..0f02c6f3b140 100644 --- a/src/customwidgets/qgiscustomwidgets.cpp +++ b/src/customwidgets/qgiscustomwidgets.cpp @@ -26,6 +26,7 @@ #include "qgsexpressionbuilderwidgetplugin.h" #include "qgsextentgroupboxplugin.h" #include "qgsexternalresourcewidgetplugin.h" +#include "qgsfeaturelistcomboboxplugin.h" #include "qgsfieldcomboboxplugin.h" #include "qgsfieldexpressionwidgetplugin.h" #include "qgsfilewidgetplugin.h" @@ -58,6 +59,7 @@ QgisCustomWidgets::QgisCustomWidgets( QObject *parent ) mWidgets.append( new QgsExpressionBuilderWidgetPlugin( this ) ); mWidgets.append( new QgsExtentGroupBoxPlugin( this ) ); mWidgets.append( new QgsExternalResourceWidgetPlugin( this ) ); + mWidgets.append( new QgsFeatureListComboBoxPlugin( this ) ); mWidgets.append( new QgsFieldComboBoxPlugin( this ) ); mWidgets.append( new QgsFieldExpressionWidgetPlugin( this ) ); mWidgets.append( new QgsFileWidgetPlugin( this ) ); diff --git a/src/customwidgets/qgsfeaturelistcomboboxplugin.cpp b/src/customwidgets/qgsfeaturelistcomboboxplugin.cpp new file mode 100644 index 000000000000..382087ac63fd --- /dev/null +++ b/src/customwidgets/qgsfeaturelistcomboboxplugin.cpp @@ -0,0 +1,96 @@ +/*************************************************************************** + qgsfeaturelistcomboboxplugin.cpp + -------------------------------------- + Date : 26.03.2020 + Copyright : (C) 2020 Denis Rouzaud + Email : denis.rouzaud@gmail.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 "qgiscustomwidgets.h" +#include "qgsfeaturelistcomboboxplugin.h" +#include "qgsfeaturelistcombobox.h" + + +QgsFeatureListComboBoxPlugin::QgsFeatureListComboBoxPlugin( QObject *parent ) + : QObject( parent ) + , mInitialized( false ) +{ +} + +QString QgsFeatureListComboBoxPlugin::name() const +{ + return "QgsFeatureListComboBox"; +} + +QString QgsFeatureListComboBoxPlugin::group() const +{ + return QgisCustomWidgets::groupName(); +} + +QString QgsFeatureListComboBoxPlugin::includeFile() const +{ + return "qgsfeaturelistcombobox.h"; +} + +QIcon QgsFeatureListComboBoxPlugin::icon() const +{ + return QIcon( ":/images/icons/qgis-icon-60x60.png" ); +} + +bool QgsFeatureListComboBoxPlugin::isContainer() const +{ + return false; +} + +QWidget *QgsFeatureListComboBoxPlugin::createWidget( QWidget *parent ) +{ + return new QgsFeatureListComboBox( parent ); +} + +bool QgsFeatureListComboBoxPlugin::isInitialized() const +{ + return mInitialized; +} + +void QgsFeatureListComboBoxPlugin::initialize( QDesignerFormEditorInterface *core ) +{ + Q_UNUSED( core ) + if ( mInitialized ) + return; + mInitialized = true; +} + + +QString QgsFeatureListComboBoxPlugin::toolTip() const +{ + return ""; +} + +QString QgsFeatureListComboBoxPlugin::whatsThis() const +{ + return ""; +} + +QString QgsFeatureListComboBoxPlugin::domXml() const +{ + return QString( "\n" + " \n" + " \n" + " \n" + " 0\n" + " 0\n" + " 90\n" + " 27\n" + " \n" + " \n" + " \n" + "\n" ) + .arg( name() ); +} diff --git a/src/customwidgets/qgsfeaturelistcomboboxplugin.h b/src/customwidgets/qgsfeaturelistcomboboxplugin.h new file mode 100644 index 000000000000..9c6ec7c4fa80 --- /dev/null +++ b/src/customwidgets/qgsfeaturelistcomboboxplugin.h @@ -0,0 +1,51 @@ +/*************************************************************************** + qgsfeaturelistcomboboxplugin.h + -------------------------------------- + Date : 26.03.2020 + Copyright : (C) 2020 Denis Rouzaud + Email : denis.rouzaud@gmail.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 QGSFEATURELISTCOMBOBOXPLUGIN_H +#define QGSFEATURELISTCOMBOBOXPLUGIN_H + + +#include +#include +#include +#include "qgis_customwidgets.h" + + +class CUSTOMWIDGETS_EXPORT QgsFeatureListComboBoxPlugin : public QObject, public QDesignerCustomWidgetInterface +{ + Q_OBJECT + Q_INTERFACES( QDesignerCustomWidgetInterface ) + + public: + explicit QgsFeatureListComboBoxPlugin( QObject *parent = nullptr ); + + private: + bool mInitialized; + + // QDesignerCustomWidgetInterface interface + public: + QString name() const override; + QString group() const override; + QString includeFile() const override; + QIcon icon() const override; + bool isContainer() const override; + QWidget *createWidget( QWidget *parent ) override; + bool isInitialized() const override; + void initialize( QDesignerFormEditorInterface *core ) override; + QString toolTip() const override; + QString whatsThis() const override; + QString domXml() const override; +}; +#endif // QGSFEATURELISTCOMBOBOXPLUGIN_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index f58cdc6e8f51..6271cc1db425 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -3,6 +3,7 @@ SET(QGIS_GUI_SRCS raster/qgsmultibandcolorrendererwidget.cpp raster/qgspalettedrendererwidget.cpp raster/qgsrasterbandcombobox.cpp + raster/qgsrastercontourrendererwidget.cpp raster/qgsrasterhistogramwidget.cpp raster/qgsrasterlayerproperties.cpp raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -112,6 +113,9 @@ SET(QGIS_GUI_SRCS auth/qgsauthsslimportdialog.cpp auth/qgsauthtrustedcasdialog.cpp + devtools/qgsdevtoolwidget.cpp + devtools/qgsdevtoolwidgetfactory.cpp + editorwidgets/core/qgseditorconfigwidget.cpp editorwidgets/core/qgseditorwidgetautoconf.cpp editorwidgets/core/qgseditorwidgetfactory.cpp @@ -219,6 +223,7 @@ SET(QGIS_GUI_SRCS layout/qgslayoutmanualtablewidget.cpp layout/qgslayoutmapgridwidget.cpp layout/qgslayoutmapwidget.cpp + layout/qgslayoutmarkerwidget.cpp layout/qgslayoutmousehandles.cpp layout/qgslayoutnewitempropertiesdialog.cpp layout/qgslayoutpagepropertieswidget.cpp @@ -263,12 +268,17 @@ SET(QGIS_GUI_SRCS processing/qgsprocessingalgorithmconfigurationwidget.cpp processing/qgsprocessingalgorithmdialogbase.cpp processing/qgsprocessingconfigurationwidgets.cpp + processing/qgsprocessingenummodelerwidget.cpp + processing/qgsprocessingfeaturesourceoptionswidget.cpp processing/qgsprocessingguiregistry.cpp processing/qgsprocessingmaplayercombobox.cpp + processing/qgsprocessingmatrixmodelerwidget.cpp processing/qgsprocessingmatrixparameterdialog.cpp processing/qgsprocessingmodelerparameterwidget.cpp processing/qgsprocessingmultipleselectiondialog.cpp + processing/qgsprocessingoutputdestinationwidget.cpp processing/qgsprocessingparameterdefinitionwidget.cpp + processing/qgsprocessingparameterswidget.cpp processing/qgsprocessingrecentalgorithmlog.cpp processing/qgsprocessingtoolboxmodel.cpp processing/qgsprocessingtoolboxtreeview.cpp @@ -282,6 +292,7 @@ SET(QGIS_GUI_SRCS processing/models/qgsmodelgraphicitem.cpp processing/models/qgsmodelgraphicsscene.cpp processing/models/qgsmodelgraphicsview.cpp + processing/models/qgsmodelgroupboxdefinitionwidget.cpp processing/models/qgsmodelsnapper.cpp processing/models/qgsmodelundocommand.cpp processing/models/qgsmodelviewmouseevent.cpp @@ -310,6 +321,11 @@ SET(QGIS_GUI_SRCS tableeditor/qgstableeditorformattingwidget.cpp tableeditor/qgstableeditorwidget.cpp + vectortile/qgsvectortilebasicrendererwidget.cpp + vectortile/qgsvectortileconnectiondialog.cpp + vectortile/qgsvectortiledataitemguiprovider.cpp + vectortile/qgsvectortileproviderguimetadata.cpp + qgisinterface.cpp qgsactionmenu.cpp qgsaddattrdialog.cpp @@ -355,6 +371,7 @@ SET(QGIS_GUI_SRCS qgscoordinateoperationwidget.cpp qgscredentialdialog.cpp qgscustomdrophandler.cpp + qgscustomprojectopenhandler.cpp qgscurveeditorwidget.cpp qgsdatabaseschemacombobox.cpp qgsdatabasetablecombobox.cpp @@ -362,7 +379,6 @@ SET(QGIS_GUI_SRCS qgsdataitemguiproviderregistry.cpp qgsdatumtransformdialog.cpp qgsdatasourceselectdialog.cpp - qgsnewdatabasetablenamewidget.cpp qgsdetaileditemdata.cpp qgsdetaileditemdelegate.cpp qgsdetaileditemwidget.cpp @@ -377,11 +393,15 @@ SET(QGIS_GUI_SRCS qgsexpressionlineedit.cpp qgsexpressionselectiondialog.cpp qgsexpressionstoredialog.cpp + qgsexpressiontreeview.cpp qgsextentgroupbox.cpp + qgsextentwidget.cpp qgsexternalresourcewidget.cpp qgsfeatureselectiondlg.cpp qgsfieldcombobox.cpp qgsfieldexpressionwidget.cpp + qgsfieldmappingwidget.cpp + qgsfieldmappingmodel.cpp qgsfeaturelistcombobox.cpp qgsfieldvalidator.cpp qgsfieldvalueslineedit.cpp @@ -402,6 +422,7 @@ SET(QGIS_GUI_SRCS qgsguiutils.cpp qgshighlight.cpp qgshighlightablecombobox.cpp + qgshighlightablelineedit.cpp qgshistogramwidget.cpp qgshelp.cpp qgsidentifymenu.cpp @@ -450,6 +471,7 @@ SET(QGIS_GUI_SRCS qgsmasksourceselectionwidget.cpp qgsnewauxiliarylayerdialog.cpp qgsnewauxiliaryfielddialog.cpp + qgsnewdatabasetablenamewidget.cpp qgsnewhttpconnection.cpp qgsnewmemorylayerdialog.cpp qgsnewnamedialog.cpp @@ -505,9 +527,8 @@ SET(QGIS_GUI_SRCS qgstabwidget.cpp qgstablewidgetitem.cpp qgstaskmanagerwidget.cpp - qgstemporalmapsettingsdialog.cpp qgstemporalmapsettingswidget.cpp - qgstemporalcontrollerdockwidget.cpp + qgstemporalcontrollerwidget.cpp qgstextformatwidget.cpp qgstextpreview.cpp qgstreewidgetitem.cpp @@ -573,6 +594,7 @@ SET(QGIS_GUI_HDRS qgscredentialdialog.h qgscurveeditorwidget.h qgscustomdrophandler.h + qgscustomprojectopenhandler.h qgsdatabaseschemacombobox.h qgsdatabasetablecombobox.h qgsdataitemguiprovider.h @@ -595,12 +617,16 @@ SET(QGIS_GUI_HDRS qgsexpressionhighlighter.h qgsexpressionlineedit.h qgsexpressionselectiondialog.h + qgsexpressiontreeview.h qgsextentgroupbox.h + qgsextentwidget.h qgsexternalresourcewidget.h qgsfeaturelistcombobox.h qgsfeatureselectiondlg.h qgsfieldcombobox.h qgsfieldexpressionwidget.h + qgsfieldmappingwidget.h + qgsfieldmappingmodel.h qgsfieldvalidator.h qgsfieldvalueslineedit.h qgsfilecontentsourcelineedit.h @@ -622,6 +648,7 @@ SET(QGIS_GUI_HDRS qgshelp.h qgshighlight.h qgshighlightablecombobox.h + qgshighlightablelineedit.h qgshistogramwidget.h qgsidentifymenu.h qgsinstallgridshiftdialog.h @@ -729,9 +756,8 @@ SET(QGIS_GUI_HDRS qgstablewidgetitem.h qgstabwidget.h qgstaskmanagerwidget.h - qgstemporalmapsettingsdialog.h qgstemporalmapsettingswidget.h - qgstemporalcontrollerdockwidget.h + qgstemporalcontrollerwidget.h qgstextformatwidget.h qgstextpreview.h qgstreewidgetitem.h @@ -788,6 +814,9 @@ SET(QGIS_GUI_HDRS callouts/qgscalloutwidget.h + devtools/qgsdevtoolwidget.h + devtools/qgsdevtoolwidgetfactory.h + editorwidgets/core/qgseditorconfigwidget.h editorwidgets/core/qgseditorwidgetautoconf.h editorwidgets/core/qgseditorwidgetfactory.h @@ -900,6 +929,7 @@ SET(QGIS_GUI_HDRS layout/qgslayoutmanualtablewidget.h layout/qgslayoutmapgridwidget.h layout/qgslayoutmapwidget.h + layout/qgslayoutmarkerwidget.h layout/qgslayoutmousehandles.h layout/qgslayoutnewitempropertiesdialog.h layout/qgslayoutpagepropertieswidget.h @@ -943,13 +973,18 @@ SET(QGIS_GUI_HDRS processing/qgsprocessingalgorithmconfigurationwidget.h processing/qgsprocessingalgorithmdialogbase.h processing/qgsprocessingconfigurationwidgets.h + processing/qgsprocessingenummodelerwidget.h + processing/qgsprocessingfeaturesourceoptionswidget.h processing/qgsprocessinggui.h processing/qgsprocessingguiregistry.h processing/qgsprocessingmaplayercombobox.h + processing/qgsprocessingmatrixmodelerwidget.h processing/qgsprocessingmatrixparameterdialog.h processing/qgsprocessingmodelerparameterwidget.h processing/qgsprocessingmultipleselectiondialog.h + processing/qgsprocessingoutputdestinationwidget.h processing/qgsprocessingparameterdefinitionwidget.h + processing/qgsprocessingparameterswidget.h processing/qgsprocessingrecentalgorithmlog.h processing/qgsprocessingtoolboxmodel.h processing/qgsprocessingtoolboxtreeview.h @@ -963,6 +998,7 @@ SET(QGIS_GUI_HDRS processing/models/qgsmodelgraphicitem.h processing/models/qgsmodelgraphicsscene.h processing/models/qgsmodelgraphicsview.h + processing/models/qgsmodelgroupboxdefinitionwidget.h processing/models/qgsmodelsnapper.h processing/models/qgsmodelundocommand.h processing/models/qgsmodelviewmouseevent.h @@ -991,6 +1027,7 @@ SET(QGIS_GUI_HDRS raster/qgsmultibandcolorrendererwidget.h raster/qgspalettedrendererwidget.h raster/qgsrasterbandcombobox.h + raster/qgsrastercontourrendererwidget.h raster/qgsrasterhistogramwidget.h raster/qgsrasterminmaxwidget.h raster/qgsrasterrendererwidget.h @@ -1055,6 +1092,11 @@ SET(QGIS_GUI_HDRS tableeditor/qgstableeditorformattingwidget.h tableeditor/qgstableeditorwidget.h + vectortile/qgsvectortilebasicrendererwidget.h + vectortile/qgsvectortileconnectiondialog.h + vectortile/qgsvectortiledataitemguiprovider.h + vectortile/qgsvectortileproviderguimetadata.h + qgsbrowserdockwidget_p.h ) @@ -1155,6 +1197,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/gui/symbology ${CMAKE_SOURCE_DIR}/src/gui/attributetable ${CMAKE_SOURCE_DIR}/src/gui/auth + ${CMAKE_SOURCE_DIR}/src/gui/devtools ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets ${CMAKE_SOURCE_DIR}/src/gui/editorwidgets/core ${CMAKE_SOURCE_DIR}/src/gui/effects @@ -1169,6 +1212,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/gui/providers/ogr ${CMAKE_SOURCE_DIR}/src/gui/raster ${CMAKE_SOURCE_DIR}/src/gui/vector + ${CMAKE_SOURCE_DIR}/src/gui/vectortile ${CMAKE_SOURCE_DIR}/src/gui/tableeditor ${CMAKE_SOURCE_DIR}/src/core ${CMAKE_SOURCE_DIR}/src/core/annotations @@ -1197,6 +1241,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/metadata ${CMAKE_SOURCE_DIR}/src/core/expression ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/native ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/external/nlohmann diff --git a/src/gui/attributeformconfig/qgsattributeformcontaineredit.cpp b/src/gui/attributeformconfig/qgsattributeformcontaineredit.cpp index 596d6b18a1e9..ff878393fc34 100644 --- a/src/gui/attributeformconfig/qgsattributeformcontaineredit.cpp +++ b/src/gui/attributeformconfig/qgsattributeformcontaineredit.cpp @@ -21,12 +21,9 @@ QgsAttributeFormContainerEdit::QgsAttributeFormContainerEdit( QTreeWidgetItem *item, QWidget *parent ) : QWidget( parent ) , mTreeItem( item ) - { setupUi( this ); - - const QgsAttributesFormProperties::DnDTreeItemData itemData = mTreeItem->data( 0, QgsAttributesFormProperties::DnDTreeRole ).value(); Q_ASSERT( itemData.type() == QgsAttributesFormProperties::DnDTreeItemData::Container ); @@ -56,7 +53,7 @@ void QgsAttributeFormContainerEdit::updateItemData() QgsAttributesFormProperties::DnDTreeItemData itemData = mTreeItem->data( 0, QgsAttributesFormProperties::DnDTreeRole ).value(); itemData.setColumnCount( mColumnCountSpinBox->value() ); - itemData.setShowAsGroupBox( mShowAsGroupBoxCheckBox->isVisible() ? mShowAsGroupBoxCheckBox->isChecked() : true ); + itemData.setShowAsGroupBox( mShowAsGroupBoxCheckBox->isVisible() ? mShowAsGroupBoxCheckBox->isChecked() : false ); itemData.setName( mTitleLineEdit->text() ); itemData.setShowLabel( mShowLabelCheckBox->isChecked() ); itemData.setBackgroundColor( mBackgroundColorButton->color() ); diff --git a/src/gui/attributeformconfig/qgsattributetypedialog.cpp b/src/gui/attributeformconfig/qgsattributetypedialog.cpp index 12e9a1e1c7fd..24bfe3e65f0c 100644 --- a/src/gui/attributeformconfig/qgsattributetypedialog.cpp +++ b/src/gui/attributeformconfig/qgsattributetypedialog.cpp @@ -71,6 +71,7 @@ QgsAttributeTypeDialog::QgsAttributeTypeDialog( QgsVectorLayer *vl, int fieldIdx } mExpressionWidget->registerExpressionContextGenerator( this ); + mExpressionWidget->setLayer( mLayer ); connect( mExpressionWidget, &QgsExpressionLineEdit::expressionChanged, this, &QgsAttributeTypeDialog::defaultExpressionChanged ); connect( mUniqueCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) @@ -78,15 +79,16 @@ QgsAttributeTypeDialog::QgsAttributeTypeDialog( QgsVectorLayer *vl, int fieldIdx mCheckBoxEnforceUnique->setEnabled( checked ); if ( !checked ) mCheckBoxEnforceUnique->setChecked( false ); - } - ); + } ); connect( notNullCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) { mCheckBoxEnforceNotNull->setEnabled( checked ); if ( !checked ) mCheckBoxEnforceNotNull->setChecked( false ); - } - ); + } ); + + mWarnDefaultValueHasFieldsWidget->setVisible( false ); + connect( mApplyDefaultValueOnUpdateCheckBox, &QCheckBox::stateChanged, this, &QgsAttributeTypeDialog::defaultExpressionChanged ); constraintExpressionWidget->setAllowEmptyFieldName( true ); constraintExpressionWidget->setLayer( vl ); @@ -357,6 +359,8 @@ void QgsAttributeTypeDialog::setLabelOnTop( bool onTop ) void QgsAttributeTypeDialog::defaultExpressionChanged() { + mWarnDefaultValueHasFieldsWidget->hide(); + QString expression = mExpressionWidget->expression(); if ( expression.isEmpty() ) { @@ -391,6 +395,11 @@ void QgsAttributeTypeDialog::defaultExpressionChanged() return; } + // if the expression uses fields and it's not on update, + // there is no warranty that the field will be available + bool expressionHasFields = exp.referencedAttributeIndexes( mLayer->fields() ).count() > 0; + mWarnDefaultValueHasFieldsWidget->setVisible( expressionHasFields && !mApplyDefaultValueOnUpdateCheckBox->isChecked() ); + QgsFieldFormatter *fieldFormatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( editorWidgetType() ); QString previewText = fieldFormatter->representValue( mLayer, mFieldIdx, editorWidgetConfig(), QVariant(), val ); diff --git a/src/gui/attributetable/qgsattributetablefiltermodel.cpp b/src/gui/attributetable/qgsattributetablefiltermodel.cpp index ea3c02a20fc2..f76fb5b31310 100644 --- a/src/gui/attributetable/qgsattributetablefiltermodel.cpp +++ b/src/gui/attributetable/qgsattributetablefiltermodel.cpp @@ -26,6 +26,7 @@ #include "qgsrenderer.h" #include "qgsvectorlayereditbuffer.h" #include "qgsexpressioncontextutils.h" +#include "qgsapplication.h" ////////////////// // Filter Model // @@ -226,6 +227,12 @@ void QgsAttributeTableFilterModel::setAttributeTableConfig( const QgsAttributeTa sort( config.sortExpression(), config.sortOrder() ); } +void QgsAttributeTableFilterModel::setFilterExpression( const QgsExpression &expression, const QgsExpressionContext &context ) +{ + mFilterExpression = expression; + mFilterExpressionContext = context; +} + void QgsAttributeTableFilterModel::sort( const QString &expression, Qt::SortOrder order ) { if ( order != Qt::AscendingOrder && order != Qt::DescendingOrder ) @@ -313,12 +320,23 @@ void QgsAttributeTableFilterModel::setFilterMode( FilterMode filterMode ) { if ( filterMode == ShowVisible ) { - connect( mCanvas, &QgsMapCanvas::extentsChanged, this, &QgsAttributeTableFilterModel::extentsChanged ); + connect( mCanvas, &QgsMapCanvas::extentsChanged, this, &QgsAttributeTableFilterModel::reloadVisible ); + connect( mTableModel, &QgsAttributeTableModel::dataChanged, this, &QgsAttributeTableFilterModel::reloadVisible ); generateListOfVisibleFeatures(); } else { - disconnect( mCanvas, &QgsMapCanvas::extentsChanged, this, &QgsAttributeTableFilterModel::extentsChanged ); + disconnect( mCanvas, &QgsMapCanvas::extentsChanged, this, &QgsAttributeTableFilterModel::reloadVisible ); + disconnect( mTableModel, &QgsAttributeTableModel::dataChanged, this, &QgsAttributeTableFilterModel::reloadVisible ); + } + + if ( filterMode == ShowFilteredList ) + { + connect( mTableModel, &QgsAttributeTableModel::dataChanged, this, &QgsAttributeTableFilterModel::filterFeatures ); + } + else + { + disconnect( mTableModel, &QgsAttributeTableModel::dataChanged, this, &QgsAttributeTableFilterModel::filterFeatures ); } mFilterMode = filterMode; @@ -372,11 +390,68 @@ bool QgsAttributeTableFilterModel::filterAcceptsRow( int sourceRow, const QModel } void QgsAttributeTableFilterModel::extentsChanged() +{ + reloadVisible(); +} + +void QgsAttributeTableFilterModel::reloadVisible() { generateListOfVisibleFeatures(); invalidateFilter(); } +void QgsAttributeTableFilterModel::filterFeatures() +{ + if ( !mFilterExpression.isValid() ) + return; + + QgsFeatureIds filteredFeatures; + QgsDistanceArea distanceArea; + + distanceArea.setSourceCrs( mTableModel->layer()->crs(), QgsProject::instance()->transformContext() ); + distanceArea.setEllipsoid( QgsProject::instance()->ellipsoid() ); + + const bool fetchGeom = mFilterExpression.needsGeometry(); + + QApplication::setOverrideCursor( Qt::WaitCursor ); + + mFilterExpression.setGeomCalculator( &distanceArea ); + mFilterExpression.setDistanceUnits( QgsProject::instance()->distanceUnits() ); + mFilterExpression.setAreaUnits( QgsProject::instance()->areaUnits() ); + QgsFeatureRequest request( mTableModel->request() ); + request.setSubsetOfAttributes( mFilterExpression.referencedColumns(), mTableModel->layer()->fields() ); + if ( !fetchGeom ) + { + request.setFlags( QgsFeatureRequest::NoGeometry ); + } + else + { + // force geometry extraction if the filter requests it + request.setFlags( request.flags() & ~QgsFeatureRequest::NoGeometry ); + } + QgsFeatureIterator featIt = mTableModel->layer()->getFeatures( request ); + + QgsFeature f; + + while ( featIt.nextFeature( f ) ) + { + mFilterExpressionContext.setFeature( f ); + if ( mFilterExpression.evaluate( &mFilterExpressionContext ).toInt() != 0 ) + filteredFeatures << f.id(); + + // check if there were errors during evaluating + if ( mFilterExpression.hasEvalError() ) + break; + } + + featIt.close(); + + setFilteredFeatures( filteredFeatures ); + + QApplication::restoreOverrideCursor(); +} + + void QgsAttributeTableFilterModel::selectionChanged() { if ( ShowSelected == mFilterMode ) diff --git a/src/gui/attributetable/qgsattributetablefiltermodel.h b/src/gui/attributetable/qgsattributetablefiltermodel.h index a797ba4a74bc..5fa24b664068 100644 --- a/src/gui/attributetable/qgsattributetablefiltermodel.h +++ b/src/gui/attributetable/qgsattributetablefiltermodel.h @@ -221,6 +221,14 @@ class GUI_EXPORT QgsAttributeTableFilterModel: public QSortFilterProxyModel, pub */ void setAttributeTableConfig( const QgsAttributeTableConfig &config ); + /** + * Set the \a expression and the \a context to be stored in case of the features + * need to be filtered again (like on filter or on main model data change). + * + * \since QGIS 3.10.3 + */ + void setFilterExpression( const QgsExpression &expression, const QgsExpressionContext &context ); + signals: /** @@ -257,12 +265,23 @@ class GUI_EXPORT QgsAttributeTableFilterModel: public QSortFilterProxyModel, pub /** * Is called upon every change of the visible extents on the map canvas. * When a change is signalled, the filter is updated and invalidated if needed. + * + * \deprecated since QGIS 3.10.3 - made private as reloadVisible() */ - void extentsChanged(); + Q_DECL_DEPRECATED void extentsChanged(); + + /** + * Updates the filtered features in the filter model. It is called when the data of the + * main table or the filter expression changed. + * + * \since QGIS 3.10.3 + */ + void filterFeatures(); private slots: void selectionChanged(); void onColumnsChanged(); + void reloadVisible(); private: QgsFeatureIds mFilteredFeatures; @@ -273,6 +292,9 @@ class GUI_EXPORT QgsAttributeTableFilterModel: public QSortFilterProxyModel, pub QgsAttributeTableConfig mConfig; QVector mColumnMapping; + QgsExpression mFilterExpression; + QgsExpressionContext mFilterExpressionContext; + int mapColumnToSource( int column ) const; int mapColumnFromSource( int column ) const; diff --git a/src/gui/attributetable/qgsattributetablemodel.cpp b/src/gui/attributetable/qgsattributetablemodel.cpp index 396bfb68b7cc..13413b8c4344 100644 --- a/src/gui/attributetable/qgsattributetablemodel.cpp +++ b/src/gui/attributetable/qgsattributetablemodel.cpp @@ -624,7 +624,7 @@ QVariant QgsAttributeTableModel::headerData( int section, Qt::Orientation orient else { const QgsField field = layer()->fields().at( mAttributes.at( section ) ); - return QgsFieldModel::fieldToolTip( field ); + return QgsFieldModel::fieldToolTipExtended( field, layer() ); } } else diff --git a/src/gui/attributetable/qgsattributetableview.cpp b/src/gui/attributetable/qgsattributetableview.cpp index fb5f62f14eb2..c4b727445b64 100644 --- a/src/gui/attributetable/qgsattributetableview.cpp +++ b/src/gui/attributetable/qgsattributetableview.cpp @@ -39,7 +39,8 @@ QgsAttributeTableView::QgsAttributeTableView( QWidget *parent ) : QTableView( parent ) { - QgsGui::instance()->enableAutoGeometryRestore( this ); + QgsSettings settings; + restoreGeometry( settings.value( QStringLiteral( "BetterAttributeTable/geometry" ) ).toByteArray() ); //verticalHeader()->setDefaultSectionSize( 20 ); horizontalHeader()->setHighlightSections( false ); @@ -55,6 +56,8 @@ QgsAttributeTableView::QgsAttributeTableView( QWidget *parent ) setSortingEnabled( true ); // At this point no data is in the model yet, so actually nothing is sorted. horizontalHeader()->setSortIndicatorShown( false ); // So hide the indicator to avoid confusion. + setHorizontalScrollMode( QAbstractItemView::ScrollPerPixel ); + verticalHeader()->viewport()->installEventFilter( this ); connect( verticalHeader(), &QHeaderView::sectionPressed, this, [ = ]( int row ) { selectRow( row, true ); } ); @@ -283,6 +286,8 @@ QWidget *QgsAttributeTableView::createActionWidget( QgsFeatureId fid ) void QgsAttributeTableView::closeEvent( QCloseEvent *e ) { Q_UNUSED( e ) + QgsSettings settings; + settings.setValue( QStringLiteral( "BetterAttributeTable/geometry" ), QVariant( saveGeometry() ) ); } void QgsAttributeTableView::mousePressEvent( QMouseEvent *event ) diff --git a/src/gui/attributetable/qgsdualview.cpp b/src/gui/attributetable/qgsdualview.cpp index e7e302ede661..4421a4db5ac5 100644 --- a/src/gui/attributetable/qgsdualview.cpp +++ b/src/gui/attributetable/qgsdualview.cpp @@ -52,6 +52,12 @@ QgsDualView::QgsDualView( QWidget *parent ) connect( mFeatureListView, &QgsFeatureListView::aboutToChangeEditSelection, this, &QgsDualView::featureListAboutToChangeEditSelection ); connect( mFeatureListView, &QgsFeatureListView::currentEditSelectionChanged, this, &QgsDualView::featureListCurrentEditSelectionChanged ); connect( mFeatureListView, &QgsFeatureListView::currentEditSelectionProgressChanged, this, &QgsDualView::updateEditSelectionProgress ); + connect( mFeatureListView, &QgsFeatureListView::willShowContextMenu, this, &QgsDualView::widgetWillShowContextMenu ); + + connect( mTableView, &QgsAttributeTableView::willShowContextMenu, this, &QgsDualView::viewWillShowContextMenu ); + mTableView->horizontalHeader()->setContextMenuPolicy( Qt::CustomContextMenu ); + connect( mTableView->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, &QgsDualView::showViewHeaderMenu ); + connect( mTableView, &QgsAttributeTableView::columnResized, this, &QgsDualView::tableColumnResized ); mConditionalFormatWidgetStack->hide(); mConditionalFormatWidget = new QgsFieldConditionalFormatWidget( this ); @@ -118,12 +124,6 @@ void QgsDualView::init( QgsVectorLayer *layer, QgsMapCanvas *mapCanvas, const Qg mLayer = layer; mEditorContext = context; - connect( mTableView, &QgsAttributeTableView::willShowContextMenu, this, &QgsDualView::viewWillShowContextMenu ); - mTableView->horizontalHeader()->setContextMenuPolicy( Qt::CustomContextMenu ); - connect( mTableView->horizontalHeader(), &QHeaderView::customContextMenuRequested, this, &QgsDualView::showViewHeaderMenu ); - connect( mTableView, &QgsAttributeTableView::columnResized, this, &QgsDualView::tableColumnResized ); - connect( mFeatureListView, &QgsFeatureListView::willShowContextMenu, this, &QgsDualView::widgetWillShowContextMenu ); - initLayerCache( !( request.flags() & QgsFeatureRequest::NoGeometry ) || !request.filterRect().isNull() ); initModels( mapCanvas, request, loadFeatures ); @@ -148,8 +148,8 @@ void QgsDualView::init( QgsVectorLayer *layer, QgsMapCanvas *mapCanvas, const Qg connect( mAttributeForm, &QgsAttributeForm::widgetValueChanged, this, &QgsDualView::featureFormAttributeChanged ); connect( mAttributeForm, &QgsAttributeForm::modeChanged, this, &QgsDualView::formModeChanged ); - connect( mMasterModel, &QgsAttributeTableModel::modelChanged, mAttributeForm, &QgsAttributeForm::refreshFeature ); connect( mAttributeForm, &QgsAttributeForm::filterExpressionSet, this, &QgsDualView::filterExpressionSet ); + connect( mMasterModel, &QgsAttributeTableModel::modelChanged, mAttributeForm, &QgsAttributeForm::refreshFeature ); connect( mFilterModel, &QgsAttributeTableFilterModel::sortColumnChanged, this, &QgsDualView::onSortColumnChanged ); if ( mFeatureListPreviewButton->defaultAction() ) @@ -919,11 +919,8 @@ void QgsDualView::modifySort() QgsExpressionBuilderWidget *expressionBuilder = new QgsExpressionBuilderWidget(); QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) ); - expressionBuilder->setExpressionContext( context ); - expressionBuilder->setLayer( mLayer ); - expressionBuilder->loadFieldNames(); - expressionBuilder->loadRecent( QStringLiteral( "generic" ) ); - expressionBuilder->loadUserExpressions( ); + + expressionBuilder->initWithLayer( mLayer, context, QStringLiteral( "generic" ) ); expressionBuilder->setExpressionText( sortExpression().isEmpty() ? mLayer->displayExpression() : sortExpression() ); sortingGroupBox->layout()->addWidget( expressionBuilder ); @@ -1076,6 +1073,13 @@ void QgsDualView::setFilteredFeatures( const QgsFeatureIds &filteredFeatures ) mFilterModel->setFilteredFeatures( filteredFeatures ); } +void QgsDualView::filterFeatures( const QgsExpression &filterExpression, const QgsExpressionContext &context ) +{ + mFilterModel->setFilterExpression( filterExpression, context ); + mFilterModel->filterFeatures(); +} + + void QgsDualView::setRequest( const QgsFeatureRequest &request ) { mMasterModel->setRequest( request ); diff --git a/src/gui/attributetable/qgsdualview.h b/src/gui/attributetable/qgsdualview.h index fb074d6805ef..a9704c3577e7 100644 --- a/src/gui/attributetable/qgsdualview.h +++ b/src/gui/attributetable/qgsdualview.h @@ -157,8 +157,17 @@ class GUI_EXPORT QgsDualView : public QStackedWidget, private Ui::QgsDualViewBas * * \param filteredFeatures A list of feature ids * + * \deprecated since filterFeatures is handled in the attribute filter model itself + */ + Q_DECL_DEPRECATED void setFilteredFeatures( const QgsFeatureIds &filteredFeatures ); + + /** + * Sets the expression and Updates the filtered features in the filter model. + * It is called when the filter expression changed. + * + * \since QGIS 3.10.3 */ - void setFilteredFeatures( const QgsFeatureIds &filteredFeatures ); + void filterFeatures( const QgsExpression &filterExpression, const QgsExpressionContext &context ); /** * Gets a list of currently visible feature ids. diff --git a/src/gui/attributetable/qgsfeaturefilterwidget.cpp b/src/gui/attributetable/qgsfeaturefilterwidget.cpp index 70530e22530c..81d0be3788a0 100644 --- a/src/gui/attributetable/qgsfeaturefilterwidget.cpp +++ b/src/gui/attributetable/qgsfeaturefilterwidget.cpp @@ -446,12 +446,6 @@ void QgsFeatureFilterWidget::setFilterExpression( const QString &filterString, Q } } - QgsFeatureIds filteredFeatures; - QgsDistanceArea myDa; - - myDa.setSourceCrs( mLayer->crs(), QgsProject::instance()->transformContext() ); - myDa.setEllipsoid( QgsProject::instance()->ellipsoid() ); - // parse search string and build parsed tree QgsExpression filterExpression( filter ); if ( filterExpression.hasParserError() ) @@ -467,50 +461,8 @@ void QgsFeatureFilterWidget::setFilterExpression( const QString &filterString, Q mMessageBar->pushMessage( tr( "Evaluation error" ), filterExpression.evalErrorString(), Qgis::Warning, mMessageBarTimeout ); } - bool fetchGeom = filterExpression.needsGeometry(); - - QApplication::setOverrideCursor( Qt::WaitCursor ); - - filterExpression.setGeomCalculator( &myDa ); - filterExpression.setDistanceUnits( QgsProject::instance()->distanceUnits() ); - filterExpression.setAreaUnits( QgsProject::instance()->areaUnits() ); - QgsFeatureRequest request( mMainView->masterModel()->request() ); - request.setSubsetOfAttributes( filterExpression.referencedColumns(), mLayer->fields() ); - if ( !fetchGeom ) - { - request.setFlags( QgsFeatureRequest::NoGeometry ); - } - else - { - // force geometry extraction if the filter requests it - request.setFlags( request.flags() & ~QgsFeatureRequest::NoGeometry ); - } - QgsFeatureIterator featIt = mLayer->getFeatures( request ); + mMainView->filterFeatures( filterExpression, context ); - QgsFeature f; - - while ( featIt.nextFeature( f ) ) - { - context.setFeature( f ); - if ( filterExpression.evaluate( &context ).toInt() != 0 ) - filteredFeatures << f.id(); - - // check if there were errors during evaluating - if ( filterExpression.hasEvalError() ) - break; - } - - featIt.close(); - - mMainView->setFilteredFeatures( filteredFeatures ); - - QApplication::restoreOverrideCursor(); - - if ( filterExpression.hasEvalError() ) - { - mMessageBar->pushMessage( tr( "Error filtering" ), filterExpression.evalErrorString(), Qgis::Warning, mMessageBarTimeout ); - return; - } mMainView->setFilterMode( QgsAttributeTableFilterModel::ShowFilteredList ); } diff --git a/src/gui/attributetable/qgsfeaturelistview.cpp b/src/gui/attributetable/qgsfeaturelistview.cpp index b568c2b229b6..8c8cbfc6db57 100644 --- a/src/gui/attributetable/qgsfeaturelistview.cpp +++ b/src/gui/attributetable/qgsfeaturelistview.cpp @@ -162,7 +162,11 @@ void QgsFeatureListView::editSelectionChanged( const QItemSelection &deselected, QItemSelection localSelected = mModel->mapSelectionFromMaster( selected ); viewport()->update( visualRegionForSelection( localDeselected ) | visualRegionForSelection( localSelected ) ); } + updateEditSelectionDependencies(); +} +void QgsFeatureListView::updateEditSelectionDependencies() +{ QItemSelection currentSelection = mCurrentEditSelectionModel->selection(); if ( currentSelection.size() == 1 ) { @@ -463,6 +467,7 @@ void QgsFeatureListView::ensureEditSelection( bool inSelection ) } mUpdateEditSelectionTimer.start(); } + updateEditSelectionDependencies(); } void QgsFeatureListView::setFeatureSelectionManager( QgsIFeatureSelectionManager *featureSelectionManager ) diff --git a/src/gui/attributetable/qgsfeaturelistview.h b/src/gui/attributetable/qgsfeaturelistview.h index 575fb1355695..2bf6b09e0b66 100644 --- a/src/gui/attributetable/qgsfeaturelistview.h +++ b/src/gui/attributetable/qgsfeaturelistview.h @@ -214,6 +214,11 @@ class GUI_EXPORT QgsFeatureListView : public QListView private slots: void editSelectionChanged( const QItemSelection &deselected, const QItemSelection &selected ); + /** + * Emits the signal for the feature and the selection information + */ + void updateEditSelectionDependencies(); + /** * Make sure, there is an edit selection. If there is none, choose the first item. * If \a inSelection is set to TRUE, the edit selection is done in selected entries if diff --git a/src/gui/devtools/qgsdevtoolwidget.cpp b/src/gui/devtools/qgsdevtoolwidget.cpp new file mode 100644 index 000000000000..e9000d107466 --- /dev/null +++ b/src/gui/devtools/qgsdevtoolwidget.cpp @@ -0,0 +1,21 @@ +/*************************************************************************** + qgsdevtoolwidget.cpp + -------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 "qgsdevtoolwidget.h" + +QgsDevToolWidget::QgsDevToolWidget( QWidget *parent ) + : QgsPanelWidget( parent ) +{ + +} diff --git a/src/gui/devtools/qgsdevtoolwidget.h b/src/gui/devtools/qgsdevtoolwidget.h new file mode 100644 index 000000000000..39ba7ab0d07d --- /dev/null +++ b/src/gui/devtools/qgsdevtoolwidget.h @@ -0,0 +1,40 @@ +/*************************************************************************** + qgsdevtoolwidget.h + ------------------ + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 QGSDEVTOOLWIDGET_H +#define QGSDEVTOOLWIDGET_H + +#include "qgspanelwidget.h" +#include "qgis_sip.h" +#include "qgis_gui.h" + +/** + * \ingroup gui + * \class QgsDevToolWidget + * \brief A panel widget that can be shown in the developer tools panel. + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsDevToolWidget : public QgsPanelWidget +{ + Q_OBJECT + public: + + /** + * Constructor for QgsDevToolWidget, with the specified \a parent widget. + */ + QgsDevToolWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + +}; + +#endif // QGSDEVTOOLWIDGET_H diff --git a/src/gui/devtools/qgsdevtoolwidgetfactory.cpp b/src/gui/devtools/qgsdevtoolwidgetfactory.cpp new file mode 100644 index 000000000000..34bd98845773 --- /dev/null +++ b/src/gui/devtools/qgsdevtoolwidgetfactory.cpp @@ -0,0 +1,22 @@ +/*************************************************************************** + qgsdevtoolwidgetfactory.cpp + -------------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 "qgsdevtoolwidgetfactory.h" + +QgsDevToolWidgetFactory::QgsDevToolWidgetFactory( const QString &title, const QIcon &icon ) + : mIcon( icon ) + , mTitle( title ) +{ +} diff --git a/src/gui/devtools/qgsdevtoolwidgetfactory.h b/src/gui/devtools/qgsdevtoolwidgetfactory.h new file mode 100644 index 000000000000..5059793e5843 --- /dev/null +++ b/src/gui/devtools/qgsdevtoolwidgetfactory.h @@ -0,0 +1,79 @@ +/*************************************************************************** + qgsdevtoolwidgetfactory.h + ------------------------ + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 QGSDEVTOOLWIDGETFACTORY_H +#define QGSDEVTOOLWIDGETFACTORY_H + +#include "qgis_gui.h" +#include "qgis_sip.h" +#include +#include + +class QgsDevToolWidget; +class QWidget; + +/** + * \ingroup gui + * \class QgsDevToolWidgetFactory + * Factory class for creating custom developer/debugging tool pages + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsDevToolWidgetFactory +{ + public: + + /** + * Constructor for a QgsDevToolWidgetFactory with the specified \a title and \a icon. + */ + QgsDevToolWidgetFactory( const QString &title = QString(), const QIcon &icon = QIcon() ); + + virtual ~QgsDevToolWidgetFactory() = default; + + /** + * Returns the icon that will be shown in the tool in the panel. + * \see setIcon() + */ + virtual QIcon icon() const { return mIcon; } + + /** + * Sets the \a icon for the factory object, which will be shown for the tool in the panel. + * \see icon() + */ + void setIcon( const QIcon &icon ) { mIcon = icon; } + + /** + * Returns the (translated) title of the tool. + * \see setTitle() + */ + virtual QString title() const { return mTitle; } + + /** + * Set the translated \a title for the tool. + */ + void setTitle( const QString &title ) { mTitle = title; } + + /** + * Factory function to create the widget on demand as needed by the dock. + * + * The \a parent argument gives the correct parent for the newly created widget. + */ + virtual QgsDevToolWidget *createWidget( QWidget *parent = nullptr ) const = 0 SIP_FACTORY; + + private: + QIcon mIcon; + QString mTitle; +}; + +#endif // QGSDEVTOOLWIDGETFACTORY_H diff --git a/src/gui/editorwidgets/qgsrelationreferencewidget.cpp b/src/gui/editorwidgets/qgsrelationreferencewidget.cpp index 6c4e61dccc21..f87b06bf0f55 100644 --- a/src/gui/editorwidgets/qgsrelationreferencewidget.cpp +++ b/src/gui/editorwidgets/qgsrelationreferencewidget.cpp @@ -156,6 +156,7 @@ QgsRelationReferenceWidget::QgsRelationReferenceWidget( QWidget *parent ) mHighlightFeatureButton->hide(); mAttributeEditorFrame->hide(); mInvalidLabel->hide(); + mAddEntryButton->hide(); // connect buttons connect( mOpenFormButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::openForm ); @@ -164,7 +165,6 @@ QgsRelationReferenceWidget::QgsRelationReferenceWidget( QWidget *parent ) connect( mRemoveFKButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::deleteForeignKeys ); connect( mAddEntryButton, &QAbstractButton::clicked, this, &QgsRelationReferenceWidget::addEntry ); connect( mComboBox, &QComboBox::editTextChanged, this, &QgsRelationReferenceWidget::updateAddEntryButton ); - connect( mComboBox, &QgsFeatureListComboBox::modelUpdated, this, &QgsRelationReferenceWidget::updateIndex ); } QgsRelationReferenceWidget::~QgsRelationReferenceWidget() @@ -173,38 +173,6 @@ QgsRelationReferenceWidget::~QgsRelationReferenceWidget() unsetMapTool(); } -void QgsRelationReferenceWidget::updateIndex() -{ - if ( mChainFilters && mComboBox->count() > 0 ) - { - int index = -1; - - // uninitialized filter - if ( ! mFilterComboBoxes.isEmpty() - && mFilterComboBoxes[0]->currentIndex() == 0 && mAllowNull ) - { - index = mComboBox->nullIndex(); - } - else if ( mComboBox->count() > mComboBox->nullIndex() ) - { - index = mComboBox->nullIndex() + 1; - } - else if ( mAllowNull ) - { - index = mComboBox->nullIndex(); - } - else - { - index = 0; - } - - if ( mComboBox->count() > index ) - { - mComboBox->setCurrentIndex( index ); - } - } -} - void QgsRelationReferenceWidget::setRelation( const QgsRelation &relation, bool allowNullValue ) { mAllowNull = allowNullValue; @@ -462,8 +430,12 @@ void QgsRelationReferenceWidget::setEditorContext( const QgsAttributeEditorConte mMapToolIdentify.reset( new QgsMapToolIdentifyFeature( mCanvas ) ); mMapToolIdentify->setButton( mMapIdentificationButton ); - mMapToolDigitize.reset( new QgsMapToolDigitizeFeature( mCanvas, context.cadDockWidget() ) ); - mMapToolDigitize->setButton( mAddEntryButton ); + if ( mEditorContext.cadDockWidget() ) + { + mMapToolDigitize.reset( new QgsMapToolDigitizeFeature( mCanvas, mEditorContext.cadDockWidget() ) ); + mMapToolDigitize->setButton( mAddEntryButton ); + updateAddEntryButton(); + } } void QgsRelationReferenceWidget::setEmbedForm( bool display ) @@ -615,8 +587,6 @@ void QgsRelationReferenceWidget::init() // Only connect after iterating, to have only one iterator on the referenced table at once connect( mComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsRelationReferenceWidget::comboReferenceChanged ); - //call it for the first time - emit mComboBox->currentIndexChanged( mComboBox->currentIndex() ); QApplication::restoreOverrideCursor(); @@ -681,7 +651,7 @@ void QgsRelationReferenceWidget::highlightFeature( QgsFeature f, CanvasExtent ca { extent.combineExtentWith( featBBox ); extent.scale( 1.1 ); - mCanvas->setExtent( extent ); + mCanvas->setExtent( extent, true ); mCanvas->refresh(); } } @@ -1041,7 +1011,7 @@ void QgsRelationReferenceWidget::entryAdded( const QgsFeature &feat ) void QgsRelationReferenceWidget::updateAddEntryButton() { - mAddEntryButton->setVisible( mAllowAddFeatures ); + mAddEntryButton->setVisible( mAllowAddFeatures && mMapToolDigitize ); mAddEntryButton->setEnabled( mReferencedLayer && mReferencedLayer->isEditable() ); } diff --git a/src/gui/editorwidgets/qgsrelationreferencewidget.h b/src/gui/editorwidgets/qgsrelationreferencewidget.h index 33f18ad357c7..e68f8329084e 100644 --- a/src/gui/editorwidgets/qgsrelationreferencewidget.h +++ b/src/gui/editorwidgets/qgsrelationreferencewidget.h @@ -112,6 +112,11 @@ class GUI_EXPORT QgsRelationReferenceWidget : public QWidget */ QVariantList foreignKeys() const; + /** + * Sets the editor \a context + * \note if context cadDockWidget is null, it won't be possible to digitize + * the geometry of a referenced feature from this widget + */ void setEditorContext( const QgsAttributeEditorContext &context, QgsMapCanvas *canvas, QgsMessageBar *messageBar ); //! determines if the form of the related feature will be shown @@ -282,12 +287,6 @@ class GUI_EXPORT QgsRelationReferenceWidget : public QWidget void entryAdded( const QgsFeature &f ); void onKeyPressed( QKeyEvent *e ); - /** - * Updates the FK index as soon as the underlying model is updated when - * the chainFilter option is activated. - */ - void updateIndex(); - private: void highlightFeature( QgsFeature f = QgsFeature(), CanvasExtent canvasExtent = Fixed ); void updateAttributeEditorFrame( const QgsFeature &feature ); diff --git a/src/gui/editorwidgets/qgsrelationreferencewidgetwrapper.cpp b/src/gui/editorwidgets/qgsrelationreferencewidgetwrapper.cpp index d94e3616f3e3..fcd2b579e379 100644 --- a/src/gui/editorwidgets/qgsrelationreferencewidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsrelationreferencewidgetwrapper.cpp @@ -76,7 +76,7 @@ void QgsRelationReferenceWidgetWrapper::initWidget( QWidget *editor ) QgsRelation relation; // invalid relation by default if ( relationName.isValid() ) relation = QgsProject::instance()->relationManager()->relation( relationName.toString() ); - else if ( ! layer()->referencingRelations( fieldIdx() ).isEmpty() ) + if ( !relation.isValid() && !layer()->referencingRelations( fieldIdx() ).isEmpty() ) relation = layer()->referencingRelations( fieldIdx() )[0]; // If this widget is already embedded by the same relation, reduce functionality @@ -88,6 +88,7 @@ void QgsRelationReferenceWidgetWrapper::initWidget( QWidget *editor ) mWidget->setReadOnlySelector( true ); mWidget->setAllowMapIdentification( false ); mWidget->setOpenFormButtonVisible( false ); + mWidget->setAllowAddFeatures( false ); break; } ctx = ctx->parentContext(); diff --git a/src/gui/editorwidgets/qgsrelationwidgetwrapper.cpp b/src/gui/editorwidgets/qgsrelationwidgetwrapper.cpp index b9e49bfe1be4..6dd2216d96a1 100644 --- a/src/gui/editorwidgets/qgsrelationwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsrelationwidgetwrapper.cpp @@ -90,7 +90,7 @@ void QgsRelationWidgetWrapper::widgetValueChanged( const QString &attribute, con if ( feature.attribute( attribute ) != newValue ) { feature.setAttribute( attribute, newValue ); - QgsAttributeEditorContext newContext { context() }; + QgsAttributeEditorContext newContext { mWidget->editorContext() }; newContext.setParentFormFeature( feature ); mWidget->setEditorContext( newContext ); mWidget->setFeature( feature, false ); diff --git a/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp b/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp index a8520657daac..98d9d3f303d9 100644 --- a/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationconfigdlg.cpp @@ -27,8 +27,10 @@ QgsValueRelationConfigDlg::QgsValueRelationConfigDlg( QgsVectorLayer *vl, int fi mLayerName->setFilters( QgsMapLayerProxyModel::VectorLayer ); mKeyColumn->setLayer( mLayerName->currentLayer() ); mValueColumn->setLayer( mLayerName->currentLayer() ); + mDescriptionExpression->setLayer( mLayerName->currentLayer() ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mKeyColumn, &QgsFieldComboBox::setLayer ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mValueColumn, &QgsFieldComboBox::setLayer ); + connect( mLayerName, &QgsMapLayerComboBox::layerChanged, mDescriptionExpression, &QgsFieldExpressionWidget::setLayer ); connect( mLayerName, &QgsMapLayerComboBox::layerChanged, this, &QgsValueRelationConfigDlg::layerChanged ); connect( mEditExpression, &QAbstractButton::clicked, this, &QgsValueRelationConfigDlg::editExpression ); @@ -39,6 +41,7 @@ QgsValueRelationConfigDlg::QgsValueRelationConfigDlg( QgsVectorLayer *vl, int fi connect( mLayerName, &QgsMapLayerComboBox::layerChanged, this, &QgsEditorConfigWidget::changed ); connect( mKeyColumn, static_cast( &QComboBox::currentIndexChanged ), this, &QgsEditorConfigWidget::changed ); connect( mValueColumn, static_cast( &QComboBox::currentIndexChanged ), this, &QgsEditorConfigWidget::changed ); + connect( mDescriptionExpression, static_cast( &QgsFieldExpressionWidget::fieldChanged ), this, &QgsEditorConfigWidget::changed ); connect( mAllowMulti, &QAbstractButton::toggled, this, &QgsEditorConfigWidget::changed ); connect( mAllowNull, &QAbstractButton::toggled, this, &QgsEditorConfigWidget::changed ); connect( mOrderByValue, &QAbstractButton::toggled, this, &QgsEditorConfigWidget::changed ); @@ -68,6 +71,7 @@ QVariantMap QgsValueRelationConfigDlg::config() QString() ); cfg.insert( QStringLiteral( "Key" ), mKeyColumn->currentField() ); cfg.insert( QStringLiteral( "Value" ), mValueColumn->currentField() ); + cfg.insert( QStringLiteral( "Description" ), mDescriptionExpression->expression() ); cfg.insert( QStringLiteral( "AllowMulti" ), mAllowMulti->isChecked() ); cfg.insert( QStringLiteral( "NofColumns" ), mNofColumns->value() ); cfg.insert( QStringLiteral( "AllowNull" ), mAllowNull->isChecked() ); @@ -84,6 +88,7 @@ void QgsValueRelationConfigDlg::setConfig( const QVariantMap &config ) mLayerName->setLayer( lyr ); mKeyColumn->setField( config.value( QStringLiteral( "Key" ) ).toString() ); mValueColumn->setField( config.value( QStringLiteral( "Value" ) ).toString() ); + mDescriptionExpression->setField( config.value( QStringLiteral( "Description" ) ).toString() ); mAllowMulti->setChecked( config.value( QStringLiteral( "AllowMulti" ) ).toBool() ); mNofColumns->setValue( config.value( QStringLiteral( "NofColumns" ), 1 ).toInt() ); if ( !mAllowMulti->isChecked() ) diff --git a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp index 84e96a2f7339..df281f841d8c 100644 --- a/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp +++ b/src/gui/editorwidgets/qgsvaluerelationwidgetwrapper.cpp @@ -342,16 +342,16 @@ void QgsValueRelationWidgetWrapper::populate( ) { if ( context().parentFormFeature().isValid() ) { - mCache = QgsValueRelationFieldFormatter::createCache( config( ), formFeature(), context().parentFormFeature() ); + mCache = QgsValueRelationFieldFormatter::createCache( config(), formFeature(), context().parentFormFeature() ); } else { - mCache = QgsValueRelationFieldFormatter::createCache( config( ), formFeature() ); + mCache = QgsValueRelationFieldFormatter::createCache( config(), formFeature() ); } } else if ( mCache.empty() ) { - mCache = QgsValueRelationFieldFormatter::createCache( config( ) ); + mCache = QgsValueRelationFieldFormatter::createCache( config() ); } if ( mComboBox ) @@ -365,6 +365,8 @@ void QgsValueRelationWidgetWrapper::populate( ) for ( const QgsValueRelationFieldFormatter::ValueRelationItem &element : qgis::as_const( mCache ) ) { whileBlocking( mComboBox )->addItem( element.value, element.key ); + if ( !element.description.isEmpty() ) + mComboBox->setItemData( mComboBox->count() - 1, element.description, Qt::ToolTipRole ); } } diff --git a/src/gui/labeling/qgslabelengineconfigdialog.cpp b/src/gui/labeling/qgslabelengineconfigdialog.cpp index d33a19be9feb..b9d15e00eb6a 100644 --- a/src/gui/labeling/qgslabelengineconfigdialog.cpp +++ b/src/gui/labeling/qgslabelengineconfigdialog.cpp @@ -28,7 +28,7 @@ #include #include -QgsLabelEngineConfigWidget::QgsLabelEngineConfigWidget( QgsMapCanvas* canvas, QWidget *parent ) +QgsLabelEngineConfigWidget::QgsLabelEngineConfigWidget( QgsMapCanvas *canvas, QWidget *parent ) : QgsPanelWidget( parent ), mCanvas( canvas ) { setupUi( this ); @@ -149,7 +149,7 @@ void QgsLabelEngineConfigWidget::showHelp() // QgsLabelEngineConfigDialog // -QgsLabelEngineConfigDialog::QgsLabelEngineConfigDialog( QgsMapCanvas* canvas, QWidget *parent ) +QgsLabelEngineConfigDialog::QgsLabelEngineConfigDialog( QgsMapCanvas *canvas, QWidget *parent ) : QDialog( parent ) { mWidget = new QgsLabelEngineConfigWidget( canvas ); diff --git a/src/gui/labeling/qgslabelinggui.cpp b/src/gui/labeling/qgslabelinggui.cpp index 91422cc38bf7..b23d74aa7988 100644 --- a/src/gui/labeling/qgslabelinggui.cpp +++ b/src/gui/labeling/qgslabelinggui.cpp @@ -186,6 +186,7 @@ QgsLabelingGui::QgsLabelingGui( QgsVectorLayer *layer, QgsMapCanvas *mapCanvas, connect( mBufferDrawChkBx, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); connect( mEnableMaskChkBx, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); connect( mShapeDrawChkBx, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); + connect( mCalloutsDrawCheckBox, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); connect( mShadowDrawChkBx, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); connect( mDirectSymbChkBx, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); connect( mFormatNumChkBx, &QAbstractButton::toggled, this, &QgsLabelingGui::updateUi ); @@ -626,6 +627,7 @@ void QgsLabelingGui::updateUi() syncDefinedCheckboxFrame( mEnableMaskDDBtn, mEnableMaskChkBx, mMaskFrame ); syncDefinedCheckboxFrame( mShapeDrawDDBtn, mShapeDrawChkBx, mShapeFrame ); syncDefinedCheckboxFrame( mShadowDrawDDBtn, mShadowDrawChkBx, mShadowFrame ); + syncDefinedCheckboxFrame( mCalloutDrawDDBtn, mCalloutsDrawCheckBox, mCalloutFrame ); syncDefinedCheckboxFrame( mDirectSymbDDBtn, mDirectSymbChkBx, mDirectSymbFrame ); syncDefinedCheckboxFrame( mFormatNumDDBtn, mFormatNumChkBx, mFormatNumFrame ); diff --git a/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp b/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp index f4c0f754fde6..08b73551505a 100644 --- a/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp +++ b/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp @@ -73,6 +73,7 @@ QgsLayerTreeOpacityWidget::QgsLayerTreeOpacityWidget( QgsMapLayer *layer ) case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } @@ -112,6 +113,7 @@ void QgsLayerTreeOpacityWidget::updateOpacityFromSlider() case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } @@ -152,6 +154,7 @@ bool QgsLayerTreeOpacityWidget::Provider::supportsLayer( QgsMapLayer *layer ) return true; case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: return false; } diff --git a/src/gui/layertree/qgslayertreeview.cpp b/src/gui/layertree/qgslayertreeview.cpp index 7fe243af10da..9e9954ff71b8 100644 --- a/src/gui/layertree/qgslayertreeview.cpp +++ b/src/gui/layertree/qgslayertreeview.cpp @@ -507,6 +507,23 @@ void QgsLayerTreeView::mouseReleaseEvent( QMouseEvent *event ) void QgsLayerTreeView::keyPressEvent( QKeyEvent *event ) { + if ( event->key() == Qt::Key_Space ) + { + const auto constSelectedNodes = selectedNodes(); + + if ( ! constSelectedNodes.isEmpty() ) + { + bool isFirstNodeChecked = constSelectedNodes[0]->itemVisibilityChecked(); + for ( QgsLayerTreeNode *node : constSelectedNodes ) + { + node->setItemVisibilityChecked( ! isFirstNodeChecked ); + } + + // if we call the original keyPress handler, the current item will be checked to the original state yet again + return; + } + } + const QgsLayerTreeModel::Flags oldFlags = layerTreeModel()->flags(); if ( event->modifiers() & Qt::ControlModifier ) layerTreeModel()->setFlags( oldFlags | QgsLayerTreeModel::ActionHierarchical ); diff --git a/src/gui/layertree/qgslayertreeviewdefaultactions.cpp b/src/gui/layertree/qgslayertreeviewdefaultactions.cpp index 83f8ae851592..26963ea338cb 100644 --- a/src/gui/layertree/qgslayertreeviewdefaultactions.cpp +++ b/src/gui/layertree/qgslayertreeviewdefaultactions.cpp @@ -138,6 +138,13 @@ QAction *QgsLayerTreeViewDefaultActions::actionMoveToTop( QObject *parent ) return a; } +QAction *QgsLayerTreeViewDefaultActions::actionMoveToBottom( QObject *parent ) +{ + QAction *a = new QAction( tr( "Move to &Bottom" ), parent ); + connect( a, &QAction::triggered, this, &QgsLayerTreeViewDefaultActions::moveToBottom ); + return a; +} + QAction *QgsLayerTreeViewDefaultActions::actionGroupSelected( QObject *parent ) { QAction *a = new QAction( tr( "&Group Selected" ), parent ); @@ -222,6 +229,11 @@ void QgsLayerTreeViewDefaultActions::checkAndAllParents() void QgsLayerTreeViewDefaultActions::addGroup() { + if ( mView->selectedNodes( true ).count() >= 2 ) + { + groupSelected(); + return; + } QgsLayerTreeGroup *group = mView->currentGroupNode(); if ( !group ) group = mView->layerTreeModel()->rootGroup(); @@ -369,7 +381,7 @@ void QgsLayerTreeViewDefaultActions::zoomToLayers( QgsMapCanvas *canvas, const Q extent.scale( 1.05 ); //zoom to bounding box - canvas->setExtent( extent ); + canvas->setExtent( extent, true ); canvas->refresh(); } @@ -424,27 +436,41 @@ void QgsLayerTreeViewDefaultActions::moveOutOfGroup() void QgsLayerTreeViewDefaultActions::moveToTop() { - QHash groupInsertIdx; - int insertIdx; - const QList< QgsLayerTreeNode * > selectedNodes = mView->selectedNodes(); - for ( QgsLayerTreeNode *n : selectedNodes ) + QList< QgsLayerTreeNode * > selectedNodes = mView->selectedNodes(); + std::reverse( selectedNodes.begin(), selectedNodes.end() ); + // sort the nodes by depth first to avoid moving a group before its contents + std::stable_sort( selectedNodes.begin(), selectedNodes.end(), []( const QgsLayerTreeNode * a, const QgsLayerTreeNode * b ) + { + return a->depth() > b->depth(); + } ); + for ( QgsLayerTreeNode *n : qgis::as_const( selectedNodes ) ) { QgsLayerTreeGroup *parentGroup = qobject_cast( n->parent() ); QgsLayerTreeNode *clonedNode = n->clone(); - if ( groupInsertIdx.contains( parentGroup ) ) - { - insertIdx = groupInsertIdx.value( parentGroup ); - } - else - { - insertIdx = 0; - } - parentGroup->insertChildNode( insertIdx, clonedNode ); + parentGroup->insertChildNode( 0, clonedNode ); parentGroup->removeChildNode( n ); - groupInsertIdx.insert( parentGroup, insertIdx + 1 ); } } + +void QgsLayerTreeViewDefaultActions::moveToBottom() +{ + QList< QgsLayerTreeNode * > selectedNodes = mView->selectedNodes(); + // sort the nodes by depth first to avoid moving a group before its contents + std::stable_sort( selectedNodes.begin(), selectedNodes.end(), []( const QgsLayerTreeNode * a, const QgsLayerTreeNode * b ) + { + return a->depth() > b->depth(); + } ); + for ( QgsLayerTreeNode *n : qgis::as_const( selectedNodes ) ) + { + QgsLayerTreeGroup *parentGroup = qobject_cast( n->parent() ); + QgsLayerTreeNode *clonedNode = n->clone(); + parentGroup->insertChildNode( -1, clonedNode ); + parentGroup->removeChildNode( n ); + } +} + + void QgsLayerTreeViewDefaultActions::groupSelected() { const QList nodes = mView->selectedNodes( true ); diff --git a/src/gui/layertree/qgslayertreeviewdefaultactions.h b/src/gui/layertree/qgslayertreeviewdefaultactions.h index fd8f7b57e661..fbce7b37ec6c 100644 --- a/src/gui/layertree/qgslayertreeviewdefaultactions.h +++ b/src/gui/layertree/qgslayertreeviewdefaultactions.h @@ -82,6 +82,12 @@ class GUI_EXPORT QgsLayerTreeViewDefaultActions : public QObject * \since QGIS 3.2 */ QAction *actionMoveToTop( QObject *parent = nullptr ) SIP_FACTORY; + + /** + * \see moveToBottom() + * \since QGIS 3.14 + */ + QAction *actionMoveToBottom( QObject *parent = nullptr ) SIP_FACTORY; QAction *actionGroupSelected( QObject *parent = nullptr ) SIP_FACTORY; /** @@ -133,6 +139,13 @@ class GUI_EXPORT QgsLayerTreeViewDefaultActions : public QObject * \since QGIS 3.2 */ void moveToTop(); + + /** + * Moves selected layer(s) and/or group(s) to the bottom of the layer panel + * or the bottom of the group if the layer/group is placed within a group. + * \since QGIS 3.14 + */ + void moveToBottom(); void groupSelected(); /** diff --git a/src/gui/layout/qgslayoutguiutils.cpp b/src/gui/layout/qgslayoutguiutils.cpp index 0c7b1843c6c8..aa913cee7021 100644 --- a/src/gui/layout/qgslayoutguiutils.cpp +++ b/src/gui/layout/qgslayoutguiutils.cpp @@ -22,9 +22,11 @@ #include "qgslayoutitemshape.h" #include "qgslayoutmapwidget.h" #include "qgslayoutshapewidget.h" +#include "qgslayoutmarkerwidget.h" #include "qgslayoutitemmap.h" #include "qgslayoutitempolygon.h" #include "qgslayoutitempolyline.h" +#include "qgslayoutitemmarker.h" #include "qgslayoutpolygonwidget.h" #include "qgslayoutpolylinewidget.h" #include "qgslayoutpicturewidget.h" @@ -303,6 +305,13 @@ void QgsLayoutGuiUtils::registerGuiForKnownItemTypes( QgsMapCanvas *mapCanvas ) return shape.release(); } ) ); + // marker + registry->addLayoutItemGuiMetadata( new QgsLayoutItemGuiMetadata( QgsLayoutItemRegistry::LayoutMarker, QObject::tr( "Marker" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddMarker.svg" ) ), + [ = ]( QgsLayoutItem * item )->QgsLayoutItemBaseWidget * + { + return new QgsLayoutMarkerWidget( qobject_cast< QgsLayoutItemMarker * >( item ) ); + }, nullptr ) ); + // arrow std::unique_ptr< QgsLayoutItemGuiMetadata > arrowMetadata = qgis::make_unique< QgsLayoutItemGuiMetadata>( QgsLayoutItemRegistry::LayoutPolyline, QObject::tr( "Arrow" ), QgsApplication::getThemeIcon( QStringLiteral( "/mActionAddArrow.svg" ) ), diff --git a/src/gui/layout/qgslayoutmanualtablewidget.cpp b/src/gui/layout/qgslayoutmanualtablewidget.cpp index e4ddbf79720c..ddbd17fe1ba9 100644 --- a/src/gui/layout/qgslayoutmanualtablewidget.cpp +++ b/src/gui/layout/qgslayoutmanualtablewidget.cpp @@ -188,7 +188,7 @@ void QgsLayoutManualTableWidget::setTableContents() for ( double width : columnWidths ) { mEditorDialog->setTableColumnWidth( col, width ); - headers << mTable->headers().value( col )->heading(); + headers << ( mTable->headers().size() > col ? mTable->headers().value( col )->heading() : QVariant() ); col++; } mEditorDialog->setTableHeaders( headers ); diff --git a/src/gui/layout/qgslayoutmapwidget.cpp b/src/gui/layout/qgslayoutmapwidget.cpp index 0f0be3ccd34f..f38fded4bd8f 100644 --- a/src/gui/layout/qgslayoutmapwidget.cpp +++ b/src/gui/layout/qgslayoutmapwidget.cpp @@ -714,7 +714,7 @@ void QgsLayoutMapWidget::viewScaleInCanvas() } const double currentScale = mMapItem->scale(); - mMapCanvas->zoomScale( currentScale ); + mMapCanvas->zoomScale( currentScale, true ); } void QgsLayoutMapWidget::mXMinLineEdit_editingFinished() diff --git a/src/gui/layout/qgslayoutmarkerwidget.cpp b/src/gui/layout/qgslayoutmarkerwidget.cpp new file mode 100644 index 000000000000..45d6edb4d926 --- /dev/null +++ b/src/gui/layout/qgslayoutmarkerwidget.cpp @@ -0,0 +1,219 @@ +/*************************************************************************** + qgslayoutmarkerwidget.cpp + -------------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgslayoutmarkerwidget.h" +#include "qgsstyle.h" +#include "qgslayoutitemmarker.h" +#include "qgslayout.h" +#include "qgslayoutundostack.h" +#include "qgsvectorlayer.h" +#include "qgslayoutitemmap.h" + +QgsLayoutMarkerWidget::QgsLayoutMarkerWidget( QgsLayoutItemMarker *marker ) + : QgsLayoutItemBaseWidget( nullptr, marker ) + , mMarker( marker ) +{ + Q_ASSERT( mMarker ); + + setupUi( this ); + setPanelTitle( tr( "Marker Properties" ) ); + + //add widget for general composer item properties + mItemPropertiesWidget = new QgsLayoutItemPropertiesWidget( this, marker ); + mItemPropertiesWidget->showFrameGroup( false ); + mainLayout->addWidget( mItemPropertiesWidget ); + + blockAllSignals( true ); + + mShapeStyleButton->setSymbolType( QgsSymbol::Marker ); + + blockAllSignals( false ); + + connect( mMarker, &QgsLayoutObject::changed, this, &QgsLayoutMarkerWidget::setGuiElementValues ); + mShapeStyleButton->registerExpressionContextGenerator( mMarker ); + + connect( mShapeStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutMarkerWidget::symbolChanged ); + + connect( mRotationFromMapCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutMarkerWidget::rotationFromMapCheckBoxChanged ); + connect( mNorthOffsetSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutMarkerWidget::northOffsetSpinBoxChanged ); + connect( mNorthTypeComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutMarkerWidget::northTypeComboBoxChanged ); + + mNorthTypeComboBox->blockSignals( true ); + mNorthTypeComboBox->addItem( tr( "Grid North" ), QgsLayoutNorthArrowHandler::GridNorth ); + mNorthTypeComboBox->addItem( tr( "True North" ), QgsLayoutNorthArrowHandler::TrueNorth ); + mNorthTypeComboBox->blockSignals( false ); + mNorthOffsetSpinBox->setClearValue( 0.0 ); + + mShapeStyleButton->setLayer( coverageLayer() ); + if ( mMarker->layout() ) + { + connect( &mMarker->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mShapeStyleButton, &QgsSymbolButton::setLayer ); + mMapComboBox->setCurrentLayout( mMarker->layout() ); + mMapComboBox->setItemType( QgsLayoutItemRegistry::LayoutMap ); + connect( mMapComboBox, &QgsLayoutItemComboBox::itemChanged, this, &QgsLayoutMarkerWidget::mapChanged ); + } + + setGuiElementValues(); +} + +void QgsLayoutMarkerWidget::setMasterLayout( QgsMasterLayoutInterface *masterLayout ) +{ + if ( mItemPropertiesWidget ) + mItemPropertiesWidget->setMasterLayout( masterLayout ); +} + +bool QgsLayoutMarkerWidget::setNewItem( QgsLayoutItem *item ) +{ + if ( item->type() != QgsLayoutItemRegistry::LayoutShape ) + return false; + + if ( mMarker ) + { + disconnect( mMarker, &QgsLayoutObject::changed, this, &QgsLayoutMarkerWidget::setGuiElementValues ); + } + + mMarker = qobject_cast< QgsLayoutItemMarker * >( item ); + mItemPropertiesWidget->setItem( mMarker ); + + if ( mMarker ) + { + connect( mMarker, &QgsLayoutObject::changed, this, &QgsLayoutMarkerWidget::setGuiElementValues ); + mShapeStyleButton->registerExpressionContextGenerator( mMarker ); + } + + setGuiElementValues(); + + return true; +} + +void QgsLayoutMarkerWidget::blockAllSignals( bool block ) +{ + mShapeStyleButton->blockSignals( block ); + mMapComboBox->blockSignals( block ); + mRotationFromMapCheckBox->blockSignals( block ); + mNorthTypeComboBox->blockSignals( block ); + mNorthOffsetSpinBox->blockSignals( block ); +} + +void QgsLayoutMarkerWidget::setGuiElementValues() +{ + if ( !mMarker ) + { + return; + } + + blockAllSignals( true ); + + mShapeStyleButton->setSymbol( mMarker->symbol()->clone() ); + + mMapComboBox->setItem( mMarker->linkedMap() ); + if ( mMarker->linkedMap() ) + { + mRotationFromMapCheckBox->setCheckState( Qt::Checked ); + mMapComboBox->setEnabled( true ); + mNorthTypeComboBox->setEnabled( true ); + mNorthOffsetSpinBox->setEnabled( true ); + } + else + { + mRotationFromMapCheckBox->setCheckState( Qt::Unchecked ); + mMapComboBox->setEnabled( false ); + mNorthTypeComboBox->setEnabled( false ); + mNorthOffsetSpinBox->setEnabled( false ); + } + mNorthTypeComboBox->setCurrentIndex( mNorthTypeComboBox->findData( mMarker->northMode() ) ); + mNorthOffsetSpinBox->setValue( mMarker->northOffset() ); + + blockAllSignals( false ); +} + +void QgsLayoutMarkerWidget::symbolChanged() +{ + if ( !mMarker ) + return; + + mMarker->layout()->undoStack()->beginCommand( mMarker, tr( "Change Marker Symbol" ), QgsLayoutItem::UndoShapeStyle ); + mMarker->setSymbol( mShapeStyleButton->clonedSymbol() ); + mMarker->layout()->undoStack()->endCommand(); +} + +void QgsLayoutMarkerWidget::rotationFromMapCheckBoxChanged( int state ) +{ + if ( !mMarker ) + { + return; + } + + mMarker->beginCommand( tr( "Toggle Rotation Sync" ) ); + if ( state == Qt::Unchecked ) + { + mMarker->setLinkedMap( nullptr ); + mMapComboBox->setEnabled( false ); + mNorthTypeComboBox->setEnabled( false ); + mNorthOffsetSpinBox->setEnabled( false ); + } + else + { + QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap * >( mMapComboBox->currentItem() ); + mMarker->setLinkedMap( map ); + mNorthTypeComboBox->setEnabled( true ); + mNorthOffsetSpinBox->setEnabled( true ); + mMapComboBox->setEnabled( true ); + } + mMarker->endCommand(); +} + +void QgsLayoutMarkerWidget::mapChanged( QgsLayoutItem *item ) +{ + if ( !mMarker ) + { + return; + } + + const QgsLayout *layout = mMarker->layout(); + if ( !layout ) + { + return; + } + + QgsLayoutItemMap *map = qobject_cast< QgsLayoutItemMap *>( item ); + if ( !map ) + { + return; + } + + mMarker->beginCommand( tr( "Change Rotation Map" ) ); + mMarker->setLinkedMap( map ); + mMarker->update(); + mMarker->endCommand(); +} + +void QgsLayoutMarkerWidget::northOffsetSpinBoxChanged( double d ) +{ + mMarker->beginCommand( tr( "Change Marker North Offset" ), QgsLayoutItem::UndoPictureNorthOffset ); + mMarker->setNorthOffset( d ); + mMarker->endCommand(); + mMarker->update(); +} + +void QgsLayoutMarkerWidget::northTypeComboBoxChanged( int index ) +{ + mMarker->beginCommand( tr( "Change Marker North Mode" ) ); + mMarker->setNorthMode( static_cast< QgsLayoutNorthArrowHandler::NorthMode >( mNorthTypeComboBox->itemData( index ).toInt() ) ); + mMarker->endCommand(); + mMarker->update(); +} diff --git a/src/gui/layout/qgslayoutmarkerwidget.h b/src/gui/layout/qgslayoutmarkerwidget.h new file mode 100644 index 000000000000..5c41a903fe98 --- /dev/null +++ b/src/gui/layout/qgslayoutmarkerwidget.h @@ -0,0 +1,69 @@ +/*************************************************************************** + qgslayoutmarkerwidget.h + -------------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSLAYOUTMARKERWIDGET_H +#define QGSLAYOUTMARKERWIDGET_H + +// We don't want to expose this in the public API +#define SIP_NO_FILE + +#include "qgis_gui.h" +#include "ui_qgslayoutmarkerwidgetbase.h" +#include "qgslayoutitemwidget.h" +#include "qgslayoutitemmarker.h" + +/** + * \ingroup gui + * A widget for configuring layout shape items. + * + * \note This class is not a part of public API + * \since QGIS 3.12 + */ +class GUI_EXPORT QgsLayoutMarkerWidget: public QgsLayoutItemBaseWidget, private Ui::QgsLayoutMarkerWidgetBase +{ + Q_OBJECT + public: + //! constructor + explicit QgsLayoutMarkerWidget( QgsLayoutItemMarker *marker ); + void setMasterLayout( QgsMasterLayoutInterface *masterLayout ) override; + + protected: + + bool setNewItem( QgsLayoutItem *item ) override; + + + private: + QPointer< QgsLayoutItemMarker > mMarker; + QgsLayoutItemPropertiesWidget *mItemPropertiesWidget = nullptr; + + //! Blocks / unblocks the signal of all GUI elements + void blockAllSignals( bool block ); + + private slots: + + void symbolChanged(); + void rotationFromMapCheckBoxChanged( int state ); + void mapChanged( QgsLayoutItem *item ); + void northOffsetSpinBoxChanged( double d ); + void northTypeComboBoxChanged( int index ); + + //! Sets the GUI elements to the currentValues of mComposerShape + void setGuiElementValues(); + +}; + +#endif // QGSLAYOUTMARKERWIDGET_H diff --git a/src/gui/layout/qgslayoutpicturewidget.cpp b/src/gui/layout/qgslayoutpicturewidget.cpp index 656c86e64be7..544c4c6e9977 100644 --- a/src/gui/layout/qgslayoutpicturewidget.cpp +++ b/src/gui/layout/qgslayoutpicturewidget.cpp @@ -23,6 +23,7 @@ #include "qgsexpressionbuilderdialog.h" #include "qgssvgcache.h" #include "qgssettings.h" +#include "qgssvgselectorwidget.h" #include #include @@ -55,12 +56,7 @@ QgsLayoutPictureWidget::QgsLayoutPictureWidget( QgsLayoutItemPicture *picture ) mAnchorPointComboBox->addItem( tr( "Bottom Center" ), QgsLayoutItem::LowerMiddle ); mAnchorPointComboBox->addItem( tr( "Bottom Right" ), QgsLayoutItem::LowerRight ); - connect( mPictureBrowseButton, &QPushButton::clicked, this, &QgsLayoutPictureWidget::mPictureBrowseButton_clicked ); - connect( mPictureLineEdit, &QLineEdit::editingFinished, this, &QgsLayoutPictureWidget::mPictureLineEdit_editingFinished ); connect( mPictureRotationSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPictureWidget::mPictureRotationSpinBox_valueChanged ); - connect( mPreviewListWidget, &QListWidget::currentItemChanged, this, &QgsLayoutPictureWidget::mPreviewListWidget_currentItemChanged ); - connect( mAddDirectoryButton, &QPushButton::clicked, this, &QgsLayoutPictureWidget::mAddDirectoryButton_clicked ); - connect( mRemoveDirectoryButton, &QPushButton::clicked, this, &QgsLayoutPictureWidget::mRemoveDirectoryButton_clicked ); connect( mRotationFromComposerMapCheckBox, &QCheckBox::stateChanged, this, &QgsLayoutPictureWidget::mRotationFromComposerMapCheckBox_stateChanged ); connect( mResizeModeComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutPictureWidget::mResizeModeComboBox_currentIndexChanged ); connect( mAnchorPointComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutPictureWidget::mAnchorPointComboBox_currentIndexChanged ); @@ -69,6 +65,13 @@ QgsLayoutPictureWidget::QgsLayoutPictureWidget( QgsLayoutItemPicture *picture ) connect( mStrokeWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPictureWidget::mStrokeWidthSpinBox_valueChanged ); connect( mPictureRotationOffsetSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutPictureWidget::mPictureRotationOffsetSpinBox_valueChanged ); connect( mNorthTypeComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutPictureWidget::mNorthTypeComboBox_currentIndexChanged ); + connect( mRadioSVG, &QRadioButton::toggled, this, &QgsLayoutPictureWidget::modeChanged ); + connect( mRadioRaster, &QRadioButton::toggled, this, &QgsLayoutPictureWidget::modeChanged ); + connect( mSvgSourceLineEdit, &QgsSvgSourceLineEdit::sourceChanged, this, &QgsLayoutPictureWidget::svgSourceChanged ); + connect( mImageSourceLineEdit, &QgsImageSourceLineEdit::sourceChanged, this, &QgsLayoutPictureWidget::rasterSourceChanged ); + + mSvgSourceLineEdit->setLastPathSettingsKey( QStringLiteral( "/UI/lastComposerPictureDir" ) ); + setPanelTitle( tr( "Picture Properties" ) ); mFillColorButton->setAllowOpacity( true ); @@ -88,6 +91,19 @@ QgsLayoutPictureWidget::QgsLayoutPictureWidget( QgsLayoutItemPicture *picture ) mPictureRotationOffsetSpinBox->setClearValue( 0.0 ); mPictureRotationSpinBox->setClearValue( 0.0 ); + viewGroups->setHeaderHidden( true ); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().width( 'X' ) * 4 ) ) ); +#else + mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 4 ) ) ); +#endif + viewImages->setGridSize( QSize( mIconSize * 1.2, mIconSize * 1.2 ) ); + populateList(); + + connect( viewImages->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsLayoutPictureWidget::setSvgName ); + connect( viewGroups->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsLayoutPictureWidget::populateIcons ); + + //add widget for general composer item properties mItemPropertiesWidget = new QgsLayoutItemPropertiesWidget( this, picture ); mainLayout->addWidget( mItemPropertiesWidget ); @@ -100,24 +116,20 @@ QgsLayoutPictureWidget::QgsLayoutPictureWidget( QgsLayoutItemPicture *picture ) } setGuiElementValues(); - mPreviewsLoadingLabel->hide(); - - mPreviewListWidget->setIconSize( QSize( 30, 30 ) ); - - // mSearchDirectoriesGroupBox is a QgsCollapsibleGroupBoxBasic, so its collapsed state should not be saved/restored - mSearchDirectoriesGroupBox->setCollapsed( true ); - // setup connection for loading previews on first expansion of group box - connect( mSearchDirectoriesGroupBox, &QgsCollapsibleGroupBoxBasic::collapsedStateChanged, this, &QgsLayoutPictureWidget::loadPicturePreviews ); connect( mPicture, &QgsLayoutObject::changed, this, &QgsLayoutPictureWidget::setGuiElementValues ); connect( mPicture, &QgsLayoutItemPicture::pictureRotationChanged, this, &QgsLayoutPictureWidget::setPicRotationSpinValue ); //connections for data defined buttons - connect( mSourceDDBtn, &QgsPropertyOverrideButton::activated, mPictureLineEdit, &QLineEdit::setDisabled ); + mSourceDDBtn->registerEnabledWidget( mImageSourceLineEdit, false ); + mSourceDDBtn->registerEnabledWidget( mSvgSourceLineEdit, false ); + registerDataDefinedButton( mSourceDDBtn, QgsLayoutObject::PictureSource ); registerDataDefinedButton( mFillColorDDBtn, QgsLayoutObject::PictureSvgBackgroundColor ); registerDataDefinedButton( mStrokeColorDDBtn, QgsLayoutObject::PictureSvgStrokeColor ); registerDataDefinedButton( mStrokeWidthDDBtn, QgsLayoutObject::PictureSvgStrokeWidth ); + + updatePictureTypeWidgets(); } void QgsLayoutPictureWidget::setMasterLayout( QgsMasterLayoutInterface *masterLayout ) @@ -126,68 +138,6 @@ void QgsLayoutPictureWidget::setMasterLayout( QgsMasterLayoutInterface *masterLa mItemPropertiesWidget->setMasterLayout( masterLayout ); } -void QgsLayoutPictureWidget::mPictureBrowseButton_clicked() -{ - QgsSettings s; - QString openDir; - QString lineEditText = mPictureLineEdit->text(); - if ( !lineEditText.isEmpty() ) - { - QFileInfo openDirFileInfo( lineEditText ); - openDir = openDirFileInfo.path(); - } - - if ( openDir.isEmpty() ) - { - openDir = s.value( QStringLiteral( "/UI/lastComposerPictureDir" ), QDir::homePath() ).toString(); - } - - //show file dialog - QString filePath = QFileDialog::getOpenFileName( this, tr( "Select SVG or Image File" ), openDir ); - if ( filePath.isEmpty() ) - { - return; - } - - //check if file exists - QFileInfo fileInfo( filePath ); - if ( !fileInfo.exists() || !fileInfo.isReadable() ) - { - QMessageBox::critical( nullptr, tr( "Select File" ), tr( "Error, file does not exist or is not readable." ) ); - return; - } - - s.setValue( QStringLiteral( "/UI/lastComposerPictureDir" ), fileInfo.absolutePath() ); - - mPictureLineEdit->blockSignals( true ); - mPictureLineEdit->setText( filePath ); - mPictureLineEdit->blockSignals( false ); - updateSvgParamGui(); - - //pass file path to QgsLayoutItemPicture - if ( mPicture ) - { - mPicture->beginCommand( tr( "Change Picture" ) ); - mPicture->setPicturePath( filePath ); - mPicture->update(); - mPicture->endCommand(); - } -} - -void QgsLayoutPictureWidget::mPictureLineEdit_editingFinished() -{ - if ( mPicture ) - { - QString filePath = mPictureLineEdit->text(); - - mPicture->beginCommand( tr( "Change Picture" ) ); - mPicture->setPicturePath( filePath ); - mPicture->update(); - mPicture->endCommand(); - updateSvgParamGui(); - } -} - void QgsLayoutPictureWidget::mPictureRotationSpinBox_valueChanged( double d ) { if ( mPicture ) @@ -198,74 +148,6 @@ void QgsLayoutPictureWidget::mPictureRotationSpinBox_valueChanged( double d ) } } -void QgsLayoutPictureWidget::mPreviewListWidget_currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous ) -{ - Q_UNUSED( previous ) - if ( !mPicture || !current ) - { - return; - } - - QString absoluteFilePath = current->data( Qt::UserRole ).toString(); - mPicture->beginCommand( tr( "Change Picture" ) ); - mPicture->setPicturePath( absoluteFilePath ); - mPictureLineEdit->setText( absoluteFilePath ); - mPicture->update(); - mPicture->endCommand(); - updateSvgParamGui(); -} - -void QgsLayoutPictureWidget::mAddDirectoryButton_clicked() -{ - //let user select a directory - QString directory = QFileDialog::getExistingDirectory( this, tr( "Select New Preview Directory" ) ); - if ( directory.isNull() ) - { - return; //dialog canceled by user - } - - //add entry to mSearchDirectoriesComboBox - mSearchDirectoriesComboBox->addItem( directory ); - - //and add icons to the preview - addDirectoryToPreview( directory ); - - //update the image directory list in the settings - QgsSettings s; - QStringList userDirList = s.value( QStringLiteral( "/Composer/PictureWidgetDirectories" ) ).toStringList(); - if ( !userDirList.contains( directory ) ) - { - userDirList.append( directory ); - } - s.setValue( QStringLiteral( "/Composer/PictureWidgetDirectories" ), userDirList ); -} - -void QgsLayoutPictureWidget::mRemoveDirectoryButton_clicked() -{ - QString directoryToRemove = mSearchDirectoriesComboBox->currentText(); - if ( directoryToRemove.isEmpty() ) - { - return; - } - mSearchDirectoriesComboBox->removeItem( mSearchDirectoriesComboBox->currentIndex() ); - - //remove entries from back to front (to have the indices of existing items constant) - for ( int i = ( mPreviewListWidget->count() - 1 ); i >= 0; --i ) - { - QListWidgetItem *currentItem = mPreviewListWidget->item( i ); - if ( currentItem && currentItem->data( Qt::UserRole ).toString().startsWith( directoryToRemove ) ) - { - delete ( mPreviewListWidget->takeItem( i ) ); - } - } - - //update the image directory list in the settings - QgsSettings s; - QStringList userDirList = s.value( QStringLiteral( "/Composer/PictureWidgetDirectories" ) ).toStringList(); - userDirList.removeOne( directoryToRemove ); - s.setValue( QStringLiteral( "/Composer/PictureWidgetDirectories" ), userDirList ); -} - void QgsLayoutPictureWidget::mResizeModeComboBox_currentIndexChanged( int ) { if ( !mPicture ) @@ -398,7 +280,6 @@ void QgsLayoutPictureWidget::setGuiElementValues() if ( mPicture ) { mPictureRotationSpinBox->blockSignals( true ); - mPictureLineEdit->blockSignals( true ); mComposerMapComboBox->blockSignals( true ); mRotationFromComposerMapCheckBox->blockSignals( true ); mNorthTypeComboBox->blockSignals( true ); @@ -409,7 +290,6 @@ void QgsLayoutPictureWidget::setGuiElementValues() mStrokeColorButton->blockSignals( true ); mStrokeWidthSpinBox->blockSignals( true ); - mPictureLineEdit->setText( mPicture->picturePath() ); mPictureRotationSpinBox->setValue( mPicture->pictureRotation() ); mComposerMapComboBox->setItem( mPicture->linkedMap() ); @@ -450,6 +330,34 @@ void QgsLayoutPictureWidget::setGuiElementValues() mAnchorPointComboBox->setEnabled( false ); } + whileBlocking( mRadioSVG )->setChecked( mPicture->mode() == QgsLayoutItemPicture::FormatSVG ); + whileBlocking( mRadioRaster )->setChecked( mPicture->mode() == QgsLayoutItemPicture::FormatRaster ); + updatePictureTypeWidgets(); + + if ( mRadioSVG->isChecked() ) + { + whileBlocking( mSvgSourceLineEdit )->setSource( mPicture->picturePath() ); + + mBlockSvgModelChanges++; + QAbstractItemModel *m = viewImages->model(); + QItemSelectionModel *selModel = viewImages->selectionModel(); + for ( int i = 0; i < m->rowCount(); i++ ) + { + QModelIndex idx( m->index( i, 0 ) ); + if ( m->data( idx ).toString() == mPicture->picturePath() ) + { + selModel->select( idx, QItemSelectionModel::SelectCurrent ); + selModel->setCurrentIndex( idx, QItemSelectionModel::SelectCurrent ); + break; + } + } + mBlockSvgModelChanges--; + } + else if ( mRadioRaster->isChecked() ) + { + whileBlocking( mImageSourceLineEdit )->setSource( mPicture->picturePath() ); + } + updateSvgParamGui( false ); mFillColorButton->setColor( mPicture->svgFillColor() ); mStrokeColorButton->setColor( mPicture->svgStrokeColor() ); @@ -457,7 +365,6 @@ void QgsLayoutPictureWidget::setGuiElementValues() mRotationFromComposerMapCheckBox->blockSignals( false ); mPictureRotationSpinBox->blockSignals( false ); - mPictureLineEdit->blockSignals( false ); mComposerMapComboBox->blockSignals( false ); mNorthTypeComboBox->blockSignals( false ); mPictureRotationOffsetSpinBox->blockSignals( false ); @@ -471,35 +378,6 @@ void QgsLayoutPictureWidget::setGuiElementValues() } } -QIcon QgsLayoutPictureWidget::svgToIcon( const QString &filePath ) const -{ - QColor fill, stroke; - double strokeWidth, fillOpacity, strokeOpacity; - bool fillParam, fillOpacityParam, strokeParam, strokeWidthParam, strokeOpacityParam; - bool hasDefaultFillColor = false, hasDefaultFillOpacity = false, hasDefaultStrokeColor = false, - hasDefaultStrokeWidth = false, hasDefaultStrokeOpacity = false; - QgsApplication::svgCache()->containsParams( filePath, fillParam, hasDefaultFillColor, fill, - fillOpacityParam, hasDefaultFillOpacity, fillOpacity, - strokeParam, hasDefaultStrokeColor, stroke, - strokeWidthParam, hasDefaultStrokeWidth, strokeWidth, - strokeOpacityParam, hasDefaultStrokeOpacity, strokeOpacity ); - - //if defaults not set in symbol, use these values - if ( !hasDefaultFillColor ) - fill = QColor( 200, 200, 200 ); - fill.setAlphaF( hasDefaultFillOpacity ? fillOpacity : 1.0 ); - if ( !hasDefaultStrokeColor ) - stroke = Qt::black; - stroke.setAlphaF( hasDefaultStrokeOpacity ? strokeOpacity : 1.0 ); - if ( !hasDefaultStrokeWidth ) - strokeWidth = 0.6; - - bool fitsInCache; // should always fit in cache at these sizes (i.e. under 559 px ^ 2, or half cache size) - const QImage &img = QgsApplication::svgCache()->svgAsImage( filePath, 30.0, fill, stroke, strokeWidth, 3.5 /*appr. 88 dpi*/, fitsInCache ); - - return QIcon( QPixmap::fromImage( img ) ); -} - void QgsLayoutPictureWidget::updateSvgParamGui( bool resetValues ) { if ( !mPicture ) @@ -558,161 +436,6 @@ void QgsLayoutPictureWidget::updateSvgParamGui( bool resetValues ) mStrokeWidthSpinBox->setEnabled( hasStrokeWidthParam ); } -int QgsLayoutPictureWidget::addDirectoryToPreview( const QString &path ) -{ - //go through all files of a directory - QDir directory( path ); - if ( !directory.exists() || !directory.isReadable() ) - { - return 1; //error - } - - QFileInfoList fileList = directory.entryInfoList( QDir::Files ); - QFileInfoList::const_iterator fileIt = fileList.constBegin(); - - QProgressDialog progress( tr( "Adding Icons…" ), tr( "Abort" ), 0, fileList.size() - 1, this ); - //cancel button does not seem to work properly with modal dialog - //progress.setWindowModality(Qt::WindowModal); - - int counter = 0; - for ( ; fileIt != fileList.constEnd(); ++fileIt ) - { - - progress.setLabelText( tr( "Creating icon for file %1" ).arg( fileIt->fileName() ) ); - progress.setValue( counter ); - QCoreApplication::processEvents(); - if ( progress.wasCanceled() ) - { - break; - } - QString filePath = fileIt->absoluteFilePath(); - - //test if file is svg or pixel format - bool fileIsPixel = false; - bool fileIsSvg = testSvgFile( filePath ); - if ( !fileIsSvg ) - { - fileIsPixel = testImageFile( filePath ); - } - - //exclude files that are not svg or image - if ( !fileIsSvg && !fileIsPixel ) - { - ++counter; - continue; - } - - QListWidgetItem *listItem = new QListWidgetItem( mPreviewListWidget ); - listItem->setFlags( Qt::ItemIsSelectable | Qt::ItemIsEnabled ); - - if ( fileIsSvg ) - { - // render SVG file - QIcon icon = svgToIcon( filePath ); - listItem->setIcon( icon ); - } - else //for pixel formats: create icon from scaled pixmap - { - QPixmap iconPixmap( filePath ); - if ( iconPixmap.isNull() ) - { - ++counter; - continue; //unknown file format or other problem - } - //set pixmap hardcoded to 30/30, same as icon size for mPreviewListWidget - QPixmap scaledPixmap( iconPixmap.scaled( QSize( 30, 30 ), Qt::KeepAspectRatio ) ); - QIcon icon( scaledPixmap ); - listItem->setIcon( icon ); - } - - listItem->setText( QString() ); - //store the absolute icon file path as user data - listItem->setData( Qt::UserRole, fileIt->absoluteFilePath() ); - ++counter; - } - - return 0; -} - -void QgsLayoutPictureWidget::addStandardDirectoriesToPreview() -{ - mPreviewListWidget->clear(); - - //list all directories in $prefix/share/qgis/svg - QStringList svgPaths = QgsApplication::svgPaths(); - for ( int i = 0; i < svgPaths.size(); i++ ) - { - QDir svgDirectory( svgPaths[i] ); - if ( !svgDirectory.exists() || !svgDirectory.isReadable() ) - { - continue; - } - - //add directory itself - mSearchDirectoriesComboBox->addItem( svgDirectory.absolutePath() ); - addDirectoryToPreview( svgDirectory.absolutePath() ); - - //and also subdirectories - QFileInfoList directoryList = svgDirectory.entryInfoList( QDir::Dirs | QDir::NoDotAndDotDot ); - QFileInfoList::const_iterator dirIt = directoryList.constBegin(); - for ( ; dirIt != directoryList.constEnd(); ++dirIt ) - { - if ( addDirectoryToPreview( dirIt->absoluteFilePath() ) == 0 ) - { - mSearchDirectoriesComboBox->addItem( dirIt->absoluteFilePath() ); - } - } - } - - //include additional user-defined directories for images - QgsSettings s; - QStringList userDirList = s.value( QStringLiteral( "/Composer/PictureWidgetDirectories" ) ).toStringList(); - QStringList::const_iterator userDirIt = userDirList.constBegin(); - for ( ; userDirIt != userDirList.constEnd(); ++userDirIt ) - { - addDirectoryToPreview( *userDirIt ); - mSearchDirectoriesComboBox->addItem( *userDirIt ); - } - - mPreviewsLoaded = true; -} - -bool QgsLayoutPictureWidget::testSvgFile( const QString &filename ) const -{ - //QSvgRenderer crashes with some (non-svg) xml documents. - //So at least we try to sort out the ones with different suffixes - if ( !filename.endsWith( QLatin1String( ".svg" ) ) ) - { - return false; - } - - QSvgRenderer svgRenderer( filename ); - return svgRenderer.isValid(); -} - -bool QgsLayoutPictureWidget::testImageFile( const QString &filename ) const -{ - QString formatName = QString( QImageReader::imageFormat( filename ) ); - return !formatName.isEmpty(); //file is in a supported pixel format -} - -void QgsLayoutPictureWidget::loadPicturePreviews( bool collapsed ) -{ - if ( mPreviewsLoaded ) - { - return; - } - - if ( !collapsed ) // load the previews only on first parent group box expansion - { - mPreviewListWidget->hide(); - mPreviewsLoadingLabel->show(); - addStandardDirectoriesToPreview(); - mPreviewsLoadingLabel->hide(); - mPreviewListWidget->show(); - } -} - void QgsLayoutPictureWidget::mFillColorButton_colorChanged( const QColor &color ) { mPicture->beginCommand( tr( "Change Picture Fill Color" ), QgsLayoutItem::UndoPictureFillColor ); @@ -753,10 +476,98 @@ void QgsLayoutPictureWidget::mNorthTypeComboBox_currentIndexChanged( int index ) mPicture->update(); } -void QgsLayoutPictureWidget::resizeEvent( QResizeEvent *event ) +void QgsLayoutPictureWidget::modeChanged() { - Q_UNUSED( event ) - mSearchDirectoriesComboBox->setMinimumWidth( mPreviewListWidget->sizeHint().width() ); + const QgsLayoutItemPicture::Format newFormat = mRadioSVG->isChecked() ? QgsLayoutItemPicture::FormatSVG : QgsLayoutItemPicture::FormatRaster; + if ( mPicture && mPicture->mode() != newFormat ) + { + whileBlocking( mSvgSourceLineEdit )->setSource( QString() ); + whileBlocking( mImageSourceLineEdit )->setSource( QString() ); + mPicture->beginCommand( tr( "Change Picture Type" ) ); + mPicture->setPicturePath( QString(), newFormat ); + mPicture->endCommand(); + } + updatePictureTypeWidgets(); +} + +void QgsLayoutPictureWidget::updatePictureTypeWidgets() +{ + mRasterFrame->setVisible( mRadioRaster->isChecked() ); + mSVGFrame->setVisible( mRadioSVG->isChecked() ); + mSVGParamsGroupBox->setVisible( mRadioSVG->isChecked() ); + + // need to move the data defined button to the appropriate frame -- we can't have two buttons linked to the one property! + if ( mRadioSVG->isChecked() ) + mSvgDDBtnFrame->layout()->addWidget( mSourceDDBtn ); + else + mRasterDDBtnFrame->layout()->addWidget( mSourceDDBtn ); +} + +void QgsLayoutPictureWidget::populateList() +{ + QAbstractItemModel *oldModel = viewGroups->model(); + QgsSvgSelectorGroupsModel *g = new QgsSvgSelectorGroupsModel( viewGroups ); + viewGroups->setModel( g ); + delete oldModel; + + // Set the tree expanded at the first level + int rows = g->rowCount( g->indexFromItem( g->invisibleRootItem() ) ); + for ( int i = 0; i < rows; i++ ) + { + viewGroups->setExpanded( g->indexFromItem( g->item( i ) ), true ); + } + + // Initially load the icons in the List view without any grouping + oldModel = viewImages->model(); + QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( viewImages, mIconSize ); + viewImages->setModel( m ); + + delete oldModel; +} + +void QgsLayoutPictureWidget::populateIcons( const QModelIndex &idx ) +{ + QString path = idx.data( Qt::UserRole + 1 ).toString(); + + QAbstractItemModel *oldModel = viewImages->model(); + QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( viewImages, path, mIconSize ); + viewImages->setModel( m ); + delete oldModel; + + connect( viewImages->selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsLayoutPictureWidget::setSvgName ); +} + +void QgsLayoutPictureWidget::setSvgName( const QModelIndex &idx ) +{ + if ( mBlockSvgModelChanges ) + return; + + QString name = idx.data( Qt::UserRole ).toString(); + whileBlocking( mSvgSourceLineEdit )->setSource( name ); + svgSourceChanged( name ); +} + +void QgsLayoutPictureWidget::svgSourceChanged( const QString &source ) +{ + if ( mPicture ) + { + mPicture->beginCommand( tr( "Change Picture" ) ); + mPicture->setPicturePath( source, QgsLayoutItemPicture::FormatSVG ); + mPicture->update(); + mPicture->endCommand(); + updateSvgParamGui(); + } +} + +void QgsLayoutPictureWidget::rasterSourceChanged( const QString &source ) +{ + if ( mPicture ) + { + mPicture->beginCommand( tr( "Change Picture" ) ); + mPicture->setPicturePath( source, QgsLayoutItemPicture::FormatRaster ); + mPicture->update(); + mPicture->endCommand(); + } } void QgsLayoutPictureWidget::populateDataDefinedButtons() @@ -767,6 +578,7 @@ void QgsLayoutPictureWidget::populateDataDefinedButtons() updateDataDefinedButton( mStrokeWidthDDBtn ); //initial state of controls - disable related controls when dd buttons are active - mPictureLineEdit->setEnabled( !mSourceDDBtn->isActive() ); + mImageSourceLineEdit->setEnabled( !mSourceDDBtn->isActive() ); + mSvgSourceLineEdit->setEnabled( !mSourceDDBtn->isActive() ); } diff --git a/src/gui/layout/qgslayoutpicturewidget.h b/src/gui/layout/qgslayoutpicturewidget.h index 9d5cd65aba06..4f4b081e5e1e 100644 --- a/src/gui/layout/qgslayoutpicturewidget.h +++ b/src/gui/layout/qgslayoutpicturewidget.h @@ -43,16 +43,8 @@ class GUI_EXPORT QgsLayoutPictureWidget: public QgsLayoutItemBaseWidget, private explicit QgsLayoutPictureWidget( QgsLayoutItemPicture *picture ); void setMasterLayout( QgsMasterLayoutInterface *masterLayout ) override; - //! Add the icons of the standard directories to the preview - void addStandardDirectoriesToPreview(); - private slots: - void mPictureBrowseButton_clicked(); - void mPictureLineEdit_editingFinished(); void mPictureRotationSpinBox_valueChanged( double d ); - void mPreviewListWidget_currentItemChanged( QListWidgetItem *current, QListWidgetItem *previous ); - void mAddDirectoryButton_clicked(); - void mRemoveDirectoryButton_clicked(); void mRotationFromComposerMapCheckBox_stateChanged( int state ); void mapChanged( QgsLayoutItem *item ); void mResizeModeComboBox_currentIndexChanged( int index ); @@ -62,8 +54,6 @@ class GUI_EXPORT QgsLayoutPictureWidget: public QgsLayoutItemBaseWidget, private bool setNewItem( QgsLayoutItem *item ) override; - void resizeEvent( QResizeEvent *event ) override; - protected slots: //! Initializes data defined buttons to current atlas coverage layer void populateDataDefinedButtons(); @@ -75,35 +65,24 @@ class GUI_EXPORT QgsLayoutPictureWidget: public QgsLayoutItemBaseWidget, private //! Sets the picture rotation GUI control value void setPicRotationSpinValue( double r ); - /** - * Load SVG and pixel-based image previews - * \param collapsed Whether the parent group box is collapsed */ - void loadPicturePreviews( bool collapsed ); - void mFillColorButton_colorChanged( const QColor &color ); void mStrokeColorButton_colorChanged( const QColor &color ); void mStrokeWidthSpinBox_valueChanged( double d ); void mPictureRotationOffsetSpinBox_valueChanged( double d ); void mNorthTypeComboBox_currentIndexChanged( int index ); - + void modeChanged(); + void updatePictureTypeWidgets(); + + void populateList(); + void populateIcons( const QModelIndex &idx ); + void setSvgName( const QModelIndex &idx ); + void svgSourceChanged( const QString &source ); + void rasterSourceChanged( const QString &source ); private: QPointer< QgsLayoutItemPicture > mPicture; QgsLayoutItemPropertiesWidget *mItemPropertiesWidget = nullptr; - - - //! Whether the picture selection previews have been loaded - bool mPreviewsLoaded = false; - - //! Add the icons of a directory to the preview. Returns 0 in case of success - int addDirectoryToPreview( const QString &path ); - - //! Tests if a file is valid svg - bool testSvgFile( const QString &filename ) const; - //! Tests if a file is a valid pixel format - bool testImageFile( const QString &filename ) const; - - //! Renders an svg file to a QIcon, correctly handling any SVG parameters present in the file - QIcon svgToIcon( const QString &filePath ) const; + int mIconSize = 30; + int mBlockSvgModelChanges = 0; void updateSvgParamGui( bool resetValues = true ); }; diff --git a/src/gui/layout/qgslayoutscalebarwidget.cpp b/src/gui/layout/qgslayoutscalebarwidget.cpp index 5040203cd680..a6524e0bc011 100644 --- a/src/gui/layout/qgslayoutscalebarwidget.cpp +++ b/src/gui/layout/qgslayoutscalebarwidget.cpp @@ -17,10 +17,12 @@ #include "qgslayoutscalebarwidget.h" #include "qgslayoutitemmap.h" #include "qgslayoutitemscalebar.h" +#include "qgsscalebarrendererregistry.h" #include "qgslayout.h" #include "qgsguiutils.h" #include "qgsvectorlayer.h" #include "qgsnumericformatselectorwidget.h" +#include "qgslayoutundostack.h" #include #include @@ -32,15 +34,11 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa { setupUi( this ); connect( mHeightSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mHeightSpinBox_valueChanged ); - connect( mLineWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mLineWidthSpinBox_valueChanged ); connect( mSegmentSizeSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mSegmentSizeSpinBox_valueChanged ); connect( mSegmentsLeftSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mSegmentsLeftSpinBox_valueChanged ); connect( mNumberOfSegmentsSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mNumberOfSegmentsSpinBox_valueChanged ); connect( mUnitLabelLineEdit, &QLineEdit::textChanged, this, &QgsLayoutScaleBarWidget::mUnitLabelLineEdit_textChanged ); connect( mMapUnitsPerBarUnitSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mMapUnitsPerBarUnitSpinBox_valueChanged ); - connect( mFillColorButton, &QgsColorButton::colorChanged, this, &QgsLayoutScaleBarWidget::mFillColorButton_colorChanged ); - connect( mFillColor2Button, &QgsColorButton::colorChanged, this, &QgsLayoutScaleBarWidget::mFillColor2Button_colorChanged ); - connect( mStrokeColorButton, &QgsColorButton::colorChanged, this, &QgsLayoutScaleBarWidget::mStrokeColorButton_colorChanged ); connect( mStyleComboBox, &QComboBox::currentTextChanged, this, &QgsLayoutScaleBarWidget::mStyleComboBox_currentIndexChanged ); connect( mLabelBarSpaceSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mLabelBarSpaceSpinBox_valueChanged ); connect( mBoxSizeSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mBoxSizeSpinBox_valueChanged ); @@ -48,8 +46,6 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa connect( mLabelVerticalPlacementComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutScaleBarWidget::mLabelVerticalPlacementComboBox_currentIndexChanged ); connect( mLabelHorizontalPlacementComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutScaleBarWidget::mLabelHorizontalPlacementComboBox_currentIndexChanged ); connect( mUnitsComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutScaleBarWidget::mUnitsComboBox_currentIndexChanged ); - connect( mLineJoinStyleCombo, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutScaleBarWidget::mLineJoinStyleCombo_currentIndexChanged ); - connect( mLineCapStyleCombo, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutScaleBarWidget::mLineCapStyleCombo_currentIndexChanged ); connect( mMinWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mMinWidthSpinBox_valueChanged ); connect( mMaxWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsLayoutScaleBarWidget::mMaxWidthSpinBox_valueChanged ); connect( mNumberFormatPushButton, &QPushButton::clicked, this, &QgsLayoutScaleBarWidget::changeNumberFormat ); @@ -70,12 +66,11 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa blockMemberSignals( true ); //style combo box - mStyleComboBox->insertItem( 0, tr( "Single Box" ) ); - mStyleComboBox->insertItem( 1, tr( "Double Box" ) ); - mStyleComboBox->insertItem( 2, tr( "Line Ticks Middle" ) ); - mStyleComboBox->insertItem( 3, tr( "Line Ticks Down" ) ); - mStyleComboBox->insertItem( 4, tr( "Line Ticks Up" ) ); - mStyleComboBox->insertItem( 5, tr( "Numeric" ) ); + const QStringList renderers = QgsApplication::scaleBarRendererRegistry()->sortedRendererList(); + for ( const QString &renderer : renderers ) + { + mStyleComboBox->addItem( QgsApplication::scaleBarRendererRegistry()->visibleName( renderer ), renderer ); + } //label vertical/horizontal placement combo box mLabelVerticalPlacementComboBox->addItem( tr( "Above Segments" ), static_cast< int >( QgsScaleBarSettings::LabelAboveSegment ) ); @@ -97,37 +92,20 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa mUnitsComboBox->addItem( tr( "Centimeters" ), QgsUnitTypes::DistanceCentimeters ); mUnitsComboBox->addItem( tr( "Millimeters" ), QgsUnitTypes::DistanceMillimeters ); - mFillColorButton->setColorDialogTitle( tr( "Select Fill Color" ) ); - mFillColorButton->setAllowOpacity( true ); - mFillColorButton->setContext( QStringLiteral( "composer" ) ); - mFillColorButton->setNoColorString( tr( "Transparent Fill" ) ); - mFillColorButton->setShowNoColor( true ); + mLineStyleButton->setSymbolType( QgsSymbol::Line ); + connect( mLineStyleButton, &QgsSymbolButton::changed, this, &QgsLayoutScaleBarWidget::lineSymbolChanged ); - mFillColor2Button->setColorDialogTitle( tr( "Select Alternate Fill Color" ) ); - mFillColor2Button->setAllowOpacity( true ); - mFillColor2Button->setContext( QStringLiteral( "composer" ) ); - mFillColor2Button->setNoColorString( tr( "Transparent Fill" ) ); - mFillColor2Button->setShowNoColor( true ); + mFillSymbol1Button->setSymbolType( QgsSymbol::Fill ); + connect( mFillSymbol1Button, &QgsSymbolButton::changed, this, &QgsLayoutScaleBarWidget::fillSymbol1Changed ); + + mFillSymbol2Button->setSymbolType( QgsSymbol::Fill ); + connect( mFillSymbol2Button, &QgsSymbolButton::changed, this, &QgsLayoutScaleBarWidget::fillSymbol2Changed ); mFontButton->setDialogTitle( tr( "Scalebar Font" ) ); mFontButton->setMode( QgsFontButton::ModeTextRenderer ); - mStrokeColorButton->setColorDialogTitle( tr( "Select Line Color" ) ); - mStrokeColorButton->setAllowOpacity( true ); - mStrokeColorButton->setContext( QStringLiteral( "composer" ) ); - mStrokeColorButton->setNoColorString( tr( "Transparent Line" ) ); - mStrokeColorButton->setShowNoColor( true ); - - mFillColorDDBtn->registerLinkedWidget( mFillColorButton ); - mFillColor2DDBtn->registerLinkedWidget( mFillColor2Button ); - mLineColorDDBtn->registerLinkedWidget( mStrokeColorButton ); - if ( mScalebar ) { - mFillColorDDBtn->registerExpressionContextGenerator( mScalebar ); - mFillColor2DDBtn->registerExpressionContextGenerator( mScalebar ); - mLineColorDDBtn->registerExpressionContextGenerator( mScalebar ); - mLineWidthDDBtn->registerExpressionContextGenerator( mScalebar ); QgsLayout *scaleBarLayout = mScalebar->layout(); if ( scaleBarLayout ) { @@ -138,19 +116,24 @@ QgsLayoutScaleBarWidget::QgsLayoutScaleBarWidget( QgsLayoutItemScaleBar *scaleBa connect( mMapItemComboBox, &QgsLayoutItemComboBox::itemChanged, this, &QgsLayoutScaleBarWidget::mapChanged ); - registerDataDefinedButton( mFillColorDDBtn, QgsLayoutObject::ScalebarFillColor ); - registerDataDefinedButton( mFillColor2DDBtn, QgsLayoutObject::ScalebarFillColor2 ); - registerDataDefinedButton( mLineColorDDBtn, QgsLayoutObject::ScalebarLineColor ); - registerDataDefinedButton( mLineWidthDDBtn, QgsLayoutObject::ScalebarLineWidth ); - blockMemberSignals( false ); setGuiElements(); //set the GUI elements to the state of scaleBar + mLineStyleButton->registerExpressionContextGenerator( mScalebar ); + mLineStyleButton->setLayer( coverageLayer() ); + mFillSymbol1Button->registerExpressionContextGenerator( mScalebar ); + mFillSymbol1Button->setLayer( coverageLayer() ); + mFillSymbol2Button->registerExpressionContextGenerator( mScalebar ); + mFillSymbol2Button->setLayer( coverageLayer() ); + connect( mFontButton, &QgsFontButton::changed, this, &QgsLayoutScaleBarWidget::textFormatChanged ); mFontButton->setLayer( coverageLayer() ); if ( mScalebar->layout() ) { connect( &mScalebar->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mFontButton, &QgsFontButton::setLayer ); + connect( &mScalebar->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mLineStyleButton, &QgsSymbolButton::setLayer ); + connect( &mScalebar->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mFillSymbol1Button, &QgsSymbolButton::setLayer ); + connect( &mScalebar->layout()->reportContext(), &QgsLayoutReportContext::layerChanged, mFillSymbol2Button, &QgsSymbolButton::setLayer ); } } @@ -183,10 +166,9 @@ bool QgsLayoutScaleBarWidget::setNewItem( QgsLayoutItem *item ) if ( mScalebar ) { connectUpdateSignal(); - mFillColorDDBtn->registerExpressionContextGenerator( mScalebar ); - mFillColor2DDBtn->registerExpressionContextGenerator( mScalebar ); - mLineColorDDBtn->registerExpressionContextGenerator( mScalebar ); - mLineWidthDDBtn->registerExpressionContextGenerator( mScalebar ); + mFillSymbol1Button->registerExpressionContextGenerator( mScalebar ); + mFillSymbol2Button->registerExpressionContextGenerator( mScalebar ); + mLineStyleButton->registerExpressionContextGenerator( mScalebar ); } setGuiElements(); @@ -194,6 +176,39 @@ bool QgsLayoutScaleBarWidget::setNewItem( QgsLayoutItem *item ) return true; } +void QgsLayoutScaleBarWidget::lineSymbolChanged() +{ + if ( !mScalebar ) + return; + + mScalebar->layout()->undoStack()->beginCommand( mScalebar, tr( "Change Scalebar Line Style" ), QgsLayoutItem::UndoShapeStyle ); + mScalebar->setLineSymbol( mLineStyleButton->clonedSymbol() ); + mScalebar->update(); + mScalebar->layout()->undoStack()->endCommand(); +} + +void QgsLayoutScaleBarWidget::fillSymbol1Changed() +{ + if ( !mScalebar ) + return; + + mScalebar->layout()->undoStack()->beginCommand( mScalebar, tr( "Change Scalebar Fill Style" ), QgsLayoutItem::UndoShapeStyle ); + mScalebar->setFillSymbol( mFillSymbol1Button->clonedSymbol() ); + mScalebar->update(); + mScalebar->layout()->undoStack()->endCommand(); +} + +void QgsLayoutScaleBarWidget::fillSymbol2Changed() +{ + if ( !mScalebar ) + return; + + mScalebar->layout()->undoStack()->beginCommand( mScalebar, tr( "Change Scalebar Fill Style" ), QgsLayoutItem::UndoShapeStyle ); + mScalebar->setAlternateFillSymbol( mFillSymbol2Button->clonedSymbol() ); + mScalebar->update(); + mScalebar->layout()->undoStack()->endCommand(); +} + void QgsLayoutScaleBarWidget::setGuiElements() { if ( !mScalebar ) @@ -205,25 +220,23 @@ void QgsLayoutScaleBarWidget::setGuiElements() mNumberOfSegmentsSpinBox->setValue( mScalebar->numberOfSegments() ); mSegmentsLeftSpinBox->setValue( mScalebar->numberOfSegmentsLeft() ); mSegmentSizeSpinBox->setValue( mScalebar->unitsPerSegment() ); - mLineWidthSpinBox->setValue( mScalebar->lineWidth() ); mHeightSpinBox->setValue( mScalebar->height() ); mMapUnitsPerBarUnitSpinBox->setValue( mScalebar->mapUnitsPerScaleBarUnit() ); mLabelBarSpaceSpinBox->setValue( mScalebar->labelBarSpace() ); mBoxSizeSpinBox->setValue( mScalebar->boxContentSpace() ); mUnitLabelLineEdit->setText( mScalebar->unitLabel() ); - mLineJoinStyleCombo->setPenJoinStyle( mScalebar->lineJoinStyle() ); - mLineCapStyleCombo->setPenCapStyle( mScalebar->lineCapStyle() ); - mFillColorButton->setColor( mScalebar->fillColor() ); - mFillColor2Button->setColor( mScalebar->fillColor2() ); - mStrokeColorButton->setColor( mScalebar->lineColor() ); mFontButton->setTextFormat( mScalebar->textFormat() ); + whileBlocking( mLineStyleButton )->setSymbol( mScalebar->lineSymbol()->clone() ); + whileBlocking( mFillSymbol1Button )->setSymbol( mScalebar->fillSymbol()->clone() ); + whileBlocking( mFillSymbol2Button )->setSymbol( mScalebar->alternateFillSymbol()->clone() ); + //map combo box mMapItemComboBox->setItem( mScalebar->linkedMap() ); //style... - QString style = mScalebar->style(); - mStyleComboBox->setCurrentIndex( mStyleComboBox->findText( tr( style.toLocal8Bit().data() ) ) ); + const QString style = mScalebar->style(); + mStyleComboBox->setCurrentIndex( mStyleComboBox->findData( style ) ); toggleStyleSpecificControls( style ); //label vertical/horizontal placement @@ -266,30 +279,11 @@ void QgsLayoutScaleBarWidget::setGuiElements() } mMinWidthSpinBox->setValue( mScalebar->minimumBarWidth() ); mMaxWidthSpinBox->setValue( mScalebar->maximumBarWidth() ); - updateDataDefinedButton( mFillColorDDBtn ); - updateDataDefinedButton( mFillColor2DDBtn ); - updateDataDefinedButton( mLineColorDDBtn ); - updateDataDefinedButton( mLineWidthDDBtn ); blockMemberSignals( false ); } //slots -void QgsLayoutScaleBarWidget::mLineWidthSpinBox_valueChanged( double d ) -{ - if ( !mScalebar ) - { - return; - } - - mScalebar->beginCommand( tr( "Set Scalebar Line Width" ), QgsLayoutItem::UndoScaleBarLineWidth ); - disconnectUpdateSignal(); - mScalebar->setLineWidth( d ); - mScalebar->update(); - connectUpdateSignal(); - mScalebar->endCommand(); -} - void QgsLayoutScaleBarWidget::mSegmentSizeSpinBox_valueChanged( double d ) { if ( !mScalebar ) @@ -387,51 +381,6 @@ void QgsLayoutScaleBarWidget::changeNumberFormat() return; } -void QgsLayoutScaleBarWidget::mFillColorButton_colorChanged( const QColor &newColor ) -{ - if ( !mScalebar ) - { - return; - } - - mScalebar->beginCommand( tr( "Set Scalebar Fill Color" ), QgsLayoutItem::UndoScaleBarFillColor ); - disconnectUpdateSignal(); - mScalebar->setFillColor( newColor ); - mScalebar->update(); - connectUpdateSignal(); - mScalebar->endCommand(); -} - -void QgsLayoutScaleBarWidget::mFillColor2Button_colorChanged( const QColor &newColor ) -{ - if ( !mScalebar ) - { - return; - } - - mScalebar->beginCommand( tr( "Set Scalebar Fill Color" ), QgsLayoutItem::UndoScaleBarFillColor2 ); - disconnectUpdateSignal(); - mScalebar->setFillColor2( newColor ); - mScalebar->update(); - connectUpdateSignal(); - mScalebar->endCommand(); -} - -void QgsLayoutScaleBarWidget::mStrokeColorButton_colorChanged( const QColor &newColor ) -{ - if ( !mScalebar ) - { - return; - } - - mScalebar->beginCommand( tr( "Set Scalebar Stroke Color" ), QgsLayoutItem::UndoScaleBarStrokeColor ); - disconnectUpdateSignal(); - mScalebar->setLineColor( newColor ); - mScalebar->update(); - connectUpdateSignal(); - mScalebar->endCommand(); -} - void QgsLayoutScaleBarWidget::mUnitLabelLineEdit_textChanged( const QString &text ) { if ( !mScalebar ) @@ -462,108 +411,58 @@ void QgsLayoutScaleBarWidget::mMapUnitsPerBarUnitSpinBox_valueChanged( double d mScalebar->endCommand(); } -void QgsLayoutScaleBarWidget::mStyleComboBox_currentIndexChanged( const QString &text ) +void QgsLayoutScaleBarWidget::mStyleComboBox_currentIndexChanged( const QString & ) { if ( !mScalebar ) { return; } + const QString rendererId = mStyleComboBox->currentData().toString(); + if ( rendererId == mScalebar->style() ) + return; + mScalebar->beginCommand( tr( "Set Scalebar Style" ) ); disconnectUpdateSignal(); - QString untranslatedStyleName; - if ( text == tr( "Single Box" ) ) - { - untranslatedStyleName = QStringLiteral( "Single Box" ); - } - else if ( text == tr( "Double Box" ) ) - { - untranslatedStyleName = QStringLiteral( "Double Box" ); - } - else if ( text == tr( "Line Ticks Middle" ) ) - { - untranslatedStyleName = QStringLiteral( "Line Ticks Middle" ); - } - else if ( text == tr( "Line Ticks Middle" ) ) - { - untranslatedStyleName = QStringLiteral( "Line Ticks Middle" ); - } - else if ( text == tr( "Line Ticks Down" ) ) - { - untranslatedStyleName = QStringLiteral( "Line Ticks Down" ); - } - else if ( text == tr( "Line Ticks Up" ) ) - { - untranslatedStyleName = QStringLiteral( "Line Ticks Up" ); - } - else if ( text == tr( "Numeric" ) ) - { - untranslatedStyleName = QStringLiteral( "Numeric" ); - } + + bool defaultsApplied = false; + std::unique_ptr< QgsScaleBarRenderer > renderer( QgsApplication::scaleBarRendererRegistry()->renderer( rendererId ) ); + if ( renderer ) + defaultsApplied = mScalebar->applyDefaultRendererSettings( renderer.get() ); //disable or enable controls which apply to specific scale bar styles - toggleStyleSpecificControls( untranslatedStyleName ); + toggleStyleSpecificControls( rendererId ); - mScalebar->setStyle( untranslatedStyleName ); + mScalebar->setStyle( rendererId ); mScalebar->update(); connectUpdateSignal(); mScalebar->endCommand(); + + if ( defaultsApplied ) + setGuiElements(); } void QgsLayoutScaleBarWidget::toggleStyleSpecificControls( const QString &style ) { - if ( style == QLatin1String( "Numeric" ) ) - { - //Disable controls which don't apply to numeric scale bars - mUnitsComboBox->setEnabled( false ); - mUnitsLabel->setEnabled( false ); - mMapUnitsPerBarUnitSpinBox->setEnabled( false ); - mMapUnitsPerBarUnitLabel->setEnabled( false ); - mUnitLabelLineEdit->setEnabled( false ); - mUnitLabelLabel->setEnabled( false ); - mGroupBoxSegments->setEnabled( false ); + std::unique_ptr< QgsScaleBarRenderer > renderer( QgsApplication::scaleBarRendererRegistry()->renderer( style ) ); + + //Selectively enable controls which apply to the scale bar style + mUnitsComboBox->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagRespectsUnits : true ); + mUnitsLabel->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagRespectsUnits : true ); + mMapUnitsPerBarUnitSpinBox->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagRespectsMapUnitsPerScaleBarUnit : true ); + mMapUnitsPerBarUnitLabel->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagRespectsMapUnitsPerScaleBarUnit : true ); + mUnitLabelLineEdit->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesUnitLabel : true ); + mUnitLabelLabel->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesUnitLabel : true ); + mGroupBoxSegments->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesSegments : true ); + if ( !mGroupBoxUnits->isEnabled() ) mGroupBoxSegments->setCollapsed( true ); - mLabelBarSpaceSpinBox->setEnabled( false ); - mLineWidthSpinBox->setEnabled( false ); - mFillColorButton->setEnabled( false ); - mFillColor2Button->setEnabled( false ); - mStrokeColorButton->setEnabled( false ); - mLineJoinStyleCombo->setEnabled( false ); - mLineCapStyleCombo->setEnabled( false ); - mLabelVerticalPlacementComboBox->setEnabled( false ); - mLabelHorizontalPlacementComboBox->setEnabled( false ); - mAlignmentComboBox->setEnabled( true ); - } - else - { - //Enable controls - mUnitsComboBox->setEnabled( true ); - mUnitsLabel->setEnabled( true ); - mMapUnitsPerBarUnitSpinBox->setEnabled( true ); - mMapUnitsPerBarUnitLabel->setEnabled( true ); - mUnitLabelLineEdit->setEnabled( true ); - mUnitLabelLabel->setEnabled( true ); - mGroupBoxSegments->setEnabled( true ); - mLabelBarSpaceSpinBox->setEnabled( true ); - mLineWidthSpinBox->setEnabled( true ); - mFillColorButton->setEnabled( true ); - mFillColor2Button->setEnabled( true ); - mStrokeColorButton->setEnabled( true ); - mLabelVerticalPlacementComboBox->setEnabled( true ); - mLabelHorizontalPlacementComboBox->setEnabled( true ); - mAlignmentComboBox->setEnabled( false ); - if ( style == QLatin1String( "Single Box" ) || style == QLatin1String( "Double Box" ) ) - { - mLineJoinStyleCombo->setEnabled( true ); - mLineCapStyleCombo->setEnabled( false ); - } - else - { - mLineJoinStyleCombo->setEnabled( false ); - mLineCapStyleCombo->setEnabled( true ); - } - - } + mLabelBarSpaceSpinBox->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesLabelBarSpace : true ); + mLabelVerticalPlacementComboBox->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesLabelVerticalPlacement : true ); + mLabelHorizontalPlacementComboBox->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesLabelHorizontalPlacement : true ); + mAlignmentComboBox->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesAlignment : true ); + mFillSymbol1Button->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesFillSymbol : true ); + mFillSymbol2Button->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesAlternateFillSymbol : true ); + mLineStyleButton->setEnabled( renderer ? renderer->flags() & QgsScaleBarRenderer::Flag::FlagUsesLineSymbol : true ); } void QgsLayoutScaleBarWidget::mLabelBarSpaceSpinBox_valueChanged( double d ) @@ -679,18 +578,15 @@ void QgsLayoutScaleBarWidget::blockMemberSignals( bool block ) mUnitLabelLineEdit->blockSignals( block ); mMapUnitsPerBarUnitSpinBox->blockSignals( block ); mHeightSpinBox->blockSignals( block ); - mLineWidthSpinBox->blockSignals( block ); + mLineStyleButton->blockSignals( block ); mLabelBarSpaceSpinBox->blockSignals( block ); mBoxSizeSpinBox->blockSignals( block ); mLabelVerticalPlacementComboBox->blockSignals( block ); mLabelHorizontalPlacementComboBox->blockSignals( block ); mAlignmentComboBox->blockSignals( block ); mUnitsComboBox->blockSignals( block ); - mLineJoinStyleCombo->blockSignals( block ); - mLineCapStyleCombo->blockSignals( block ); - mFillColorButton->blockSignals( block ); - mFillColor2Button->blockSignals( block ); - mStrokeColorButton->blockSignals( block ); + mFillSymbol1Button->blockSignals( block ); + mFillSymbol2Button->blockSignals( block ); mSegmentSizeRadioGroup.blockSignals( block ); mMapItemComboBox->blockSignals( block ); mFontButton->blockSignals( block ); @@ -714,32 +610,6 @@ void QgsLayoutScaleBarWidget::disconnectUpdateSignal() } } -void QgsLayoutScaleBarWidget::mLineJoinStyleCombo_currentIndexChanged( int index ) -{ - Q_UNUSED( index ) - if ( !mScalebar ) - { - return; - } - - mScalebar->beginCommand( tr( "Set Scalebar Join Style" ) ); - mScalebar->setLineJoinStyle( mLineJoinStyleCombo->penJoinStyle() ); - mScalebar->endCommand(); -} - -void QgsLayoutScaleBarWidget::mLineCapStyleCombo_currentIndexChanged( int index ) -{ - Q_UNUSED( index ) - if ( !mScalebar ) - { - return; - } - - mScalebar->beginCommand( tr( "Set Scalebar Cap Style" ) ); - mScalebar->setLineCapStyle( mLineCapStyleCombo->penCapStyle() ); - mScalebar->endCommand(); -} - void QgsLayoutScaleBarWidget::segmentSizeRadioChanged( QAbstractButton *radio ) { bool fixedSizeMode = radio == mFixedSizeRadio; diff --git a/src/gui/layout/qgslayoutscalebarwidget.h b/src/gui/layout/qgslayoutscalebarwidget.h index c7fed989b7bb..2bb6c21c73a5 100644 --- a/src/gui/layout/qgslayoutscalebarwidget.h +++ b/src/gui/layout/qgslayoutscalebarwidget.h @@ -50,17 +50,15 @@ class GUI_EXPORT QgsLayoutScaleBarWidget: public QgsLayoutItemBaseWidget, public bool setNewItem( QgsLayoutItem *item ) override; private slots: - + void lineSymbolChanged(); + void fillSymbol1Changed(); + void fillSymbol2Changed(); void mHeightSpinBox_valueChanged( double d ); - void mLineWidthSpinBox_valueChanged( double d ); void mSegmentSizeSpinBox_valueChanged( double d ); void mSegmentsLeftSpinBox_valueChanged( int i ); void mNumberOfSegmentsSpinBox_valueChanged( int i ); void mUnitLabelLineEdit_textChanged( const QString &text ); void mMapUnitsPerBarUnitSpinBox_valueChanged( double d ); - void mFillColorButton_colorChanged( const QColor &newColor ); - void mFillColor2Button_colorChanged( const QColor &newColor ); - void mStrokeColorButton_colorChanged( const QColor &newColor ); void mStyleComboBox_currentIndexChanged( const QString &text ); void mLabelBarSpaceSpinBox_valueChanged( double d ); void mBoxSizeSpinBox_valueChanged( double d ); @@ -68,8 +66,6 @@ class GUI_EXPORT QgsLayoutScaleBarWidget: public QgsLayoutItemBaseWidget, public void mLabelHorizontalPlacementComboBox_currentIndexChanged( int index ); void alignmentChanged(); void mUnitsComboBox_currentIndexChanged( int index ); - void mLineJoinStyleCombo_currentIndexChanged( int index ); - void mLineCapStyleCombo_currentIndexChanged( int index ); void mMinWidthSpinBox_valueChanged( double d ); void mMaxWidthSpinBox_valueChanged( double d ); diff --git a/src/gui/layout/qgslayoutviewtooladditem.cpp b/src/gui/layout/qgslayoutviewtooladditem.cpp index 77f0d3022879..edbf80ea9866 100644 --- a/src/gui/layout/qgslayoutviewtooladditem.cpp +++ b/src/gui/layout/qgslayoutviewtooladditem.cpp @@ -53,6 +53,7 @@ void QgsLayoutViewToolAddItem::layoutPressEvent( QgsLayoutViewMouseEvent *event mDrawing = true; mMousePressStartPos = event->pos(); + mMousePressStartLayoutPos = event->layoutPoint(); mRubberBand.reset( QgsGui::layoutItemGuiRegistry()->createItemRubberBand( mItemMetadataId, view() ) ); if ( mRubberBand ) { @@ -85,7 +86,7 @@ void QgsLayoutViewToolAddItem::layoutReleaseEvent( QgsLayoutViewMouseEvent *even } mDrawing = false; - QRectF rect = mRubberBand->finish( event->snappedPoint(), event->modifiers() ); + QRectF rect = mRubberBand ? mRubberBand->finish( event->snappedPoint(), event->modifiers() ) : QRectF(); QString undoText; if ( QgsLayoutItemAbstractGuiMetadata *metadata = QgsGui::layoutItemGuiRegistry()->itemMetadata( mItemMetadataId ) ) @@ -107,7 +108,7 @@ void QgsLayoutViewToolAddItem::layoutReleaseEvent( QgsLayoutViewMouseEvent *even // click? or click-and-drag? bool clickOnly = !isClickAndDrag( mMousePressStartPos, event->pos() ); - if ( clickOnly ) + if ( clickOnly && mRubberBand ) { QgsLayoutItemPropertiesDialog dlg( view() ); dlg.setLayout( layout() ); @@ -125,17 +126,25 @@ void QgsLayoutViewToolAddItem::layoutReleaseEvent( QgsLayoutViewMouseEvent *even return; } } - else + else if ( mRubberBand ) { item->attemptResize( QgsLayoutSize( rect.width(), rect.height(), QgsUnitTypes::LayoutMillimeters ) ); item->attemptMove( QgsLayoutPoint( rect.left(), rect.top(), QgsUnitTypes::LayoutMillimeters ) ); } + else + { + // item type doesn't use rubber bands -- e.g. marker items + item->attemptMove( QgsLayoutPoint( mMousePressStartLayoutPos, layout()->units() ) ); + } // record last created item size - QgsSettings settings; - settings.setValue( QStringLiteral( "LayoutDesigner/lastItemWidth" ), item->sizeWithUnits().width() ); - settings.setValue( QStringLiteral( "LayoutDesigner/lastItemHeight" ), item->sizeWithUnits().height() ); - settings.setEnumValue( QStringLiteral( "LayoutDesigner/lastSizeUnit" ), item->sizeWithUnits().units() ); + if ( mRubberBand ) + { + QgsSettings settings; + settings.setValue( QStringLiteral( "LayoutDesigner/lastItemWidth" ), item->sizeWithUnits().width() ); + settings.setValue( QStringLiteral( "LayoutDesigner/lastItemHeight" ), item->sizeWithUnits().height() ); + settings.setEnumValue( QStringLiteral( "LayoutDesigner/lastSizeUnit" ), item->sizeWithUnits().units() ); + } QgsGui::layoutItemGuiRegistry()->newItemAddedToLayout( mItemMetadataId, item ); @@ -154,7 +163,8 @@ void QgsLayoutViewToolAddItem::deactivate() if ( mDrawing ) { // canceled mid operation - mRubberBand->finish(); + if ( mRubberBand ) + mRubberBand->finish(); mDrawing = false; } QgsLayoutViewTool::deactivate(); diff --git a/src/gui/layout/qgslayoutviewtooladditem.h b/src/gui/layout/qgslayoutviewtooladditem.h index e314f52906d2..1c3c891bd499 100644 --- a/src/gui/layout/qgslayoutviewtooladditem.h +++ b/src/gui/layout/qgslayoutviewtooladditem.h @@ -77,6 +77,9 @@ class GUI_EXPORT QgsLayoutViewToolAddItem : public QgsLayoutViewTool //! Start position for mouse press QPoint mMousePressStartPos; + //! Start position for mouse press in layout coordinates + QPointF mMousePressStartLayoutPos; + //! Start of rubber band creation QPointF mRubberBandStartPos; diff --git a/src/gui/locator/qgslocatorwidget.cpp b/src/gui/locator/qgslocatorwidget.cpp index 70425140aae9..21c93a8bda7d 100644 --- a/src/gui/locator/qgslocatorwidget.cpp +++ b/src/gui/locator/qgslocatorwidget.cpp @@ -144,11 +144,19 @@ void QgsLocatorWidget::setMapCanvas( QgsMapCanvas *canvas ) void QgsLocatorWidget::search( const QString &string ) { - mLineEdit->setText( string ); window()->activateWindow(); // window must also be active - otherwise floating docks can steal keystrokes - scheduleDelayedPopup(); - mLineEdit->setFocus(); - performSearch(); + if ( string.isEmpty() ) + { + mLineEdit->setFocus(); + mLineEdit->selectAll(); + } + else + { + scheduleDelayedPopup(); + mLineEdit->setFocus(); + mLineEdit->setText( string ); + performSearch(); + } } void QgsLocatorWidget::invalidateResults() diff --git a/src/gui/numericformats/qgsnumericformatguiregistry.cpp b/src/gui/numericformats/qgsnumericformatguiregistry.cpp index 07a03bc5edc5..7201f259d2bd 100644 --- a/src/gui/numericformats/qgsnumericformatguiregistry.cpp +++ b/src/gui/numericformats/qgsnumericformatguiregistry.cpp @@ -69,6 +69,16 @@ class QgsScientificNumericFormatConfigurationWidgetFactory : public QgsNumericFo return new QgsScientificNumericFormatWidget( format ); } }; + +class QgsFractionNumericFormatConfigurationWidgetFactory : public QgsNumericFormatConfigurationWidgetFactory +{ + public: + + QgsNumericFormatWidget *create( const QgsNumericFormat *format ) const + { + return new QgsFractionNumericFormatWidget( format ); + } +}; ///@endcond QgsNumericFormatGuiRegistry::QgsNumericFormatGuiRegistry() @@ -78,6 +88,7 @@ QgsNumericFormatGuiRegistry::QgsNumericFormatGuiRegistry() addFormatConfigurationWidgetFactory( QStringLiteral( "currency" ), new QgsCurrencyNumericFormatConfigurationWidgetFactory() ); addFormatConfigurationWidgetFactory( QStringLiteral( "percentage" ), new QgsPercentageNumericFormatConfigurationWidgetFactory() ); addFormatConfigurationWidgetFactory( QStringLiteral( "scientific" ), new QgsScientificNumericFormatConfigurationWidgetFactory() ); + addFormatConfigurationWidgetFactory( QStringLiteral( "fraction" ), new QgsFractionNumericFormatConfigurationWidgetFactory() ); } QgsNumericFormatGuiRegistry::~QgsNumericFormatGuiRegistry() diff --git a/src/gui/numericformats/qgsnumericformatselectorwidget.cpp b/src/gui/numericformats/qgsnumericformatselectorwidget.cpp index e8653070b03a..3f09a6e775fb 100644 --- a/src/gui/numericformats/qgsnumericformatselectorwidget.cpp +++ b/src/gui/numericformats/qgsnumericformatselectorwidget.cpp @@ -142,7 +142,6 @@ void QgsNumericFormatSelectorWidget::updateFormatWidget() stackedWidget->setCurrentWidget( w ); // start receiving updates from widget connect( w, &QgsNumericFormatWidget::changed, this, &QgsNumericFormatSelectorWidget::formatChanged ); - return; } else { diff --git a/src/gui/numericformats/qgsnumericformatwidget.cpp b/src/gui/numericformats/qgsnumericformatwidget.cpp index 272a5841a47a..b8f438a76615 100644 --- a/src/gui/numericformats/qgsnumericformatwidget.cpp +++ b/src/gui/numericformats/qgsnumericformatwidget.cpp @@ -19,6 +19,7 @@ #include "qgspercentagenumericformat.h" #include "qgsbearingnumericformat.h" #include "qgsscientificnumericformat.h" +#include "qgsfractionnumericformat.h" #include "qgsgui.h" #include "qgis.h" #include @@ -410,3 +411,71 @@ QgsNumericFormat *QgsScientificNumericFormatWidget::format() } + +// +// QgsFractionNumericFormatWidget +// +QgsFractionNumericFormatWidget::QgsFractionNumericFormatWidget( const QgsNumericFormat *format, QWidget *parent ) + : QgsNumericFormatWidget( parent ) +{ + setupUi( this ); + setFormat( format->clone() ); + + mThousandsLineEdit->setShowClearButton( true ); + + connect( mUseDedicatedUnicodeCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) + { + mFormat->setUseDedicatedUnicodeCharacters( checked ); + if ( !mBlockSignals ) + emit changed(); + } ); + + connect( mUseUnicodeSupersubscriptCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) + { + mFormat->setUseUnicodeSuperSubscript( checked ); + if ( !mBlockSignals ) + emit changed(); + } ); + + connect( mShowPlusCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) + { + mFormat->setShowPlusSign( checked ); + if ( !mBlockSignals ) + emit changed(); + } ); + + connect( mShowThousandsCheckBox, &QCheckBox::toggled, this, [ = ]( bool checked ) + { + mFormat->setShowThousandsSeparator( checked ); + if ( !mBlockSignals ) + emit changed(); + } ); + + connect( mThousandsLineEdit, &QLineEdit::textChanged, this, [ = ]( const QString & text ) + { + mFormat->setThousandsSeparator( text.isEmpty() ? QChar() : text.at( 0 ) ); + if ( !mBlockSignals ) + emit changed(); + } ); + +} + +QgsFractionNumericFormatWidget::~QgsFractionNumericFormatWidget() = default; + +void QgsFractionNumericFormatWidget::setFormat( QgsNumericFormat *format ) +{ + mFormat.reset( static_cast< QgsFractionNumericFormat * >( format ) ); + + mBlockSignals = true; + mUseDedicatedUnicodeCheckBox->setChecked( mFormat->useDedicatedUnicodeCharacters() ); + mUseUnicodeSupersubscriptCheckBox->setChecked( mFormat->useUnicodeSuperSubscript() ); + mShowPlusCheckBox->setChecked( mFormat->showPlusSign() ); + mShowThousandsCheckBox->setChecked( mFormat->showThousandsSeparator() ); + mThousandsLineEdit->setText( mFormat->thousandsSeparator().isNull() ? QString() : mFormat->thousandsSeparator() ); + mBlockSignals = false; +} + +QgsNumericFormat *QgsFractionNumericFormatWidget::format() +{ + return mFormat->clone(); +} diff --git a/src/gui/numericformats/qgsnumericformatwidget.h b/src/gui/numericformats/qgsnumericformatwidget.h index 59caa4b32274..df7a20ee7ee1 100644 --- a/src/gui/numericformats/qgsnumericformatwidget.h +++ b/src/gui/numericformats/qgsnumericformatwidget.h @@ -21,6 +21,8 @@ #include #include +class QgsFractionNumericFormat; + /** * \ingroup gui * \class QgsNumericFormatWidget @@ -262,4 +264,33 @@ class GUI_EXPORT QgsScientificNumericFormatWidget : public QgsNumericFormatWidge }; +#include "ui_qgsfractionnumericformatwidgetbase.h" + +/** + * \ingroup gui + * \class QgsFractionNumericFormatWidget + * A widget which allow control over the properties of a QgsFractionNumericFormat. + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsFractionNumericFormatWidget : public QgsNumericFormatWidget, private Ui::QgsFractionNumericFormatWidgetBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsFractionNumericFormatWidget, initially showing the specified \a format. + */ + QgsFractionNumericFormatWidget( const QgsNumericFormat *format, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + ~QgsFractionNumericFormatWidget() override; + + void setFormat( QgsNumericFormat *format ) override; + + QgsNumericFormat *format() override SIP_FACTORY; + + private: + std::unique_ptr< QgsFractionNumericFormat > mFormat; + bool mBlockSignals = false; + +}; #endif // QGSNUMERICFORMATWIDGET_H diff --git a/src/gui/ogr/qgsvectorlayersaveasdialog.cpp b/src/gui/ogr/qgsvectorlayersaveasdialog.cpp index b23f9ed847d2..a53ab4b69c75 100644 --- a/src/gui/ogr/qgsvectorlayersaveasdialog.cpp +++ b/src/gui/ogr/qgsvectorlayersaveasdialog.cpp @@ -19,6 +19,7 @@ #include "qgsvectorlayersaveasdialog.h" #include "qgsprojectionselectiondialog.h" #include "qgsvectordataprovider.h" +#include "qgsogrdataitems.h" #include "qgscoordinatereferencesystem.h" #include "qgseditorwidgetfactory.h" #include "qgseditorwidgetregistry.h" @@ -274,97 +275,58 @@ void QgsVectorLayerSaveAsDialog::accept() QgsVectorFileWriter::editionCapabilities( filename() ); bool layerExists = QgsVectorFileWriter::targetLayerExists( filename(), layername() ); + QMessageBox msgBox; + msgBox.setIcon( QMessageBox::Question ); + msgBox.setWindowTitle( tr( "Save Vector Layer As" ) ); + QPushButton *overwriteFileButton = msgBox.addButton( tr( "Overwrite File" ), QMessageBox::ActionRole ); + QPushButton *overwriteLayerButton = msgBox.addButton( tr( "Overwrite Layer" ), QMessageBox::ActionRole ); + QPushButton *appendToLayerButton = msgBox.addButton( tr( "Append to Layer" ), QMessageBox::ActionRole ); + msgBox.setStandardButtons( QMessageBox::Cancel ); + msgBox.setDefaultButton( QMessageBox::Cancel ); + overwriteFileButton->hide(); + overwriteLayerButton->hide(); + appendToLayerButton->hide(); if ( layerExists ) { if ( !( caps & QgsVectorFileWriter::CanAppendToExistingLayer ) && ( caps & QgsVectorFileWriter::CanDeleteLayer ) && ( caps & QgsVectorFileWriter::CanAddNewLayer ) ) { - QMessageBox msgBox; - msgBox.setIcon( QMessageBox::Question ); - msgBox.setWindowTitle( tr( "Save Vector Layer As" ) ); msgBox.setText( tr( "The layer already exists. Do you want to overwrite the whole file or overwrite the layer?" ) ); - QPushButton *overwriteFileButton = msgBox.addButton( tr( "Overwrite File" ), QMessageBox::ActionRole ); - QPushButton *overwriteLayerButton = msgBox.addButton( tr( "Overwrite Layer" ), QMessageBox::ActionRole ); - msgBox.setStandardButtons( QMessageBox::Cancel ); - msgBox.setDefaultButton( QMessageBox::Cancel ); - int ret = msgBox.exec(); - if ( ret == QMessageBox::Cancel ) - return; - if ( msgBox.clickedButton() == overwriteFileButton ) - mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; - else if ( msgBox.clickedButton() == overwriteLayerButton ) - mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteLayer; + overwriteFileButton->setVisible( true ); + overwriteLayerButton->setVisible( true ); } else if ( !( caps & QgsVectorFileWriter::CanAppendToExistingLayer ) ) { - if ( QMessageBox::question( this, - tr( "Save Vector Layer As" ), - tr( "The file already exists. Do you want to overwrite it?" ) ) == QMessageBox::NoButton ) - { - return; - } - mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; + msgBox.setText( tr( "The file already exists. Do you want to overwrite it?" ) ); + overwriteFileButton->setVisible( true ); } else if ( ( caps & QgsVectorFileWriter::CanDeleteLayer ) && ( caps & QgsVectorFileWriter::CanAddNewLayer ) ) { - QMessageBox msgBox; - msgBox.setIcon( QMessageBox::Question ); - msgBox.setWindowTitle( tr( "Save Vector Layer As" ) ); msgBox.setText( tr( "The layer already exists. Do you want to overwrite the whole file, overwrite the layer or append features to the layer?" ) ); - QPushButton *overwriteFileButton = msgBox.addButton( tr( "Overwrite File" ), QMessageBox::ActionRole ); - QPushButton *overwriteLayerButton = msgBox.addButton( tr( "Overwrite Layer" ), QMessageBox::ActionRole ); - QPushButton *appendToLayerButton = msgBox.addButton( tr( "Append to Layer" ), QMessageBox::ActionRole ); - msgBox.setStandardButtons( QMessageBox::Cancel ); - msgBox.setDefaultButton( QMessageBox::Cancel ); - int ret = msgBox.exec(); - if ( ret == QMessageBox::Cancel ) - return; - if ( msgBox.clickedButton() == overwriteFileButton ) - mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; - else if ( msgBox.clickedButton() == overwriteLayerButton ) - mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteLayer; - else if ( msgBox.clickedButton() == appendToLayerButton ) - mActionOnExistingFile = QgsVectorFileWriter::AppendToLayerNoNewFields; + appendToLayerButton->setVisible( true ); + overwriteFileButton->setVisible( true ); + overwriteLayerButton->setVisible( true ); } else { - QMessageBox msgBox; - msgBox.setIcon( QMessageBox::Question ); - msgBox.setWindowTitle( tr( "Save Vector Layer As" ) ); msgBox.setText( tr( "The layer already exists. Do you want to overwrite the whole file or append features to the layer?" ) ); - QPushButton *overwriteFileButton = msgBox.addButton( tr( "Overwrite File" ), QMessageBox::ActionRole ); - QPushButton *appendToLayerButton = msgBox.addButton( tr( "Append to Layer" ), QMessageBox::ActionRole ); - msgBox.setStandardButtons( QMessageBox::Cancel ); - msgBox.setDefaultButton( QMessageBox::Cancel ); - int ret = msgBox.exec(); - if ( ret == QMessageBox::Cancel ) - return; - if ( msgBox.clickedButton() == overwriteFileButton ) - mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; - else if ( msgBox.clickedButton() == appendToLayerButton ) - mActionOnExistingFile = QgsVectorFileWriter::AppendToLayerNoNewFields; - } - - if ( mActionOnExistingFile == QgsVectorFileWriter::AppendToLayerNoNewFields ) - { - if ( QgsVectorFileWriter::areThereNewFieldsToCreate( filename(), - layername(), - mLayer, - selectedAttributes() ) ) - { - if ( QMessageBox::question( this, - tr( "Save Vector Layer As" ), - tr( "The existing layer has different fields. Do you want to add the missing fields to the layer?" ) ) == QMessageBox::Yes ) - { - mActionOnExistingFile = QgsVectorFileWriter::AppendToLayerAddFields; - } - } + appendToLayerButton->setVisible( true ); + overwriteFileButton->setVisible( true ); } + int ret = msgBox.exec(); + if ( ret == QMessageBox::Cancel ) + return; + if ( msgBox.clickedButton() == overwriteFileButton ) + mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteFile; + else if ( msgBox.clickedButton() == overwriteLayerButton ) + mActionOnExistingFile = QgsVectorFileWriter::CreateOrOverwriteLayer; + else if ( msgBox.clickedButton() == appendToLayerButton ) + mActionOnExistingFile = QgsVectorFileWriter::AppendToLayerNoNewFields; } - else + else // !layerExists { if ( ( caps & QgsVectorFileWriter::CanAddNewLayer ) ) { @@ -372,6 +334,7 @@ void QgsVectorLayerSaveAsDialog::accept() } else { + // should not reach here, layer does not exist and cannot add new layer if ( QMessageBox::question( this, tr( "Save Vector Layer As" ), tr( "The file already exists. Do you want to overwrite it?" ) ) == QMessageBox::NoButton ) @@ -383,6 +346,41 @@ void QgsVectorLayerSaveAsDialog::accept() } } + if ( mActionOnExistingFile == QgsVectorFileWriter::AppendToLayerNoNewFields ) + { + if ( QgsVectorFileWriter::areThereNewFieldsToCreate( filename(), layername(), mLayer, selectedAttributes() ) ) + { + if ( QMessageBox::question( this, + tr( "Save Vector Layer As" ), + tr( "The existing layer has additional fields. Do you want to add the missing fields to the layer?" ) ) == QMessageBox::Yes ) + { + mActionOnExistingFile = QgsVectorFileWriter::AppendToLayerAddFields; + } + } + } + else if ( mActionOnExistingFile == QgsVectorFileWriter::CreateOrOverwriteFile ) + { + const QList subLayers = QgsOgrLayerItem::subLayers( filename(), format() ); + QStringList layerList; + for ( const QgsOgrDbLayerInfo *layer : subLayers ) + { + layerList.append( layer->name() ); + } + qDeleteAll( subLayers ); + if ( layerList.length() > 1 ) + { + layerList.sort( Qt::CaseInsensitive ); + QMessageBox msgBox; + msgBox.setIcon( QMessageBox::Warning ); + msgBox.setWindowTitle( tr( "Overwrite File" ) ); + msgBox.setText( tr( "This file contains %1 layers that will be lost!\n" ).arg( QString::number( layerList.length() ) ) ); + msgBox.setDetailedText( tr( "The following layers will be permanently lost:\n\n%1" ).arg( layerList.join( "\n" ) ) ); + msgBox.setStandardButtons( QMessageBox::Ok | QMessageBox::Cancel ); + if ( msgBox.exec() == QMessageBox::Cancel ) + return; + } + } + QgsSettings settings; settings.setValue( QStringLiteral( "UI/lastVectorFileFilterDir" ), QFileInfo( filename() ).absolutePath() ); settings.setValue( QStringLiteral( "UI/lastVectorFormat" ), format() ); @@ -412,7 +410,11 @@ void QgsVectorLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( int idx ) } bool selectAllFields = true; - bool fieldsAsDisplayedValues = false; + + // Is it a format for which fields that have attached widgets of types + // ValueMap, ValueRelation, etc. should be by default exported with their displayed + // values + bool isFormatForFieldsAsDisplayedValues = false; const QString sFormat( format() ); if ( sFormat == QLatin1String( "DXF" ) || sFormat == QLatin1String( "DGN" ) ) @@ -423,8 +425,13 @@ void QgsVectorLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( int idx ) else { if ( mOptions & Fields ) + { mAttributesSelection->setVisible( true ); - fieldsAsDisplayedValues = ( sFormat == QLatin1String( "CSV" ) || sFormat == QLatin1String( "XLS" ) || sFormat == QLatin1String( "XLSX" ) || sFormat == QLatin1String( "ODS" ) ); + isFormatForFieldsAsDisplayedValues = ( sFormat == QLatin1String( "CSV" ) || + sFormat == QLatin1String( "XLS" ) || + sFormat == QLatin1String( "XLSX" ) || + sFormat == QLatin1String( "ODS" ) ); + } } // Show symbology options only for some formats @@ -487,6 +494,7 @@ void QgsVectorLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( int idx ) mAttributeTableItemChangedSlotEnabled = false; + bool checkReplaceRawFieldValues = selectAllFields && isFormatForFieldsAsDisplayedValues; for ( int i = 0; i < mLayer->fields().size(); ++i ) { QgsField fld = mLayer->fields().at( i ); @@ -505,13 +513,21 @@ void QgsVectorLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( int idx ) { const QgsEditorWidgetSetup setup = QgsGui::editorWidgetRegistry()->findBest( mLayer, mLayer->fields()[i].name() ); QgsEditorWidgetFactory *factory = nullptr; + const QString widgetId( setup.type() ); if ( flags == Qt::ItemIsEnabled && - setup.type() != QLatin1String( "TextEdit" ) && - ( factory = QgsGui::editorWidgetRegistry()->factory( setup.type() ) ) ) + widgetId != QLatin1String( "TextEdit" ) && + ( factory = QgsGui::editorWidgetRegistry()->factory( widgetId ) ) ) { item = new QTableWidgetItem( tr( "Use %1" ).arg( factory->name() ) ); item->setFlags( ( selectAllFields ) ? ( Qt::ItemIsEnabled | Qt::ItemIsUserCheckable ) : Qt::ItemIsUserCheckable ); - item->setCheckState( ( selectAllFields && fieldsAsDisplayedValues ) ? Qt::Checked : Qt::Unchecked ); + const bool checkItem = ( selectAllFields && isFormatForFieldsAsDisplayedValues && + ( widgetId == QLatin1String( "ValueMap" ) || + widgetId == QLatin1String( "ValueRelation" ) || + widgetId == QLatin1String( "CheckBox" ) || + widgetId == QLatin1String( "RelationReference" ) ) ); + checkReplaceRawFieldValues &= checkItem; + item->setCheckState( checkItem ? + Qt::Checked : Qt::Unchecked ); mAttributeTable->setItem( i, COLUMN_IDX_EXPORT_AS_DISPLAYED_VALUE, item ); } else @@ -526,7 +542,7 @@ void QgsVectorLayerSaveAsDialog::mFormatComboBox_currentIndexChanged( int idx ) mAttributeTableItemChangedSlotEnabled = true; mReplaceRawFieldValuesStateChangedSlotEnabled = false; - mReplaceRawFieldValues->setChecked( selectAllFields && fieldsAsDisplayedValues ); + mReplaceRawFieldValues->setChecked( checkReplaceRawFieldValues ); mReplaceRawFieldValuesStateChangedSlotEnabled = true; mReplaceRawFieldValues->setEnabled( selectAllFields ); mReplaceRawFieldValues->setVisible( foundFieldThatCanBeExportedAsDisplayedValue ); diff --git a/src/gui/processing/models/qgsmodelarrowitem.cpp b/src/gui/processing/models/qgsmodelarrowitem.cpp index 320edd6bb565..7e5e16004578 100644 --- a/src/gui/processing/models/qgsmodelarrowitem.cpp +++ b/src/gui/processing/models/qgsmodelarrowitem.cpp @@ -24,14 +24,17 @@ ///@cond NOT_STABLE -QgsModelArrowItem::QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex ) +QgsModelArrowItem::QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, bool startIsOutgoing, + QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex, bool endIsIncoming ) : QObject( nullptr ) , mStartItem( startItem ) , mStartEdge( startEdge ) , mStartIndex( startIndex ) + , mStartIsOutgoing( startIsOutgoing ) , mEndItem( endItem ) , mEndEdge( endEdge ) , mEndIndex( endIndex ) + , mEndIsIncoming( endIsIncoming ) { setCacheMode( QGraphicsItem::DeviceCoordinateCache ); setFlag( QGraphicsItem::ItemIsSelectable, false ); @@ -48,17 +51,17 @@ QgsModelArrowItem::QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Q } QgsModelArrowItem::QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, QgsModelComponentGraphicItem *endItem ) - : QgsModelArrowItem( startItem, startEdge, startIndex, endItem, Qt::LeftEdge, -1 ) + : QgsModelArrowItem( startItem, startEdge, startIndex, true, endItem, Qt::LeftEdge, -1, true ) { } QgsModelArrowItem::QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex ) - : QgsModelArrowItem( startItem, Qt::LeftEdge, -1, endItem, endEdge, endIndex ) + : QgsModelArrowItem( startItem, Qt::LeftEdge, -1, true, endItem, endEdge, endIndex, true ) { } QgsModelArrowItem::QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, QgsModelComponentGraphicItem *endItem ) - : QgsModelArrowItem( startItem, Qt::LeftEdge, -1, endItem, Qt::LeftEdge, -1 ) + : QgsModelArrowItem( startItem, Qt::LeftEdge, -1, true, endItem, Qt::LeftEdge, -1, true ) { } @@ -107,14 +110,14 @@ void QgsModelArrowItem::updatePath() bool hasStartPt = false; if ( mStartIndex != -1 ) { - startPt = mStartItem->linkPoint( mStartEdge, mStartIndex ); + startPt = mStartItem->linkPoint( mStartEdge, mStartIndex, !mStartIsOutgoing ); hasStartPt = true; } QPointF endPt; bool hasEndPt = false; if ( mEndIndex != -1 ) { - endPt = mEndItem->linkPoint( mEndEdge, mEndIndex ); + endPt = mEndItem->linkPoint( mEndEdge, mEndIndex, mEndIsIncoming ); hasEndPt = true; } @@ -129,13 +132,13 @@ void QgsModelArrowItem::updatePath() controlPoints.append( pt ); mNodePoints.append( pt ); - controlPoints.append( bezierPointForCurve( pt, startEdge ) ); + controlPoints.append( bezierPointForCurve( pt, startEdge, !mStartIsOutgoing ) ); } else { mNodePoints.append( mStartItem->pos() + startPt ); controlPoints.append( mStartItem->pos() + startPt ); - controlPoints.append( bezierPointForCurve( mStartItem->pos() + startPt, mStartEdge == Qt::BottomEdge ? Qt::RightEdge : Qt::LeftEdge ) ); + controlPoints.append( bezierPointForCurve( mStartItem->pos() + startPt, mStartEdge == Qt::BottomEdge ? Qt::RightEdge : Qt::LeftEdge, !mStartIsOutgoing ) ); } if ( !hasEndPt ) @@ -147,14 +150,14 @@ void QgsModelArrowItem::updatePath() else pt = mEndItem->calculateAutomaticLinkPoint( startPt + mStartItem->pos(), endEdge ); - controlPoints.append( bezierPointForCurve( pt, endEdge ) ); + controlPoints.append( bezierPointForCurve( pt, endEdge, mEndIsIncoming ) ); controlPoints.append( pt ); mNodePoints.append( pt ); } else { mNodePoints.append( mEndItem->pos() + endPt ); - controlPoints.append( bezierPointForCurve( mEndItem->pos() + endPt, mEndEdge == Qt::BottomEdge ? Qt::RightEdge : Qt::LeftEdge ) ); + controlPoints.append( bezierPointForCurve( mEndItem->pos() + endPt, mEndEdge == Qt::BottomEdge ? Qt::RightEdge : Qt::LeftEdge, mEndIsIncoming ) ); controlPoints.append( mEndItem->pos() + endPt ); } @@ -164,21 +167,21 @@ void QgsModelArrowItem::updatePath() setPath( path ); } -QPointF QgsModelArrowItem::bezierPointForCurve( const QPointF &point, Qt::Edge edge ) const +QPointF QgsModelArrowItem::bezierPointForCurve( const QPointF &point, Qt::Edge edge, bool incoming ) const { switch ( edge ) { case Qt::LeftEdge: - return point + QPointF( -50, 0 ); + return point + QPointF( incoming ? -50 : 50, 0 ); case Qt::RightEdge: - return point + QPointF( 50, 0 ); + return point + QPointF( incoming ? -50 : 50, 0 ); case Qt::TopEdge: - return point + QPointF( 0, -30 ); + return point + QPointF( 0, incoming ? -30 : 30 ); case Qt::BottomEdge: - return point + QPointF( 0, 30 ); + return point + QPointF( 0, incoming ? -30 : 30 ); } return QPointF(); } diff --git a/src/gui/processing/models/qgsmodelarrowitem.h b/src/gui/processing/models/qgsmodelarrowitem.h index ca74c2041a28..96acc9011391 100644 --- a/src/gui/processing/models/qgsmodelarrowitem.h +++ b/src/gui/processing/models/qgsmodelarrowitem.h @@ -43,8 +43,8 @@ class GUI_EXPORT QgsModelArrowItem : public QObject, public QGraphicsPathItem * The arrow will link \a startItem to \a endItem, joining the specified \a startEdge and \a startIndex * to \a endEdge and \a endIndex. */ - QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, - QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex ); + QgsModelArrowItem( QgsModelComponentGraphicItem *startItem, Qt::Edge startEdge, int startIndex, bool startIsOutgoing, + QgsModelComponentGraphicItem *endItem, Qt::Edge endEdge, int endIndex, bool endIsIncoming ); /** * Constructor for QgsModelArrowItem, with the specified \a parent item. @@ -88,15 +88,17 @@ class GUI_EXPORT QgsModelArrowItem : public QObject, public QGraphicsPathItem private: - QPointF bezierPointForCurve( const QPointF &point, Qt::Edge edge ) const; + QPointF bezierPointForCurve( const QPointF &point, Qt::Edge edge, bool incoming ) const; QgsModelComponentGraphicItem *mStartItem = nullptr; Qt::Edge mStartEdge = Qt::LeftEdge; int mStartIndex = -1; + bool mStartIsOutgoing = true; QgsModelComponentGraphicItem *mEndItem = nullptr; Qt::Edge mEndEdge = Qt::LeftEdge; int mEndIndex = -1; + bool mEndIsIncoming = false; QList< QPointF > mNodePoints; diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp index 30561e10ef90..adbd3b3f6a8a 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.cpp @@ -18,6 +18,7 @@ #include "qgsprocessingmodelparameter.h" #include "qgsprocessingmodelchildalgorithm.h" #include "qgsprocessingmodeloutput.h" +#include "qgsprocessingmodelgroupbox.h" #include "qgsmodelgraphicsscene.h" #include "qgsapplication.h" #include "qgsmodelgraphicitem.h" @@ -25,6 +26,7 @@ #include "qgsmodelgraphicsview.h" #include "qgsmodelviewtool.h" #include "qgsmodelviewmouseevent.h" +#include "qgsmodelgroupboxdefinitionwidget.h" #include #include @@ -128,32 +130,67 @@ void QgsModelComponentGraphicItem::previewItemMove( qreal dx, qreal dy ) emit updateArrowPaths(); } -void QgsModelComponentGraphicItem::setItemRect( QRectF ) +void QgsModelComponentGraphicItem::setItemRect( QRectF rect ) { - mComponent->setPosition( pos() ); + rect = rect.normalized(); + + if ( rect.width() < MIN_COMPONENT_WIDTH ) + rect.setWidth( MIN_COMPONENT_WIDTH ); + if ( rect.height() < MIN_COMPONENT_HEIGHT ) + rect.setHeight( MIN_COMPONENT_HEIGHT ); + + setPos( rect.center() ); prepareGeometryChange(); - mComponent->setSize( mTempSize ); - mTempSize = QSizeF(); emit aboutToChange( tr( "Resize %1" ).arg( mComponent->description() ) ); + + mComponent->setPosition( pos() ); + mComponent->setSize( rect.size() ); updateStoredComponentPosition( pos(), mComponent->size() ); updateButtonPositions(); - emit changed(); - emit sizePositionChanged(); emit updateArrowPaths(); + emit sizePositionChanged(); } -void QgsModelComponentGraphicItem::previewItemRectChange( QRectF rect ) +QRectF QgsModelComponentGraphicItem::previewItemRectChange( QRectF rect ) { + rect = rect.normalized(); + + if ( rect.width() < MIN_COMPONENT_WIDTH ) + rect.setWidth( MIN_COMPONENT_WIDTH ); + if ( rect.height() < MIN_COMPONENT_HEIGHT ) + rect.setHeight( MIN_COMPONENT_HEIGHT ); + setPos( rect.center() ); prepareGeometryChange(); + mTempSize = rect.size(); updateButtonPositions(); emit updateArrowPaths(); + + return rect; +} + +void QgsModelComponentGraphicItem::finalizePreviewedItemRectChange( QRectF ) +{ + mComponent->setPosition( pos() ); + prepareGeometryChange(); + mComponent->setSize( mTempSize ); + mTempSize = QSizeF(); + + emit aboutToChange( tr( "Resize %1" ).arg( mComponent->description() ) ); + updateStoredComponentPosition( pos(), mComponent->size() ); + + updateButtonPositions(); + + emit changed(); + + emit sizePositionChanged(); + emit updateArrowPaths(); } void QgsModelComponentGraphicItem::modelHoverEnterEvent( QgsModelViewMouseEvent *event ) @@ -252,22 +289,64 @@ QVariant QgsModelComponentGraphicItem::itemChange( QGraphicsItem::GraphicsItemCh QRectF QgsModelComponentGraphicItem::boundingRect() const { QFontMetricsF fm( mFont ); - const int linksAbove = mComponent->linksCollapsed( Qt::TopEdge ) ? 0 : linkPointCount( Qt::TopEdge ); - const int linksBelow = mComponent->linksCollapsed( Qt::BottomEdge ) ? 0 : linkPointCount( Qt::BottomEdge ); + const int linksAbove = linkPointCount( Qt::TopEdge ); + const int linksBelow = linkPointCount( Qt::BottomEdge ); - const double hUp = fm.height() * 1.2 * ( linksAbove + 2 ); - const double hDown = fm.height() * 1.2 * ( linksBelow + 2 ); - return QRectF( -( itemSize().width() + 2 ) / 2, - -( itemSize().height() + 2 ) / 2 - hUp, - itemSize().width() + 2, - itemSize().height() + hDown + hUp ); + const double hUp = linksAbove == 0 ? 0 : + fm.height() * 1.2 * ( ( mComponent->linksCollapsed( Qt::TopEdge ) ? 0 : linksAbove ) + 2 ); + const double hDown = linksBelow == 0 ? 0 : + fm.height() * 1.2 * ( ( mComponent->linksCollapsed( Qt::BottomEdge ) ? 0 : linksBelow ) + 2 ); + return QRectF( -( itemSize().width() ) / 2 - RECT_PEN_SIZE, + -( itemSize().height() ) / 2 - hUp - RECT_PEN_SIZE, + itemSize().width() + 2 * RECT_PEN_SIZE, + itemSize().height() + hDown + hUp + 2 * RECT_PEN_SIZE ); +} + +bool QgsModelComponentGraphicItem::contains( const QPointF &point ) const +{ + QRectF paintingBounds = boundingRect(); + if ( point.x() < paintingBounds.left() + RECT_PEN_SIZE ) + return false; + if ( point.x() > paintingBounds.right() - RECT_PEN_SIZE ) + return false; + if ( point.y() < paintingBounds.top() + RECT_PEN_SIZE ) + return false; + if ( point.y() > paintingBounds.bottom() - RECT_PEN_SIZE ) + return false; + + return true; } void QgsModelComponentGraphicItem::paint( QPainter *painter, const QStyleOptionGraphicsItem *, QWidget * ) { const QRectF rect = itemRect(); - QColor color = fillColor( state() ); - QColor stroke = strokeColor( state() ); + QColor color; + QColor stroke; + QColor foreColor; + if ( mComponent->color().isValid() ) + { + color = mComponent->color(); + switch ( state() ) + { + case Selected: + color = color.darker( 110 ); + break; + case Hover: + color = color.darker( 105 ); + break; + + case Normal: + break; + } + stroke = color.darker( 110 ); + foreColor = color.lightness() > 150 ? QColor( 0, 0, 0 ) : QColor( 255, 255, 255 ); + } + else + { + color = fillColor( state() ); + stroke = strokeColor( state() ); + foreColor = textColor( state() ); + } QPen strokePen = QPen( stroke, 0 ) ; // 0 width "cosmetic" pen strokePen.setStyle( strokeStyle( state() ) ); @@ -275,7 +354,7 @@ void QgsModelComponentGraphicItem::paint( QPainter *painter, const QStyleOptionG painter->setBrush( QBrush( color, Qt::SolidPattern ) ); painter->drawRect( rect ); painter->setFont( font() ); - painter->setPen( QPen( textColor( state() ) ) ); + painter->setPen( QPen( foreColor ) ); QString text; @@ -287,13 +366,14 @@ void QgsModelComponentGraphicItem::paint( QPainter *painter, const QStyleOptionG if ( iconPicture().isNull() && iconPixmap().isNull() ) { - QRectF labelRect = QRectF( rect.left() + 4, rect.top() + 4, rect.width() - 8 - mButtonSize.width(), rect.height() - 8 ); + QRectF labelRect = QRectF( rect.left() + TEXT_MARGIN, rect.top() + TEXT_MARGIN, rect.width() - 2 * TEXT_MARGIN - mButtonSize.width() - BUTTON_MARGIN, rect.height() - 2 * TEXT_MARGIN ); text = label(); - painter->drawText( labelRect, Qt::TextWordWrap, text ); + painter->drawText( labelRect, Qt::TextWordWrap | titleAlignment(), text ); } else { - QRectF labelRect = QRectF( rect.left() + 25, rect.top() + 4, rect.width() - 8 - mButtonSize.width(), rect.height() - 8 ); + QRectF labelRect = QRectF( rect.left() + 21 + TEXT_MARGIN, rect.top() + TEXT_MARGIN, + rect.width() - 2 * TEXT_MARGIN - mButtonSize.width() - BUTTON_MARGIN - 21, rect.height() - 2 * TEXT_MARGIN ); text = label(); painter->drawText( labelRect, Qt::TextWordWrap | Qt::AlignVCenter, text ); } @@ -340,14 +420,14 @@ void QgsModelComponentGraphicItem::paint( QPainter *painter, const QStyleOptionG const QPixmap px = iconPixmap(); if ( !px.isNull() ) { - painter->drawPixmap( -( componentSize.width() / 2.0 ) + 3, -8, px ); + painter->drawPixmap( QPointF( -( componentSize.width() / 2.0 ) + 3, -8 ), px ); } else { const QPicture pic = iconPicture(); if ( !pic.isNull() ) { - painter->drawPicture( -( componentSize.width() / 2.0 ) + 3, -8, pic ); + painter->drawPicture( QPointF( -( componentSize.width() / 2.0 ) + 3, -8 ), pic ); } } } @@ -356,16 +436,16 @@ QRectF QgsModelComponentGraphicItem::itemRect( bool storedRect ) const { if ( storedRect ) { - return QRectF( mComponent->position().x() - ( mComponent->size().width() + 2 ) / 2.0, - mComponent->position().y() - ( mComponent->size().height() + 2 ) / 2.0, - mComponent->size().width() + 2, - mComponent->size().height() + 2 ); + return QRectF( mComponent->position().x() - ( mComponent->size().width() ) / 2.0, + mComponent->position().y() - ( mComponent->size().height() ) / 2.0, + mComponent->size().width(), + mComponent->size().height() ); } else - return QRectF( -( itemSize().width() + 2 ) / 2.0, - -( itemSize().height() + 2 ) / 2.0, - itemSize().width() + 2, - itemSize().height() + 2 ); + return QRectF( -( itemSize().width() ) / 2.0, + -( itemSize().height() ) / 2.0, + itemSize().width(), + itemSize().height() ); } QString QgsModelComponentGraphicItem::truncatedTextForItem( const QString &text ) const @@ -394,6 +474,11 @@ Qt::PenStyle QgsModelComponentGraphicItem::strokeStyle( QgsModelComponentGraphic return Qt::SolidLine; } +Qt::Alignment QgsModelComponentGraphicItem::titleAlignment() const +{ + return Qt::AlignLeft; +} + QPicture QgsModelComponentGraphicItem::iconPicture() const { return QPicture(); @@ -406,19 +491,19 @@ QPixmap QgsModelComponentGraphicItem::iconPixmap() const void QgsModelComponentGraphicItem::updateButtonPositions() { - mEditButton->setPosition( QPointF( itemSize().width() / 2.0 - mButtonSize.width() / 2.0 - 2, - itemSize().height() / 2.0 - mButtonSize.height() / 2.0 - 2 ) ); - mDeleteButton->setPosition( QPointF( itemSize().width() / 2.0 - mButtonSize.width() / 2.0 - 2, - mButtonSize.height() / 2.0 - itemSize().height() / 2.0 + 2 ) ); + mEditButton->setPosition( QPointF( itemSize().width() / 2.0 - mButtonSize.width() / 2.0 - BUTTON_MARGIN, + itemSize().height() / 2.0 - mButtonSize.height() / 2.0 - BUTTON_MARGIN ) ); + mDeleteButton->setPosition( QPointF( itemSize().width() / 2.0 - mButtonSize.width() / 2.0 - BUTTON_MARGIN, + mButtonSize.height() / 2.0 - itemSize().height() / 2.0 + BUTTON_MARGIN ) ); if ( mExpandTopButton ) { - QPointF pt = linkPoint( Qt::TopEdge, -1 ); + QPointF pt = linkPoint( Qt::TopEdge, -1, true ); mExpandTopButton->setPosition( QPointF( 0, pt.y() ) ); } if ( mExpandBottomButton ) { - QPointF pt = linkPoint( Qt::BottomEdge, -1 ); + QPointF pt = linkPoint( Qt::BottomEdge, -1, false ); mExpandBottomButton->setPosition( QPointF( 0, pt.y() ) ); } } @@ -499,7 +584,7 @@ QString QgsModelComponentGraphicItem::linkPointText( Qt::Edge, int ) const return QString(); } -QPointF QgsModelComponentGraphicItem::linkPoint( Qt::Edge edge, int index ) const +QPointF QgsModelComponentGraphicItem::linkPoint( Qt::Edge edge, int index, bool incoming ) const { switch ( edge ) { @@ -507,6 +592,11 @@ QPointF QgsModelComponentGraphicItem::linkPoint( Qt::Edge edge, int index ) cons { if ( linkPointCount( Qt::BottomEdge ) ) { + double offsetX = 25; + if ( mComponent->linksCollapsed( Qt::BottomEdge ) ) + { + offsetX = 17; + } const int pointIndex = !mComponent->linksCollapsed( Qt::BottomEdge ) ? index : -1; const QString text = truncatedTextForItem( linkPointText( Qt::BottomEdge, index ) ); QFontMetricsF fm( mFont ); @@ -514,7 +604,9 @@ QPointF QgsModelComponentGraphicItem::linkPoint( Qt::Edge edge, int index ) cons const double h = fm.height() * 1.2 * ( pointIndex + 1 ) + fm.height() / 2.0; const double y = h + itemSize().height() / 2.0 + 5; const double x = !mComponent->linksCollapsed( Qt::BottomEdge ) ? ( -itemSize().width() / 2 + 33 + w + 5 ) : 10; - return QPointF( x, y ); + return QPointF( incoming ? -itemSize().width() / 2 + offsetX + : x, + y ); } break; } @@ -531,9 +623,13 @@ QPointF QgsModelComponentGraphicItem::linkPoint( Qt::Edge edge, int index ) cons offsetX = 17; } QFontMetricsF fm( mFont ); + const QString text = truncatedTextForItem( linkPointText( Qt::TopEdge, index ) ); + const double w = fm.boundingRect( text ).width(); double h = -( fm.height() * 1.2 ) * ( paramIndex + 2 ) - fm.height() / 2.0 + 8; h = h - itemSize().height() / 2.0; - return QPointF( -itemSize().width() / 2 + offsetX, h ); + return QPointF( incoming ? -itemSize().width() / 2 + offsetX + : ( !mComponent->linksCollapsed( Qt::TopEdge ) ? ( -itemSize().width() / 2 + 33 + w + 5 ) : 10 ), + h ); } break; } @@ -883,7 +979,15 @@ QString QgsModelChildAlgorithmGraphicItem::linkPointText( Qt::Edge edge, int ind switch ( edge ) { case Qt::BottomEdge: - return truncatedTextForItem( child->algorithm()->outputDefinitions().at( index )->description() ); + { + const QgsProcessingOutputDefinition *output = child->algorithm()->outputDefinitions().at( index ); + QString title = output->description(); + if ( mResults.contains( output->name() ) ) + { + title += QStringLiteral( ": %1" ).arg( mResults.value( output->name() ).toString() ); + } + return truncatedTextForItem( title ); + } case Qt::TopEdge: { @@ -893,7 +997,11 @@ QString QgsModelChildAlgorithmGraphicItem::linkPointText( Qt::Edge edge, int ind return param->flags() & QgsProcessingParameterDefinition::FlagHidden || param->isDestination(); } ), params.end() ); - return truncatedTextForItem( params.at( index )->description() ); + + QString title = params.at( index )->description(); + if ( !mInputs.value( params.at( index )->name() ).toString().isEmpty() ) + title += QStringLiteral( ": %1" ).arg( mInputs.value( params.at( index )->name() ).toString() ); + return truncatedTextForItem( title ); } case Qt::LeftEdge: @@ -922,6 +1030,26 @@ bool QgsModelChildAlgorithmGraphicItem::canDeleteComponent() return false; } +void QgsModelChildAlgorithmGraphicItem::setResults( const QVariantMap &results ) +{ + if ( mResults == results ) + return; + + mResults = results; + update(); + emit updateArrowPaths(); +} + +void QgsModelChildAlgorithmGraphicItem::setInputs( const QVariantMap &inputs ) +{ + if ( mInputs == inputs ) + return; + + mInputs = inputs; + update(); + emit updateArrowPaths(); +} + void QgsModelChildAlgorithmGraphicItem::deleteComponent() { if ( const QgsProcessingModelChildAlgorithm *child = dynamic_cast< const QgsProcessingModelChildAlgorithm * >( component() ) ) @@ -1050,7 +1178,129 @@ void QgsModelOutputGraphicItem::deleteComponent() } +// +// QgsModelGroupBoxGraphicItem +// + +QgsModelGroupBoxGraphicItem::QgsModelGroupBoxGraphicItem( QgsProcessingModelGroupBox *box, QgsProcessingModelAlgorithm *model, QGraphicsItem *parent ) + : QgsModelComponentGraphicItem( box, model, parent ) +{ + setZValue( QgsModelGraphicsScene::ZValues::GroupBox ); + setLabel( box->description() ); + + QFont f = font(); + f.setBold( true ); + f.setPixelSize( 14 ); + setFont( f ); +} + +void QgsModelGroupBoxGraphicItem::contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) +{ + QMenu *popupmenu = new QMenu( event->widget() ); + QAction *removeAction = popupmenu->addAction( QObject::tr( "Remove" ) ); + connect( removeAction, &QAction::triggered, this, &QgsModelGroupBoxGraphicItem::deleteComponent ); + QAction *editAction = popupmenu->addAction( QObject::tr( "Edit…" ) ); + connect( editAction, &QAction::triggered, this, &QgsModelGroupBoxGraphicItem::editComponent ); + popupmenu->exec( event->screenPos() ); +} + +QgsModelGroupBoxGraphicItem::~QgsModelGroupBoxGraphicItem() = default; + +QColor QgsModelGroupBoxGraphicItem::fillColor( QgsModelComponentGraphicItem::State state ) const +{ + QColor c( 230, 230, 230 ); + switch ( state ) + { + case Selected: + c = c.darker( 110 ); + break; + case Hover: + c = c.darker( 105 ); + break; + + case Normal: + break; + } + return c; +} + +QColor QgsModelGroupBoxGraphicItem::strokeColor( QgsModelComponentGraphicItem::State state ) const +{ + switch ( state ) + { + case Selected: + return QColor( 50, 50, 50 ); + case Hover: + case Normal: + return QColor( 150, 150, 150 ); + } + return QColor(); +} + +QColor QgsModelGroupBoxGraphicItem::textColor( QgsModelComponentGraphicItem::State ) const +{ + return QColor( 100, 100, 100 ); +} + +Qt::PenStyle QgsModelGroupBoxGraphicItem::strokeStyle( QgsModelComponentGraphicItem::State ) const +{ + return Qt::DotLine; +} + +Qt::Alignment QgsModelGroupBoxGraphicItem::titleAlignment() const +{ + return Qt::AlignHCenter; +} + +void QgsModelGroupBoxGraphicItem::updateStoredComponentPosition( const QPointF &pos, const QSizeF &size ) +{ + if ( QgsProcessingModelGroupBox *box = dynamic_cast< QgsProcessingModelGroupBox * >( component() ) ) + { + box->setPosition( pos ); + box->setSize( size ); + model()->addGroupBox( *box ); + } +} + +bool QgsModelGroupBoxGraphicItem::canDeleteComponent() +{ + if ( dynamic_cast< QgsProcessingModelGroupBox * >( component() ) ) + { + return true; + } + return false; +} + +void QgsModelGroupBoxGraphicItem::deleteComponent() +{ + if ( const QgsProcessingModelGroupBox *box = dynamic_cast< const QgsProcessingModelGroupBox * >( component() ) ) + { + emit aboutToChange( tr( "Delete Group Box" ) ); + model()->removeGroupBox( box->uuid() ); + emit changed(); + emit requestModelRepaint(); + } +} + +void QgsModelGroupBoxGraphicItem::editComponent() +{ + if ( const QgsProcessingModelGroupBox *box = dynamic_cast< const QgsProcessingModelGroupBox * >( component() ) ) + { + QgsModelGroupBoxDefinitionDialog dlg( *box, this->scene()->views().at( 0 ) ); + + if ( dlg.exec() ) + { + emit aboutToChange( tr( "Edit Group Box" ) ); + model()->addGroupBox( dlg.groupBox() ); + emit changed(); + emit requestModelRepaint(); + } + } +} +// +// QgsModelCommentGraphicItem +// QgsModelCommentGraphicItem::QgsModelCommentGraphicItem( QgsProcessingModelComment *comment, QgsModelComponentGraphicItem *parentItem, QgsProcessingModelAlgorithm *model, QGraphicsItem *parent ) : QgsModelComponentGraphicItem( comment, model, parent ) diff --git a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h index 12e2fa933dcd..434498344fae 100644 --- a/src/gui/processing/models/qgsmodelcomponentgraphicitem.h +++ b/src/gui/processing/models/qgsmodelcomponentgraphicitem.h @@ -33,6 +33,7 @@ class QgsModelDesignerFlatButtonGraphicItem; class QgsModelDesignerFoldButtonGraphicItem; class QgsModelGraphicsView; class QgsModelViewMouseEvent; +class QgsProcessingModelGroupBox; ///@cond NOT_STABLE @@ -130,12 +131,17 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject */ void setItemRect( QRectF rect ); +#ifndef SIP_RUN + /** * Shows a preview of setting a new \a rect for the item. */ - void previewItemRectChange( QRectF rect ); + QRectF previewItemRectChange( QRectF rect ); -#ifndef SIP_RUN + /** + * Sets a new scene \a rect for the item. + */ + void finalizePreviewedItemRectChange( QRectF rect ); /** * Handles a model hover enter \a event. @@ -163,6 +169,7 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject void hoverLeaveEvent( QGraphicsSceneHoverEvent *event ) override; QVariant itemChange( GraphicsItemChange change, const QVariant &value ) override; QRectF boundingRect() const override; + bool contains( const QPointF &point ) const override; void paint( QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr ) override; /** @@ -202,7 +209,7 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject /** * Returns the location of the link point with the specified \a index on the specified \a edge. */ - QPointF linkPoint( Qt::Edge edge, int index ) const; + QPointF linkPoint( Qt::Edge edge, int index, bool incoming ) const; /** * Returns the best link point to use for a link originating at a specified \a other item. @@ -317,6 +324,11 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject */ virtual Qt::PenStyle strokeStyle( State state ) const; + /** + * Returns the title alignment + */ + virtual Qt::Alignment titleAlignment() const; + /** * Returns a QPicture version of the item's icon, if available. */ @@ -357,8 +369,14 @@ class GUI_EXPORT QgsModelComponentGraphicItem : public QGraphicsObject QgsModelDesignerFlatButtonGraphicItem *mEditButton = nullptr; QgsModelDesignerFlatButtonGraphicItem *mDeleteButton = nullptr; + static constexpr double MIN_COMPONENT_WIDTH = 70; + static constexpr double MIN_COMPONENT_HEIGHT = 30; + static constexpr double DEFAULT_BUTTON_WIDTH = 16; static constexpr double DEFAULT_BUTTON_HEIGHT = 16; + static constexpr double BUTTON_MARGIN = 2; + static constexpr double TEXT_MARGIN = 4; + static constexpr double RECT_PEN_SIZE = 2; QSizeF mButtonSize { DEFAULT_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT }; QFont mFont; @@ -440,6 +458,16 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra void contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) override; bool canDeleteComponent() override; + /** + * Sets the results obtained for this child algorithm for the last model execution through the dialog. + */ + void setResults( const QVariantMap &results ); + + /** + * Sets the inputs used for this child algorithm for the last model execution through the dialog. + */ + void setInputs( const QVariantMap &inputs ); + protected: QColor fillColor( State state ) const override; @@ -463,6 +491,8 @@ class GUI_EXPORT QgsModelChildAlgorithmGraphicItem : public QgsModelComponentGra private: QPicture mPicture; QPixmap mPixmap; + QVariantMap mResults; + QVariantMap mInputs; }; @@ -559,6 +589,52 @@ class GUI_EXPORT QgsModelCommentGraphicItem : public QgsModelComponentGraphicIte }; + + +/** + * \ingroup gui + * \brief A graphic item representing a group box in the model designer. + * \warning Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsModelGroupBoxGraphicItem : public QgsModelComponentGraphicItem +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsModelGroupBoxGraphicItem for the specified group \a box, with the specified \a parent item. + * + * The \a model argument specifies the associated processing model. Ownership of \a model is not transferred, and + * it must exist for the lifetime of this object. + * + * Ownership of \a output is transferred to the item. + */ + QgsModelGroupBoxGraphicItem( QgsProcessingModelGroupBox *box SIP_TRANSFER, + QgsProcessingModelAlgorithm *model, + QGraphicsItem *parent SIP_TRANSFERTHIS ); + ~QgsModelGroupBoxGraphicItem() override; + void contextMenuEvent( QGraphicsSceneContextMenuEvent *event ) override; + bool canDeleteComponent() override; + protected: + + QColor fillColor( State state ) const override; + QColor strokeColor( State state ) const override; + QColor textColor( State state ) const override; + Qt::PenStyle strokeStyle( State state ) const override; + Qt::Alignment titleAlignment() const override; + void updateStoredComponentPosition( const QPointF &pos, const QSizeF &size ) override; + + protected slots: + + void deleteComponent() override; + void editComponent() override; + private: + + +}; + ///@endcond #endif // QGSMODELCOMPONENTGRAPHICITEM_H diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.cpp b/src/gui/processing/models/qgsmodeldesignerdialog.cpp index 41160627afd0..de6fc2a2187d 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.cpp +++ b/src/gui/processing/models/qgsmodeldesignerdialog.cpp @@ -27,6 +27,7 @@ #include "qgsmodelviewtoolselect.h" #include "qgsmodelgraphicsscene.h" #include "qgsmodelcomponentgraphicitem.h" +#include "processing/models/qgsprocessingmodelgroupbox.h" #include #include @@ -98,7 +99,7 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mPropertiesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable ); mInputsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable ); mAlgorithmsDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable ); - mVariablesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable ); + mVariablesDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable ); mAlgorithmsTree->header()->setVisible( false ); mAlgorithmSearchEdit->setShowSearchIcon( true ); @@ -118,6 +119,7 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mainLayout->insertWidget( 0, mMessageBar ); mView->setAcceptDrops( true ); + QgsSettings settings; connect( mActionClose, &QAction::triggered, this, &QWidget::close ); connect( mActionZoomIn, &QAction::triggered, this, &QgsModelDesignerDialog::zoomIn ); @@ -131,6 +133,20 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags connect( mActionSave, &QAction::triggered, this, [ = ] { saveModel( false ); } ); connect( mActionSaveAs, &QAction::triggered, this, [ = ] { saveModel( true ); } ); connect( mActionDeleteComponents, &QAction::triggered, this, &QgsModelDesignerDialog::deleteSelected ); + connect( mActionSnapSelected, &QAction::triggered, mView, &QgsModelGraphicsView::snapSelected ); + + mActionSnappingEnabled->setChecked( settings.value( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), false ).toBool() ); + connect( mActionSnappingEnabled, &QAction::toggled, this, [ = ]( bool enabled ) + { + mView->snapper()->setSnapToGrid( enabled ); + QgsSettings().setValue( QStringLiteral( "/Processing/Modeler/enableSnapToGrid" ), enabled ); + } ); + mView->snapper()->setSnapToGrid( mActionSnappingEnabled->isChecked() ); + + connect( mActionSelectAll, &QAction::triggered, this, [ = ] + { + mScene->selectAll(); + } ); mUndoAction = mUndoStack->createUndoAction( this ); mUndoAction->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionUndo.svg" ) ) ); @@ -146,7 +162,10 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mToolbar->insertAction( mActionZoomIn, mRedoAction ); mToolbar->insertSeparator( mActionZoomIn ); - QgsSettings settings; + mGroupMenu = new QMenu( tr( "Zoom To" ), this ); + mMenuView->insertMenu( mActionZoomIn, mGroupMenu ); + connect( mGroupMenu, &QMenu::aboutToShow, this, &QgsModelDesignerDialog::populateZoomToMenu ); + QgsProcessingToolboxProxyModel::Filters filters = QgsProcessingToolboxProxyModel::FilterModeler; if ( settings.value( QStringLiteral( "Processing/Configuration/SHOW_ALGORITHMS_KNOWN_ISSUES" ), false ).toBool() ) { @@ -184,7 +203,7 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mUndoDock->setObjectName( QStringLiteral( "UndoDock" ) ); mUndoView = new QUndoView( mUndoStack, this ); mUndoDock->setWidget( mUndoView ); - mUndoDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable ); + mUndoDock->setFeatures( QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable ); addDockWidget( Qt::DockWidgetArea::LeftDockWidgetArea, mUndoDock ); tabifyDockWidget( mUndoDock, mPropertiesDock ); @@ -254,7 +273,18 @@ QgsModelDesignerDialog::QgsModelDesignerDialog( QWidget *parent, Qt::WindowFlags mIgnoreUndoStackChanges--; } ); + connect( mActionAddGroupBox, &QAction::triggered, this, [ = ] + { + const QPointF viewCenter = mView->mapToScene( mView->viewport()->rect().center() ); + QgsProcessingModelGroupBox group; + group.setPosition( viewCenter ); + group.setDescription( tr( "New Group" ) ); + beginUndoCommand( tr( "Add Group Box" ) ); + model()->addGroupBox( group ); + repaintModel(); + endUndoCommand(); + } ); updateWindowTitle(); } @@ -341,6 +371,7 @@ void QgsModelDesignerDialog::setModelScene( QgsModelGraphicsScene *scene ) mScene = scene; mScene->setParent( this ); + mScene->setChildAlgorithmResults( mChildResults ); mView->setModelScene( mScene ); @@ -422,6 +453,20 @@ bool QgsModelDesignerDialog::checkForUnsavedChanges() } } +void QgsModelDesignerDialog::setLastRunChildAlgorithmResults( const QVariantMap &results ) +{ + mChildResults = results; + if ( mScene ) + mScene->setChildAlgorithmResults( mChildResults ); +} + +void QgsModelDesignerDialog::setLastRunChildAlgorithmInputs( const QVariantMap &inputs ) +{ + mChildInputs = inputs; + if ( mScene ) + mScene->setChildAlgorithmInputs( mChildInputs ); +} + void QgsModelDesignerDialog::zoomIn() { mView->setTransformationAnchor( QGraphicsView::NoAnchor ); @@ -602,6 +647,10 @@ void QgsModelDesignerDialog::deleteSelected() return true; else if ( dynamic_cast< QgsModelCommentGraphicItem *>( p2 ) ) return false; + else if ( dynamic_cast< QgsModelGroupBoxGraphicItem *>( p1 ) ) + return true; + else if ( dynamic_cast< QgsModelGroupBoxGraphicItem *>( p2 ) ) + return false; else if ( dynamic_cast< QgsModelOutputGraphicItem *>( p1 ) ) return true; else if ( dynamic_cast< QgsModelOutputGraphicItem *>( p2 ) ) @@ -661,6 +710,23 @@ void QgsModelDesignerDialog::deleteSelected() repaintModel(); } +void QgsModelDesignerDialog::populateZoomToMenu() +{ + mGroupMenu->clear(); + for ( const QgsProcessingModelGroupBox &box : model()->groupBoxes() ) + { + if ( QgsModelComponentGraphicItem *item = mScene->groupBoxItem( box.uuid() ) ) + { + QAction *zoomAction = new QAction( box.description(), mGroupMenu ); + connect( zoomAction, &QAction::triggered, this, [ = ] + { + mView->centerOn( item ); + } ); + mGroupMenu->addAction( zoomAction ); + } + } +} + bool QgsModelDesignerDialog::isDirty() const { return mHasChanged && mUndoStack->index() != -1; diff --git a/src/gui/processing/models/qgsmodeldesignerdialog.h b/src/gui/processing/models/qgsmodeldesignerdialog.h index 8f1033851092..711432adc6f5 100644 --- a/src/gui/processing/models/qgsmodeldesignerdialog.h +++ b/src/gui/processing/models/qgsmodeldesignerdialog.h @@ -120,6 +120,16 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode */ bool checkForUnsavedChanges(); + /** + * Sets the results of child algorithms for the last run of the model through the designer window. + */ + void setLastRunChildAlgorithmResults( const QVariantMap &results ); + + /** + * Sets the inputs for child algorithms for the last run of the model through the designer window. + */ + void setLastRunChildAlgorithmInputs( const QVariantMap &inputs ); + private slots: void zoomIn(); void zoomOut(); @@ -132,6 +142,7 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode void toggleComments( bool show ); void updateWindowTitle(); void deleteSelected(); + void populateZoomToMenu(); private: @@ -157,6 +168,8 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode QUndoView *mUndoView = nullptr; QgsDockWidget *mUndoDock = nullptr; + QMenu *mGroupMenu = nullptr; + int mBlockUndoCommands = 0; int mIgnoreUndoStackChanges = 0; @@ -164,6 +177,9 @@ class GUI_EXPORT QgsModelDesignerDialog : public QMainWindow, public Ui::QgsMode int mBlockRepaints = 0; + QVariantMap mChildResults; + QVariantMap mChildInputs; + bool isDirty() const; void fillInputsTree(); diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.cpp b/src/gui/processing/models/qgsmodelgraphicsscene.cpp index 97a85164d1d6..0eaa0da3672e 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsscene.cpp @@ -18,6 +18,7 @@ #include "qgsprocessingmodelalgorithm.h" #include "qgsmodelcomponentgraphicitem.h" #include "qgsmodelarrowitem.h" +#include "qgsprocessingmodelgroupbox.h" #include ///@cond NOT_STABLE @@ -48,7 +49,7 @@ QgsModelComponentGraphicItem *QgsModelGraphicsScene::createParameterGraphicItem( return new QgsModelParameterGraphicItem( param, model, nullptr ); } -QgsModelComponentGraphicItem *QgsModelGraphicsScene::createChildAlgGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelChildAlgorithm *child ) const +QgsModelChildAlgorithmGraphicItem *QgsModelGraphicsScene::createChildAlgGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelChildAlgorithm *child ) const { return new QgsModelChildAlgorithmGraphicItem( child, model, nullptr ); } @@ -63,8 +64,27 @@ QgsModelComponentGraphicItem *QgsModelGraphicsScene::createCommentGraphicItem( Q return new QgsModelCommentGraphicItem( comment, parentItem, model, nullptr ); } +QgsModelComponentGraphicItem *QgsModelGraphicsScene::createGroupBoxGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelGroupBox *box ) const +{ + return new QgsModelGroupBoxGraphicItem( box, model, nullptr ); +} + void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, QgsProcessingContext &context ) { + // model group boxes + const QList boxes = model->groupBoxes(); + mGroupBoxItems.clear(); + for ( const QgsProcessingModelGroupBox &box : boxes ) + { + QgsModelComponentGraphicItem *item = createGroupBoxGraphicItem( model, box.clone() ); + addItem( item ); + item->setPos( box.position().x(), box.position().y() ); + mGroupBoxItems.insert( box.uuid(), item ); + connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); + connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); + connect( item, &QgsModelComponentGraphicItem::aboutToChange, this, &QgsModelGraphicsScene::componentAboutToChange ); + } + // model input parameters const QMap params = model->parameterComponents(); for ( auto it = params.constBegin(); it != params.constEnd(); ++it ) @@ -100,9 +120,11 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs const QMap childAlgs = model->childAlgorithms(); for ( auto it = childAlgs.constBegin(); it != childAlgs.constEnd(); ++it ) { - QgsModelComponentGraphicItem *item = createChildAlgGraphicItem( model, it.value().clone() ); + QgsModelChildAlgorithmGraphicItem *item = createChildAlgGraphicItem( model, it.value().clone() ); addItem( item ); item->setPos( it.value().position().x(), it.value().position().y() ); + item->setResults( mChildResults.value( it.value().childId() ).toMap() ); + item->setInputs( mChildInputs.value( it.value().childId() ).toMap() ); mChildAlgorithmItems.insert( it.value().childId(), item ); connect( item, &QgsModelComponentGraphicItem::requestModelRepaint, this, &QgsModelGraphicsScene::rebuildRequired ); connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); @@ -114,14 +136,15 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs // arrows linking child algorithms for ( auto it = childAlgs.constBegin(); it != childAlgs.constEnd(); ++it ) { - int idx = 0; + int topIdx = 0; + int bottomIdx = 0; if ( !it.value().algorithm() ) continue; const QgsProcessingParameterDefinitions parameters = it.value().algorithm()->parameterDefinitions(); for ( const QgsProcessingParameterDefinition *parameter : parameters ) { - if ( !parameter->isDestination() && !( parameter->flags() & QgsProcessingParameterDefinition::FlagHidden ) ) + if ( !( parameter->flags() & QgsProcessingParameterDefinition::FlagHidden ) ) { QList< QgsProcessingModelChildParameterSource > sources; if ( it.value().parameterSources().contains( parameter->name() ) ) @@ -133,14 +156,21 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs { QgsModelArrowItem *arrow = nullptr; if ( link.linkIndex == -1 ) - arrow = new QgsModelArrowItem( link.item, mChildAlgorithmItems.value( it.value().childId() ), Qt::TopEdge, idx ); + arrow = new QgsModelArrowItem( link.item, mChildAlgorithmItems.value( it.value().childId() ), parameter->isDestination() ? Qt::BottomEdge : Qt::TopEdge, parameter->isDestination() ? bottomIdx : topIdx ); else - arrow = new QgsModelArrowItem( link.item, link.edge, link.linkIndex, mChildAlgorithmItems.value( it.value().childId() ), Qt::TopEdge, idx ); + arrow = new QgsModelArrowItem( link.item, link.edge, link.linkIndex, true, + mChildAlgorithmItems.value( it.value().childId() ), + parameter->isDestination() ? Qt::BottomEdge : Qt::TopEdge, + parameter->isDestination() ? bottomIdx : topIdx, + true ); addItem( arrow ); } - idx += 1; } } + if ( parameter->isDestination() ) + bottomIdx++; + else + topIdx++; } const QStringList dependencies = it.value().dependencies(); for ( const QString &depend : dependencies ) @@ -163,8 +193,6 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs connect( item, &QgsModelComponentGraphicItem::changed, this, &QgsModelGraphicsScene::componentChanged ); connect( item, &QgsModelComponentGraphicItem::aboutToChange, this, &QgsModelGraphicsScene::componentAboutToChange ); - addCommentItemForComponent( model, outputIt.value(), item ); - QPointF pos = outputIt.value().position(); int idx = -1; int i = 0; @@ -186,6 +214,8 @@ void QgsModelGraphicsScene::createItems( QgsProcessingModelAlgorithm *model, Qgs item->setPos( pos ); outputItems.insert( outputIt.key(), item ); addItem( new QgsModelArrowItem( mChildAlgorithmItems[it.value().childId()], Qt::BottomEdge, idx, item ) ); + + addCommentItemForComponent( model, outputIt.value(), item ); } mOutputItems.insert( it.value().childId(), outputItems ); } @@ -222,6 +252,28 @@ QgsModelComponentGraphicItem *QgsModelGraphicsScene::componentItemAt( QPointF po return nullptr; } +QgsModelComponentGraphicItem *QgsModelGraphicsScene::groupBoxItem( const QString &uuid ) +{ + return mGroupBoxItems.value( uuid ); +} + +void QgsModelGraphicsScene::selectAll() +{ + //select all items in scene + QgsModelComponentGraphicItem *focusedItem = nullptr; + const QList itemList = items(); + for ( QGraphicsItem *graphicsItem : itemList ) + { + if ( QgsModelComponentGraphicItem *componentItem = dynamic_cast( graphicsItem ) ) + { + componentItem->setSelected( true ); + if ( !focusedItem ) + focusedItem = componentItem; + } + } + emit selectedItemChanged( focusedItem ); +} + void QgsModelGraphicsScene::deselectAll() { //we can't use QGraphicsScene::clearSelection, as that emits no signals @@ -248,6 +300,32 @@ void QgsModelGraphicsScene::setSelectedItem( QgsModelComponentGraphicItem *item emit selectedItemChanged( item ); } +void QgsModelGraphicsScene::setChildAlgorithmResults( const QVariantMap &results ) +{ + mChildResults = results; + + for ( auto it = mChildResults.constBegin(); it != mChildResults.constEnd(); ++it ) + { + if ( QgsModelChildAlgorithmGraphicItem *item = mChildAlgorithmItems.value( it.key() ) ) + { + item->setResults( it.value().toMap() ); + } + } +} + +void QgsModelGraphicsScene::setChildAlgorithmInputs( const QVariantMap &inputs ) +{ + mChildInputs = inputs; + + for ( auto it = mChildInputs.constBegin(); it != mChildInputs.constEnd(); ++it ) + { + if ( QgsModelChildAlgorithmGraphicItem *item = mChildAlgorithmItems.value( it.key() ) ) + { + item->setInputs( it.value().toMap() ); + } + } +} + QList QgsModelGraphicsScene::linkSourcesForParameterValue( QgsProcessingModelAlgorithm *model, const QVariant &value, const QString &childId, QgsProcessingContext &context ) const { QList res; @@ -317,6 +395,7 @@ QList QgsModelGraphicsScene::linkSourcesForPa case QgsProcessingModelChildParameterSource::StaticValue: case QgsProcessingModelChildParameterSource::ExpressionText: + case QgsProcessingModelChildParameterSource::ModelOutput: break; } } diff --git a/src/gui/processing/models/qgsmodelgraphicsscene.h b/src/gui/processing/models/qgsmodelgraphicsscene.h index 9cdc3c5a0814..d39d2f07a59b 100644 --- a/src/gui/processing/models/qgsmodelgraphicsscene.h +++ b/src/gui/processing/models/qgsmodelgraphicsscene.h @@ -28,6 +28,8 @@ class QgsProcessingModelChildAlgorithm; class QgsProcessingModelOutput; class QgsProcessingModelComponent; class QgsProcessingModelComment; +class QgsModelChildAlgorithmGraphicItem; +class QgsProcessingModelGroupBox; ///@cond NOT_STABLE @@ -46,8 +48,9 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene //! Z values for scene items enum ZValues { - ArrowLink = 0, //!< An arrow linking model items - ModelComponent = 1, //!< Model components (e.g. algorithms, inputs and outputs) + GroupBox = 0, //!< A logical group box + ArrowLink = 1, //!< An arrow linking model items + ModelComponent = 2, //!< Model components (e.g. algorithms, inputs and outputs) MouseHandles = 99, //!< Mouse handles RubberBand = 100, //!< Rubber band item ZSnapIndicator = 101, //!< Z-value for snapping indicator @@ -106,6 +109,16 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ QgsModelComponentGraphicItem *componentItemAt( QPointF position ) const; + /** + * Returns the graphic item corresponding to the specified group box \a uuid. + */ + QgsModelComponentGraphicItem *groupBoxItem( const QString &uuid ); + + /** + * Selects all the components in the scene. + */ + void selectAll(); + /** * Clears any selected items in the scene. * @@ -119,6 +132,16 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene */ void setSelectedItem( QgsModelComponentGraphicItem *item ); + /** + * Sets the results for child algorithms for the last model execution. + */ + void setChildAlgorithmResults( const QVariantMap &results ); + + /** + * Sets the inputs for child algorithms for the last model execution. + */ + void setChildAlgorithmInputs( const QVariantMap &inputs ); + signals: /** @@ -155,7 +178,7 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene /** * Creates a new graphic item for a model child algorithm. */ - virtual QgsModelComponentGraphicItem *createChildAlgGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelChildAlgorithm *child ) const SIP_FACTORY; + virtual QgsModelChildAlgorithmGraphicItem *createChildAlgGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelChildAlgorithm *child ) const SIP_FACTORY; /** * Creates a new graphic item for a model output. @@ -168,6 +191,10 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene virtual QgsModelComponentGraphicItem *createCommentGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelComment *comment, QgsModelComponentGraphicItem *parentItem ) const SIP_FACTORY; + /** + * Creates a new graphic item for a model group box. + */ + QgsModelComponentGraphicItem *createGroupBoxGraphicItem( QgsProcessingModelAlgorithm *model, QgsProcessingModelGroupBox *box ) const SIP_FACTORY; private: @@ -184,8 +211,11 @@ class GUI_EXPORT QgsModelGraphicsScene : public QGraphicsScene Flags mFlags = nullptr; QMap< QString, QgsModelComponentGraphicItem * > mParameterItems; - QMap< QString, QgsModelComponentGraphicItem * > mChildAlgorithmItems; + QMap< QString, QgsModelChildAlgorithmGraphicItem * > mChildAlgorithmItems; QMap< QString, QMap< QString, QgsModelComponentGraphicItem * > > mOutputItems; + QMap< QString, QgsModelComponentGraphicItem * > mGroupBoxItems; + QVariantMap mChildResults; + QVariantMap mChildInputs; }; diff --git a/src/gui/processing/models/qgsmodelgraphicsview.cpp b/src/gui/processing/models/qgsmodelgraphicsview.cpp index b31936da6a6c..cb7481e530a3 100644 --- a/src/gui/processing/models/qgsmodelgraphicsview.cpp +++ b/src/gui/processing/models/qgsmodelgraphicsview.cpp @@ -435,6 +435,29 @@ void QgsModelGraphicsView::endMacroCommand() emit macroCommandEnded(); } +void QgsModelGraphicsView::snapSelected() +{ + QgsModelGraphicsScene *s = modelScene(); + const QList itemList = s->selectedComponentItems(); + startMacroCommand( tr( "Snap Items" ) ); + if ( !itemList.empty() ) + { + bool prevSetting = mSnapper.snapToGrid(); + mSnapper.setSnapToGrid( true ); + for ( QgsModelComponentGraphicItem *item : itemList ) + { + bool wasSnapped = false; + QRectF snapped = mSnapper.snapRectWithResize( item->mapRectToScene( item->itemRect( ) ), transform().m11(), wasSnapped ); + if ( wasSnapped ) + { + item->setItemRect( snapped ); + } + } + mSnapper.setSnapToGrid( prevSetting ); + } + endMacroCommand(); +} + QgsModelViewSnapMarker::QgsModelViewSnapMarker() : QGraphicsRectItem( QRectF( 0, 0, 0, 0 ) ) diff --git a/src/gui/processing/models/qgsmodelgraphicsview.h b/src/gui/processing/models/qgsmodelgraphicsview.h index f0242ea32184..5d1dbe802feb 100644 --- a/src/gui/processing/models/qgsmodelgraphicsview.h +++ b/src/gui/processing/models/qgsmodelgraphicsview.h @@ -110,6 +110,13 @@ class GUI_EXPORT QgsModelGraphicsView : public QGraphicsView */ void endMacroCommand(); + public slots: + + /** + * Snaps the selected items to the grid. + */ + void snapSelected(); + signals: /** diff --git a/src/gui/processing/models/qgsmodelgroupboxdefinitionwidget.cpp b/src/gui/processing/models/qgsmodelgroupboxdefinitionwidget.cpp new file mode 100644 index 000000000000..cf11b721f8bd --- /dev/null +++ b/src/gui/processing/models/qgsmodelgroupboxdefinitionwidget.cpp @@ -0,0 +1,77 @@ +/*************************************************************************** + qgsmodelgroupboxdefinitionwidget.cpp + ------------------------------------------ + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsmodelgroupboxdefinitionwidget.h" + +#include "qgscolorbutton.h" +#include "qgsgui.h" +#include +#include +#include +#include + +QgsModelGroupBoxDefinitionDialog::QgsModelGroupBoxDefinitionDialog( const QgsProcessingModelGroupBox &box, + QWidget *parent ) + : QDialog( parent ) + , mBox( box ) +{ + QVBoxLayout *commentLayout = new QVBoxLayout(); + commentLayout->addWidget( new QLabel( tr( "Title" ) ) ); + mCommentEdit = new QTextEdit(); + mCommentEdit->setAcceptRichText( false ); + mCommentEdit->setText( box.description() ); + commentLayout->addWidget( mCommentEdit, 1 ); + + QHBoxLayout *hl = new QHBoxLayout(); + hl->setContentsMargins( 0, 0, 0, 0 ); + hl->addWidget( new QLabel( tr( "Color" ) ) ); + mCommentColorButton = new QgsColorButton(); + mCommentColorButton->setAllowOpacity( true ); + mCommentColorButton->setWindowTitle( tr( "Comment Color" ) ); + mCommentColorButton->setShowNull( true, tr( "Default" ) ); + + if ( box.color().isValid() ) + mCommentColorButton->setColor( box.color() ); + else + mCommentColorButton->setToNull(); + + hl->addWidget( mCommentColorButton ); + commentLayout->addLayout( hl ); + + QDialogButtonBox *bbox = new QDialogButtonBox( QDialogButtonBox::Cancel | QDialogButtonBox::Ok ); + connect( bbox, &QDialogButtonBox::accepted, this, &QgsModelGroupBoxDefinitionDialog::accept ); + connect( bbox, &QDialogButtonBox::rejected, this, &QgsModelGroupBoxDefinitionDialog::reject ); + + commentLayout->addWidget( bbox ); + setLayout( commentLayout ); + setWindowTitle( tr( "Group Box Properties" ) ); + setObjectName( QStringLiteral( "QgsModelGroupBoxDefinitionWidget" ) ); + QgsGui::enableAutoGeometryRestore( this ); + + mCommentEdit->setFocus(); + mCommentEdit->selectAll(); +} + +QgsProcessingModelGroupBox QgsModelGroupBoxDefinitionDialog::groupBox() const +{ + QgsProcessingModelGroupBox box = mBox; + box.setColor( mCommentColorButton->isNull() ? QColor() : mCommentColorButton->color() ); + box.setDescription( mCommentEdit->toPlainText() ); + return box; +} + diff --git a/src/gui/processing/models/qgsmodelgroupboxdefinitionwidget.h b/src/gui/processing/models/qgsmodelgroupboxdefinitionwidget.h new file mode 100644 index 000000000000..ecfd294b57f9 --- /dev/null +++ b/src/gui/processing/models/qgsmodelgroupboxdefinitionwidget.h @@ -0,0 +1,69 @@ +/*************************************************************************** + qgsmodelgroupboxdefinitionwidget.h + ---------------------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSPROCESSINGGROUPBBOXDEFINITIONWIDGET_H +#define QGSPROCESSINGGROUPBBOXDEFINITIONWIDGET_H + +#include +#include +#include "processing/models/qgsprocessingmodelgroupbox.h" +#include "qgis_gui.h" + +#define SIP_NO_FILE + +class QLineEdit; +class QCheckBox; +class QTabWidget; +class QTextEdit; +class QgsColorButton; +class QgsProcessingModelGroupBox; + + +/** + * A widget which allow users to specify the properties of a model group box. + * + * \ingroup gui + * \note Not available in Python bindings. + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsModelGroupBoxDefinitionDialog: public QDialog +{ + Q_OBJECT + public: + + /** + * Constructor for QgsModelGroupBoxDefinitionWidget, for the specified group \a box. + */ + QgsModelGroupBoxDefinitionDialog( const QgsProcessingModelGroupBox &box, + QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Returns a new instance of the group box, using the current settings defined in the dialog. + */ + QgsProcessingModelGroupBox groupBox() const; + + private: + + QTextEdit *mCommentEdit = nullptr; + QgsColorButton *mCommentColorButton = nullptr; + QgsProcessingModelGroupBox mBox; + +}; + + +#endif // QGSPROCESSINGGROUPBBOXDEFINITIONWIDGET_H diff --git a/src/gui/processing/models/qgsmodelsnapper.cpp b/src/gui/processing/models/qgsmodelsnapper.cpp index c526e7b77dd6..161b4a021986 100644 --- a/src/gui/processing/models/qgsmodelsnapper.cpp +++ b/src/gui/processing/models/qgsmodelsnapper.cpp @@ -16,11 +16,12 @@ #include "qgsmodelsnapper.h" #include "qgssettings.h" +#include QgsModelSnapper::QgsModelSnapper() { QgsSettings s; - mTolerance = s.value( QStringLiteral( "LayoutDesigner/defaultSnapTolerancePixels" ), 5, QgsSettings::Gui ).toInt(); + mTolerance = s.value( QStringLiteral( "/Processing/Modeler/snapTolerancePixels" ), 40 ).toInt(); } void QgsModelSnapper::setSnapTolerance( const int snapTolerance ) @@ -33,19 +34,19 @@ void QgsModelSnapper::setSnapToGrid( bool enabled ) mSnapToGrid = enabled; } -QPointF QgsModelSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped ) const +QPointF QgsModelSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped, bool snapHorizontal, bool snapVertical ) const { snapped = false; bool snappedXToGrid = false; bool snappedYToGrid = false; QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid ); - if ( snappedXToGrid ) + if ( snappedXToGrid && snapVertical ) { snapped = true; point.setX( res.x() ); } - if ( snappedYToGrid ) + if ( snappedYToGrid && snapHorizontal ) { snapped = true; point.setY( res.y() ); @@ -54,27 +55,22 @@ QPointF QgsModelSnapper::snapPoint( QPointF point, double scaleFactor, bool &sna return point; } -QRectF QgsModelSnapper::snapRect( const QRectF &rect, double scaleFactor, bool &snapped ) const +QRectF QgsModelSnapper::snapRect( const QRectF &rect, double scaleFactor, bool &snapped, bool snapHorizontal, bool snapVertical ) const { snapped = false; QRectF snappedRect = rect; - QList< double > xCoords; - xCoords << rect.left() << rect.center().x() << rect.right(); - QList< double > yCoords; - yCoords << rect.top() << rect.center().y() << rect.bottom(); - bool snappedXToGrid = false; bool snappedYToGrid = false; QList< QPointF > points; points << rect.topLeft() << rect.topRight() << rect.bottomLeft() << rect.bottomRight(); QPointF res = snapPointsToGrid( points, scaleFactor, snappedXToGrid, snappedYToGrid ); - if ( snappedXToGrid ) + if ( snappedXToGrid && snapVertical ) { snapped = true; snappedRect.translate( res.x(), 0 ); } - if ( snappedYToGrid ) + if ( snappedYToGrid && snapHorizontal ) { snapped = true; snappedRect.translate( 0, res.y() ); @@ -83,6 +79,39 @@ QRectF QgsModelSnapper::snapRect( const QRectF &rect, double scaleFactor, bool & return snappedRect; } +QRectF QgsModelSnapper::snapRectWithResize( const QRectF &rect, double scaleFactor, bool &snapped, bool snapHorizontal, bool snapVertical ) const +{ + snapped = false; + QRectF snappedRect = rect; + + bool snappedXToGrid = false; + bool snappedYToGrid = false; + QPointF res = snapPointsToGrid( QList< QPointF >() << rect.topLeft(), scaleFactor, snappedXToGrid, snappedYToGrid ); + if ( snappedXToGrid && snapVertical ) + { + snapped = true; + snappedRect.setLeft( snappedRect.left() + res.x() ); + } + if ( snappedYToGrid && snapHorizontal ) + { + snapped = true; + snappedRect.setTop( snappedRect.top() + res.y() ); + } + res = snapPointsToGrid( QList< QPointF >() << rect.bottomRight(), scaleFactor, snappedXToGrid, snappedYToGrid ); + if ( snappedXToGrid && snapVertical ) + { + snapped = true; + snappedRect.setRight( snappedRect.right() + res.x() ); + } + if ( snappedYToGrid && snapHorizontal ) + { + snapped = true; + snappedRect.setBottom( snappedRect.bottom() + res.y() ); + } + + return snappedRect; +} + QPointF QgsModelSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX, bool &snappedY ) const { QPointF delta = snapPointsToGrid( QList< QPointF >() << point, scaleFactor, snappedX, snappedY ); @@ -110,21 +139,21 @@ QPointF QgsModelSnapper::snapPointsToGrid( const QList &points, double for ( QPointF point : points ) { //snap x coordinate - double gridRes = 10; //mLayout->convertToLayoutUnits( grid.resolution() ); + double gridRes = 30; //mLayout->convertToLayoutUnits( grid.resolution() ); int xRatio = static_cast< int >( ( point.x() ) / gridRes + 0.5 ); //NOLINT int yRatio = static_cast< int >( ( point.y() ) / gridRes + 0.5 ); //NOLINT double xSnapped = xRatio * gridRes; double ySnapped = yRatio * gridRes; - double currentDiffX = std::abs( xSnapped - point.x() ); + double currentDiffX = std::fabs( xSnapped - point.x() ); if ( currentDiffX < smallestDiffX ) { smallestDiffX = currentDiffX; deltaX = xSnapped - point.x(); } - double currentDiffY = std::abs( ySnapped - point.y() ); + double currentDiffY = std::fabs( ySnapped - point.y() ); if ( currentDiffY < smallestDiffY ) { smallestDiffY = currentDiffY; diff --git a/src/gui/processing/models/qgsmodelsnapper.h b/src/gui/processing/models/qgsmodelsnapper.h index b7ab0041e11d..1074230b799f 100644 --- a/src/gui/processing/models/qgsmodelsnapper.h +++ b/src/gui/processing/models/qgsmodelsnapper.h @@ -80,7 +80,7 @@ class GUI_EXPORT QgsModelSnapper * \see snapRect() */ - QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT ) const; + QPointF snapPoint( QPointF point, double scaleFactor, bool &snapped SIP_OUT, bool snapHorizontal = true, bool snapVertical = true ) const; /** * Snaps a layout coordinate \a rect. If \a rect was snapped, \a snapped will be set to TRUE. @@ -101,7 +101,25 @@ class GUI_EXPORT QgsModelSnapper * * \see snapPoint() */ - QRectF snapRect( const QRectF &rect, double scaleFactor, bool &snapped SIP_OUT ) const; + QRectF snapRect( const QRectF &rect, double scaleFactor, bool &snapped SIP_OUT, bool snapHorizontal = true, bool snapVertical = true ) const; + + /** + * Snaps a layout coordinate \a rect. If \a rect was snapped, \a snapped will be set to TRUE. + * + * The \a scaleFactor argument should be set to the transformation from + * scalar transform from layout coordinates to pixels, i.e. the + * graphics view transform().m11() value. + * + * This method considers snapping to the grid, snap lines, etc. + * + * If the \a horizontalSnapLine and \a verticalSnapLine arguments are specified, then the snapper + * will automatically display and position these lines to indicate snapping positions to item bounds. + * + * A list of items to ignore during the snapping can be specified via the \a ignoreItems list. + * + * \see snapPoint() + */ + QRectF snapRectWithResize( const QRectF &rect, double scaleFactor, bool &snapped SIP_OUT, bool snapHorizontal = true, bool snapVertical = true ) const; /** * Snaps a layout coordinate \a point to the grid. If \a point diff --git a/src/gui/processing/models/qgsmodelviewmousehandles.cpp b/src/gui/processing/models/qgsmodelviewmousehandles.cpp index 78db51c54ab1..2092ebeff323 100644 --- a/src/gui/processing/models/qgsmodelviewmousehandles.cpp +++ b/src/gui/processing/models/qgsmodelviewmousehandles.cpp @@ -137,16 +137,17 @@ void QgsModelViewMouseHandles::setItemRect( QGraphicsItem *item, QRectF rect ) { if ( QgsModelComponentGraphicItem *componentItem = dynamic_cast( item ) ) { - componentItem->setItemRect( rect ); + componentItem->finalizePreviewedItemRectChange( rect ); } } -void QgsModelViewMouseHandles::previewSetItemRect( QGraphicsItem *item, QRectF rect ) +QRectF QgsModelViewMouseHandles::previewSetItemRect( QGraphicsItem *item, QRectF rect ) { if ( QgsModelComponentGraphicItem *componentItem = dynamic_cast( item ) ) { - componentItem->previewItemRectChange( rect ); + return componentItem->previewItemRectChange( rect ); } + return rect; } void QgsModelViewMouseHandles::startMacroCommand( const QString &text ) @@ -159,5 +160,21 @@ void QgsModelViewMouseHandles::endMacroCommand() mView->endMacroCommand(); } +QPointF QgsModelViewMouseHandles::snapPoint( QPointF originalPoint, QgsGraphicsViewMouseHandles::SnapGuideMode mode, bool snapHorizontal, bool snapVertical ) +{ + bool snapped = false; + + QPointF snappedPoint; + switch ( mode ) + { + case Item: + case Point: + snappedPoint = mView->snapper()->snapPoint( originalPoint, mView->transform().m11(), snapped, snapHorizontal, snapVertical ); + break; + } + + return snapped ? snappedPoint : originalPoint; +} + ///@endcond PRIVATE diff --git a/src/gui/processing/models/qgsmodelviewmousehandles.h b/src/gui/processing/models/qgsmodelviewmousehandles.h index c71c8fe0435d..ed761dafd067 100644 --- a/src/gui/processing/models/qgsmodelviewmousehandles.h +++ b/src/gui/processing/models/qgsmodelviewmousehandles.h @@ -62,9 +62,10 @@ class GUI_EXPORT QgsModelViewMouseHandles: public QgsGraphicsViewMouseHandles void moveItem( QGraphicsItem *item, double deltaX, double deltaY ) override; void previewItemMove( QGraphicsItem *item, double deltaX, double deltaY ) override; void setItemRect( QGraphicsItem *item, QRectF rect ) override; - void previewSetItemRect( QGraphicsItem *item, QRectF rect ) override; + QRectF previewSetItemRect( QGraphicsItem *item, QRectF rect ) override; void startMacroCommand( const QString &text ) override; void endMacroCommand() override; + QPointF snapPoint( QPointF originalPoint, SnapGuideMode mode, bool snapHorizontal = true, bool snapVertical = true ) override; public slots: diff --git a/src/gui/processing/qgsprocessingalgorithmdialogbase.cpp b/src/gui/processing/qgsprocessingalgorithmdialogbase.cpp index 206cbeb19ecd..d377bc984f34 100644 --- a/src/gui/processing/qgsprocessingalgorithmdialogbase.cpp +++ b/src/gui/processing/qgsprocessingalgorithmdialogbase.cpp @@ -24,6 +24,7 @@ #include "processing/qgsprocessingalgrunnertask.h" #include "qgsstringutils.h" #include "qgsapplication.h" +#include "qgspanelwidget.h" #include #include #include @@ -34,33 +35,43 @@ ///@cond NOT_STABLE +QgsProcessingAlgorithmDialogFeedback::QgsProcessingAlgorithmDialogFeedback() + : QgsProcessingFeedback( false ) +{} + void QgsProcessingAlgorithmDialogFeedback::setProgressText( const QString &text ) { + QgsProcessingFeedback::setProgressText( text ); emit progressTextChanged( text ); } void QgsProcessingAlgorithmDialogFeedback::reportError( const QString &error, bool fatalError ) { + QgsProcessingFeedback::reportError( error, fatalError ); emit errorReported( error, fatalError ); } void QgsProcessingAlgorithmDialogFeedback::pushInfo( const QString &info ) { + QgsProcessingFeedback::pushInfo( info ); emit infoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushCommandInfo( const QString &info ) { + QgsProcessingFeedback::pushCommandInfo( info ); emit commandInfoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushDebugInfo( const QString &info ) { + QgsProcessingFeedback::pushDebugInfo( info ); emit debugInfoPushed( info ); } void QgsProcessingAlgorithmDialogFeedback::pushConsoleInfo( const QString &info ) { + QgsProcessingFeedback::pushConsoleInfo( info ); emit consoleInfoPushed( info ); } @@ -95,16 +106,20 @@ QgsProcessingAlgorithmDialogBase::QgsProcessingAlgorithmDialogBase( QWidget *par mSplitterState = splitter->saveState(); splitterChanged( 0, 0 ); - connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsProcessingAlgorithmDialogBase::closeClicked ); - connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsProcessingAlgorithmDialogBase::runAlgorithm ); - // Rename OK button to Run mButtonRun = mButtonBox->button( QDialogButtonBox::Ok ); mButtonRun->setText( tr( "Run" ) ); + // Rename Yes button. Yes is used to ensure same position of Run and Change Parameters with respect to Close button. + mButtonChangeParameters = mButtonBox->button( QDialogButtonBox::Yes ); + mButtonChangeParameters->setText( tr( "Change Parameters" ) ); + buttonCancel->setEnabled( false ); mButtonClose = mButtonBox->button( QDialogButtonBox::Close ); + connect( mButtonRun, &QPushButton::clicked, this, &QgsProcessingAlgorithmDialogBase::runAlgorithm ); + connect( mButtonChangeParameters, &QPushButton::clicked, this, &QgsProcessingAlgorithmDialogBase::showParameters ); + connect( mButtonBox, &QDialogButtonBox::rejected, this, &QgsProcessingAlgorithmDialogBase::closeClicked ); connect( mButtonBox, &QDialogButtonBox::helpRequested, this, &QgsProcessingAlgorithmDialogBase::openHelp ); connect( mButtonCollapse, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::toggleCollapsed ); connect( splitter, &QSplitter::splitterMoved, this, &QgsProcessingAlgorithmDialogBase::splitterChanged ); @@ -113,6 +128,8 @@ QgsProcessingAlgorithmDialogBase::QgsProcessingAlgorithmDialogBase( QWidget *par connect( mButtonCopyLog, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::copyLogToClipboard ); connect( mButtonClearLog, &QToolButton::clicked, this, &QgsProcessingAlgorithmDialogBase::clearLog ); + connect( mTabWidget, &QTabWidget::currentChanged, this, &QgsProcessingAlgorithmDialogBase::mTabWidget_currentChanged ); + mMessageBar = new QgsMessageBar(); mMessageBar->setSizePolicy( QSizePolicy::Minimum, QSizePolicy::Fixed ); verticalLayout->insertWidget( 0, mMessageBar ); @@ -168,15 +185,17 @@ QgsProcessingAlgorithm *QgsProcessingAlgorithmDialogBase::algorithm() return mAlgorithm.get(); } -void QgsProcessingAlgorithmDialogBase::setMainWidget( QWidget *widget ) +void QgsProcessingAlgorithmDialogBase::setMainWidget( QgsPanelWidget *widget ) { if ( mMainWidget ) { mMainWidget->deleteLater(); } + mPanelStack->setMainPanel( widget ); + widget->setDockMode( true ); + mMainWidget = widget; - mTabWidget->widget( 0 )->layout()->addWidget( mMainWidget ); } QWidget *QgsProcessingAlgorithmDialogBase::mainWidget() @@ -184,11 +203,6 @@ QWidget *QgsProcessingAlgorithmDialogBase::mainWidget() return mMainWidget; } -QVariantMap QgsProcessingAlgorithmDialogBase::getParameterValues() const -{ - return QVariantMap(); -} - void QgsProcessingAlgorithmDialogBase::saveLogToFile( const QString &path, const LogFormat format ) { QFile logFile( path ); @@ -239,6 +253,11 @@ void QgsProcessingAlgorithmDialogBase::showLog() mTabWidget->setCurrentIndex( 1 ); } +void QgsProcessingAlgorithmDialogBase::showParameters() +{ + mTabWidget->setCurrentIndex( 0 ); +} + QPushButton *QgsProcessingAlgorithmDialogBase::runButton() { return mButtonRun; @@ -249,6 +268,11 @@ QPushButton *QgsProcessingAlgorithmDialogBase::cancelButton() return buttonCancel; } +QPushButton *QgsProcessingAlgorithmDialogBase::changeParametersButton() +{ + return mButtonChangeParameters; +} + void QgsProcessingAlgorithmDialogBase::clearProgress() { progressBar->setMaximum( 0 ); @@ -259,6 +283,11 @@ void QgsProcessingAlgorithmDialogBase::setExecuted( bool executed ) mExecuted = executed; } +void QgsProcessingAlgorithmDialogBase::setExecutedAnyResult( bool executedAnyResult ) +{ + mExecutedAnyResult = executedAnyResult; +} + void QgsProcessingAlgorithmDialogBase::setResults( const QVariantMap &results ) { mResults = results; @@ -311,6 +340,11 @@ void QgsProcessingAlgorithmDialogBase::splitterChanged( int, int ) } } +void QgsProcessingAlgorithmDialogBase::mTabWidget_currentChanged( int ) +{ + updateRunButtonVisibility(); +} + void QgsProcessingAlgorithmDialogBase::linkClicked( const QUrl &url ) { QDesktopServices::openUrl( url.toString() ); @@ -553,7 +587,43 @@ void QgsProcessingAlgorithmDialogBase::resetGui() progressBar->setMaximum( 100 ); progressBar->setValue( 0 ); mButtonRun->setEnabled( true ); + mButtonChangeParameters->setEnabled( true ); mButtonClose->setEnabled( true ); + if ( mMainWidget ) + { + mMainWidget->setEnabled( true ); + } + updateRunButtonVisibility(); + resetAdditionalGui(); +} + +void QgsProcessingAlgorithmDialogBase::updateRunButtonVisibility() +{ + // Activate run button if current tab is Parameters + bool runButtonVisible = mTabWidget->currentIndex() == 0; + mButtonRun->setVisible( runButtonVisible ); + mButtonChangeParameters->setVisible( !runButtonVisible && mExecutedAnyResult && mButtonChangeParameters->isEnabled() ); +} + +void QgsProcessingAlgorithmDialogBase::resetAdditionalGui() +{ + +} + +void QgsProcessingAlgorithmDialogBase::blockControlsWhileRunning() +{ + mButtonRun->setEnabled( false ); + mButtonChangeParameters->setEnabled( false ); + if ( mMainWidget ) + { + mMainWidget->setEnabled( false ); + } + blockAdditionalControlsWhileRunning(); +} + +void QgsProcessingAlgorithmDialogBase::blockAdditionalControlsWhileRunning() +{ + } QgsMessageBar *QgsProcessingAlgorithmDialogBase::messageBar() diff --git a/src/gui/processing/qgsprocessingalgorithmdialogbase.h b/src/gui/processing/qgsprocessingalgorithmdialogbase.h index 89cf00fb77f1..bdfdc1a6321d 100644 --- a/src/gui/processing/qgsprocessingalgorithmdialogbase.h +++ b/src/gui/processing/qgsprocessingalgorithmdialogbase.h @@ -22,6 +22,7 @@ #include "ui_qgsprocessingalgorithmprogressdialogbase.h" #include "processing/qgsprocessingcontext.h" #include "processing/qgsprocessingfeedback.h" +#include "qgsprocessingwidgetwrapper.h" ///@cond NOT_STABLE @@ -49,7 +50,7 @@ class QgsProcessingAlgorithmDialogFeedback : public QgsProcessingFeedback /** * Constructor for QgsProcessingAlgorithmDialogFeedback. */ - QgsProcessingAlgorithmDialogFeedback() = default; + QgsProcessingAlgorithmDialogFeedback(); signals: @@ -69,7 +70,6 @@ class QgsProcessingAlgorithmDialogFeedback : public QgsProcessingFeedback void pushDebugInfo( const QString &info ) override; void pushConsoleInfo( const QString &info ) override; - }; #endif @@ -79,7 +79,7 @@ class QgsProcessingAlgorithmDialogFeedback : public QgsProcessingFeedback * \note This is not considered stable API and may change in future QGIS versions. * \since QGIS 3.0 */ -class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui::QgsProcessingDialogBase +class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, public QgsProcessingParametersGenerator, private Ui::QgsProcessingDialogBase { Q_OBJECT @@ -120,7 +120,7 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: * Sets the main \a widget for the dialog, usually a panel for configuring algorithm parameters. * \see mainWidget() */ - void setMainWidget( QWidget *widget SIP_TRANSFER ); + void setMainWidget( QgsPanelWidget *widget SIP_TRANSFER ); /** * Returns the main widget for the dialog, usually a panel for configuring algorithm parameters. @@ -153,11 +153,6 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: */ QgsProcessingFeedback *createFeedback() SIP_FACTORY; - /** - * Returns the parameter values for the algorithm to run in the dialog. - */ - virtual QVariantMap getParameterValues() const; - /** * Saves the log contents to a text file (specified by the file \a path), in * the given \a format. @@ -230,6 +225,11 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: */ void copyLogToClipboard(); + /** + * Switches the dialog to the parameters page. + */ + void showParameters(); + protected: void closeEvent( QCloseEvent *e ) override; @@ -244,6 +244,11 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: */ QPushButton *cancelButton(); + /** + * Returns the dialog's change parameters button. + */ + QPushButton *changeParametersButton(); + /** * Returns the dialog's button box. */ @@ -266,6 +271,11 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: */ void setExecuted( bool executed ); + /** + * Sets whether the algorithm was executed through the dialog (no matter the result). + */ + void setExecutedAnyResult( bool executedAnyResult ); + /** * Sets the algorithm results. * \see results() @@ -283,6 +293,29 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: */ void resetGui(); + /** + * For subclasses to register their own GUI controls to be reset, ready + * for another algorithm execution. + */ + virtual void resetAdditionalGui(); + + /** + * Sets visibility for mutually exclusive buttons Run and Change Parameters. + */ + void updateRunButtonVisibility(); + + /** + * Blocks run and changeParameters buttons and parameters tab while the + * algorithm is running. + */ + void blockControlsWhileRunning(); + + /** + * For subclasses to register their own GUI controls to be blocked while + * the algorithm is running. + */ + virtual void blockAdditionalControlsWhileRunning(); + /** * Returns the dialog's message bar. */ @@ -306,6 +339,15 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: */ static QString formatStringForLog( const QString &string ); + signals: + + /** + * Emitted whenever an algorithm has finished executing in the dialog. + * + * \since QGIS 3.14 + */ + void algorithmFinished( bool successful, const QVariantMap &result ); + protected slots: /** @@ -324,6 +366,7 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: void toggleCollapsed(); void splitterChanged( int pos, int index ); + void mTabWidget_currentChanged( int index ); void linkClicked( const QUrl &url ); void algExecuted( bool successful, const QVariantMap &results ); void taskTriggered( QgsTask *task ); @@ -333,11 +376,13 @@ class GUI_EXPORT QgsProcessingAlgorithmDialogBase : public QDialog, private Ui:: QPushButton *mButtonRun = nullptr; QPushButton *mButtonClose = nullptr; + QPushButton *mButtonChangeParameters = nullptr; QByteArray mSplitterState; QToolButton *mButtonCollapse = nullptr; QgsMessageBar *mMessageBar = nullptr; bool mExecuted = false; + bool mExecutedAnyResult = false; QVariantMap mResults; QWidget *mMainWidget = nullptr; std::unique_ptr< QgsProcessingAlgorithm > mAlgorithm; diff --git a/src/gui/processing/qgsprocessingenummodelerwidget.cpp b/src/gui/processing/qgsprocessingenummodelerwidget.cpp new file mode 100644 index 000000000000..7ac30ff9a831 --- /dev/null +++ b/src/gui/processing/qgsprocessingenummodelerwidget.cpp @@ -0,0 +1,187 @@ +/*************************************************************************** + qgsprocessingenummodelerwidget.cpp + ------------------------------------ + Date : March 2020 + Copyright : (C) 2020 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 "qgsprocessingenummodelerwidget.h" +#include "qgsgui.h" +#include +#include + +///@cond NOT_STABLE + +QgsProcessingEnumModelerWidget::QgsProcessingEnumModelerWidget( QWidget *parent ) + : QWidget( parent ) +{ + setupUi( this ); + + mModel = new QStandardItemModel( this ); + mItemList->setModel( mModel ); + connect( mModel, &QStandardItemModel::itemChanged, this, &QgsProcessingEnumModelerWidget::onItemChanged ); + + connect( mButtonAdd, &QToolButton::clicked, this, &QgsProcessingEnumModelerWidget::addItem ); + connect( mButtonRemove, &QToolButton::clicked, this, [ = ] { removeItems( false ); } ); + connect( mButtonClear, &QToolButton::clicked, this, [ = ] { removeItems( true ); } ); +} + +void QgsProcessingEnumModelerWidget::addItem() +{ + QStandardItem *item = new QStandardItem( tr( "new item" ) ); + item->setCheckable( true ); + item->setDropEnabled( false ); + item->setData( Qt::Unchecked ); + mModel->appendRow( item ); +} + +void QgsProcessingEnumModelerWidget::removeItems( const bool removeAll ) +{ + if ( removeAll ) + { + if ( QMessageBox::question( nullptr, tr( "Delete items" ), + tr( "Are you sure you want to delete all items" ), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) == QMessageBox::Yes ) + mModel->clear(); + } + else + { + QModelIndexList selected = mItemList->selectionModel()->selectedIndexes(); + QSet< int > rows; + rows.reserve( selected.count() ); + for ( const QModelIndex &i : selected ) + rows << i.row(); + + QList< int > rowsToDelete = rows.toList(); + std::sort( rowsToDelete.begin(), rowsToDelete.end(), std::greater() ); + + mItemList->setUpdatesEnabled( false ); + for ( int i : qgis::as_const( rowsToDelete ) ) + mModel->removeRows( i, 1 ); + mItemList->setUpdatesEnabled( true ); + } +} + +void QgsProcessingEnumModelerWidget::onItemChanged( QStandardItem *item ) +{ + int checkedItemIndex = -1; + for ( int i = 0; i < mModel->rowCount(); i++ ) + { + QStandardItem *itm = mModel->item( i ); + if ( itm->checkState() == Qt::Checked && itm->data() == Qt::Checked ) + { + checkedItemIndex = i; + break; + } + } + + mModel->blockSignals( true ); + if ( checkedItemIndex < 0 ) + { + item->setData( item->checkState() ); + } + else + { + if ( mAllowMultiple->isChecked() ) + { + item->setData( item->checkState() ); + } + else + { + mModel->item( checkedItemIndex )->setCheckState( Qt::Unchecked ); + mModel->item( checkedItemIndex )->setData( Qt::Unchecked ); + item->setData( item->checkState() ); + } + } + mModel->blockSignals( false ); +} + +QStringList QgsProcessingEnumModelerWidget::options() const +{ + QStringList options; + options.reserve( mModel->rowCount() ); + for ( int i = 0; i < mModel->rowCount(); ++i ) + { + options << mModel->item( i )->text(); + } + return options; +} + +void QgsProcessingEnumModelerWidget::setOptions( const QStringList &options ) +{ + for ( const QString &option : options ) + { + QStandardItem *item = new QStandardItem( option ); + item->setCheckable( true ); + item->setDropEnabled( false ); + item->setData( Qt::Unchecked ); + mModel->appendRow( item ); + } +} + +QVariant QgsProcessingEnumModelerWidget::defaultOptions() const +{ + QVariantList defaults; + for ( int i = 0; i < mModel->rowCount(); ++i ) + { + if ( mModel->item( i )->checkState() == Qt::Checked ) + defaults << i; + } + QVariant val( defaults ); + return val; +} + +void QgsProcessingEnumModelerWidget::setDefaultOptions( const QVariant &defaultValue ) +{ + if ( !defaultValue.isValid() ) + return; + + QVariant val = defaultValue; + QList< int > values; + if ( val.type() == QVariant::List || val.type() == QVariant::StringList ) + { + for ( const QVariant &var : val.toList() ) + values << var.toInt(); + } + else if ( val.type() == QVariant::String ) + { + QStringList split = val.toString().split( ',' ); + for ( const QString &var : split ) + values << var.toInt(); + } + else if ( val.type() == QVariant::Int ) + { + values << val.toInt(); + } + + QStandardItem *item; + for ( const int &i : values ) + { + item = mModel->item( i ); + if ( item ) + { + item->setCheckState( Qt::Checked ); + item->setData( Qt::Checked ); + } + } +} + +bool QgsProcessingEnumModelerWidget::allowMultiple() const +{ + return mAllowMultiple->isChecked(); +} + +void QgsProcessingEnumModelerWidget::setAllowMultiple( bool allowMultiple ) +{ + mAllowMultiple->setChecked( allowMultiple ); +} + +///@endcond diff --git a/src/gui/processing/qgsprocessingenummodelerwidget.h b/src/gui/processing/qgsprocessingenummodelerwidget.h new file mode 100644 index 000000000000..ffc22091c00a --- /dev/null +++ b/src/gui/processing/qgsprocessingenummodelerwidget.h @@ -0,0 +1,100 @@ +/*************************************************************************** + qgsprocessingenummodelerwidget.h + ---------------------------------- + Date : March 2020 + Copyright : (C) 2020 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 QGSPROCESSINGENUMMODELERWIDGET_H +#define QGSPROCESSINGENUMMODELERWIDGET_H + +#define SIP_NO_FILE + +#include "qgis.h" +#include "qgis_gui.h" +#include "ui_qgsprocessingenummodelerwidgetbase.h" +#include +#include + +///@cond PRIVATE + +/** + * Processing enum widget for configuring enum parameter in modeler. + * \ingroup gui + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingEnumModelerWidget : public QWidget, private Ui::QgsProcessingEnumModelerWidgetBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingEnumModelerWidget. + */ + QgsProcessingEnumModelerWidget( QWidget *parent = nullptr ); + + /** + * Returns list of all available options. + * + * \see setOptions() + */ + QStringList options() const; + + /** + * Populate widget with available options. + * + * \see options() + */ + void setOptions( const QStringList &options ); + + /** + * Returns indices of options used by default. + * + * \see setDefaultOptions() + */ + QVariant defaultOptions() const; + + /** + * Mark default options as checked. + * + * \see defaultOptions() + */ + void setDefaultOptions( const QVariant &defaultValue ); + + /** + * Returns TRUE if the parameter allows multiple selected values. + * \see setAllowMultiple() + */ + bool allowMultiple() const; + + /** + * Sets whether the parameter allows multiple selected values. + * \see allowMultiple() + */ + void setAllowMultiple( bool allowMultiple ); + + private slots: + + void addItem(); + void removeItems( const bool removeAll ); + void onItemChanged( QStandardItem *item ); + + private: + QStandardItemModel *mModel = nullptr; + + friend class TestProcessingGui; +}; + +///@endcond + +#endif // QGSPROCESSINGENUMMODELERWIDGET_H diff --git a/src/gui/processing/qgsprocessingfeaturesourceoptionswidget.cpp b/src/gui/processing/qgsprocessingfeaturesourceoptionswidget.cpp new file mode 100644 index 000000000000..ddfbbe240032 --- /dev/null +++ b/src/gui/processing/qgsprocessingfeaturesourceoptionswidget.cpp @@ -0,0 +1,67 @@ +/*************************************************************************** + qgsprocessingfeaturesourceoptionswidget.cpp + ------------------------------------ + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 "qgsprocessingfeaturesourceoptionswidget.h" +#include "qgis.h" + +///@cond NOT_STABLE + +QgsProcessingFeatureSourceOptionsWidget::QgsProcessingFeatureSourceOptionsWidget( QWidget *parent ) + : QgsPanelWidget( parent ) +{ + setupUi( this ); + + mFeatureLimitSpinBox->setClearValue( 0, tr( "Not set" ) ); + mFeatureLimitSpinBox->clear(); + + mComboInvalidFeatureFiltering->addItem( tr( "Use Default" ) ); + mComboInvalidFeatureFiltering->addItem( tr( "Do not Filter (Better Performance)" ), QgsFeatureRequest::GeometryNoCheck ); + mComboInvalidFeatureFiltering->addItem( tr( "Skip (Ignore) Features with Invalid Geometries" ), QgsFeatureRequest::GeometrySkipInvalid ); + mComboInvalidFeatureFiltering->addItem( tr( "Stop Algorithm Execution When a Geometry is Invalid" ), QgsFeatureRequest::GeometryAbortOnInvalid ); + + connect( mFeatureLimitSpinBox, qgis::overload::of( &QSpinBox::valueChanged ), this, &QgsPanelWidget::widgetChanged ); + connect( mComboInvalidFeatureFiltering, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsPanelWidget::widgetChanged ); +} + +void QgsProcessingFeatureSourceOptionsWidget::setGeometryCheckMethod( bool isOverridden, QgsFeatureRequest::InvalidGeometryCheck check ) +{ + if ( !isOverridden ) + mComboInvalidFeatureFiltering->setCurrentIndex( mComboInvalidFeatureFiltering->findData( QVariant() ) ); + else + mComboInvalidFeatureFiltering->setCurrentIndex( mComboInvalidFeatureFiltering->findData( check ) ); +} + +void QgsProcessingFeatureSourceOptionsWidget::setFeatureLimit( int limit ) +{ + mFeatureLimitSpinBox->setValue( limit ); +} + +QgsFeatureRequest::InvalidGeometryCheck QgsProcessingFeatureSourceOptionsWidget::geometryCheckMethod() const +{ + return mComboInvalidFeatureFiltering->currentData().isValid() ? static_cast< QgsFeatureRequest::InvalidGeometryCheck >( mComboInvalidFeatureFiltering->currentData().toInt() ) : QgsFeatureRequest::GeometryAbortOnInvalid; +} + +bool QgsProcessingFeatureSourceOptionsWidget::isOverridingInvalidGeometryCheck() const +{ + return mComboInvalidFeatureFiltering->currentData().isValid(); +} + +int QgsProcessingFeatureSourceOptionsWidget::featureLimit() const +{ + return mFeatureLimitSpinBox->value() > 0 ? mFeatureLimitSpinBox->value() : -1; +} + +///@endcond diff --git a/src/gui/processing/qgsprocessingfeaturesourceoptionswidget.h b/src/gui/processing/qgsprocessingfeaturesourceoptionswidget.h new file mode 100644 index 000000000000..d2844ad46685 --- /dev/null +++ b/src/gui/processing/qgsprocessingfeaturesourceoptionswidget.h @@ -0,0 +1,87 @@ +/*************************************************************************** + qgsprocessingfeaturesourceoptionswidget.h + ---------------------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 QGSPROCESSINGFEATURESOURCEOPTIONSWIDGET_H +#define QGSPROCESSINGFEATURESOURCEOPTIONSWIDGET_H + +#include "qgis.h" +#include "qgis_gui.h" +#include "qgsfeaturerequest.h" +#include "ui_qgsprocessingfeaturesourceoptionsbase.h" + +#define SIP_NO_FILE + +///@cond NOT_STABLE + +/** + * \ingroup gui + * \brief Widget for configuring advanced settings for a feature source. + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingFeatureSourceOptionsWidget : public QgsPanelWidget, private Ui::QgsProcessingFeatureSourceOptionsBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingFeatureSourceOptionsWidget, with the specified \a parent widget. + */ + QgsProcessingFeatureSourceOptionsWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Sets the geometry check method to use, and whether the default method is overridden. + * + * \see isOverridingInvalidGeometryCheck() + * \see geometryCheckMethod() + */ + void setGeometryCheckMethod( bool isOverridden, QgsFeatureRequest::InvalidGeometryCheck check ); + + /** + * Sets the feature \a limit for the source. + * + * \see featureLimit() + */ + void setFeatureLimit( int limit ); + + /** + * Returns the selected geometry check method. Also check isOverridingInvalidGeometryCheck() to verify + * whether this method should be applied, or the default one used instead. + * + * \see isOverridingInvalidGeometryCheck() + * \see setGeometryCheckMethod() + */ + QgsFeatureRequest::InvalidGeometryCheck geometryCheckMethod() const; + + /** + * Returns TRUE if the default geometry check method is being overridden. + * \see geometryCheckMethod() + * \see setGeometryCheckMethod() + */ + bool isOverridingInvalidGeometryCheck() const; + + /** + * Returns the feature limit set in the widget, or -1 if no limit is set. + * + * \see setFeatureLimit() + */ + int featureLimit() const; + +}; + +///@endcond + +#endif // QGSPROCESSINGFEATURESOURCEOPTIONSWIDGET_H diff --git a/src/gui/processing/qgsprocessingguiregistry.cpp b/src/gui/processing/qgsprocessingguiregistry.cpp index eb98edb8b2a7..8f53a035dcd4 100644 --- a/src/gui/processing/qgsprocessingguiregistry.cpp +++ b/src/gui/processing/qgsprocessingguiregistry.cpp @@ -48,6 +48,21 @@ QgsProcessingGuiRegistry::QgsProcessingGuiRegistry() addParameterWidgetFactory( new QgsProcessingMapThemeWidgetWrapper() ); addParameterWidgetFactory( new QgsProcessingDateTimeWidgetWrapper() ); addParameterWidgetFactory( new QgsProcessingProviderConnectionWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingDatabaseSchemaWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingDatabaseTableWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingExtentWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingMapLayerWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingVectorLayerWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingFeatureSourceWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingRasterLayerWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingMeshLayerWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingBandWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingMultipleLayerWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingFeatureSinkWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingVectorDestinationWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingRasterDestinationWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingFileDestinationWidgetWrapper() ); + addParameterWidgetFactory( new QgsProcessingFolderDestinationWidgetWrapper() ); } QgsProcessingGuiRegistry::~QgsProcessingGuiRegistry() diff --git a/src/gui/processing/qgsprocessingmaplayercombobox.cpp b/src/gui/processing/qgsprocessingmaplayercombobox.cpp index 560a19cad17b..d7cdac226140 100644 --- a/src/gui/processing/qgsprocessingmaplayercombobox.cpp +++ b/src/gui/processing/qgsprocessingmaplayercombobox.cpp @@ -20,15 +20,24 @@ #include "qgssettings.h" #include "qgsvectorlayer.h" #include "qgsfeatureid.h" +#include "qgsapplication.h" +#include "qgsguiutils.h" +#include "qgspanelwidget.h" +#include "qgsprocessingfeaturesourceoptionswidget.h" +#include "qgsdatasourceselectdialog.h" +#include "qgsprocessingwidgetwrapper.h" #include #include #include #include #include +#include +#include +#include ///@cond PRIVATE -QgsProcessingMapLayerComboBox::QgsProcessingMapLayerComboBox( const QgsProcessingParameterDefinition *parameter, QWidget *parent ) +QgsProcessingMapLayerComboBox::QgsProcessingMapLayerComboBox( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QWidget( parent ) , mParameter( parameter->clone() ) { @@ -41,12 +50,60 @@ QgsProcessingMapLayerComboBox::QgsProcessingMapLayerComboBox( const QgsProcessin layout->addWidget( mCombo ); layout->setAlignment( mCombo, Qt::AlignTop ); + int iconSize = QgsGuiUtils::scaleIconSize( 24 ); + if ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() && type == QgsProcessingGui::Standard ) + { + mIterateButton = new QToolButton(); + mIterateButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconIterate.svg" ) ) ); + mIterateButton->setToolTip( tr( "Iterate over this layer, creating a separate output for every feature in the layer" ) ); + mIterateButton->setCheckable( true ); + mIterateButton->setAutoRaise( true ); + + // button width is 1.25 * icon size, height 1.1 * icon size. But we round to ensure even pixel sizes for equal margins + mIterateButton->setFixedSize( 2 * static_cast< int >( 1.25 * iconSize / 2.0 ), 2 * static_cast< int >( iconSize * 1.1 / 2.0 ) ); + mIterateButton->setIconSize( QSize( iconSize, iconSize ) ); + + layout->addWidget( mIterateButton ); + layout->setAlignment( mIterateButton, Qt::AlignTop ); + } + + if ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() ) + { + mSettingsButton = new QToolButton(); + mSettingsButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionOptions.svg" ) ) ); + mSettingsButton->setToolTip( tr( "Advanced options" ) ); + + // button width is 1.25 * icon size, height 1.1 * icon size. But we round to ensure even pixel sizes for equal margins + mSettingsButton->setFixedSize( 2 * static_cast< int >( 1.25 * iconSize / 2.0 ), 2 * static_cast< int >( iconSize * 1.1 / 2.0 ) ); + mSettingsButton->setIconSize( QSize( iconSize, iconSize ) ); + mSettingsButton->setAutoRaise( true ); + + connect( mSettingsButton, &QToolButton::clicked, this, &QgsProcessingMapLayerComboBox::showSourceOptions ); + layout->addWidget( mSettingsButton ); + layout->setAlignment( mSettingsButton, Qt::AlignTop ); + } + mSelectButton = new QToolButton(); mSelectButton->setText( QString( QChar( 0x2026 ) ) ); - mSelectButton->setToolTip( tr( "Select file" ) ); - connect( mSelectButton, &QToolButton::clicked, this, &QgsProcessingMapLayerComboBox::triggerFileSelection ); + mSelectButton->setToolTip( tr( "Select input" ) ); layout->addWidget( mSelectButton ); layout->setAlignment( mSelectButton, Qt::AlignTop ); + if ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() ) + { + mFeatureSourceMenu = new QMenu( this ); + QAction *selectFromFileAction = new QAction( tr( "Select File…" ), mFeatureSourceMenu ); + connect( selectFromFileAction, &QAction::triggered, this, &QgsProcessingMapLayerComboBox::selectFromFile ); + mFeatureSourceMenu->addAction( selectFromFileAction ); + QAction *browseForLayerAction = new QAction( tr( "Browse for Layer…" ), mFeatureSourceMenu ); + connect( browseForLayerAction, &QAction::triggered, this, &QgsProcessingMapLayerComboBox::browseForLayer ); + mFeatureSourceMenu->addAction( browseForLayerAction ); + mSelectButton->setMenu( mFeatureSourceMenu ); + mSelectButton->setPopupMode( QToolButton::InstantPopup ); + } + else + { + connect( mSelectButton, &QToolButton::clicked, this, &QgsProcessingMapLayerComboBox::selectFromFile ); + } QVBoxLayout *vl = new QVBoxLayout(); vl->setMargin( 0 ); @@ -56,7 +113,7 @@ QgsProcessingMapLayerComboBox::QgsProcessingMapLayerComboBox( const QgsProcessin QgsMapLayerProxyModel::Filters filters = nullptr; - if ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() ) + if ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() && type == QgsProcessingGui::Standard ) { mUseSelectionCheckBox = new QCheckBox( tr( "Selected features only" ) ); mUseSelectionCheckBox->setChecked( false ); @@ -91,6 +148,26 @@ QgsProcessingMapLayerComboBox::QgsProcessingMapLayerComboBox( const QgsProcessin { filters = QgsMapLayerProxyModel::MeshLayer; } + else if ( mParameter->type() == QgsProcessingParameterMapLayer::typeName() ) + { + QList dataTypes; + dataTypes = static_cast< QgsProcessingParameterMapLayer *>( mParameter.get() )->dataTypes(); + + if ( dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) ) + filters |= QgsMapLayerProxyModel::HasGeometry; + if ( dataTypes.contains( QgsProcessing::TypeVectorPoint ) ) + filters |= QgsMapLayerProxyModel::PointLayer; + if ( dataTypes.contains( QgsProcessing::TypeVectorLine ) ) + filters |= QgsMapLayerProxyModel::LineLayer; + if ( dataTypes.contains( QgsProcessing::TypeVectorPolygon ) ) + filters |= QgsMapLayerProxyModel::PolygonLayer; + if ( dataTypes.contains( QgsProcessing::TypeRaster ) ) + filters |= QgsMapLayerProxyModel::RasterLayer; + if ( dataTypes.contains( QgsProcessing::TypeMesh ) ) + filters |= QgsMapLayerProxyModel::MeshLayer; + if ( !filters ) + filters = QgsMapLayerProxyModel::All; + } QgsSettings settings; if ( settings.value( QStringLiteral( "Processing/Configuration/SHOW_CRS_DEF" ), true ).toBool() ) @@ -141,14 +218,31 @@ QString QgsProcessingMapLayerComboBox::currentText() void QgsProcessingMapLayerComboBox::setValue( const QVariant &value, QgsProcessingContext &context ) { + if ( !value.isValid() && mParameter->flags() & QgsProcessingParameterDefinition::FlagOptional ) + { + setLayer( nullptr ); + return; + } + QVariant val = value; bool found = false; bool selectedOnly = false; + bool iterate = false; if ( val.canConvert() ) { QgsProcessingFeatureSourceDefinition fromVar = qvariant_cast( val ); val = fromVar.source; selectedOnly = fromVar.selectedFeaturesOnly; + iterate = fromVar.flags & QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature; + mFeatureLimit = fromVar.featureLimit; + mIsOverridingDefaultGeometryCheck = fromVar.flags & QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck; + mGeometryCheck = fromVar.geometryCheck; + } + else + { + mFeatureLimit = -1; + mIsOverridingDefaultGeometryCheck = false; + mGeometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid; } if ( val.canConvert() ) @@ -190,6 +284,11 @@ void QgsProcessingMapLayerComboBox::setValue( const QVariant &value, QgsProcessi mUseSelectionCheckBox->setChecked( false ); mUseSelectionCheckBox->setEnabled( false ); } + + if ( mIterateButton ) + { + mIterateButton->setChecked( iterate ); + } } mBlockChangedSignal--; if ( changed ) @@ -204,6 +303,9 @@ void QgsProcessingMapLayerComboBox::setValue( const QVariant &value, QgsProcessi mUseSelectionCheckBox->setChecked( false ); mUseSelectionCheckBox->setEnabled( false ); } + if ( mIterateButton ) + mIterateButton->setChecked( iterate ); + if ( !string.isEmpty() ) { mBlockChangedSignal++; @@ -227,10 +329,18 @@ void QgsProcessingMapLayerComboBox::setValue( const QVariant &value, QgsProcessi QVariant QgsProcessingMapLayerComboBox::value() const { + if ( isEditable() && mCombo->currentText() != mCombo->itemText( mCombo->currentIndex() ) ) + return mCombo->currentText(); + + const bool iterate = mIterateButton && mIterateButton->isChecked(); + const bool selectedOnly = mUseSelectionCheckBox && mUseSelectionCheckBox->isChecked(); if ( QgsMapLayer *layer = mCombo->currentLayer() ) { - if ( mUseSelectionCheckBox && mUseSelectionCheckBox->isChecked() ) - return QgsProcessingFeatureSourceDefinition( layer->id(), true ); + if ( selectedOnly || iterate || mFeatureLimit != -1 || mIsOverridingDefaultGeometryCheck ) + return QgsProcessingFeatureSourceDefinition( layer->id(), selectedOnly, mFeatureLimit, + ( iterate ? QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature : QgsProcessingFeatureSourceDefinition::Flags() ) + | ( mIsOverridingDefaultGeometryCheck ? QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck : QgsProcessingFeatureSourceDefinition::Flags() ), + mGeometryCheck ); else return layer->id(); } @@ -238,8 +348,11 @@ QVariant QgsProcessingMapLayerComboBox::value() const { if ( !mCombo->currentText().isEmpty() ) { - if ( mUseSelectionCheckBox && mUseSelectionCheckBox->isChecked() ) - return QgsProcessingFeatureSourceDefinition( mCombo->currentText(), true ); + if ( selectedOnly || iterate || mFeatureLimit != -1 || mIsOverridingDefaultGeometryCheck ) + return QgsProcessingFeatureSourceDefinition( mCombo->currentText(), selectedOnly, mFeatureLimit, + ( iterate ? QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature : QgsProcessingFeatureSourceDefinition::Flags() ) + | ( mIsOverridingDefaultGeometryCheck ? QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck : QgsProcessingFeatureSourceDefinition::Flags() ), + mGeometryCheck ); else return mCombo->currentText(); } @@ -247,6 +360,20 @@ QVariant QgsProcessingMapLayerComboBox::value() const return QVariant(); } +void QgsProcessingMapLayerComboBox::setWidgetContext( const QgsProcessingParameterWidgetContext &context ) +{ + mBrowserModel = context.browserModel(); +} + +void QgsProcessingMapLayerComboBox::setEditable( bool editable ) +{ + mCombo->setEditable( editable ); +} + +bool QgsProcessingMapLayerComboBox::isEditable() const +{ + return mCombo->isEditable(); +} QgsMapLayer *QgsProcessingMapLayerComboBox::compatibleMapLayerFromMimeData( const QMimeData *data, bool &incompatibleLayerSelected ) const { @@ -276,47 +403,89 @@ QString QgsProcessingMapLayerComboBox::compatibleUriFromMimeData( const QMimeDat for ( const QgsMimeDataUtils::Uri &u : uriList ) { if ( ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() - || mParameter->type() == QgsProcessingParameterVectorLayer::typeName() - || mParameter->type() == QgsProcessingParameterMapLayer::typeName() ) - && u.layerType == QLatin1String( "vector" ) && u.providerKey == QLatin1String( "ogr" ) ) + || mParameter->type() == QgsProcessingParameterVectorLayer::typeName() ) + && u.layerType == QLatin1String( "vector" ) ) { QList< int > dataTypes = mParameter->type() == QgsProcessingParameterFeatureSource::typeName() ? static_cast< QgsProcessingParameterFeatureSource * >( mParameter.get() )->dataTypes() : ( mParameter->type() == QgsProcessingParameterVectorLayer::typeName() ? static_cast( mParameter.get() )->dataTypes() : QList< int >() ); + bool acceptable = false; switch ( QgsWkbTypes::geometryType( u.wkbType ) ) { case QgsWkbTypes::UnknownGeometry: - return u.uri; + acceptable = true; + break; case QgsWkbTypes::PointGeometry: if ( dataTypes.isEmpty() || dataTypes.contains( QgsProcessing::TypeVector ) || dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) || dataTypes.contains( QgsProcessing::TypeVectorPoint ) ) - return u.uri; + acceptable = true; break; case QgsWkbTypes::LineGeometry: if ( dataTypes.isEmpty() || dataTypes.contains( QgsProcessing::TypeVector ) || dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) || dataTypes.contains( QgsProcessing::TypeVectorLine ) ) - return u.uri; + acceptable = true; break; case QgsWkbTypes::PolygonGeometry: if ( dataTypes.isEmpty() || dataTypes.contains( QgsProcessing::TypeVector ) || dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) || dataTypes.contains( QgsProcessing::TypeVectorPolygon ) ) - return u.uri; + acceptable = true; break; case QgsWkbTypes::NullGeometry: if ( dataTypes.contains( QgsProcessing::TypeVector ) ) - return u.uri; + acceptable = true; break; } + if ( acceptable ) + return u.providerKey != QLatin1String( "ogr" ) ? QgsProcessingUtils::encodeProviderKeyAndUri( u.providerKey, u.uri ) : u.uri; } - else if ( ( mParameter->type() == QgsProcessingParameterRasterLayer::typeName() - || mParameter->type() == QgsProcessingParameterMapLayer::typeName() ) + else if ( mParameter->type() == QgsProcessingParameterRasterLayer::typeName() && u.layerType == QLatin1String( "raster" ) && u.providerKey == QLatin1String( "gdal" ) ) return u.uri; - else if ( ( mParameter->type() == QgsProcessingParameterMeshLayer::typeName() - || mParameter->type() == QgsProcessingParameterMapLayer::typeName() ) + else if ( mParameter->type() == QgsProcessingParameterMeshLayer::typeName() && u.layerType == QLatin1String( "mesh" ) && u.providerKey == QLatin1String( "mdal" ) ) return u.uri; + else if ( mParameter->type() == QgsProcessingParameterMapLayer::typeName() ) + { + QList< int > dataTypes = static_cast< QgsProcessingParameterMapLayer * >( mParameter.get() )->dataTypes(); + if ( dataTypes.isEmpty() || dataTypes.contains( QgsProcessing::TypeMapLayer ) ) + { + return u.uri; + } + + if ( u.layerType == QLatin1String( "vector" ) && u.providerKey == QLatin1String( "ogr" ) ) + { + switch ( QgsWkbTypes::geometryType( u.wkbType ) ) + { + case QgsWkbTypes::UnknownGeometry: + return u.uri; + + case QgsWkbTypes::PointGeometry: + if ( dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) || dataTypes.contains( QgsProcessing::TypeVectorPoint ) ) + return u.uri; + break; + + case QgsWkbTypes::LineGeometry: + if ( dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) || dataTypes.contains( QgsProcessing::TypeVectorLine ) ) + return u.uri; + break; + + case QgsWkbTypes::PolygonGeometry: + if ( dataTypes.contains( QgsProcessing::TypeVectorAnyGeometry ) || dataTypes.contains( QgsProcessing::TypeVectorPolygon ) ) + return u.uri; + break; + + case QgsWkbTypes::NullGeometry: + return u.uri; + } + } + else if ( u.layerType == QLatin1String( "raster" ) && u.providerKey == QLatin1String( "gdal" ) + && dataTypes.contains( QgsProcessing::TypeRaster ) ) + return u.uri; + else if ( u.layerType == QLatin1String( "mesh" ) && u.providerKey == QLatin1String( "mdal" ) + && dataTypes.contains( QgsProcessing::TypeMesh ) ) + return u.uri; + } } if ( !uriList.isEmpty() ) return QString(); @@ -405,7 +574,7 @@ void QgsProcessingMapLayerComboBox::dropEvent( QDropEvent *event ) void QgsProcessingMapLayerComboBox::onLayerChanged( QgsMapLayer *layer ) { - if ( mParameter->type() == QgsProcessingParameterFeatureSource::typeName() ) + if ( mUseSelectionCheckBox && mParameter->type() == QgsProcessingParameterFeatureSource::typeName() ) { if ( QgsVectorLayer *vl = qobject_cast< QgsVectorLayer * >( layer ) ) { @@ -432,6 +601,89 @@ void QgsProcessingMapLayerComboBox::selectionChanged( const QgsFeatureIds &selec mUseSelectionCheckBox->setEnabled( !selected.isEmpty() ); } +void QgsProcessingMapLayerComboBox::showSourceOptions() +{ + if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) ) + { + QgsProcessingFeatureSourceOptionsWidget *widget = new QgsProcessingFeatureSourceOptionsWidget(); + widget->setPanelTitle( tr( "%1 Options" ).arg( mParameter->description() ) ); + + widget->setGeometryCheckMethod( mIsOverridingDefaultGeometryCheck, mGeometryCheck ); + widget->setFeatureLimit( mFeatureLimit ); + + panel->openPanel( widget ); + + connect( widget, &QgsPanelWidget::widgetChanged, this, [ = ] + { + bool changed = false; + changed = changed | ( widget->featureLimit() != mFeatureLimit ); + changed = changed | ( widget->isOverridingInvalidGeometryCheck() != mIsOverridingDefaultGeometryCheck ); + changed = changed | ( widget->geometryCheckMethod() != mGeometryCheck ); + + mFeatureLimit = widget->featureLimit(); + mIsOverridingDefaultGeometryCheck = widget->isOverridingInvalidGeometryCheck(); + mGeometryCheck = widget->geometryCheckMethod(); + + if ( changed ) + emit valueChanged(); + } ); + } +} + +void QgsProcessingMapLayerComboBox::selectFromFile() +{ + QgsSettings settings; + const QString initialValue = currentText(); + QString path; + + if ( QFileInfo( initialValue ).isDir() && QFileInfo::exists( initialValue ) ) + path = initialValue; + else if ( QFileInfo::exists( QFileInfo( initialValue ).path() ) && QFileInfo( initialValue ).path() != '.' ) + path = QFileInfo( initialValue ).path(); + else if ( settings.contains( QStringLiteral( "/Processing/LastInputPath" ) ) ) + path = settings.value( QStringLiteral( "/Processing/LastInputPath" ) ).toString(); + + QString filter; + if ( const QgsFileFilterGenerator *generator = dynamic_cast< const QgsFileFilterGenerator * >( mParameter.get() ) ) + filter = generator->createFileFilter(); + else + filter = QObject::tr( "All files (*.*)" ); + + const QString filename = QFileDialog::getOpenFileName( this, tr( "Select File" ), path, filter ); + if ( filename.isEmpty() ) + return; + + settings.setValue( QStringLiteral( "/Processing/LastInputPath" ), QFileInfo( filename ).path() ); + QgsProcessingContext context; + setValue( filename, context ); +} + +void QgsProcessingMapLayerComboBox::browseForLayer() +{ + if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) ) + { + QgsDataSourceSelectWidget *widget = new QgsDataSourceSelectWidget( mBrowserModel, true, QgsMapLayerType::VectorLayer ); + widget->setPanelTitle( tr( "Browse for \"%1\"" ).arg( mParameter->description() ) ); + + panel->openPanel( widget ); + + connect( widget, &QgsDataSourceSelectWidget::itemTriggered, this, [ = ]( const QgsMimeDataUtils::Uri & ) + { + widget->acceptPanel(); + } ); + connect( widget, &QgsPanelWidget::panelAccepted, this, [ = ]() + { + QgsProcessingContext context; + if ( widget->uri().uri.isEmpty() ) + setValue( QVariant(), context ); + else if ( widget->uri().providerKey == QLatin1String( "ogr" ) ) + setValue( widget->uri().uri, context ); + else + setValue( QgsProcessingUtils::encodeProviderKeyAndUri( widget->uri().providerKey, widget->uri().uri ), context ); + } ); + } +} + ///@endcond diff --git a/src/gui/processing/qgsprocessingmaplayercombobox.h b/src/gui/processing/qgsprocessingmaplayercombobox.h index 3fc14f4aba1d..ef29eda2fdbd 100644 --- a/src/gui/processing/qgsprocessingmaplayercombobox.h +++ b/src/gui/processing/qgsprocessingmaplayercombobox.h @@ -23,12 +23,14 @@ #include "qgsfeatureid.h" #include "qgsmimedatautils.h" #include "qgsprocessingcontext.h" - +#include "qgsprocessinggui.h" class QgsMapLayerComboBox; class QToolButton; class QCheckBox; class QgsProcessingParameterDefinition; +class QgsBrowserGuiModel; +class QgsProcessingParameterWidgetContext; ///@cond PRIVATE @@ -47,7 +49,7 @@ class GUI_EXPORT QgsProcessingMapLayerComboBox : public QWidget /** * Constructor for QgsProcessingMapLayerComboBox, with the specified \a parameter definition. */ - QgsProcessingMapLayerComboBox( const QgsProcessingParameterDefinition *parameter, QWidget *parent = nullptr ); + QgsProcessingMapLayerComboBox( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); ~QgsProcessingMapLayerComboBox() override; @@ -92,18 +94,34 @@ class GUI_EXPORT QgsProcessingMapLayerComboBox : public QWidget */ QVariant value() const; - signals: + /** + * Sets the \a context in which the widget is shown. + * \since QGIS 3.14 + */ + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ); /** - * Emitted whenever the value is changed in the widget. + * Sets whether the combo box value can be freely edited. + * + * \see isEditable() + * \since QGIS 3.14 */ - void valueChanged(); + void setEditable( bool editable ); /** - * Emitted when the widget has triggered a file selection operation (to be - * handled in Python for now). + * Returns whether the combo box value can be freely edited. + * + * \see setEditable() + * \since QGIS 3.14 */ - void triggerFileSelection(); + bool isEditable() const; + + signals: + + /** + * Emitted whenever the value is changed in the widget. + */ + void valueChanged(); protected: @@ -115,15 +133,27 @@ class GUI_EXPORT QgsProcessingMapLayerComboBox : public QWidget void onLayerChanged( QgsMapLayer *layer ); void selectionChanged( const QgsFeatureIds &selected, const QgsFeatureIds &deselected, bool clearAndSelect ); + void showSourceOptions(); + void selectFromFile(); + void browseForLayer(); private: std::unique_ptr< QgsProcessingParameterDefinition > mParameter; QgsMapLayerComboBox *mCombo = nullptr; QToolButton *mSelectButton = nullptr; + QToolButton *mIterateButton = nullptr; + QToolButton *mSettingsButton = nullptr; QCheckBox *mUseSelectionCheckBox = nullptr; bool mDragActive = false; + long long mFeatureLimit = -1; + bool mIsOverridingDefaultGeometryCheck = false; + QgsFeatureRequest::InvalidGeometryCheck mGeometryCheck = QgsFeatureRequest::GeometryAbortOnInvalid; QPointer< QgsMapLayer> mPrevLayer; int mBlockChangedSignal = 0; + + QgsBrowserGuiModel *mBrowserModel = nullptr; + + QMenu *mFeatureSourceMenu = nullptr; QgsMapLayer *compatibleMapLayerFromMimeData( const QMimeData *data, bool &incompatibleLayerSelected ) const; QString compatibleUriFromMimeData( const QMimeData *data ) const; }; diff --git a/src/gui/processing/qgsprocessingmatrixmodelerwidget.cpp b/src/gui/processing/qgsprocessingmatrixmodelerwidget.cpp new file mode 100644 index 000000000000..e5e28d958fec --- /dev/null +++ b/src/gui/processing/qgsprocessingmatrixmodelerwidget.cpp @@ -0,0 +1,162 @@ +/*************************************************************************** + qgsprocessingmatrixmodelerwidget.cpp + ------------------------------------ + Date : March 2020 + Copyright : (C) 2020 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 "qgsprocessingmatrixmodelerwidget.h" +#include "qgsgui.h" +#include +#include +#include +#include + +///@cond NOT_STABLE + +QgsProcessingMatrixModelerWidget::QgsProcessingMatrixModelerWidget( QWidget *parent ) + : QWidget( parent ) +{ + setupUi( this ); + + mModel = new QStandardItemModel( this ); + mModel->appendColumn( QList< QStandardItem * >() << new QStandardItem( '0' ) ); + mTableView->setModel( mModel ); + + connect( mButtonAddColumn, &QToolButton::clicked, this, &QgsProcessingMatrixModelerWidget::addColumn ); + connect( mButtonRemoveColumn, &QToolButton::clicked, this, &QgsProcessingMatrixModelerWidget::removeColumns ); + connect( mButtonAddRow, &QToolButton::clicked, this, &QgsProcessingMatrixModelerWidget::addRow ); + connect( mButtonRemoveRow, &QToolButton::clicked, this, &QgsProcessingMatrixModelerWidget::removeRows ); + connect( mButtonClear, &QToolButton::clicked, this, &QgsProcessingMatrixModelerWidget::clearTable ); + connect( mTableView->horizontalHeader(), &QHeaderView::sectionDoubleClicked, this, &QgsProcessingMatrixModelerWidget::changeHeader ); +} + +void QgsProcessingMatrixModelerWidget::addColumn() +{ + QList< QStandardItem * > items; + for ( int i = 0; i < mModel->rowCount(); ++i ) + items << new QStandardItem( '0' ); + + mModel->appendColumn( items ); +} + +void QgsProcessingMatrixModelerWidget::removeColumns() +{ + QModelIndexList selected = mTableView->selectionModel()->selectedColumns(); + std::sort( selected.begin(), selected.end(), []( const QModelIndex & a, const QModelIndex & b ) { return b < a; } ); + + mTableView->setUpdatesEnabled( false ); + for ( QModelIndex i : qgis::as_const( selected ) ) + mModel->removeColumns( i.column(), 1 ); + + mTableView->setUpdatesEnabled( true ); +} + +void QgsProcessingMatrixModelerWidget::addRow() +{ + QList< QStandardItem * > items; + for ( int i = 0; i < mModel->columnCount(); ++i ) + items << new QStandardItem( '0' ); + + mModel->appendRow( items ); +} + +void QgsProcessingMatrixModelerWidget::removeRows() +{ + QModelIndexList selected = mTableView->selectionModel()->selectedRows(); + std::sort( selected.begin(), selected.end(), []( const QModelIndex & a, const QModelIndex & b ) { return b < a; } ); + + mTableView->setUpdatesEnabled( false ); + for ( QModelIndex i : qgis::as_const( selected ) ) + mModel->removeRows( i.row(), 1 ); + + mTableView->setUpdatesEnabled( true ); +} + +void QgsProcessingMatrixModelerWidget::clearTable() +{ + if ( QMessageBox::question( nullptr, tr( "Clear table" ), + tr( "Are you sure you want to clear table?" ), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) == QMessageBox::Yes ) + mModel->clear(); +} + +void QgsProcessingMatrixModelerWidget::changeHeader( int index ) +{ + bool ok; + QString text = QInputDialog::getText( nullptr, tr( "Enter column name" ), + tr( "Column name" ), QLineEdit::Normal, + QString(), &ok ); + if ( ok && !text.isEmpty() ) + mModel->setHeaderData( index, Qt::Horizontal, text ); +} + +QStringList QgsProcessingMatrixModelerWidget::headers() const +{ + QStringList headers; + for ( int i = 0; i < mModel->columnCount(); ++i ) + { + headers << mModel->headerData( i, Qt::Horizontal ).toString(); + } + return headers; +} + +QVariant QgsProcessingMatrixModelerWidget::value() const +{ + QVariantList defaults; + const int cols = mModel->columnCount(); + const int rows = mModel->rowCount(); + + for ( int row = 0; row < rows; ++row ) + { + for ( int col = 0; col < cols; ++col ) + { + defaults << mModel->item( row, col )->text(); + } + } + + QVariant val( defaults ); + return val; +} + +void QgsProcessingMatrixModelerWidget::setValue( const QStringList &headers, const QVariant &defaultValue ) +{ + QVariantList contents = defaultValue.toList(); + + const int cols = headers.count(); + const int rows = contents.count() / cols; + + mModel->setRowCount( rows ); + mModel->setColumnCount( cols ); + mModel->setHorizontalHeaderLabels( headers ); + + for ( int row = 0; row < rows; ++row ) + { + for ( int col = 0; col < cols; ++col ) + { + QStandardItem *item = new QStandardItem( contents.at( row * cols + col ).toString() ); + mModel->setItem( row, col, item ); + } + } + mTableView->setModel( mModel ); +} + +bool QgsProcessingMatrixModelerWidget::fixedRows() const +{ + return mFixedRows->isChecked(); +} + +void QgsProcessingMatrixModelerWidget::setFixedRows( bool fixedRows ) +{ + mFixedRows->setChecked( fixedRows ); +} + +///@endcond diff --git a/src/gui/processing/qgsprocessingmatrixmodelerwidget.h b/src/gui/processing/qgsprocessingmatrixmodelerwidget.h new file mode 100644 index 000000000000..b252bb121671 --- /dev/null +++ b/src/gui/processing/qgsprocessingmatrixmodelerwidget.h @@ -0,0 +1,94 @@ +/*************************************************************************** + qgsprocessingmatrixmodelerwidget.h + ---------------------------------- + Date : March 2020 + Copyright : (C) 2020 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 QGSPROCESSINGMATRIXMODELERWIDGET_H +#define QGSPROCESSINGMATRIXMODELERWIDGET_H + +#define SIP_NO_FILE + +#include "qgis.h" +#include "qgis_gui.h" +#include "ui_qgsprocessingmatrixmodelerwidgetbase.h" +#include +#include + +///@cond PRIVATE + +/** + * Processing matrix widget for configuring matrix parameter in modeler. + * \ingroup gui + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingMatrixModelerWidget : public QWidget, private Ui::QgsProcessingMatrixModelerWidgetBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingMatrixModelerWidget. + */ + QgsProcessingMatrixModelerWidget( QWidget *parent = nullptr ); + + /** + * Returns list of matrix headers. + */ + QStringList headers() const; + + /** + * Returns matrix defined by user. + * + * \see setValue() + */ + QVariant value() const; + + /** + * Sets value of the widget. + * + * \see value() + */ + void setValue( const QStringList &headers, const QVariant &defaultValue ); + + /** + * Returns TRUE if the parameter has fixed number of rows. + * \see setFixedRows() + */ + bool fixedRows() const; + + /** + * Sets whether the parameter has fixed number of rows. + * \see fixedRows() + */ + void setFixedRows( bool fixedRows ); + + private slots: + + void addColumn(); + void removeColumns(); + void addRow(); + void removeRows(); + void clearTable(); + void changeHeader( int index ); + + private: + QStandardItemModel *mModel = nullptr; + + friend class TestProcessingGui; +}; + +///@endcond + +#endif // QGSPROCESSINGMATRIXMODELERWIDGET_H diff --git a/src/gui/processing/qgsprocessingmodelerparameterwidget.cpp b/src/gui/processing/qgsprocessingmodelerparameterwidget.cpp index 3c53a4e77dc1..c04b368055af 100644 --- a/src/gui/processing/qgsprocessingmodelerparameterwidget.cpp +++ b/src/gui/processing/qgsprocessingmodelerparameterwidget.cpp @@ -26,6 +26,7 @@ #include "qgsguiutils.h" #include "qgsexpressioncontext.h" #include "qgsapplication.h" +#include "qgsfilterlineedit.h" #include #include #include @@ -108,12 +109,25 @@ QgsProcessingModelerParameterWidget::QgsProcessingModelerParameterWidget( QgsPro hWidget3->setLayout( hLayout3 ); mStackedWidget->addWidget( hWidget3 ); + if ( mParameterDefinition->isDestination() ) + { + mModelOutputName = new QgsFilterLineEdit(); + mModelOutputName->setPlaceholderText( tr( "[Enter name if this is a final result]" ) ); + QHBoxLayout *hLayout4 = new QHBoxLayout(); + hLayout4->setMargin( 0 ); + hLayout4->setContentsMargins( 0, 0, 0, 0 ); + hLayout4->addWidget( mModelOutputName ); + QWidget *hWidget4 = new QWidget(); + hWidget4->setLayout( hLayout4 ); + mStackedWidget->addWidget( hWidget4 ); + } + hLayout->setMargin( 0 ); hLayout->setContentsMargins( 0, 0, 0, 0 ); hLayout->addWidget( mStackedWidget, 1 ); setLayout( hLayout ); - setSourceType( QgsProcessingModelChildParameterSource::StaticValue ); + setSourceType( mParameterDefinition->isDestination() ? QgsProcessingModelChildParameterSource::ModelOutput : QgsProcessingModelChildParameterSource::StaticValue ); } QgsProcessingModelerParameterWidget::~QgsProcessingModelerParameterWidget() = default; @@ -157,27 +171,77 @@ void QgsProcessingModelerParameterWidget::setWidgetValue( const QgsProcessingMod setSourceType( value.source() ); } -QgsProcessingModelChildParameterSource QgsProcessingModelerParameterWidget::value() const +void QgsProcessingModelerParameterWidget::setWidgetValue( const QList &values ) +{ + if ( values.size() == 1 ) + setWidgetValue( values.at( 0 ) ); + else + { + QVariantList r; + for ( const QgsProcessingModelChildParameterSource &v : values ) + r << QVariant::fromValue( v ); + mStaticValue = r; + updateUi(); + setSourceType( QgsProcessingModelChildParameterSource::StaticValue ); + } +} + +void QgsProcessingModelerParameterWidget::setToModelOutput( const QString &value ) +{ + if ( mModelOutputName ) + mModelOutputName->setText( value ); + setSourceType( QgsProcessingModelChildParameterSource::ModelOutput ); +} + +bool QgsProcessingModelerParameterWidget::isModelOutput() const +{ + return currentSourceType() == ModelOutput; +} + +QString QgsProcessingModelerParameterWidget::modelOutputName() const +{ + return mModelOutputName ? mModelOutputName->text().trimmed() : QString(); +} + +QVariant QgsProcessingModelerParameterWidget::value() const { switch ( currentSourceType() ) { case StaticValue: - return QgsProcessingModelChildParameterSource::fromStaticValue( mStaticWidgetWrapper->parameterValue() ); + { + const QVariant v = mStaticWidgetWrapper->parameterValue(); + + if ( v.type() == QVariant::List ) + { + const QVariantList vList = v.toList(); + if ( std::all_of( vList.begin(), vList.end(), []( const QVariant & val ) + { + return val.canConvert< QgsProcessingModelChildParameterSource >(); + } ) ) + { + return v; + } + } + return QVariant::fromValue( QgsProcessingModelChildParameterSource::fromStaticValue( v ) ); + } case Expression: - return QgsProcessingModelChildParameterSource::fromExpression( mExpressionWidget->expression() ); + return QVariant::fromValue( QgsProcessingModelChildParameterSource::fromExpression( mExpressionWidget->expression() ) ); case ModelParameter: - return QgsProcessingModelChildParameterSource::fromModelParameter( mModelInputCombo->currentData().toString() ); + return QVariant::fromValue( QgsProcessingModelChildParameterSource::fromModelParameter( mModelInputCombo->currentData().toString() ) ); case ChildOutput: { const QStringList parts = mChildOutputCombo->currentData().toStringList(); - return QgsProcessingModelChildParameterSource::fromChildOutput( parts.value( 0, QString() ), parts.value( 1, QString() ) ); + return QVariant::fromValue( QgsProcessingModelChildParameterSource::fromChildOutput( parts.value( 0, QString() ), parts.value( 1, QString() ) ) ); } + + case ModelOutput: + return mModelOutputName ? ( mModelOutputName->text().trimmed().isEmpty() ? QVariant() : mModelOutputName->text() ) : QVariant(); } - return QgsProcessingModelChildParameterSource(); + return QVariant::fromValue( QgsProcessingModelChildParameterSource() ); } void QgsProcessingModelerParameterWidget::setDialog( QDialog *dialog ) @@ -219,6 +283,14 @@ void QgsProcessingModelerParameterWidget::sourceMenuAboutToShow() const SourceType currentSource = currentSourceType(); + if ( mParameterDefinition->isDestination() ) + { + QAction *modelOutputAction = mSourceMenu->addAction( tr( "Model Output" ) ); + modelOutputAction->setCheckable( currentSource == ModelOutput ); + modelOutputAction->setChecked( currentSource == ModelOutput ); + modelOutputAction->setData( QgsProcessingModelChildParameterSource::ModelOutput ); + } + if ( mHasStaticWrapper ) { QAction *fixedValueAction = mSourceMenu->addAction( tr( "Value" ) ); @@ -288,6 +360,14 @@ void QgsProcessingModelerParameterWidget::setSourceType( QgsProcessingModelChild break; } + case QgsProcessingModelChildParameterSource::ModelOutput: + { + mSourceButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mIconModelOutput.svg" ) ) ); + mStackedWidget->setCurrentIndex( static_cast< int >( ModelOutput ) ); + mSourceButton->setToolTip( tr( "Model Output" ) ); + break; + } + case QgsProcessingModelChildParameterSource::ExpressionText: break; } @@ -342,6 +422,7 @@ void QgsProcessingModelerParameterWidget::populateSources( const QStringList &co case QgsProcessingModelChildParameterSource::StaticValue: case QgsProcessingModelChildParameterSource::Expression: case QgsProcessingModelChildParameterSource::ExpressionText: + case QgsProcessingModelChildParameterSource::ModelOutput: break; } diff --git a/src/gui/processing/qgsprocessingmodelerparameterwidget.h b/src/gui/processing/qgsprocessingmodelerparameterwidget.h index 5420b34e5cdd..0077b01631f5 100644 --- a/src/gui/processing/qgsprocessingmodelerparameterwidget.h +++ b/src/gui/processing/qgsprocessingmodelerparameterwidget.h @@ -32,6 +32,7 @@ class QgsExpressionLineEdit; class QgsProcessingModelAlgorithm; class QgsProcessingParameterWidgetContext; class QgsProcessingContextGenerator; +class QgsFilterLineEdit; class QLabel; class QToolButton; @@ -141,12 +142,47 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg */ virtual void setWidgetValue( const QgsProcessingModelChildParameterSource &value ); + /** + * Sets the current \a values for the parameter. + * + * \see value() + * \since QGIS 3.14 + */ + void setWidgetValue( const QList< QgsProcessingModelChildParameterSource > &values ); + + /** + * Sets the widget to a model output, for destination parameters only. + * + * \see isModelOutput() + * \see modelOutputName() + * \since QGIS 3.14 + */ + void setToModelOutput( const QString &value ); + + /** + * Returns TRUE if the widget is set to the model output mode. + * + * \see setToModelOutput() + * \see modelOutputName() + * \since QGIS 3.14 + */ + bool isModelOutput() const; + + /** + * Returns the model output name, if isModelOutput() is TRUE. + * + * \see setToModelOutput() + * \see isModelOutput() + * \since QGIS 3.14 + */ + QString modelOutputName() const; + /** * Returns the current value of the parameter. * * \see setWidgetValue() */ - virtual QgsProcessingModelChildParameterSource value() const; + virtual QVariant value() const; /** * Sets the parent \a dialog in which the widget is shown. @@ -171,6 +207,7 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg Expression = 1, ModelParameter = 2, ChildOutput = 3, + ModelOutput = 4, }; SourceType currentSourceType() const; @@ -198,6 +235,7 @@ class GUI_EXPORT QgsProcessingModelerParameterWidget : public QWidget, public Qg QgsExpressionLineEdit *mExpressionWidget = nullptr; QComboBox *mModelInputCombo = nullptr; QComboBox *mChildOutputCombo = nullptr; + QgsFilterLineEdit *mModelOutputName = nullptr; friend class TestProcessingGui; }; diff --git a/src/gui/processing/qgsprocessingmultipleselectiondialog.cpp b/src/gui/processing/qgsprocessingmultipleselectiondialog.cpp index 92448ecbe510..c989993ae91a 100644 --- a/src/gui/processing/qgsprocessingmultipleselectiondialog.cpp +++ b/src/gui/processing/qgsprocessingmultipleselectiondialog.cpp @@ -15,19 +15,37 @@ #include "qgsprocessingmultipleselectiondialog.h" #include "qgsgui.h" +#include "qgssettings.h" +#include "qgsfileutils.h" +#include "qgsvectorlayer.h" +#include "qgsmeshlayer.h" +#include "qgsrasterlayer.h" +#include "processing/models/qgsprocessingmodelchildparametersource.h" #include #include #include #include #include +#include ///@cond NOT_STABLE -QgsProcessingMultipleSelectionDialog::QgsProcessingMultipleSelectionDialog( const QVariantList &availableOptions, +QgsProcessingMultipleSelectionPanelWidget::QgsProcessingMultipleSelectionPanelWidget( const QVariantList &availableOptions, const QVariantList &selectedOptions, - QWidget *parent, Qt::WindowFlags flags ) - : QDialog( parent, flags ) - , mValueFormatter( []( const QVariant & v )->QString { return v.toString(); } ) + QWidget *parent ) + : QgsPanelWidget( parent ) + , mValueFormatter( []( const QVariant & v )->QString +{ + if ( v.canConvert< QgsProcessingModelChildParameterSource >() ) + { + return v.value< QgsProcessingModelChildParameterSource >().staticValue().toString(); + } + else + { + return v.toString(); + } + return QString(); +} ) { setupUi( this ); @@ -48,12 +66,15 @@ QgsProcessingMultipleSelectionDialog::QgsProcessingMultipleSelectionDialog( cons connect( mButtonSelectAll, &QPushButton::clicked, this, [ = ] { selectAll( true ); } ); connect( mButtonClearSelection, &QPushButton::clicked, this, [ = ] { selectAll( false ); } ); - connect( mButtonToggleSelection, &QPushButton::clicked, this, &QgsProcessingMultipleSelectionDialog::toggleSelection ); + connect( mButtonToggleSelection, &QPushButton::clicked, this, &QgsProcessingMultipleSelectionPanelWidget::toggleSelection ); + connect( mButtonBox, &QDialogButtonBox::accepted, this, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked ); populateList( availableOptions, selectedOptions ); + + connect( mModel, &QStandardItemModel::itemChanged, this, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged ); } -void QgsProcessingMultipleSelectionDialog::setValueFormatter( const std::function &formatter ) +void QgsProcessingMultipleSelectionPanelWidget::setValueFormatter( const std::function &formatter ) { mValueFormatter = formatter; // update item text using new formatter @@ -63,7 +84,7 @@ void QgsProcessingMultipleSelectionDialog::setValueFormatter( const std::functio } } -QVariantList QgsProcessingMultipleSelectionDialog::selectedOptions() const +QVariantList QgsProcessingMultipleSelectionPanelWidget::selectedOptions() const { QVariantList options; options.reserve( mModel->rowCount() ); @@ -75,7 +96,8 @@ QVariantList QgsProcessingMultipleSelectionDialog::selectedOptions() const return options; } -void QgsProcessingMultipleSelectionDialog::selectAll( const bool checked ) + +void QgsProcessingMultipleSelectionPanelWidget::selectAll( const bool checked ) { const QList items = currentItems(); for ( QStandardItem *item : items ) @@ -84,7 +106,7 @@ void QgsProcessingMultipleSelectionDialog::selectAll( const bool checked ) } } -void QgsProcessingMultipleSelectionDialog::toggleSelection() +void QgsProcessingMultipleSelectionPanelWidget::toggleSelection() { const QList items = currentItems(); for ( QStandardItem *item : items ) @@ -93,7 +115,7 @@ void QgsProcessingMultipleSelectionDialog::toggleSelection() } } -QList QgsProcessingMultipleSelectionDialog::currentItems() +QList QgsProcessingMultipleSelectionPanelWidget::currentItems() { QList items; const QModelIndexList selection = mSelectionList->selectionModel()->selectedIndexes(); @@ -116,7 +138,7 @@ QList QgsProcessingMultipleSelectionDialog::currentItems() return items; } -void QgsProcessingMultipleSelectionDialog::populateList( const QVariantList &availableOptions, const QVariantList &selectedOptions ) +void QgsProcessingMultipleSelectionPanelWidget::populateList( const QVariantList &availableOptions, const QVariantList &selectedOptions ) { mModel = new QStandardItemModel( this ); @@ -128,26 +150,296 @@ void QgsProcessingMultipleSelectionDialog::populateList( const QVariantList &ava // if isinstance(t, QgsProcessingModelChildParameterSource): // item = QStandardItem(t.staticValue()) // else: - std::unique_ptr< QStandardItem > item = qgis::make_unique< QStandardItem >( mValueFormatter( option ) ); - item->setData( option, Qt::UserRole ); - item->setCheckState( Qt::Checked ); - item->setCheckable( true ); - item->setDropEnabled( false ); - mModel->appendRow( item.release() ); + + addOption( option, mValueFormatter( option ), true ); remainingOptions.removeAll( option ); } for ( const QVariant &option : qgis::as_const( remainingOptions ) ) { - std::unique_ptr< QStandardItem > item = qgis::make_unique< QStandardItem >( mValueFormatter( option ) ); - item->setData( option, Qt::UserRole ); - item->setCheckState( Qt::Unchecked ); - item->setCheckable( true ); - item->setDropEnabled( false ); - mModel->appendRow( item.release() ); + addOption( option, mValueFormatter( option ), false ); } mSelectionList->setModel( mModel ); } + +void QgsProcessingMultipleSelectionPanelWidget::addOption( const QVariant &value, const QString &title, bool selected, bool updateExistingTitle ) +{ + // don't add duplicate options + for ( int i = 0; i < mModel->rowCount(); ++i ) + { + if ( mModel->item( i )->data( Qt::UserRole ) == value || + ( mModel->item( i )->data( Qt::UserRole ).canConvert< QgsProcessingModelChildParameterSource >() && + value.canConvert< QgsProcessingModelChildParameterSource >() && + mModel->item( i )->data( Qt::UserRole ).value< QgsProcessingModelChildParameterSource >() == + value.value< QgsProcessingModelChildParameterSource >() ) + ) + { + if ( updateExistingTitle ) + mModel->item( i )->setText( title ); + return; + } + } + + std::unique_ptr< QStandardItem > item = qgis::make_unique< QStandardItem >( title ); + item->setData( value, Qt::UserRole ); + item->setCheckState( selected ? Qt::Checked : Qt::Unchecked ); + item->setCheckable( true ); + item->setDropEnabled( false ); + mModel->appendRow( item.release() ); +} + +// +// QgsProcessingMultipleSelectionDialog +// + + + +QgsProcessingMultipleSelectionDialog::QgsProcessingMultipleSelectionDialog( const QVariantList &availableOptions, const QVariantList &selectedOptions, QWidget *parent, Qt::WindowFlags flags ) + : QDialog( parent, flags ) +{ + setWindowTitle( tr( "Multiple Selection" ) ); + QVBoxLayout *vLayout = new QVBoxLayout(); + mWidget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, selectedOptions ); + vLayout->addWidget( mWidget ); + mWidget->buttonBox()->addButton( QDialogButtonBox::Cancel ); + connect( mWidget->buttonBox(), &QDialogButtonBox::accepted, this, &QDialog::accept ); + connect( mWidget->buttonBox(), &QDialogButtonBox::rejected, this, &QDialog::reject ); + setLayout( vLayout ); +} + +void QgsProcessingMultipleSelectionDialog::setValueFormatter( const std::function &formatter ) +{ + mWidget->setValueFormatter( formatter ); +} + +QVariantList QgsProcessingMultipleSelectionDialog::selectedOptions() const +{ + return mWidget->selectedOptions(); +} + + +// +// QgsProcessingMultipleInputPanelWidget +// + +QgsProcessingMultipleInputPanelWidget::QgsProcessingMultipleInputPanelWidget( const QgsProcessingParameterMultipleLayers *parameter, const QVariantList &selectedOptions, + const QList &modelSources, + QgsProcessingModelAlgorithm *model, QWidget *parent ) + : QgsProcessingMultipleSelectionPanelWidget( QVariantList(), selectedOptions, parent ) + , mParameter( parameter ) +{ + QPushButton *addFileButton = new QPushButton( tr( "Add File(s)…" ) ); + connect( addFileButton, &QPushButton::clicked, this, &QgsProcessingMultipleInputPanelWidget::addFiles ); + buttonBox()->addButton( addFileButton, QDialogButtonBox::ActionRole ); + + QPushButton *addDirButton = new QPushButton( tr( "Add Directory…" ) ); + connect( addDirButton, &QPushButton::clicked, this, &QgsProcessingMultipleInputPanelWidget::addDirectory ); + buttonBox()->addButton( addDirButton, QDialogButtonBox::ActionRole ); + + for ( const QgsProcessingModelChildParameterSource &source : modelSources ) + { + addOption( QVariant::fromValue( source ), source.friendlyIdentifier( model ), false, true ); + } +} + +void QgsProcessingMultipleInputPanelWidget::setProject( QgsProject *project ) +{ + if ( mParameter->layerType() != QgsProcessing::TypeFile ) + populateFromProject( project ); +} + +void QgsProcessingMultipleInputPanelWidget::addFiles() +{ + QgsSettings settings; + QString path = settings.value( QStringLiteral( "/Processing/LastInputPath" ), QDir::homePath() ).toString(); + + QString filter; + if ( const QgsFileFilterGenerator *generator = dynamic_cast< const QgsFileFilterGenerator * >( mParameter ) ) + filter = generator->createFileFilter(); + else + filter = QObject::tr( "All files (*.*)" ); + + const QStringList filenames = QFileDialog::getOpenFileNames( this, tr( "Select File(s)" ), path, filter ); + if ( filenames.empty() ) + return; + + settings.setValue( QStringLiteral( "/Processing/LastInputPath" ), QFileInfo( filenames.at( 0 ) ).path() ); + + for ( const QString &file : filenames ) + { + addOption( file, file, true ); + } + + emit selectionChanged(); +} + +void QgsProcessingMultipleInputPanelWidget::addDirectory() +{ + QgsSettings settings; + QString path = settings.value( QStringLiteral( "/Processing/LastInputPath" ), QDir::homePath() ).toString(); + + const QString dir = QFileDialog::getExistingDirectory( this, tr( "Select Directory" ), path ); + if ( dir.isEmpty() ) + return; + + settings.setValue( QStringLiteral( "/Processing/LastInputPath" ), dir ); + + QStringList nameFilters; + if ( const QgsFileFilterGenerator *generator = dynamic_cast< const QgsFileFilterGenerator * >( mParameter ) ) + { + const QStringList extensions = QgsFileUtils::extensionsFromFilter( generator->createFileFilter() ); + for ( const QString &extension : extensions ) + { + nameFilters << QStringLiteral( "*.%1" ).arg( extension ); + nameFilters << QStringLiteral( "*.%1" ).arg( extension.toUpper() ); + nameFilters << QStringLiteral( "*.%1" ).arg( extension.toLower() ); + } + } + + QDirIterator it( path, nameFilters, QDir::Files | QDir::NoSymLinks | QDir::NoDotAndDotDot, QDirIterator::Subdirectories ); + QStringList files; + while ( it.hasNext() ) + { + const QString fullPath = it.next(); + addOption( fullPath, fullPath, true ); + } + emit selectionChanged(); +} + +void QgsProcessingMultipleInputPanelWidget::populateFromProject( QgsProject *project ) +{ + QgsSettings settings; + auto addLayer = [&]( const QgsMapLayer * layer ) + { + const QString authid = layer->crs().authid(); + QString title; + if ( settings.value( QStringLiteral( "Processing/Configuration/SHOW_CRS_DEF" ), true ).toBool() && !authid.isEmpty() ) + title = QStringLiteral( "%1 [%2]" ).arg( layer->name(), authid ); + else + title = layer->name(); + + + QString id = layer->id(); + for ( int i = 0; i < mModel->rowCount(); ++i ) + { + // try to match project layers to current layers + if ( mModel->item( i )->data( Qt::UserRole ) == layer->id() ) + { + id = layer->id(); + break; + } + else if ( mModel->item( i )->data( Qt::UserRole ) == layer->source() ) + { + id = layer->source(); + break; + } + } + + addOption( id, title, false, true ); + }; + + switch ( mParameter->layerType() ) + { + case QgsProcessing::TypeFile: + break; + + case QgsProcessing::TypeRaster: + { + const QList options = QgsProcessingUtils::compatibleRasterLayers( project, false ); + for ( const QgsRasterLayer *layer : options ) + { + addLayer( layer ); + } + break; + } + + case QgsProcessing::TypeMesh: + { + const QList options = QgsProcessingUtils::compatibleMeshLayers( project, false ); + for ( const QgsMeshLayer *layer : options ) + { + addLayer( layer ); + } + + break; + } + + case QgsProcessing::TypeVector: + case QgsProcessing::TypeVectorAnyGeometry: + { + const QList options = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() ); + for ( const QgsVectorLayer *layer : options ) + { + addLayer( layer ); + } + + break; + } + + case QgsProcessing::TypeMapLayer: + { + const QList vectors = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() ); + for ( const QgsVectorLayer *layer : vectors ) + { + addLayer( layer ); + } + const QList rasters = QgsProcessingUtils::compatibleRasterLayers( project ); + for ( const QgsRasterLayer *layer : rasters ) + { + addLayer( layer ); + } + const QList meshes = QgsProcessingUtils::compatibleMeshLayers( project ); + for ( const QgsMeshLayer *layer : meshes ) + { + addLayer( layer ); + } + + break; + } + + case QgsProcessing::TypeVectorPoint: + case QgsProcessing::TypeVectorLine: + case QgsProcessing::TypeVectorPolygon: + { + const QList vectors = QgsProcessingUtils::compatibleVectorLayers( project, QList< int >() << mParameter->layerType() ); + for ( const QgsVectorLayer *layer : vectors ) + { + addLayer( layer ); + } + break; + } + } +} + +// +// QgsProcessingMultipleInputDialog +// + +QgsProcessingMultipleInputDialog::QgsProcessingMultipleInputDialog( const QgsProcessingParameterMultipleLayers *parameter, const QVariantList &selectedOptions, + const QList< QgsProcessingModelChildParameterSource > &modelSources, QgsProcessingModelAlgorithm *model, QWidget *parent, Qt::WindowFlags flags ) + : QDialog( parent, flags ) +{ + setWindowTitle( tr( "Multiple Selection" ) ); + QVBoxLayout *vLayout = new QVBoxLayout(); + mWidget = new QgsProcessingMultipleInputPanelWidget( parameter, selectedOptions, modelSources, model ); + vLayout->addWidget( mWidget ); + mWidget->buttonBox()->addButton( QDialogButtonBox::Cancel ); + connect( mWidget->buttonBox(), &QDialogButtonBox::accepted, this, &QDialog::accept ); + connect( mWidget->buttonBox(), &QDialogButtonBox::rejected, this, &QDialog::reject ); + setLayout( vLayout ); +} + +QVariantList QgsProcessingMultipleInputDialog::selectedOptions() const +{ + return mWidget->selectedOptions(); +} + +void QgsProcessingMultipleInputDialog::setProject( QgsProject *project ) +{ + mWidget->setProject( project ); +} + + ///@endcond diff --git a/src/gui/processing/qgsprocessingmultipleselectiondialog.h b/src/gui/processing/qgsprocessingmultipleselectiondialog.h index 0ebd27cd50f8..640870337921 100644 --- a/src/gui/processing/qgsprocessingmultipleselectiondialog.h +++ b/src/gui/processing/qgsprocessingmultipleselectiondialog.h @@ -20,28 +20,30 @@ #include "qgis_gui.h" #include "ui_qgsprocessingmultipleselectiondialogbase.h" #include "qgsprocessingparameters.h" - +#include class QStandardItemModel; class QToolButton; class QStandardItem; +class QgsProcessingModelChildParameterSource; +class QgsProcessingModelAlgorithm; ///@cond NOT_STABLE /** * \ingroup gui - * \brief Dialog for configuration of a matrix (fixed table) parameter. + * \brief A panel widget for selection of multiple options from a fixed list of options. * \note Not stable API - * \since QGIS 3.6 + * \since QGIS 3.14 */ -class GUI_EXPORT QgsProcessingMultipleSelectionDialog : public QDialog, private Ui::QgsProcessingMultipleSelectionDialogBase +class GUI_EXPORT QgsProcessingMultipleSelectionPanelWidget : public QgsPanelWidget, private Ui::QgsProcessingMultipleSelectionDialogBase { Q_OBJECT public: /** - * Constructor for QgsProcessingMultipleSelectionDialog. + * Constructor for QgsProcessingMultipleSelectionPanelWidget. * * The \a availableOptions list specifies the list of standard known options for the parameter, * whilst the \a selectedOptions list specifies which options should be initially selected. @@ -49,10 +51,9 @@ class GUI_EXPORT QgsProcessingMultipleSelectionDialog : public QDialog, private * The \a selectedOptions list may contain extra options which are not present in \a availableOptions, * in which case they will be also added as existing options within the dialog. */ - QgsProcessingMultipleSelectionDialog( const QVariantList &availableOptions = QVariantList(), - const QVariantList &selectedOptions = QVariantList(), - QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags flags = nullptr ); - + QgsProcessingMultipleSelectionPanelWidget( const QVariantList &availableOptions = QVariantList(), + const QVariantList &selectedOptions = QVariantList(), + QWidget *parent SIP_TRANSFERTHIS = nullptr ); /** * Sets a callback function to use when encountering an invalid geometry and @@ -92,26 +93,205 @@ class GUI_EXPORT QgsProcessingMultipleSelectionDialog : public QDialog, private */ QVariantList selectedOptions() const; + /** + * Returns the widget's button box. + */ + QDialogButtonBox *buttonBox() { return mButtonBox; } + + signals: + + /** + * Emitted when the accept button is clicked. + */ + void acceptClicked(); + + /** + * Emitted when the selection changes in the widget. + */ + void selectionChanged(); + + protected: + + /** + * Adds a new option to the widget. + */ + void addOption( const QVariant &value, const QString &title, bool selected, bool updateExistingTitle = false ); + + //! Dialog list model + QStandardItemModel *mModel = nullptr; + //! Value formatter + std::function< QString( const QVariant & )> mValueFormatter; private slots: void selectAll( bool checked ); void toggleSelection(); private: - std::function< QString( const QVariant & )> mValueFormatter; QPushButton *mButtonSelectAll = nullptr; QPushButton *mButtonClearSelection = nullptr; QPushButton *mButtonToggleSelection = nullptr; - QStandardItemModel *mModel = nullptr; QList< QStandardItem * > currentItems(); + void populateList( const QVariantList &availableOptions, const QVariantList &selectedOptions ); friend class TestProcessingGui; }; + +/** + * \ingroup gui + * \brief A dialog for selection of multiple options from a fixed list of options. + * \note Not stable API + * \since QGIS 3.6 + */ +class GUI_EXPORT QgsProcessingMultipleSelectionDialog : public QDialog +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingMultipleSelectionPanelWidget. + * + * The \a availableOptions list specifies the list of standard known options for the parameter, + * whilst the \a selectedOptions list specifies which options should be initially selected. + * + * The \a selectedOptions list may contain extra options which are not present in \a availableOptions, + * in which case they will be also added as existing options within the dialog. + */ + QgsProcessingMultipleSelectionDialog( const QVariantList &availableOptions = QVariantList(), + const QVariantList &selectedOptions = QVariantList(), + QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags flags = nullptr ); + + + /** + * Sets a callback function to use when encountering an invalid geometry and + */ +#ifndef SIP_RUN + void setValueFormatter( const std::function< QString( const QVariant & )> &formatter ); +#else + void setValueFormatter( SIP_PYCALLABLE ); + % MethodCode + + Py_BEGIN_ALLOW_THREADS + + sipCpp->setValueFormatter( [a0]( const QVariant &v )->QString + { + QString res; + SIP_BLOCK_THREADS + PyObject *s = sipCallMethod( NULL, a0, "D", &v, sipType_QVariant, NULL ); + int state; + int sipIsError = 0; + QString *t1 = reinterpret_cast( sipConvertToType( s, sipType_QString, 0, SIP_NOT_NONE, &state, &sipIsError ) ); + if ( sipIsError == 0 ) + { + res = QString( *t1 ); + } + sipReleaseType( t1, sipType_QString, state ); + SIP_UNBLOCK_THREADS + return res; + } ); + + Py_END_ALLOW_THREADS + % End +#endif + + + /** + * Returns the ordered list of selected options. + */ + QVariantList selectedOptions() const; + + private: + + QgsProcessingMultipleSelectionPanelWidget *mWidget = nullptr; + +}; + + +/** + * \ingroup gui + * \brief A panel widget for selection of multiple inputs from a fixed list of options. + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingMultipleInputPanelWidget : public QgsProcessingMultipleSelectionPanelWidget +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingMultipleInputPanelWidget. + */ + QgsProcessingMultipleInputPanelWidget( const QgsProcessingParameterMultipleLayers *parameter, + const QVariantList &selectedOptions, + const QList< QgsProcessingModelChildParameterSource > &modelSources, + QgsProcessingModelAlgorithm *model = nullptr, + QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Sets the project associated with the widget. + */ + void setProject( QgsProject *project ); + + private slots: + + void addFiles(); + void addDirectory(); + + private: + + void populateFromProject( QgsProject *project ); + + const QgsProcessingParameterMultipleLayers *mParameter = nullptr; +}; + + +/** + * \ingroup gui + * \brief A dialog for selection of multiple layer inputs. + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingMultipleInputDialog : public QDialog +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingMultipleInputDialog. + * + * The \a selectedOptions list may contain extra options which are not present in \a availableOptions, + * in which case they will be also added as existing options within the dialog. + */ + QgsProcessingMultipleInputDialog( const QgsProcessingParameterMultipleLayers *parameter, + const QVariantList &selectedOptions, + const QList< QgsProcessingModelChildParameterSource > &modelSources, + QgsProcessingModelAlgorithm *model = nullptr, + QWidget *parent SIP_TRANSFERTHIS = nullptr, Qt::WindowFlags flags = nullptr ); + + /** + * Returns the ordered list of selected options. + */ + QVariantList selectedOptions() const; + + /** + * Sets the project associated with the dialog. + */ + void setProject( QgsProject *project ); + + private: + + QgsProcessingMultipleInputPanelWidget *mWidget = nullptr; + +}; + + ///@endcond #endif // QGSPROCESSINGMULTIPLESELECTIONDIALOG_H diff --git a/src/gui/processing/qgsprocessingoutputdestinationwidget.cpp b/src/gui/processing/qgsprocessingoutputdestinationwidget.cpp new file mode 100644 index 000000000000..3082cf763e7e --- /dev/null +++ b/src/gui/processing/qgsprocessingoutputdestinationwidget.cpp @@ -0,0 +1,716 @@ +/*************************************************************************** + qgsprocessingmatrixparameterdialog.cpp + ------------------------------------ + Date : February 2019 + Copyright : (C) 2019 Nyall Dawson + Email : nyall dot dawson 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 "qgsprocessingoutputdestinationwidget.h" +#include "qgsgui.h" +#include "qgsprocessingparameters.h" +#include "qgsproviderregistry.h" +#include "qgsprovidermetadata.h" +#include "qgsnewdatabasetablenamewidget.h" +#include "qgssettings.h" +#include "qgsfileutils.h" +#include "qgsdatasourceuri.h" +#include "qgsencodingfiledialog.h" +#include "qgsdatasourceselectdialog.h" +#include "qgsprocessingcontext.h" +#include "qgsprocessingalgorithm.h" +#include "qgsfieldmappingwidget.h" +#include +#include +#include +#include + +///@cond NOT_STABLE + +QgsProcessingLayerOutputDestinationWidget::QgsProcessingLayerOutputDestinationWidget( const QgsProcessingDestinationParameter *param, bool defaultSelection, QWidget *parent ) + : QWidget( parent ) + , mParameter( param ) + , mDefaultSelection( defaultSelection ) +{ + Q_ASSERT( mParameter ); + + setupUi( this ); + + leText->setClearButtonEnabled( false ); + + connect( leText, &QLineEdit::textEdited, this, &QgsProcessingLayerOutputDestinationWidget::textChanged ); + + mMenu = new QMenu( this ); + connect( mMenu, &QMenu::aboutToShow, this, &QgsProcessingLayerOutputDestinationWidget::menuAboutToShow ); + mSelectButton->setMenu( mMenu ); + mSelectButton->setPopupMode( QToolButton::InstantPopup ); + + QgsSettings settings; + mEncoding = settings.value( QStringLiteral( "/Processing/encoding" ), QStringLiteral( "System" ) ).toString(); + + if ( !mParameter->defaultValue().isValid() ) + { + // no default value -- we default to either skipping the output or a temporary output, depending on the createByDefault value + if ( mParameter->flags() & QgsProcessingParameterDefinition::FlagOptional && !mParameter->createByDefault() ) + setValue( QVariant() ); + else + setValue( QgsProcessing::TEMPORARY_OUTPUT ); + } + else + { + setValue( mParameter->defaultValue() ); + } + + setToolTip( mParameter->toolTip() ); + + setAcceptDrops( true ); + leText->setAcceptDrops( false ); +} + +bool QgsProcessingLayerOutputDestinationWidget::outputIsSkipped() const +{ + return leText->text().isEmpty() && !mUseTemporary; +} + +void QgsProcessingLayerOutputDestinationWidget::setValue( const QVariant &value ) +{ + const bool prevSkip = outputIsSkipped(); + mUseRemapping = false; + if ( !value.isValid() || ( value.type() == QVariant::String && value.toString().isEmpty() ) ) + { + if ( mParameter->flags() & QgsProcessingParameterDefinition::FlagOptional ) + skipOutput(); + else + saveToTemporary(); + } + else + { + if ( value.toString() == QStringLiteral( "memory:" ) || value.toString() == QgsProcessing::TEMPORARY_OUTPUT ) + { + saveToTemporary(); + } + else if ( value.canConvert< QgsProcessingOutputLayerDefinition >() ) + { + const QgsProcessingOutputLayerDefinition def = value.value< QgsProcessingOutputLayerDefinition >(); + if ( def.sink.staticValue().toString() == QStringLiteral( "memory:" ) || def.sink.staticValue().toString() == QgsProcessing::TEMPORARY_OUTPUT || def.sink.staticValue().toString().isEmpty() ) + { + saveToTemporary(); + } + else + { + const QVariant prev = QgsProcessingLayerOutputDestinationWidget::value(); + leText->setText( def.sink.staticValue().toString() ); + mUseTemporary = false; + if ( prevSkip ) + emit skipOutputChanged( false ); + if ( prev != QgsProcessingLayerOutputDestinationWidget::value() ) + emit destinationChanged(); + } + mUseRemapping = def.useRemapping(); + mRemapDefinition = def.remappingDefinition(); + mEncoding = def.createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(); + } + else + { + const QVariant prev = QgsProcessingLayerOutputDestinationWidget::value(); + leText->setText( value.toString() ); + mUseTemporary = false; + if ( prevSkip ) + emit skipOutputChanged( false ); + + if ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() || mParameter->type() == QgsProcessingParameterFileDestination::typeName() ) + { + if ( prev.toString() != QgsProcessingLayerOutputDestinationWidget::value().toString() ) + emit destinationChanged(); + } + else + { + if ( !prev.canConvert() || + !( prev.value< QgsProcessingOutputLayerDefinition >() == QgsProcessingLayerOutputDestinationWidget::value().value< QgsProcessingOutputLayerDefinition >() ) ) + emit destinationChanged(); + } + } + } +} + +QVariant QgsProcessingLayerOutputDestinationWidget::value() const +{ + QgsSettings settings; + QString key; + if ( mUseTemporary && mParameter->type() == QgsProcessingParameterFeatureSink::typeName() ) + { + key = QgsProcessing::TEMPORARY_OUTPUT; + } + else if ( mUseTemporary && !mDefaultSelection ) + { + key = QgsProcessing::TEMPORARY_OUTPUT; + } + else + { + key = leText->text(); + } + + if ( key.isEmpty() && mParameter->flags() & QgsProcessingParameterDefinition::FlagOptional ) + return QVariant(); + + QString provider; + QString uri; + if ( !key.isEmpty() && key != QgsProcessing::TEMPORARY_OUTPUT + && !key.startsWith( QLatin1String( "memory:" ) ) + && !key.startsWith( QLatin1String( "ogr:" ) ) + && !key.startsWith( QLatin1String( "postgres:" ) ) + && !key.startsWith( QLatin1String( "postgis:" ) ) + && !QgsProcessingUtils::decodeProviderKeyAndUri( key, provider, uri ) ) + { + // output should be a file path + QString folder = QFileInfo( key ).path(); + if ( folder == '.' ) + { + // output name does not include a folder - use default + QString defaultFolder = settings.value( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ) ).toString(); + key = QDir( defaultFolder ).filePath( key ); + } + } + + if ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() ) + return key; + else if ( mParameter->type() == QgsProcessingParameterFileDestination::typeName() ) + return key; + + QgsProcessingOutputLayerDefinition value( key ); + value.createOptions.insert( QStringLiteral( "fileEncoding" ), mEncoding ); + if ( mUseRemapping ) + value.setRemappingDefinition( mRemapDefinition ); + return value; +} + +void QgsProcessingLayerOutputDestinationWidget::setWidgetContext( const QgsProcessingParameterWidgetContext &context ) +{ + mBrowserModel = context.browserModel(); +} + +void QgsProcessingLayerOutputDestinationWidget::setContext( QgsProcessingContext *context ) +{ + mContext = context; +} + +void QgsProcessingLayerOutputDestinationWidget::registerProcessingParametersGenerator( QgsProcessingParametersGenerator *generator ) +{ + mParametersGenerator = generator; +} + +void QgsProcessingLayerOutputDestinationWidget::addOpenAfterRunningOption() +{ + mOpenAfterRunningCheck = new QCheckBox( tr( "Open output file after running algorithm" ) ); + mOpenAfterRunningCheck->setChecked( !outputIsSkipped() ); + mOpenAfterRunningCheck->setEnabled( !outputIsSkipped() ); + gridLayout->addWidget( mOpenAfterRunningCheck, 1, 0, 1, 2 ); + + connect( this, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged, this, [ = ]( bool skipped ) + { + bool enabled = !skipped; + mOpenAfterRunningCheck->setEnabled( enabled ); + mOpenAfterRunningCheck->setChecked( enabled ); + } ); +} + +bool QgsProcessingLayerOutputDestinationWidget::openAfterRunning() const +{ + return mOpenAfterRunningCheck && mOpenAfterRunningCheck->isChecked(); +} + +void QgsProcessingLayerOutputDestinationWidget::menuAboutToShow() +{ + mMenu->clear(); + + if ( !mDefaultSelection ) + { + if ( mParameter->flags() & QgsProcessingParameterDefinition::FlagOptional ) + { + QAction *actionSkipOutput = new QAction( tr( "Skip Output" ), this ); + connect( actionSkipOutput, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::skipOutput ); + mMenu->addAction( actionSkipOutput ); + } + + QAction *actionSaveToTemp = nullptr; + if ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() && mParameter->supportsNonFileBasedOutput() ) + { + // use memory layers for temporary layers if supported + actionSaveToTemp = new QAction( tr( "Create Temporary Layer" ), this ); + } + else if ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() ) + { + actionSaveToTemp = new QAction( tr( "Save to a Temporary Directory" ), this ); + } + else + { + actionSaveToTemp = new QAction( tr( "Save to a Temporary File" ), this ); + } + + connect( actionSaveToTemp, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::saveToTemporary ); + mMenu->addAction( actionSaveToTemp ); + } + + QAction *actionSaveToFile = nullptr; + if ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() ) + { + actionSaveToFile = new QAction( tr( "Save to Directory…" ), this ); + connect( actionSaveToFile, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::selectDirectory ); + } + else + { + actionSaveToFile = new QAction( tr( "Save to File…" ), this ); + connect( actionSaveToFile, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::selectFile ); + } + mMenu->addAction( actionSaveToFile ); + + if ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() && mParameter->supportsNonFileBasedOutput() ) + { + QAction *actionSaveToGpkg = new QAction( tr( "Save to GeoPackage…" ), this ); + connect( actionSaveToGpkg, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::saveToGeopackage ); + mMenu->addAction( actionSaveToGpkg ); + + QAction *actionSaveToDatabase = new QAction( tr( "Save to Database Table…" ), this ); + connect( actionSaveToDatabase, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::saveToDatabase ); + mMenu->addAction( actionSaveToDatabase ); + + if ( mParameter->algorithm() && dynamic_cast< const QgsProcessingParameterFeatureSink * >( mParameter )->supportsAppend() ) + { + mMenu->addSeparator(); + QAction *actionAppendToLayer = new QAction( tr( "Append to Layer…" ), this ); + connect( actionAppendToLayer, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::appendToLayer ); + mMenu->addAction( actionAppendToLayer ); + if ( mUseRemapping ) + { + QAction *editMappingAction = new QAction( tr( "Edit Field Mapping…" ), this ); + connect( editMappingAction, &QAction::triggered, this, [ = ] + { + setAppendDestination( value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), mRemapDefinition.destinationFields() ); + } ); + mMenu->addAction( editMappingAction ); + } + } + } + + if ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() ) + { + mMenu->addSeparator(); + QAction *actionSetEncoding = new QAction( tr( "Change File Encoding (%1)…" ).arg( mEncoding ), this ); + connect( actionSetEncoding, &QAction::triggered, this, &QgsProcessingLayerOutputDestinationWidget::selectEncoding ); + mMenu->addAction( actionSetEncoding ); + } +} + +void QgsProcessingLayerOutputDestinationWidget::skipOutput() +{ + leText->setPlaceholderText( tr( "[Skip output]" ) ); + leText->clear(); + mUseTemporary = false; + mUseRemapping = false; + + emit skipOutputChanged( true ); + emit destinationChanged(); +} + +void QgsProcessingLayerOutputDestinationWidget::saveToTemporary() +{ + const bool prevSkip = outputIsSkipped(); + + if ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() && mParameter->supportsNonFileBasedOutput() ) + { + leText->setPlaceholderText( tr( "[Create temporary layer]" ) ); + } + else if ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() ) + { + leText->setPlaceholderText( tr( "[Save to temporary folder]" ) ); + } + else + { + leText->setPlaceholderText( tr( "[Save to temporary file]" ) ); + } + leText->clear(); + + if ( mUseTemporary ) + return; + + mUseTemporary = true; + mUseRemapping = false; + if ( prevSkip ) + emit skipOutputChanged( false ); + emit destinationChanged(); +} + +void QgsProcessingLayerOutputDestinationWidget::selectDirectory() +{ + QString lastDir = leText->text(); + QgsSettings settings; + if ( lastDir.isEmpty() ) + lastDir = settings.value( QStringLiteral( "/Processing/LastOutputPath" ), QDir::homePath() ).toString(); + + const QString dirName = QFileDialog::getExistingDirectory( this, tr( "Select Directory" ), lastDir, QFileDialog::ShowDirsOnly ); + if ( !dirName.isEmpty() ) + { + leText->setText( QDir::toNativeSeparators( dirName ) ); + settings.setValue( QStringLiteral( "/Processing/LastOutputPath" ), dirName ); + mUseTemporary = false; + mUseRemapping = false; + emit skipOutputChanged( false ); + emit destinationChanged(); + } +} + +void QgsProcessingLayerOutputDestinationWidget::selectFile() +{ + const QString fileFilter = mParameter->createFileFilter(); + + QgsSettings settings; + + QString lastExtPath; + QString lastExt; + if ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() || mParameter->type() == QgsProcessingParameterVectorDestination::typeName() ) + { + lastExtPath = QStringLiteral( "/Processing/LastVectorOutputExt" ); + lastExt = settings.value( lastExtPath, QStringLiteral( ".%1" ).arg( mParameter->defaultFileExtension() ) ).toString() ; + } + else if ( mParameter->type() == QgsProcessingParameterRasterDestination::typeName() ) + { + lastExtPath = QStringLiteral( "/Processing/LastRasterOutputExt" ); + lastExt = settings.value( lastExtPath, QStringLiteral( ".%1" ).arg( mParameter->defaultFileExtension() ) ).toString(); + } + + // get default filter + const QStringList filters = fileFilter.split( QStringLiteral( ";;" ) ); + QString lastFilter; + for ( const QString &f : filters ) + { + if ( f.contains( QStringLiteral( "*.%1" ).arg( lastExt ), Qt::CaseInsensitive ) ) + { + lastFilter = f; + break; + } + } + + QString path; + if ( settings.contains( QStringLiteral( "/Processing/LastOutputPath" ) ) ) + path = settings.value( QStringLiteral( "/Processing/LastOutputPath" ) ).toString(); + else + path = settings.value( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ) ).toString(); + + QString filename = QFileDialog::getSaveFileName( this, tr( "Save file" ), path, fileFilter, &lastFilter ); + if ( !filename.isEmpty() ) + { + mUseTemporary = false; + mUseRemapping = false; + filename = QgsFileUtils::addExtensionFromFilter( filename, lastFilter ); + + leText->setText( filename ); + settings.setValue( QStringLiteral( "/Processing/LastOutputPath" ), QFileInfo( filename ).path() ); + if ( !lastExtPath.isEmpty() ) + settings.setValue( lastExtPath, QFileInfo( filename ).suffix().toLower() ); + + emit skipOutputChanged( false ); + emit destinationChanged(); + } +} + +void QgsProcessingLayerOutputDestinationWidget::saveToGeopackage() +{ + QgsSettings settings; + QString lastPath = settings.value( QStringLiteral( "/Processing/LastOutputPath" ), QString() ).toString(); + if ( lastPath.isEmpty() ) + lastPath = settings.value( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), QString() ).toString(); + + QString filename = QFileDialog::getSaveFileName( this, tr( "Save to GeoPackage" ), lastPath, tr( "GeoPackage files (*.gpkg);;All files (*.*)" ), nullptr, QFileDialog::DontConfirmOverwrite ); + + if ( filename.isEmpty() ) + return; + + const QString layerName = QInputDialog::getText( this, tr( "Save to GeoPackage" ), tr( "Layer name" ), QLineEdit::Normal, mParameter->name().toLower() ); + if ( layerName.isEmpty() ) + return; + + mUseTemporary = false; + mUseRemapping = false; + + filename = QgsFileUtils::ensureFileNameHasExtension( filename, QStringList() << QStringLiteral( "gpkg" ) ); + + settings.setValue( QStringLiteral( "/Processing/LastOutputPath" ), QFileInfo( filename ).path() ); + + QgsDataSourceUri uri; + uri.setTable( layerName ); + uri.setDatabase( filename ); + + QString geomColumn; + if ( const QgsProcessingParameterFeatureSink *sink = dynamic_cast< const QgsProcessingParameterFeatureSink * >( mParameter ) ) + { + if ( sink->hasGeometry() ) + geomColumn = QStringLiteral( "geom" ); + } + uri.setGeometryColumn( geomColumn ); + + leText->setText( QStringLiteral( "ogr:%1" ).arg( uri.uri() ) ); + + emit skipOutputChanged( false ); + emit destinationChanged(); +} + +void QgsProcessingLayerOutputDestinationWidget::saveToDatabase() +{ + if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) ) + { + + QgsNewDatabaseTableNameWidget *widget = new QgsNewDatabaseTableNameWidget( mBrowserModel, QStringList() << QStringLiteral( "postgres" ) + << QStringLiteral( "mssql" ) + << QStringLiteral( "ogr" ) + << QStringLiteral( "spatialite" ), this ); + widget->setPanelTitle( tr( "Save “%1” to Database Table" ).arg( mParameter->description() ) ); + widget->setAcceptButtonVisible( true ); + + panel->openPanel( widget ); + + auto changed = [ = ] + { + mUseTemporary = false; + mUseRemapping = false; + + QString geomColumn; + if ( const QgsProcessingParameterFeatureSink *sink = dynamic_cast< const QgsProcessingParameterFeatureSink * >( mParameter ) ) + { + if ( sink->hasGeometry() ) + geomColumn = QStringLiteral( "geom" ); + } + + if ( widget->dataProviderKey() == QLatin1String( "ogr" ) ) + { + QgsDataSourceUri uri; + uri.setTable( widget->table() ); + uri.setDatabase( widget->schema() ); + uri.setGeometryColumn( geomColumn ); + leText->setText( QStringLiteral( "ogr:%1" ).arg( uri.uri() ) ); + } + else + { + QgsDataSourceUri uri( widget->uri() ); + uri.setGeometryColumn( geomColumn ); + leText->setText( QgsProcessingUtils::encodeProviderKeyAndUri( widget->dataProviderKey(), uri.uri() ) ); + } + + emit skipOutputChanged( false ); + emit destinationChanged(); + }; + + connect( widget, &QgsNewDatabaseTableNameWidget::tableNameChanged, this, [ = ] { changed(); } ); + connect( widget, &QgsNewDatabaseTableNameWidget::schemaNameChanged, this, [ = ] { changed(); } ); + connect( widget, &QgsNewDatabaseTableNameWidget::validationChanged, this, [ = ] { changed(); } ); + connect( widget, &QgsNewDatabaseTableNameWidget::providerKeyChanged, this, [ = ] { changed(); } ); + connect( widget, &QgsNewDatabaseTableNameWidget::accepted, this, [ = ] + { + changed(); + widget->acceptPanel(); + } ); + } +} + +void QgsProcessingLayerOutputDestinationWidget::appendToLayer() +{ + if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) ) + { + QgsDataSourceSelectWidget *widget = new QgsDataSourceSelectWidget( mBrowserModel, true, QgsMapLayerType::VectorLayer ); + widget->setPanelTitle( tr( "Append \"%1\" to Layer" ).arg( mParameter->description() ) ); + + panel->openPanel( widget ); + + connect( widget, &QgsDataSourceSelectWidget::itemTriggered, this, [ = ]( const QgsMimeDataUtils::Uri & ) + { + widget->acceptPanel(); + } ); + connect( widget, &QgsPanelWidget::panelAccepted, this, [ = ]() + { + if ( widget->uri().uri.isEmpty() ) + setValue( QVariant() ); + else + { + // get fields for destination + std::unique_ptr< QgsVectorLayer > dest = qgis::make_unique< QgsVectorLayer >( widget->uri().uri, QString(), widget->uri().providerKey ); + if ( widget->uri().providerKey == QLatin1String( "ogr" ) ) + setAppendDestination( widget->uri().uri, dest->fields() ); + else + setAppendDestination( QgsProcessingUtils::encodeProviderKeyAndUri( widget->uri().providerKey, widget->uri().uri ), dest->fields() ); + } + } ); + } +} + + +void QgsProcessingLayerOutputDestinationWidget::setAppendDestination( const QString &uri, const QgsFields &destFields ) +{ + const QgsProcessingAlgorithm *alg = mParameter->algorithm(); + QVariantMap props; + if ( mParametersGenerator ) + props = mParametersGenerator->createProcessingParameters(); + props.insert( mParameter->name(), uri ); + + const QgsProcessingAlgorithm::VectorProperties outputProps = alg->sinkProperties( mParameter->name(), props, *mContext, QMap() ); + if ( outputProps.availability == QgsProcessingAlgorithm::Available ) + { + if ( QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ) ) + { + // get mapping from fields output by algorithm to destination fields + QgsFieldMappingWidget *widget = new QgsFieldMappingWidget( nullptr, outputProps.fields, destFields ); + widget->setPanelTitle( tr( "Append \"%1\" to Layer" ).arg( mParameter->description() ) ); + if ( !mRemapDefinition.fieldMap().isEmpty() ) + widget->setFieldPropertyMap( mRemapDefinition.fieldMap() ); + + panel->openPanel( widget ); + + connect( widget, &QgsPanelWidget::panelAccepted, this, [ = ]() + { + QgsProcessingOutputLayerDefinition def( uri ); + QgsRemappingSinkDefinition remap; + remap.setSourceCrs( outputProps.crs ); + remap.setFieldMap( widget->fieldPropertyMap() ); + remap.setDestinationFields( destFields ); + def.setRemappingDefinition( remap ); + setValue( def ); + } ); + } + } +} + +void QgsProcessingLayerOutputDestinationWidget::selectEncoding() +{ + QgsEncodingSelectionDialog dialog( this, tr( "File encoding" ), mEncoding ); + if ( dialog.exec() ) + { + mEncoding = dialog.encoding(); + QgsSettings settings; + settings.setValue( QStringLiteral( "/Processing/encoding" ), mEncoding ); + emit destinationChanged(); + } +} + +void QgsProcessingLayerOutputDestinationWidget::textChanged( const QString &text ) +{ + mUseTemporary = text.isEmpty(); + mUseRemapping = false; + emit destinationChanged(); +} + + +QString QgsProcessingLayerOutputDestinationWidget::mimeDataToPath( const QMimeData *data ) +{ + const QgsMimeDataUtils::UriList uriList = QgsMimeDataUtils::decodeUriList( data ); + for ( const QgsMimeDataUtils::Uri &u : uriList ) + { + if ( ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() + || mParameter->type() == QgsProcessingParameterVectorDestination::typeName() + || mParameter->type() == QgsProcessingParameterFileDestination::typeName() ) + && u.layerType == QLatin1String( "vector" ) && u.providerKey == QLatin1String( "ogr" ) ) + { + return u.uri; + } + else if ( ( mParameter->type() == QgsProcessingParameterRasterDestination::typeName() + || mParameter->type() == QgsProcessingParameterFileDestination::typeName() ) + && u.layerType == QLatin1String( "raster" ) && u.providerKey == QLatin1String( "gdal" ) ) + return u.uri; +#if 0 + else if ( ( mParameter->type() == QgsProcessingParameterMeshDestination::typeName() + || mParameter->type() == QgsProcessingParameterFileDestination::typeName() ) + && u.layerType == QLatin1String( "mesh" ) && u.providerKey == QLatin1String( "mdal" ) ) + return u.uri; + +#endif + else if ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() + && u.layerType == QLatin1String( "directory" ) ) + { + return u.uri; + } + } + if ( !uriList.isEmpty() ) + return QString(); + + // files dragged from file explorer, outside of QGIS + QStringList rawPaths; + if ( data->hasUrls() ) + { + const QList< QUrl > urls = data->urls(); + rawPaths.reserve( urls.count() ); + for ( const QUrl &url : urls ) + { + const QString local = url.toLocalFile(); + if ( !rawPaths.contains( local ) ) + rawPaths.append( local ); + } + } + if ( !data->text().isEmpty() && !rawPaths.contains( data->text() ) ) + rawPaths.append( data->text() ); + + for ( const QString &path : qgis::as_const( rawPaths ) ) + { + QFileInfo file( path ); + if ( file.isFile() && ( mParameter->type() == QgsProcessingParameterFeatureSink::typeName() + || mParameter->type() == QgsProcessingParameterVectorDestination::typeName() + || mParameter->type() == QgsProcessingParameterRasterDestination::typeName() + || mParameter->type() == QgsProcessingParameterVectorDestination::typeName() + || mParameter->type() == QgsProcessingParameterFileDestination::typeName() ) ) + { + // TODO - we should check to see if it's a valid extension for the parameter, but that's non-trivial + return path; + } + else if ( file.isDir() && ( mParameter->type() == QgsProcessingParameterFolderDestination::typeName() ) ) + return path; + } + + return QString(); +} + +void QgsProcessingLayerOutputDestinationWidget::dragEnterEvent( QDragEnterEvent *event ) +{ + if ( !( event->possibleActions() & Qt::CopyAction ) ) + return; + + const QString path = mimeDataToPath( event->mimeData() ); + if ( !path.isEmpty() ) + { + // dragged an acceptable path, phew + event->setDropAction( Qt::CopyAction ); + event->accept(); + leText->setHighlighted( true ); + } +} + +void QgsProcessingLayerOutputDestinationWidget::dragLeaveEvent( QDragLeaveEvent *event ) +{ + QWidget::dragLeaveEvent( event ); + if ( leText->isHighlighted() ) + { + event->accept(); + leText->setHighlighted( false ); + } +} + +void QgsProcessingLayerOutputDestinationWidget::dropEvent( QDropEvent *event ) +{ + if ( !( event->possibleActions() & Qt::CopyAction ) ) + return; + + const QString path = mimeDataToPath( event->mimeData() ); + if ( !path.isEmpty() ) + { + // dropped an acceptable path, phew + setFocus( Qt::MouseFocusReason ); + event->setDropAction( Qt::CopyAction ); + event->accept(); + setValue( path ); + } + leText->setHighlighted( false ); +} + +///@endcond diff --git a/src/gui/processing/qgsprocessingoutputdestinationwidget.h b/src/gui/processing/qgsprocessingoutputdestinationwidget.h new file mode 100644 index 000000000000..815ac6201385 --- /dev/null +++ b/src/gui/processing/qgsprocessingoutputdestinationwidget.h @@ -0,0 +1,149 @@ +/*************************************************************************** + qgsprocessingoutputdestinationwidget.h + ---------------------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 QGSPROCESSINGOUTPUTDESTINATIONWIDGET_H +#define QGSPROCESSINGOUTPUTDESTINATIONWIDGET_H + +#include "qgis.h" +#include "qgis_gui.h" +#include "ui_qgsprocessingdestinationwidgetbase.h" +#include "qgsprocessingwidgetwrapper.h" +#include "qgsprocessingcontext.h" +#include + +class QgsProcessingDestinationParameter; +class QgsBrowserGuiModel; +class QCheckBox; +///@cond NOT_STABLE + +/** + * \ingroup gui + * \brief A widget which allows users to select the destination path for an output style Processing parameter. + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingLayerOutputDestinationWidget : public QWidget, private Ui::QgsProcessingDestinationWidgetBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingLayerOutputDestinationWidget, associated with the specified \a parameter. + */ + QgsProcessingLayerOutputDestinationWidget( const QgsProcessingDestinationParameter *parameter, bool defaultSelection, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Returns TRUE if the output is set to be skipped. + */ + bool outputIsSkipped() const; + + /** + * Sets the \a value to show in the widget. + */ + void setValue( const QVariant &value ); + + /** + * Returns the widgets current value. + */ + QVariant value() const; + + /** + * Sets the \a context in which the widget is shown, e.g., the + * parent model algorithm, a linked map canvas, and other relevant information which allows the widget + * to fine-tune its behavior. + */ + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ); + + /** + * Sets the processing \a context in which this widget is being shown. + */ + void setContext( QgsProcessingContext *context ); + + /** + * Registers a Processing parameters \a generator class that will be used to retrieve + * algorithm parameters for the widget when required. + * + * \since QGIS 3.14 + */ + void registerProcessingParametersGenerator( QgsProcessingParametersGenerator *generator ); + + /** + * Adds the "Open output file after running" option to the widget. + */ + void addOpenAfterRunningOption(); + + /** + * Returns TRUE if the widget has the "Open output file after running" option checked. + */ + bool openAfterRunning() const; + + signals: + + /** + * Emitted whenever the "skip output" option is toggled in the widget. + */ + void skipOutputChanged( bool skipped ); + + /** + * Emitted whenever the destination value is changed in the widget. + */ + void destinationChanged(); + protected: + + void dragEnterEvent( QDragEnterEvent *event ) override; + void dragLeaveEvent( QDragLeaveEvent *event ) override; + void dropEvent( QDropEvent *event ) override; + + private slots: + + void menuAboutToShow(); + void skipOutput(); + void saveToTemporary(); + void selectDirectory(); + void selectFile(); + void saveToGeopackage(); + void saveToDatabase(); + void appendToLayer(); + void selectEncoding(); + void textChanged( const QString &text ); + + private: + + void setAppendDestination( const QString &uri, const QgsFields &destFields ); + + QString mimeDataToPath( const QMimeData *data ); + + const QgsProcessingDestinationParameter *mParameter = nullptr; + QgsProcessingParametersGenerator *mParametersGenerator = nullptr; + QMenu *mMenu = nullptr; + + bool mUseTemporary = true; + bool mDefaultSelection = false; + QString mEncoding; + QgsBrowserGuiModel *mBrowserModel = nullptr; + QCheckBox *mOpenAfterRunningCheck = nullptr; + + QgsRemappingSinkDefinition mRemapDefinition; + bool mUseRemapping = false; + + QgsProcessingContext *mContext = nullptr; + + friend class TestProcessingGui; +}; + +///@endcond + +#endif // QGSPROCESSINGOUTPUTDESTINATIONWIDGET_H diff --git a/src/gui/processing/qgsprocessingparameterdefinitionwidget.cpp b/src/gui/processing/qgsprocessingparameterdefinitionwidget.cpp index 3690fb1ea3b7..6ba8c7b37a68 100644 --- a/src/gui/processing/qgsprocessingparameterdefinitionwidget.cpp +++ b/src/gui/processing/qgsprocessingparameterdefinitionwidget.cpp @@ -22,6 +22,7 @@ #include "qgsapplication.h" #include "qgsprocessingregistry.h" #include "qgsprocessingparametertype.h" +#include "qgscolorbutton.h" #include #include #include @@ -145,7 +146,18 @@ QgsProcessingParameterDefinitionDialog::QgsProcessingParameterDefinitionDialog( QVBoxLayout *commentLayout = new QVBoxLayout(); mCommentEdit = new QTextEdit(); mCommentEdit->setAcceptRichText( false ); - commentLayout->addWidget( mCommentEdit ); + commentLayout->addWidget( mCommentEdit, 1 ); + + QHBoxLayout *hl = new QHBoxLayout(); + hl->setContentsMargins( 0, 0, 0, 0 ); + hl->addWidget( new QLabel( tr( "Color" ) ) ); + mCommentColorButton = new QgsColorButton(); + mCommentColorButton->setAllowOpacity( true ); + mCommentColorButton->setWindowTitle( tr( "Comment Color" ) ); + mCommentColorButton->setShowNull( true, tr( "Default" ) ); + hl->addWidget( mCommentColorButton ); + commentLayout->addLayout( hl ); + QWidget *w2 = new QWidget(); w2->setLayout( commentLayout ); mTabWidget->addTab( w2, tr( "Comments" ) ); @@ -178,6 +190,19 @@ QString QgsProcessingParameterDefinitionDialog::comments() const return mCommentEdit->toPlainText(); } +void QgsProcessingParameterDefinitionDialog::setCommentColor( const QColor &color ) +{ + if ( color.isValid() ) + mCommentColorButton->setColor( color ); + else + mCommentColorButton->setToNull(); +} + +QColor QgsProcessingParameterDefinitionDialog::commentColor() const +{ + return !mCommentColorButton->isNull() ? mCommentColorButton->color() : QColor(); +} + void QgsProcessingParameterDefinitionDialog::switchToCommentTab() { mTabWidget->setCurrentIndex( 1 ); diff --git a/src/gui/processing/qgsprocessingparameterdefinitionwidget.h b/src/gui/processing/qgsprocessingparameterdefinitionwidget.h index 7f964aa97ebd..c47cef60cc81 100644 --- a/src/gui/processing/qgsprocessingparameterdefinitionwidget.h +++ b/src/gui/processing/qgsprocessingparameterdefinitionwidget.h @@ -31,6 +31,7 @@ class QLineEdit; class QCheckBox; class QTabWidget; class QTextEdit; +class QgsColorButton; /** * Abstract base class for widgets which allow users to specify the properties of a @@ -183,6 +184,20 @@ class GUI_EXPORT QgsProcessingParameterDefinitionDialog: public QDialog */ QString comments() const; + /** + * Sets the color for the comments for the parameter. + * \see commentColor() + * \since QGIS 3.14 + */ + void setCommentColor( const QColor &color ); + + /** + * Returns the color for the comments for the parameter. + * \see setCommentColor() + * \since QGIS 3.14 + */ + QColor commentColor() const; + /** * Switches the dialog to the comments tab. */ @@ -195,6 +210,7 @@ class GUI_EXPORT QgsProcessingParameterDefinitionDialog: public QDialog QTabWidget *mTabWidget = nullptr; QTextEdit *mCommentEdit = nullptr; + QgsColorButton *mCommentColorButton = nullptr; QgsProcessingParameterDefinitionWidget *mWidget = nullptr; }; diff --git a/src/gui/processing/qgsprocessingparameterswidget.cpp b/src/gui/processing/qgsprocessingparameterswidget.cpp new file mode 100644 index 000000000000..7f785fcd9e53 --- /dev/null +++ b/src/gui/processing/qgsprocessingparameterswidget.cpp @@ -0,0 +1,84 @@ +/*************************************************************************** + qgsprocessingparameterswidget.cpp + ------------------------------------ + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 "qgsprocessingparameterswidget.h" +#include "qgsprocessingalgorithm.h" +#include "qgsprocessingparameters.h" + +///@cond NOT_STABLE + +QgsProcessingParametersWidget::QgsProcessingParametersWidget( const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsPanelWidget( parent ) + , mAlgorithm( algorithm ) +{ + Q_ASSERT( mAlgorithm ); + + setupUi( this ); + + grpAdvanced->hide(); + scrollAreaWidgetContents->setContentsMargins( 4, 4, 4, 4 ); +} + +const QgsProcessingAlgorithm *QgsProcessingParametersWidget::algorithm() const +{ + return mAlgorithm; +} + +void QgsProcessingParametersWidget::initWidgets() +{ + // if there are advanced parameters — show corresponding groupbox + const QgsProcessingParameterDefinitions defs = mAlgorithm->parameterDefinitions(); + for ( const QgsProcessingParameterDefinition *param : defs ) + { + if ( param->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) + { + grpAdvanced->show(); + break; + } + } +} + +void QgsProcessingParametersWidget::addParameterWidget( const QgsProcessingParameterDefinition *parameter, QWidget *widget ) +{ + if ( parameter->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) + mAdvancedGroupLayout->addWidget( widget ); + else + mScrollAreaLayout->insertWidget( mScrollAreaLayout->count() - 2, widget ); +} + +void QgsProcessingParametersWidget::addParameterLabel( const QgsProcessingParameterDefinition *parameter, QWidget *label ) +{ + if ( parameter->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) + mAdvancedGroupLayout->addWidget( label ); + else + mScrollAreaLayout->insertWidget( mScrollAreaLayout->count() - 2, label ); +} + +void QgsProcessingParametersWidget::addOutputLabel( QWidget *label ) +{ + mScrollAreaLayout->insertWidget( mScrollAreaLayout->count() - 1, label ); +} + +void QgsProcessingParametersWidget::addOutputWidget( QWidget *widget ) +{ + mScrollAreaLayout->insertWidget( mScrollAreaLayout->count() - 1, widget ); +} + +void QgsProcessingParametersWidget::addExtraWidget( QWidget *widget ) +{ + mScrollAreaLayout->addWidget( widget ); +} + +///@endcond diff --git a/src/gui/processing/qgsprocessingparameterswidget.h b/src/gui/processing/qgsprocessingparameterswidget.h new file mode 100644 index 000000000000..43bef826f17d --- /dev/null +++ b/src/gui/processing/qgsprocessingparameterswidget.h @@ -0,0 +1,70 @@ +/*************************************************************************** + qgsprocessingparameterswidget.h + ---------------------------------- + Date : March 2020 + Copyright : (C) 2020 Nyall Dawson + Email : nyall dot dawson 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 QGSPROCESSINGPARAMETERSWIDGET_H +#define QGSPROCESSINGPARAMETERSWIDGET_H + +#include "qgis.h" +#include "qgis_gui.h" +#include "ui_qgsprocessingparameterswidgetbase.h" +#include +#include "qgsprocessingwidgetwrapper.h" + +class QgsProcessingAlgorithm; +class QgsProcessingParameterDefinition; + +///@cond NOT_STABLE + +/** + * \ingroup gui + * \brief A widget which allows users to select the value for the parameters for an algorithm. + * \note Not stable API + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingParametersWidget : public QgsPanelWidget, public QgsProcessingParametersGenerator, private Ui::QgsProcessingParametersWidgetBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsProcessingParametersWidget, for the specified \a algorithm. + */ + QgsProcessingParametersWidget( const QgsProcessingAlgorithm *algorithm, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + const QgsProcessingAlgorithm *algorithm() const; + + protected: + + virtual void initWidgets(); + + void addParameterWidget( const QgsProcessingParameterDefinition *parameter, QWidget *widget SIP_TRANSFER ); + void addParameterLabel( const QgsProcessingParameterDefinition *parameter, QWidget *label SIP_TRANSFER ); + + void addOutputLabel( QWidget *label SIP_TRANSFER ); + void addOutputWidget( QWidget *widget SIP_TRANSFER ); + + void addExtraWidget( QWidget *widget SIP_TRANSFER ); + + private: + + const QgsProcessingAlgorithm *mAlgorithm = nullptr; + + friend class TestProcessingGui; +}; + +///@endcond + +#endif // QGSPROCESSINGPARAMETERSWIDGET_H diff --git a/src/gui/processing/qgsprocessingtoolboxmodel.cpp b/src/gui/processing/qgsprocessingtoolboxmodel.cpp index 7d5c3bc2cf3c..930e8f142565 100644 --- a/src/gui/processing/qgsprocessingtoolboxmodel.cpp +++ b/src/gui/processing/qgsprocessingtoolboxmodel.cpp @@ -19,6 +19,7 @@ #include "qgsprocessingregistry.h" #include "qgsprocessingrecentalgorithmlog.h" #include +#include #ifdef ENABLE_MODELTEST #include "modeltest.h" @@ -442,6 +443,26 @@ QVariant QgsProcessingToolboxModel::data( const QModelIndex &index, int role ) c } break; + case RoleProviderFlags: + switch ( index.column() ) + { + case 0: + { + if ( provider ) + return static_cast< int >( provider->flags() ); + else if ( algorithm && algorithm->provider() ) + return static_cast< int >( algorithm->provider()->flags() ); + else if ( index.parent().data( RoleProviderFlags ).isValid() ) // group node + return static_cast< int >( index.parent().data( RoleProviderFlags ).toInt() ); + else + return QVariant(); + } + + default: + return QVariant(); + } + break; + case RoleAlgorithmId: switch ( index.column() ) { @@ -825,3 +846,21 @@ bool QgsProcessingToolboxProxyModel::lessThan( const QModelIndex &left, const QM QString rightStr = sourceModel()->data( right ).toString(); return QString::localeAwareCompare( leftStr, rightStr ) < 0; } + +QVariant QgsProcessingToolboxProxyModel::data( const QModelIndex &index, int role ) const +{ + if ( role == Qt::ForegroundRole && !mFilterString.isEmpty() ) + { + QModelIndex sourceIndex = mapToSource( index ); + const QVariant flags = sourceModel()->data( sourceIndex, QgsProcessingToolboxModel::RoleProviderFlags ); + if ( flags.isValid() && flags.toInt() & QgsProcessingProvider::FlagDeemphasiseSearchResults ) + { + QBrush brush( qApp->palette().color( QPalette::Text ), Qt::SolidPattern ); + QColor fadedTextColor = brush.color(); + fadedTextColor.setAlpha( 100 ); + brush.setColor( fadedTextColor ); + return brush; + } + } + return QSortFilterProxyModel::data( index, role ); +} diff --git a/src/gui/processing/qgsprocessingtoolboxmodel.h b/src/gui/processing/qgsprocessingtoolboxmodel.h index fef9dd01ee5b..0df0c4f98326 100644 --- a/src/gui/processing/qgsprocessingtoolboxmodel.h +++ b/src/gui/processing/qgsprocessingtoolboxmodel.h @@ -286,6 +286,7 @@ class GUI_EXPORT QgsProcessingToolboxModel : public QAbstractItemModel RoleAlgorithmName, //!< Untranslated algorithm name, for algorithm nodes RoleAlgorithmShortDescription, //!< Short algorithm description, for algorithm nodes RoleAlgorithmTags, //!< List of algorithm tags, for algorithm nodes + RoleProviderFlags, //!< Returns the node's provider flags }; /** @@ -493,6 +494,7 @@ class GUI_EXPORT QgsProcessingToolboxProxyModel: public QSortFilterProxyModel bool filterAcceptsRow( int sourceRow, const QModelIndex &sourceParent ) const override; bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; private: diff --git a/src/gui/processing/qgsprocessingwidgetwrapper.cpp b/src/gui/processing/qgsprocessingwidgetwrapper.cpp index da1ee07f977d..6070b39e75f6 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapper.cpp +++ b/src/gui/processing/qgsprocessingwidgetwrapper.cpp @@ -23,7 +23,7 @@ #include "qgsexpressioncontext.h" #include "models/qgsprocessingmodelalgorithm.h" #include "qgsexpressioncontextutils.h" - +#include "qgsprocessingwidgetwrapperimpl.h" #include #include @@ -51,6 +51,16 @@ QgsMessageBar *QgsProcessingParameterWidgetContext::messageBar() const return mMessageBar; } +void QgsProcessingParameterWidgetContext::setBrowserModel( QgsBrowserGuiModel *model ) +{ + mBrowserModel = model; +} + +QgsBrowserGuiModel *QgsProcessingParameterWidgetContext::browserModel() const +{ + return mBrowserModel; +} + void QgsProcessingParameterWidgetContext::setProject( QgsProject *project ) { mProject = project; @@ -71,6 +81,16 @@ void QgsProcessingParameterWidgetContext::setModelChildAlgorithmId( const QStrin mModelChildAlgorithmId = modelChildAlgorithmId; } +QgsMapLayer *QgsProcessingParameterWidgetContext::activeLayer() const +{ + return mActiveLayer; +} + +void QgsProcessingParameterWidgetContext::setActiveLayer( QgsMapLayer *activeLayer ) +{ + mActiveLayer = activeLayer; +} + QgsProcessingModelAlgorithm *QgsProcessingParameterWidgetContext::model() const { return mModel; @@ -115,7 +135,7 @@ QWidget *QgsAbstractProcessingParameterWidgetWrapper::createWrappedWidget( QgsPr mWidget = createWidget(); QWidget *wrappedWidget = mWidget; - if ( mType != QgsProcessingGui::Batch && mParameterDefinition->isDynamic() ) + if ( mParameterDefinition->isDynamic() ) { QHBoxLayout *hLayout = new QHBoxLayout(); hLayout->setMargin( 0 ); @@ -131,7 +151,11 @@ QWidget *QgsAbstractProcessingParameterWidgetWrapper::createWrappedWidget( QgsPr wrappedWidget->setLayout( hLayout ); } - setWidgetValue( mParameterDefinition->defaultValue(), context ); + if ( !dynamic_cast( mParameterDefinition ) ) + { + // an exception -- output widgets handle this themselves + setWidgetValue( mParameterDefinition->defaultValue(), context ); + } return wrappedWidget; } @@ -183,11 +207,21 @@ QVariant QgsAbstractProcessingParameterWidgetWrapper::parameterValue() const return widgetValue(); } +QVariantMap QgsAbstractProcessingParameterWidgetWrapper::customProperties() const +{ + return QVariantMap(); +} + void QgsAbstractProcessingParameterWidgetWrapper::registerProcessingContextGenerator( QgsProcessingContextGenerator *generator ) { mProcessingContextGenerator = generator; } +void QgsAbstractProcessingParameterWidgetWrapper::registerProcessingParametersGenerator( QgsProcessingParametersGenerator *generator ) +{ + mParametersGenerator = generator; +} + QLabel *QgsAbstractProcessingParameterWidgetWrapper::createLabel() { switch ( mType ) @@ -320,7 +354,7 @@ void QgsAbstractProcessingParameterWidgetWrapper::setDynamicParentLayerParameter QgsProcessingModelerParameterWidget *QgsProcessingParameterWidgetFactoryInterface::createModelerWidgetWrapper( QgsProcessingModelAlgorithm *model, const QString &childId, const QgsProcessingParameterDefinition *parameter, QgsProcessingContext &context ) { std::unique_ptr< QgsProcessingModelerParameterWidget > widget = qgis::make_unique< QgsProcessingModelerParameterWidget >( model, childId, parameter, context ); - widget->populateSources( compatibleParameterTypes(), compatibleOutputTypes(), compatibleDataTypes() ); + widget->populateSources( compatibleParameterTypes(), compatibleOutputTypes(), compatibleDataTypes( parameter ) ); widget->setExpressionHelpText( modelerExpressionFormatString() ); return widget.release(); } @@ -332,6 +366,11 @@ QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingParameterWidgetFact return nullptr; } +QList QgsProcessingParameterWidgetFactoryInterface::compatibleDataTypes( const QgsProcessingParameterDefinition * ) const +{ + return QList< int >(); +} + QString QgsProcessingParameterWidgetFactoryInterface::modelerExpressionFormatString() const { return QString(); diff --git a/src/gui/processing/qgsprocessingwidgetwrapper.h b/src/gui/processing/qgsprocessingwidgetwrapper.h index 5f277fc01ff6..60d65441073e 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapper.h +++ b/src/gui/processing/qgsprocessingwidgetwrapper.h @@ -39,6 +39,7 @@ class QgsMapCanvas; class QgsProcessingAlgorithm; class QgsProcessingAbstractParameterDefinitionWidget; class QgsMessageBar; +class QgsBrowserGuiModel; /** * \class QgsProcessingContextGenerator @@ -64,6 +65,28 @@ class GUI_EXPORT QgsProcessingContextGenerator virtual ~QgsProcessingContextGenerator() = default; }; +/** + * \class QgsProcessingParametersGenerator + * + * An interface for objects which can create sets of parameter values for processing algorithms. + * + * \ingroup gui + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsProcessingParametersGenerator +{ + public: + + /** + * This method needs to be reimplemented in all classes which implement this interface + * and return a algorithm parameters. + */ + virtual QVariantMap createProcessingParameters() = 0; + + virtual ~QgsProcessingParametersGenerator() = default; +}; + + /** * \ingroup gui * \class QgsProcessingParameterWidgetContext @@ -111,6 +134,20 @@ class GUI_EXPORT QgsProcessingParameterWidgetContext */ QgsMessageBar *messageBar() const; + /** + * Sets the browser \a model associated with the widget. This will usually be the shared app instance of the browser model + * \see browserModel() + * \since QGIS 3.14 + */ + void setBrowserModel( QgsBrowserGuiModel *model ); + + /** + * Returns the browser model associated with the widget. + * \see setBrowserModel() + * \since QGIS 3.12 + */ + QgsBrowserGuiModel *browserModel() const; + /** * Sets the \a project associated with the widget. This allows the widget to retrieve the map layers * and other properties from the correct project. @@ -157,6 +194,22 @@ class GUI_EXPORT QgsProcessingParameterWidgetContext */ void setModelChildAlgorithmId( const QString &id ); + /** + * Returns the current active layer. + * + * \see setActiveLayer() + * \since QGIS 3.14 + */ + QgsMapLayer *activeLayer() const; + + /** + * Sets the current active \a layer. + * + * \see activeLayer() + * \since QGIS 3.14 + */ + void setActiveLayer( QgsMapLayer *layer ); + private: QgsProcessingModelAlgorithm *mModel = nullptr; @@ -169,6 +222,10 @@ class GUI_EXPORT QgsProcessingParameterWidgetContext QgsProject *mProject = nullptr; + QgsBrowserGuiModel *mBrowserModel = nullptr; + + QgsMapLayer *mActiveLayer = nullptr; + }; #ifndef SIP_RUN @@ -309,12 +366,26 @@ class GUI_EXPORT QgsAbstractProcessingParameterWidgetWrapper : public QObject, p */ QVariant parameterValue() const; + /** + * Returns any custom properties set by the wrapper. + */ + virtual QVariantMap customProperties() const; + /** * Registers a Processing context \a generator class that will be used to retrieve * a Processing context for the wrapper when required. */ void registerProcessingContextGenerator( QgsProcessingContextGenerator *generator ); + /** + * Registers a Processing parameters \a generator class that will be used to retrieve + * algorithm parameters for the wrapper when required (e.g. when a wrapper needs access + * to other parameter's values). + * + * \since QGIS 3.14 + */ + void registerProcessingParametersGenerator( QgsProcessingParametersGenerator *generator ); + /** * Called after all wrappers have been created within a particular dialog or context, * allowing the wrapper to connect to the wrappers of other, related parameters. @@ -394,6 +465,7 @@ class GUI_EXPORT QgsAbstractProcessingParameterWidgetWrapper : public QObject, p protected: QgsProcessingContextGenerator *mProcessingContextGenerator = nullptr; + QgsProcessingParametersGenerator *mParametersGenerator = nullptr; QgsProcessingParameterWidgetContext mWidgetContext; private slots: @@ -538,7 +610,7 @@ class GUI_EXPORT QgsProcessingParameterWidgetFactoryInterface /** * Returns a list of compatible Processing data types for inputs - * for this parameter. + * for this widget for the specified \a parameter. * * In order to determine the available sources for the parameter in a model * the types returned by this method are checked. The returned list corresponds @@ -550,7 +622,7 @@ class GUI_EXPORT QgsProcessingParameterWidgetFactoryInterface * \see compatibleParameterTypes() * \see compatibleOutputTypes() */ - virtual QList< int > compatibleDataTypes() const = 0; + virtual QList< int > compatibleDataTypes( const QgsProcessingParameterDefinition *parameter ) const; /** * Returns the expected expression format string for expression results for the parameter diff --git a/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp b/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp index da063d312820..89ef1e8ba2cd 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp +++ b/src/gui/processing/qgsprocessingwidgetwrapperimpl.cpp @@ -49,6 +49,15 @@ #include "qgsmapthemecollection.h" #include "qgsdatetimeedit.h" #include "qgsproviderconnectioncombobox.h" +#include "qgsdatabaseschemacombobox.h" +#include "qgsdatabasetablecombobox.h" +#include "qgsextentwidget.h" +#include "qgsprocessingenummodelerwidget.h" +#include "qgsprocessingmatrixmodelerwidget.h" +#include "qgsprocessingmaplayercombobox.h" +#include "qgsrasterbandcombobox.h" +#include "qgsprocessingoutputdestinationwidget.h" +#include "qgscheckablecombobox.h" #include #include #include @@ -60,6 +69,7 @@ #include #include #include +#include ///@cond PRIVATE @@ -211,11 +221,6 @@ QStringList QgsProcessingBooleanWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputBoolean::typeName(); } -QList QgsProcessingBooleanWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingBooleanWidgetWrapper::parameterType() const { return QgsProcessingParameterBoolean::typeName(); @@ -236,6 +241,32 @@ QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingBooleanWidgetWrappe // QgsProcessingCrsWidgetWrapper // +QgsProcessingCrsParameterDefinitionWidget::QgsProcessingCrsParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mCrsSelector = new QgsProjectionSelectionWidget(); + if ( const QgsProcessingParameterCrs *crsParam = dynamic_cast( definition ) ) + mCrsSelector->setCrs( QgsProcessingParameters::parameterAsCrs( crsParam, crsParam->defaultValue(), context ) ); + else + mCrsSelector->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + + vlayout->addWidget( mCrsSelector ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingCrsParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterCrs >( name, description, mCrsSelector->crs().authid() ); + param->setFlags( flags ); + return param.release(); +} + QgsProcessingCrsWidgetWrapper::QgsProcessingCrsWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -342,11 +373,6 @@ QStringList QgsProcessingCrsWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingCrsWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingCrsWidgetWrapper::modelerExpressionFormatString() const { return tr( "string as EPSG code, WKT or PROJ format, or a string identifying a map layer" ); @@ -362,6 +388,11 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingCrsWidgetWrapper::crea return new QgsProcessingCrsWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingCrsWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingCrsParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // @@ -493,11 +524,6 @@ QStringList QgsProcessingStringWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingStringWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingStringWidgetWrapper::parameterType() const { return QgsProcessingParameterString::typeName(); @@ -574,11 +600,6 @@ QStringList QgsProcessingAuthConfigWidgetWrapper::compatibleOutputTypes() const return QStringList() << QgsProcessingOutputString::typeName(); } -QList QgsProcessingAuthConfigWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingAuthConfigWidgetWrapper::parameterType() const { return QgsProcessingParameterAuthConfig::typeName(); @@ -593,6 +614,67 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingAuthConfigWidgetWrappe // QgsProcessingNumericWidgetWrapper // +QgsProcessingNumberParameterDefinitionWidget::QgsProcessingNumberParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Number type" ) ) ); + + mTypeComboBox = new QComboBox(); + mTypeComboBox->addItem( tr( "Float" ), QgsProcessingParameterNumber::Double ); + mTypeComboBox->addItem( tr( "Integer" ), QgsProcessingParameterNumber::Integer ); + vlayout->addWidget( mTypeComboBox ); + + vlayout->addWidget( new QLabel( tr( "Minimum value" ) ) ); + mMinLineEdit = new QLineEdit(); + vlayout->addWidget( mMinLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Maximum value" ) ) ); + mMaxLineEdit = new QLineEdit(); + vlayout->addWidget( mMaxLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + mDefaultLineEdit = new QLineEdit(); + vlayout->addWidget( mDefaultLineEdit ); + + if ( const QgsProcessingParameterNumber *numberParam = dynamic_cast( definition ) ) + { + mTypeComboBox->setCurrentIndex( mTypeComboBox->findData( numberParam->dataType() ) ); + mMinLineEdit->setText( QString::number( numberParam->minimum() ) ); + mMaxLineEdit->setText( QString::number( numberParam->maximum() ) ); + mDefaultLineEdit->setText( numberParam->defaultValue().toString() ); + } + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingNumberParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + bool ok; + double val = mDefaultLineEdit->text().toDouble( &ok ); + + QgsProcessingParameterNumber::Type dataType = static_cast< QgsProcessingParameterNumber::Type >( mTypeComboBox->currentData().toInt() ); + auto param = qgis::make_unique< QgsProcessingParameterNumber >( name, description, dataType, ok ? val : QVariant() ); + + val = mMinLineEdit->text().toDouble( &ok ); + if ( ok ) + { + param->setMinimum( val ); + } + + val = mMaxLineEdit->text().toDouble( &ok ); + if ( ok ) + { + param->setMaximum( val ); + } + + param->setFlags( flags ); + return param.release(); +} + QgsProcessingNumericWidgetWrapper::QgsProcessingNumericWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -792,11 +874,6 @@ QStringList QgsProcessingNumericWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingNumericWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - double QgsProcessingNumericWidgetWrapper::calculateStep( const double minimum, const double maximum ) { const double valueRange = maximum - minimum; @@ -822,10 +899,125 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingNumericWidgetWrapper:: return new QgsProcessingNumericWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingNumericWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingNumberParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // // QgsProcessingDistanceWidgetWrapper // +QgsProcessingDistanceParameterDefinitionWidget::QgsProcessingDistanceParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Linked input" ) ) ); + + mParentLayerComboBox = new QComboBox(); + + QString initialParent; + if ( const QgsProcessingParameterDistance *distParam = dynamic_cast( definition ) ) + initialParent = distParam->parentParameterName(); + + if ( widgetContext.model() ) + { + // populate combo box with other model input choices + const QMap components = widgetContext.model()->parameterComponents(); + for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) + { + if ( const QgsProcessingParameterFeatureSource *definition = dynamic_cast< const QgsProcessingParameterFeatureSource * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + else if ( const QgsProcessingParameterVectorLayer *definition = dynamic_cast< const QgsProcessingParameterVectorLayer * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + else if ( const QgsProcessingParameterMapLayer *definition = dynamic_cast< const QgsProcessingParameterMapLayer * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + else if ( const QgsProcessingParameterCrs *definition = dynamic_cast< const QgsProcessingParameterCrs * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + } + } + + if ( mParentLayerComboBox->count() == 0 && !initialParent.isEmpty() ) + { + // if no parent candidates found, we just add the existing one as a placeholder + mParentLayerComboBox->addItem( initialParent, initialParent ); + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + + vlayout->addWidget( mParentLayerComboBox ); + + vlayout->addWidget( new QLabel( tr( "Minimum value" ) ) ); + mMinLineEdit = new QLineEdit(); + vlayout->addWidget( mMinLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Maximum value" ) ) ); + mMaxLineEdit = new QLineEdit(); + vlayout->addWidget( mMaxLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + mDefaultLineEdit = new QLineEdit(); + vlayout->addWidget( mDefaultLineEdit ); + + if ( const QgsProcessingParameterDistance *distParam = dynamic_cast( definition ) ) + { + mMinLineEdit->setText( QString::number( distParam->minimum() ) ); + mMaxLineEdit->setText( QString::number( distParam->maximum() ) ); + mDefaultLineEdit->setText( distParam->defaultValue().toString() ); + } + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingDistanceParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + bool ok; + double val = mDefaultLineEdit->text().toDouble( &ok ); + + auto param = qgis::make_unique< QgsProcessingParameterDistance >( name, description, ok ? val : QVariant(), mParentLayerComboBox->currentData().toString() ); + + val = mMinLineEdit->text().toDouble( &ok ); + if ( ok ) + { + param->setMinimum( val ); + } + + val = mMaxLineEdit->text().toFloat( &ok ); + if ( ok ) + { + param->setMaximum( val ); + } + + param->setFlags( flags ); + return param.release(); +} + QgsProcessingDistanceWidgetWrapper::QgsProcessingDistanceWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsProcessingNumericWidgetWrapper( parameter, type, parent ) { @@ -991,10 +1183,45 @@ QVariant QgsProcessingDistanceWidgetWrapper::widgetValue() const } } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingDistanceWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingDistanceParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // // QgsProcessingScaleWidgetWrapper // +QgsProcessingScaleParameterDefinitionWidget::QgsProcessingScaleParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultLineEdit = new QLineEdit(); + + if ( const QgsProcessingParameterScale *scaleParam = dynamic_cast( definition ) ) + { + mDefaultLineEdit->setText( scaleParam->defaultValue().toString() ); + } + + vlayout->addWidget( mDefaultLineEdit ); + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingScaleParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + bool ok; + double val = mDefaultLineEdit->text().toDouble( &ok ); + auto param = qgis::make_unique< QgsProcessingParameterScale >( name, description, ok ? val : QVariant() ); + param->setFlags( flags ); + return param.release(); +} + QgsProcessingScaleWidgetWrapper::QgsProcessingScaleWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsProcessingNumericWidgetWrapper( parameter, type, parent ) { @@ -1066,11 +1293,77 @@ void QgsProcessingScaleWidgetWrapper::setWidgetValue( const QVariant &value, Qgs } } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingScaleWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingScaleParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // // QgsProcessingRangeWidgetWrapper // +QgsProcessingRangeParameterDefinitionWidget::QgsProcessingRangeParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Number type" ) ) ); + + mTypeComboBox = new QComboBox(); + mTypeComboBox->addItem( tr( "Float" ), QgsProcessingParameterNumber::Double ); + mTypeComboBox->addItem( tr( "Integer" ), QgsProcessingParameterNumber::Integer ); + vlayout->addWidget( mTypeComboBox ); + + vlayout->addWidget( new QLabel( tr( "Minimum value" ) ) ); + mMinLineEdit = new QLineEdit(); + vlayout->addWidget( mMinLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Maximum value" ) ) ); + mMaxLineEdit = new QLineEdit(); + vlayout->addWidget( mMaxLineEdit ); + + if ( const QgsProcessingParameterRange *rangeParam = dynamic_cast( definition ) ) + { + mTypeComboBox->setCurrentIndex( mTypeComboBox->findData( rangeParam->dataType() ) ); + const QList< double > range = QgsProcessingParameters::parameterAsRange( rangeParam, rangeParam->defaultValue(), context ); + mMinLineEdit->setText( QString::number( range.at( 0 ) ) ); + mMaxLineEdit->setText( QString::number( range.at( 1 ) ) ); + } + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingRangeParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QString defaultValue; + if ( mMinLineEdit->text().isEmpty() ) + { + defaultValue = QStringLiteral( "None" ); + } + else + { + defaultValue = mMinLineEdit->text(); + } + + if ( mMaxLineEdit->text().isEmpty() ) + { + defaultValue += QStringLiteral( ",None" ); + } + else + { + defaultValue += QStringLiteral( "," ) + mMaxLineEdit->text(); + } + + QgsProcessingParameterNumber::Type dataType = static_cast< QgsProcessingParameterNumber::Type >( mTypeComboBox->currentData().toInt() ); + auto param = qgis::make_unique< QgsProcessingParameterRange >( name, description, dataType, defaultValue ); + param->setFlags( flags ); + return param.release(); +} + + QgsProcessingRangeWidgetWrapper::QgsProcessingRangeWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -1247,11 +1540,6 @@ QStringList QgsProcessingRangeWidgetWrapper::compatibleOutputTypes() const return QStringList() << QgsProcessingOutputString::typeName(); } -QList QgsProcessingRangeWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingRangeWidgetWrapper::modelerExpressionFormatString() const { return tr( "string as two comma delimited floats, e.g. '1,10'" ); @@ -1267,12 +1555,41 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingRangeWidgetWrapper::cr return new QgsProcessingRangeWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingRangeWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingRangeParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} // // QgsProcessingMatrixWidgetWrapper // +QgsProcessingMatrixParameterDefinitionWidget::QgsProcessingMatrixParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + mMatrixWidget = new QgsProcessingMatrixModelerWidget(); + if ( const QgsProcessingParameterMatrix *matrixParam = dynamic_cast( definition ) ) + { + mMatrixWidget->setValue( matrixParam->headers(), matrixParam->defaultValue() ); + mMatrixWidget->setFixedRows( matrixParam->hasFixedNumberRows() ); + } + vlayout->addWidget( mMatrixWidget ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingMatrixParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterMatrix >( name, description, 1, mMatrixWidget->fixedRows(), mMatrixWidget->headers(), mMatrixWidget->value() ); + param->setFlags( flags ); + return param.release(); +} + + QgsProcessingMatrixWidgetWrapper::QgsProcessingMatrixWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -1327,11 +1644,6 @@ QStringList QgsProcessingMatrixWidgetWrapper::compatibleOutputTypes() const return QStringList(); } -QList QgsProcessingMatrixWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingMatrixWidgetWrapper::modelerExpressionFormatString() const { return tr( "comma delimited string of values, or an array of values" ); @@ -1347,7 +1659,10 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingMatrixWidgetWrapper::c return new QgsProcessingMatrixWidgetWrapper( parameter, type ); } - +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingMatrixWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingMatrixParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} // @@ -1504,11 +1819,6 @@ QStringList QgsProcessingFileWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputMapLayer::typeName(); } -QList QgsProcessingFileWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingFileWidgetWrapper::modelerExpressionFormatString() const { return tr( "string representing a path to a file or folder" ); @@ -1531,25 +1841,90 @@ QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingFileWidgetWrapper:: - // // QgsProcessingExpressionWidgetWrapper // -QgsProcessingExpressionWidgetWrapper::QgsProcessingExpressionWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) - : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +QgsProcessingExpressionParameterDefinitionWidget::QgsProcessingExpressionParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) { + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); -} + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); -QWidget *QgsProcessingExpressionWidgetWrapper::createWidget() -{ - const QgsProcessingParameterExpression *expParam = dynamic_cast< const QgsProcessingParameterExpression *>( parameterDefinition() ); - switch ( type() ) - { - case QgsProcessingGui::Standard: - case QgsProcessingGui::Modeler: - case QgsProcessingGui::Batch: + mDefaultLineEdit = new QgsExpressionLineEdit(); + if ( const QgsProcessingParameterExpression *expParam = dynamic_cast( definition ) ) + mDefaultLineEdit->setExpression( QgsProcessingParameters::parameterAsExpression( expParam, expParam->defaultValue(), context ) ); + vlayout->addWidget( mDefaultLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Parent layer" ) ) ); + + mParentLayerComboBox = new QComboBox(); + mParentLayerComboBox->addItem( tr( "None" ), QVariant() ); + + QString initialParent; + if ( const QgsProcessingParameterExpression *expParam = dynamic_cast( definition ) ) + initialParent = expParam->parentLayerParameterName(); + + if ( widgetContext.model() ) + { + // populate combo box with other model input choices + const QMap components = widgetContext.model()->parameterComponents(); + for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) + { + if ( const QgsProcessingParameterFeatureSource *definition = dynamic_cast< const QgsProcessingParameterFeatureSource * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + else if ( const QgsProcessingParameterVectorLayer *definition = dynamic_cast< const QgsProcessingParameterVectorLayer * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + } + } + + if ( mParentLayerComboBox->count() == 1 && !initialParent.isEmpty() ) + { + // if no parent candidates found, we just add the existing one as a placeholder + mParentLayerComboBox->addItem( initialParent, initialParent ); + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + + vlayout->addWidget( mParentLayerComboBox ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingExpressionParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterExpression >( name, description, mDefaultLineEdit->expression(), mParentLayerComboBox->currentData().toString() ); + param->setFlags( flags ); + return param.release(); +} + +QgsProcessingExpressionWidgetWrapper::QgsProcessingExpressionWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingExpressionWidgetWrapper::createWidget() +{ + const QgsProcessingParameterExpression *expParam = dynamic_cast< const QgsProcessingParameterExpression *>( parameterDefinition() ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Modeler: + case QgsProcessingGui::Batch: { if ( expParam->parentLayerParameterName().isEmpty() ) { @@ -1688,11 +2063,6 @@ QStringList QgsProcessingExpressionWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputNumber::typeName(); } -QList QgsProcessingExpressionWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingExpressionWidgetWrapper::modelerExpressionFormatString() const { return tr( "string representation of an expression" ); @@ -1716,6 +2086,11 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingExpressionWidgetWrappe return new QgsProcessingExpressionWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingExpressionWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingExpressionParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // @@ -1769,16 +2144,39 @@ void QgsProcessingEnumPanelWidget::showDialog() availableOptions << i; } - QgsProcessingMultipleSelectionDialog dlg( availableOptions, mValue, this, nullptr ); const QStringList options = mParam ? mParam->options() : QStringList(); - dlg.setValueFormatter( [options]( const QVariant & v ) -> QString + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) { - const int i = v.toInt(); - return options.size() > i ? options.at( i ) : QString(); - } ); - if ( dlg.exec() ) + QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, mValue ); + widget->setPanelTitle( mParam->description() ); + + widget->setValueFormatter( [options]( const QVariant & v ) -> QString + { + const int i = v.toInt(); + return options.size() > i ? options.at( i ) : QString(); + } ); + + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [ = ]() + { + setValue( widget->selectedOptions() ); + } ); + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel ); + panel->openPanel( widget ); + } + else { - setValue( dlg.selectedOptions() ); + QgsProcessingMultipleSelectionDialog dlg( availableOptions, mValue, this, nullptr ); + + dlg.setValueFormatter( [options]( const QVariant & v ) -> QString + { + const int i = v.toInt(); + return options.size() > i ? options.at( i ) : QString(); + } ); + if ( dlg.exec() ) + { + setValue( dlg.selectedOptions() ); + } } } @@ -1911,6 +2309,32 @@ void QgsProcessingEnumCheckboxPanelWidget::deselectAll() // QgsProcessingEnumWidgetWrapper // +QgsProcessingEnumParameterDefinitionWidget::QgsProcessingEnumParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + mEnumWidget = new QgsProcessingEnumModelerWidget(); + if ( const QgsProcessingParameterEnum *enumParam = dynamic_cast( definition ) ) + { + mEnumWidget->setAllowMultiple( enumParam->allowMultiple() ); + mEnumWidget->setOptions( enumParam->options() ); + mEnumWidget->setDefaultOptions( enumParam->defaultValue() ); + } + vlayout->addWidget( mEnumWidget ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingEnumParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterEnum >( name, description, mEnumWidget->options(), mEnumWidget->allowMultiple(), mEnumWidget->defaultOptions() ); + param->setFlags( flags ); + return param.release(); +} + + QgsProcessingEnumWidgetWrapper::QgsProcessingEnumWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -2029,11 +2453,6 @@ QStringList QgsProcessingEnumWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputNumber::typeName(); } -QList QgsProcessingEnumWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingEnumWidgetWrapper::modelerExpressionFormatString() const { return tr( "selected option index (starting from 0), array of indices, or comma separated string of options (e.g. '1,3')" ); @@ -2049,6 +2468,11 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingEnumWidgetWrapper::cre return new QgsProcessingEnumWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingEnumWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingEnumParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // // QgsProcessingLayoutWidgetWrapper // @@ -2142,11 +2566,6 @@ QStringList QgsProcessingLayoutWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingLayoutWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingLayoutWidgetWrapper::modelerExpressionFormatString() const { return tr( "string representing the name of an existing print layout" ); @@ -2361,11 +2780,6 @@ QStringList QgsProcessingLayoutItemWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingLayoutItemWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingLayoutItemWidgetWrapper::modelerExpressionFormatString() const { return tr( "string representing the UUID or ID of an existing print layout item" ); @@ -2525,11 +2939,39 @@ void QgsProcessingPointPanel::pointPicked() - // // QgsProcessingPointWidgetWrapper // +QgsProcessingPointParameterDefinitionWidget::QgsProcessingPointParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultLineEdit = new QLineEdit(); + mDefaultLineEdit->setToolTip( tr( "Point as 'x,y'" ) ); + mDefaultLineEdit->setPlaceholderText( tr( "Point as 'x,y'" ) ); + if ( const QgsProcessingParameterPoint *pointParam = dynamic_cast( definition ) ) + { + QgsPointXY point = QgsProcessingParameters::parameterAsPoint( pointParam, pointParam->defaultValue(), context ); + mDefaultLineEdit->setText( QStringLiteral( "%1,%2" ).arg( QString::number( point.x(), 'f' ), QString::number( point.y(), 'f' ) ) ); + } + + vlayout->addWidget( mDefaultLineEdit ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingPointParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterPoint >( name, description, mDefaultLineEdit->text() ); + param->setFlags( flags ); + return param.release(); +} + QgsProcessingPointWidgetWrapper::QgsProcessingPointWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -2649,11 +3091,6 @@ QStringList QgsProcessingPointWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingPointWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingPointWidgetWrapper::modelerExpressionFormatString() const { return tr( "string of the format 'x,y' or a geometry value (centroid is used)" ); @@ -2669,6 +3106,10 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingPointWidgetWrapper::cr return new QgsProcessingPointWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingPointWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingPointParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} @@ -2797,11 +3238,6 @@ QStringList QgsProcessingColorWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingColorWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingColorWidgetWrapper::modelerExpressionFormatString() const { return tr( "color style string, e.g. #ff0000 or 255,0,0" ); @@ -2863,7 +3299,7 @@ QgsProcessingCoordinateOperationParameterDefinitionWidget::QgsProcessingCoordina const QMap components = widgetContext.model()->parameterComponents(); for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) { - if ( it->parameterName() == definition->name() ) + if ( definition && it->parameterName() == definition->name() ) continue; // TODO - we should probably filter this list? @@ -3084,11 +3520,6 @@ QStringList QgsProcessingCoordinateOperationWidgetWrapper::compatibleOutputTypes << QgsProcessingOutputString::typeName(); } -QList QgsProcessingCoordinateOperationWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingCoordinateOperationWidgetWrapper::modelerExpressionFormatString() const { return tr( "Proj coordinate operation string, e.g. '+proj=pipeline +step +inv...'" ); @@ -3209,14 +3640,36 @@ void QgsProcessingFieldPanelWidget::showDialog() availableOptions << field.name(); } - QgsProcessingMultipleSelectionDialog dlg( availableOptions, mValue, this, nullptr ); - dlg.setValueFormatter( []( const QVariant & v ) -> QString + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) { - return v.toString(); - } ); - if ( dlg.exec() ) + QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, mValue ); + widget->setPanelTitle( mParam->description() ); + + widget->setValueFormatter( []( const QVariant & v ) -> QString + { + return v.toString(); + } ); + + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [ = ]() + { + setValue( widget->selectedOptions() ); + } ); + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel ); + panel->openPanel( widget ); + } + else { - setValue( dlg.selectedOptions() ); + QgsProcessingMultipleSelectionDialog dlg( availableOptions, mValue, this, nullptr ); + + dlg.setValueFormatter( []( const QVariant & v ) -> QString + { + return v.toString(); + } ); + if ( dlg.exec() ) + { + setValue( dlg.selectedOptions() ); + } } } @@ -3231,6 +3684,105 @@ void QgsProcessingFieldPanelWidget::updateSummaryText() // QgsProcessingFieldWidgetWrapper // +QgsProcessingFieldParameterDefinitionWidget::QgsProcessingFieldParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Parent layer" ) ) ); + mParentLayerComboBox = new QComboBox(); + + QString initialParent; + if ( const QgsProcessingParameterField *fieldParam = dynamic_cast( definition ) ) + initialParent = fieldParam->parentLayerParameterName(); + + if ( widgetContext.model() ) + { + // populate combo box with other model input choices + const QMap components = widgetContext.model()->parameterComponents(); + for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) + { + if ( const QgsProcessingParameterFeatureSource *definition = dynamic_cast< const QgsProcessingParameterFeatureSource * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + else if ( const QgsProcessingParameterVectorLayer *definition = dynamic_cast< const QgsProcessingParameterVectorLayer * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + } + } + + if ( mParentLayerComboBox->count() == 0 && !initialParent.isEmpty() ) + { + // if no parent candidates found, we just add the existing one as a placeholder + mParentLayerComboBox->addItem( initialParent, initialParent ); + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + + vlayout->addWidget( mParentLayerComboBox ); + + vlayout->addWidget( new QLabel( tr( "Allowed data type" ) ) ); + mDataTypeComboBox = new QComboBox(); + mDataTypeComboBox->addItem( tr( "Any" ), QgsProcessingParameterField::Any ); + mDataTypeComboBox->addItem( tr( "Number" ), QgsProcessingParameterField::Numeric ); + mDataTypeComboBox->addItem( tr( "String" ), QgsProcessingParameterField::String ); + mDataTypeComboBox->addItem( tr( "Date/time" ), QgsProcessingParameterField::DateTime ); + if ( const QgsProcessingParameterField *fieldParam = dynamic_cast( definition ) ) + mDataTypeComboBox->setCurrentIndex( mDataTypeComboBox->findData( fieldParam->dataType() ) ); + + vlayout->addWidget( mDataTypeComboBox ); + + mAllowMultipleCheckBox = new QCheckBox( tr( "Accept multiple fields" ) ); + if ( const QgsProcessingParameterField *fieldParam = dynamic_cast( definition ) ) + mAllowMultipleCheckBox->setChecked( fieldParam->allowMultiple() ); + + vlayout->addWidget( mAllowMultipleCheckBox ); + + mDefaultToAllCheckBox = new QCheckBox( tr( "Select all fields by default" ) ); + mDefaultToAllCheckBox->setEnabled( mAllowMultipleCheckBox->isChecked() ); + if ( const QgsProcessingParameterField *fieldParam = dynamic_cast( definition ) ) + mDefaultToAllCheckBox->setChecked( fieldParam->defaultToAllFields() ); + + vlayout->addWidget( mDefaultToAllCheckBox ); + + connect( mAllowMultipleCheckBox, &QCheckBox::stateChanged, this, [ = ] + { + mDefaultToAllCheckBox->setEnabled( mAllowMultipleCheckBox->isChecked() ); + } ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultLineEdit = new QLineEdit(); + mDefaultLineEdit->setToolTip( tr( "Default field name, or ; separated list of field names for multiple field parameters" ) ); + if ( const QgsProcessingParameterField *fieldParam = dynamic_cast( definition ) ) + { + const QStringList fields = QgsProcessingParameters::parameterAsFields( fieldParam, fieldParam->defaultValue(), context ); + mDefaultLineEdit->setText( fields.join( ';' ) ); + } + vlayout->addWidget( mDefaultLineEdit ); + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingFieldParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QgsProcessingParameterField::DataType dataType = static_cast< QgsProcessingParameterField::DataType >( mDataTypeComboBox->currentData().toInt() ); + auto param = qgis::make_unique< QgsProcessingParameterField >( name, description, mDefaultLineEdit->text(), mParentLayerComboBox->currentData().toString(), dataType, mAllowMultipleCheckBox->isChecked(), false, mDefaultToAllCheckBox->isChecked() ); + param->setFlags( flags ); + return param.release(); +} + QgsProcessingFieldWidgetWrapper::QgsProcessingFieldWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) { @@ -3479,11 +4031,6 @@ QStringList QgsProcessingFieldWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingFieldWidgetWrapper::compatibleDataTypes() const -{ - return QList(); -} - QString QgsProcessingFieldWidgetWrapper::modelerExpressionFormatString() const { return tr( "selected field names as an array of names, or semicolon separated string of options (e.g. 'fid;place_name')" ); @@ -3539,6 +4086,11 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingFieldWidgetWrapper::cr return new QgsProcessingFieldWidgetWrapper( parameter, type ); } +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingFieldWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingFieldParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + // // QgsProcessingMapThemeWidgetWrapper // @@ -3675,11 +4227,6 @@ QStringList QgsProcessingMapThemeWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingMapThemeWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingMapThemeWidgetWrapper::modelerExpressionFormatString() const { return tr( "map theme as a string value (e.g. 'base maps')" ); @@ -3849,24 +4396,22 @@ QStringList QgsProcessingDateTimeWidgetWrapper::compatibleOutputTypes() const << QgsProcessingOutputString::typeName(); } -QList QgsProcessingDateTimeWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingDateTimeWidgetWrapper::modelerExpressionFormatString() const { const QgsProcessingParameterDateTime *dateTimeParam = dynamic_cast< const QgsProcessingParameterDateTime *>( parameterDefinition() ); - switch ( dateTimeParam->dataType() ) + if ( dateTimeParam ) { - case QgsProcessingParameterDateTime::DateTime: - return tr( "datetime value, or a ISO string representation of a datetime" ); + switch ( dateTimeParam->dataType() ) + { + case QgsProcessingParameterDateTime::DateTime: + return tr( "datetime value, or a ISO string representation of a datetime" ); - case QgsProcessingParameterDateTime::Date: - return tr( "date value, or a ISO string representation of a date" ); + case QgsProcessingParameterDateTime::Date: + return tr( "date value, or a ISO string representation of a date" ); - case QgsProcessingParameterDateTime::Time: - return tr( "time value, or a ISO string representation of a time" ); + case QgsProcessingParameterDateTime::Time: + return tr( "time value, or a ISO string representation of a time" ); + } } return QString(); } @@ -4020,11 +4565,6 @@ QStringList QgsProcessingProviderConnectionWidgetWrapper::compatibleOutputTypes( << QgsProcessingOutputString::typeName(); } -QList QgsProcessingProviderConnectionWidgetWrapper::compatibleDataTypes() const -{ - return QList< int >(); -} - QString QgsProcessingProviderConnectionWidgetWrapper::modelerExpressionFormatString() const { return tr( "connection name as a string value" ); @@ -4043,5 +4583,2163 @@ QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingProviderConnectionWidg +// +// QgsProcessingDatabaseSchemaWidgetWrapper +// + +QgsProcessingDatabaseSchemaParameterDefinitionWidget::QgsProcessingDatabaseSchemaParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + const QgsProcessingParameterDatabaseSchema *schemaParam = dynamic_cast< const QgsProcessingParameterDatabaseSchema *>( definition ); + + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + mConnectionParamComboBox = new QComboBox(); + QString initialConnection; + if ( schemaParam ) + { + initialConnection = schemaParam->parentConnectionParameterName(); + } + + if ( widgetContext.model() ) + { + // populate combo box with other model input choices + const QMap components = widgetContext.model()->parameterComponents(); + for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) + { + if ( definition && it->parameterName() == definition->name() ) + continue; + + if ( !dynamic_cast< const QgsProcessingParameterProviderConnection * >( widgetContext.model()->parameterDefinition( it->parameterName() ) ) ) + continue; + + mConnectionParamComboBox->addItem( it->parameterName(), it->parameterName() ); + if ( !initialConnection.isEmpty() && initialConnection == it->parameterName() ) + { + mConnectionParamComboBox->setCurrentIndex( mConnectionParamComboBox->count() - 1 ); + } + } + } + + if ( mConnectionParamComboBox->count() == 0 && !initialConnection.isEmpty() ) + { + // if no candidates found, we just add the existing one as a placeholder + mConnectionParamComboBox->addItem( initialConnection, initialConnection ); + mConnectionParamComboBox->setCurrentIndex( mConnectionParamComboBox->count() - 1 ); + } + + vlayout->addWidget( new QLabel( tr( "Provider connection parameter" ) ) ); + vlayout->addWidget( mConnectionParamComboBox ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultEdit = new QLineEdit(); + vlayout->addWidget( mDefaultEdit ); + setLayout( vlayout ); + + if ( schemaParam ) + { + mDefaultEdit->setText( schemaParam->defaultValue().toString() ); + } +} + +QgsProcessingParameterDefinition *QgsProcessingDatabaseSchemaParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QVariant defaultVal; + if ( mDefaultEdit->text().isEmpty() ) + defaultVal = QVariant(); + else + defaultVal = mDefaultEdit->text(); + auto param = qgis::make_unique< QgsProcessingParameterDatabaseSchema>( name, description, mConnectionParamComboBox->currentData().toString(), defaultVal ); + param->setFlags( flags ); + return param.release(); +} + + +QgsProcessingDatabaseSchemaWidgetWrapper::QgsProcessingDatabaseSchemaWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingDatabaseSchemaWidgetWrapper::createWidget() +{ + const QgsProcessingParameterDatabaseSchema *schemaParam = dynamic_cast< const QgsProcessingParameterDatabaseSchema *>( parameterDefinition() ); + + mSchemaComboBox = new QgsDatabaseSchemaComboBox( QString(), QString() ); + if ( schemaParam->flags() & QgsProcessingParameterDefinition::FlagOptional ) + mSchemaComboBox->setAllowEmptySchema( true ); + + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + break; + case QgsProcessingGui::Modeler: + mSchemaComboBox->comboBox()->setEditable( true ); + break; + } + + mSchemaComboBox->setToolTip( parameterDefinition()->toolTip() ); + connect( mSchemaComboBox->comboBox(), &QComboBox::currentTextChanged, this, [ = ]( const QString & ) + { + if ( mBlockSignals ) + return; + + emit widgetValueHasChanged( this ); + } ); + + return mSchemaComboBox; +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingDatabaseSchemaWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingDatabaseSchemaParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + +void QgsProcessingDatabaseSchemaWidgetWrapper::setParentConnectionWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ) +{ + // evaluate value to connection + QgsProcessingContext *context = nullptr; + std::unique_ptr< QgsProcessingContext > tmpContext; + if ( mProcessingContextGenerator ) + context = mProcessingContextGenerator->processingContext(); + + if ( !context ) + { + tmpContext = qgis::make_unique< QgsProcessingContext >(); + context = tmpContext.get(); + } + + const QVariant value = parentWrapper->parameterValue(); + const QString connection = value.isValid() ? QgsProcessingParameters::parameterAsConnectionName( parentWrapper->parameterDefinition(), value, *context ) : QString(); + + if ( mSchemaComboBox ) + mSchemaComboBox->setConnectionName( connection, dynamic_cast< const QgsProcessingParameterProviderConnection * >( parentWrapper->parameterDefinition() )->providerId() ); + + const QgsProcessingParameterDatabaseSchema *schemaParam = static_cast< const QgsProcessingParameterDatabaseSchema * >( parameterDefinition() ); + if ( schemaParam->defaultValue().isValid() ) + setWidgetValue( parameterDefinition()->defaultValue(), *context ); +} + +void QgsProcessingDatabaseSchemaWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext &context ) +{ + const QString v = QgsProcessingParameters::parameterAsSchema( parameterDefinition(), value, context ); + + if ( !value.isValid() ) + mSchemaComboBox->comboBox()->setCurrentIndex( -1 ); + else + { + if ( mSchemaComboBox->comboBox()->isEditable() ) + { + const QString prev = mSchemaComboBox->comboBox()->currentText(); + mBlockSignals++; + mSchemaComboBox->setSchema( v ); + mSchemaComboBox->comboBox()->setCurrentText( v ); + + mBlockSignals--; + if ( prev != v ) + emit widgetValueHasChanged( this ); + } + else + mSchemaComboBox->setSchema( v ); + } +} + +QVariant QgsProcessingDatabaseSchemaWidgetWrapper::widgetValue() const +{ + if ( mSchemaComboBox ) + if ( mSchemaComboBox->comboBox()->isEditable() ) + return mSchemaComboBox->comboBox()->currentText().isEmpty() ? QVariant() : QVariant( mSchemaComboBox->comboBox()->currentText() ); + else + return mSchemaComboBox->currentSchema().isEmpty() ? QVariant() : QVariant( mSchemaComboBox->currentSchema() ); + else + return QVariant(); +} + +QStringList QgsProcessingDatabaseSchemaWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterProviderConnection::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingDatabaseSchemaWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName(); +} + +QString QgsProcessingDatabaseSchemaWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "database schema name as a string value" ); +} + +QString QgsProcessingDatabaseSchemaWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterDatabaseSchema::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingDatabaseSchemaWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingDatabaseSchemaWidgetWrapper( parameter, type ); +} + +void QgsProcessingDatabaseSchemaWidgetWrapper::postInitialize( const QList &wrappers ) +{ + QgsAbstractProcessingParameterWidgetWrapper::postInitialize( wrappers ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + for ( const QgsAbstractProcessingParameterWidgetWrapper *wrapper : wrappers ) + { + if ( wrapper->parameterDefinition()->name() == static_cast< const QgsProcessingParameterDatabaseSchema * >( parameterDefinition() )->parentConnectionParameterName() ) + { + setParentConnectionWrapperValue( wrapper ); + connect( wrapper, &QgsAbstractProcessingParameterWidgetWrapper::widgetValueHasChanged, this, [ = ] + { + setParentConnectionWrapperValue( wrapper ); + } ); + break; + } + } + break; + } + + case QgsProcessingGui::Modeler: + break; + } +} + + + +// +// QgsProcessingDatabaseTableWidgetWrapper +// + +QgsProcessingDatabaseTableParameterDefinitionWidget::QgsProcessingDatabaseTableParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + const QgsProcessingParameterDatabaseTable *tableParam = dynamic_cast< const QgsProcessingParameterDatabaseTable *>( definition ); + + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + mConnectionParamComboBox = new QComboBox(); + mSchemaParamComboBox = new QComboBox(); + QString initialConnection; + QString initialSchema; + if ( tableParam ) + { + initialConnection = tableParam->parentConnectionParameterName(); + initialSchema = tableParam->parentSchemaParameterName(); + } + + if ( widgetContext.model() ) + { + // populate combo box with other model input choices + const QMap components = widgetContext.model()->parameterComponents(); + for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) + { + if ( definition && it->parameterName() == definition->name() ) + continue; + + if ( dynamic_cast< const QgsProcessingParameterProviderConnection * >( widgetContext.model()->parameterDefinition( it->parameterName() ) ) ) + { + mConnectionParamComboBox->addItem( it->parameterName(), it->parameterName() ); + if ( !initialConnection.isEmpty() && initialConnection == it->parameterName() ) + { + mConnectionParamComboBox->setCurrentIndex( mConnectionParamComboBox->count() - 1 ); + } + } + else if ( dynamic_cast< const QgsProcessingParameterDatabaseSchema * >( widgetContext.model()->parameterDefinition( it->parameterName() ) ) ) + { + mSchemaParamComboBox->addItem( it->parameterName(), it->parameterName() ); + if ( !initialConnection.isEmpty() && initialConnection == it->parameterName() ) + { + mSchemaParamComboBox->setCurrentIndex( mSchemaParamComboBox->count() - 1 ); + } + } + } + } + + if ( mConnectionParamComboBox->count() == 0 && !initialConnection.isEmpty() ) + { + // if no candidates found, we just add the existing one as a placeholder + mConnectionParamComboBox->addItem( initialConnection, initialConnection ); + mConnectionParamComboBox->setCurrentIndex( mConnectionParamComboBox->count() - 1 ); + } + + if ( mSchemaParamComboBox->count() == 0 && !initialSchema.isEmpty() ) + { + // if no candidates found, we just add the existing one as a placeholder + mSchemaParamComboBox->addItem( initialSchema, initialSchema ); + mSchemaParamComboBox->setCurrentIndex( mSchemaParamComboBox->count() - 1 ); + } + + vlayout->addWidget( new QLabel( tr( "Provider connection parameter" ) ) ); + vlayout->addWidget( mConnectionParamComboBox ); + + vlayout->addWidget( new QLabel( tr( "Database schema parameter" ) ) ); + vlayout->addWidget( mSchemaParamComboBox ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultEdit = new QLineEdit(); + vlayout->addWidget( mDefaultEdit ); + setLayout( vlayout ); + + if ( tableParam ) + { + mDefaultEdit->setText( tableParam->defaultValue().toString() ); + } +} + +QgsProcessingParameterDefinition *QgsProcessingDatabaseTableParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QVariant defaultVal; + if ( mDefaultEdit->text().isEmpty() ) + defaultVal = QVariant(); + else + defaultVal = mDefaultEdit->text(); + auto param = qgis::make_unique< QgsProcessingParameterDatabaseTable>( name, description, + mConnectionParamComboBox->currentData().toString(), + mSchemaParamComboBox->currentData().toString(), + defaultVal ); + param->setFlags( flags ); + return param.release(); +} + + +QgsProcessingDatabaseTableWidgetWrapper::QgsProcessingDatabaseTableWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingDatabaseTableWidgetWrapper::createWidget() +{ + const QgsProcessingParameterDatabaseTable *tableParam = dynamic_cast< const QgsProcessingParameterDatabaseTable *>( parameterDefinition() ); + + mTableComboBox = new QgsDatabaseTableComboBox( QString(), QString() ); + if ( tableParam->flags() & QgsProcessingParameterDefinition::FlagOptional ) + mTableComboBox->setAllowEmptyTable( true ); + + if ( type() == QgsProcessingGui::Modeler || tableParam->allowNewTableNames() ) + mTableComboBox->comboBox()->setEditable( true ); + + mTableComboBox->setToolTip( parameterDefinition()->toolTip() ); + connect( mTableComboBox->comboBox(), &QComboBox::currentTextChanged, this, [ = ]( const QString & ) + { + if ( mBlockSignals ) + return; + + emit widgetValueHasChanged( this ); + } ); + + return mTableComboBox; +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingDatabaseTableWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingDatabaseTableParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + +void QgsProcessingDatabaseTableWidgetWrapper::setParentConnectionWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ) +{ + // evaluate value to connection + QgsProcessingContext *context = nullptr; + std::unique_ptr< QgsProcessingContext > tmpContext; + if ( mProcessingContextGenerator ) + context = mProcessingContextGenerator->processingContext(); + + if ( !context ) + { + tmpContext = qgis::make_unique< QgsProcessingContext >(); + context = tmpContext.get(); + } + + QVariant value = parentWrapper->parameterValue(); + mConnection = value.isValid() ? QgsProcessingParameters::parameterAsConnectionName( parentWrapper->parameterDefinition(), value, *context ) : QString(); + mProvider = dynamic_cast< const QgsProcessingParameterProviderConnection * >( parentWrapper->parameterDefinition() )->providerId(); + if ( mTableComboBox && !mSchema.isEmpty() ) + { + mTableComboBox->setSchema( mSchema ); + mTableComboBox->setConnectionName( mConnection, mProvider ); + + const QgsProcessingParameterDatabaseTable *tableParam = static_cast< const QgsProcessingParameterDatabaseTable * >( parameterDefinition() ); + if ( tableParam->defaultValue().isValid() ) + setWidgetValue( parameterDefinition()->defaultValue(), *context ); + } +} + +void QgsProcessingDatabaseTableWidgetWrapper::setParentSchemaWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ) +{ + // evaluate value to schema + QgsProcessingContext *context = nullptr; + std::unique_ptr< QgsProcessingContext > tmpContext; + if ( mProcessingContextGenerator ) + context = mProcessingContextGenerator->processingContext(); + + if ( !context ) + { + tmpContext = qgis::make_unique< QgsProcessingContext >(); + context = tmpContext.get(); + } + + QVariant value = parentWrapper->parameterValue(); + mSchema = value.isValid() ? QgsProcessingParameters::parameterAsSchema( parentWrapper->parameterDefinition(), value, *context ) : QString(); + + if ( mTableComboBox && !mSchema.isEmpty() && !mConnection.isEmpty() ) + { + mTableComboBox->setSchema( mSchema ); + mTableComboBox->setConnectionName( mConnection, mProvider ); + + const QgsProcessingParameterDatabaseTable *tableParam = static_cast< const QgsProcessingParameterDatabaseTable * >( parameterDefinition() ); + if ( tableParam->defaultValue().isValid() ) + setWidgetValue( parameterDefinition()->defaultValue(), *context ); + } + +} + +void QgsProcessingDatabaseTableWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext &context ) +{ + const QString v = QgsProcessingParameters::parameterAsDatabaseTableName( parameterDefinition(), value, context ); + + if ( !value.isValid() ) + mTableComboBox->comboBox()->setCurrentIndex( -1 ); + else + { + if ( mTableComboBox->comboBox()->isEditable() ) + { + const QString prev = mTableComboBox->comboBox()->currentText(); + mBlockSignals++; + mTableComboBox->setTable( v ); + mTableComboBox->comboBox()->setCurrentText( v ); + + mBlockSignals--; + if ( prev != v ) + emit widgetValueHasChanged( this ); + } + else + mTableComboBox->setTable( v ); + } +} + +QVariant QgsProcessingDatabaseTableWidgetWrapper::widgetValue() const +{ + if ( mTableComboBox ) + if ( mTableComboBox->comboBox()->isEditable() ) + return mTableComboBox->comboBox()->currentText().isEmpty() ? QVariant() : QVariant( mTableComboBox->comboBox()->currentText() ); + else + return mTableComboBox->currentTable().isEmpty() ? QVariant() : QVariant( mTableComboBox->currentTable() ); + else + return QVariant(); +} + +QStringList QgsProcessingDatabaseTableWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterProviderConnection::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingDatabaseTableWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName(); +} + +QString QgsProcessingDatabaseTableWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "database table name as a string value" ); +} + +QString QgsProcessingDatabaseTableWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterDatabaseTable::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingDatabaseTableWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingDatabaseTableWidgetWrapper( parameter, type ); +} + +void QgsProcessingDatabaseTableWidgetWrapper::postInitialize( const QList &wrappers ) +{ + QgsAbstractProcessingParameterWidgetWrapper::postInitialize( wrappers ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + for ( const QgsAbstractProcessingParameterWidgetWrapper *wrapper : wrappers ) + { + if ( wrapper->parameterDefinition()->name() == static_cast< const QgsProcessingParameterDatabaseTable * >( parameterDefinition() )->parentConnectionParameterName() ) + { + setParentConnectionWrapperValue( wrapper ); + connect( wrapper, &QgsAbstractProcessingParameterWidgetWrapper::widgetValueHasChanged, this, [ = ] + { + setParentConnectionWrapperValue( wrapper ); + } ); + } + else if ( wrapper->parameterDefinition()->name() == static_cast< const QgsProcessingParameterDatabaseTable * >( parameterDefinition() )->parentSchemaParameterName() ) + { + setParentSchemaWrapperValue( wrapper ); + connect( wrapper, &QgsAbstractProcessingParameterWidgetWrapper::widgetValueHasChanged, this, [ = ] + { + setParentSchemaWrapperValue( wrapper ); + } ); + } + } + break; + } + + case QgsProcessingGui::Modeler: + break; + } +} + + +// +// QgsProcessingExtentWidgetWrapper +// + +QgsProcessingExtentParameterDefinitionWidget::QgsProcessingExtentParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultWidget = new QgsExtentWidget(); + mDefaultWidget->setNullValueAllowed( true, tr( "Not set" ) ); + if ( const QgsProcessingParameterExtent *extentParam = dynamic_cast( definition ) ) + { + if ( extentParam->defaultValue().isValid() ) + { + QgsRectangle rect = QgsProcessingParameters::parameterAsExtent( extentParam, extentParam->defaultValue(), context ); + QgsCoordinateReferenceSystem crs = QgsProcessingParameters::parameterAsExtentCrs( extentParam, extentParam->defaultValue(), context ); + mDefaultWidget->setCurrentExtent( rect, crs ); + mDefaultWidget->setOutputExtentFromCurrent(); + } + else + { + mDefaultWidget->clear(); + } + } + + vlayout->addWidget( mDefaultWidget ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingExtentParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + const QString defaultVal = mDefaultWidget->isValid() ? QStringLiteral( "%1,%2,%3,%4%5" ).arg( + QString::number( mDefaultWidget->outputExtent().xMinimum(), 'f', 9 ), + QString::number( mDefaultWidget->outputExtent().xMaximum(), 'f', 9 ), + QString::number( mDefaultWidget->outputExtent().yMinimum(), 'f', 9 ), + QString::number( mDefaultWidget->outputExtent().yMaximum(), 'f', 9 ), + mDefaultWidget->outputCrs().isValid() ? QStringLiteral( " [%1]" ).arg( mDefaultWidget->outputCrs().authid() ) : QString() + ) : QString(); + auto param = qgis::make_unique< QgsProcessingParameterExtent >( name, description, !defaultVal.isEmpty() ? QVariant( defaultVal ) : QVariant() ); + param->setFlags( flags ); + return param.release(); +} + + + +QgsProcessingExtentWidgetWrapper::QgsProcessingExtentWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingExtentWidgetWrapper::createWidget() +{ + const QgsProcessingParameterExtent *extentParam = dynamic_cast< const QgsProcessingParameterExtent *>( parameterDefinition() ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + { + mExtentWidget = new QgsExtentWidget( nullptr ); + if ( widgetContext().mapCanvas() ) + mExtentWidget->setMapCanvas( widgetContext().mapCanvas() ); + + if ( extentParam->flags() & QgsProcessingParameterDefinition::FlagOptional ) + mExtentWidget->setNullValueAllowed( true, tr( "Not set" ) ); + + mExtentWidget->setToolTip( parameterDefinition()->toolTip() ); + + connect( mExtentWidget, &QgsExtentWidget::extentChanged, this, [ = ] + { + emit widgetValueHasChanged( this ); + } ); + + if ( mDialog && type() != QgsProcessingGui::Modeler ) + setDialog( mDialog ); // setup connections to panel - dialog was previously set before the widget was created + + return mExtentWidget; + } + } + return nullptr; +} + +void QgsProcessingExtentWidgetWrapper::setWidgetContext( const QgsProcessingParameterWidgetContext &context ) +{ + QgsAbstractProcessingParameterWidgetWrapper::setWidgetContext( context ); + if ( mExtentWidget && context.mapCanvas() && type() != QgsProcessingGui::Modeler ) + mExtentWidget->setMapCanvas( context.mapCanvas() ); +} + +void QgsProcessingExtentWidgetWrapper::setDialog( QDialog *dialog ) +{ + mDialog = dialog; + if ( mExtentWidget && mDialog && type() != QgsProcessingGui::Modeler ) + { + connect( mExtentWidget, &QgsExtentWidget::toggleDialogVisibility, mDialog, [ = ]( bool visible ) + { + if ( !visible ) + mDialog->showMinimized(); + else + { + mDialog->showNormal(); + mDialog->raise(); + mDialog->activateWindow(); + } + } ); + } + QgsAbstractProcessingParameterWidgetWrapper::setDialog( dialog ); +} + +void QgsProcessingExtentWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext &context ) +{ + if ( mExtentWidget ) + { + if ( !value.isValid() || ( value.type() == QVariant::String && value.toString().isEmpty() ) ) + mExtentWidget->clear(); + else + { + QgsRectangle r = QgsProcessingParameters::parameterAsExtent( parameterDefinition(), value, context ); + QgsCoordinateReferenceSystem crs = QgsProcessingParameters::parameterAsPointCrs( parameterDefinition(), value, context ); + mExtentWidget->setCurrentExtent( r, crs ); + mExtentWidget->setOutputExtentFromCurrent(); + } + } +} + +QVariant QgsProcessingExtentWidgetWrapper::widgetValue() const +{ + if ( mExtentWidget ) + { + const QString val = mExtentWidget->isValid() ? QStringLiteral( "%1,%2,%3,%4%5" ).arg( + QString::number( mExtentWidget->outputExtent().xMinimum(), 'f', 9 ), + QString::number( mExtentWidget->outputExtent().xMaximum(), 'f', 9 ), + QString::number( mExtentWidget->outputExtent().yMinimum(), 'f', 9 ), + QString::number( mExtentWidget->outputExtent().yMaximum(), 'f', 9 ), + mExtentWidget->outputCrs().isValid() ? QStringLiteral( " [%1]" ).arg( mExtentWidget->outputCrs().authid() ) : QString() + ) : QString(); + + return val.isEmpty() ? QVariant() : QVariant( val ); + } + else + return QVariant(); +} + +QStringList QgsProcessingExtentWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterExtent::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterRasterLayer::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterMeshLayer::typeName(); + +} + +QStringList QgsProcessingExtentWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName() + << QgsProcessingOutputRasterLayer::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMapLayer::typeName(); +} + +QString QgsProcessingExtentWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "string of the format 'x min,x max,y min,y max' or a geometry value (bounding box is used)" ); +} + +QString QgsProcessingExtentWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterExtent::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingExtentWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingExtentWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingExtentWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingExtentParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + + + +// +// QgsProcessingMapLayerWidgetWrapper +// + +QgsProcessingMapLayerParameterDefinitionWidget::QgsProcessingMapLayerParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Layer type" ) ) ); + mLayerTypeComboBox = new QgsCheckableComboBox(); + mLayerTypeComboBox->addItem( tr( "Any Map Layer" ), QgsProcessing::TypeMapLayer ); + mLayerTypeComboBox->addItem( tr( "Vector (Point)" ), QgsProcessing::TypeVectorPoint ); + mLayerTypeComboBox->addItem( tr( "Vector (Line)" ), QgsProcessing::TypeVectorLine ); + mLayerTypeComboBox->addItem( tr( "Vector (Polygon)" ), QgsProcessing::TypeVectorPolygon ); + mLayerTypeComboBox->addItem( tr( "Vector (Any Geometry Type)" ), QgsProcessing::TypeVectorAnyGeometry ); + mLayerTypeComboBox->addItem( tr( "Raster" ), QgsProcessing::TypeRaster ); + mLayerTypeComboBox->addItem( tr( "Mesh" ), QgsProcessing::TypeMesh ); + + if ( const QgsProcessingParameterMapLayer *layerParam = dynamic_cast( definition ) ) + { + for ( int i : layerParam->dataTypes() ) + { + mLayerTypeComboBox->setItemCheckState( mLayerTypeComboBox->findData( i ), Qt::Checked ); + } + } + + vlayout->addWidget( mLayerTypeComboBox ); + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingMapLayerParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QList< int > dataTypes; + for ( const QVariant &v : mLayerTypeComboBox->checkedItemsData() ) + dataTypes << v.toInt(); + + auto param = qgis::make_unique< QgsProcessingParameterMapLayer >( name, description ); + param->setDataTypes( dataTypes ); + param->setFlags( flags ); + return param.release(); +} + +QgsProcessingMapLayerWidgetWrapper::QgsProcessingMapLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingMapLayerWidgetWrapper::createWidget() +{ + mComboBox = new QgsProcessingMapLayerComboBox( parameterDefinition(), type() ); + + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + break; + case QgsProcessingGui::Modeler: + mComboBox->setEditable( true ); + break; + } + + mComboBox->setToolTip( parameterDefinition()->toolTip() ); + + connect( mComboBox, &QgsProcessingMapLayerComboBox::valueChanged, this, [ = ]() + { + if ( mBlockSignals ) + return; + + emit widgetValueHasChanged( this ); + } ); + + setWidgetContext( widgetContext() ); + return mComboBox; +} + +void QgsProcessingMapLayerWidgetWrapper::setWidgetContext( const QgsProcessingParameterWidgetContext &context ) +{ + QgsAbstractProcessingParameterWidgetWrapper::setWidgetContext( context ); + if ( mComboBox ) + { + mComboBox->setWidgetContext( context ); + + if ( !( parameterDefinition()->flags() & QgsProcessingParameterDefinition::FlagOptional ) ) + { + // non optional parameter -- if no default value set, default to active layer + if ( !parameterDefinition()->defaultValue().isValid() ) + mComboBox->setLayer( context.activeLayer() ); + } + } +} + +void QgsProcessingMapLayerWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext &context ) +{ + mComboBox->setValue( value, context ); +} + +QVariant QgsProcessingMapLayerWidgetWrapper::widgetValue() const +{ + return mComboBox->value(); +} + +QStringList QgsProcessingMapLayerWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterRasterLayer::typeName() + << QgsProcessingParameterMeshLayer::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingMapLayerWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName() + << QgsProcessingOutputRasterLayer::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMapLayer::typeName() + << QgsProcessingOutputFile::typeName(); +} + +QString QgsProcessingMapLayerWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to a map layer" ); +} + +QString QgsProcessingMapLayerWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterMapLayer::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingMapLayerWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingMapLayerWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingMapLayerWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingMapLayerParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + + +// +// QgsProcessingRasterLayerWidgetWrapper +// + +QgsProcessingRasterLayerWidgetWrapper::QgsProcessingRasterLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingMapLayerWidgetWrapper( parameter, type, parent ) +{ + +} + +QStringList QgsProcessingRasterLayerWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterRasterLayer::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingRasterLayerWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName() + << QgsProcessingOutputRasterLayer::typeName() + << QgsProcessingOutputMapLayer::typeName() + << QgsProcessingOutputFile::typeName(); +} + +QString QgsProcessingRasterLayerWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to a raster layer" ); +} + +QString QgsProcessingRasterLayerWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterRasterLayer::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingRasterLayerWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingRasterLayerWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingRasterLayerWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + Q_UNUSED( context ); + Q_UNUSED( widgetContext ); + Q_UNUSED( definition ); + Q_UNUSED( algorithm ); + + return nullptr; +} + + +// +// QgsProcessingVectorLayerWidgetWrapper +// + +QgsProcessingVectorLayerParameterDefinitionWidget::QgsProcessingVectorLayerParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Geometry type" ) ) ); + mGeometryTypeComboBox = new QgsCheckableComboBox(); + mGeometryTypeComboBox->addItem( tr( "Geometry Not Required" ), QgsProcessing::TypeVector ); + mGeometryTypeComboBox->addItem( tr( "Point" ), QgsProcessing::TypeVectorPoint ); + mGeometryTypeComboBox->addItem( tr( "Line" ), QgsProcessing::TypeVectorLine ); + mGeometryTypeComboBox->addItem( tr( "Polygon" ), QgsProcessing::TypeVectorPolygon ); + mGeometryTypeComboBox->addItem( tr( "Any Geometry Type" ), QgsProcessing::TypeVectorAnyGeometry ); + + if ( const QgsProcessingParameterVectorLayer *vectorParam = dynamic_cast( definition ) ) + { + for ( int i : vectorParam->dataTypes() ) + { + mGeometryTypeComboBox->setItemCheckState( mGeometryTypeComboBox->findData( i ), Qt::Checked ); + } + } + + vlayout->addWidget( mGeometryTypeComboBox ); + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingVectorLayerParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QList< int > dataTypes; + for ( const QVariant &v : mGeometryTypeComboBox->checkedItemsData() ) + dataTypes << v.toInt(); + + auto param = qgis::make_unique< QgsProcessingParameterVectorLayer >( name, description, dataTypes ); + param->setFlags( flags ); + return param.release(); +} + + +QgsProcessingVectorLayerWidgetWrapper::QgsProcessingVectorLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingMapLayerWidgetWrapper( parameter, type, parent ) +{ + +} + +QStringList QgsProcessingVectorLayerWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingVectorLayerWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMapLayer::typeName() + << QgsProcessingOutputFile::typeName(); +} + +QString QgsProcessingVectorLayerWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to a vector layer" ); +} + +QList QgsProcessingVectorLayerWidgetWrapper::compatibleDataTypes( const QgsProcessingParameterDefinition *parameter ) const +{ + if ( const QgsProcessingParameterVectorLayer *param = dynamic_cast< const QgsProcessingParameterVectorLayer *>( parameter ) ) + return param->dataTypes(); + else + return QList< int >(); +} + +QString QgsProcessingVectorLayerWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterVectorLayer::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingVectorLayerWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingVectorLayerWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingVectorLayerWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingVectorLayerParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + + + +// +// QgsProcessingFeatureSourceLayerWidgetWrapper +// + +QgsProcessingFeatureSourceParameterDefinitionWidget::QgsProcessingFeatureSourceParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Geometry type" ) ) ); + mGeometryTypeComboBox = new QgsCheckableComboBox(); + mGeometryTypeComboBox->addItem( tr( "Geometry Not Required" ), QgsProcessing::TypeVector ); + mGeometryTypeComboBox->addItem( tr( "Point" ), QgsProcessing::TypeVectorPoint ); + mGeometryTypeComboBox->addItem( tr( "Line" ), QgsProcessing::TypeVectorLine ); + mGeometryTypeComboBox->addItem( tr( "Polygon" ), QgsProcessing::TypeVectorPolygon ); + mGeometryTypeComboBox->addItem( tr( "Any Geometry Type" ), QgsProcessing::TypeVectorAnyGeometry ); + + if ( const QgsProcessingParameterFeatureSource *sourceParam = dynamic_cast( definition ) ) + { + for ( int i : sourceParam->dataTypes() ) + { + mGeometryTypeComboBox->setItemCheckState( mGeometryTypeComboBox->findData( i ), Qt::Checked ); + } + } + + vlayout->addWidget( mGeometryTypeComboBox ); + + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingFeatureSourceParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + QList< int > dataTypes; + for ( const QVariant &v : mGeometryTypeComboBox->checkedItemsData() ) + dataTypes << v.toInt(); + + auto param = qgis::make_unique< QgsProcessingParameterFeatureSource >( name, description, dataTypes ); + param->setFlags( flags ); + return param.release(); +} + +QgsProcessingFeatureSourceWidgetWrapper::QgsProcessingFeatureSourceWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingMapLayerWidgetWrapper( parameter, type, parent ) +{ + +} + +QStringList QgsProcessingFeatureSourceWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingFeatureSourceWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMapLayer::typeName() + << QgsProcessingOutputFile::typeName(); +} + +QString QgsProcessingFeatureSourceWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to a vector layer" ); +} + +QList QgsProcessingFeatureSourceWidgetWrapper::compatibleDataTypes( const QgsProcessingParameterDefinition *parameter ) const +{ + if ( const QgsProcessingParameterFeatureSource *param = dynamic_cast< const QgsProcessingParameterFeatureSource *>( parameter ) ) + return param->dataTypes(); + else + return QList< int >(); +} + +QString QgsProcessingFeatureSourceWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterFeatureSource::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingFeatureSourceWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingFeatureSourceWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingFeatureSourceWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingFeatureSourceParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + +// +// QgsProcessingMeshLayerWidgetWrapper +// + +QgsProcessingMeshLayerWidgetWrapper::QgsProcessingMeshLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingMapLayerWidgetWrapper( parameter, type, parent ) +{ + +} + +QStringList QgsProcessingMeshLayerWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterMeshLayer::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingMeshLayerWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName() + // TODO << QgsProcessingOutputMeshLayer::typeName() + << QgsProcessingOutputMapLayer::typeName() + << QgsProcessingOutputFile::typeName(); +} + +QString QgsProcessingMeshLayerWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to a mesh layer" ); +} + +QString QgsProcessingMeshLayerWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterMeshLayer::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingMeshLayerWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingMeshLayerWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingMeshLayerWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + Q_UNUSED( context ); + Q_UNUSED( widgetContext ); + Q_UNUSED( definition ); + Q_UNUSED( algorithm ); + + return nullptr; +} + + + +// +// QgsProcessingRasterBandPanelWidget +// + +QgsProcessingRasterBandPanelWidget::QgsProcessingRasterBandPanelWidget( QWidget *parent, const QgsProcessingParameterBand *param ) + : QWidget( parent ) + , mParam( param ) +{ + QHBoxLayout *hl = new QHBoxLayout(); + hl->setMargin( 0 ); + hl->setContentsMargins( 0, 0, 0, 0 ); + + mLineEdit = new QLineEdit(); + mLineEdit->setEnabled( false ); + hl->addWidget( mLineEdit, 1 ); + + mToolButton = new QToolButton(); + mToolButton->setText( QString( QChar( 0x2026 ) ) ); + hl->addWidget( mToolButton ); + + setLayout( hl ); + + if ( mParam ) + { + mLineEdit->setText( tr( "%1 bands selected" ).arg( 0 ) ); + } + + connect( mToolButton, &QToolButton::clicked, this, &QgsProcessingRasterBandPanelWidget::showDialog ); +} + +void QgsProcessingRasterBandPanelWidget::setBands( const QList< int > &bands ) +{ + mBands = bands; +} + +void QgsProcessingRasterBandPanelWidget::setBandNames( const QHash &names ) +{ + mBandNames = names; +} + +void QgsProcessingRasterBandPanelWidget::setValue( const QVariant &value ) +{ + if ( value.isValid() ) + mValue = value.type() == QVariant::List ? value.toList() : QVariantList() << value; + else + mValue.clear(); + + updateSummaryText(); + emit changed(); +} + +void QgsProcessingRasterBandPanelWidget::showDialog() +{ + QVariantList availableOptions; + QStringList fieldNames; + availableOptions.reserve( mBands.size() ); + for ( int band : qgis::as_const( mBands ) ) + { + availableOptions << band; + } + + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) + { + QgsProcessingMultipleSelectionPanelWidget *widget = new QgsProcessingMultipleSelectionPanelWidget( availableOptions, mValue ); + widget->setPanelTitle( mParam->description() ); + + widget->setValueFormatter( [this]( const QVariant & v ) -> QString + { + int band = v.toInt(); + return mBandNames.contains( band ) ? mBandNames.value( band ) : v.toString(); + } ); + + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [ = ]() + { + setValue( widget->selectedOptions() ); + } ); + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel ); + panel->openPanel( widget ); + } + else + { + QgsProcessingMultipleSelectionDialog dlg( availableOptions, mValue, this, nullptr ); + + dlg.setValueFormatter( [this]( const QVariant & v ) -> QString + { + int band = v.toInt(); + return mBandNames.contains( band ) ? mBandNames.value( band ) : v.toString(); + } ); + if ( dlg.exec() ) + { + setValue( dlg.selectedOptions() ); + } + } +} + +void QgsProcessingRasterBandPanelWidget::updateSummaryText() +{ + if ( mParam ) + mLineEdit->setText( tr( "%1 bands selected" ).arg( mValue.count() ) ); +} + + + +// +// QgsProcessingBandWidgetWrapper +// + +QgsProcessingBandParameterDefinitionWidget::QgsProcessingBandParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Default value" ) ) ); + + mDefaultLineEdit = new QLineEdit(); + mDefaultLineEdit->setToolTip( tr( "Band number (separate bands with ; for multiple band parameters)" ) ); + if ( const QgsProcessingParameterBand *bandParam = dynamic_cast( definition ) ) + { + const QList< int > bands = QgsProcessingParameters::parameterAsInts( bandParam, bandParam->defaultValue(), context ); + QStringList defVal; + for ( int b : bands ) + { + defVal << QString::number( b ); + } + + mDefaultLineEdit->setText( defVal.join( ';' ) ); + } + vlayout->addWidget( mDefaultLineEdit ); + + vlayout->addWidget( new QLabel( tr( "Parent layer" ) ) ); + mParentLayerComboBox = new QComboBox(); + + QString initialParent; + if ( const QgsProcessingParameterBand *bandParam = dynamic_cast( definition ) ) + initialParent = bandParam->parentLayerParameterName(); + + if ( widgetContext.model() ) + { + // populate combo box with other model input choices + const QMap components = widgetContext.model()->parameterComponents(); + for ( auto it = components.constBegin(); it != components.constEnd(); ++it ) + { + if ( const QgsProcessingParameterRasterLayer *definition = dynamic_cast< const QgsProcessingParameterRasterLayer * >( widgetContext.model()->parameterDefinition( it.value().parameterName() ) ) ) + { + mParentLayerComboBox-> addItem( definition->description(), definition->name() ); + if ( !initialParent.isEmpty() && initialParent == definition->name() ) + { + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + } + } + } + + if ( mParentLayerComboBox->count() == 0 && !initialParent.isEmpty() ) + { + // if no parent candidates found, we just add the existing one as a placeholder + mParentLayerComboBox->addItem( initialParent, initialParent ); + mParentLayerComboBox->setCurrentIndex( mParentLayerComboBox->count() - 1 ); + } + + vlayout->addWidget( mParentLayerComboBox ); + + mAllowMultipleCheckBox = new QCheckBox( tr( "Allow multiple" ) ); + if ( const QgsProcessingParameterBand *bandParam = dynamic_cast( definition ) ) + mAllowMultipleCheckBox->setChecked( bandParam->allowMultiple() ); + + vlayout->addWidget( mAllowMultipleCheckBox ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingBandParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterBand >( name, description, mDefaultLineEdit->text().split( ';' ), mParentLayerComboBox->currentData().toString(), false, mAllowMultipleCheckBox->isChecked() ); + param->setFlags( flags ); + return param.release(); +} + +QgsProcessingBandWidgetWrapper::QgsProcessingBandWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingBandWidgetWrapper::createWidget() +{ + const QgsProcessingParameterBand *bandParam = dynamic_cast< const QgsProcessingParameterBand *>( parameterDefinition() ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + if ( bandParam->allowMultiple() ) + { + mPanel = new QgsProcessingRasterBandPanelWidget( nullptr, bandParam ); + mPanel->setToolTip( parameterDefinition()->toolTip() ); + connect( mPanel, &QgsProcessingRasterBandPanelWidget::changed, this, [ = ] + { + emit widgetValueHasChanged( this ); + } ); + return mPanel; + } + else + { + mComboBox = new QgsRasterBandComboBox(); + mComboBox->setShowNotSetOption( bandParam->flags() & QgsProcessingParameterDefinition::FlagOptional ); + + mComboBox->setToolTip( parameterDefinition()->toolTip() ); + connect( mComboBox, &QgsRasterBandComboBox::bandChanged, this, [ = ]( int ) + { + emit widgetValueHasChanged( this ); + } ); + return mComboBox; + } + } + + case QgsProcessingGui::Modeler: + { + mLineEdit = new QLineEdit(); + mLineEdit->setToolTip( QObject::tr( "Band number (separate bands with ; for multiple band parameters)" ) ); + connect( mLineEdit, &QLineEdit::textChanged, this, [ = ] + { + emit widgetValueHasChanged( this ); + } ); + return mLineEdit; + } + + } + return nullptr; +} + +void QgsProcessingBandWidgetWrapper::postInitialize( const QList &wrappers ) +{ + QgsAbstractProcessingParameterWidgetWrapper::postInitialize( wrappers ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + for ( const QgsAbstractProcessingParameterWidgetWrapper *wrapper : wrappers ) + { + if ( wrapper->parameterDefinition()->name() == static_cast< const QgsProcessingParameterBand * >( parameterDefinition() )->parentLayerParameterName() ) + { + setParentLayerWrapperValue( wrapper ); + connect( wrapper, &QgsAbstractProcessingParameterWidgetWrapper::widgetValueHasChanged, this, [ = ] + { + setParentLayerWrapperValue( wrapper ); + } ); + break; + } + } + break; + } + + case QgsProcessingGui::Modeler: + break; + } +} + +void QgsProcessingBandWidgetWrapper::setParentLayerWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ) +{ + // evaluate value to layer + QgsProcessingContext *context = nullptr; + std::unique_ptr< QgsProcessingContext > tmpContext; + if ( mProcessingContextGenerator ) + context = mProcessingContextGenerator->processingContext(); + + if ( !context ) + { + tmpContext = qgis::make_unique< QgsProcessingContext >(); + context = tmpContext.get(); + } + + QVariant value = parentWrapper->parameterValue(); + + QgsRasterLayer *layer = QgsProcessingParameters::parameterAsRasterLayer( parentWrapper->parameterDefinition(), value, *context ); + if ( layer && layer->isValid() ) + { + // need to grab ownership of layer if required - otherwise layer may be deleted when context + // goes out of scope + std::unique_ptr< QgsMapLayer > ownedLayer( context->takeResultLayer( layer->id() ) ); + if ( ownedLayer && ownedLayer->type() == QgsMapLayerType::RasterLayer ) + { + mParentLayer.reset( qobject_cast< QgsRasterLayer * >( ownedLayer.release() ) ); + layer = mParentLayer.get(); + } + else + { + // don't need ownership of this layer - it wasn't owned by context (so e.g. is owned by the project) + } + + if ( mComboBox ) + mComboBox->setLayer( layer ); + else if ( mPanel ) + { + QgsRasterDataProvider *provider = layer->dataProvider(); + if ( provider && layer->isValid() ) + { + //fill available bands + int nBands = provider->bandCount(); + QList< int > bands; + QHash< int, QString > bandNames; + for ( int i = 1; i <= nBands; ++i ) + { + bandNames.insert( i, QgsRasterBandComboBox::displayBandName( provider, i ) ); + bands << i; + } + mPanel->setBands( bands ); + mPanel->setBandNames( bandNames ); + } + } + } + else + { + if ( mComboBox ) + mComboBox->setLayer( nullptr ); + else if ( mPanel ) + mPanel->setBands( QList< int >() ); + + if ( value.isValid() && widgetContext().messageBar() ) + { + widgetContext().messageBar()->clearWidgets(); + widgetContext().messageBar()->pushMessage( QString(), QObject::tr( "Could not load selected layer/table. Dependent bands could not be populated" ), + Qgis::Warning, 5 ); + } + } + + if ( parameterDefinition()->defaultValue().isValid() ) + setWidgetValue( parameterDefinition()->defaultValue(), *context ); +} + +void QgsProcessingBandWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext &context ) +{ + if ( mComboBox ) + { + if ( !value.isValid() ) + mComboBox->setBand( -1 ); + else + { + const int v = QgsProcessingParameters::parameterAsInt( parameterDefinition(), value, context ); + mComboBox->setBand( v ); + } + } + else if ( mPanel ) + { + QVariantList opts; + if ( value.isValid() ) + { + const QList< int > v = QgsProcessingParameters::parameterAsInts( parameterDefinition(), value, context ); + opts.reserve( v.size() ); + for ( int i : v ) + opts << i; + } + if ( mPanel ) + mPanel->setValue( value.isValid() ? opts : QVariant() ); + } + else if ( mLineEdit ) + { + const QgsProcessingParameterBand *bandParam = static_cast< const QgsProcessingParameterBand * >( parameterDefinition() ); + if ( bandParam->allowMultiple() ) + { + const QList< int > v = QgsProcessingParameters::parameterAsInts( parameterDefinition(), value, context ); + QStringList opts; + opts.reserve( v.size() ); + for ( int i : v ) + opts << QString::number( i ); + mLineEdit->setText( value.isValid() && !opts.empty() ? opts.join( ';' ) : QString() ); + } + else + { + if ( value.isValid() ) + mLineEdit->setText( QString::number( QgsProcessingParameters::parameterAsInt( parameterDefinition(), value, context ) ) ); + else + mLineEdit->clear(); + } + } +} + +QVariant QgsProcessingBandWidgetWrapper::widgetValue() const +{ + if ( mComboBox ) + return mComboBox->currentBand() == -1 ? QVariant() : mComboBox->currentBand(); + else if ( mPanel ) + return !mPanel->value().toList().isEmpty() ? mPanel->value() : QVariant(); + else if ( mLineEdit ) + { + const QgsProcessingParameterBand *bandParam = static_cast< const QgsProcessingParameterBand * >( parameterDefinition() ); + if ( bandParam->allowMultiple() ) + { + const QStringList parts = mLineEdit->text().split( ';', QString::SkipEmptyParts ); + QVariantList res; + res.reserve( parts.count() ); + for ( const QString &s : parts ) + { + bool ok = false; + int band = s.toInt( &ok ); + if ( ok ) + res << band; + } + return res.isEmpty() ? QVariant() : res; + } + else + { + return mLineEdit->text().isEmpty() ? QVariant() : mLineEdit->text(); + } + } + else + return QVariant(); +} + +QStringList QgsProcessingBandWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterBand::typeName() + << QgsProcessingParameterNumber::typeName(); +} + +QStringList QgsProcessingBandWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputNumber::typeName(); +} + +QString QgsProcessingBandWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "selected band numbers as an array of numbers, or semicolon separated string of options (e.g. '1;3')" ); +} + +QString QgsProcessingBandWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterBand::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingBandWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingBandWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingBandWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingBandParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + + + +// +// QgsProcessingMultipleLayerPanelWidget +// + +QgsProcessingMultipleLayerPanelWidget::QgsProcessingMultipleLayerPanelWidget( QWidget *parent, const QgsProcessingParameterMultipleLayers *param ) + : QWidget( parent ) + , mParam( param ) +{ + QHBoxLayout *hl = new QHBoxLayout(); + hl->setMargin( 0 ); + hl->setContentsMargins( 0, 0, 0, 0 ); + + mLineEdit = new QLineEdit(); + mLineEdit->setEnabled( false ); + hl->addWidget( mLineEdit, 1 ); + + mToolButton = new QToolButton(); + mToolButton->setText( QString( QChar( 0x2026 ) ) ); + hl->addWidget( mToolButton ); + + setLayout( hl ); + + if ( mParam ) + { + mLineEdit->setText( tr( "%1 inputs selected" ).arg( 0 ) ); + } + + connect( mToolButton, &QToolButton::clicked, this, &QgsProcessingMultipleLayerPanelWidget::showDialog ); +} + +void QgsProcessingMultipleLayerPanelWidget::setValue( const QVariant &value ) +{ + if ( value.isValid() ) + mValue = value.type() == QVariant::List ? value.toList() : QVariantList() << value; + else + mValue.clear(); + + updateSummaryText(); + emit changed(); +} + +void QgsProcessingMultipleLayerPanelWidget::setProject( QgsProject *project ) +{ + mProject = project; +} + +void QgsProcessingMultipleLayerPanelWidget::setModel( QgsProcessingModelAlgorithm *model, const QString &modelChildAlgorithmID ) +{ + mModel = model; + if ( !model ) + return; + + switch ( mParam->layerType() ) + { + case QgsProcessing::TypeFile: + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFile::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() ); + break; + + case QgsProcessing::TypeRaster: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterRasterLayer::typeName() + << QgsProcessingParameterMultipleLayers::typeName() + << QgsProcessingParameterFile::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputRasterLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName() ); + break; + } + + case QgsProcessing::TypeMesh: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterMeshLayer::typeName() + << QgsProcessingParameterMultipleLayers::typeName() + << QgsProcessingParameterFile::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputMultipleLayers::typeName() ); + break; + } + + case QgsProcessing::TypeVector: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterFile::typeName() + << QgsProcessingParameterMultipleLayers::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName(), + QList< int >() << QgsProcessing::TypeVector ); + break; + } + + case QgsProcessing::TypeVectorAnyGeometry: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterFile::typeName() + << QgsProcessingParameterMultipleLayers::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName() ); + break; + } + + case QgsProcessing::TypeVectorPoint: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterFile::typeName() + << QgsProcessingParameterMultipleLayers::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName(), + QList< int >() << QgsProcessing::TypeVectorAnyGeometry << QgsProcessing::TypeVectorPoint ); + break; + } + + case QgsProcessing::TypeVectorLine: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterFile::typeName() + << QgsProcessingParameterMultipleLayers::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName(), + QList< int >() << QgsProcessing::TypeVectorAnyGeometry << QgsProcessing::TypeVectorLine ); + break; + } + + case QgsProcessing::TypeVectorPolygon: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterFile::typeName() + << QgsProcessingParameterMultipleLayers::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName(), + QList< int >() << QgsProcessing::TypeVectorAnyGeometry << QgsProcessing::TypeVectorPolygon ); + break; + } + + case QgsProcessing::TypeMapLayer: + { + mModelSources = model->availableSourcesForChild( modelChildAlgorithmID, QStringList() << QgsProcessingParameterFeatureSource::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterRasterLayer::typeName() + << QgsProcessingParameterMeshLayer::typeName() + << QgsProcessingParameterFile::typeName() + << QgsProcessingParameterMultipleLayers::typeName(), + QStringList() << QgsProcessingOutputFile::typeName() + << QgsProcessingOutputVectorLayer::typeName() + << QgsProcessingOutputRasterLayer::typeName() + // << QgsProcessingOutputMeshLayer::typeName() + << QgsProcessingOutputMultipleLayers::typeName() ); + break; + } + } +} + +void QgsProcessingMultipleLayerPanelWidget::showDialog() +{ + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) + { + QgsProcessingMultipleInputPanelWidget *widget = new QgsProcessingMultipleInputPanelWidget( mParam, mValue, mModelSources, mModel ); + widget->setPanelTitle( mParam->description() ); + widget->setProject( mProject ); + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::selectionChanged, this, [ = ]() + { + setValue( widget->selectedOptions() ); + } ); + connect( widget, &QgsProcessingMultipleSelectionPanelWidget::acceptClicked, widget, &QgsPanelWidget::acceptPanel ); + panel->openPanel( widget ); + } + else + { + QgsProcessingMultipleInputDialog dlg( mParam, mValue, mModelSources, mModel, this, nullptr ); + dlg.setProject( mProject ); + if ( dlg.exec() ) + { + setValue( dlg.selectedOptions() ); + } + } +} + +void QgsProcessingMultipleLayerPanelWidget::updateSummaryText() +{ + if ( mParam ) + mLineEdit->setText( tr( "%1 inputs selected" ).arg( mValue.count() ) ); +} + +// +// QgsProcessingMultipleLayerWidgetWrapper +// + +QgsProcessingMultipleLayerParameterDefinitionWidget::QgsProcessingMultipleLayerParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm, QWidget *parent ) + : QgsProcessingAbstractParameterDefinitionWidget( context, widgetContext, definition, algorithm, parent ) +{ + QVBoxLayout *vlayout = new QVBoxLayout(); + vlayout->setMargin( 0 ); + vlayout->setContentsMargins( 0, 0, 0, 0 ); + + vlayout->addWidget( new QLabel( tr( "Allowed layer type" ) ) ); + mLayerTypeComboBox = new QComboBox(); + mLayerTypeComboBox->addItem( tr( "Any Map Layer" ), QgsProcessing::TypeMapLayer ); + mLayerTypeComboBox->addItem( tr( "Vector (No Geometry Required)" ), QgsProcessing::TypeVector ); + mLayerTypeComboBox->addItem( tr( "Vector (Point)" ), QgsProcessing::TypeVectorPoint ); + mLayerTypeComboBox->addItem( tr( "Vector (Line)" ), QgsProcessing::TypeVectorLine ); + mLayerTypeComboBox->addItem( tr( "Vector (Polygon)" ), QgsProcessing::TypeVectorPolygon ); + mLayerTypeComboBox->addItem( tr( "Any Geometry Type" ), QgsProcessing::TypeVectorAnyGeometry ); + mLayerTypeComboBox->addItem( tr( "Raster" ), QgsProcessing::TypeRaster ); + mLayerTypeComboBox->addItem( tr( "File" ), QgsProcessing::TypeFile ); + mLayerTypeComboBox->addItem( tr( "Mesh" ), QgsProcessing::TypeMesh ); + if ( const QgsProcessingParameterMultipleLayers *layersParam = dynamic_cast( definition ) ) + mLayerTypeComboBox->setCurrentIndex( mLayerTypeComboBox->findData( layersParam->layerType() ) ); + + vlayout->addWidget( mLayerTypeComboBox ); + setLayout( vlayout ); +} + +QgsProcessingParameterDefinition *QgsProcessingMultipleLayerParameterDefinitionWidget::createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const +{ + auto param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( name, description, static_cast< QgsProcessing::SourceType >( mLayerTypeComboBox->currentData().toInt() ) ); + param->setFlags( flags ); + return param.release(); +} + +QgsProcessingMultipleLayerWidgetWrapper::QgsProcessingMultipleLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingMultipleLayerWidgetWrapper::createWidget() +{ + const QgsProcessingParameterMultipleLayers *layerParam = dynamic_cast< const QgsProcessingParameterMultipleLayers *>( parameterDefinition() ); + + mPanel = new QgsProcessingMultipleLayerPanelWidget( nullptr, layerParam ); + mPanel->setToolTip( parameterDefinition()->toolTip() ); + mPanel->setProject( widgetContext().project() ); + mPanel->setModel( widgetContext().model(), widgetContext().modelChildAlgorithmId() ); + connect( mPanel, &QgsProcessingMultipleLayerPanelWidget::changed, this, [ = ] + { + emit widgetValueHasChanged( this ); + } ); + return mPanel; +} + +void QgsProcessingMultipleLayerWidgetWrapper::setWidgetContext( const QgsProcessingParameterWidgetContext &context ) +{ + QgsAbstractProcessingParameterWidgetWrapper::setWidgetContext( context ); + if ( mPanel ) + { + mPanel->setProject( context.project() ); + mPanel->setModel( widgetContext().model(), widgetContext().modelChildAlgorithmId() ); + } +} + +void QgsProcessingMultipleLayerWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext &context ) +{ + if ( mPanel ) + { + QVariantList opts; + if ( value.isValid() ) + { + const QList< QgsMapLayer * > v = QgsProcessingParameters::parameterAsLayerList( parameterDefinition(), value, context ); + opts.reserve( v.size() ); + for ( const QgsMapLayer *l : v ) + opts << l->source(); + } + + for ( const QVariant &v : value.toList() ) + { + if ( v.canConvert< QgsProcessingModelChildParameterSource >() ) + { + const QgsProcessingModelChildParameterSource source = v.value< QgsProcessingModelChildParameterSource >(); + opts << QVariant::fromValue( source ); + } + } + + if ( mPanel ) + mPanel->setValue( value.isValid() ? opts : QVariant() ); + } +} + +QVariant QgsProcessingMultipleLayerWidgetWrapper::widgetValue() const +{ + if ( mPanel ) + return !mPanel->value().toList().isEmpty() ? mPanel->value() : QVariant(); + else + return QVariant(); +} + +QStringList QgsProcessingMultipleLayerWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterBand::typeName() + << QgsProcessingParameterNumber::typeName(); +} + +QStringList QgsProcessingMultipleLayerWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputNumber::typeName(); +} + +QString QgsProcessingMultipleLayerWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "an array of layer paths, or semicolon separated string of layer paths" ); +} + +QString QgsProcessingMultipleLayerWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterMultipleLayers::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingMultipleLayerWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingMultipleLayerWidgetWrapper( parameter, type ); +} + +QgsProcessingAbstractParameterDefinitionWidget *QgsProcessingMultipleLayerWidgetWrapper::createParameterDefinitionWidget( QgsProcessingContext &context, const QgsProcessingParameterWidgetContext &widgetContext, const QgsProcessingParameterDefinition *definition, const QgsProcessingAlgorithm *algorithm ) +{ + return new QgsProcessingMultipleLayerParameterDefinitionWidget( context, widgetContext, definition, algorithm ); +} + + +// +// QgsProcessingOutputWidgetWrapper +// + +QgsProcessingOutputWidgetWrapper::QgsProcessingOutputWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsAbstractProcessingParameterWidgetWrapper( parameter, type, parent ) +{ + +} + +QWidget *QgsProcessingOutputWidgetWrapper::createWidget() +{ + const QgsProcessingDestinationParameter *destParam = dynamic_cast< const QgsProcessingDestinationParameter * >( parameterDefinition() ); + switch ( type() ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Modeler: + { + mOutputWidget = new QgsProcessingLayerOutputDestinationWidget( destParam, false ); + if ( mProcessingContextGenerator ) + mOutputWidget->setContext( mProcessingContextGenerator->processingContext() ); + if ( mParametersGenerator ) + mOutputWidget->registerProcessingParametersGenerator( mParametersGenerator ); + mOutputWidget->setToolTip( parameterDefinition()->toolTip() ); + + connect( mOutputWidget, &QgsProcessingLayerOutputDestinationWidget::destinationChanged, this, [ = ]() + { + if ( mBlockSignals ) + return; + + emit widgetValueHasChanged( this ); + } ); + + if ( type() == QgsProcessingGui::Standard + && ( destParam->type() == QgsProcessingParameterRasterDestination::typeName() || + destParam->type() == QgsProcessingParameterFeatureSink::typeName() || + destParam->type() == QgsProcessingParameterVectorDestination::typeName() ) ) + mOutputWidget->addOpenAfterRunningOption(); + + return mOutputWidget; + } + case QgsProcessingGui::Batch: + break; + } + + return nullptr; +} + + +void QgsProcessingOutputWidgetWrapper::setWidgetValue( const QVariant &value, QgsProcessingContext & ) +{ + if ( mOutputWidget ) + mOutputWidget->setValue( value ); +} + +QVariant QgsProcessingOutputWidgetWrapper::widgetValue() const +{ + if ( mOutputWidget ) + return mOutputWidget->value(); + + return QVariant(); +} + +QVariantMap QgsProcessingOutputWidgetWrapper::customProperties() const +{ + QVariantMap res; + if ( mOutputWidget ) + res.insert( QStringLiteral( "OPEN_AFTER_RUNNING" ), mOutputWidget->openAfterRunning() ); + return res; +} + +QStringList QgsProcessingOutputWidgetWrapper::compatibleParameterTypes() const +{ + return QStringList() + << QgsProcessingParameterRasterLayer::typeName() + << QgsProcessingParameterMeshLayer::typeName() + << QgsProcessingParameterVectorLayer::typeName() + << QgsProcessingParameterMapLayer::typeName() + << QgsProcessingParameterString::typeName() + << QgsProcessingParameterExpression::typeName(); +} + +QStringList QgsProcessingOutputWidgetWrapper::compatibleOutputTypes() const +{ + return QStringList() + << QgsProcessingOutputString::typeName(); +} + +// +// QgsProcessingFeatureSinkWidgetWrapper +// + +QgsProcessingFeatureSinkWidgetWrapper::QgsProcessingFeatureSinkWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingOutputWidgetWrapper( parameter, type, parent ) +{ + +} + +QString QgsProcessingFeatureSinkWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterFeatureSink::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingFeatureSinkWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingFeatureSinkWidgetWrapper( parameter, type ); +} + +QString QgsProcessingFeatureSinkWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to layer destination" ); +} + +// +// QgsProcessingFeatureSinkWidgetWrapper +// + +QgsProcessingVectorDestinationWidgetWrapper::QgsProcessingVectorDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingOutputWidgetWrapper( parameter, type, parent ) +{ + +} + +QString QgsProcessingVectorDestinationWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterVectorDestination::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingVectorDestinationWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingVectorDestinationWidgetWrapper( parameter, type ); +} + +QString QgsProcessingVectorDestinationWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to layer destination" ); +} + +// +// QgsProcessingFeatureSinkWidgetWrapper +// + +QgsProcessingRasterDestinationWidgetWrapper::QgsProcessingRasterDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingOutputWidgetWrapper( parameter, type, parent ) +{ + +} + +QString QgsProcessingRasterDestinationWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterRasterDestination::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingRasterDestinationWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingRasterDestinationWidgetWrapper( parameter, type ); +} + +QString QgsProcessingRasterDestinationWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to layer destination" ); +} + +// +// QgsProcessingFileDestinationWidgetWrapper +// + +QgsProcessingFileDestinationWidgetWrapper::QgsProcessingFileDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingOutputWidgetWrapper( parameter, type, parent ) +{ + +} + +QString QgsProcessingFileDestinationWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterFileDestination::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingFileDestinationWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingFileDestinationWidgetWrapper( parameter, type ); +} + +QString QgsProcessingFileDestinationWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to file destination" ); +} + +// +// QgsProcessingFolderDestinationWidgetWrapper +// + +QgsProcessingFolderDestinationWidgetWrapper::QgsProcessingFolderDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type, QWidget *parent ) + : QgsProcessingOutputWidgetWrapper( parameter, type, parent ) +{ + +} + +QString QgsProcessingFolderDestinationWidgetWrapper::parameterType() const +{ + return QgsProcessingParameterFolderDestination::typeName(); +} + +QgsAbstractProcessingParameterWidgetWrapper *QgsProcessingFolderDestinationWidgetWrapper::createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) +{ + return new QgsProcessingFolderDestinationWidgetWrapper( parameter, type ); +} + +QString QgsProcessingFolderDestinationWidgetWrapper::modelerExpressionFormatString() const +{ + return tr( "path to folder destination" ); +} ///@endcond PRIVATE diff --git a/src/gui/processing/qgsprocessingwidgetwrapperimpl.h b/src/gui/processing/qgsprocessingwidgetwrapperimpl.h index 40d43d54d556..1b1a027f6db5 100644 --- a/src/gui/processing/qgsprocessingwidgetwrapperimpl.h +++ b/src/gui/processing/qgsprocessingwidgetwrapperimpl.h @@ -24,6 +24,7 @@ #include "qgsprocessingparameterdefinitionwidget.h" #include "qgsmaptool.h" #include "qgsprocessingcontext.h" +#include "processing/models/qgsprocessingmodelchildparametersource.h" #include @@ -55,6 +56,15 @@ class QgsDateTimeEdit; class QgsDateEdit; class QgsTimeEdit; class QgsProviderConnectionComboBox; +class QgsDatabaseSchemaComboBox; +class QgsDatabaseTableComboBox; +class QgsExtentWidget; +class QgsProcessingEnumModelerWidget; +class QgsProcessingMatrixModelerWidget; +class QgsProcessingMapLayerComboBox; +class QgsRasterBandComboBox; +class QgsProcessingLayerOutputDestinationWidget; +class QgsCheckableComboBox; ///@cond PRIVATE @@ -106,8 +116,6 @@ class GUI_EXPORT QgsProcessingBooleanWidgetWrapper : public QgsAbstractProcessin QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; - private: QCheckBox *mCheckBox = nullptr; @@ -116,6 +124,24 @@ class GUI_EXPORT QgsProcessingBooleanWidgetWrapper : public QgsAbstractProcessin friend class TestProcessingGui; }; + + +class GUI_EXPORT QgsProcessingCrsParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingCrsParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsProjectionSelectionWidget *mCrsSelector = nullptr; +}; + class GUI_EXPORT QgsProcessingCrsWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { Q_OBJECT @@ -128,6 +154,11 @@ class GUI_EXPORT QgsProcessingCrsWidgetWrapper : public QgsAbstractProcessingPar // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -139,7 +170,6 @@ class GUI_EXPORT QgsProcessingCrsWidgetWrapper : public QgsAbstractProcessingPar QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -199,9 +229,6 @@ class GUI_EXPORT QgsProcessingStringWidgetWrapper : public QgsAbstractProcessing QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; - private: QLineEdit *mLineEdit = nullptr; @@ -235,9 +262,6 @@ class GUI_EXPORT QgsProcessingAuthConfigWidgetWrapper : public QgsAbstractProces QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; - private: QgsAuthConfigSelect *mAuthConfigSelect = nullptr; @@ -246,6 +270,27 @@ class GUI_EXPORT QgsProcessingAuthConfigWidgetWrapper : public QgsAbstractProces }; +class GUI_EXPORT QgsProcessingNumberParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingNumberParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mTypeComboBox = nullptr; + QLineEdit *mMinLineEdit = nullptr; + QLineEdit *mMaxLineEdit = nullptr; + QLineEdit *mDefaultLineEdit = nullptr; + +}; + + class GUI_EXPORT QgsProcessingNumericWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { Q_OBJECT @@ -258,6 +303,11 @@ class GUI_EXPORT QgsProcessingNumericWidgetWrapper : public QgsAbstractProcessin // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -271,8 +321,6 @@ class GUI_EXPORT QgsProcessingNumericWidgetWrapper : public QgsAbstractProcessin QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; - protected: QgsSpinBox *mSpinBox = nullptr; @@ -288,6 +336,26 @@ class GUI_EXPORT QgsProcessingNumericWidgetWrapper : public QgsAbstractProcessin }; +class GUI_EXPORT QgsProcessingDistanceParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingDistanceParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mParentLayerComboBox = nullptr; + QLineEdit *mMinLineEdit = nullptr; + QLineEdit *mMaxLineEdit = nullptr; + QLineEdit *mDefaultLineEdit = nullptr; + +}; + class GUI_EXPORT QgsProcessingDistanceWidgetWrapper : public QgsProcessingNumericWidgetWrapper { Q_OBJECT @@ -300,6 +368,11 @@ class GUI_EXPORT QgsProcessingDistanceWidgetWrapper : public QgsProcessingNumeri // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -324,6 +397,23 @@ class GUI_EXPORT QgsProcessingDistanceWidgetWrapper : public QgsProcessingNumeri }; +class GUI_EXPORT QgsProcessingScaleParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingScaleParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QLineEdit *mDefaultLineEdit = nullptr; + +}; + class GUI_EXPORT QgsProcessingScaleWidgetWrapper : public QgsProcessingNumericWidgetWrapper { Q_OBJECT @@ -336,6 +426,11 @@ class GUI_EXPORT QgsProcessingScaleWidgetWrapper : public QgsProcessingNumericWi // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -352,6 +447,26 @@ class GUI_EXPORT QgsProcessingScaleWidgetWrapper : public QgsProcessingNumericWi friend class TestProcessingGui; }; + +class GUI_EXPORT QgsProcessingRangeParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingRangeParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mTypeComboBox = nullptr; + QLineEdit *mMinLineEdit = nullptr; + QLineEdit *mMaxLineEdit = nullptr; + +}; + class GUI_EXPORT QgsProcessingRangeWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { Q_OBJECT @@ -364,6 +479,11 @@ class GUI_EXPORT QgsProcessingRangeWidgetWrapper : public QgsAbstractProcessingP // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -374,7 +494,6 @@ class GUI_EXPORT QgsProcessingRangeWidgetWrapper : public QgsAbstractProcessingP QVariant widgetValue() const override; QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; protected: @@ -390,6 +509,22 @@ class GUI_EXPORT QgsProcessingRangeWidgetWrapper : public QgsAbstractProcessingP friend class TestProcessingGui; }; +class GUI_EXPORT QgsProcessingMatrixParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingMatrixParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsProcessingMatrixModelerWidget *mMatrixWidget = nullptr; + +}; class GUI_EXPORT QgsProcessingMatrixWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { @@ -403,6 +538,11 @@ class GUI_EXPORT QgsProcessingMatrixWidgetWrapper : public QgsAbstractProcessing // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -414,7 +554,6 @@ class GUI_EXPORT QgsProcessingMatrixWidgetWrapper : public QgsAbstractProcessing QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -472,8 +611,6 @@ class GUI_EXPORT QgsProcessingFileWidgetWrapper : public QgsAbstractProcessingPa QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -483,6 +620,26 @@ class GUI_EXPORT QgsProcessingFileWidgetWrapper : public QgsAbstractProcessingPa friend class TestProcessingGui; }; + + +class GUI_EXPORT QgsProcessingExpressionParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingExpressionParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mParentLayerComboBox = nullptr; + QgsExpressionLineEdit *mDefaultLineEdit = nullptr; + +}; + class GUI_EXPORT QgsProcessingExpressionWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { Q_OBJECT @@ -495,6 +652,11 @@ class GUI_EXPORT QgsProcessingExpressionWidgetWrapper : public QgsAbstractProces // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -510,7 +672,6 @@ class GUI_EXPORT QgsProcessingExpressionWidgetWrapper : public QgsAbstractProces QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; const QgsVectorLayer *linkedVectorLayer() const override; private: @@ -585,6 +746,22 @@ class GUI_EXPORT QgsProcessingEnumPanelWidget : public QWidget friend class TestProcessingGui; }; +class GUI_EXPORT QgsProcessingEnumParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingEnumParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsProcessingEnumModelerWidget *mEnumWidget = nullptr; + +}; class GUI_EXPORT QgsProcessingEnumWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { @@ -598,6 +775,11 @@ class GUI_EXPORT QgsProcessingEnumWidgetWrapper : public QgsAbstractProcessingPa // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -611,7 +793,6 @@ class GUI_EXPORT QgsProcessingEnumWidgetWrapper : public QgsAbstractProcessingPa QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -649,7 +830,6 @@ class GUI_EXPORT QgsProcessingLayoutWidgetWrapper : public QgsAbstractProcessing QStringList compatibleOutputTypes() const override; - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -713,8 +893,6 @@ class GUI_EXPORT QgsProcessingLayoutItemWidgetWrapper : public QgsAbstractProces QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -783,6 +961,23 @@ class GUI_EXPORT QgsProcessingPointPanel : public QWidget }; +class GUI_EXPORT QgsProcessingPointParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingPointParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QLineEdit *mDefaultLineEdit = nullptr; + +}; + class GUI_EXPORT QgsProcessingPointWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface { Q_OBJECT @@ -795,6 +990,11 @@ class GUI_EXPORT QgsProcessingPointWidgetWrapper : public QgsAbstractProcessingP // QgsProcessingParameterWidgetFactoryInterface QString parameterType() const override; QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -809,8 +1009,6 @@ class GUI_EXPORT QgsProcessingPointWidgetWrapper : public QgsAbstractProcessingP QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -822,6 +1020,65 @@ class GUI_EXPORT QgsProcessingPointWidgetWrapper : public QgsAbstractProcessingP }; + + +class GUI_EXPORT QgsProcessingExtentParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingExtentParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsExtentWidget *mDefaultWidget = nullptr; + +}; + +class GUI_EXPORT QgsProcessingExtentWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingExtentWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ) override; + void setDialog( QDialog *dialog ) override; + + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + QString modelerExpressionFormatString() const override; + private: + + QgsExtentWidget *mExtentWidget = nullptr; + QDialog *mDialog = nullptr; + + friend class TestProcessingGui; +}; + class GUI_EXPORT QgsProcessingColorParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget { Q_OBJECT @@ -869,8 +1126,6 @@ class GUI_EXPORT QgsProcessingColorWidgetWrapper : public QgsAbstractProcessingP QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -934,8 +1189,6 @@ class GUI_EXPORT QgsProcessingCoordinateOperationWidgetWrapper : public QgsAbstr QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -988,19 +1241,43 @@ class GUI_EXPORT QgsProcessingFieldPanelWidget : public QWidget friend class TestProcessingGui; }; - -class GUI_EXPORT QgsProcessingFieldWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +class GUI_EXPORT QgsProcessingFieldParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget { Q_OBJECT - public: - QgsProcessingFieldWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, - QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + QgsProcessingFieldParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; - // QgsProcessingParameterWidgetFactoryInterface - QString parameterType() const override; - QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + private: + + QComboBox *mParentLayerComboBox = nullptr; + QComboBox *mDataTypeComboBox = nullptr; + QLineEdit *mDefaultLineEdit = nullptr; + QCheckBox *mAllowMultipleCheckBox = nullptr; + QCheckBox *mDefaultToAllCheckBox = nullptr; +}; + +class GUI_EXPORT QgsProcessingFieldWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingFieldWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; // QgsProcessingParameterWidgetWrapper interface QWidget *createWidget() override SIP_FACTORY; @@ -1017,8 +1294,6 @@ class GUI_EXPORT QgsProcessingFieldWidgetWrapper : public QgsAbstractProcessingP QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; const QgsVectorLayer *linkedVectorLayer() const override; @@ -1082,8 +1357,6 @@ class GUI_EXPORT QgsProcessingMapThemeWidgetWrapper : public QgsAbstractProcessi QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -1140,8 +1413,6 @@ class GUI_EXPORT QgsProcessingDateTimeWidgetWrapper : public QgsAbstractProcessi QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -1207,8 +1478,6 @@ class GUI_EXPORT QgsProcessingProviderConnectionWidgetWrapper : public QgsAbstra QStringList compatibleParameterTypes() const override; QStringList compatibleOutputTypes() const override; - - QList< int > compatibleDataTypes() const override; QString modelerExpressionFormatString() const override; private: @@ -1219,6 +1488,663 @@ class GUI_EXPORT QgsProcessingProviderConnectionWidgetWrapper : public QgsAbstra friend class TestProcessingGui; }; + + +class GUI_EXPORT QgsProcessingDatabaseSchemaParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingDatabaseSchemaParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mConnectionParamComboBox = nullptr; + QLineEdit *mDefaultEdit = nullptr; + +}; + +class GUI_EXPORT QgsProcessingDatabaseSchemaWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingDatabaseSchemaWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + void postInitialize( const QList< QgsAbstractProcessingParameterWidgetWrapper * > &wrappers ) override; + + + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + public slots: + void setParentConnectionWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ); + + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + QString modelerExpressionFormatString() const override; + + private: + + QgsDatabaseSchemaComboBox *mSchemaComboBox = nullptr; + int mBlockSignals = 0; + + friend class TestProcessingGui; +}; + + + + +class GUI_EXPORT QgsProcessingDatabaseTableParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingDatabaseTableParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mConnectionParamComboBox = nullptr; + QComboBox *mSchemaParamComboBox = nullptr; + QLineEdit *mDefaultEdit = nullptr; + +}; + +class GUI_EXPORT QgsProcessingDatabaseTableWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingDatabaseTableWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + void postInitialize( const QList< QgsAbstractProcessingParameterWidgetWrapper * > &wrappers ) override; + + + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + public slots: + void setParentConnectionWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ); + void setParentSchemaWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ); + + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + QString modelerExpressionFormatString() const override; + + private: + + QgsDatabaseTableComboBox *mTableComboBox = nullptr; + int mBlockSignals = 0; + QString mConnection; + QString mProvider; + QString mSchema; + + friend class TestProcessingGui; +}; + +class GUI_EXPORT QgsProcessingMapLayerParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingMapLayerParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsCheckableComboBox *mLayerTypeComboBox = nullptr; +}; + +class GUI_EXPORT QgsProcessingMapLayerWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingMapLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ) override; + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + QString modelerExpressionFormatString() const override; + + private: + + QgsProcessingMapLayerComboBox *mComboBox = nullptr; + int mBlockSignals = 0; + + friend class TestProcessingGui; +}; + + +class GUI_EXPORT QgsProcessingRasterLayerWidgetWrapper : public QgsProcessingMapLayerWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingRasterLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + protected: + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + + QString modelerExpressionFormatString() const override; + +}; + + +class GUI_EXPORT QgsProcessingVectorLayerParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingVectorLayerParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsCheckableComboBox *mGeometryTypeComboBox = nullptr; +}; + +class GUI_EXPORT QgsProcessingVectorLayerWidgetWrapper : public QgsProcessingMapLayerWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingVectorLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + protected: + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + + QString modelerExpressionFormatString() const override; + QList< int > compatibleDataTypes( const QgsProcessingParameterDefinition *parameter ) const override; +}; + + +class GUI_EXPORT QgsProcessingFeatureSourceParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingFeatureSourceParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QgsCheckableComboBox *mGeometryTypeComboBox = nullptr; +}; + +class GUI_EXPORT QgsProcessingFeatureSourceWidgetWrapper : public QgsProcessingMapLayerWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingFeatureSourceWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + protected: + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + + QString modelerExpressionFormatString() const override; + QList< int > compatibleDataTypes( const QgsProcessingParameterDefinition *parameter ) const override; +}; + + +class GUI_EXPORT QgsProcessingMeshLayerWidgetWrapper : public QgsProcessingMapLayerWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingMeshLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + protected: + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + + QString modelerExpressionFormatString() const override; + +}; + + +class GUI_EXPORT QgsProcessingRasterBandPanelWidget : public QWidget +{ + Q_OBJECT + + public: + + QgsProcessingRasterBandPanelWidget( QWidget *parent = nullptr, const QgsProcessingParameterBand *param = nullptr ); + + void setBands( const QList< int > &bands ); + void setBandNames( const QHash &names ); + QList< int > bands() const { return mBands; } + + QVariant value() const { return mValue; } + void setValue( const QVariant &value ); + + signals: + + void changed(); + + private slots: + + void showDialog(); + + private: + + void updateSummaryText(); + + QList< int > mBands; + QHash mBandNames; + + const QgsProcessingParameterBand *mParam = nullptr; + QLineEdit *mLineEdit = nullptr; + QToolButton *mToolButton = nullptr; + + QVariantList mValue; + + friend class TestProcessingGui; +}; + +class GUI_EXPORT QgsProcessingBandParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingBandParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mParentLayerComboBox = nullptr; + QLineEdit *mDefaultLineEdit = nullptr; + QCheckBox *mAllowMultipleCheckBox = nullptr; +}; + +class GUI_EXPORT QgsProcessingBandWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingBandWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + void postInitialize( const QList< QgsAbstractProcessingParameterWidgetWrapper * > &wrappers ) override; + + public slots: + void setParentLayerWrapperValue( const QgsAbstractProcessingParameterWidgetWrapper *parentWrapper ); + + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + QString modelerExpressionFormatString() const override; + + private: + + QgsRasterBandComboBox *mComboBox = nullptr; + QgsProcessingRasterBandPanelWidget *mPanel = nullptr; + QLineEdit *mLineEdit = nullptr; + + std::unique_ptr< QgsRasterLayer > mParentLayer; + + friend class TestProcessingGui; +}; + + + +class GUI_EXPORT QgsProcessingMultipleLayerPanelWidget : public QWidget +{ + Q_OBJECT + + public: + + QgsProcessingMultipleLayerPanelWidget( QWidget *parent = nullptr, const QgsProcessingParameterMultipleLayers *param = nullptr ); + + QVariant value() const { return mValue; } + void setValue( const QVariant &value ); + + void setProject( QgsProject *project ); + void setModel( QgsProcessingModelAlgorithm *model, const QString &modelChildAlgorithmID ); + + signals: + + void changed(); + + private slots: + + void showDialog(); + + private: + + void updateSummaryText(); + + const QgsProcessingParameterMultipleLayers *mParam = nullptr; + QLineEdit *mLineEdit = nullptr; + QToolButton *mToolButton = nullptr; + + QVariantList mValue; + QList< QgsProcessingModelChildParameterSource > mModelSources; + QgsProcessingModelAlgorithm *mModel = nullptr; + + QgsProject *mProject = nullptr; + + friend class TestProcessingGui; +}; + +class GUI_EXPORT QgsProcessingMultipleLayerParameterDefinitionWidget : public QgsProcessingAbstractParameterDefinitionWidget +{ + Q_OBJECT + public: + + QgsProcessingMultipleLayerParameterDefinitionWidget( QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsProcessingParameterDefinition *createParameter( const QString &name, const QString &description, QgsProcessingParameterDefinition::Flags flags ) const override; + + private: + + QComboBox *mLayerTypeComboBox = nullptr; +}; + +class GUI_EXPORT QgsProcessingMultipleLayerWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingMultipleLayerWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + QgsProcessingAbstractParameterDefinitionWidget *createParameterDefinitionWidget( + QgsProcessingContext &context, + const QgsProcessingParameterWidgetContext &widgetContext, + const QgsProcessingParameterDefinition *definition = nullptr, + const QgsProcessingAlgorithm *algorithm = nullptr ) override; + + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + void setWidgetContext( const QgsProcessingParameterWidgetContext &context ) override; + + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + QString modelerExpressionFormatString() const override; + + private: + + QgsProcessingMultipleLayerPanelWidget *mPanel = nullptr; + + friend class TestProcessingGui; +}; + + +class GUI_EXPORT QgsProcessingOutputWidgetWrapper : public QgsAbstractProcessingParameterWidgetWrapper, public QgsProcessingParameterWidgetFactoryInterface +{ + Q_OBJECT + + public: + + QgsProcessingOutputWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetWrapper interface + QWidget *createWidget() override SIP_FACTORY; + protected: + + void setWidgetValue( const QVariant &value, QgsProcessingContext &context ) override; + QVariant widgetValue() const override; + QVariantMap customProperties() const override; + + QStringList compatibleParameterTypes() const override; + + QStringList compatibleOutputTypes() const override; + + private: + + QgsProcessingLayerOutputDestinationWidget *mOutputWidget = nullptr; + int mBlockSignals = 0; + + friend class TestProcessingGui; +}; + + +class GUI_EXPORT QgsProcessingFeatureSinkWidgetWrapper : public QgsProcessingOutputWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingFeatureSinkWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + + protected: + QString modelerExpressionFormatString() const override; + + private: + QgsProcessingContext mContext; + +}; + +class GUI_EXPORT QgsProcessingVectorDestinationWidgetWrapper : public QgsProcessingOutputWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingVectorDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + + protected: + QString modelerExpressionFormatString() const override; + +}; + +class GUI_EXPORT QgsProcessingRasterDestinationWidgetWrapper : public QgsProcessingOutputWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingRasterDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + + protected: + QString modelerExpressionFormatString() const override; + +}; + +class GUI_EXPORT QgsProcessingFileDestinationWidgetWrapper : public QgsProcessingOutputWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingFileDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + + protected: + QString modelerExpressionFormatString() const override; + +}; + +class GUI_EXPORT QgsProcessingFolderDestinationWidgetWrapper : public QgsProcessingOutputWidgetWrapper +{ + Q_OBJECT + + public: + + QgsProcessingFolderDestinationWidgetWrapper( const QgsProcessingParameterDefinition *parameter = nullptr, + QgsProcessingGui::WidgetType type = QgsProcessingGui::Standard, QWidget *parent = nullptr ); + + // QgsProcessingParameterWidgetFactoryInterface + QString parameterType() const override; + QgsAbstractProcessingParameterWidgetWrapper *createWidgetWrapper( const QgsProcessingParameterDefinition *parameter, QgsProcessingGui::WidgetType type ) override; + + protected: + QString modelerExpressionFormatString() const override; + +}; + ///@endcond PRIVATE #endif // QGSPROCESSINGWIDGETWRAPPERIMPL_H diff --git a/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp b/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp index c55887be4634..d9b9872cf98a 100644 --- a/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp +++ b/src/gui/providers/ogr/qgsgeopackageitemguiprovider.cpp @@ -120,7 +120,7 @@ void QgsGeoPackageItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu QVariantMap dataDelete; dataDelete.insert( QStringLiteral( "path" ), collectionItem->path() ); dataDelete.insert( QStringLiteral( "parent" ), QVariant::fromValue( QPointer< QgsDataItem >( collectionItem->parent() ) ) ); - actionAddTable->setData( dataDelete ); + actionDelete->setData( dataDelete ); connect( actionDelete, &QAction::triggered, this, &QgsGeoPackageItemGuiProvider::deleteGpkg ); menu->addAction( actionDelete ); diff --git a/src/gui/qgisinterface.h b/src/gui/qgisinterface.h index c748e3aa6bf3..58c9de8f29ed 100644 --- a/src/gui/qgisinterface.h +++ b/src/gui/qgisinterface.h @@ -39,6 +39,7 @@ class QWidget; class QgsAdvancedDigitizingDockWidget; class QgsAttributeDialog; class QgsCustomDropHandler; +class QgsCustomProjectOpenHandler; class QgsLayoutCustomDropHandler; class QgsFeature; class QgsLayerTreeMapCanvasBridge; @@ -61,6 +62,7 @@ class QgsLocatorFilter; class QgsStatusBar; class QgsMeshLayer; class QgsBrowserGuiModel; +class QgsDevToolWidgetFactory; /** @@ -511,6 +513,18 @@ class GUI_EXPORT QgisInterface : public QObject virtual QAction *actionShowAllLayers() = 0; virtual QAction *actionHideSelectedLayers() = 0; + /** + * Returns the Toggle Selected Layers action. + * \since QGIS 3.14 + */ + virtual QAction *actionToggleSelectedLayers() = 0; + + /** + * Returns the Toggle Selected Layers Independently action. + * \since QGIS 3.14 + */ + virtual QAction *actionToggleSelectedLayersIndependently() = 0; + /** * Returns the Hide Deselected Layers action. * \since QGIS 3.0 @@ -928,9 +942,25 @@ class GUI_EXPORT QgisInterface : public QObject virtual void unregisterOptionsWidgetFactory( QgsOptionsWidgetFactory *factory ) = 0; /** - * Register a new custom drop handler. + * Register a new tool in the development/debugging tools dock. * \note Ownership of the factory is not transferred, and the factory must * be unregistered when plugin is unloaded. + * \see unregisterDevToolWidgetFactory() + * \since QGIS 3.14 + */ + virtual void registerDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) = 0; + + /** + * Unregister a previously registered tool factory from the development/debugging tools dock. + * \see registerDevToolWidgetFactory() + * \since QGIS 3.14 + */ + virtual void unregisterDevToolWidgetFactory( QgsDevToolWidgetFactory *factory ) = 0; + + /** + * Register a new custom drop \a handler. + * \note Ownership of \a handler is not transferred, and the handler must + * be unregistered when plugin is unloaded. * \see QgsCustomDropHandler * \see unregisterCustomDropHandler() * \since QGIS 3.0 @@ -938,16 +968,34 @@ class GUI_EXPORT QgisInterface : public QObject virtual void registerCustomDropHandler( QgsCustomDropHandler *handler ) = 0; /** - * Unregister a previously registered custom drop handler. + * Unregister a previously registered custom drop \a handler. * \see QgsCustomDropHandler * \see registerCustomDropHandler() * \since QGIS 3.0 */ virtual void unregisterCustomDropHandler( QgsCustomDropHandler *handler ) = 0; + /** + * Register a new custom project open \a handler. + * \note Ownership of \a handler is not transferred, and the handler must + * be unregistered when plugin is unloaded. + * \see QgsCustomProjectOpenHandler + * \see unregisterCustomProjectOpenHandler() + * \since QGIS 3.14 + */ + virtual void registerCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) = 0; + + /** + * Unregister a previously registered custom project open \a handler. + * \see QgsCustomDropHandler + * \see registerCustomProjectOpenHandler() + * \since QGIS 3.14 + */ + virtual void unregisterCustomProjectOpenHandler( QgsCustomProjectOpenHandler *handler ) = 0; + /** * Register a new custom drop \a handler for layout windows. - * \note Ownership of the factory is not transferred, and the factory must + * \note Ownership of \a handler is not transferred, and the handler must * be unregistered when plugin is unloaded. * \see QgsLayoutCustomDropHandler * \see unregisterCustomLayoutDropHandler() diff --git a/src/gui/qgsaddtaborgroup.cpp b/src/gui/qgsaddtaborgroup.cpp index 0ce2b1b80044..2f6146db33ff 100644 --- a/src/gui/qgsaddtaborgroup.cpp +++ b/src/gui/qgsaddtaborgroup.cpp @@ -25,7 +25,7 @@ #include #include -QgsAddTabOrGroup::QgsAddTabOrGroup( QgsVectorLayer *lyr, const QList < TabPair > &tabList, QWidget *parent ) +QgsAddTabOrGroup::QgsAddTabOrGroup( QgsVectorLayer *lyr, const QList < TabPair > &tabList, QTreeWidgetItem *currentTab, QWidget *parent ) : QDialog( parent ) , mLayer( lyr ) , mTabs( tabList ) @@ -43,6 +43,11 @@ QgsAddTabOrGroup::QgsAddTabOrGroup( QgsVectorLayer *lyr, const QList < TabPair > for ( const TabPair &tab : constMTabs ) { mTabList->addItem( tab.first, i ); + if ( tab.second == currentTab ) + { + mTabList->setCurrentIndex( i ); + mGroupButton->setChecked( true ); + } ++i; } } diff --git a/src/gui/qgsaddtaborgroup.h b/src/gui/qgsaddtaborgroup.h index 8f7a9292a02c..44932c39a5ce 100644 --- a/src/gui/qgsaddtaborgroup.h +++ b/src/gui/qgsaddtaborgroup.h @@ -46,7 +46,7 @@ class GUI_EXPORT QgsAddTabOrGroup : public QDialog, private Ui::QgsAddTabOrGroup public: //! constructor - QgsAddTabOrGroup( QgsVectorLayer *lyr, const QList &tabList, QWidget *parent = nullptr ); + QgsAddTabOrGroup( QgsVectorLayer *lyr, const QList &tabList, QTreeWidgetItem *currentTab = nullptr, QWidget *parent = nullptr ); //! Returns the name of the tab or group QString name(); diff --git a/src/gui/qgsattributeform.cpp b/src/gui/qgsattributeform.cpp index 6d5ba905fb1b..79a9826ce4c7 100644 --- a/src/gui/qgsattributeform.cpp +++ b/src/gui/qgsattributeform.cpp @@ -43,6 +43,7 @@ #include "qgsexpressioncontextutils.h" #include "qgsfeaturerequest.h" #include "qgstexteditwrapper.h" +#include "qgsfieldmodel.h" #include #include @@ -1506,7 +1507,7 @@ void QgsAttributeForm::init() // This will also create the widget QLabel *l = new QLabel( labelText ); - l->setToolTip( QStringLiteral( "%1

    %2

    " ).arg( fieldName, field.comment() ) ); + l->setToolTip( QgsFieldModel::fieldToolTipExtended( field, mLayer ) ); QSvgWidget *i = new QSvgWidget(); i->setFixedSize( 18, 18 ); diff --git a/src/gui/qgsbrowserdockwidget_p.cpp b/src/gui/qgsbrowserdockwidget_p.cpp index 2fd1931766ad..147f008232d3 100644 --- a/src/gui/qgsbrowserdockwidget_p.cpp +++ b/src/gui/qgsbrowserdockwidget_p.cpp @@ -44,6 +44,7 @@ #include "qgsnative.h" #include "qgsmaptoolpan.h" #include "qgsvectorlayercache.h" +#include "qgsvectortilelayer.h" #include "qgsattributetablemodel.h" #include "qgsattributetablefiltermodel.h" #include "qgsapplication.h" @@ -208,6 +209,13 @@ void QgsBrowserLayerProperties::setItem( QgsDataItem *item ) break; } + case QgsMapLayerType::VectorTileLayer: + { + QgsDebugMsgLevel( QStringLiteral( "creating vector tile layer" ), 2 ); + mLayer = qgis::make_unique< QgsVectorTileLayer >( layerItem->uri(), layerItem->name() ); + break; + } + case QgsMapLayerType::PluginLayer: { // TODO: support display of properties for plugin layers diff --git a/src/gui/qgscheckablecombobox.cpp b/src/gui/qgscheckablecombobox.cpp index 891c8e6492f1..db36cf45bebb 100644 --- a/src/gui/qgscheckablecombobox.cpp +++ b/src/gui/qgscheckablecombobox.cpp @@ -148,6 +148,24 @@ QStringList QgsCheckableComboBox::checkedItems() const return items; } +QVariantList QgsCheckableComboBox::checkedItemsData() const +{ + QVariantList data; + + if ( model() ) + { + QModelIndex index = model()->index( 0, modelColumn(), rootModelIndex() ); + QModelIndexList indexes = model()->match( index, Qt::CheckStateRole, Qt::Checked, -1, Qt::MatchExactly ); + const auto constIndexes = indexes; + for ( const QModelIndex &index : constIndexes ) + { + data += index.data( Qt::UserRole ).toString(); + } + } + + return data; +} + Qt::CheckState QgsCheckableComboBox::itemCheckState( int index ) const { return static_cast( itemData( index, Qt::CheckStateRole ).toInt() ); diff --git a/src/gui/qgscheckablecombobox.h b/src/gui/qgscheckablecombobox.h index 0fcc628f448a..14f0f068ffc9 100644 --- a/src/gui/qgscheckablecombobox.h +++ b/src/gui/qgscheckablecombobox.h @@ -169,6 +169,13 @@ class GUI_EXPORT QgsCheckableComboBox : public QComboBox */ QStringList checkedItems() const; + /** + * Returns userData (stored in the Qt::UserRole) associated with + * currently checked items. + * \see checkedItems() + */ + QVariantList checkedItemsData() const; + /** * Returns the checked state of the item identified by index * \param index item index diff --git a/src/gui/qgscolorbutton.cpp b/src/gui/qgscolorbutton.cpp index bbb35b72cb69..e87ab097327c 100644 --- a/src/gui/qgscolorbutton.cpp +++ b/src/gui/qgscolorbutton.cpp @@ -508,7 +508,7 @@ void QgsColorButton::prepareMenu() { if ( mShowNull ) { - QAction *nullAction = new QAction( tr( "Clear Color" ), this ); + QAction *nullAction = new QAction( mNullColorString.isEmpty() ? tr( "Clear Color" ) : mNullColorString, this ); nullAction->setIcon( createMenuIcon( Qt::transparent, false ) ); mMenu->addAction( nullAction ); connect( nullAction, &QAction::triggered, this, &QgsColorButton::setToNull ); @@ -825,9 +825,10 @@ void QgsColorButton::setDefaultColor( const QColor &color ) mDefaultColor = color; } -void QgsColorButton::setShowNull( bool showNull ) +void QgsColorButton::setShowNull( bool showNull, const QString &nullString ) { mShowNull = showNull; + mNullColorString = nullString; } bool QgsColorButton::showNull() const diff --git a/src/gui/qgscolorbutton.h b/src/gui/qgscolorbutton.h index c3c9277b585b..20a724f21a06 100644 --- a/src/gui/qgscolorbutton.h +++ b/src/gui/qgscolorbutton.h @@ -202,11 +202,12 @@ class GUI_EXPORT QgsColorButton : public QToolButton /** * Sets whether a set to null (clear) option is shown in the button's drop-down menu. * \param showNull set to TRUE to show a null option + * \param nullString translated string to use for the null option. If not set, a default "Clear Color" string will be used. * \see showNull() * \see isNull() * \since QGIS 2.16 */ - void setShowNull( bool showNull ); + void setShowNull( bool showNull, const QString &nullString = QString() ); /** * Returns whether the set to null (clear) option is shown in the button's drop-down menu. @@ -481,6 +482,7 @@ class GUI_EXPORT QgsColorButton : public QToolButton bool mShowNoColorOption = false; QString mNoColorString; bool mShowNull = false; + QString mNullColorString; QPoint mDragStartPosition; bool mPickingColor = false; diff --git a/src/gui/qgscustomprojectopenhandler.cpp b/src/gui/qgscustomprojectopenhandler.cpp new file mode 100644 index 000000000000..2c16c7439899 --- /dev/null +++ b/src/gui/qgscustomprojectopenhandler.cpp @@ -0,0 +1,27 @@ +/*************************************************************************** + qgscustomprojectopenhandler.h + --------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgscustomprojectopenhandler.h" +#include + +bool QgsCustomProjectOpenHandler::createDocumentThumbnailAfterOpen() const +{ + return false; +} + +QIcon QgsCustomProjectOpenHandler::icon() const +{ + return QIcon(); +} diff --git a/src/gui/qgscustomprojectopenhandler.h b/src/gui/qgscustomprojectopenhandler.h new file mode 100644 index 000000000000..ebfbdf3d63af --- /dev/null +++ b/src/gui/qgscustomprojectopenhandler.h @@ -0,0 +1,75 @@ +/*************************************************************************** + qgscustomprojectopenhandler.h + --------------------- + begin : April 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSCUSTOMPROJECTOPENHANDLER_H +#define QGSCUSTOMPROJECTOPENHANDLER_H + +#include "qgis_gui.h" +#include +#include + +/** + * \ingroup gui + * Abstract base class that may be implemented to handle new project file types within + * the QGIS application. + * + * This interface allows extending the QGIS interface by adding support for opening additional + * (non QGS/QGZ) project files, e.g. allowing plugins to add support for opening other + * vendor project formats (such as ArcGIS MXD documents or MapInfo WOR workspaces). + * + * Handler implementations should indicate the file types they support via their filters() + * implementation, and then implement handleProjectOpen() to open the associated files. + * + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsCustomProjectOpenHandler : public QObject +{ + Q_OBJECT + + public: + + /** + * Called when the specified project \a file has been opened within QGIS. If TRUE + * is returned, then the handler has accepted this file and it should not + * be further processed (e.g. by other QgsCustomProjectOpenHandler). + * + * It it is the subclasses' responsibility to ignore file types it cannot handle + * by returning FALSE for these. + * + * The base class implementation does nothing. + */ + virtual bool handleProjectOpen( const QString &file ) = 0; + + /** + * Returns file filters associated with this handler, e.g. "MXD Documents (*.mxd)", "MapInfo Workspaces (*.wor)". + * + * Each individual filter should be reflected as one entry in the returned list. + */ + virtual QStringList filters() const = 0; + + /** + * Returns TRUE if a document thumbnail should automatically be created after opening the project. + * + * The default behavior is to return FALSE. + */ + virtual bool createDocumentThumbnailAfterOpen() const; + + /** + * Returns a custom icon used to represent this handler. + */ + virtual QIcon icon() const; +}; + +#endif // QgsCustomProjectOpenHandler_H diff --git a/src/gui/qgsdatabaseschemacombobox.cpp b/src/gui/qgsdatabaseschemacombobox.cpp index 378151e1e583..7926c4b2dfda 100644 --- a/src/gui/qgsdatabaseschemacombobox.cpp +++ b/src/gui/qgsdatabaseschemacombobox.cpp @@ -24,7 +24,8 @@ QgsDatabaseSchemaComboBox::QgsDatabaseSchemaComboBox( const QString &provider, c : QWidget( parent ) , mProvider( provider ) { - mModel = new QgsDatabaseSchemaModel( provider, connection, this ); + if ( !provider.isEmpty() && !connection.isEmpty() ) + mModel = new QgsDatabaseSchemaModel( provider, connection, this ); init(); } @@ -35,12 +36,27 @@ QgsDatabaseSchemaComboBox::QgsDatabaseSchemaComboBox( QgsAbstractDatabaseProvide init(); } +void QgsDatabaseSchemaComboBox::setAllowEmptySchema( bool allowEmpty ) +{ + mAllowEmpty = allowEmpty; + if ( mModel ) + mModel->setAllowEmptySchema( mAllowEmpty ); +} + +bool QgsDatabaseSchemaComboBox::allowEmptySchema() const +{ + return mAllowEmpty; +} + void QgsDatabaseSchemaComboBox::init() { mComboBox = new QComboBox(); - mSortModel = new QSortFilterProxyModel( this ); - mSortModel->setSourceModel( mModel ); + mSortModel = new QgsDatabaseSchemaComboBoxSortModel( this ); + + if ( mModel ) + mSortModel->setSourceModel( mModel ); + mSortModel->setSortRole( Qt::DisplayRole ); mSortModel->setSortLocaleAware( true ); mSortModel->setSortCaseSensitivity( Qt::CaseInsensitive ); @@ -74,7 +90,11 @@ void QgsDatabaseSchemaComboBox::setSchema( const QString &schema ) if ( schema.isEmpty() ) { - mComboBox->setCurrentIndex( -1 ); + if ( mAllowEmpty ) + mComboBox->setCurrentIndex( 0 ); + else + mComboBox->setCurrentIndex( -1 ); + emit schemaChanged( QString() ); return; } @@ -101,9 +121,20 @@ void QgsDatabaseSchemaComboBox::setConnectionName( const QString &connection, co const QString oldSchema = currentSchema(); QgsDatabaseSchemaModel *oldModel = mModel; - mModel = new QgsDatabaseSchemaModel( mProvider, connection, this ); - mSortModel->setSourceModel( mModel ); - oldModel->deleteLater(); + if ( !connection.isEmpty() && !mProvider.isEmpty() ) + { + mModel = new QgsDatabaseSchemaModel( mProvider, connection, this ); + mModel->setAllowEmptySchema( mAllowEmpty ); + mSortModel->setSourceModel( mModel ); + } + else + { + mModel = nullptr; + mSortModel->setSourceModel( nullptr ); + } + if ( oldModel ) + oldModel->deleteLater(); + if ( currentSchema() != oldSchema ) setSchema( oldSchema ); } @@ -111,7 +142,8 @@ void QgsDatabaseSchemaComboBox::setConnectionName( const QString &connection, co void QgsDatabaseSchemaComboBox::refreshSchemas() { const QString oldSchema = currentSchema(); - mModel->refresh(); + if ( mModel ) + mModel->refresh(); setSchema( oldSchema ); } @@ -134,7 +166,7 @@ void QgsDatabaseSchemaComboBox::indexChanged( int i ) void QgsDatabaseSchemaComboBox::rowsChanged() { - if ( mComboBox->count() == 1 ) + if ( mComboBox->count() == 1 || ( mAllowEmpty && mComboBox->count() == 2 && mComboBox->currentIndex() == 1 ) ) { //currently selected connection item has changed emit schemaChanged( currentSchema() ); @@ -144,3 +176,27 @@ void QgsDatabaseSchemaComboBox::rowsChanged() emit schemaChanged( QString() ); } } + + +///@cond PRIVATE +QgsDatabaseSchemaComboBoxSortModel::QgsDatabaseSchemaComboBoxSortModel( QObject *parent ) + : QSortFilterProxyModel( parent ) +{ + +} + +bool QgsDatabaseSchemaComboBoxSortModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const +{ + // empty row is always first + if ( sourceModel()->data( left, QgsDatabaseSchemaModel::RoleEmpty ).toBool() ) + return true; + else if ( sourceModel()->data( right, QgsDatabaseSchemaModel::RoleEmpty ).toBool() ) + return false; + + // default mode is alphabetical order + QString leftStr = sourceModel()->data( left ).toString(); + QString rightStr = sourceModel()->data( right ).toString(); + return QString::localeAwareCompare( leftStr, rightStr ) < 0; +} + +///@endcond diff --git a/src/gui/qgsdatabaseschemacombobox.h b/src/gui/qgsdatabaseschemacombobox.h index ef20a04aac3f..d2985ac51246 100644 --- a/src/gui/qgsdatabaseschemacombobox.h +++ b/src/gui/qgsdatabaseschemacombobox.h @@ -20,11 +20,24 @@ #include "qgis_gui.h" #include "qgis_sip.h" +#include class QgsDatabaseSchemaModel; -class QSortFilterProxyModel; class QgsAbstractDatabaseProviderConnection; +///@cond PRIVATE +#ifndef SIP_RUN +class GUI_EXPORT QgsDatabaseSchemaComboBoxSortModel: public QSortFilterProxyModel +{ + public: + explicit QgsDatabaseSchemaComboBoxSortModel( QObject *parent = nullptr ); + protected: + bool lessThan( const QModelIndex &source_left, const QModelIndex &source_right ) const override; + +}; +#endif +///@endcond + /** * \ingroup gui * \brief The QgsDatabaseSchemaComboBox class is a combo box which displays the list of schemas for a specific database connection. @@ -55,6 +68,18 @@ class GUI_EXPORT QgsDatabaseSchemaComboBox : public QWidget */ explicit QgsDatabaseSchemaComboBox( QgsAbstractDatabaseProviderConnection *connection SIP_TRANSFER, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + /** + * Sets whether an optional empty schema ("not set") option is present in the combobox. + * \see allowEmptySchema() + */ + void setAllowEmptySchema( bool allowEmpty ); + + /** + * Returns TRUE if the combobox allows the empty schema ("not set") choice. + * \see setAllowEmptySchema() + */ + bool allowEmptySchema() const; + /** * Returns the name of the current schema selected in the combo box. */ @@ -95,6 +120,7 @@ class GUI_EXPORT QgsDatabaseSchemaComboBox : public QWidget private: void init(); + bool mAllowEmpty = false; QString mProvider; QgsDatabaseSchemaModel *mModel = nullptr; QSortFilterProxyModel *mSortModel = nullptr; diff --git a/src/gui/qgsdatabasetablecombobox.cpp b/src/gui/qgsdatabasetablecombobox.cpp index e9f912a47892..4251267e9adc 100644 --- a/src/gui/qgsdatabasetablecombobox.cpp +++ b/src/gui/qgsdatabasetablecombobox.cpp @@ -26,7 +26,8 @@ QgsDatabaseTableComboBox::QgsDatabaseTableComboBox( const QString &provider, con , mConnection( connection ) , mSchema( schema ) { - mModel = new QgsDatabaseTableModel( provider, connection, schema, this ); + if ( !provider.isEmpty() && !connection.isEmpty() ) + mModel = new QgsDatabaseTableModel( provider, connection, schema, this ); init(); } @@ -38,12 +39,25 @@ QgsDatabaseTableComboBox::QgsDatabaseTableComboBox( QgsAbstractDatabaseProviderC init(); } +void QgsDatabaseTableComboBox::setAllowEmptyTable( bool allowEmpty ) +{ + mAllowEmpty = allowEmpty; + if ( mModel ) + mModel->setAllowEmptyTable( allowEmpty ); +} + +bool QgsDatabaseTableComboBox::allowEmptyTable() const +{ + return mAllowEmpty; +} + void QgsDatabaseTableComboBox::init() { mComboBox = new QComboBox(); - mSortModel = new QSortFilterProxyModel( this ); - mSortModel->setSourceModel( mModel ); + mSortModel = new QgsDatabaseTableComboBoxSortModel( this ); + if ( mModel ) + mSortModel->setSourceModel( mModel ); mSortModel->setSortRole( Qt::DisplayRole ); mSortModel->setSortLocaleAware( true ); mSortModel->setSortCaseSensitivity( Qt::CaseInsensitive ); @@ -77,7 +91,11 @@ void QgsDatabaseTableComboBox::setTable( const QString &table, const QString &sc if ( table.isEmpty() ) { - mComboBox->setCurrentIndex( -1 ); + if ( mAllowEmpty ) + mComboBox->setCurrentIndex( 0 ); + else + mComboBox->setCurrentIndex( -1 ); + emit tableChanged( QString() ); return; } @@ -99,6 +117,9 @@ void QgsDatabaseTableComboBox::setTable( const QString &table, const QString &sc void QgsDatabaseTableComboBox::setConnectionName( const QString &connection, const QString &provider ) { + if ( provider.isEmpty() && mConnection == connection ) + return; + if ( !provider.isEmpty() ) mProvider = provider; @@ -107,29 +128,44 @@ void QgsDatabaseTableComboBox::setConnectionName( const QString &connection, con const QString oldTable = currentTable(); const QString oldSchema = currentSchema(); QgsDatabaseTableModel *oldModel = mModel; - mModel = new QgsDatabaseTableModel( mProvider, mConnection, mSchema, this ); + if ( !mConnection.isEmpty() ) + { + mModel = new QgsDatabaseTableModel( mProvider, mConnection, mSchema, this ); + mModel->setAllowEmptyTable( mAllowEmpty ); + } + else + mModel = nullptr; + mSortModel->setSourceModel( mModel ); - oldModel->deleteLater(); + if ( oldModel ) + oldModel->deleteLater(); if ( currentTable() != oldTable || currentSchema() != oldSchema ) setTable( oldTable, oldSchema ); } void QgsDatabaseTableComboBox::setSchema( const QString &schema ) { - const QString oldTable = currentTable(); - QgsDatabaseTableModel *oldModel = mModel; + if ( schema == mSchema ) + return; mSchema = schema; - mModel = new QgsDatabaseTableModel( mProvider, mConnection, mSchema, this ); - mSortModel->setSourceModel( mModel ); - oldModel->deleteLater(); - setTable( oldTable ); + + if ( !mProvider.isEmpty() && !mConnection.isEmpty() ) + { + const QString oldTable = currentTable(); + QgsDatabaseTableModel *oldModel = mModel; + mModel = new QgsDatabaseTableModel( mProvider, mConnection, mSchema, this ); + mSortModel->setSourceModel( mModel ); + oldModel->deleteLater(); + setTable( oldTable ); + } } void QgsDatabaseTableComboBox::refreshTables() { const QString oldSchema = currentSchema(); const QString oldTable = currentTable(); - mModel->refresh(); + if ( mModel ) + mModel->refresh(); setTable( oldTable, oldSchema ); } @@ -163,7 +199,7 @@ void QgsDatabaseTableComboBox::indexChanged( int i ) void QgsDatabaseTableComboBox::rowsChanged() { - if ( mComboBox->count() == 1 ) + if ( mComboBox->count() == 1 || ( mAllowEmpty && mComboBox->count() == 2 && mComboBox->currentIndex() == 1 ) ) { //currently selected connection item has changed emit tableChanged( currentTable(), currentSchema() ); @@ -173,3 +209,26 @@ void QgsDatabaseTableComboBox::rowsChanged() emit tableChanged( QString() ); } } + +///@cond PRIVATE +QgsDatabaseTableComboBoxSortModel::QgsDatabaseTableComboBoxSortModel( QObject *parent ) + : QSortFilterProxyModel( parent ) +{ + +} + +bool QgsDatabaseTableComboBoxSortModel::lessThan( const QModelIndex &left, const QModelIndex &right ) const +{ + // empty row is always first + if ( sourceModel()->data( left, QgsDatabaseTableModel::RoleEmpty ).toBool() ) + return true; + else if ( sourceModel()->data( right, QgsDatabaseTableModel::RoleEmpty ).toBool() ) + return false; + + // default mode is alphabetical order + QString leftStr = sourceModel()->data( left ).toString(); + QString rightStr = sourceModel()->data( right ).toString(); + return QString::localeAwareCompare( leftStr, rightStr ) < 0; +} + +///@endcond diff --git a/src/gui/qgsdatabasetablecombobox.h b/src/gui/qgsdatabasetablecombobox.h index 66b360237fc8..953999952f48 100644 --- a/src/gui/qgsdatabasetablecombobox.h +++ b/src/gui/qgsdatabasetablecombobox.h @@ -20,11 +20,24 @@ #include "qgis_gui.h" #include "qgis_sip.h" +#include class QgsDatabaseTableModel; -class QSortFilterProxyModel; class QgsAbstractDatabaseProviderConnection; +///@cond PRIVATE +#ifndef SIP_RUN +class GUI_EXPORT QgsDatabaseTableComboBoxSortModel: public QSortFilterProxyModel +{ + public: + explicit QgsDatabaseTableComboBoxSortModel( QObject *parent = nullptr ); + protected: + bool lessThan( const QModelIndex &source_left, const QModelIndex &source_right ) const override; + +}; +#endif +///@endcond + /** * \ingroup gui * \brief The QgsDatabaseTableComboBox class is a combo box which displays the list of tables for a specific database connection. @@ -59,6 +72,18 @@ class GUI_EXPORT QgsDatabaseTableComboBox : public QWidget */ explicit QgsDatabaseTableComboBox( QgsAbstractDatabaseProviderConnection *connection SIP_TRANSFER, const QString &schema = QString(), QWidget *parent SIP_TRANSFERTHIS = nullptr ); + /** + * Sets whether an optional empty table ("not set") option is present in the combobox. + * \see allowEmptyTable() + */ + void setAllowEmptyTable( bool allowEmpty ); + + /** + * Returns TRUE if the combobox allows the empty table ("not set") choice. + * \see setAllowEmptyTable() + */ + bool allowEmptyTable() const; + /** * Returns the name of the current table selected in the combo box. */ @@ -111,6 +136,7 @@ class GUI_EXPORT QgsDatabaseTableComboBox : public QWidget private: void init(); + bool mAllowEmpty = false; QString mProvider; QString mConnection; QString mSchema; diff --git a/src/gui/qgsdatasourceselectdialog.cpp b/src/gui/qgsdatasourceselectdialog.cpp index 9bdfa00a2034..4c68dd63b787 100644 --- a/src/gui/qgsdatasourceselectdialog.cpp +++ b/src/gui/qgsdatasourceselectdialog.cpp @@ -15,7 +15,6 @@ ***************************************************************************/ #include "qgsdatasourceselectdialog.h" -#include "ui_qgsdatasourceselectdialog.h" #include "qgis.h" #include "qgsbrowsermodel.h" @@ -27,13 +26,14 @@ #include #include #include +#include -QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( +QgsDataSourceSelectWidget::QgsDataSourceSelectWidget( QgsBrowserGuiModel *browserModel, bool setFilterByLayerType, QgsMapLayerType layerType, QWidget *parent ) - : QDialog( parent ) + : QgsPanelWidget( parent ) { if ( ! browserModel ) { @@ -47,8 +47,6 @@ QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( } setupUi( this ); - setWindowTitle( tr( "Select a Data Source" ) ); - QgsGui::enableAutoGeometryRestore( this ); mBrowserProxyModel.setBrowserModel( mBrowserModel ); mBrowserTreeView->setHeaderHidden( true ); @@ -61,7 +59,7 @@ QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( else { mBrowserTreeView->setModel( &mBrowserProxyModel ); - buttonBox->button( QDialogButtonBox::StandardButton::Ok )->setEnabled( false ); + setValid( false ); } mBrowserTreeView->setBrowserModel( mBrowserModel ); @@ -77,7 +75,7 @@ QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( action->setData( "case" ); action->setCheckable( true ); action->setChecked( false ); - connect( action, &QAction::toggled, this, &QgsDataSourceSelectDialog::setCaseSensitive ); + connect( action, &QAction::toggled, this, &QgsDataSourceSelectWidget::setCaseSensitive ); menu->addAction( action ); QActionGroup *group = new QActionGroup( menu ); action = new QAction( tr( "Filter Pattern Syntax" ), group ); @@ -97,17 +95,15 @@ QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( action->setCheckable( true ); menu->addAction( action ); - mBrowserTreeView->setExpandsOnDoubleClick( false ); - connect( mActionRefresh, &QAction::triggered, this, [ = ] { refreshModel( QModelIndex() ); } ); - connect( mBrowserTreeView, &QgsBrowserTreeView::clicked, this, &QgsDataSourceSelectDialog::onLayerSelected ); - connect( mBrowserTreeView, &QgsBrowserTreeView::doubleClicked, this, &QgsDataSourceSelectDialog::itemDoubleClicked ); + connect( mBrowserTreeView, &QgsBrowserTreeView::clicked, this, &QgsDataSourceSelectWidget::onLayerSelected ); + connect( mBrowserTreeView, &QgsBrowserTreeView::doubleClicked, this, &QgsDataSourceSelectWidget::itemDoubleClicked ); connect( mActionCollapse, &QAction::triggered, mBrowserTreeView, &QgsBrowserTreeView::collapseAll ); - connect( mActionShowFilter, &QAction::triggered, this, &QgsDataSourceSelectDialog::showFilterWidget ); - connect( mLeFilter, &QgsFilterLineEdit::returnPressed, this, &QgsDataSourceSelectDialog::setFilter ); - connect( mLeFilter, &QgsFilterLineEdit::cleared, this, &QgsDataSourceSelectDialog::setFilter ); - connect( mLeFilter, &QgsFilterLineEdit::textChanged, this, &QgsDataSourceSelectDialog::setFilter ); - connect( group, &QActionGroup::triggered, this, &QgsDataSourceSelectDialog::setFilterSyntax ); + connect( mActionShowFilter, &QAction::triggered, this, &QgsDataSourceSelectWidget::showFilterWidget ); + connect( mLeFilter, &QgsFilterLineEdit::returnPressed, this, &QgsDataSourceSelectWidget::setFilter ); + connect( mLeFilter, &QgsFilterLineEdit::cleared, this, &QgsDataSourceSelectWidget::setFilter ); + connect( mLeFilter, &QgsFilterLineEdit::textChanged, this, &QgsDataSourceSelectWidget::setFilter ); + connect( group, &QActionGroup::triggered, this, &QgsDataSourceSelectWidget::setFilterSyntax ); mBrowserToolbar->setIconSize( QgsGuiUtils::iconSize( true ) ); @@ -117,11 +113,11 @@ QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( } } -QgsDataSourceSelectDialog::~QgsDataSourceSelectDialog() = default; +QgsDataSourceSelectWidget::~QgsDataSourceSelectWidget() = default; -void QgsDataSourceSelectDialog::showEvent( QShowEvent *e ) +void QgsDataSourceSelectWidget::showEvent( QShowEvent *e ) { - QDialog::showEvent( e ); + QgsPanelWidget::showEvent( e ); QString lastSelectedPath( QgsSettings().value( QStringLiteral( "datasourceSelectLastSelectedItem" ), QString(), QgsSettings::Section::Gui ).toString() ); if ( ! lastSelectedPath.isEmpty() ) @@ -144,7 +140,7 @@ void QgsDataSourceSelectDialog::showEvent( QShowEvent *e ) } } -void QgsDataSourceSelectDialog::showFilterWidget( bool visible ) +void QgsDataSourceSelectWidget::showFilterWidget( bool visible ) { QgsSettings().setValue( QStringLiteral( "datasourceSelectFilterVisible" ), visible, QgsSettings::Section::Gui ); mWidgetFilter->setVisible( visible ); @@ -159,7 +155,7 @@ void QgsDataSourceSelectDialog::showFilterWidget( bool visible ) } } -void QgsDataSourceSelectDialog::setDescription( const QString &description ) +void QgsDataSourceSelectWidget::setDescription( const QString &description ) { if ( !description.isEmpty() ) { @@ -193,20 +189,20 @@ void QgsDataSourceSelectDialog::setDescription( const QString &description ) } } -void QgsDataSourceSelectDialog::setFilter() +void QgsDataSourceSelectWidget::setFilter() { QString filter = mLeFilter->text(); mBrowserProxyModel.setFilterString( filter ); } -void QgsDataSourceSelectDialog::refreshModel( const QModelIndex &index ) +void QgsDataSourceSelectWidget::refreshModel( const QModelIndex &index ) { QgsDataItem *item = mBrowserModel->dataItem( index ); if ( item ) { - QgsDebugMsg( "path = " + item->path() ); + QgsDebugMsgLevel( "path = " + item->path(), 2 ); } else { @@ -240,34 +236,43 @@ void QgsDataSourceSelectDialog::refreshModel( const QModelIndex &index ) } } +void QgsDataSourceSelectWidget::setValid( bool valid ) +{ + const bool prev = mIsValid; + mIsValid = valid; + if ( prev != mIsValid ) + emit validationChanged( mIsValid ); + +} -void QgsDataSourceSelectDialog::setFilterSyntax( QAction *action ) + +void QgsDataSourceSelectWidget::setFilterSyntax( QAction *action ) { if ( !action ) return; mBrowserProxyModel.setFilterSyntax( static_cast< QgsBrowserProxyModel::FilterSyntax >( action->data().toInt() ) ); } -void QgsDataSourceSelectDialog::setCaseSensitive( bool caseSensitive ) +void QgsDataSourceSelectWidget::setCaseSensitive( bool caseSensitive ) { mBrowserProxyModel.setFilterCaseSensitivity( caseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive ); } -void QgsDataSourceSelectDialog::setLayerTypeFilter( QgsMapLayerType layerType ) +void QgsDataSourceSelectWidget::setLayerTypeFilter( QgsMapLayerType layerType ) { mBrowserProxyModel.setFilterByLayerType( true ); mBrowserProxyModel.setLayerType( layerType ); // reset model and button mBrowserTreeView->setModel( &mBrowserProxyModel ); - buttonBox->button( QDialogButtonBox::StandardButton::Ok )->setEnabled( false ); + setValid( false ); } -QgsMimeDataUtils::Uri QgsDataSourceSelectDialog::uri() const +QgsMimeDataUtils::Uri QgsDataSourceSelectWidget::uri() const { return mUri; } -void QgsDataSourceSelectDialog::onLayerSelected( const QModelIndex &index ) +void QgsDataSourceSelectWidget::onLayerSelected( const QModelIndex &index ) { bool isLayerCompatible = false; mUri = QgsMimeDataUtils::Uri(); @@ -287,13 +292,75 @@ void QgsDataSourceSelectDialog::onLayerSelected( const QModelIndex &index ) } } } - buttonBox->button( QDialogButtonBox::StandardButton::Ok )->setEnabled( isLayerCompatible ); + setValid( isLayerCompatible ); + emit selectionChanged(); } -void QgsDataSourceSelectDialog::itemDoubleClicked( const QModelIndex &index ) +void QgsDataSourceSelectWidget::itemDoubleClicked( const QModelIndex &index ) { onLayerSelected( index ); - if ( buttonBox->button( QDialogButtonBox::StandardButton::Ok )->isEnabled() ) - accept(); + if ( mIsValid ) + emit itemTriggered( uri() ); +} + +// +// QgsDataSourceSelectDialog +// + +QgsDataSourceSelectDialog::QgsDataSourceSelectDialog( QgsBrowserGuiModel *browserModel, bool setFilterByLayerType, QgsMapLayerType layerType, QWidget *parent ) + : QDialog( parent ) +{ + setWindowTitle( tr( "Select a Data Source" ) ); + setObjectName( QStringLiteral( "QgsDataSourceSelectDialog" ) ); + QgsGui::enableAutoGeometryRestore( this ); + + mWidget = new QgsDataSourceSelectWidget( browserModel, setFilterByLayerType, layerType ); + + QVBoxLayout *vl = new QVBoxLayout(); + vl->addWidget( mWidget, 1 ); + vl->setContentsMargins( 4, 4, 4, 4 ); + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel ); + connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept ); + connect( buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject ); + buttonBox->button( QDialogButtonBox::Ok )->setEnabled( false ); + connect( mWidget, &QgsDataSourceSelectWidget::validationChanged, buttonBox->button( QDialogButtonBox::Ok ), &QWidget::setEnabled ); + connect( mWidget, &QgsDataSourceSelectWidget::itemTriggered, this, &QDialog::accept ); + vl->addWidget( buttonBox ); + setLayout( vl ); } +void QgsDataSourceSelectDialog::setLayerTypeFilter( QgsMapLayerType layerType ) +{ + mWidget->setLayerTypeFilter( layerType ); +} + +void QgsDataSourceSelectDialog::setDescription( const QString &description ) +{ + mWidget->setDescription( description ); +} + +QgsMimeDataUtils::Uri QgsDataSourceSelectDialog::uri() const +{ + return mWidget->uri(); +} + +void QgsDataSourceSelectDialog::showFilterWidget( bool visible ) +{ + mWidget->showFilterWidget( visible ); +} + +void QgsDataSourceSelectDialog::setFilterSyntax( QAction *syntax ) +{ + mWidget->setFilterSyntax( syntax ); +} + +void QgsDataSourceSelectDialog::setCaseSensitive( bool caseSensitive ) +{ + mWidget->setCaseSensitive( caseSensitive ); +} + +void QgsDataSourceSelectDialog::setFilter() +{ + mWidget->setFilter(); + +} diff --git a/src/gui/qgsdatasourceselectdialog.h b/src/gui/qgsdatasourceselectdialog.h index f74a67205115..5a614bf70044 100644 --- a/src/gui/qgsdatasourceselectdialog.h +++ b/src/gui/qgsdatasourceselectdialog.h @@ -26,10 +26,11 @@ #include #include +#include /** * \ingroup gui - * The QgsDataSourceSelectDialog class embeds the browser view to + * The QgsDataSourceSelectWidget class embeds the browser view to * select an existing data source. * * By default any layer type can be chosen, the valid layer @@ -40,29 +41,28 @@ * To retrieve the selected data source, uri() can be called and it * will return a (possibly invalid) QgsMimeDataUtils::Uri. * - * \since QGIS 3.6 + * \since QGIS 3.14 */ -class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog, private Ui::QgsDataSourceSelectDialog +class GUI_EXPORT QgsDataSourceSelectWidget: public QgsPanelWidget, private Ui::QgsDataSourceSelectDialog { Q_OBJECT - public: /** - * Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type + * Constructs a QgsDataSourceSelectWidget, optionally filtering by layer type * * \param browserModel an existing browser model (typically from app), if NULLPTR an instance will be created * \param setFilterByLayerType activates filtering by layer type * \param layerType sets the layer type filter, this is in effect only if filtering by layer type is also active * \param parent the object */ - QgsDataSourceSelectDialog( QgsBrowserGuiModel *browserModel = nullptr, + QgsDataSourceSelectWidget( QgsBrowserGuiModel *browserModel = nullptr, bool setFilterByLayerType = false, QgsMapLayerType layerType = QgsMapLayerType::VectorLayer, QWidget *parent = nullptr ); - ~QgsDataSourceSelectDialog() override; + ~QgsDataSourceSelectWidget() override; /** * Sets layer type filter to \a layerType and activates the filtering @@ -93,6 +93,25 @@ class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog, private Ui::QgsDataS //! Scroll to last selected index and expand it's children void showEvent( QShowEvent *e ) override; + signals: + + /** + * This signal is emitted whenever the validation status of the widget changes. + * + * \param isValid TRUE if the current status of the widget is valid + */ + void validationChanged( bool isValid ); + + /** + * Emitted when the current selection changes in the widget. + */ + void selectionChanged(); + + /** + * Emitted when an item is triggered, e.g. via a double-click. + */ + void itemTriggered( const QgsMimeDataUtils::Uri &uri ); + private slots: //! Triggered when a layer is selected in the browser @@ -105,10 +124,80 @@ class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog, private Ui::QgsDataS //! Refresh the model void refreshModel( const QModelIndex &index ); + void setValid( bool valid ); + QgsBrowserProxyModel mBrowserProxyModel; QgsBrowserGuiModel *mBrowserModel = nullptr; QgsMimeDataUtils::Uri mUri; QLabel *mDescriptionLabel = nullptr; + bool mIsValid = true; +}; + + +/** + * \ingroup gui + * The QgsDataSourceSelectDialog class embeds the browser view to + * select an existing data source. + * + * By default any layer type can be chosen, the valid layer + * type can be restricted by setting a layer type filter with + * setLayerTypeFilter(layerType) or by activating the filter + * directly from the constructor. + * + * To retrieve the selected data source, uri() can be called and it + * will return a (possibly invalid) QgsMimeDataUtils::Uri. + * + * \since QGIS 3.6 + */ +class GUI_EXPORT QgsDataSourceSelectDialog: public QDialog +{ + Q_OBJECT + + public: + + /** + * Constructs a QgsDataSourceSelectDialog, optionally filtering by layer type + * + * \param browserModel an existing browser model (typically from app), if NULLPTR an instance will be created + * \param setFilterByLayerType activates filtering by layer type + * \param layerType sets the layer type filter, this is in effect only if filtering by layer type is also active + * \param parent the object + */ + QgsDataSourceSelectDialog( QgsBrowserGuiModel *browserModel = nullptr, + bool setFilterByLayerType = false, + QgsMapLayerType layerType = QgsMapLayerType::VectorLayer, + QWidget *parent = nullptr ); + + /** + * Sets layer type filter to \a layerType and activates the filtering + */ + void setLayerTypeFilter( QgsMapLayerType layerType ); + + /** + * Sets a description label + * \param description a description string + * \note the description will be displayed at the bottom of the dialog + * \since 3.8 + */ + void setDescription( const QString &description ); + + /** + * Returns the (possibly invalid) uri of the selected data source + */ + QgsMimeDataUtils::Uri uri() const; + + //! Show/hide filter widget + void showFilterWidget( bool visible ); + //! Sets filter syntax + void setFilterSyntax( QAction * ); + //! Sets filter case sensitivity + void setCaseSensitive( bool caseSensitive ); + //! Apply filter to the model + void setFilter(); + + private: + + QgsDataSourceSelectWidget *mWidget = nullptr; }; diff --git a/src/gui/qgsexpressionbuilderdialog.cpp b/src/gui/qgsexpressionbuilderdialog.cpp index c641ada0cce4..6926c6dc1129 100644 --- a/src/gui/qgsexpressionbuilderdialog.cpp +++ b/src/gui/qgsexpressionbuilderdialog.cpp @@ -31,9 +31,8 @@ QgsExpressionBuilderDialog::QgsExpressionBuilderDialog( QgsVectorLayer *layer, c builder->setExpressionContext( context ); builder->setLayer( layer ); builder->setExpressionText( startText ); - builder->loadFieldNames(); - builder->loadRecent( mRecentKey ); - builder->loadUserExpressions( ); + builder->expressionTree()->loadRecent( mRecentKey ); + builder->expressionTree()->loadUserExpressions( ); connect( buttonBox, &QDialogButtonBox::helpRequested, this, &QgsExpressionBuilderDialog::showHelp ); } @@ -80,7 +79,7 @@ void QgsExpressionBuilderDialog::done( int r ) void QgsExpressionBuilderDialog::accept() { - builder->saveToRecent( mRecentKey ); + builder->expressionTree()->saveToRecent( builder->expressionText(), mRecentKey ); QDialog::accept(); } diff --git a/src/gui/qgsexpressionbuilderwidget.cpp b/src/gui/qgsexpressionbuilderwidget.cpp index c7163f344e1b..c24add5e8241 100644 --- a/src/gui/qgsexpressionbuilderwidget.cpp +++ b/src/gui/qgsexpressionbuilderwidget.cpp @@ -13,6 +13,23 @@ * * ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + #include "qgsexpressionbuilderwidget.h" #include "qgslogger.h" #include "qgsexpression.h" @@ -27,22 +44,32 @@ #include "qgsvectorlayer.h" #include "qgssettings.h" #include "qgsproject.h" -#include "qgsrelationmanager.h" #include "qgsrelation.h" #include "qgsexpressioncontextutils.h" #include "qgsfieldformatterregistry.h" #include "qgsfieldformatter.h" #include "qgsexpressionstoredialog.h" +#include "qgsexpressiontreeview.h" + + + +bool formatterCanProvideAvailableValues( QgsVectorLayer *layer, const QString &fieldName ) +{ + if ( layer ) + { + const QgsFields fields = layer->fields(); + int fieldIndex = fields.lookupField( fieldName ); + if ( fieldIndex != -1 ) + { + const QgsEditorWidgetSetup setup = fields.at( fieldIndex ).editorWidgetSetup(); + const QgsFieldFormatter *formatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() ); + + return ( formatter->flags() & QgsFieldFormatter::CanProvideAvailableValues ); + } + } + return false; +} -#include -#include -#include -#include -#include -#include -#include -#include -#include QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) : QWidget( parent ) @@ -53,44 +80,41 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) connect( btnRun, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnRun_pressed ); connect( btnNewFile, &QToolButton::pressed, this, &QgsExpressionBuilderWidget::btnNewFile_pressed ); connect( cmbFileNames, &QListWidget::currentItemChanged, this, &QgsExpressionBuilderWidget::cmbFileNames_currentItemChanged ); - connect( expressionTree, &QTreeView::doubleClicked, this, &QgsExpressionBuilderWidget::expressionTree_doubleClicked ); connect( txtExpressionString, &QgsCodeEditorExpression::textChanged, this, &QgsExpressionBuilderWidget::txtExpressionString_textChanged ); connect( txtPython, &QgsCodeEditorPython::textChanged, this, &QgsExpressionBuilderWidget::txtPython_textChanged ); connect( txtSearchEditValues, &QgsFilterLineEdit::textChanged, this, &QgsExpressionBuilderWidget::txtSearchEditValues_textChanged ); - connect( txtSearchEdit, &QgsFilterLineEdit::textChanged, this, &QgsExpressionBuilderWidget::txtSearchEdit_textChanged ); connect( lblPreview, &QLabel::linkActivated, this, &QgsExpressionBuilderWidget::lblPreview_linkActivated ); connect( mValuesListView, &QListView::doubleClicked, this, &QgsExpressionBuilderWidget::mValuesListView_doubleClicked ); connect( btnSaveExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::storeCurrentUserExpression ); + connect( btnEditExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::editSelectedUserExpression ); connect( btnRemoveExpression, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::removeSelectedUserExpression ); + connect( btnImportExpressions, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::importUserExpressions_pressed ); + connect( btnExportExpressions, &QPushButton::pressed, this, &QgsExpressionBuilderWidget::exportUserExpressions_pressed ); connect( btnClearEditor, &QPushButton::pressed, txtExpressionString, &QgsCodeEditorExpression::clear ); + connect( txtSearchEdit, &QgsFilterLineEdit::textChanged, mExpressionTreeView, &QgsExpressionTreeView::setSearchText ); + connect( mExpressionTreeView, &QgsExpressionTreeView::expressionItemDoubleClicked, this, &QgsExpressionBuilderWidget::insertExpressionText ); + connect( mExpressionTreeView, &QgsExpressionTreeView::currentExpressionItemChanged, this, &QgsExpressionBuilderWidget::expressionTreeItemChanged ); + + mExpressionTreeMenuProvider = new ExpressionTreeMenuProvider( this ); + mExpressionTreeView->setMenuProvider( mExpressionTreeMenuProvider ); + txtHelpText->setOpenExternalLinks( true ); mValueGroupBox->hide(); // highlighter = new QgsExpressionHighlighter( txtExpressionString->document() ); - mModel = qgis::make_unique(); - mProxyModel = qgis::make_unique(); - mProxyModel->setDynamicSortFilter( true ); - mProxyModel->setSourceModel( mModel.get() ); - expressionTree->setModel( mProxyModel.get() ); - expressionTree->setSortingEnabled( true ); - expressionTree->sortByColumn( 0, Qt::AscendingOrder ); - - expressionTree->setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection ); - // Note: must be in sync with the json help file for UserGroup mUserExpressionsGroupName = QgsExpression::group( QStringLiteral( "UserGroup" ) ); // Set icons for tool buttons btnSaveExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFileSave.svg" ) ) ); + btnEditExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionToggleEditing.svg" ) ) ); btnRemoveExpression->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionDeleteSelected.svg" ) ) ); + btnExportExpressions->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionSharingExport.svg" ) ) ); + btnImportExpressions->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionSharingImport.svg" ) ) ); btnClearEditor->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "mActionFileNew.svg" ) ) ); - expressionTree->setContextMenuPolicy( Qt::CustomContextMenu ); connect( this, &QgsExpressionBuilderWidget::expressionParsed, this, &QgsExpressionBuilderWidget::setExpressionState ); - connect( expressionTree, &QWidget::customContextMenuRequested, this, &QgsExpressionBuilderWidget::showContextMenu ); - connect( expressionTree->selectionModel(), &QItemSelectionModel::currentChanged, - this, &QgsExpressionBuilderWidget::currentChanged ); connect( btnLoadAll, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::loadAllValues ); connect( btnLoadSample, &QAbstractButton::pressed, this, &QgsExpressionBuilderWidget::loadSampleValues ); @@ -135,9 +159,6 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) txtExpressionString->setFoldingVisible( false ); - updateFunctionTree(); - loadUserExpressions(); - if ( QgsPythonRunner::isValid() ) { QgsPythonRunner::eval( QStringLiteral( "qgis.user.expressionspath" ), mFunctionsPath ); @@ -148,11 +169,6 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) tab_2->hide(); } - // select the first item in the function list - // in order to avoid a blank help widget - QModelIndex firstItem = mProxyModel->index( 0, 0, QModelIndex() ); - expressionTree->setCurrentIndex( firstItem ); - txtExpressionString->setWrapMode( QsciScintilla::WrapWord ); lblAutoSave->clear(); @@ -229,34 +245,66 @@ QgsExpressionBuilderWidget::~QgsExpressionBuilderWidget() settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/splitter" ), splitter->saveState() ); settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/editorsplitter" ), editorSplit->saveState() ); settings.setValue( QStringLiteral( "Windows/QgsExpressionBuilderWidget/functionsplitter" ), functionsplit->saveState() ); + delete mExpressionTreeMenuProvider; } +void QgsExpressionBuilderWidget::init( const QgsExpressionContext &context, const QString &recentCollection, const Flags &flags ) +{ + setExpressionContext( context ); + + if ( flags.testFlag( LoadRecent ) ) + mExpressionTreeView->loadRecent( recentCollection ); + + if ( flags.testFlag( LoadUserExpressions ) ) + mExpressionTreeView->loadUserExpressions(); +} + +void QgsExpressionBuilderWidget::initWithLayer( QgsVectorLayer *layer, const QgsExpressionContext &context, const QString &recentCollection, const Flags &flags ) +{ + init( context, recentCollection, flags ); + setLayer( layer ); +} + +void QgsExpressionBuilderWidget::initWithFields( const QgsFields &fields, const QgsExpressionContext &context, const QString &recentCollection, const Flags &flags ) +{ + init( context, recentCollection, flags ); + mExpressionTreeView->loadFieldNames( fields ); +} + + void QgsExpressionBuilderWidget::setLayer( QgsVectorLayer *layer ) { mLayer = layer; + mExpressionTreeView->setLayer( mLayer ); //TODO - remove existing layer scope from context if ( mLayer ) + { mExpressionContext << QgsExpressionContextUtils::layerScope( mLayer ); + + txtExpressionString->setFields( mLayer->fields() ); + } } -void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QModelIndex & ) +QgsVectorLayer *QgsExpressionBuilderWidget::layer() const +{ + return mLayer; +} + +void QgsExpressionBuilderWidget::expressionTreeItemChanged( QgsExpressionItem *item ) { txtSearchEditValues->clear(); - // Get the item - QModelIndex idx = mProxyModel->mapToSource( index ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); if ( !item ) return; bool isField = mLayer && item->getItemType() == QgsExpressionItem::Field; if ( isField ) { - loadFieldValues( mFieldValues.value( item->text() ) ); + mValuesModel->clear(); - cbxValuesInUse->setVisible( formatterCanProvideAvailableValues( item->text() ) ); + cbxValuesInUse->setVisible( formatterCanProvideAvailableValues( mLayer, item->text() ) ); cbxValuesInUse->setChecked( false ); } mValueGroupBox->setVisible( isField ); @@ -267,9 +315,10 @@ void QgsExpressionBuilderWidget::currentChanged( const QModelIndex &index, const QString help = loadFunctionHelp( item ); txtHelpText->setText( help ); - btnRemoveExpression->setEnabled( item->parent() && - item->parent()->text() == mUserExpressionsGroupName ); + bool isUserExpression = item->parent() && item->parent()->text() == mUserExpressionsGroupName; + btnRemoveExpression->setEnabled( isUserExpression ); + btnEditExpression->setEnabled( isUserExpression ); } void QgsExpressionBuilderWidget::btnRun_pressed() @@ -289,10 +338,7 @@ void QgsExpressionBuilderWidget::runPythonCode( const QString &code ) QString pythontext = code; QgsPythonRunner::run( pythontext ); } - updateFunctionTree(); - loadFieldNames(); - loadRecent( mRecentKey ); - loadUserExpressions( ); + mExpressionTreeView->refresh(); } void QgsExpressionBuilderWidget::saveFunctionFile( QString fileName ) @@ -342,8 +388,8 @@ void QgsExpressionBuilderWidget::updateFunctionFileList( const QString &path ) { // Create default sample entry. newFunctionFile( "default" ); - txtPython->setText( QString( "'''\n#Sample custom function file\n " - "(uncomment to use and customize or Add button to create a new file) \n%1 \n '''" ).arg( txtPython->text() ) ); + txtPython->setText( QStringLiteral( "'''\n#Sample custom function file\n " + "(uncomment to use and customize or Add button to create a new file) \n%1 \n '''" ).arg( txtPython->text() ) ); saveFunctionFile( "default" ); } } @@ -400,76 +446,17 @@ void QgsExpressionBuilderWidget::loadFunctionCode( const QString &code ) txtPython->setText( code ); } -void QgsExpressionBuilderWidget::expressionTree_doubleClicked( const QModelIndex &index ) +void QgsExpressionBuilderWidget::insertExpressionText( const QString &text ) { - QModelIndex idx = mProxyModel->mapToSource( index ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); - if ( !item ) - return; - - // Don't handle the double-click if we are on a header node. - if ( item->getItemType() == QgsExpressionItem::Header ) - return; - // Insert the expression text or replace selected text - txtExpressionString->insertText( item->getExpressionText() ); + txtExpressionString->insertText( text ); txtExpressionString->setFocus(); } -void QgsExpressionBuilderWidget::loadFieldNames() -{ - // TODO We should really return a error the user of the widget that - // the there is no layer set. - if ( !mLayer ) - return; - - loadFieldNames( mLayer->fields() ); -} - - -void QgsExpressionBuilderWidget::loadFieldNames( const QgsFields &fields ) -{ - if ( fields.isEmpty() ) - return; - - txtExpressionString->setFields( fields ); - - for ( int i = 0; i < fields.count(); ++i ) - { - const QgsField field = fields.at( i ); - QIcon icon = fields.iconForField( i ); - registerItem( QStringLiteral( "Fields and Values" ), field.displayNameWithAlias(), - " \"" + field.name() + "\" ", QString(), QgsExpressionItem::Field, false, i, icon ); - } -} - void QgsExpressionBuilderWidget::loadFieldsAndValues( const QMap &fieldValues ) { - mFieldValues.clear(); - QgsFields fields; - for ( auto it = fieldValues.constBegin(); it != fieldValues.constEnd(); ++it ) - { - fields.append( QgsField( it.key() ) ); - const QStringList values = it.value(); - QVariantMap map; - for ( const QString &value : values ) - { - map.insert( value, value ); - } - mFieldValues.insert( it.key(), map ); - } - loadFieldNames( fields ); -} - -void QgsExpressionBuilderWidget::loadFieldsAndValues( const QMap &fieldValues ) -{ - QgsFields fields; - for ( auto it = fieldValues.constBegin(); it != fieldValues.constEnd(); ++it ) - { - fields.append( QgsField( it.key() ) ); - } - loadFieldNames( fields ); - mFieldValues = fieldValues; + Q_UNUSED( fieldValues ) + // This is not maintained and setLayer() should be used instead. } void QgsExpressionBuilderWidget::fillFieldValues( const QString &fieldName, int countLimit, bool forceUsedValues ) @@ -524,20 +511,6 @@ void QgsExpressionBuilderWidget::fillFieldValues( const QString &fieldName, int } } -bool QgsExpressionBuilderWidget::formatterCanProvideAvailableValues( const QString &fieldName ) -{ - const QgsFields fields = mLayer->fields(); - int fieldIndex = fields.lookupField( fieldName ); - if ( fieldIndex != -1 ) - { - const QgsEditorWidgetSetup setup = fields.at( fieldIndex ).editorWidgetSetup(); - const QgsFieldFormatter *formatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() ); - - return ( formatter->flags() & QgsFieldFormatter::CanProvideAvailableValues ); - } - return false; -} - QString QgsExpressionBuilderWidget::getFunctionHelp( QgsExpressionFunction *function ) { if ( !function ) @@ -549,50 +522,7 @@ QString QgsExpressionBuilderWidget::getFunctionHelp( QgsExpressionFunction *func } -void QgsExpressionBuilderWidget::registerItem( const QString &group, - const QString &label, - const QString &expressionText, - const QString &helpText, - QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, QIcon icon, const QStringList &tags ) -{ - QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type ); - item->setData( label, Qt::UserRole ); - item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE ); - item->setData( tags, QgsExpressionItem::SEARCH_TAGS_ROLE ); - item->setIcon( icon ); - - // Look up the group and insert the new function. - if ( mExpressionGroups.contains( group ) ) - { - QgsExpressionItem *groupNode = mExpressionGroups.value( group ); - groupNode->appendRow( item ); - } - else - { - // If the group doesn't exist yet we make it first. - QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), QString(), QgsExpressionItem::Header ); - newgroupNode->setData( group, Qt::UserRole ); - //Recent group should always be last group - newgroupNode->setData( group.startsWith( QLatin1String( "Recent (" ) ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE ); - newgroupNode->appendRow( item ); - newgroupNode->setBackground( QBrush( QColor( 238, 238, 238 ) ) ); - mModel->appendRow( newgroupNode ); - mExpressionGroups.insert( group, newgroupNode ); - } - - if ( highlightedItem ) - { - //insert a copy as a top level item - QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type ); - topLevelItem->setData( label, Qt::UserRole ); - item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE ); - QFont font = topLevelItem->font(); - font.setBold( true ); - topLevelItem->setFont( font ); - mModel->appendRow( topLevelItem ); - } -} bool QgsExpressionBuilderWidget::isExpressionValid() { @@ -601,189 +531,35 @@ bool QgsExpressionBuilderWidget::isExpressionValid() void QgsExpressionBuilderWidget::saveToRecent( const QString &collection ) { - QgsSettings settings; - QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection ); - QStringList expressions = settings.value( location ).toStringList(); - expressions.removeAll( this->expressionText() ); - - expressions.prepend( this->expressionText() ); - - while ( expressions.count() > 20 ) - { - expressions.pop_back(); - } - - settings.setValue( location, expressions ); - loadRecent( collection ); + mExpressionTreeView->saveToRecent( expressionText(), collection ); } void QgsExpressionBuilderWidget::loadRecent( const QString &collection ) { - mRecentKey = collection; - QString name = tr( "Recent (%1)" ).arg( collection ); - if ( mExpressionGroups.contains( name ) ) - { - QgsExpressionItem *node = mExpressionGroups.value( name ); - node->removeRows( 0, node->rowCount() ); - } + mExpressionTreeView->loadRecent( collection ); +} - QgsSettings settings; - const QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection ); - const QStringList expressions = settings.value( location ).toStringList(); - int i = 0; - for ( const QString &expression : expressions ) - { - QString help = formatRecentExpressionHelp( expression, expression ); - registerItem( name, expression, expression, help, QgsExpressionItem::ExpressionNode, false, i ); - i++; - } +QgsExpressionTreeView *QgsExpressionBuilderWidget::expressionTree() const +{ + return mExpressionTreeView; } +// this is potentially very slow if there are thousands of user expressions, every time entire cleanup and load void QgsExpressionBuilderWidget::loadUserExpressions( ) { - // Cleanup - if ( mExpressionGroups.contains( QStringLiteral( "UserGroup" ) ) ) - { - QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "UserGroup" ) ); - node->removeRows( 0, node->rowCount() ); - } - - QgsSettings settings; - const QString location = QStringLiteral( "user" ); - settings.beginGroup( location, QgsSettings::Section::Expressions ); - QString label; - QString helpText; - QString expression; - int i = 0; - mUserExpressionLabels = settings.childGroups(); - for ( const auto &label : qgis::as_const( mUserExpressionLabels ) ) - { - settings.beginGroup( label ); - expression = settings.value( QStringLiteral( "expression" ) ).toString(); - helpText = formatUserExpressionHelp( label, expression, settings.value( QStringLiteral( "helpText" ) ).toString() ); - registerItem( QStringLiteral( "UserGroup" ), label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ ); - settings.endGroup(); - } + mExpressionTreeView->loadUserExpressions(); } void QgsExpressionBuilderWidget::saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ) { - QgsSettings settings; - const QString location = QStringLiteral( "user" ); - settings.beginGroup( location, QgsSettings::Section::Expressions ); - settings.beginGroup( label ); - settings.setValue( QStringLiteral( "expression" ), expression ); - settings.setValue( QStringLiteral( "helpText" ), helpText ); - loadUserExpressions( ); - // Scroll - const QModelIndexList idxs { expressionTree->model()->match( expressionTree->model()->index( 0, 0 ), - Qt::DisplayRole, label, 1, - Qt::MatchFlag::MatchRecursive ) }; - if ( ! idxs.isEmpty() ) - { - expressionTree->scrollTo( idxs.first() ); - } + mExpressionTreeView->saveToUserExpressions( label, expression, helpText ); } void QgsExpressionBuilderWidget::removeFromUserExpressions( const QString &label ) { - QgsSettings settings; - settings.remove( QStringLiteral( "user/%1" ).arg( label ), QgsSettings::Section::Expressions ); - loadUserExpressions( ); -} - -void QgsExpressionBuilderWidget::loadLayers() -{ - if ( !mProject ) - return; - - QMap layers = mProject->mapLayers(); - QMap::const_iterator layerIt = layers.constBegin(); - for ( ; layerIt != layers.constEnd(); ++layerIt ) - { - registerItemForAllGroups( QStringList() << tr( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ) ); - } -} - -void QgsExpressionBuilderWidget::loadRelations() -{ - if ( !mProject ) - return; - - QMap relations = mProject->relationManager()->relations(); - QMap::const_iterator relIt = relations.constBegin(); - for ( ; relIt != relations.constEnd(); ++relIt ) - { - registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) ); - } + mExpressionTreeView->removeFromUserExpressions( label ); } -void QgsExpressionBuilderWidget::updateFunctionTree() -{ - mModel->clear(); - mExpressionGroups.clear(); - // TODO Can we move this stuff to QgsExpression, like the functions? - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "+" ), QStringLiteral( " + " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "-" ), QStringLiteral( " - " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "*" ), QStringLiteral( " * " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "/" ), QStringLiteral( " / " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "%" ), QStringLiteral( " % " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "^" ), QStringLiteral( " ^ " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "=" ), QStringLiteral( " = " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "~" ), QStringLiteral( " ~ " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">" ), QStringLiteral( " > " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<" ), QStringLiteral( " < " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<>" ), QStringLiteral( " <> " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<=" ), QStringLiteral( " <= " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">=" ), QStringLiteral( " >= " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "[]" ), QStringLiteral( "[ ]" ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "||" ), QStringLiteral( " || " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "ILIKE" ), QStringLiteral( " ILIKE " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS" ), QStringLiteral( " IS " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "OR" ), QStringLiteral( " OR " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "AND" ), QStringLiteral( " AND " ) ); - registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT" ), QStringLiteral( " NOT " ) ); - - QString casestring = QStringLiteral( "CASE WHEN condition THEN result END" ); - registerItem( QStringLiteral( "Conditionals" ), QStringLiteral( "CASE" ), casestring ); - - // use -1 as sort order here -- NULL should always show before the field list - registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 ); - - // Load the functions from the QgsExpression class - int count = QgsExpression::functionCount(); - for ( int i = 0; i < count; i++ ) - { - QgsExpressionFunction *func = QgsExpression::Functions()[i]; - QString name = func->name(); - if ( name.startsWith( '_' ) ) // do not display private functions - continue; - if ( func->isDeprecated() ) // don't show deprecated functions - continue; - if ( func->isContextual() ) - { - //don't show contextual functions by default - it's up the the QgsExpressionContext - //object to provide them if supported - continue; - } - if ( func->params() != 0 ) - name += '('; - else if ( !name.startsWith( '$' ) ) - name += QLatin1String( "()" ); - // this is where the functions are being registered, including functions under "Custom" - registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) ); - } - - // load relation names - loadRelations(); - - // load layer IDs - loadLayers(); - - loadExpressionContext(); -} void QgsExpressionBuilderWidget::setGeomCalculator( const QgsDistanceArea &da ) { @@ -814,10 +590,8 @@ void QgsExpressionBuilderWidget::setExpectedOutputFormat( const QString &expecte void QgsExpressionBuilderWidget::setExpressionContext( const QgsExpressionContext &context ) { mExpressionContext = context; - updateFunctionTree(); - loadFieldNames(); - loadRecent( mRecentKey ); - loadUserExpressions( ); + txtExpressionString->setExpressionContext( mExpressionContext ); + mExpressionTreeView->setExpressionContext( context ); } void QgsExpressionBuilderWidget::txtExpressionString_textChanged() @@ -900,108 +674,6 @@ void QgsExpressionBuilderWidget::txtExpressionString_textChanged() } -void QgsExpressionBuilderWidget::loadExpressionContext() -{ - txtExpressionString->setExpressionContext( mExpressionContext ); - QStringList variableNames = mExpressionContext.filteredVariableNames(); - const auto constVariableNames = variableNames; - for ( const QString &variable : constVariableNames ) - { - registerItem( QStringLiteral( "Variables" ), variable, " @" + variable + ' ', - formatVariableHelp( variable, mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ), - QgsExpressionItem::ExpressionNode, - mExpressionContext.isHighlightedVariable( variable ) ); - } - - // Load the functions from the expression context - QStringList contextFunctions = mExpressionContext.functionNames(); - const auto constContextFunctions = contextFunctions; - for ( const QString &functionName : constContextFunctions ) - { - QgsExpressionFunction *func = mExpressionContext.function( functionName ); - QString name = func->name(); - if ( name.startsWith( '_' ) ) // do not display private functions - continue; - if ( func->params() != 0 ) - name += '('; - registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) ); - } -} - -void QgsExpressionBuilderWidget::registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QStringList &tags ) -{ - const auto constGroups = groups; - for ( const QString &group : constGroups ) - { - registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder, QIcon(), tags ); - } -} - -QString QgsExpressionBuilderWidget::formatRelationHelp( const QgsRelation &relation ) const -{ - QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) - .arg( QCoreApplication::translate( "relation_help", "relation %1" ).arg( relation.name() ), - tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) ); - - text += QStringLiteral( "

    %1

    %2
    " ) - .arg( tr( "Current value" ), relation.id() ); - - return text; -} - -QString QgsExpressionBuilderWidget::formatLayerHelp( const QgsMapLayer *layer ) const -{ - QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) - .arg( QCoreApplication::translate( "layer_help", "map layer %1" ).arg( layer->name() ), - tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) ); - - text += QStringLiteral( "

    %1

    %2
    " ) - .arg( tr( "Current value" ), layer->id() ); - - return text; -} - -QString QgsExpressionBuilderWidget::formatRecentExpressionHelp( const QString &label, const QString &expression ) const -{ - QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) - .arg( QCoreApplication::translate( "recent_expression_help", "expression %1" ).arg( label ), - QCoreApplication::translate( "recent_expression_help", "Recently used expression." ) ); - - text += QStringLiteral( "

    %1

    %2
    " ) - .arg( tr( "Expression" ), expression ); - - return text; -} - -QString QgsExpressionBuilderWidget::formatUserExpressionHelp( const QString &label, const QString &expression, const QString &description ) const -{ - QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) - .arg( QCoreApplication::translate( "user_expression_help", "expression %1" ).arg( label ), description ); - - text += QStringLiteral( "

    %1

    %2
    " ) - .arg( tr( "Expression" ), expression ); - - return text; -} - -QString QgsExpressionBuilderWidget::formatVariableHelp( const QString &variable, const QString &description, bool showValue, const QVariant &value ) const -{ - QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) - .arg( QCoreApplication::translate( "variable_help", "variable %1" ).arg( variable ), description ); - - if ( showValue ) - { - QString valueString = !value.isValid() - ? QCoreApplication::translate( "variable_help", "not set" ) - : QStringLiteral( "
    %1
    " ).arg( QgsExpression::formatPreviewString( value ) ); - - text += QStringLiteral( "

    %1

    %2

    " ) - .arg( tr( "Current value" ), valueString ); - } - - return text; -} - bool QgsExpressionBuilderWidget::parserError() const { return mParserError; @@ -1016,17 +688,6 @@ void QgsExpressionBuilderWidget::setParserError( bool parserError ) emit parserErrorChanged(); } -void QgsExpressionBuilderWidget::loadFieldValues( const QVariantMap &values ) -{ - mValuesModel->clear(); - for ( QVariantMap::ConstIterator it = values.constBegin(); it != values.constEnd(); ++ it ) - { - QStandardItem *item = new QStandardItem( it.key() ); - item->setData( it.value() ); - mValuesModel->appendRow( item ); - } -} - bool QgsExpressionBuilderWidget::evalError() const { return mEvalError; @@ -1043,7 +704,9 @@ void QgsExpressionBuilderWidget::setEvalError( bool evalError ) QStandardItemModel *QgsExpressionBuilderWidget::model() { - return mModel.get(); + Q_NOWARN_DEPRECATED_PUSH + return mExpressionTreeView->model(); + Q_NOWARN_DEPRECATED_POP } QgsProject *QgsExpressionBuilderWidget::project() @@ -1054,7 +717,7 @@ QgsProject *QgsExpressionBuilderWidget::project() void QgsExpressionBuilderWidget::setProject( QgsProject *project ) { mProject = project; - updateFunctionTree(); + mExpressionTreeView->setProject( project ); } void QgsExpressionBuilderWidget::showEvent( QShowEvent *e ) @@ -1182,25 +845,6 @@ void QgsExpressionBuilderWidget::clearErrors() txtExpressionString->clearIndicatorRange( 0, 0, lastLine, txtExpressionString->text( lastLine ).length(), QgsExpression::ParserError::FunctionNamedArgsError ); } -void QgsExpressionBuilderWidget::txtSearchEdit_textChanged() -{ - mProxyModel->setFilterWildcard( txtSearchEdit->text() ); - if ( txtSearchEdit->text().isEmpty() ) - { - expressionTree->collapseAll(); - } - else - { - expressionTree->expandAll(); - QModelIndex index = mProxyModel->index( 0, 0 ); - if ( mProxyModel->hasChildren( index ) ) - { - QModelIndex child = mProxyModel->index( 0, 0, index ); - expressionTree->selectionModel()->setCurrentIndex( child, QItemSelectionModel::ClearAndSelect ); - } - } -} - void QgsExpressionBuilderWidget::txtSearchEditValues_textChanged() { mProxyValues->setFilterCaseSensitivity( Qt::CaseInsensitive ); @@ -1232,33 +876,9 @@ void QgsExpressionBuilderWidget::operatorButtonClicked() txtExpressionString->setFocus(); } -void QgsExpressionBuilderWidget::showContextMenu( QPoint pt ) -{ - QModelIndex idx = expressionTree->indexAt( pt ); - idx = mProxyModel->mapToSource( idx ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); - if ( !item ) - return; - - if ( item->getItemType() == QgsExpressionItem::Field && mLayer ) - { - QMenu *menu = new QMenu( this ); - menu->addAction( tr( "Load First 10 Unique Values" ), this, SLOT( loadSampleValues() ) ); - menu->addAction( tr( "Load All Unique Values" ), this, SLOT( loadAllValues() ) ); - - if ( formatterCanProvideAvailableValues( item->text() ) ) - { - menu->addAction( tr( "Load First 10 Unique Used Values" ), this, SLOT( loadSampleUsedValues() ) ); - menu->addAction( tr( "Load All Unique Used Values" ), this, SLOT( loadAllUsedValues() ) ); - } - menu->popup( expressionTree->mapToGlobal( pt ) ); - } -} - void QgsExpressionBuilderWidget::loadSampleValues() { - QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); + QgsExpressionItem *item = mExpressionTreeView->currentItem(); // TODO We should really return a error the user of the widget that // the there is no layer set. if ( !mLayer || !item ) @@ -1270,8 +890,7 @@ void QgsExpressionBuilderWidget::loadSampleValues() void QgsExpressionBuilderWidget::loadAllValues() { - QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); + QgsExpressionItem *item = mExpressionTreeView->currentItem(); // TODO We should really return a error the user of the widget that // the there is no layer set. if ( !mLayer || !item ) @@ -1283,8 +902,7 @@ void QgsExpressionBuilderWidget::loadAllValues() void QgsExpressionBuilderWidget::loadSampleUsedValues() { - QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); + QgsExpressionItem *item = mExpressionTreeView->currentItem(); // TODO We should really return a error the user of the widget that // the there is no layer set. if ( !mLayer || !item ) @@ -1296,8 +914,7 @@ void QgsExpressionBuilderWidget::loadSampleUsedValues() void QgsExpressionBuilderWidget::loadAllUsedValues() { - QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); + QgsExpressionItem *item = mExpressionTreeView->currentItem(); // TODO We should really return a error the user of the widget that // the there is no layer set. if ( !mLayer || !item ) @@ -1342,19 +959,41 @@ void QgsExpressionBuilderWidget::autosave() void QgsExpressionBuilderWidget::storeCurrentUserExpression() { const QString expression { this->expressionText() }; - QgsExpressionStoreDialog dlg { expression, expression, QString( ), mUserExpressionLabels }; + QgsExpressionStoreDialog dlg { expression, expression, QString( ), mExpressionTreeView->userExpressionLabels() }; if ( dlg.exec() == QDialog::DialogCode::Accepted ) { - saveToUserExpressions( dlg.label(), dlg.expression(), dlg.helpText() ); + mExpressionTreeView->saveToUserExpressions( dlg.label(), dlg.expression(), dlg.helpText() ); + } +} + +void QgsExpressionBuilderWidget::editSelectedUserExpression() +{ + // Get the item + QgsExpressionItem *item = mExpressionTreeView->currentItem(); + if ( !item ) + return; + + // Don't handle remove if we are on a header node or the parent + // is not the user group + if ( item->getItemType() == QgsExpressionItem::Header || + ( item->parent() && item->parent()->text() != mUserExpressionsGroupName ) ) + return; + + QgsSettings settings; + QString helpText = settings.value( QStringLiteral( "user/%1/helpText" ).arg( item->text() ), "", QgsSettings::Section::Expressions ).toString(); + QgsExpressionStoreDialog dlg { item->text(), item->getExpressionText(), helpText }; + + if ( dlg.exec() == QDialog::DialogCode::Accepted ) + { + mExpressionTreeView->saveToUserExpressions( dlg.label(), dlg.expression(), dlg.helpText() ); } } void QgsExpressionBuilderWidget::removeSelectedUserExpression() { + // Get the item + QgsExpressionItem *item = mExpressionTreeView->currentItem(); -// Get the item - QModelIndex idx = mProxyModel->mapToSource( expressionTree->currentIndex() ); - QgsExpressionItem *item = dynamic_cast( mModel->itemFromIndex( idx ) ); if ( !item ) return; @@ -1368,20 +1007,87 @@ void QgsExpressionBuilderWidget::removeSelectedUserExpression() tr( "Do you really want to remove stored expressions '%1'?" ).arg( item->text() ), QMessageBox::Yes | QMessageBox::No ) ) { - removeFromUserExpressions( item->text() ); + mExpressionTreeView->removeFromUserExpressions( item->text() ); } } -const QList QgsExpressionBuilderWidget::findExpressions( const QString &label ) +void QgsExpressionBuilderWidget::exportUserExpressions_pressed() { - QList result; - const QList found { mModel->findItems( label, Qt::MatchFlag::MatchRecursive ) }; - for ( const auto &item : qgis::as_const( found ) ) + QgsSettings settings; + QString lastSaveDir = settings.value( QStringLiteral( "lastExportExpressionsDir" ), QDir::homePath(), QgsSettings::App ).toString(); + QString saveFileName = QFileDialog::getSaveFileName( + this, + tr( "Export User Expressions" ), + lastSaveDir, + tr( "User expressions" ) + " (*.json)" ); + + if ( saveFileName.isEmpty() ) + return; + + QFileInfo saveFileInfo( saveFileName ); + + if ( saveFileInfo.suffix().isEmpty() ) { - result.push_back( dynamic_cast( item ) ); + QString saveFileNameWithSuffix = saveFileName.append( ".json" ); + saveFileInfo = QFileInfo( saveFileNameWithSuffix ); } - return result; + + settings.setValue( QStringLiteral( "lastExportExpressionsDir" ), saveFileInfo.absolutePath(), QgsSettings::App ); + + QJsonDocument exportJson = mExpressionTreeView->exportUserExpressions(); + QFile jsonFile( saveFileName ); + + if ( !jsonFile.open( QFile::WriteOnly | QIODevice::Truncate ) ) + QMessageBox::warning( this, tr( "Export user expressions" ), tr( "Error while creating the expressions file." ) ); + + if ( ! jsonFile.write( exportJson.toJson() ) ) + QMessageBox::warning( this, tr( "Export user expressions" ), tr( "Error while creating the expressions file." ) ); + else + jsonFile.close(); +} + +void QgsExpressionBuilderWidget::importUserExpressions_pressed() +{ + QgsSettings settings; + QString lastImportDir = settings.value( QStringLiteral( "lastImportExpressionsDir" ), QDir::homePath(), QgsSettings::App ).toString(); + QString loadFileName = QFileDialog::getOpenFileName( + this, + tr( "Import User Expressions" ), + lastImportDir, + tr( "User expressions" ) + " (*.json)" ); + + if ( loadFileName.isEmpty() ) + return; + + QFileInfo loadFileInfo( loadFileName ); + + settings.setValue( QStringLiteral( "lastImportExpressionsDir" ), loadFileInfo.absolutePath(), QgsSettings::App ); + + QFile jsonFile( loadFileName ); + + if ( !jsonFile.open( QFile::ReadOnly ) ) + QMessageBox::warning( this, tr( "Import User Expressions" ), tr( "Error while reading the expressions file." ) ); + + QTextStream jsonStream( &jsonFile ); + QString jsonString = jsonFile.readAll(); + jsonFile.close(); + + QJsonDocument importJson = QJsonDocument::fromJson( jsonString.toUtf8() ); + + if ( importJson.isNull() ) + { + QMessageBox::warning( this, tr( "Import User Expressions" ), tr( "Error while reading the expressions file." ) ); + return; + } + + mExpressionTreeView->loadExpressionsFromJson( importJson ); +} + + +const QList QgsExpressionBuilderWidget::findExpressions( const QString &label ) +{ + return mExpressionTreeView->findExpressions( label ); } void QgsExpressionBuilderWidget::indicatorClicked( int line, int index, Qt::KeyboardModifiers state ) @@ -1435,65 +1141,25 @@ QString QgsExpressionBuilderWidget::loadFunctionHelp( QgsExpressionItem *express return "" + helpContents + ""; } -QgsExpressionItemSearchProxy::QgsExpressionItemSearchProxy() -{ - setFilterCaseSensitivity( Qt::CaseInsensitive ); -} -bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const -{ - QModelIndex index = sourceModel()->index( source_row, 0, source_parent ); - QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() ); +// ************* +// Menu provider - int count = sourceModel()->rowCount( index ); - bool matchchild = false; - for ( int i = 0; i < count; ++i ) +QMenu *QgsExpressionBuilderWidget::ExpressionTreeMenuProvider::createContextMenu( QgsExpressionItem *item ) +{ + QMenu *menu = nullptr; + QgsVectorLayer *layer = mExpressionBuilderWidget->layer(); + if ( item->getItemType() == QgsExpressionItem::Field && layer ) { - if ( filterAcceptsRow( i, index ) ) - { - matchchild = true; - break; - } - } + menu = new QMenu( mExpressionBuilderWidget ); + menu->addAction( tr( "Load First 10 Unique Values" ), mExpressionBuilderWidget, &QgsExpressionBuilderWidget::loadSampleValues ); + menu->addAction( tr( "Load All Unique Values" ), mExpressionBuilderWidget, &QgsExpressionBuilderWidget::loadAllValues ); - if ( itemType == QgsExpressionItem::Header && matchchild ) - return true; - - if ( itemType == QgsExpressionItem::Header ) - return false; - - // check match of item label or tags - if ( QSortFilterProxyModel::filterAcceptsRow( source_row, source_parent ) ) - { - return true; - } - else - { - const QStringList tags = sourceModel()->data( index, QgsExpressionItem::SEARCH_TAGS_ROLE ).toStringList(); - for ( const QString &tag : tags ) + if ( formatterCanProvideAvailableValues( layer, item->text() ) ) { - if ( tag.contains( filterRegExp() ) ) - return true; + menu->addAction( tr( "Load First 10 Unique Used Values" ), mExpressionBuilderWidget, SLOT( loadSampleUsedValues() ) ); + menu->addAction( tr( "Load All Unique Used Values" ), mExpressionBuilderWidget, &QgsExpressionBuilderWidget::loadAllUsedValues ); } } - return false; -} - -bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const -{ - int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt(); - int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt(); - if ( leftSort != rightSort ) - return leftSort < rightSort; - - QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString(); - QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString(); - - //ignore $ prefixes when sorting - if ( leftString.startsWith( '$' ) ) - leftString = leftString.mid( 1 ); - if ( rightString.startsWith( '$' ) ) - rightString = rightString.mid( 1 ); - - return QString::localeAwareCompare( leftString, rightString ) < 0; + return menu; } diff --git a/src/gui/qgsexpressionbuilderwidget.h b/src/gui/qgsexpressionbuilderwidget.h index 6789d41c5468..91e98591f7e0 100644 --- a/src/gui/qgsexpressionbuilderwidget.h +++ b/src/gui/qgsexpressionbuilderwidget.h @@ -17,152 +17,96 @@ #define QGSEXPRESSIONBUILDER_H #include -#include "qgis_sip.h" +#include +#include + #include "ui_qgsexpressionbuilder.h" + +#include "qgis_sip.h" +#include "qgis_gui.h" #include "qgsdistancearea.h" #include "qgsexpressioncontext.h" #include "qgsexpression.h" +#include "qgsexpressiontreeview.h" -#include "QStandardItemModel" -#include "QStandardItem" -#include "QSortFilterProxyModel" -#include "QStringListModel" -#include "qgis_gui.h" class QgsFields; class QgsExpressionHighlighter; class QgsRelation; + /** * \ingroup gui - * An expression item that can be used in the QgsExpressionBuilderWidget tree. + * A reusable widget that can be used to build a expression string. + * See QgsExpressionBuilderDialog for example of usage. */ -class GUI_EXPORT QgsExpressionItem : public QStandardItem +class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExpressionBuilderWidgetBase { + Q_OBJECT public: - enum ItemType - { - Header, - Field, - ExpressionNode - }; - QgsExpressionItem( const QString &label, - const QString &expressionText, - const QString &helpText, - QgsExpressionItem::ItemType itemType = ExpressionNode ) - : QStandardItem( label ) - { - mExpressionText = expressionText; - mHelpText = helpText; - mType = itemType; - setData( itemType, ITEM_TYPE_ROLE ); - } - - QgsExpressionItem( const QString &label, - const QString &expressionText, - QgsExpressionItem::ItemType itemType = ExpressionNode ) - : QStandardItem( label ) + /** + * Flag to determine what should be loaded + * \since QGIS 3.14 + */ + enum Flag { - mExpressionText = expressionText; - mType = itemType; - setData( itemType, ITEM_TYPE_ROLE ); - } + LoadNothing = 0, //!< Do not load anything + LoadRecent = 1 << 1, //!< Load recent expressions given the collection key + LoadUserExpressions = 1 << 2, //!< Load user expressions + LoadAll = LoadRecent | LoadUserExpressions, //!< Load everything + }; + Q_DECLARE_FLAGS( Flags, Flag ) + Q_FLAG( Flag ) - QString getExpressionText() const { return mExpressionText; } /** - * Gets the help text that is associated with this expression item. - * - * \returns The help text. - */ - QString getHelpText() const { return mHelpText; } + * Create a new expression builder widget with an optional parent. + */ + QgsExpressionBuilderWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + ~QgsExpressionBuilderWidget() override; /** - * Set the help text for the current item - * - * \note The help text can be set as a html string. - */ - void setHelpText( const QString &helpText ) { mHelpText = helpText; } + * Initialize without any layer + * \since QGIS 3.14 + */ + void init( const QgsExpressionContext &context = QgsExpressionContext(), const QString &recentCollection = QStringLiteral( "generic" ), const Flags &flags = LoadAll ); /** - * Gets the type of expression item, e.g., header, field, ExpressionNode. - * - * \returns The QgsExpressionItem::ItemType - */ - QgsExpressionItem::ItemType getItemType() const { return mType; } - - //! Custom sort order role - static const int CUSTOM_SORT_ROLE = Qt::UserRole + 1; - //! Item type role - static const int ITEM_TYPE_ROLE = Qt::UserRole + 2; - //! Search tags role - static const int SEARCH_TAGS_ROLE = Qt::UserRole + 3; - - private: - QString mExpressionText; - QString mHelpText; - QgsExpressionItem::ItemType mType; - -}; - -/** - * \ingroup gui - * Search proxy used to filter the QgsExpressionBuilderWidget tree. - * The default search for a tree model only searches top level this will handle one - * level down - */ -class GUI_EXPORT QgsExpressionItemSearchProxy : public QSortFilterProxyModel -{ - Q_OBJECT - - public: - QgsExpressionItemSearchProxy(); - - bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override; - - protected: - - bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; -}; - - -/** - * \ingroup gui - * A reusable widget that can be used to build a expression string. - * See QgsExpressionBuilderDialog for example of usage. - */ -class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExpressionBuilderWidgetBase -{ - Q_OBJECT - public: + * Initialize with a layer + * \since QGIS 3.14 + */ + void initWithLayer( QgsVectorLayer *layer, const QgsExpressionContext &context = QgsExpressionContext(), const QString &recentCollection = QStringLiteral( "generic" ), const Flags &flags = LoadAll ); /** - * Create a new expression builder widget with an optional parent. + * Initialize with given fields without any layer + * \since QGIS 3.14 */ - QgsExpressionBuilderWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); - ~QgsExpressionBuilderWidget() override; + void initWithFields( const QgsFields &fields, const QgsExpressionContext &context = QgsExpressionContext(), const QString &recentCollection = QStringLiteral( "generic" ), const Flags &flags = LoadAll ); /** * Sets layer in order to get the fields and values - * \note this needs to be called before calling loadFieldNames(). - */ + * \note this needs to be called before calling loadFieldNames(). + */ void setLayer( QgsVectorLayer *layer ); /** - * Loads all the field names from the layer. - * @remarks Should this really be public couldn't we just do this for the user? - */ - void loadFieldNames(); + * Returns the current layer or a nullptr. + */ + QgsVectorLayer *layer() const; - void loadFieldNames( const QgsFields &fields ); + //! \deprecated since QGIS 3.14 this is now done automatically + Q_DECL_DEPRECATED void loadFieldNames() {} SIP_DEPRECATED + + //! \deprecated since QGIS 3.14 use expressionTree()->loadFieldNames() instead + Q_DECL_DEPRECATED void loadFieldNames( const QgsFields &fields ) {mExpressionTreeView->loadFieldNames( fields );} SIP_DEPRECATED /** * Loads field names and values from the specified map. - * \note The field values must be quoted appropriately if they are strings. * \since QGIS 2.12 + * \deprecated since QGIS 3.14 this will not do anything, use setLayer() instead */ - void loadFieldsAndValues( const QMap &fieldValues ); + Q_DECL_DEPRECATED void loadFieldsAndValues( const QMap &fieldValues ) SIP_DEPRECATED; //! Sets geometry calculator used in distance/area calculations. void setGeomCalculator( const QgsDistanceArea &da ); @@ -200,63 +144,56 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp /** * Sets the expression context for the widget. The context is used for the expression - * preview result and for populating the list of available functions and variables. + * preview result and to populate the list of available functions and variables. * \param context expression context * \see expressionContext * \since QGIS 2.12 */ void setExpressionContext( const QgsExpressionContext &context ); - /** - * Registers a node item for the expression builder. - * \param group The group the item will be show in the tree view. If the group doesn't exist it will be created. - * \param label The label that is show to the user for the item in the tree. - * \param expressionText The text that is inserted into the expression area when the user double clicks on the item. - * \param helpText The help text that the user will see when item is selected. - * \param type The type of the expression item. - * \param highlightedItem set to TRUE to make the item highlighted, which inserts a bold copy of the item at the top level - * \param sortOrder sort ranking for item - * \param icon custom icon to show for item - * \param tags tags to find function - */ - void registerItem( const QString &group, const QString &label, const QString &expressionText, - const QString &helpText = QString(), - QgsExpressionItem::ItemType type = QgsExpressionItem::ExpressionNode, - bool highlightedItem = false, int sortOrder = 1, - QIcon icon = QIcon(), - const QStringList &tags = QStringList() ); - + //! Returns if the expression is valid bool isExpressionValid(); /** * Adds the current expression to the given \a collection. * By default it is saved to the collection "generic". + * \deprecated since QGIS 3.14 use expressionTree()->saveRecent() instead */ - void saveToRecent( const QString &collection = "generic" ); + Q_DECL_DEPRECATED void saveToRecent( const QString &collection = "generic" ) SIP_DEPRECATED; /** * Loads the recent expressions from the given \a collection. * By default it is loaded from the collection "generic". + * \deprecated since QGIS 3.14 use expressionTree()->loadRecent() instead + */ + Q_DECL_DEPRECATED void loadRecent( const QString &collection = QStringLiteral( "generic" ) )SIP_DEPRECATED ; + + /** + * Returns the expression tree + * \since QGIS 3.14 */ - void loadRecent( const QString &collection = QStringLiteral( "generic" ) ); + QgsExpressionTreeView *expressionTree() const; /** * Loads the user expressions. + * \deprecated since QGIS 3.14 use expressionTree()->loadUserExpressions() instead * \since QGIS 3.12 */ - void loadUserExpressions( ); + Q_DECL_DEPRECATED void loadUserExpressions() SIP_DEPRECATED; /** * Stores the user \a expression with given \a label and \a helpText. + * \deprecated since QGIS 3.14 use expressionTree()->saveToUserExpressions() instead * \since QGIS 3.12 */ - void saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ); + Q_DECL_DEPRECATED void saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ) SIP_DEPRECATED; /** * Removes the expression \a label from the user stored expressions. + * \deprecated since QGIS 3.14 use expressionTree()->removeFromUserExpressions() instead * \since QGIS 3.12 */ - void removeFromUserExpressions( const QString &label ); + Q_DECL_DEPRECATED void removeFromUserExpressions( const QString &label ) SIP_DEPRECATED; /** * Creates a new file in the function editor @@ -287,8 +224,9 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp * Returns a pointer to the dialog's function item model. * This method is exposed for testing purposes only - it should not be used to modify the model. * \since QGIS 3.0 + * \deprecated since QGIS 3.14 */ - QStandardItemModel *model(); + Q_DECL_DEPRECATED QStandardItemModel *model() SIP_DEPRECATED; /** * Returns the project currently associated with the widget. @@ -380,25 +318,45 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp */ void removeSelectedUserExpression( ); + /** + * Edits the selected expression from the stored user expressions, + * the selected expression must be a user stored expression. + * \since QGIS 3.14 + */ + void editSelectedUserExpression(); + /** * Returns the list of expression items matching a \a label. * \since QGIS 3.12 + * \deprecated since QGIS 3.14 use expressionTree()->findExpressions instead */ const QList findExpressions( const QString &label ); private slots: void indicatorClicked( int line, int index, Qt::KeyboardModifiers state ); - void showContextMenu( QPoint ); void setExpressionState( bool state ); - void currentChanged( const QModelIndex &index, const QModelIndex & ); + void expressionTreeItemChanged( QgsExpressionItem *item ); void operatorButtonClicked(); void btnRun_pressed(); void btnNewFile_pressed(); + + /** + * Display a file dialog to choose where to store the exported expressions JSON file + * and saves them to the selected destination. + * \since QGIS 3.14 + */ + void exportUserExpressions_pressed(); + + /** + * Display a file dialog to choose where to load the expression JSON file from + * and adds them to user expressions group. + * \since QGIS 3.14 + */ + void importUserExpressions_pressed(); void cmbFileNames_currentItemChanged( QListWidgetItem *item, QListWidgetItem *lastitem ); - void expressionTree_doubleClicked( const QModelIndex &index ); + void insertExpressionText( const QString &text ); void txtExpressionString_textChanged(); - void txtSearchEdit_textChanged(); void txtSearchEditValues_textChanged(); void lblPreview_linkActivated( const QString &link ); void mValuesListView_doubleClicked( const QModelIndex &index ); @@ -434,68 +392,31 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp void showEvent( QShowEvent *e ) override; private: + class ExpressionTreeMenuProvider : public QgsExpressionTreeView::MenuProvider + { + public: + ExpressionTreeMenuProvider( QgsExpressionBuilderWidget *expressionBuilderWidget ) + : QgsExpressionTreeView::MenuProvider() + , mExpressionBuilderWidget( expressionBuilderWidget ) {} + + QMenu *createContextMenu( QgsExpressionItem *item ) override; + + private: + QgsExpressionBuilderWidget *mExpressionBuilderWidget; + }; + int FUNCTION_MARKER_ID = 25; + void createErrorMarkers( QList errors ); void createMarkers( const QgsExpressionNode *node ); void clearFunctionMarkers(); void clearErrors(); void runPythonCode( const QString &code ); - void updateFunctionTree(); void fillFieldValues( const QString &fieldName, int countLimit, bool forceUsedValues = false ); - bool formatterCanProvideAvailableValues( const QString &fieldName ); QString getFunctionHelp( QgsExpressionFunction *function ); QString loadFunctionHelp( QgsExpressionItem *functionName ); QString helpStylesheet() const; - void loadExpressionContext(); - - //! Loads current project relations names/id into the expression help tree - void loadRelations(); - - //! Loads current project layer names/ids into the expression help tree - void loadLayers(); - - /** - * Registers a node item for the expression builder, adding multiple items when the function exists in multiple groups - * \param groups The groups the item will be show in the tree view. If a group doesn't exist it will be created. - * \param label The label that is show to the user for the item in the tree. - * \param expressionText The text that is inserted into the expression area when the user double clicks on the item. - * \param helpText The help text that the user will see when item is selected. - * \param type The type of the expression item. - * \param highlightedItem set to TRUE to make the item highlighted, which inserts a bold copy of the item at the top level - * \param sortOrder sort ranking for item - * \param tags tags to find function - */ - void registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, - const QString &helpText = QString(), - QgsExpressionItem::ItemType type = QgsExpressionItem::ExpressionNode, - bool highlightedItem = false, int sortOrder = 1, const QStringList &tags = QStringList() ); - - /** - * Returns a HTML formatted string for use as a \a relation item help. - */ - QString formatRelationHelp( const QgsRelation &relation ) const; - - /** - * Returns a HTML formatted string for use as a \a layer item help. - */ - QString formatLayerHelp( const QgsMapLayer *layer ) const; - - /** - * Returns a HTML formatted string for use as a \a recent \a expression item help. - */ - QString formatRecentExpressionHelp( const QString &label, const QString &expression ) const; - - /** - * Returns a HTML formatted string for use as a \a user \a expression item help. - */ - QString formatUserExpressionHelp( const QString &label, const QString &expression, const QString &description ) const; - - /** - * Returns a HTML formatted string for use as a \a variable item help. - */ - QString formatVariableHelp( const QString &variable, const QString &description, bool showValue, const QVariant &value ) const; - /** * Will be set to TRUE if the current expression text reported an eval error * with the context. @@ -512,33 +433,26 @@ class GUI_EXPORT QgsExpressionBuilderWidget : public QWidget, private Ui::QgsExp */ void setParserError( bool parserError ); - void loadFieldValues( const QVariantMap &values ); - - void loadFieldsAndValues( const QMap &fieldValues ); - - bool mAutoSave = true; - QString mFunctionsPath; - QgsVectorLayer *mLayer = nullptr; - std::unique_ptr mModel; // Will hold items with // * a display string that matches the represented field values // * custom data in Qt::UserRole + 1 that contains a ready to use expression literal ('quoted string' or NULL or a plain number ) std::unique_ptr mValuesModel; std::unique_ptr mProxyValues; - std::unique_ptr mProxyModel; - QMap mExpressionGroups; + + ExpressionTreeMenuProvider *mExpressionTreeMenuProvider = nullptr; + + bool mAutoSave = true; + QString mFunctionsPath; + QgsVectorLayer *mLayer = nullptr; QgsExpressionHighlighter *highlighter = nullptr; bool mExpressionValid = false; QgsDistanceArea mDa; - QString mRecentKey; - QMap mFieldValues; QgsExpressionContext mExpressionContext; QPointer< QgsProject > mProject; bool mEvalError = true; bool mParserError = true; // Translated name of the user expressions group QString mUserExpressionsGroupName; - QStringList mUserExpressionLabels; }; // clazy:excludeall=qstring-allocations diff --git a/src/gui/qgsexpressionselectiondialog.cpp b/src/gui/qgsexpressionselectiondialog.cpp index 6bddd3bfea25..ff2a5043d4d5 100644 --- a/src/gui/qgsexpressionselectiondialog.cpp +++ b/src/gui/qgsexpressionselectiondialog.cpp @@ -42,7 +42,7 @@ QgsExpressionSelectionDialog::QgsExpressionSelectionDialog( QgsVectorLayer *laye connect( mButtonZoomToFeatures, &QToolButton::clicked, this, &QgsExpressionSelectionDialog::mButtonZoomToFeatures_clicked ); connect( mPbnClose, &QPushButton::clicked, this, &QgsExpressionSelectionDialog::mPbnClose_clicked ); - setWindowTitle( QStringLiteral( "Select by Expression - %1" ).arg( layer->name() ) ); + setWindowTitle( QStringLiteral( "%1 — Select by Expression" ).arg( layer->name() ) ); mActionSelect->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpressionSelect.svg" ) ) ); mActionAddToSelection->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mIconSelectAdd.svg" ) ) ); @@ -55,12 +55,9 @@ QgsExpressionSelectionDialog::QgsExpressionSelectionDialog( QgsVectorLayer *laye mButtonSelect->addAction( mActionSelectIntersect ); mButtonSelect->setDefaultAction( mActionSelect ); - mExpressionBuilder->setLayer( layer ); - mExpressionBuilder->setExpressionText( startText ); - QgsExpressionContext context( QgsExpressionContextUtils::globalProjectLayerScopes( mLayer ) ); - mExpressionBuilder->loadRecent( QStringLiteral( "selection" ) ); - mExpressionBuilder->setExpressionContext( context ); + mExpressionBuilder->initWithLayer( layer, context, QStringLiteral( "selection" ) ); + mExpressionBuilder->setExpressionText( startText ); // by default, zoom to features is hidden, shown only if canvas is set mButtonZoomToFeatures->setVisible( false ); @@ -223,7 +220,7 @@ void QgsExpressionSelectionDialog::done( int r ) void QgsExpressionSelectionDialog::saveRecent() { - mExpressionBuilder->saveToRecent( QStringLiteral( "selection" ) ); + mExpressionBuilder->expressionTree()->saveToRecent( mExpressionBuilder->expressionText(), QStringLiteral( "selection" ) ); } void QgsExpressionSelectionDialog::showHelp() diff --git a/src/gui/qgsexpressionstoredialog.h b/src/gui/qgsexpressionstoredialog.h index 3d210640fe57..811087c8f257 100644 --- a/src/gui/qgsexpressionstoredialog.h +++ b/src/gui/qgsexpressionstoredialog.h @@ -39,7 +39,7 @@ class GUI_EXPORT QgsExpressionStoreDialog : public QDialog, private Ui::QgsExpre QgsExpressionStoreDialog( const QString &label, const QString &expression, const QString &helpText, - const QStringList &existingLabels, + const QStringList &existingLabels = QStringList(), QWidget *parent = nullptr ); /** diff --git a/src/gui/qgsexpressiontreeview.cpp b/src/gui/qgsexpressiontreeview.cpp new file mode 100644 index 000000000000..9170ca35cd84 --- /dev/null +++ b/src/gui/qgsexpressiontreeview.cpp @@ -0,0 +1,858 @@ +/*************************************************************************** + qgsexpressiontreeview.cpp + -------------------------------------- + Date : march 2020 - quarantine day 9 + Copyright : (C) 2020 by Denis Rouzaud + Email : denis@opengis.ch + *************************************************************************** + * * + * 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 +#include + +#include "qgsexpressiontreeview.h" +#include "qgis.h" +#include "qgsfieldformatterregistry.h" +#include "qgsvectorlayer.h" +#include "qgsexpressioncontextutils.h" +#include "qgssettings.h" +#include "qgsrelationmanager.h" +#include "qgsapplication.h" + + +//! Returns a HTML formatted string for use as a \a relation item help. +QString formatRelationHelp( const QgsRelation &relation ) +{ + QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) + .arg( QCoreApplication::translate( "relation_help", "relation %1" ).arg( relation.name() ), + QObject::tr( "Inserts the relation ID for the relation named '%1'." ).arg( relation.name() ) ); + + text += QStringLiteral( "

    %1

    %2
    " ) + .arg( QObject::tr( "Current value" ), relation.id() ); + + return text; +} + + +//! Returns a HTML formatted string for use as a \a layer item help. +QString formatLayerHelp( const QgsMapLayer *layer ) +{ + QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) + .arg( QCoreApplication::translate( "layer_help", "map layer %1" ).arg( layer->name() ), + QObject::tr( "Inserts the layer ID for the layer named '%1'." ).arg( layer->name() ) ); + + text += QStringLiteral( "

    %1

    %2
    " ) + .arg( QObject::tr( "Current value" ), layer->id() ); + + return text; +} + +//! Returns a HTML formatted string for use as a \a recent \a expression item help. +QString formatRecentExpressionHelp( const QString &label, const QString &expression ) +{ + QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) + .arg( QCoreApplication::translate( "recent_expression_help", "expression %1" ).arg( label ), + QCoreApplication::translate( "recent_expression_help", "Recently used expression." ) ); + + text += QStringLiteral( "

    %1

    %2
    " ) + .arg( QObject::tr( "Expression" ), expression ); + + return text; +} + +//! Returns a HTML formatted string for use as a \a user \a expression item help. +QString formatUserExpressionHelp( const QString &label, const QString &expression, const QString &description ) +{ + QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) + .arg( QCoreApplication::translate( "user_expression_help", "expression %1" ).arg( label ), description ); + + text += QStringLiteral( "

    %1

    %2
    " ) + .arg( QObject::tr( "Expression" ), expression ); + + return text; +} + +//! Returns a HTML formatted string for use as a \a variable item help. +QString formatVariableHelp( const QString &variable, const QString &description, bool showValue, const QVariant &value ) +{ + QString text = QStringLiteral( "

    %1

    \n

    %2

    " ) + .arg( QCoreApplication::translate( "variable_help", "variable %1" ).arg( variable ), description ); + + if ( showValue ) + { + QString valueString = !value.isValid() + ? QCoreApplication::translate( "variable_help", "not set" ) + : QStringLiteral( "
    %1
    " ).arg( QgsExpression::formatPreviewString( value ) ); + + text += QStringLiteral( "

    %1

    %2

    " ) + .arg( QObject::tr( "Current value" ), valueString ); + } + + return text; +} + + +// **************************** +// **************************** +// QgsExpressionTreeView +// **************************** + + +QgsExpressionTreeView::QgsExpressionTreeView( QWidget *parent ) + : QTreeView( parent ) + , mProject( QgsProject::instance() ) +{ + connect( this, &QTreeView::doubleClicked, this, &QgsExpressionTreeView::onDoubleClicked ); + + mModel = qgis::make_unique(); + mProxyModel = qgis::make_unique(); + mProxyModel->setDynamicSortFilter( true ); + mProxyModel->setSourceModel( mModel.get() ); + setModel( mProxyModel.get() ); + setSortingEnabled( true ); + sortByColumn( 0, Qt::AscendingOrder ); + + setSelectionMode( QAbstractItemView::SelectionMode::SingleSelection ); + + setContextMenuPolicy( Qt::CustomContextMenu ); + connect( this, &QWidget::customContextMenuRequested, this, &QgsExpressionTreeView::showContextMenu ); + connect( selectionModel(), &QItemSelectionModel::currentChanged, this, &QgsExpressionTreeView::currentItemChanged ); + + updateFunctionTree(); + loadUserExpressions(); + + // select the first item in the function list + // in order to avoid a blank help widget + QModelIndex firstItem = mProxyModel->index( 0, 0, QModelIndex() ); + setCurrentIndex( firstItem ); +} + +void QgsExpressionTreeView::setLayer( QgsVectorLayer *layer ) +{ + mLayer = layer; + + //TODO - remove existing layer scope from context + + if ( mLayer ) + mExpressionContext << QgsExpressionContextUtils::layerScope( mLayer ); + + loadFieldNames(); +} + +void QgsExpressionTreeView::setExpressionContext( const QgsExpressionContext &context ) +{ + mExpressionContext = context; + updateFunctionTree(); + loadFieldNames(); + loadRecent( mRecentKey ); + loadUserExpressions( ); +} + +void QgsExpressionTreeView::setMenuProvider( QgsExpressionTreeView::MenuProvider *provider ) +{ + mMenuProvider = provider; +} + +void QgsExpressionTreeView::refresh() +{ + updateFunctionTree(); + loadFieldNames(); + loadRecent( mRecentKey ); + loadUserExpressions( ); +} + +QgsExpressionItem *QgsExpressionTreeView::currentItem() const +{ + QModelIndex idx = mProxyModel->mapToSource( currentIndex() ); + QgsExpressionItem *item = static_cast( mModel->itemFromIndex( idx ) ); + return item; +} + +QStandardItemModel *QgsExpressionTreeView::model() +{ + return mModel.get(); +} + +QgsProject *QgsExpressionTreeView::project() +{ + return mProject; +} + +void QgsExpressionTreeView::setProject( QgsProject *project ) +{ + mProject = project; + updateFunctionTree(); +} + + +void QgsExpressionTreeView::setSearchText( const QString &text ) +{ + mProxyModel->setFilterWildcard( text ); + if ( text.isEmpty() ) + { + collapseAll(); + } + else + { + expandAll(); + QModelIndex index = mProxyModel->index( 0, 0 ); + if ( mProxyModel->hasChildren( index ) ) + { + QModelIndex child = mProxyModel->index( 0, 0, index ); + selectionModel()->setCurrentIndex( child, QItemSelectionModel::ClearAndSelect ); + } + } +} + +void QgsExpressionTreeView::onDoubleClicked( const QModelIndex &index ) +{ + QModelIndex idx = mProxyModel->mapToSource( index ); + QgsExpressionItem *item = static_cast( mModel->itemFromIndex( idx ) ); + if ( !item ) + return; + + // Don't handle the double-click if we are on a header node. + if ( item->getItemType() == QgsExpressionItem::Header ) + return; + + emit expressionItemDoubleClicked( item->getExpressionText() ); +} + +void QgsExpressionTreeView::showContextMenu( QPoint pt ) +{ + QModelIndex idx = indexAt( pt ); + idx = mProxyModel->mapToSource( idx ); + QgsExpressionItem *item = static_cast( mModel->itemFromIndex( idx ) ); + if ( !item ) + return; + + if ( !mMenuProvider ) + return; + + QMenu *menu = mMenuProvider->createContextMenu( item ); + + if ( menu ) + menu->popup( mapToGlobal( pt ) ); +} + +void QgsExpressionTreeView::currentItemChanged( const QModelIndex &index, const QModelIndex & ) +{ + // Get the item + QModelIndex idx = mProxyModel->mapToSource( index ); + QgsExpressionItem *item = static_cast( mModel->itemFromIndex( idx ) ); + if ( !item ) + return; + + emit currentExpressionItemChanged( item ); +} + +void QgsExpressionTreeView::updateFunctionTree() +{ + mModel->clear(); + mExpressionGroups.clear(); + + // TODO Can we move this stuff to QgsExpression, like the functions? + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "+" ), QStringLiteral( " + " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "-" ), QStringLiteral( " - " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "*" ), QStringLiteral( " * " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "/" ), QStringLiteral( " / " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "%" ), QStringLiteral( " % " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "^" ), QStringLiteral( " ^ " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "=" ), QStringLiteral( " = " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "~" ), QStringLiteral( " ~ " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">" ), QStringLiteral( " > " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<" ), QStringLiteral( " < " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<>" ), QStringLiteral( " <> " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<=" ), QStringLiteral( " <= " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">=" ), QStringLiteral( " >= " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "[]" ), QStringLiteral( "[ ]" ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "||" ), QStringLiteral( " || " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "ILIKE" ), QStringLiteral( " ILIKE " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IS" ), QStringLiteral( " IS " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "OR" ), QStringLiteral( " OR " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "AND" ), QStringLiteral( " AND " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "NOT" ), QStringLiteral( " NOT " ) ); + + QString casestring = QStringLiteral( "CASE WHEN condition THEN result END" ); + registerItem( QStringLiteral( "Conditionals" ), QStringLiteral( "CASE" ), casestring ); + + // use -1 as sort order here -- NULL should always show before the field list + registerItem( QStringLiteral( "Fields and Values" ), QStringLiteral( "NULL" ), QStringLiteral( "NULL" ), QString(), QgsExpressionItem::ExpressionNode, false, -1 ); + + // Load the functions from the QgsExpression class + int count = QgsExpression::functionCount(); + for ( int i = 0; i < count; i++ ) + { + QgsExpressionFunction *func = QgsExpression::Functions()[i]; + QString name = func->name(); + if ( name.startsWith( '_' ) ) // do not display private functions + continue; + if ( func->isDeprecated() ) // don't show deprecated functions + continue; + if ( func->isContextual() ) + { + //don't show contextual functions by default - it's up the the QgsExpressionContext + //object to provide them if supported + continue; + } + if ( func->params() != 0 ) + name += '('; + else if ( !name.startsWith( '$' ) ) + name += QLatin1String( "()" ); + // this is where the functions are being registered, including functions under "Custom" + registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) ); + } + + // load relation names + loadRelations(); + + // load layer IDs + loadLayers(); + + loadExpressionContext(); +} + +void QgsExpressionTreeView::registerItem( const QString &group, + const QString &label, + const QString &expressionText, + const QString &helpText, + QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, QIcon icon, const QStringList &tags ) +{ + QgsExpressionItem *item = new QgsExpressionItem( label, expressionText, helpText, type ); + item->setData( label, Qt::UserRole ); + item->setData( sortOrder, QgsExpressionItem::CUSTOM_SORT_ROLE ); + item->setData( tags, QgsExpressionItem::SEARCH_TAGS_ROLE ); + item->setIcon( icon ); + + // Look up the group and insert the new function. + if ( mExpressionGroups.contains( group ) ) + { + QgsExpressionItem *groupNode = mExpressionGroups.value( group ); + groupNode->appendRow( item ); + } + else + { + // If the group doesn't exist yet we make it first. + QgsExpressionItem *newgroupNode = new QgsExpressionItem( QgsExpression::group( group ), QString(), QgsExpressionItem::Header ); + newgroupNode->setData( group, Qt::UserRole ); + //Recent group should always be last group + newgroupNode->setData( group.startsWith( QLatin1String( "Recent (" ) ) ? 2 : 1, QgsExpressionItem::CUSTOM_SORT_ROLE ); + newgroupNode->appendRow( item ); + newgroupNode->setBackground( QBrush( QColor( 150, 150, 150, 150 ) ) ); + mModel->appendRow( newgroupNode ); + mExpressionGroups.insert( group, newgroupNode ); + } + + if ( highlightedItem ) + { + //insert a copy as a top level item + QgsExpressionItem *topLevelItem = new QgsExpressionItem( label, expressionText, helpText, type ); + topLevelItem->setData( label, Qt::UserRole ); + item->setData( 0, QgsExpressionItem::CUSTOM_SORT_ROLE ); + QFont font = topLevelItem->font(); + font.setBold( true ); + topLevelItem->setFont( font ); + mModel->appendRow( topLevelItem ); + } +} + +void QgsExpressionTreeView::registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, const QString &helpText, QgsExpressionItem::ItemType type, bool highlightedItem, int sortOrder, const QStringList &tags ) +{ + const auto constGroups = groups; + for ( const QString &group : constGroups ) + { + registerItem( group, label, expressionText, helpText, type, highlightedItem, sortOrder, QIcon(), tags ); + } +} + +void QgsExpressionTreeView::loadExpressionContext() +{ + QStringList variableNames = mExpressionContext.filteredVariableNames(); + const auto constVariableNames = variableNames; + for ( const QString &variable : constVariableNames ) + { + registerItem( QStringLiteral( "Variables" ), variable, " @" + variable + ' ', + formatVariableHelp( variable, mExpressionContext.description( variable ), true, mExpressionContext.variable( variable ) ), + QgsExpressionItem::ExpressionNode, + mExpressionContext.isHighlightedVariable( variable ) ); + } + + // Load the functions from the expression context + QStringList contextFunctions = mExpressionContext.functionNames(); + const auto constContextFunctions = contextFunctions; + for ( const QString &functionName : constContextFunctions ) + { + QgsExpressionFunction *func = mExpressionContext.function( functionName ); + QString name = func->name(); + if ( name.startsWith( '_' ) ) // do not display private functions + continue; + if ( func->params() != 0 ) + name += '('; + registerItemForAllGroups( func->groups(), func->name(), ' ' + name + ' ', func->helpText(), QgsExpressionItem::ExpressionNode, mExpressionContext.isHighlightedFunction( func->name() ), 1, QgsExpression::tags( func->name() ) ); + } +} + +void QgsExpressionTreeView::loadLayers() +{ + if ( !mProject ) + return; + + QMap layers = mProject->mapLayers(); + QMap::const_iterator layerIt = layers.constBegin(); + for ( ; layerIt != layers.constEnd(); ++layerIt ) + { + registerItemForAllGroups( QStringList() << tr( "Map Layers" ), layerIt.value()->name(), QStringLiteral( "'%1'" ).arg( layerIt.key() ), formatLayerHelp( layerIt.value() ) ); + } +} + +void QgsExpressionTreeView::loadFieldNames( const QgsFields &fields ) +{ + for ( int i = 0; i < fields.count(); ++i ) + { + const QgsField field = fields.at( i ); + QIcon icon = fields.iconForField( i ); + registerItem( QStringLiteral( "Fields and Values" ), field.displayNameWithAlias(), + " \"" + field.name() + "\" ", QString(), QgsExpressionItem::Field, false, i, icon ); + } +} + +void QgsExpressionTreeView::loadFieldNames() +{ + if ( mExpressionGroups.contains( QStringLiteral( "Fields and Values" ) ) ) + { + QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "Fields and Values" ) ); + node->removeRows( 0, node->rowCount() ); + } + + // this can happen if fields are manually set + if ( !mLayer ) + return; + + const QgsFields &fields = mLayer->fields(); + + loadFieldNames( fields ); +} + +void QgsExpressionTreeView::loadRelations() +{ + if ( !mProject ) + return; + + QMap relations = mProject->relationManager()->relations(); + QMap::const_iterator relIt = relations.constBegin(); + for ( ; relIt != relations.constEnd(); ++relIt ) + { + registerItemForAllGroups( QStringList() << tr( "Relations" ), relIt->name(), QStringLiteral( "'%1'" ).arg( relIt->id() ), formatRelationHelp( relIt.value() ) ); + } +} + +void QgsExpressionTreeView::loadRecent( const QString &collection ) +{ + mRecentKey = collection; + QString name = tr( "Recent (%1)" ).arg( collection ); + if ( mExpressionGroups.contains( name ) ) + { + QgsExpressionItem *node = mExpressionGroups.value( name ); + node->removeRows( 0, node->rowCount() ); + } + + QgsSettings settings; + const QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection ); + const QStringList expressions = settings.value( location ).toStringList(); + int i = 0; + for ( const QString &expression : expressions ) + { + QString help = formatRecentExpressionHelp( expression, expression ); + registerItem( name, expression, expression, help, QgsExpressionItem::ExpressionNode, false, i ); + i++; + } +} + +void QgsExpressionTreeView::saveToRecent( const QString &expressionText, const QString &collection ) +{ + QgsSettings settings; + QString location = QStringLiteral( "/expressions/recent/%1" ).arg( collection ); + QStringList expressions = settings.value( location ).toStringList(); + expressions.removeAll( expressionText ); + + expressions.prepend( expressionText ); + + while ( expressions.count() > 20 ) + { + expressions.pop_back(); + } + + settings.setValue( location, expressions ); + loadRecent( collection ); +} + +void QgsExpressionTreeView::saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ) +{ + QgsSettings settings; + const QString location = QStringLiteral( "user" ); + settings.beginGroup( location, QgsSettings::Section::Expressions ); + settings.beginGroup( label ); + settings.setValue( QStringLiteral( "expression" ), expression ); + settings.setValue( QStringLiteral( "helpText" ), helpText ); + loadUserExpressions( ); + // Scroll + const QModelIndexList idxs { mModel->match( mModel->index( 0, 0 ), + Qt::DisplayRole, label, 1, + Qt::MatchFlag::MatchRecursive ) }; + if ( ! idxs.isEmpty() ) + { + scrollTo( idxs.first() ); + } +} + +void QgsExpressionTreeView::removeFromUserExpressions( const QString &label ) +{ + QgsSettings settings; + settings.remove( QStringLiteral( "user/%1" ).arg( label ), QgsSettings::Section::Expressions ); + loadUserExpressions( ); +} + +// this is potentially very slow if there are thousands of user expressions, every time entire cleanup and load +void QgsExpressionTreeView::loadUserExpressions( ) +{ + // Cleanup + if ( mExpressionGroups.contains( QStringLiteral( "UserGroup" ) ) ) + { + QgsExpressionItem *node = mExpressionGroups.value( QStringLiteral( "UserGroup" ) ); + node->removeRows( 0, node->rowCount() ); + } + + QgsSettings settings; + const QString location = QStringLiteral( "user" ); + settings.beginGroup( location, QgsSettings::Section::Expressions ); + QString label; + QString helpText; + QString expression; + int i = 0; + mUserExpressionLabels = settings.childGroups(); + for ( const auto &label : qgis::as_const( mUserExpressionLabels ) ) + { + settings.beginGroup( label ); + expression = settings.value( QStringLiteral( "expression" ) ).toString(); + helpText = formatUserExpressionHelp( label, expression, settings.value( QStringLiteral( "helpText" ) ).toString() ); + registerItem( QStringLiteral( "UserGroup" ), label, expression, helpText, QgsExpressionItem::ExpressionNode, false, i++ ); + settings.endGroup(); + } +} + +QStringList QgsExpressionTreeView::userExpressionLabels() const +{ + return mUserExpressionLabels; +} + +QJsonDocument QgsExpressionTreeView::exportUserExpressions() +{ + const QString group = QStringLiteral( "user" ); + QgsSettings settings; + QJsonArray exportList; + QJsonObject exportObject + { + {"qgis_version", Qgis::version()}, + {"exported_at", QDateTime::currentDateTime().toString( Qt::ISODate )}, + {"author", QgsApplication::userFullName()}, + {"expressions", exportList} + }; + + settings.beginGroup( group, QgsSettings::Section::Expressions ); + + mUserExpressionLabels = settings.childGroups(); + + for ( const QString &label : qgis::as_const( mUserExpressionLabels ) ) + { + settings.beginGroup( label ); + + const QString expression = settings.value( QStringLiteral( "expression" ) ).toString(); + const QString helpText = settings.value( QStringLiteral( "helpText" ) ).toString(); + const QJsonObject expressionObject + { + {"name", label}, + {"type", "expression"}, + {"expression", expression}, + {"group", group}, + {"description", helpText} + }; + exportList.push_back( expressionObject ); + + settings.endGroup(); + } + + exportObject["expressions"] = exportList; + QJsonDocument exportJson = QJsonDocument( exportObject ); + + return exportJson; +} + +void QgsExpressionTreeView::loadExpressionsFromJson( const QJsonDocument &expressionsDocument ) +{ + // if the root of the json document is not an object, it means it's a wrong file + if ( ! expressionsDocument.isObject() ) + return; + + QJsonObject expressionsObject = expressionsDocument.object(); + + // validate json for manadatory fields + if ( ! expressionsObject["qgis_version"].isString() + || ! expressionsObject["exported_at"].isString() + || ! expressionsObject["author"].isString() + || ! expressionsObject["expressions"].isArray() ) + return; + + // validate versions + QVersionNumber qgisJsonVersion = QVersionNumber::fromString( expressionsObject["qgis_version"].toString() ); + QVersionNumber qgisVersion = QVersionNumber::fromString( Qgis::version() ); + + // if the expressions are from newer version of QGIS, we ask the user to confirm + // they want to proceed + if ( qgisJsonVersion > qgisVersion ) + { + QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No; + switch ( QMessageBox::question( this, + tr( "QGIS Version Mismatch" ), + tr( "The imported expressions are from newer version of QGIS (%1) " + "and some of the expression might not work the current version (%2). " + "Are you sure you want to continue?" ).arg( qgisJsonVersion.toString(), qgisVersion.toString() ), buttons ) ) + { + case QMessageBox::No: + return; + + case QMessageBox::Yes: + break; + + default: + break; + } + } + + // we store the number of + QStringList skippedExpressionLabels; + bool isApplyToAll = false; + bool isOkToOverwrite = false; + + QgsSettings settings; + settings.beginGroup( QStringLiteral( "user" ), QgsSettings::Section::Expressions ); + mUserExpressionLabels = settings.childGroups(); + + for ( const QJsonValue &expressionValue : expressionsObject["expressions"].toArray() ) + { + // validate the type of the array element, can be anything + if ( ! expressionValue.isObject() ) + { + // try to stringify and put and indicator what happened + skippedExpressionLabels.append( expressionValue.toString() ); + continue; + } + + QJsonObject expressionObj = expressionValue.toObject(); + + // make sure the required keys are the correct types + if ( ! expressionObj["name"].isString() + || ! expressionObj["type"].isString() + || ! expressionObj["expression"].isString() + || ! expressionObj["group"].isString() + || ! expressionObj["description"].isString() ) + { + // try to stringify and put an indicator what happened. Try to stringify the name, if fails, go with the expression. + if ( ! expressionObj["name"].toString().isEmpty() ) + skippedExpressionLabels.append( expressionObj["name"].toString() ); + else + skippedExpressionLabels.append( expressionObj["expression"].toString() ); + + continue; + } + + // we want to import only items of type expression for now + if ( expressionObj["type"].toString() != QStringLiteral( "expression" ) ) + { + skippedExpressionLabels.append( expressionObj["name"].toString() ); + continue; + } + + // we want to import only items of type expression for now + if ( expressionObj["group"].toString() != QStringLiteral( "user" ) ) + { + skippedExpressionLabels.append( expressionObj["name"].toString() ); + continue; + } + + const QString label = expressionObj["name"].toString(); + const QString expression = expressionObj["expression"].toString(); + const QString helpText = expressionObj["description"].toString(); + + // make sure they have valid name + if ( label.contains( "\\" ) || label.contains( '/' ) ) + { + skippedExpressionLabels.append( expressionObj["name"].toString() ); + continue; + } + + settings.beginGroup( label ); + const QString oldExpression = settings.value( QStringLiteral( "expression" ) ).toString(); + settings.endGroup(); + + // TODO would be nice to skip the cases when labels and expressions match + if ( mUserExpressionLabels.contains( label ) && expression != oldExpression ) + { + if ( ! isApplyToAll ) + showMessageBoxConfirmExpressionOverwrite( isApplyToAll, isOkToOverwrite, label, oldExpression, expression ); + + if ( isOkToOverwrite ) + saveToUserExpressions( label, expression, helpText ); + else + { + skippedExpressionLabels.append( label ); + continue; + } + } + else + { + saveToUserExpressions( label, expression, helpText ); + } + } + + loadUserExpressions( ); + + if ( ! skippedExpressionLabels.isEmpty() ) + { + QStringList skippedExpressionLabelsQuoted; + for ( const QString &skippedExpressionLabel : skippedExpressionLabels ) + skippedExpressionLabelsQuoted.append( QStringLiteral( "'%1'" ).arg( skippedExpressionLabel ) ); + + QMessageBox::information( this, + tr( "Skipped Expression Imports" ), + QStringLiteral( "%1\n%2" ).arg( tr( "The following expressions have been skipped:" ), + skippedExpressionLabelsQuoted.join( ", " ) ) ); + } +} + +const QList QgsExpressionTreeView::findExpressions( const QString &label ) +{ + QList result; + const QList found { mModel->findItems( label, Qt::MatchFlag::MatchRecursive ) }; + for ( const auto &item : qgis::as_const( found ) ) + { + result.push_back( static_cast( item ) ); + } + return result; +} + +void QgsExpressionTreeView::showMessageBoxConfirmExpressionOverwrite( + bool &isApplyToAll, + bool &isOkToOverwrite, + const QString &label, + const QString &oldExpression, + const QString &newExpression ) +{ + QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll; + switch ( QMessageBox::question( this, + tr( "Expression Overwrite" ), + tr( "The expression with label '%1' was already defined." + "The old expression \"%2\" will be overwritten by \"%3\"." + "Are you sure you want to overwrite the expression?" ).arg( label, oldExpression, newExpression ), buttons ) ) + { + case QMessageBox::NoToAll: + isApplyToAll = true; + isOkToOverwrite = false; + break; + + case QMessageBox::No: + isApplyToAll = false; + isOkToOverwrite = false; + break; + + case QMessageBox::YesToAll: + isApplyToAll = true; + isOkToOverwrite = true; + break; + + case QMessageBox::Yes: + isApplyToAll = false; + isOkToOverwrite = true; + break; + + default: + break; + } +} + + +// **************************** +// **************************** +// QgsExpressionItemSearchProxy +// **************************** + + +QgsExpressionItemSearchProxy::QgsExpressionItemSearchProxy() +{ + setFilterCaseSensitivity( Qt::CaseInsensitive ); +} + +bool QgsExpressionItemSearchProxy::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const +{ + QModelIndex index = sourceModel()->index( source_row, 0, source_parent ); + QgsExpressionItem::ItemType itemType = QgsExpressionItem::ItemType( sourceModel()->data( index, QgsExpressionItem::ITEM_TYPE_ROLE ).toInt() ); + + int count = sourceModel()->rowCount( index ); + bool matchchild = false; + for ( int i = 0; i < count; ++i ) + { + if ( filterAcceptsRow( i, index ) ) + { + matchchild = true; + break; + } + } + + if ( itemType == QgsExpressionItem::Header && matchchild ) + return true; + + if ( itemType == QgsExpressionItem::Header ) + return false; + + // check match of item label or tags + if ( QSortFilterProxyModel::filterAcceptsRow( source_row, source_parent ) ) + { + return true; + } + else + { + const QStringList tags = sourceModel()->data( index, QgsExpressionItem::SEARCH_TAGS_ROLE ).toStringList(); + for ( const QString &tag : tags ) + { + if ( tag.contains( filterRegExp() ) ) + return true; + } + } + return false; +} + +bool QgsExpressionItemSearchProxy::lessThan( const QModelIndex &left, const QModelIndex &right ) const +{ + int leftSort = sourceModel()->data( left, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt(); + int rightSort = sourceModel()->data( right, QgsExpressionItem::CUSTOM_SORT_ROLE ).toInt(); + if ( leftSort != rightSort ) + return leftSort < rightSort; + + QString leftString = sourceModel()->data( left, Qt::DisplayRole ).toString(); + QString rightString = sourceModel()->data( right, Qt::DisplayRole ).toString(); + + //ignore $ prefixes when sorting + if ( leftString.startsWith( '$' ) ) + leftString = leftString.mid( 1 ); + if ( rightString.startsWith( '$' ) ) + rightString = rightString.mid( 1 ); + + return QString::localeAwareCompare( leftString, rightString ) < 0; +} diff --git a/src/gui/qgsexpressiontreeview.h b/src/gui/qgsexpressiontreeview.h new file mode 100644 index 000000000000..667975508bc4 --- /dev/null +++ b/src/gui/qgsexpressiontreeview.h @@ -0,0 +1,362 @@ +/*************************************************************************** + qgsexpressiontreeview.h + -------------------------------------- + Date : march 2020 - quarantine day 9 + Copyright : (C) 2020 by Denis Rouzaud + Email : denis@opengis.ch + *************************************************************************** + * * + * 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 QGSEXPRESSIONTREEVIEW_H +#define QGSEXPRESSIONTREEVIEW_H + +#include +#include +#include + +#include "qgis_gui.h" +#include "qgis_sip.h" +#include "qgsexpressioncontext.h" +#include "qgsproject.h" + + +class QgsVectorLayer; + + + +/** + * \ingroup gui + * An expression item that can be used in the QgsExpressionBuilderWidget tree. + */ +class GUI_EXPORT QgsExpressionItem : public QStandardItem +{ + public: + enum ItemType + { + Header, + Field, + ExpressionNode + }; + + QgsExpressionItem( const QString &label, + const QString &expressionText, + const QString &helpText, + QgsExpressionItem::ItemType itemType = ExpressionNode ) + : QStandardItem( label ) + { + mExpressionText = expressionText; + mHelpText = helpText; + mType = itemType; + setData( itemType, ITEM_TYPE_ROLE ); + } + + QgsExpressionItem( const QString &label, + const QString &expressionText, + QgsExpressionItem::ItemType itemType = ExpressionNode ) + : QStandardItem( label ) + { + mExpressionText = expressionText; + mType = itemType; + setData( itemType, ITEM_TYPE_ROLE ); + } + + QString getExpressionText() const { return mExpressionText; } + + /** + * Gets the help text that is associated with this expression item. + * + * \returns The help text. + */ + QString getHelpText() const { return mHelpText; } + + /** + * Set the help text for the current item + * + * \note The help text can be set as a html string. + */ + void setHelpText( const QString &helpText ) { mHelpText = helpText; } + + /** + * Gets the type of expression item, e.g., header, field, ExpressionNode. + * + * \returns The QgsExpressionItem::ItemType + */ + QgsExpressionItem::ItemType getItemType() const { return mType; } + + //! Custom sort order role + static const int CUSTOM_SORT_ROLE = Qt::UserRole + 1; + //! Item type role + static const int ITEM_TYPE_ROLE = Qt::UserRole + 2; + //! Search tags role + static const int SEARCH_TAGS_ROLE = Qt::UserRole + 3; + + private: + QString mExpressionText; + QString mHelpText; + QgsExpressionItem::ItemType mType; +}; + + +/** + * \ingroup gui + * Search proxy used to filter the QgsExpressionBuilderWidget tree. + * The default search for a tree model only searches top level this will handle one + * level down + */ +class GUI_EXPORT QgsExpressionItemSearchProxy : public QSortFilterProxyModel +{ + Q_OBJECT + + public: + QgsExpressionItemSearchProxy(); + + bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override; + + protected: + + bool lessThan( const QModelIndex &left, const QModelIndex &right ) const override; +}; + +/** + * \ingroup gui + * \class QgsExpressionTreeView + * QgsExpressionTreeView is a tree view to list all expressions + * functions, variables and fields that can be used in an expression. + * \see QgsExpressionBuilderWidget + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsExpressionTreeView : public QTreeView +{ + Q_OBJECT + public: + + /** + * \ingroup gui + * \class MenuProvider + * Implementation of this interface can be implemented to allow QgsExpressionTreeView + * instance to provide custom context menus (opened upon right-click). + * \since QGIS 3.14 + */ + class MenuProvider + { + public: + //! Constructor + explicit MenuProvider() = default; + virtual ~MenuProvider() = default; + + //! Returns a newly created menu instance + virtual QMenu *createContextMenu( QgsExpressionItem *item ) SIP_FACTORY {Q_UNUSED( item ) return nullptr;} + }; + + //! Constructor + QgsExpressionTreeView( QWidget *parent = nullptr ); + + /** + * Sets layer in order to get the fields and values + */ + void setLayer( QgsVectorLayer *layer ); + + /** + * This allows loading fields without specifying a layer + */ + void loadFieldNames( const QgsFields &fields ); + + /** + * Sets the expression context for the tree view. The context is used + * to populate the list of available functions and variables. + * \param context expression context + * \see expressionContext + */ + void setExpressionContext( const QgsExpressionContext &context ); + + /** + * Returns the expression context for the widget. The context is used for the expression + * preview result and for populating the list of available functions and variables. + * \see setExpressionContext + */ + QgsExpressionContext expressionContext() const { return mExpressionContext; } + + /** + * Returns the project currently associated with the widget. + * \see setProject() + */ + QgsProject *project(); + + /** + * Sets the \a project currently associated with the widget. This + * controls which layers and relations and other project-specific items are shown in the widget. + * \see project() + */ + void setProject( QgsProject *project ); + + /** + * Sets the menu provider. + * This does not take ownership of the provider + */ + void setMenuProvider( MenuProvider *provider ); + + /** + * Refreshes the content of the tree + */ + void refresh(); + + /** + * Returns the current item or a nullptr + */ + QgsExpressionItem *currentItem() const; + + /** + * Returns a pointer to the dialog's function item model. + * This method is exposed for testing purposes only - it should not be used to modify the model + * \note will be removed in QGIS 4 + * \deprecated since QGIS 3.14 + */ + Q_DECL_DEPRECATED QStandardItemModel *model() SIP_SKIP; // TODO remove QGIS 4 + + /** + * Loads the recent expressions from the given \a collection. + * By default it is loaded from the collection "generic". + */ + void loadRecent( const QString &collection = QStringLiteral( "generic" ) ); + + /** + * Adds the current expression to the given \a collection. + * By default it is saved to the collection "generic". + */ + void saveToRecent( const QString &expressionText, const QString &collection = "generic" ); + + /** + * Stores the user \a expression with given \a label and \a helpText. + */ + void saveToUserExpressions( const QString &label, const QString expression, const QString &helpText ); + + /** + * Removes the expression \a label from the user stored expressions. + */ + void removeFromUserExpressions( const QString &label ); + + /** + * Loads the user expressions. + * This is done on request since it can be very slow if there are thousands of user expressions + */ + void loadUserExpressions( ); + + /** + * Returns the list of expression items matching a \a label. + */ + const QList findExpressions( const QString &label ); + + /** + * Returns the user expression labels + */ + QStringList userExpressionLabels() const SIP_SKIP; + + /** + * Create the expressions JSON document storing all the user expressions to be exported. + * \returns the created expressions JSON file + */ + QJsonDocument exportUserExpressions(); + + /** + * Load and permanently store the expressions from the expressions JSON document. + * \param expressionsDocument the parsed expressions JSON file + */ + void loadExpressionsFromJson( const QJsonDocument &expressionsDocument ); + + signals: + //! Emitted when a expression item is double clicked + void expressionItemDoubleClicked( const QString &text ); + + //! Emitter when the current expression item changed + void currentExpressionItemChanged( QgsExpressionItem *item ); + + public slots: + //! Sets the text to filter the expression tree + void setSearchText( const QString &text ); + + + private slots: + void onDoubleClicked( const QModelIndex &index ); + + void showContextMenu( QPoint pt ); + + void currentItemChanged( const QModelIndex &index, const QModelIndex & ); + + private: + void updateFunctionTree(); + + /** + * Registers a node item for the expression builder. + * \param group The group the item will be show in the tree view. If the group doesn't exist it will be created. + * \param label The label that is show to the user for the item in the tree. + * \param expressionText The text that is inserted into the expression area when the user double clicks on the item. + * \param helpText The help text that the user will see when item is selected. + * \param type The type of the expression item. + * \param highlightedItem set to TRUE to make the item highlighted, which inserts a bold copy of the item at the top level + * \param sortOrder sort ranking for item + * \param icon custom icon to show for item + * \param tags tags to find function + */ + void registerItem( const QString &group, const QString &label, const QString &expressionText, + const QString &helpText = QString(), + QgsExpressionItem::ItemType type = QgsExpressionItem::ExpressionNode, + bool highlightedItem = false, int sortOrder = 1, + QIcon icon = QIcon(), + const QStringList &tags = QStringList() ); + + /** + * Registers a node item for the expression builder, adding multiple items when the function exists in multiple groups + * \param groups The groups the item will be show in the tree view. If a group doesn't exist it will be created. + * \param label The label that is show to the user for the item in the tree. + * \param expressionText The text that is inserted into the expression area when the user double clicks on the item. + * \param helpText The help text that the user will see when item is selected. + * \param type The type of the expression item. + * \param highlightedItem set to TRUE to make the item highlighted, which inserts a bold copy of the item at the top level + * \param sortOrder sort ranking for item + * \param tags tags to find function + */ + void registerItemForAllGroups( const QStringList &groups, const QString &label, const QString &expressionText, + const QString &helpText = QString(), + QgsExpressionItem::ItemType type = QgsExpressionItem::ExpressionNode, + bool highlightedItem = false, int sortOrder = 1, const QStringList &tags = QStringList() ); + + void loadExpressionContext(); + void loadRelations(); + void loadLayers(); + void loadFieldNames(); + + /** + * Display a message box to ask the user what to do when an expression + * with the same \a label already exists. Answering "Yes" will replace + * the old expression with the one from the file, while "No" will keep + * the old expression. + * \param isApplyToAll whether the decision of the user should be applied to any future label collision + * \param isOkToOverwrite whether to overwrite the old expression with the new one in case of label collision + * \param label the label of the expression + * \param oldExpression the old expression for a given label + * \param newExpression the new expression for a given label + */ + void showMessageBoxConfirmExpressionOverwrite( bool &isApplyToAll, bool &isOkToOverwrite, const QString &label, const QString &oldExpression, const QString &newExpression ); + + + std::unique_ptr mModel; + std::unique_ptr mProxyModel; + QMap mExpressionGroups; + + MenuProvider *mMenuProvider = nullptr; + + QgsVectorLayer *mLayer = nullptr; + QPointer< QgsProject > mProject; + QgsExpressionContext mExpressionContext; + QString mRecentKey; + + QStringList mUserExpressionLabels; +}; + +#endif // QGSEXPRESSIONTREEVIEW_H diff --git a/src/gui/qgsextentgroupbox.cpp b/src/gui/qgsextentgroupbox.cpp index 1a39ccc34d68..9131f95cf819 100644 --- a/src/gui/qgsextentgroupbox.cpp +++ b/src/gui/qgsextentgroupbox.cpp @@ -14,198 +14,80 @@ ***************************************************************************/ #include "qgsextentgroupbox.h" - -#include "qgslogger.h" -#include "qgscoordinatetransform.h" -#include "qgsmapcanvas.h" -#include "qgsmaplayermodel.h" -#include "qgsexception.h" -#include "qgsproject.h" - -#include -#include -#include +#include "qgsextentwidget.h" QgsExtentGroupBox::QgsExtentGroupBox( QWidget *parent ) : QgsCollapsibleGroupBox( parent ) , mTitleBase( tr( "Extent" ) ) { - setupUi( this ); - connect( mXMinLineEdit, &QLineEdit::textEdited, this, &QgsExtentGroupBox::setOutputExtentFromLineEdit ); - connect( mXMaxLineEdit, &QLineEdit::textEdited, this, &QgsExtentGroupBox::setOutputExtentFromLineEdit ); - connect( mYMinLineEdit, &QLineEdit::textEdited, this, &QgsExtentGroupBox::setOutputExtentFromLineEdit ); - connect( mYMaxLineEdit, &QLineEdit::textEdited, this, &QgsExtentGroupBox::setOutputExtentFromLineEdit ); - - mLayerMenu = new QMenu( this ); - mButtonCalcFromLayer->setMenu( mLayerMenu ); - connect( mLayerMenu, &QMenu::aboutToShow, this, &QgsExtentGroupBox::layerMenuAboutToShow ); - mMapLayerModel = new QgsMapLayerModel( this ); - - mXMinLineEdit->setValidator( new QDoubleValidator( this ) ); - mXMaxLineEdit->setValidator( new QDoubleValidator( this ) ); - mYMinLineEdit->setValidator( new QDoubleValidator( this ) ); - mYMaxLineEdit->setValidator( new QDoubleValidator( this ) ); - - mOriginalExtentButton->setVisible( false ); - mButtonDrawOnCanvas->setVisible( false ); - mCurrentExtentButton->setVisible( false ); - - connect( mCurrentExtentButton, &QAbstractButton::clicked, this, &QgsExtentGroupBox::setOutputExtentFromCurrent ); - connect( mOriginalExtentButton, &QAbstractButton::clicked, this, &QgsExtentGroupBox::setOutputExtentFromOriginal ); - connect( mButtonDrawOnCanvas, &QAbstractButton::clicked, this, &QgsExtentGroupBox::setOutputExtentFromDrawOnCanvas ); - - connect( this, &QGroupBox::clicked, this, &QgsExtentGroupBox::groupBoxClicked ); + mWidget = new QgsExtentWidget( nullptr, QgsExtentWidget::ExpandedStyle ); + QVBoxLayout *layout = new QVBoxLayout(); + layout->addWidget( mWidget ); + setLayout( layout ); + + connect( this, &QGroupBox::toggled, this, &QgsExtentGroupBox::groupBoxClicked ); + connect( mWidget, &QgsExtentWidget::extentChanged, this, &QgsExtentGroupBox::widgetExtentChanged ); + connect( mWidget, &QgsExtentWidget::validationChanged, this, &QgsExtentGroupBox::validationChanged ); + + connect( mWidget, &QgsExtentWidget::toggleDialogVisibility, this, [ = ]( bool visible ) + { + window()->setVisible( visible ); + } ); } void QgsExtentGroupBox::setOriginalExtent( const QgsRectangle &originalExtent, const QgsCoordinateReferenceSystem &originalCrs ) { - mOriginalExtent = originalExtent; - mOriginalCrs = originalCrs; + mWidget->setOriginalExtent( originalExtent, originalCrs ); +} - mOriginalExtentButton->setVisible( true ); +QgsRectangle QgsExtentGroupBox::originalExtent() const +{ + return mWidget->originalExtent(); } +QgsCoordinateReferenceSystem QgsExtentGroupBox::originalCrs() const +{ + return mWidget->originalCrs(); +} void QgsExtentGroupBox::setCurrentExtent( const QgsRectangle ¤tExtent, const QgsCoordinateReferenceSystem ¤tCrs ) { - mCurrentExtent = currentExtent; - mCurrentCrs = currentCrs; - - mCurrentExtentButton->setVisible( true ); + mWidget->setCurrentExtent( currentExtent, currentCrs ); } -void QgsExtentGroupBox::setOutputCrs( const QgsCoordinateReferenceSystem &outputCrs ) +QgsRectangle QgsExtentGroupBox::currentExtent() const { - if ( mOutputCrs != outputCrs ) - { - bool prevExtentEnabled = isChecked(); - switch ( mExtentState ) - { - case CurrentExtent: - mOutputCrs = outputCrs; - setOutputExtentFromCurrent(); - break; - - case OriginalExtent: - mOutputCrs = outputCrs; - setOutputExtentFromOriginal(); - break; - - case ProjectLayerExtent: - mOutputCrs = outputCrs; - setOutputExtentFromLayer( mExtentLayer.data() ); - break; - - case DrawOnCanvas: - mOutputCrs = outputCrs; - extentDrawn( outputExtent() ); - break; - - case UserExtent: - try - { - QgsCoordinateTransform ct( mOutputCrs, outputCrs, QgsProject::instance() ); - QgsRectangle extent = ct.transformBoundingBox( outputExtent() ); - mOutputCrs = outputCrs; - setOutputExtentFromUser( extent, outputCrs ); - } - catch ( QgsCsException & ) - { - // can't reproject - mOutputCrs = outputCrs; - } - break; - } - - if ( !prevExtentEnabled ) - setChecked( false ); - } - + return mWidget->currentExtent(); } -void QgsExtentGroupBox::setOutputExtent( const QgsRectangle &r, const QgsCoordinateReferenceSystem &srcCrs, ExtentState state ) +QgsCoordinateReferenceSystem QgsExtentGroupBox::currentCrs() const { - QgsRectangle extent; - if ( mOutputCrs == srcCrs ) - { - extent = r; - } - else - { - try - { - QgsCoordinateTransform ct( srcCrs, mOutputCrs, QgsProject::instance() ); - extent = ct.transformBoundingBox( r ); - } - catch ( QgsCsException & ) - { - // can't reproject - extent = r; - } - } - - int decimals = 4; - switch ( mOutputCrs.mapUnits() ) - { - case QgsUnitTypes::DistanceDegrees: - case QgsUnitTypes::DistanceUnknownUnit: - decimals = 9; - break; - case QgsUnitTypes::DistanceMeters: - case QgsUnitTypes::DistanceKilometers: - case QgsUnitTypes::DistanceFeet: - case QgsUnitTypes::DistanceNauticalMiles: - case QgsUnitTypes::DistanceYards: - case QgsUnitTypes::DistanceMiles: - case QgsUnitTypes::DistanceCentimeters: - case QgsUnitTypes::DistanceMillimeters: - decimals = 4; - break; - } - mXMinLineEdit->setText( QString::number( extent.xMinimum(), 'f', decimals ) ); - mXMaxLineEdit->setText( QString::number( extent.xMaximum(), 'f', decimals ) ); - mYMinLineEdit->setText( QString::number( extent.yMinimum(), 'f', decimals ) ); - mYMaxLineEdit->setText( QString::number( extent.yMaximum(), 'f', decimals ) ); - - mExtentState = state; - - if ( isCheckable() && !isChecked() ) - setChecked( true ); - - updateTitle(); - - emit extentChanged( extent ); + return mWidget->currentCrs(); } - -void QgsExtentGroupBox::setOutputExtentFromLineEdit() +void QgsExtentGroupBox::setOutputCrs( const QgsCoordinateReferenceSystem &outputCrs ) { - mExtentState = UserExtent; - - updateTitle(); - - emit extentChanged( outputExtent() ); + mWidget->setOutputCrs( outputCrs ); } - void QgsExtentGroupBox::updateTitle() { QString msg; - switch ( mExtentState ) + switch ( mWidget->extentState() ) { - case OriginalExtent: + case QgsExtentWidget::OriginalExtent: msg = tr( "layer" ); break; - case CurrentExtent: + case QgsExtentWidget::CurrentExtent: msg = tr( "map view" ); break; - case UserExtent: + case QgsExtentWidget::UserExtent: msg = tr( "user defined" ); break; - case ProjectLayerExtent: - msg = mExtentLayerName; + case QgsExtentWidget::ProjectLayerExtent: + msg = mWidget->extentLayerName(); break; - case DrawOnCanvas: + case QgsExtentWidget::DrawOnCanvas: msg = tr( "drawn on canvas" ); break; } @@ -216,106 +98,34 @@ void QgsExtentGroupBox::updateTitle() setTitle( msg ); } -void QgsExtentGroupBox::layerMenuAboutToShow() -{ - qDeleteAll( mMenuActions ); - mMenuActions.clear(); - mLayerMenu->clear(); - for ( int i = 0; i < mMapLayerModel->rowCount(); ++i ) - { - QModelIndex index = mMapLayerModel->index( i, 0 ); - QIcon icon = qvariant_cast( mMapLayerModel->data( index, Qt::DecorationRole ) ); - QString text = mMapLayerModel->data( index, Qt::DisplayRole ).toString(); - QAction *act = new QAction( icon, text, mLayerMenu ); - act->setToolTip( mMapLayerModel->data( index, Qt::ToolTipRole ).toString() ); - QString layerId = mMapLayerModel->data( index, QgsMapLayerModel::LayerIdRole ).toString(); - if ( mExtentState == ProjectLayerExtent && mExtentLayer && mExtentLayer->id() == layerId ) - { - act->setCheckable( true ); - act->setChecked( true ); - } - connect( act, &QAction::triggered, this, [this, layerId] - { - setExtentToLayerExtent( layerId ); - } ); - mLayerMenu->addAction( act ); - mMenuActions << act; - } -} - -void QgsExtentGroupBox::setExtentToLayerExtent( const QString &layerId ) -{ - QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId ); - if ( !layer ) - return; - - setOutputExtentFromLayer( layer ); -} - void QgsExtentGroupBox::setOutputExtentFromCurrent() { - if ( mCanvas ) - { - // Use unrotated visible extent to insure output size and scale matches canvas - QgsMapSettings ms = mCanvas->mapSettings(); - ms.setRotation( 0 ); - setOutputExtent( ms.visibleExtent(), ms.destinationCrs(), CurrentExtent ); - } - else - { - setOutputExtent( mCurrentExtent, mCurrentCrs, CurrentExtent ); - } + mWidget->setOutputExtentFromCurrent(); } - void QgsExtentGroupBox::setOutputExtentFromOriginal() { - setOutputExtent( mOriginalExtent, mOriginalCrs, OriginalExtent ); + mWidget->setOutputExtentFromOriginal(); } void QgsExtentGroupBox::setOutputExtentFromUser( const QgsRectangle &extent, const QgsCoordinateReferenceSystem &crs ) { - setOutputExtent( extent, crs, UserExtent ); + mWidget->setOutputExtentFromUser( extent, crs ); } void QgsExtentGroupBox::setOutputExtentFromLayer( const QgsMapLayer *layer ) { - if ( !layer ) - return; - - mExtentLayer = layer; - mExtentLayerName = layer->name(); - - setOutputExtent( layer->extent(), layer->crs(), ProjectLayerExtent ); + mWidget->setOutputExtentFromLayer( layer ); } void QgsExtentGroupBox::setOutputExtentFromDrawOnCanvas() { - if ( mCanvas ) - { - mMapToolPrevious = mCanvas->mapTool(); - if ( !mMapToolExtent ) - { - mMapToolExtent.reset( new QgsMapToolExtent( mCanvas ) ); - connect( mMapToolExtent.get(), &QgsMapToolExtent::extentChanged, this, &QgsExtentGroupBox::extentDrawn ); - connect( mMapToolExtent.get(), &QgsMapTool::deactivated, this, [ = ] - { - window()->setVisible( true ); - mMapToolPrevious = nullptr; - } ); - } - mMapToolExtent->setRatio( mRatio ); - mCanvas->setMapTool( mMapToolExtent.get() ); - window()->setVisible( false ); - } + mWidget->setOutputExtentFromDrawOnCanvas(); } -void QgsExtentGroupBox::extentDrawn( const QgsRectangle &extent ) +void QgsExtentGroupBox::setRatio( QSize ratio ) { - setOutputExtent( extent, mCanvas->mapSettings().destinationCrs(), DrawOnCanvas ); - mCanvas->setMapTool( mMapToolPrevious ); - window()->setVisible( true ); - mMapToolPrevious = nullptr; + mWidget->setRatio( ratio ); } void QgsExtentGroupBox::groupBoxClicked() @@ -329,14 +139,40 @@ void QgsExtentGroupBox::groupBoxClicked() emit extentChanged( outputExtent() ); } +void QgsExtentGroupBox::widgetExtentChanged() +{ + updateTitle(); + + emit extentChanged( outputExtent() ); +} + +void QgsExtentGroupBox::validationChanged( bool valid ) +{ + if ( valid ) + { + if ( isCheckable() && !isChecked() ) + setChecked( true ); + } + else if ( isCheckable() && isChecked() ) + setChecked( false ); +} QgsRectangle QgsExtentGroupBox::outputExtent() const { if ( isCheckable() && !isChecked() ) return QgsRectangle(); - return QgsRectangle( mXMinLineEdit->text().toDouble(), mYMinLineEdit->text().toDouble(), - mXMaxLineEdit->text().toDouble(), mYMaxLineEdit->text().toDouble() ); + return mWidget->outputExtent(); +} + +QgsCoordinateReferenceSystem QgsExtentGroupBox::outputCrs() const +{ + return mWidget->outputCrs(); +} + +QgsExtentGroupBox::ExtentState QgsExtentGroupBox::extentState() const +{ + return static_cast< QgsExtentGroupBox::ExtentState >( mWidget->extentState() ); } void QgsExtentGroupBox::setTitleBase( const QString &title ) @@ -352,15 +188,10 @@ QString QgsExtentGroupBox::titleBase() const void QgsExtentGroupBox::setMapCanvas( QgsMapCanvas *canvas ) { - if ( canvas ) - { - mCanvas = canvas; - mButtonDrawOnCanvas->setVisible( true ); - mCurrentExtentButton->setVisible( true ); - } - else - { - mButtonDrawOnCanvas->setVisible( false ); - mCurrentExtentButton->setVisible( false ); - } + mWidget->setMapCanvas( canvas ); +} + +QSize QgsExtentGroupBox::ratio() const +{ + return mWidget->ratio(); } diff --git a/src/gui/qgsextentgroupbox.h b/src/gui/qgsextentgroupbox.h index a72fbe057b0e..73878569780c 100644 --- a/src/gui/qgsextentgroupbox.h +++ b/src/gui/qgsextentgroupbox.h @@ -17,12 +17,8 @@ #define QGSEXTENTGROUPBOX_H #include "qgscollapsiblegroupbox.h" -#include "qgsmaptool.h" -#include "qgsmaptoolextent.h" #include "qgis_sip.h" -#include "ui_qgsextentgroupboxwidget.h" - #include "qgscoordinatereferencesystem.h" #include "qgsrectangle.h" #include "qgis_gui.h" @@ -30,8 +26,9 @@ #include class QgsCoordinateReferenceSystem; -class QgsMapLayerModel; class QgsMapLayer; +class QgsExtentWidget; +class QgsMapCanvas; /** * \ingroup gui @@ -40,17 +37,22 @@ class QgsMapLayer; * Besides allowing the user to enter the extent manually, it comes with options to use * original extent or extent defined by the current view in map canvas. * - * When using the widget, make sure to call setOriginalExtent(), setCurrentExtent() and setOutputCrs() during initialization. + * When using the group box, make sure to call setOriginalExtent(), setCurrentExtent() and setOutputCrs() during initialization. + * + * + * \see QgsExtentWidget * * \since QGIS 2.4 */ -class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui::QgsExtentGroupBoxWidget +class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox { Q_OBJECT Q_PROPERTY( QString titleBase READ titleBase WRITE setTitleBase ) public: + // TODO QGIS 4.0 -- use QgsExtentWidget enum instead + //! Available states for the current extent selection in the widget enum ExtentState { @@ -78,14 +80,14 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: * \see setOriginalExtent() * \see originalCrs() */ - QgsRectangle originalExtent() const { return mOriginalExtent; } + QgsRectangle originalExtent() const; /** * Returns the original coordinate reference system set for the widget. * \see originalExtent() * \see setOriginalExtent() */ - QgsCoordinateReferenceSystem originalCrs() const { return mOriginalCrs; } + QgsCoordinateReferenceSystem originalCrs() const; /** * Sets the current extent to show in the widget - should be called as part of initialization (or whenever current extent changes). @@ -101,7 +103,7 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: * \see setCurrentExtent() * \see currentCrs() */ - QgsRectangle currentExtent() const { return mCurrentExtent; } + QgsRectangle currentExtent() const; /** * Returns the coordinate reference system for the current extent set for the widget. The current @@ -109,7 +111,7 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: * \see setCurrentExtent() * \see currentExtent() */ - QgsCoordinateReferenceSystem currentCrs() const { return mCurrentCrs; } + QgsCoordinateReferenceSystem currentCrs() const; /** * Sets the output CRS - may need to be used for transformation from original/current extent. @@ -129,12 +131,12 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: * \see outputExtent * \since QGIS 3.0 */ - QgsCoordinateReferenceSystem outputCrs() const { return mOutputCrs; } + QgsCoordinateReferenceSystem outputCrs() const; /** * Returns the currently selected state for the widget's extent. */ - QgsExtentGroupBox::ExtentState extentState() const { return mExtentState; } + QgsExtentGroupBox::ExtentState extentState() const; /** * Sets the base part of \a title of the group box (will be appended with extent state) @@ -162,7 +164,7 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: * If the aspect ratio isn't fixed, the width and height will be set to zero. * \since QGIS 3.0 */ - QSize ratio() const { return mRatio; } + QSize ratio() const; public slots: @@ -199,7 +201,7 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: * \param ratio aspect ratio's width and height * \since QGIS 3.0 */ - void setRatio( QSize ratio ) { mRatio = ratio; } + void setRatio( QSize ratio ); signals: @@ -211,41 +213,19 @@ class GUI_EXPORT QgsExtentGroupBox : public QgsCollapsibleGroupBox, private Ui:: private slots: void groupBoxClicked(); - void layerMenuAboutToShow(); - void extentDrawn( const QgsRectangle &extent ); + void widgetExtentChanged(); + + void validationChanged( bool valid ); private: - void setOutputExtent( const QgsRectangle &r, const QgsCoordinateReferenceSystem &srcCrs, QgsExtentGroupBox::ExtentState state ); - void setOutputExtentFromLineEdit(); void updateTitle(); + QgsExtentWidget *mWidget = nullptr; + //! Base part of the title used for the extent QString mTitleBase; - ExtentState mExtentState = OriginalExtent; - - QgsCoordinateReferenceSystem mOutputCrs; - - QgsRectangle mCurrentExtent; - QgsCoordinateReferenceSystem mCurrentCrs; - - QgsRectangle mOriginalExtent; - QgsCoordinateReferenceSystem mOriginalCrs; - - QMenu *mLayerMenu = nullptr; - QgsMapLayerModel *mMapLayerModel = nullptr; - QList< QAction * > mMenuActions; - QPointer< const QgsMapLayer > mExtentLayer; - QString mExtentLayerName; - - std::unique_ptr< QgsMapToolExtent > mMapToolExtent; - QPointer< QgsMapTool > mMapToolPrevious = nullptr; - QgsMapCanvas *mCanvas = nullptr; - QSize mRatio; - - void setExtentToLayerExtent( const QString &layerId ); - }; #endif // QGSEXTENTGROUPBOX_H diff --git a/src/gui/qgsextentwidget.cpp b/src/gui/qgsextentwidget.cpp new file mode 100644 index 000000000000..393a99d6c1a0 --- /dev/null +++ b/src/gui/qgsextentwidget.cpp @@ -0,0 +1,496 @@ +/*************************************************************************** + qgsextentwidget.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 "qgsextentwidget.h" + +#include "qgslogger.h" +#include "qgscoordinatetransform.h" +#include "qgsmapcanvas.h" +#include "qgsmaplayermodel.h" +#include "qgsexception.h" +#include "qgsproject.h" + +#include +#include +#include +#include + +QgsExtentWidget::QgsExtentWidget( QWidget *parent, WidgetStyle style ) + : QWidget( parent ) +{ + setupUi( this ); + connect( mXMinLineEdit, &QLineEdit::textEdited, this, &QgsExtentWidget::setOutputExtentFromLineEdit ); + connect( mXMaxLineEdit, &QLineEdit::textEdited, this, &QgsExtentWidget::setOutputExtentFromLineEdit ); + connect( mYMinLineEdit, &QLineEdit::textEdited, this, &QgsExtentWidget::setOutputExtentFromLineEdit ); + connect( mYMaxLineEdit, &QLineEdit::textEdited, this, &QgsExtentWidget::setOutputExtentFromLineEdit ); + + mCondensedRe = QRegularExpression( QStringLiteral( "\\s*([\\d\\.]+)\\s*,\\s*([\\d\\.]+)\\s*,\\s*([\\d\\.]+)\\s*,\\s*([\\d\\.]+)\\s*(\\[.*?\\])" ) ); + mCondensedLineEdit->setValidator( new QRegularExpressionValidator( mCondensedRe, this ) ); + mCondensedLineEdit->setShowClearButton( false ); + connect( mCondensedLineEdit, &QgsFilterLineEdit::cleared, this, &QgsExtentWidget::clear ); + + connect( mCondensedLineEdit, &QLineEdit::textEdited, this, &QgsExtentWidget::setOutputExtentFromCondensedLineEdit ); + + mLayerMenu = new QMenu( tr( "Calculate from Layer" ) ); + mButtonCalcFromLayer->setMenu( mLayerMenu ); + connect( mLayerMenu, &QMenu::aboutToShow, this, &QgsExtentWidget::layerMenuAboutToShow ); + mMapLayerModel = new QgsMapLayerModel( this ); + + mMenu = new QMenu( this ); + mUseCanvasExtentAction = new QAction( tr( "Use Map Canvas Extent" ), this ); + connect( mUseCanvasExtentAction, &QAction::triggered, this, &QgsExtentWidget::setOutputExtentFromCurrent ); + + mUseCurrentExtentAction = new QAction( tr( "Use Current Layer Extent" ), this ); + connect( mUseCurrentExtentAction, &QAction::triggered, this, &QgsExtentWidget::setOutputExtentFromCurrent ); + + mDrawOnCanvasAction = new QAction( tr( "Draw on Canvas" ), this ); + connect( mDrawOnCanvasAction, &QAction::triggered, this, &QgsExtentWidget::setOutputExtentFromDrawOnCanvas ); + + mMenu->addMenu( mLayerMenu ); + + mCondensedToolButton->setMenu( mMenu ); + mCondensedToolButton->setPopupMode( QToolButton::InstantPopup ); + + mXMinLineEdit->setValidator( new QDoubleValidator( this ) ); + mXMaxLineEdit->setValidator( new QDoubleValidator( this ) ); + mYMinLineEdit->setValidator( new QDoubleValidator( this ) ); + mYMaxLineEdit->setValidator( new QDoubleValidator( this ) ); + + mOriginalExtentButton->setVisible( false ); + mButtonDrawOnCanvas->setVisible( false ); + mCurrentExtentButton->setVisible( false ); + + connect( mCurrentExtentButton, &QAbstractButton::clicked, this, &QgsExtentWidget::setOutputExtentFromCurrent ); + connect( mOriginalExtentButton, &QAbstractButton::clicked, this, &QgsExtentWidget::setOutputExtentFromOriginal ); + connect( mButtonDrawOnCanvas, &QAbstractButton::clicked, this, &QgsExtentWidget::setOutputExtentFromDrawOnCanvas ); + + switch ( style ) + { + case CondensedStyle: + mExpandedWidget->hide(); + break; + + case ExpandedStyle: + mCondensedFrame->hide(); + break; + } + + setAcceptDrops( true ); +} + +void QgsExtentWidget::setOriginalExtent( const QgsRectangle &originalExtent, const QgsCoordinateReferenceSystem &originalCrs ) +{ + mOriginalExtent = originalExtent; + mOriginalCrs = originalCrs; + + mOriginalExtentButton->setVisible( true ); +} + + +void QgsExtentWidget::setCurrentExtent( const QgsRectangle ¤tExtent, const QgsCoordinateReferenceSystem ¤tCrs ) +{ + mCurrentExtent = currentExtent; + mCurrentCrs = currentCrs; + + mCurrentExtentButton->setVisible( true ); + mMenu->addAction( mUseCurrentExtentAction ); +} + +void QgsExtentWidget::setOutputCrs( const QgsCoordinateReferenceSystem &outputCrs ) +{ + mHasFixedOutputCrs = true; + if ( mOutputCrs != outputCrs ) + { + bool prevExtentEnabled = mIsValid; + switch ( mExtentState ) + { + case CurrentExtent: + mOutputCrs = outputCrs; + setOutputExtentFromCurrent(); + break; + + case OriginalExtent: + mOutputCrs = outputCrs; + setOutputExtentFromOriginal(); + break; + + case ProjectLayerExtent: + mOutputCrs = outputCrs; + setOutputExtentFromLayer( mExtentLayer.data() ); + break; + + case DrawOnCanvas: + mOutputCrs = outputCrs; + extentDrawn( outputExtent() ); + break; + + case UserExtent: + try + { + QgsCoordinateTransform ct( mOutputCrs, outputCrs, QgsProject::instance() ); + QgsRectangle extent = ct.transformBoundingBox( outputExtent() ); + mOutputCrs = outputCrs; + setOutputExtentFromUser( extent, outputCrs ); + } + catch ( QgsCsException & ) + { + // can't reproject + mOutputCrs = outputCrs; + } + break; + } + + if ( !prevExtentEnabled ) + setValid( false ); + } +} + +void QgsExtentWidget::setOutputExtent( const QgsRectangle &r, const QgsCoordinateReferenceSystem &srcCrs, ExtentState state ) +{ + QgsRectangle extent; + if ( !mHasFixedOutputCrs ) + { + mOutputCrs = srcCrs; + extent = r; + } + else + { + if ( mOutputCrs == srcCrs ) + { + extent = r; + } + else + { + try + { + QgsCoordinateTransform ct( srcCrs, mOutputCrs, QgsProject::instance() ); + extent = ct.transformBoundingBox( r ); + } + catch ( QgsCsException & ) + { + // can't reproject + extent = r; + } + } + } + + int decimals = 4; + switch ( mOutputCrs.mapUnits() ) + { + case QgsUnitTypes::DistanceDegrees: + case QgsUnitTypes::DistanceUnknownUnit: + decimals = 9; + break; + case QgsUnitTypes::DistanceMeters: + case QgsUnitTypes::DistanceKilometers: + case QgsUnitTypes::DistanceFeet: + case QgsUnitTypes::DistanceNauticalMiles: + case QgsUnitTypes::DistanceYards: + case QgsUnitTypes::DistanceMiles: + case QgsUnitTypes::DistanceCentimeters: + case QgsUnitTypes::DistanceMillimeters: + decimals = 4; + break; + } + mXMinLineEdit->setText( QString::number( extent.xMinimum(), 'f', decimals ) ); + mXMaxLineEdit->setText( QString::number( extent.xMaximum(), 'f', decimals ) ); + mYMinLineEdit->setText( QString::number( extent.yMinimum(), 'f', decimals ) ); + mYMaxLineEdit->setText( QString::number( extent.yMaximum(), 'f', decimals ) ); + + QString condensed = QStringLiteral( "%1,%2,%3,%4" ).arg( mXMinLineEdit->text(), + mXMaxLineEdit->text(), + mYMinLineEdit->text(), + mYMaxLineEdit->text() ); + condensed += QStringLiteral( " [%1]" ).arg( mOutputCrs.userFriendlyIdentifier( QgsCoordinateReferenceSystem::ShortString ) ); + mCondensedLineEdit->setText( condensed ); + + mExtentState = state; + + if ( !mIsValid ) + setValid( true ); + + emit extentChanged( extent ); +} + +void QgsExtentWidget::setOutputExtentFromLineEdit() +{ + mExtentState = UserExtent; + emit extentChanged( outputExtent() ); +} + +void QgsExtentWidget::setOutputExtentFromCondensedLineEdit() +{ + const QString text = mCondensedLineEdit->text(); + if ( text.isEmpty() ) + { + clear(); + } + else + { + const QRegularExpressionMatch match = mCondensedRe.match( text ); + if ( match.hasMatch() ) + { + whileBlocking( mXMinLineEdit )->setText( match.captured( 1 ) ); + whileBlocking( mXMaxLineEdit )->setText( match.captured( 2 ) ); + whileBlocking( mYMinLineEdit )->setText( match.captured( 3 ) ); + whileBlocking( mYMaxLineEdit )->setText( match.captured( 4 ) ); + emit extentChanged( outputExtent() ); + } + } +} + +void QgsExtentWidget::clear() +{ + bool prevWasNull = mIsValid; + + whileBlocking( mXMinLineEdit )->clear(); + whileBlocking( mXMaxLineEdit )->clear(); + whileBlocking( mYMinLineEdit )->clear(); + whileBlocking( mYMaxLineEdit )->clear(); + whileBlocking( mCondensedLineEdit )->clearValue(); + setValid( false ); + + if ( prevWasNull ) + emit extentChanged( outputExtent() ); +} + +QString QgsExtentWidget::extentLayerName() const +{ + return mExtentLayerName; +} + +bool QgsExtentWidget::isValid() const +{ + return mIsValid; +} + +void QgsExtentWidget::setNullValueAllowed( bool allowed, const QString ¬SetText ) +{ + mCondensedLineEdit->setShowClearButton( allowed ); + mCondensedLineEdit->setNullValue( notSetText ); +} + +void QgsExtentWidget::setValid( bool valid ) +{ + if ( valid == mIsValid ) + return; + + mIsValid = valid; + emit validationChanged( mIsValid ); +} + +void QgsExtentWidget::layerMenuAboutToShow() +{ + qDeleteAll( mLayerMenuActions ); + mLayerMenuActions.clear(); + mLayerMenu->clear(); + for ( int i = 0; i < mMapLayerModel->rowCount(); ++i ) + { + QModelIndex index = mMapLayerModel->index( i, 0 ); + QIcon icon = qvariant_cast( mMapLayerModel->data( index, Qt::DecorationRole ) ); + QString text = mMapLayerModel->data( index, Qt::DisplayRole ).toString(); + QAction *act = new QAction( icon, text, mLayerMenu ); + act->setToolTip( mMapLayerModel->data( index, Qt::ToolTipRole ).toString() ); + QString layerId = mMapLayerModel->data( index, QgsMapLayerModel::LayerIdRole ).toString(); + if ( mExtentState == ProjectLayerExtent && mExtentLayer && mExtentLayer->id() == layerId ) + { + act->setCheckable( true ); + act->setChecked( true ); + } + connect( act, &QAction::triggered, this, [this, layerId] + { + setExtentToLayerExtent( layerId ); + } ); + mLayerMenu->addAction( act ); + mLayerMenuActions << act; + } +} + +void QgsExtentWidget::setExtentToLayerExtent( const QString &layerId ) +{ + QgsMapLayer *layer = QgsProject::instance()->mapLayer( layerId ); + if ( !layer ) + return; + + setOutputExtentFromLayer( layer ); +} + +QgsMapLayer *QgsExtentWidget::mapLayerFromMimeData( const QMimeData *data ) const +{ + const QgsMimeDataUtils::UriList uriList = QgsMimeDataUtils::decodeUriList( data ); + for ( const QgsMimeDataUtils::Uri &u : uriList ) + { + // is this uri from the current project? + if ( QgsMapLayer *layer = u.mapLayer() ) + { + return layer; + } + } + return nullptr; +} + +void QgsExtentWidget::setOutputExtentFromCurrent() +{ + if ( mCanvas ) + { + // Use unrotated visible extent to insure output size and scale matches canvas + QgsMapSettings ms = mCanvas->mapSettings(); + ms.setRotation( 0 ); + setOutputExtent( ms.visibleExtent(), ms.destinationCrs(), CurrentExtent ); + } + else + { + setOutputExtent( mCurrentExtent, mCurrentCrs, CurrentExtent ); + } +} + + +void QgsExtentWidget::setOutputExtentFromOriginal() +{ + setOutputExtent( mOriginalExtent, mOriginalCrs, OriginalExtent ); +} + +void QgsExtentWidget::setOutputExtentFromUser( const QgsRectangle &extent, const QgsCoordinateReferenceSystem &crs ) +{ + setOutputExtent( extent, crs, UserExtent ); +} + +void QgsExtentWidget::setOutputExtentFromLayer( const QgsMapLayer *layer ) +{ + if ( !layer ) + return; + + mExtentLayer = layer; + mExtentLayerName = layer->name(); + + setOutputExtent( layer->extent(), layer->crs(), ProjectLayerExtent ); +} + +void QgsExtentWidget::setOutputExtentFromDrawOnCanvas() +{ + if ( mCanvas ) + { + mMapToolPrevious = mCanvas->mapTool(); + if ( !mMapToolExtent ) + { + mMapToolExtent.reset( new QgsMapToolExtent( mCanvas ) ); + connect( mMapToolExtent.get(), &QgsMapToolExtent::extentChanged, this, &QgsExtentWidget::extentDrawn ); + connect( mMapToolExtent.get(), &QgsMapTool::deactivated, this, [ = ] + { + emit toggleDialogVisibility( true ); + mMapToolPrevious = nullptr; + } ); + } + mMapToolExtent->setRatio( mRatio ); + mCanvas->setMapTool( mMapToolExtent.get() ); + + emit toggleDialogVisibility( false ); + } +} + +void QgsExtentWidget::extentDrawn( const QgsRectangle &extent ) +{ + setOutputExtent( extent, mCanvas->mapSettings().destinationCrs(), DrawOnCanvas ); + mCanvas->setMapTool( mMapToolPrevious ); + emit toggleDialogVisibility( true ); + mMapToolPrevious = nullptr; +} + +QgsRectangle QgsExtentWidget::outputExtent() const +{ + return QgsRectangle( mXMinLineEdit->text().toDouble(), mYMinLineEdit->text().toDouble(), + mXMaxLineEdit->text().toDouble(), mYMaxLineEdit->text().toDouble() ); +} + +void QgsExtentWidget::setMapCanvas( QgsMapCanvas *canvas ) +{ + if ( canvas ) + { + mCanvas = canvas; + mButtonDrawOnCanvas->setVisible( true ); + mCurrentExtentButton->setVisible( true ); + + mMenu->addAction( mUseCanvasExtentAction ); + mMenu->addAction( mDrawOnCanvasAction ); + } + else + { + mButtonDrawOnCanvas->setVisible( false ); + mCurrentExtentButton->setVisible( false ); + mMenu->removeAction( mUseCanvasExtentAction ); + mMenu->removeAction( mDrawOnCanvasAction ); + } +} + +void QgsExtentWidget::dragEnterEvent( QDragEnterEvent *event ) +{ + if ( !( event->possibleActions() & Qt::CopyAction ) ) + { + event->ignore(); + return; + } + + if ( mapLayerFromMimeData( event->mimeData() ) ) + { + // dragged an acceptable layer, phew + event->setDropAction( Qt::CopyAction ); + event->accept(); + mCondensedLineEdit->setHighlighted( true ); + update(); + } + else + { + event->ignore(); + } +} + +void QgsExtentWidget::dragLeaveEvent( QDragLeaveEvent *event ) +{ + if ( mCondensedLineEdit->isHighlighted() ) + { + event->accept(); + mCondensedLineEdit->setHighlighted( false ); + update(); + } + else + { + event->ignore(); + } +} + +void QgsExtentWidget::dropEvent( QDropEvent *event ) +{ + if ( !( event->possibleActions() & Qt::CopyAction ) ) + { + event->ignore(); + return; + } + + if ( QgsMapLayer *layer = mapLayerFromMimeData( event->mimeData() ) ) + { + // dropped a map layer + setFocus( Qt::MouseFocusReason ); + event->setDropAction( Qt::CopyAction ); + event->accept(); + + setOutputExtentFromLayer( layer ); + } + else + { + event->ignore(); + } + mCondensedLineEdit->setHighlighted( false ); + update(); +} diff --git a/src/gui/qgsextentwidget.h b/src/gui/qgsextentwidget.h new file mode 100644 index 000000000000..e925dfdd30c9 --- /dev/null +++ b/src/gui/qgsextentwidget.h @@ -0,0 +1,291 @@ +/*************************************************************************** + qgsextentwidget.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson 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 QGSEXTENTWIDGET_H +#define QGSEXTENTWIDGET_H + +#include "qgscollapsiblegroupbox.h" +#include "qgsmaptool.h" +#include "qgsmaptoolextent.h" +#include "qgis_sip.h" + +#include "ui_qgsextentgroupboxwidget.h" + +#include "qgscoordinatereferencesystem.h" +#include "qgsrectangle.h" +#include "qgis_gui.h" + +#include +#include + +class QgsCoordinateReferenceSystem; +class QgsMapLayerModel; +class QgsMapLayer; + +/** + * \ingroup gui + * A widget for configuration of a map extent. + * + * Besides allowing the user to enter the extent manually, it comes with options to use + * original extent or extent defined by the current view in map canvas. + * + * When using the widget, make sure to call setOriginalExtent(), setCurrentExtent() and setOutputCrs() during initialization. + * + * \see QgsExtentGroupBox + * + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsExtentWidget : public QWidget, private Ui::QgsExtentGroupBoxWidget +{ + Q_OBJECT + + public: + + //! Available states for the current extent selection in the widget + enum ExtentState + { + OriginalExtent, //!< Layer's extent + CurrentExtent, //!< Map canvas extent + UserExtent, //!< Extent manually entered/modified by the user + ProjectLayerExtent, //!< Extent taken from a layer within the project + DrawOnCanvas, //!< Extent taken from a rectangled drawn onto the map canvas + }; + + //! Widget styles + enum WidgetStyle + { + CondensedStyle, //!< Shows a compressed widget, for use when available space is minimal + ExpandedStyle, //!< Shows an expanded widget, for use when space is not constrained + }; + + /** + * Constructor for QgsExtentWidget. + */ + explicit QgsExtentWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr, WidgetStyle style = CondensedStyle ); + + /** + * Sets the original extent and coordinate reference system for the widget. This should be called as part of initialization. + * \see originalExtent() + * \see originalCrs() + */ + void setOriginalExtent( const QgsRectangle &originalExtent, const QgsCoordinateReferenceSystem &originalCrs ); + + /** + * Returns the original extent set for the widget. + * \see setOriginalExtent() + * \see originalCrs() + */ + QgsRectangle originalExtent() const { return mOriginalExtent; } + + /** + * Returns the original coordinate reference system set for the widget. + * \see originalExtent() + * \see setOriginalExtent() + */ + QgsCoordinateReferenceSystem originalCrs() const { return mOriginalCrs; } + + /** + * Sets the current extent to show in the widget - should be called as part of initialization (or whenever current extent changes). + * The current extent is usually set to match the current map canvas extent. + * \see currentExtent() + * \see currentCrs() + */ + void setCurrentExtent( const QgsRectangle ¤tExtent, const QgsCoordinateReferenceSystem ¤tCrs ); + + /** + * Returns the current extent set for the widget. The current extent is usually set to match the + * current map canvas extent. + * \see setCurrentExtent() + * \see currentCrs() + */ + QgsRectangle currentExtent() const { return mCurrentExtent; } + + /** + * Returns the coordinate reference system for the current extent set for the widget. The current + * extent and CRS usually reflects the map canvas extent and CRS. + * \see setCurrentExtent() + * \see currentExtent() + */ + QgsCoordinateReferenceSystem currentCrs() const { return mCurrentCrs; } + + /** + * Sets the output CRS - may need to be used for transformation from original/current extent. + * Should be called as part of initialization and whenever the the output CRS is changed. + * The current extent will be reprojected into the new output CRS. + */ + void setOutputCrs( const QgsCoordinateReferenceSystem &outputCrs ); + + /** + * Returns the extent shown in the widget - in output CRS coordinates. + * \see outputCrs + */ + QgsRectangle outputExtent() const; + + /** + * Returns the current output CRS, used in the display. + * \see outputExtent + */ + QgsCoordinateReferenceSystem outputCrs() const { return mOutputCrs; } + + /** + * Returns the currently selected state for the widget's extent. + */ + QgsExtentWidget::ExtentState extentState() const { return mExtentState; } + + /** + * Sets the map canvas to enable dragging of extent on a canvas. + * \param canvas the map canvas + */ + void setMapCanvas( QgsMapCanvas *canvas ); + + /** + * Returns the current fixed aspect ratio to be used when dragging extent onto the canvas. + * If the aspect ratio isn't fixed, the width and height will be set to zero. + */ + QSize ratio() const { return mRatio; } + + /** + * Returns the name of the extent layer. + */ + QString extentLayerName() const; + + /** + * Returns TRUE if the widget is in a valid state, i.e. has an extent set. + */ + bool isValid() const; + + /** + * Sets whether the widget can be set to a "not set" (null) state. + * + * The specified \a notSetText will be used for showing null values. + * + * \note This mode only applies to widgets in the condensed state! + */ + void setNullValueAllowed( bool allowed, const QString ¬SetText = QString() ); + + public slots: + + /** + * Sets the output extent to be the same as original extent (may be transformed to output CRS). + */ + void setOutputExtentFromOriginal(); + + /** + * Sets the output extent to be the same as current extent (may be transformed to output CRS). + */ + void setOutputExtentFromCurrent(); + + /** + * Sets the output extent to a custom extent (may be transformed to output CRS). + */ + void setOutputExtentFromUser( const QgsRectangle &extent, const QgsCoordinateReferenceSystem &crs ); + + /** + * Sets the output extent to match a \a layer's extent (may be transformed to output CRS). + */ + void setOutputExtentFromLayer( const QgsMapLayer *layer ); + + /** + * Sets the output extent by dragging on the canvas. + */ + void setOutputExtentFromDrawOnCanvas(); + + /** + * Sets a fixed aspect ratio to be used when dragging extent onto the canvas. + * To unset a fixed aspect ratio, set the width and height to zero. + * \param ratio aspect ratio's width and height + */ + void setRatio( QSize ratio ) { mRatio = ratio; } + + /** + * Clears the widget, setting it to a null value. + */ + void clear(); + + signals: + + /** + * Emitted when the widget's extent is changed. + */ + void extentChanged( const QgsRectangle &r ); + + /** + * Emitted when the widget's validation state changes. + */ + void validationChanged( bool valid ); + + /** + * Emitted when the parent dialog visibility must be changed (e.g. + * to permit access to the map canvas) + */ + void toggleDialogVisibility( bool visible ); + + protected: + + void dragEnterEvent( QDragEnterEvent *event ) override; + void dragLeaveEvent( QDragLeaveEvent *event ) override; + void dropEvent( QDropEvent *event ) override; + + private slots: + + void layerMenuAboutToShow(); + + void extentDrawn( const QgsRectangle &extent ); + + private: + void setOutputExtent( const QgsRectangle &r, const QgsCoordinateReferenceSystem &srcCrs, QgsExtentWidget::ExtentState state ); + void setOutputExtentFromLineEdit(); + void setOutputExtentFromCondensedLineEdit(); + + ExtentState mExtentState = OriginalExtent; + + QgsCoordinateReferenceSystem mOutputCrs; + + QgsRectangle mCurrentExtent; + QgsCoordinateReferenceSystem mCurrentCrs; + + QgsRectangle mOriginalExtent; + QgsCoordinateReferenceSystem mOriginalCrs; + + QMenu *mMenu = nullptr; + QMenu *mLayerMenu = nullptr; + QgsMapLayerModel *mMapLayerModel = nullptr; + QList< QAction * > mLayerMenuActions; + QAction *mUseCanvasExtentAction = nullptr; + QAction *mUseCurrentExtentAction = nullptr; + QAction *mDrawOnCanvasAction = nullptr; + + QPointer< const QgsMapLayer > mExtentLayer; + QString mExtentLayerName; + + std::unique_ptr< QgsMapToolExtent > mMapToolExtent; + QPointer< QgsMapTool > mMapToolPrevious = nullptr; + QgsMapCanvas *mCanvas = nullptr; + QSize mRatio; + + bool mIsValid = false; + bool mHasFixedOutputCrs = false; + + QRegularExpression mCondensedRe; + void setValid( bool valid ); + + void setExtentToLayerExtent( const QString &layerId ); + + QgsMapLayer *mapLayerFromMimeData( const QMimeData *data ) const; + + +}; + +#endif // QGSEXTENTWIDGET_H diff --git a/src/gui/qgsfeaturelistcombobox.cpp b/src/gui/qgsfeaturelistcombobox.cpp index fe95578d4d74..459452d14ccd 100644 --- a/src/gui/qgsfeaturelistcombobox.cpp +++ b/src/gui/qgsfeaturelistcombobox.cpp @@ -236,6 +236,7 @@ bool QgsFeatureListComboBox::allowNull() const void QgsFeatureListComboBox::setAllowNull( bool allowNull ) { mModel->setAllowNull( allowNull ); + mLineEdit->setClearMode( allowNull ? QgsFilterLineEdit::ClearToNull : QgsFilterLineEdit::ClearToDefault ); } QVariant QgsFeatureListComboBox::identifierValue() const diff --git a/src/gui/qgsfieldexpressionwidget.cpp b/src/gui/qgsfieldexpressionwidget.cpp index eb5955de5009..7d5e61af7da2 100644 --- a/src/gui/qgsfieldexpressionwidget.cpp +++ b/src/gui/qgsfieldexpressionwidget.cpp @@ -32,7 +32,7 @@ QgsFieldExpressionWidget::QgsFieldExpressionWidget( QWidget *parent ) : QWidget( parent ) , mExpressionDialogTitle( tr( "Expression Dialog" ) ) - , mDa( nullptr ) + , mDistanceArea( nullptr ) { QHBoxLayout *layout = new QHBoxLayout( this ); @@ -112,7 +112,7 @@ void QgsFieldExpressionWidget::setLeftHandButtonStyle( bool isLeft ) void QgsFieldExpressionWidget::setGeomCalculator( const QgsDistanceArea &da ) { - mDa = std::shared_ptr( new QgsDistanceArea( da ) ); + mDistanceArea = std::shared_ptr( new QgsDistanceArea( da ) ); } QString QgsFieldExpressionWidget::currentText() const @@ -218,6 +218,11 @@ void QgsFieldExpressionWidget::setField( const QString &fieldName ) currentFieldChanged(); } +void QgsFieldExpressionWidget::setFields( const QgsFields &fields ) +{ + mFieldProxyModel->sourceFieldModel()->setFields( fields ); +} + void QgsFieldExpressionWidget::setExpression( const QString &expression ) { setField( expression ); @@ -231,13 +236,16 @@ void QgsFieldExpressionWidget::editExpression() QgsExpressionContext context = mExpressionContextGenerator ? mExpressionContextGenerator->createExpressionContext() : mExpressionContext; QgsExpressionBuilderDialog dlg( vl, currentExpression, this, QStringLiteral( "generic" ), context ); - if ( mDa ) + if ( mDistanceArea ) { - dlg.setGeomCalculator( *mDa ); + dlg.setGeomCalculator( *mDistanceArea ); } dlg.setWindowTitle( mExpressionDialogTitle ); dlg.setAllowEvalErrors( mAllowEvalErrors ); + if ( !vl ) + dlg.expressionBuilder()->expressionTree()->loadFieldNames( mFieldProxyModel->sourceFieldModel()->fields() ); + if ( dlg.exec() ) { QString newExpression = dlg.expressionText(); diff --git a/src/gui/qgsfieldexpressionwidget.h b/src/gui/qgsfieldexpressionwidget.h index dda4083cac11..1049d218a057 100644 --- a/src/gui/qgsfieldexpressionwidget.h +++ b/src/gui/qgsfieldexpressionwidget.h @@ -37,7 +37,7 @@ class QgsExpressionContextGenerator; /** * \ingroup gui * \brief The QgsFieldExpressionWidget class reates a widget to choose fields and edit expressions - * It contains a combo boxto display the fields and expression and a button to open the expression dialog. + * It contains a combo box to display the fields and expression and a button to open the expression dialog. * The combo box is editable, allowing expressions to be edited inline. * The validity of the expression is checked live on key press, invalid expressions are displayed in red. * The expression will be added to the model (and the fieldChanged signals emitted) @@ -199,6 +199,12 @@ class GUI_EXPORT QgsFieldExpressionWidget : public QWidget //! sets the current field or expression in the widget void setField( const QString &fieldName ); + /** + * Sets the fields used in the widget to \a fields, this allows the widget to work without a layer. + * \since QGIS 3.14 + */ + void setFields( const QgsFields &fields ); + /** * Sets the current expression text and if applicable also the field. * Alias for setField. @@ -244,7 +250,7 @@ class GUI_EXPORT QgsFieldExpressionWidget : public QWidget QToolButton *mButton = nullptr; QgsFieldProxyModel *mFieldProxyModel = nullptr; QString mExpressionDialogTitle; - std::shared_ptr mDa; + std::shared_ptr mDistanceArea; QgsExpressionContext mExpressionContext; const QgsExpressionContextGenerator *mExpressionContextGenerator = nullptr; QString mBackupExpression; diff --git a/src/gui/qgsfieldmappingmodel.cpp b/src/gui/qgsfieldmappingmodel.cpp new file mode 100644 index 000000000000..fc3beb1f994c --- /dev/null +++ b/src/gui/qgsfieldmappingmodel.cpp @@ -0,0 +1,506 @@ +/*************************************************************************** + qgsfieldmappingmodel.cpp - QgsFieldMappingModel + + --------------------- + begin : 17.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgsfieldmappingmodel.h" +#include "qgsexpressioncontextutils.h" +#include "qgsexpressionnodeimpl.h" + +QgsFieldMappingModel::QgsFieldMappingModel( const QgsFields &sourceFields, + const QgsFields &destinationFields, + const QMap &expressions, + QObject *parent ) + : QAbstractTableModel( parent ) + , mSourceFields( sourceFields ) + , mExpressionContextGenerator( new ExpressionContextGenerator( &mSourceFields ) ) +{ + setDestinationFields( destinationFields, expressions ); +} + +QVariant QgsFieldMappingModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( role == Qt::DisplayRole ) + { + switch ( orientation ) + { + case Qt::Horizontal: + { + switch ( static_cast( section ) ) + { + case ColumnDataIndex::SourceExpression: + { + return tr( "Source expression" ); + } + case ColumnDataIndex::DestinationName: + { + return tr( "Name" ); + } + case ColumnDataIndex::DestinationType: + { + return tr( "Type" ); + } + case ColumnDataIndex::DestinationLength: + { + return tr( "Length" ); + } + case ColumnDataIndex::DestinationPrecision: + { + return tr( "Precision" ); + } + case ColumnDataIndex::DestinationConstraints: + { + return tr( "Constraints" ); + } + } + break; + } + case Qt::Vertical: + { + return section; + } + } + } + return QVariant(); +} + +QgsFields QgsFieldMappingModel::sourceFields() const +{ + return mSourceFields; +} + +int QgsFieldMappingModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return mMapping.count(); +} + +int QgsFieldMappingModel::columnCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + return 6; +} + +QVariant QgsFieldMappingModel::data( const QModelIndex &index, int role ) const +{ + if ( index.isValid() ) + { + const ColumnDataIndex col { static_cast( index.column() ) }; + const Field &f { mMapping.at( index.row() ) }; + + const QgsFieldConstraints::Constraints constraints { fieldConstraints( f.field ) }; + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::EditRole: + { + switch ( col ) + { + case ColumnDataIndex::SourceExpression: + { + return f.expression; + } + case ColumnDataIndex::DestinationName: + { + return f.field.displayName(); + } + case ColumnDataIndex::DestinationType: + { + return static_cast( f.field.type() ); + } + case ColumnDataIndex::DestinationLength: + { + return f.field.length(); + } + case ColumnDataIndex::DestinationPrecision: + { + return f.field.precision(); + } + case ColumnDataIndex::DestinationConstraints: + { + return constraints != 0 ? tr( "Constraints active" ) : QString(); + } + } + break; + } + case Qt::ToolTipRole: + { + if ( col == ColumnDataIndex::DestinationConstraints && + constraints != 0 ) + { + QStringList constraintDescription; + if ( constraints.testFlag( QgsFieldConstraints::Constraint::ConstraintUnique ) ) + { + constraintDescription.push_back( tr( "Unique" ) ); + } + if ( constraints.testFlag( QgsFieldConstraints::Constraint::ConstraintNotNull ) ) + { + constraintDescription.push_back( tr( "Not null" ) ); + } + if ( constraints.testFlag( QgsFieldConstraints::Constraint::ConstraintExpression ) ) + { + constraintDescription.push_back( tr( "Expression" ) ); + } + return constraintDescription.join( QStringLiteral( "
    " ) ); + } + break; + } + case Qt::BackgroundRole: + { + if ( constraints != 0 ) + { + return QBrush( QColor( 255, 224, 178 ) ); + } + break; + } + } + } + return QVariant(); +} + +Qt::ItemFlags QgsFieldMappingModel::flags( const QModelIndex &index ) const +{ + if ( index.isValid() && + index.column() != static_cast( ColumnDataIndex::DestinationConstraints ) && + ( index.column() == static_cast( ColumnDataIndex::SourceExpression ) || destinationEditable() ) ) + { + return Qt::ItemFlags( Qt::ItemIsSelectable | + Qt::ItemIsEditable | + Qt::ItemIsEnabled ); + } + return Qt::ItemFlags(); +} + +bool QgsFieldMappingModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( index.isValid() ) + { + if ( role == Qt::EditRole ) + { + Field &f = mMapping[index.row()]; + switch ( static_cast( index.column() ) ) + { + case ColumnDataIndex::SourceExpression: + { + const QgsExpression exp { value.toString() }; + f.expression = exp; + break; + } + case ColumnDataIndex::DestinationName: + { + f.field.setName( value.toString() ); + break; + } + case ColumnDataIndex::DestinationType: + { + f.field.setType( static_cast( value.toInt( ) ) ); + break; + } + case ColumnDataIndex::DestinationLength: + { + bool ok; + const int length { value.toInt( &ok ) }; + if ( ok ) + f.field.setLength( length ); + break; + } + case ColumnDataIndex::DestinationPrecision: + { + bool ok; + const int precision { value.toInt( &ok ) }; + if ( ok ) + f.field.setPrecision( precision ); + break; + } + case ColumnDataIndex::DestinationConstraints: + { + // Not editable: do nothing + } + } + emit dataChanged( index, index ); + } + return true; + } + else + { + return false; + } +} + +QgsFieldConstraints::Constraints QgsFieldMappingModel::fieldConstraints( const QgsField &field ) const +{ + QgsFieldConstraints::Constraints constraints; + + const QgsFieldConstraints fieldConstraints { field.constraints() }; + + if ( fieldConstraints.constraints() & QgsFieldConstraints::ConstraintNotNull && + fieldConstraints.constraintStrength( QgsFieldConstraints::ConstraintNotNull ) & QgsFieldConstraints::ConstraintStrengthHard ) + constraints.setFlag( QgsFieldConstraints::ConstraintNotNull ); + + if ( fieldConstraints.constraints() & QgsFieldConstraints::ConstraintUnique && + fieldConstraints.constraintStrength( QgsFieldConstraints::ConstraintUnique ) & QgsFieldConstraints::ConstraintStrengthHard ) + constraints.setFlag( QgsFieldConstraints::ConstraintUnique ); + + if ( fieldConstraints.constraints() & QgsFieldConstraints::ConstraintExpression && + fieldConstraints.constraintStrength( QgsFieldConstraints::ConstraintExpression ) & QgsFieldConstraints::ConstraintStrengthHard ) + constraints.setFlag( QgsFieldConstraints::ConstraintExpression ); + + return constraints; +} + +bool QgsFieldMappingModel::moveUpOrDown( const QModelIndex &index, bool up ) +{ + if ( ! index.isValid() && index.model() == this ) + return false; + + // Always swap down + const int row { up ? index.row() - 1 : index.row() }; + // Range checking + if ( row < 0 || row + 1 >= rowCount( QModelIndex() ) ) + { + return false; + } + beginMoveRows( QModelIndex( ), row, row, QModelIndex(), row + 2 ); +#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) + mMapping.swap( row, row + 1 ); +#else + mMapping.swapItemsAt( row, row + 1 ); +#endif + endMoveRows(); + return true; +} + +QString QgsFieldMappingModel::findExpressionForDestinationField( const QgsFieldMappingModel::Field &f, QStringList &excludedFieldNames ) +{ + // Search for fields in the source + // 1. match by name + for ( const QgsField &sf : qgis::as_const( mSourceFields ) ) + { + if ( sf.name() == f.field.name() ) + { + excludedFieldNames.push_back( sf.name() ); + return QgsExpression::quotedColumnRef( sf.name() ); + } + } + // 2. match by type + for ( const QgsField &sf : qgis::as_const( mSourceFields ) ) + { + if ( excludedFieldNames.contains( sf.name() ) || sf.type() != f.field.type() ) + continue; + excludedFieldNames.push_back( sf.name() ); + return QgsExpression::quotedColumnRef( sf.name() ); + } + return QString(); +} + +void QgsFieldMappingModel::setSourceFields( const QgsFields &sourceFields ) +{ + mSourceFields = sourceFields; + QStringList usedFields; + beginResetModel(); + for ( const Field &f : qgis::as_const( mMapping ) ) + { + if ( QgsExpression( f.expression ).isField() ) + { + usedFields.push_back( f.expression.mid( 1, f.expression.length() - 2 ) ); + } + } + for ( auto it = mMapping.begin(); it != mMapping.end(); ++it ) + { + if ( it->expression.isEmpty() ) + { + const QString expression { findExpressionForDestinationField( *it, usedFields ) }; + if ( ! expression.isEmpty() ) + it->expression = expression; + } + } + endResetModel(); +} + +QgsExpressionContextGenerator *QgsFieldMappingModel::contextGenerator() const +{ + return mExpressionContextGenerator.get(); +} + +void QgsFieldMappingModel::setDestinationFields( const QgsFields &destinationFields, + const QMap &expressions ) +{ + beginResetModel(); + mMapping.clear(); + // Prepare the model data + QStringList usedFields; + for ( const QgsField &df : destinationFields ) + { + Field f; + f.field = df; + f.originalName = df.name(); + if ( expressions.contains( f.field.name() ) ) + { + f.expression = expressions.value( f.field.name() ); + const QgsExpression exp { f.expression }; + // if it's source field + if ( exp.isField() && + mSourceFields.names().contains( exp.referencedColumns().toList().first() ) ) + { + usedFields.push_back( exp.referencedColumns().toList().first() ); + } + } + else + { + const QString expression { findExpressionForDestinationField( f, usedFields ) }; + if ( ! expression.isEmpty() ) + f.expression = expression; + } + mMapping.push_back( f ); + } + endResetModel(); +} + +bool QgsFieldMappingModel::destinationEditable() const +{ + return mDestinationEditable; +} + +void QgsFieldMappingModel::setDestinationEditable( bool destinationEditable ) +{ + mDestinationEditable = destinationEditable; +} + +const QMap QgsFieldMappingModel::dataTypes() const +{ + static const QMap sDataTypes + { + { QVariant::Type::Int, tr( "Whole number (integer - 32bit)" ) }, + { QVariant::Type::LongLong, tr( "Whole number (integer - 64bit)" ) }, + { QVariant::Type::Double, tr( "Decimal number (double)" ) }, + { QVariant::Type::String, tr( "Text (string)" ) }, + { QVariant::Type::Date, tr( "Date" ) }, + { QVariant::Type::Time, tr( "Time" ) }, + { QVariant::Type::DateTime, tr( "Date & Time" ) }, + { QVariant::Type::Bool, tr( "Boolean" ) }, + { QVariant::Type::ByteArray, tr( "Binary object (BLOB)" ) }, + }; + return sDataTypes; +} + +QList QgsFieldMappingModel::mapping() const +{ + return mMapping; +} + +QMap QgsFieldMappingModel::fieldPropertyMap() const +{ + QMap< QString, QgsProperty > fieldMap; + for ( const QgsFieldMappingModel::Field &field : mMapping ) + { + const QgsExpression exp( field.expression ); + const bool isField = exp.isField(); + fieldMap.insert( field.originalName, isField + ? QgsProperty::fromField( static_cast( exp.rootNode() )->name() ) + : QgsProperty::fromExpression( field.expression ) ); + } + return fieldMap; +} + +void QgsFieldMappingModel::setFieldPropertyMap( const QMap &map ) +{ + beginResetModel(); + for ( int i = 0; i < mMapping.count(); ++i ) + { + Field &f = mMapping[i]; + if ( map.contains( f.field.name() ) ) + { + const QgsProperty prop = map.value( f.field.name() ); + switch ( prop.propertyType() ) + { + case QgsProperty::StaticProperty: + f.expression = QgsExpression::quotedValue( prop.staticValue() ); + break; + + case QgsProperty::FieldBasedProperty: + f.expression = prop.field(); + break; + + case QgsProperty::ExpressionBasedProperty: + f.expression = prop.expressionString(); + break; + + case QgsProperty::InvalidProperty: + f.expression.clear(); + break; + } + } + else + { + f.expression.clear(); + } + } + endResetModel(); +} + +void QgsFieldMappingModel::appendField( const QgsField &field, const QString &expression ) +{ + const int lastRow { rowCount( QModelIndex( ) ) }; + beginInsertRows( QModelIndex(), lastRow, lastRow ); + Field f; + f.field = field; + f.expression = expression; + f.originalName = field.name(); + mMapping.push_back( f ); + endInsertRows( ); +} + +bool QgsFieldMappingModel::removeField( const QModelIndex &index ) +{ + if ( index.isValid() && index.model() == this && index.row() < rowCount( QModelIndex() ) ) + { + beginRemoveRows( QModelIndex(), index.row(), index.row() ); + mMapping.removeAt( index.row() ); + endRemoveRows(); + return true; + } + else + { + return false; + } +} + +bool QgsFieldMappingModel::moveUp( const QModelIndex &index ) +{ + return moveUpOrDown( index ); +} + +bool QgsFieldMappingModel::moveDown( const QModelIndex &index ) +{ + return moveUpOrDown( index, false ); +} + +QgsFieldMappingModel::ExpressionContextGenerator::ExpressionContextGenerator( const QgsFields *sourceFields ) + : mSourceFields( sourceFields ) +{ +} + +QgsExpressionContext QgsFieldMappingModel::ExpressionContextGenerator::createExpressionContext() const +{ + QgsExpressionContext ctx; + ctx.appendScope( QgsExpressionContextUtils::globalScope() ); + ctx.setFields( *mSourceFields ); + QgsFeature feature { *mSourceFields }; + feature.setValid( true ); + ctx.setFeature( feature ); + return ctx; +} diff --git a/src/gui/qgsfieldmappingmodel.h b/src/gui/qgsfieldmappingmodel.h new file mode 100644 index 000000000000..768662658110 --- /dev/null +++ b/src/gui/qgsfieldmappingmodel.h @@ -0,0 +1,191 @@ +/*************************************************************************** + qgsfieldmappingmodel.h - QgsFieldMappingModel + + --------------------- + begin : 17.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSFIELDMAPPINGMODEL_H +#define QGSFIELDMAPPINGMODEL_H + +#include +#include + +#include "qgsfields.h" +#include "qgsexpressioncontextgenerator.h" +#include "qgsfieldconstraints.h" +#include "qgsproperty.h" +#include "qgis_gui.h" + + +/** + * \ingroup gui + * The QgsFieldMappingModel holds mapping information for mapping from one set of QgsFields to another, + * for each set of "destination" fields an expression defines how to obtain the values of the + * "destination" fields. + * The model can be optionally set "editable" allowing to modify all the fields, by default only + * the mapping expression is editable. + * + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsFieldMappingModel: public QAbstractTableModel +{ + + Q_OBJECT + + public: + + /** + * The ColumnDataIndex enum represents the column index for the view + */ + enum class ColumnDataIndex : int + { + SourceExpression, //!< Expression + DestinationName, //!< Destination field name + DestinationType, //!< Destination field QVariant::Type casted to (int) + DestinationLength, //!< Destination field length + DestinationPrecision, //!< Destination field precision + DestinationConstraints, //!< Destination field constraints + }; + + Q_ENUM( ColumnDataIndex ); + + /** + * The Field struct holds information about a mapped field + */ + struct Field + { + //! The original name of the field + QString originalName; + //! The field in its current status (it might have been renamed) + QgsField field; + //! The expression for the mapped field from the source fields + QString expression; + }; + + /** + * Constructs a QgsFieldMappingModel from a set of \a sourceFields + * and \a destinationFields, initial values for the expressions can be + * optionally specified through \a expressions which is a map from the original + * field name to the corresponding expression. A \a parent object + * can be also specified. + */ + QgsFieldMappingModel( const QgsFields &sourceFields = QgsFields(), + const QgsFields &destinationFields = QgsFields(), + const QMap &expressions = QMap(), + QObject *parent = nullptr ); + + //! Returns TRUE if the destination fields are editable + bool destinationEditable() const; + + //! Sets the destination fields editable state to \a editable + void setDestinationEditable( bool editable ); + + //! Returns a static map of supported data types + const QMap dataTypes() const; + + //! Returns a list of source fields + QgsFields sourceFields() const; + + //! Returns a list of Field objects representing the current status of the model + QList mapping() const; + + /** + * Returns a map of destination field name to QgsProperty definition for field value, + * representing the current status of the model. + * + * \see setFieldPropertyMap() + */ + QMap< QString, QgsProperty > fieldPropertyMap() const; + + /** + * Sets a map of destination field name to QgsProperty definition for field value. + * + * \see fieldPropertyMap() + */ + void setFieldPropertyMap( const QMap< QString, QgsProperty > &map ); + + //! Appends a new \a field to the model, with an optional \a expression + void appendField( const QgsField &field, const QString &expression = QString() ); + + //! Removes the field at \a index from the model, returns TRUE on success + bool removeField( const QModelIndex &index ); + + //! Moves down the field at \a index + bool moveUp( const QModelIndex &index ); + + //! Moves up the field at \a index + bool moveDown( const QModelIndex &index ); + + //! Set source fields to \a sourceFields + void setSourceFields( const QgsFields &sourceFields ); + + //! Returns the context generator with the source fields + QgsExpressionContextGenerator *contextGenerator() const; + + /** + * Set destination fields to \a destinationFields, initial values for the expressions can be + * optionally specified through \a expressions which is a map from the original + * field name to the corresponding expression. + */ + void setDestinationFields( const QgsFields &destinationFields, + const QMap &expressions = QMap() ); + + // QAbstractItemModel interface + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + + private: + + class ExpressionContextGenerator: public QgsExpressionContextGenerator + { + + public: + + ExpressionContextGenerator( const QgsFields *sourceFields ); + + // QgsExpressionContextGenerator interface + QgsExpressionContext createExpressionContext() const override; + + private: + + const QgsFields *mSourceFields; + + }; + + + QgsFieldConstraints::Constraints fieldConstraints( const QgsField &field ) const; + + bool moveUpOrDown( const QModelIndex &index, bool up = true ); + + /** + * Try to find the best expression for a destination \a field by searching in the + * source fields for fields with: + * - the same name + * - the same type + * Returns an expression containing a reference to the field that matches first. + */ + QString findExpressionForDestinationField( const QgsFieldMappingModel::Field &field, QStringList &excludedFieldNames ); + + QList mMapping; + bool mDestinationEditable = false; + QgsFields mSourceFields; + std::unique_ptr mExpressionContextGenerator; + +}; + + + +#endif // QGSFIELDMAPPINGMODEL_H diff --git a/src/gui/qgsfieldmappingwidget.cpp b/src/gui/qgsfieldmappingwidget.cpp new file mode 100644 index 000000000000..20732ec5402e --- /dev/null +++ b/src/gui/qgsfieldmappingwidget.cpp @@ -0,0 +1,298 @@ +/*************************************************************************** + qgsfieldmappingwidget.cpp - QgsFieldMappingWidget + + --------------------- + begin : 16.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgsfieldmappingwidget.h" +#include "qgsfieldexpressionwidget.h" +#include "qgsexpression.h" + +#ifdef ENABLE_MODELTEST +#include "modeltest.h" +#endif + +QgsFieldMappingWidget::QgsFieldMappingWidget( QWidget *parent, + const QgsFields &sourceFields, + const QgsFields &destinationFields, + const QMap &expressions ) + : QgsPanelWidget( parent ) +{ + + setupUi( this ); + + mModel = new QgsFieldMappingModel( sourceFields, destinationFields, expressions, this ); + +#ifdef ENABLE_MODELTEST + new ModelTest( mModel, this ); +#endif + + mTableView->setModel( mModel ); + mTableView->setItemDelegateForColumn( static_cast( QgsFieldMappingModel::ColumnDataIndex::SourceExpression ), new ExpressionDelegate( mTableView ) ); + mTableView->setItemDelegateForColumn( static_cast( QgsFieldMappingModel::ColumnDataIndex::DestinationType ), new TypeDelegate( mTableView ) ); + updateColumns(); + // Make sure columns are updated when rows are added + connect( mModel, &QgsFieldMappingModel::rowsInserted, this, [ = ] { updateColumns(); } ); + connect( mModel, &QgsFieldMappingModel::modelReset, this, [ = ] { updateColumns(); } ); +} + +void QgsFieldMappingWidget::setDestinationEditable( bool editable ) +{ + qobject_cast( mModel )->setDestinationEditable( editable ); + updateColumns(); +} + +bool QgsFieldMappingWidget::destinationEditable() const +{ + return qobject_cast( mModel )->destinationEditable(); +} + +QgsFieldMappingModel *QgsFieldMappingWidget::model() const +{ + return qobject_cast( mModel ); +} + +QList QgsFieldMappingWidget::mapping() const +{ + return model()->mapping(); +} + +QMap QgsFieldMappingWidget::fieldPropertyMap() const +{ + return model()->fieldPropertyMap(); +} + +void QgsFieldMappingWidget::setFieldPropertyMap( const QMap &map ) +{ + model()->setFieldPropertyMap( map ); +} + +QItemSelectionModel *QgsFieldMappingWidget::selectionModel() +{ + return mTableView->selectionModel(); +} + +void QgsFieldMappingWidget::setSourceFields( const QgsFields &sourceFields ) +{ + model()->setSourceFields( sourceFields ); +} + +void QgsFieldMappingWidget::setDestinationFields( const QgsFields &destinationFields, const QMap &expressions ) +{ + model()->setDestinationFields( destinationFields, expressions ); +} + +void QgsFieldMappingWidget::scrollTo( const QModelIndex &index ) const +{ + mTableView->scrollTo( index ); +} + +void QgsFieldMappingWidget::appendField( const QgsField &field, const QString &expression ) +{ + model()->appendField( field, expression ); +} + +bool QgsFieldMappingWidget::removeSelectedFields() +{ + if ( ! mTableView->selectionModel()->hasSelection() ) + return false; + + std::list rowsToRemove { selectedRows() }; + rowsToRemove.reverse(); + for ( int row : rowsToRemove ) + { + if ( ! model()->removeField( model()->index( row, 0, QModelIndex() ) ) ) + { + return false; + } + } + return true; +} + +bool QgsFieldMappingWidget::moveSelectedFieldsUp() +{ + if ( ! mTableView->selectionModel()->hasSelection() ) + return false; + + const std::list rowsToMoveUp { selectedRows() }; + for ( int row : rowsToMoveUp ) + { + if ( ! model()->moveUp( model()->index( row, 0, QModelIndex() ) ) ) + { + return false; + } + } + return true; +} + +bool QgsFieldMappingWidget::moveSelectedFieldsDown() +{ + if ( ! mTableView->selectionModel()->hasSelection() ) + return false; + + std::list rowsToMoveDown { selectedRows() }; + rowsToMoveDown.reverse(); + for ( int row : rowsToMoveDown ) + { + if ( ! model()->moveDown( model()->index( row, 0, QModelIndex() ) ) ) + { + return false; + } + } + return true; +} + +void QgsFieldMappingWidget::updateColumns() +{ + for ( int i = 0; i < mModel->rowCount(); ++i ) + { + mTableView->openPersistentEditor( mModel->index( i, static_cast( QgsFieldMappingModel::ColumnDataIndex::SourceExpression ) ) ); + mTableView->openPersistentEditor( mModel->index( i, static_cast( QgsFieldMappingModel::ColumnDataIndex::DestinationType ) ) ); + } + + for ( int i = 0; i < mModel->columnCount(); ++i ) + { + mTableView->resizeColumnToContents( i ); + } +} + +std::list QgsFieldMappingWidget::selectedRows() +{ + std::list rows; + if ( mTableView->selectionModel()->hasSelection() ) + { + const QModelIndexList constSelection { mTableView->selectionModel()->selectedIndexes() }; + for ( const QModelIndex &index : constSelection ) + { + rows.push_back( index.row() ); + } + rows.sort(); + rows.unique(); + } + return rows; +} + +void QgsFieldMappingWidget::ExpressionDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + QgsFieldExpressionWidget *editorWidget { qobject_cast( editor ) }; + if ( ! editorWidget ) + return; + + bool isExpression; + bool isValid; + const QString currentValue { editorWidget->currentField( &isExpression, &isValid ) }; + if ( isExpression ) + { + model->setData( index, currentValue, Qt::EditRole ); + } + else + { + model->setData( index, QgsExpression::quotedColumnRef( currentValue ), Qt::EditRole ); + } +} + +void QgsFieldMappingWidget::ExpressionDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const +{ + QgsFieldExpressionWidget *editorWidget { qobject_cast( editor ) }; + if ( ! editorWidget ) + return; + + const QVariant value = index.model()->data( index, Qt::EditRole ); + editorWidget->setField( value.toString() ); +} + +QWidget *QgsFieldMappingWidget::ExpressionDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + Q_UNUSED( option ) + QgsFieldExpressionWidget *editor = new QgsFieldExpressionWidget( parent ); + editor->setAutoFillBackground( true ); + editor->setAllowEvalErrors( false ); + editor->setAllowEmptyFieldName( true ); + const QgsFieldMappingModel *model { qobject_cast( index.model() ) }; + Q_ASSERT( model ); + editor->registerExpressionContextGenerator( model->contextGenerator() ); + editor->setFields( model->sourceFields() ); + editor->setField( index.model()->data( index, Qt::DisplayRole ).toString() ); + connect( editor, + qgis::overload::of( &QgsFieldExpressionWidget::fieldChanged ), + this, + [ = ]( const QString & fieldName, bool isValid ) + { + Q_UNUSED( fieldName ) + Q_UNUSED( isValid ) + const_cast< QgsFieldMappingWidget::ExpressionDelegate *>( this )->emit commitData( editor ); + } ); + return editor; +} + +QgsFieldMappingWidget::ExpressionDelegate::ExpressionDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ +} + +QgsFieldMappingWidget::TypeDelegate::TypeDelegate( QObject *parent ) + : QStyledItemDelegate( parent ) +{ +} + +QWidget *QgsFieldMappingWidget::TypeDelegate::createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const +{ + Q_UNUSED( option ) + QComboBox *editor = new QComboBox( parent ); + const QgsFieldMappingModel *model { qobject_cast( index.model() ) }; + Q_ASSERT( model ); + const QMap typeList { model->dataTypes() }; + int i = 0; + for ( auto it = typeList.constBegin(); it != typeList.constEnd(); ++it ) + { + editor->addItem( typeList[ it.key() ] ); + editor->setItemData( i, static_cast( it.key() ), Qt::UserRole ); + ++i; + } + if ( ! model->destinationEditable() ) + { + editor->setEnabled( false ); + } + else + { + connect( editor, + qgis::overload::of( &QComboBox::currentIndexChanged ), + this, + [ = ]( int currentIndex ) + { + Q_UNUSED( currentIndex ) + const_cast< QgsFieldMappingWidget::TypeDelegate *>( this )->emit commitData( editor ); + } ); + } + return editor; +} + +void QgsFieldMappingWidget::TypeDelegate::setEditorData( QWidget *editor, const QModelIndex &index ) const +{ + QComboBox *editorWidget { qobject_cast( editor ) }; + if ( ! editorWidget ) + return; + + const QVariant value { index.model()->data( index, Qt::EditRole ) }; + editorWidget->setCurrentIndex( editorWidget->findData( value ) ); +} + +void QgsFieldMappingWidget::TypeDelegate::setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const +{ + QComboBox *editorWidget { qobject_cast( editor ) }; + if ( ! editorWidget ) + return; + + const QVariant currentValue { editorWidget->currentData( ) }; + model->setData( index, currentValue, Qt::EditRole ); +} diff --git a/src/gui/qgsfieldmappingwidget.h b/src/gui/qgsfieldmappingwidget.h new file mode 100644 index 000000000000..d93be3c49303 --- /dev/null +++ b/src/gui/qgsfieldmappingwidget.h @@ -0,0 +1,149 @@ +/*************************************************************************** + qgsfieldmappingwidget.h - QgsFieldMappingWidget + + --------------------- + begin : 16.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSFIELDMAPPINGWIDGET_H +#define QGSFIELDMAPPINGWIDGET_H + +#include +#include +#include + +#include "qgis_gui.h" +#include "ui_qgsfieldmappingwidget.h" +#include "qgsfieldmappingmodel.h" + +/** + * \ingroup gui + * The QgsFieldMappingWidget class creates a mapping from one set of QgsFields to another, + * for each set of "destination" fields an expression defines how to obtain the values of the + * "destination" fields. + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsFieldMappingWidget : public QgsPanelWidget, private Ui::QgsFieldMappingWidget +{ + Q_OBJECT + + public: + + /** + * Constructs a QgsFieldMappingWidget from a set of \a sourceFields + * and \a destinationFields, initial values for the expressions can be + * optionally specified through \a expressions which is a map from the original + * field name to the corresponding expression. A \a parent object + * can also be specified. + */ + explicit QgsFieldMappingWidget( QWidget *parent = nullptr, + const QgsFields &sourceFields = QgsFields(), + const QgsFields &destinationFields = QgsFields(), + const QMap &expressions = QMap() ); + + //! Sets the destination fields editable state to \a editable + void setDestinationEditable( bool editable ); + + //! Returns TRUE if the destination fields are editable in the model + bool destinationEditable() const; + + //! Returns the underlying mapping model + QgsFieldMappingModel *model() const; + + //! Returns a list of Field objects representing the current status of the underlying mapping model + QList mapping() const; + + /** + * Returns a map of destination field name to QgsProperty definition for field value, + * representing the current status of the widget. + * + * \see setFieldPropertyMap() + */ + QMap< QString, QgsProperty > fieldPropertyMap() const; + + /** + * Sets a map of destination field name to QgsProperty definition for field value. + * + * \see fieldPropertyMap() + */ + void setFieldPropertyMap( const QMap< QString, QgsProperty > &map ); + + //! Returns the selection model + QItemSelectionModel *selectionModel(); + + //! Set source fields of the underlying mapping model to \a sourceFields + void setSourceFields( const QgsFields &sourceFields ); + + /** + * Set destination fields to \a destinationFields in the underlying model, + * initial values for the expressions can be optionally specified through + * \a expressions which is a map from the original field name to the + * corresponding expression. + */ + void setDestinationFields( const QgsFields &destinationFields, + const QMap &expressions = QMap() ); + + /** + * Scroll the fields view to \a index + */ + void scrollTo( const QModelIndex &index ) const; + + public slots: + + //! Appends a new \a field to the model, with an optional \a expression + void appendField( const QgsField &field, const QString &expression = QString() ); + + //! Removes the currently selected field from the model + bool removeSelectedFields( ); + + //! Moves down currently selected field + bool moveSelectedFieldsUp( ); + + //! Moves up the currently selected field + bool moveSelectedFieldsDown( ); + + private: + + QAbstractTableModel *mModel; + void updateColumns(); + //! Returns selected row indexes in ascending order + std::list selectedRows( ); + + class ExpressionDelegate: public QStyledItemDelegate + { + + public: + + ExpressionDelegate( QObject *parent = nullptr ); + + // QAbstractItemDelegate interface + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const override; + void setEditorData( QWidget *editor, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; + }; + + class TypeDelegate: public QStyledItemDelegate + { + + public: + + TypeDelegate( QObject *parent = nullptr ); + + // QAbstractItemDelegate interface + QWidget *createEditor( QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index ) const override; + void setEditorData( QWidget *editor, const QModelIndex &index ) const override; + void setModelData( QWidget *editor, QAbstractItemModel *model, const QModelIndex &index ) const override; + }; + +}; + + +#endif // QGSFIELDMAPPINGWIDGET_H diff --git a/src/gui/qgsfilecontentsourcelineedit.cpp b/src/gui/qgsfilecontentsourcelineedit.cpp index ccaa18c4941a..87fd07399a99 100644 --- a/src/gui/qgsfilecontentsourcelineedit.cpp +++ b/src/gui/qgsfilecontentsourcelineedit.cpp @@ -32,6 +32,7 @@ QgsAbstractFileContentSourceLineEdit::QgsAbstractFileContentSourceLineEdit( QWid : QWidget( parent ) { QHBoxLayout *layout = new QHBoxLayout( this ); + layout->setContentsMargins( 0, 0, 0, 0 ); mFileLineEdit = new QgsFilterLineEdit( this ); mFileLineEdit->setShowClearButton( true ); mFileToolButton = new QToolButton( this ); diff --git a/src/gui/qgsfilewidget.cpp b/src/gui/qgsfilewidget.cpp index b39eeafaba2f..5f06f2c714af 100644 --- a/src/gui/qgsfilewidget.cpp +++ b/src/gui/qgsfilewidget.cpp @@ -408,9 +408,8 @@ QString QgsFileWidget::toUrl( const QString &path ) const QgsFileDropEdit::QgsFileDropEdit( QWidget *parent ) - : QgsFilterLineEdit( parent ) + : QgsHighlightableLineEdit( parent ) { - mDragActive = false; setAcceptDrops( true ); } @@ -510,8 +509,7 @@ void QgsFileDropEdit::dragEnterEvent( QDragEnterEvent *event ) if ( !filePath.isEmpty() ) { event->acceptProposedAction(); - mDragActive = true; - update(); + setHighlighted( true ); } else { @@ -523,8 +521,7 @@ void QgsFileDropEdit::dragLeaveEvent( QDragLeaveEvent *event ) { QgsFilterLineEdit::dragLeaveEvent( event ); event->accept(); - mDragActive = false; - update(); + setHighlighted( false ); } void QgsFileDropEdit::dropEvent( QDropEvent *event ) @@ -536,21 +533,7 @@ void QgsFileDropEdit::dropEvent( QDropEvent *event ) selectAll(); setFocus( Qt::MouseFocusReason ); event->acceptProposedAction(); - mDragActive = false; - update(); - } -} - -void QgsFileDropEdit::paintEvent( QPaintEvent *e ) -{ - QgsFilterLineEdit::paintEvent( e ); - if ( mDragActive ) - { - QPainter p( this ); - int width = 2; // width of highlight rectangle inside frame - p.setPen( QPen( palette().highlight(), width ) ); - QRect r = rect().adjusted( width, width, -width, -width ); - p.drawRect( r ); + setHighlighted( false ); } } diff --git a/src/gui/qgsfilewidget.h b/src/gui/qgsfilewidget.h index ba596f43bf71..eb3c577e8a2f 100644 --- a/src/gui/qgsfilewidget.h +++ b/src/gui/qgsfilewidget.h @@ -26,7 +26,7 @@ class QHBoxLayout; #include "qgis_gui.h" #include "qgis_sip.h" -#include "qgsfilterlineedit.h" +#include "qgshighlightablelineedit.h" /** * \ingroup gui @@ -243,7 +243,7 @@ class GUI_EXPORT QgsFileWidget : public QWidget * or directories only. By default, dropping is limited to files only. * \note not available in Python bindings */ -class GUI_EXPORT QgsFileDropEdit: public QgsFilterLineEdit +class GUI_EXPORT QgsFileDropEdit: public QgsHighlightableLineEdit { Q_OBJECT @@ -259,7 +259,6 @@ class GUI_EXPORT QgsFileDropEdit: public QgsFilterLineEdit void dragEnterEvent( QDragEnterEvent *event ) override; void dragLeaveEvent( QDragLeaveEvent *event ) override; void dropEvent( QDropEvent *event ) override; - void paintEvent( QPaintEvent *e ) override; private: @@ -268,7 +267,6 @@ class GUI_EXPORT QgsFileDropEdit: public QgsFilterLineEdit QStringList mAcceptableExtensions; QgsFileWidget::StorageMode mStorageMode = QgsFileWidget::GetFile; - bool mDragActive; friend class TestQgsFileWidget; }; diff --git a/src/gui/qgsgraphicsviewmousehandles.cpp b/src/gui/qgsgraphicsviewmousehandles.cpp index 7fbc1dd7b322..7080443a0616 100644 --- a/src/gui/qgsgraphicsviewmousehandles.cpp +++ b/src/gui/qgsgraphicsviewmousehandles.cpp @@ -65,9 +65,9 @@ void QgsGraphicsViewMouseHandles::previewItemMove( QGraphicsItem *, double, doub } -void QgsGraphicsViewMouseHandles::previewSetItemRect( QGraphicsItem *, QRectF ) +QRectF QgsGraphicsViewMouseHandles::previewSetItemRect( QGraphicsItem *, QRectF ) { - + return QRectF(); } void QgsGraphicsViewMouseHandles::startMacroCommand( const QString & ) @@ -1025,9 +1025,8 @@ void QgsGraphicsViewMouseHandles::resizeMouseMove( QPointF currentPosition, bool mResizeRect = QRectF( QPointF( -( mBeginHandleWidth + rx ), -( mBeginHandleHeight + ry ) ), QPointF( 0, 0 ) ); } - setRect( 0, 0, std::fabs( mBeginHandleWidth + rx ), std::fabs( mBeginHandleHeight + ry ) ); - const QList selectedItems = selectedSceneItems( false ); + QRectF newHandleBounds; for ( QGraphicsItem *item : selectedItems ) { //get stored item bounds in mouse handle item's coordinate system @@ -1035,9 +1034,12 @@ void QgsGraphicsViewMouseHandles::resizeMouseMove( QPointF currentPosition, bool //now, resize it relative to the current resized dimensions of the mouse handles relativeResizeRect( thisItemRect, QRectF( -mResizeMoveX, -mResizeMoveY, mBeginHandleWidth, mBeginHandleHeight ), mResizeRect ); - previewSetItemRect( item, mapRectToScene( thisItemRect ) ); + thisItemRect = mapRectFromScene( previewSetItemRect( item, mapRectToScene( thisItemRect ) ) ); + newHandleBounds = newHandleBounds.isValid() ? newHandleBounds.united( thisItemRect ) : thisItemRect; } + setRect( newHandleBounds.isValid() ? newHandleBounds : QRectF( 0, 0, std::fabs( mBeginHandleWidth + rx ), std::fabs( mBeginHandleHeight + ry ) ) ); + //show current size of selection in status bar showStatusMessage( tr( "width: %1 mm height: %2 mm" ).arg( rect().width() ).arg( rect().height() ) ); diff --git a/src/gui/qgsgraphicsviewmousehandles.h b/src/gui/qgsgraphicsviewmousehandles.h index e079df65c658..e6e5497b940e 100644 --- a/src/gui/qgsgraphicsviewmousehandles.h +++ b/src/gui/qgsgraphicsviewmousehandles.h @@ -120,7 +120,17 @@ class GUI_EXPORT QgsGraphicsViewMouseHandles: public QObject, public QGraphicsRe virtual void moveItem( QGraphicsItem *item, double deltaX, double deltaY ) = 0; virtual void previewItemMove( QGraphicsItem *item, double deltaX, double deltaY ); virtual void setItemRect( QGraphicsItem *item, QRectF rect ) = 0; - virtual void previewSetItemRect( QGraphicsItem *item, QRectF rect ); + + /** + * Called when a resize or move action is in progress and the effects can be previewed for the specified \a item. The + * \a rect argument gives the new "transient" rectangular bounds of \a item (in item coordinates). + * + * If implemented, the method should return the item's calculated desired rect given the specified \a rect. This allows + * an item to override the rect results, e.g. by applying a minimum size constraint. The returned value + * should be in the item's coordinates. + */ + virtual QRectF previewSetItemRect( QGraphicsItem *item, QRectF rect ); + virtual void startMacroCommand( const QString &text ); virtual void endMacroCommand(); virtual void createItemCommand( QGraphicsItem *item ); diff --git a/src/gui/qgshighlightablelineedit.cpp b/src/gui/qgshighlightablelineedit.cpp new file mode 100644 index 000000000000..ccfbef303c34 --- /dev/null +++ b/src/gui/qgshighlightablelineedit.cpp @@ -0,0 +1,41 @@ +/*************************************************************************** + qgshighlightablelineedit.h + ------------------------- + Date : March 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson 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 "qgshighlightablelineedit.h" +#include + +QgsHighlightableLineEdit::QgsHighlightableLineEdit( QWidget *parent ) + : QgsFilterLineEdit( parent ) +{} + +void QgsHighlightableLineEdit::paintEvent( QPaintEvent *e ) +{ + QgsFilterLineEdit::paintEvent( e ); + if ( mHighlight ) + { + QPainter p( this ); + int width = 2; // width of highlight rectangle inside frame + p.setPen( QPen( palette().highlight(), width ) ); + QRect r = rect().adjusted( width, width, -width, -width ); + p.drawRect( r ); + } +} + +void QgsHighlightableLineEdit::setHighlighted( bool highlighted ) +{ + mHighlight = highlighted; + update(); +} diff --git a/src/gui/qgshighlightablelineedit.h b/src/gui/qgshighlightablelineedit.h new file mode 100644 index 000000000000..c29d4eb94060 --- /dev/null +++ b/src/gui/qgshighlightablelineedit.h @@ -0,0 +1,65 @@ +/*************************************************************************** + qgshighlightablelineedit.h + ------------------------- + Date : March 2020 + Copyright : (C) 2020 by Nyall Dawson + Email : nyall dot dawson 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 QGSHIGHLIGHTABLELINEEDIT_H +#define QGSHIGHLIGHTABLELINEEDIT_H + +#include + +#include "qgis_gui.h" +#include "qgis_sip.h" +#include "qgsfilterlineedit.h" + +/** + * \class QgsHighlightableLineEdit + * \ingroup gui + * + * A QgsFilterLineEdit subclass with the ability to "highlight" the edges of the widget. + * + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsHighlightableLineEdit: public QgsFilterLineEdit +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsHighlightableLineEdit, with the specified \a parent widget. + */ + QgsHighlightableLineEdit( QWidget *parent SIP_TRANSFERTHIS = nullptr ); + + /** + * Returns TRUE if the line edit is currently highlighted. + * \see setHighlighted() + */ + bool isHighlighted() const { return mHighlight; } + + /** + * Sets whether the line edit is currently \a highlighted. + * \see isHighlighted() + */ + void setHighlighted( bool highlighted ); + + protected: + void paintEvent( QPaintEvent *e ) override; + + private: + + bool mHighlight = false; +}; + + +#endif // QGSHIGHLIGHTABLELINEEDIT_H diff --git a/src/gui/qgsidentifymenu.cpp b/src/gui/qgsidentifymenu.cpp index cd973bd07546..f387c00d8278 100644 --- a/src/gui/qgsidentifymenu.cpp +++ b/src/gui/qgsidentifymenu.cpp @@ -126,6 +126,10 @@ QList QgsIdentifyMenu::exec( const QList 1 zooms out, interval (0,1) zooms in - * If point is given, re-center on it + * If point is given, re-center on it. + * + * If \a ignoreScaleLock is set to TRUE, then any existing constraint on the map scale + * of the canvas will be ignored during the zoom operation. */ - void zoomByFactor( double scaleFactor, const QgsPointXY *center = nullptr ); + void zoomByFactor( double scaleFactor, const QgsPointXY *center = nullptr, bool ignoreScaleLock = false ); //! Zooms in/out with a given center void zoomWithCenter( int x, int y, bool zoomIn ); diff --git a/src/gui/qgsmaplayercombobox.cpp b/src/gui/qgsmaplayercombobox.cpp index 0b8c08dc0e22..9cdce45585af 100644 --- a/src/gui/qgsmaplayercombobox.cpp +++ b/src/gui/qgsmaplayercombobox.cpp @@ -29,6 +29,8 @@ QgsMapLayerComboBox::QgsMapLayerComboBox( QWidget *parent ) connect( mProxyModel, &QAbstractItemModel::rowsInserted, this, &QgsMapLayerComboBox::rowsChanged ); connect( mProxyModel, &QAbstractItemModel::rowsRemoved, this, &QgsMapLayerComboBox::rowsChanged ); + setSizeAdjustPolicy( QComboBox::AdjustToMinimumContentsLengthWithIcon ); + setAcceptDrops( true ); } @@ -74,7 +76,7 @@ QStringList QgsMapLayerComboBox::additionalItems() const void QgsMapLayerComboBox::setLayer( QgsMapLayer *layer ) { - if ( layer == currentLayer() ) + if ( layer == currentLayer() && ( layer || !isEditable() || currentText().isEmpty() ) ) return; if ( !layer ) diff --git a/src/gui/qgsmaplayerconfigwidgetfactory.h b/src/gui/qgsmaplayerconfigwidgetfactory.h index 692bd133c9d8..5371895b6e00 100644 --- a/src/gui/qgsmaplayerconfigwidgetfactory.h +++ b/src/gui/qgsmaplayerconfigwidgetfactory.h @@ -110,7 +110,7 @@ class GUI_EXPORT QgsMapLayerConfigWidgetFactory * \returns A new QgsMapStylePanel which is shown in the map style dock. * \note This function is called each time the panel is selected. Keep it light for better UX. */ - virtual QgsMapLayerConfigWidget *createWidget( QgsMapLayer *layer, QgsMapCanvas *canvas, bool dockWidget = true, QWidget *parent SIP_TRANSFERTHIS = nullptr ) const = 0 SIP_FACTORY; + virtual QgsMapLayerConfigWidget *createWidget( QgsMapLayer *layer, QgsMapCanvas *canvas, bool dockWidget = true, QWidget *parent = nullptr ) const = 0 SIP_FACTORY; private: QIcon mIcon; diff --git a/src/gui/qgsmaplayerstyleguiutils.cpp b/src/gui/qgsmaplayerstyleguiutils.cpp index b83d4dcc9daf..79b6d2132435 100644 --- a/src/gui/qgsmaplayerstyleguiutils.cpp +++ b/src/gui/qgsmaplayerstyleguiutils.cpp @@ -94,6 +94,32 @@ void QgsMapLayerStyleGuiUtils::addStyleManagerActions( QMenu *m, QgsMapLayer *la m->addAction( a ); } +void QgsMapLayerStyleGuiUtils::removesExtraMenuSeparators( QMenu *m ) +{ + if ( !m ) + return; + + // Get rid of previously added style manager actions (they are dynamic) + bool gotFirstSeparator = false; + QList actions = m->actions(); + for ( int i = 0; i < actions.count(); ++i ) + { + if ( actions[i]->isSeparator() ) + { + if ( gotFirstSeparator ) + { + // remove all actions after second separator (including it) + while ( actions.count() != i ) + delete actions.takeAt( i ); + break; + } + else + gotFirstSeparator = true; + } + } + +} + void QgsMapLayerStyleGuiUtils::addStyle() { QAction *a = qobject_cast( sender() ); diff --git a/src/gui/qgsmaplayerstyleguiutils.h b/src/gui/qgsmaplayerstyleguiutils.h index bfb0908e8a91..d3c9eb2895bf 100644 --- a/src/gui/qgsmaplayerstyleguiutils.h +++ b/src/gui/qgsmaplayerstyleguiutils.h @@ -51,6 +51,15 @@ class GUI_EXPORT QgsMapLayerStyleGuiUtils : public QObject */ void addStyleManagerActions( QMenu *m, QgsMapLayer *layer ); + /** + * \brief removes extra separators from the menu + * + * \since QGIS 3.14 + */ + void removesExtraMenuSeparators( QMenu *m ); + + public slots: + private : QAction *actionAddStyle( QgsMapLayer *layer, QObject *parent = nullptr ); QAction *actionRemoveStyle( QgsMapLayer *layer, QObject *parent = nullptr ); diff --git a/src/gui/qgsmaptooldigitizefeature.cpp b/src/gui/qgsmaptooldigitizefeature.cpp index c32ac41700ab..e32e3b91e08b 100644 --- a/src/gui/qgsmaptooldigitizefeature.cpp +++ b/src/gui/qgsmaptooldigitizefeature.cpp @@ -319,18 +319,33 @@ void QgsMapToolDigitizeFeature::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) QgsGeometry g( poly ); f->setGeometry( g ); - QgsGeometry featGeom = f->geometry(); - int avoidIntersectionsReturn = featGeom.avoidIntersections( QgsProject::instance()->avoidIntersectionsLayers() ); - f->setGeometry( featGeom ); - if ( avoidIntersectionsReturn == 1 ) + QList avoidIntersectionsLayers; + switch ( QgsProject::instance()->avoidIntersectionsMode() ) { - //not a polygon type. Impossible to get there + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsCurrentLayer: + avoidIntersectionsLayers.append( vlayer ); + break; + case QgsProject::AvoidIntersectionsMode::AvoidIntersectionsLayers: + avoidIntersectionsLayers = QgsProject::instance()->avoidIntersectionsLayers(); + break; + case QgsProject::AvoidIntersectionsMode::AllowIntersections: + break; } - if ( f->geometry().isEmpty() ) //avoid intersection might have removed the whole geometry + if ( avoidIntersectionsLayers.size() > 0 ) { - emit messageEmitted( tr( "The feature cannot be added because it's geometry collapsed due to intersection avoidance" ), Qgis::Critical ); - stopCapturing(); - return; + QgsGeometry featGeom = f->geometry(); + int avoidIntersectionsReturn = featGeom.avoidIntersections( avoidIntersectionsLayers ); + f->setGeometry( featGeom ); + if ( avoidIntersectionsReturn == 1 ) + { + //not a polygon type. Impossible to get there + } + if ( f->geometry().isEmpty() ) //avoid intersection might have removed the whole geometry + { + emit messageEmitted( tr( "The feature cannot be added because its geometry collapsed due to intersection avoidance" ), Qgis::Critical ); + stopCapturing(); + return; + } } } f->setValid( true ); diff --git a/src/gui/qgsmaptoolextent.h b/src/gui/qgsmaptoolextent.h index c79f49db84be..c1fe8223c2e6 100644 --- a/src/gui/qgsmaptoolextent.h +++ b/src/gui/qgsmaptoolextent.h @@ -20,8 +20,7 @@ #include "qgspointxy.h" #include "qgsrubberband.h" #include "qgis_gui.h" - -#include +#include "qobjectuniqueptr.h" class QgsMapCanvas; @@ -76,7 +75,7 @@ class GUI_EXPORT QgsMapToolExtent : public QgsMapTool void drawExtent(); - std::unique_ptr< QgsRubberBand > mRubberBand; + QObjectUniquePtr< QgsRubberBand > mRubberBand; QgsPointXY mStartPoint; QgsPointXY mEndPoint; diff --git a/src/gui/qgsmaptoolidentify.cpp b/src/gui/qgsmaptoolidentify.cpp index bb6e3176cd86..966a33994e9a 100644 --- a/src/gui/qgsmaptoolidentify.cpp +++ b/src/gui/qgsmaptoolidentify.cpp @@ -244,8 +244,8 @@ bool QgsMapToolIdentify::identifyMeshLayer( QListrendererSettings(); - const QgsMeshDatasetIndex scalarDatasetIndex = rendererSettings.activeScalarDataset(); - const QgsMeshDatasetIndex vectorDatasetIndex = rendererSettings.activeVectorDataset(); + const QgsMeshDatasetIndex scalarDatasetIndex = layer->activeScalarDatasetAtTime( mCanvas->temporalRange() ); + const QgsMeshDatasetIndex vectorDatasetIndex = layer->activeVectorDatasetAtTime( mCanvas->temporalRange() ); if ( ! scalarDatasetIndex.isValid() && ! vectorDatasetIndex.isValid() ) return false; diff --git a/src/gui/qgsmaskingwidget.cpp b/src/gui/qgsmaskingwidget.cpp index 053020bcc545..4ff23ace0bc8 100644 --- a/src/gui/qgsmaskingwidget.cpp +++ b/src/gui/qgsmaskingwidget.cpp @@ -14,6 +14,7 @@ ***************************************************************************/ #include +#include #include "qgsmaskingwidget.h" #include "qgsmasksourceselectionwidget.h" @@ -42,6 +43,14 @@ QgsMaskingWidget::QgsMaskingWidget( QWidget *parent ) : { emit widgetChanged(); } ); + + connect( mEditMaskSettingsGroup, &QGroupBox::toggled, this, [&]( bool on ) + { + if ( on && mLayer ) + { + populate(); + } + } ); } /** @@ -133,8 +142,16 @@ QList>> symbolLayerMasks( void QgsMaskingWidget::setLayer( QgsVectorLayer *layer ) { mLayer = layer; + if ( mEditMaskSettingsGroup->isChecked() ) + { + populate(); + } +} + +void QgsMaskingWidget::populate() +{ mMaskSourcesWidget->update(); - mMaskTargetsWidget->setLayer( layer ); + mMaskTargetsWidget->setLayer( mLayer ); // collect masks and filter on those which have the current layer as destination QSet maskedSymbolLayers; diff --git a/src/gui/qgsmaskingwidget.h b/src/gui/qgsmaskingwidget.h index 4d13a9d2893e..13cb7af486bb 100644 --- a/src/gui/qgsmaskingwidget.h +++ b/src/gui/qgsmaskingwidget.h @@ -48,7 +48,9 @@ class GUI_EXPORT QgsMaskingWidget: public QgsPanelWidget, private Ui::QgsMasking void widgetChanged(); private: - QgsVectorLayer *mLayer; + QgsVectorLayer *mLayer = nullptr; + //! Populate the mask source and target widgets + void populate(); }; #endif diff --git a/src/gui/qgsmasksourceselectionwidget.cpp b/src/gui/qgsmasksourceselectionwidget.cpp index 576a4f387d94..8fc3d99241b0 100644 --- a/src/gui/qgsmasksourceselectionwidget.cpp +++ b/src/gui/qgsmasksourceselectionwidget.cpp @@ -63,6 +63,7 @@ QgsMaskSourceSelectionWidget::QgsMaskSourceSelectionWidget( QWidget *parent ) // place the tree in a layout QVBoxLayout *vbox = new QVBoxLayout(); + vbox->setContentsMargins( 0, 0, 0, 0 ); vbox->addWidget( mTree ); setLayout( vbox ); diff --git a/src/gui/qgsnewdatabasetablenamewidget.cpp b/src/gui/qgsnewdatabasetablenamewidget.cpp index 6c439118db1f..2294a6dd90e1 100644 --- a/src/gui/qgsnewdatabasetablenamewidget.cpp +++ b/src/gui/qgsnewdatabasetablenamewidget.cpp @@ -23,16 +23,19 @@ #include "qgsproviderregistry.h" #include "qgsprovidermetadata.h" #include "qgssettings.h" +#include "qgsguiutils.h" +#include +#include // List of data item provider keys that are filesystem based -QStringList QgsNewDatabaseTableNameWidget::FILESYSTEM_BASED_DATAITEM_PROVIDERS { QStringLiteral( "GPKG" ), QStringLiteral( "SPATIALITE" ) }; +QStringList QgsNewDatabaseTableNameWidget::FILESYSTEM_BASED_DATAITEM_PROVIDERS { QStringLiteral( "GPKG" ), QStringLiteral( "spatialite" ) }; QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( QgsBrowserGuiModel *browserModel, const QStringList &providersFilter, QWidget *parent ) - : QWidget( parent ) + : QgsPanelWidget( parent ) { // Initialize the browser @@ -49,6 +52,9 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( setupUi( this ); + mOkButton->hide(); + mOkButton->setEnabled( false ); + QStringList shownDataItemProvidersFilter; const auto providerList { QgsApplication::dataItemProviderRegistry()->providers() }; @@ -72,6 +78,8 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( } } + mBrowserToolbar->setIconSize( QgsGuiUtils::iconSize( true ) ); + mBrowserProxyModel.setBrowserModel( mBrowserModel ); // If a filter was specified but the data provider could not be found // this makes sure no providers are shown instead of ALL of them @@ -145,9 +153,17 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( } } ); + connect( this, &QgsNewDatabaseTableNameWidget::validationChanged, mOkButton, &QWidget::setEnabled ); + connect( mOkButton, &QPushButton::clicked, this, &QgsNewDatabaseTableNameWidget::accepted ); + validate(); } +void QgsNewDatabaseTableNameWidget::setAcceptButtonVisible( bool visible ) +{ + mOkButton->setVisible( visible ); +} + void QgsNewDatabaseTableNameWidget::refreshModel( const QModelIndex &index ) { @@ -371,3 +387,51 @@ void QgsNewDatabaseTableNameWidget::showEvent( QShowEvent *e ) } } } + +// +// QgsNewDatabaseTableNameDialog +// +QgsNewDatabaseTableNameDialog::QgsNewDatabaseTableNameDialog( QgsBrowserGuiModel *browserModel, const QStringList &providersFilter, QWidget *parent ) + : QDialog( parent ) +{ + mWidget = new QgsNewDatabaseTableNameWidget( browserModel, providersFilter ); + QVBoxLayout *vl = new QVBoxLayout(); + vl->addWidget( mWidget, 1 ); + QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel ); + connect( buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept ); + connect( buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject ); + buttonBox->button( QDialogButtonBox::Ok )->setEnabled( false ); + connect( mWidget, &QgsNewDatabaseTableNameWidget::validationChanged, buttonBox->button( QDialogButtonBox::Ok ), &QWidget::setEnabled ); + vl->addWidget( buttonBox ); + setLayout( vl ); +} + +QString QgsNewDatabaseTableNameDialog::schema() const +{ + return mWidget->schema(); +} + +QString QgsNewDatabaseTableNameDialog::uri() const +{ + return mWidget->uri(); +} + +QString QgsNewDatabaseTableNameDialog::table() const +{ + return mWidget->table(); +} + +QString QgsNewDatabaseTableNameDialog::dataProviderKey() const +{ + return mWidget->dataProviderKey(); +} + +bool QgsNewDatabaseTableNameDialog::isValid() const +{ + return mWidget->isValid(); +} + +QString QgsNewDatabaseTableNameDialog::validationError() const +{ + return mWidget->validationError(); +} diff --git a/src/gui/qgsnewdatabasetablenamewidget.h b/src/gui/qgsnewdatabasetablenamewidget.h index 5ad1437307f8..4038ba547943 100644 --- a/src/gui/qgsnewdatabasetablenamewidget.h +++ b/src/gui/qgsnewdatabasetablenamewidget.h @@ -22,8 +22,10 @@ #include "qgis_gui.h" #include "qgsbrowserguimodel.h" #include "qgsbrowserproxymodel.h" +#include "qgspanelwidget.h" #include +#include /** * \ingroup gui @@ -39,7 +41,7 @@ * * \since QGIS 3.14 */ -class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::QgsNewDatabaseTableNameWidget +class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QgsPanelWidget, private Ui::QgsNewDatabaseTableNameWidget { Q_OBJECT @@ -58,6 +60,13 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs const QStringList &providersFilter = QStringList(), QWidget *parent = nullptr ); + /** + * Sets whether the optional "Ok"/accept button should be visible. + * + * By default this is hidden, to better allow the widget to be embedded inside other widgets and dialogs. + */ + void setAcceptButtonVisible( bool visible ); + /** * Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table */ @@ -130,6 +139,10 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs */ void uriChanged( const QString &uri ); + /** + * Emitted when the OK/accept button is clicked. + */ + void accepted(); private: @@ -160,4 +173,73 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs }; + +/** + * \ingroup gui + * QgsNewDatabaseTableNameDialog is a dialog which allows selection of a DB schema and a new table name. + * + * The table name is validated for uniqueness and the selected + * data item provider, schema and table names can be retrieved with + * getters. + * + * \warning The data provider that originated the data item provider + * must support the connections API + * + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsNewDatabaseTableNameDialog: public QDialog +{ + Q_OBJECT + + public: + + /** + * Constructs a new QgsNewDatabaseTableNameDialog + * + * \param browserModel an existing browser model (typically from app), if NULL an instance will be created + * \param providersFilter optional white list of data provider keys that should be + * shown in the widget, if not specified all providers data items with database + * capabilities will be shown + * \param parent optional parent for this widget + */ + explicit QgsNewDatabaseTableNameDialog( QgsBrowserGuiModel *browserModel = nullptr, + const QStringList &providersFilter = QStringList(), + QWidget *parent = nullptr ); + + /** + * Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table + */ + QString schema() const; + + /** + * Returns the (possibly blank) string representation of the new table data source URI. + * The URI might be invalid in case the widget is not in a valid state. + */ + QString uri() const; + + /** + * Returns the current name of the new table + */ + QString table() const; + + /** + * Returns the currently selected data item provider key + */ + QString dataProviderKey() const; + + /** + * Returns TRUE if the widget contains a valid new table name + */ + bool isValid() const; + + /** + * Returns the validation error or an empty string is the widget status is valid + */ + QString validationError() const; + + private: + + QgsNewDatabaseTableNameWidget *mWidget = nullptr; + +}; #endif // QGSNEWDATABASETABLENAMEWIDGET_H diff --git a/src/gui/qgsnewgeopackagelayerdialog.cpp b/src/gui/qgsnewgeopackagelayerdialog.cpp index 91bbd9aca494..e978c72e9928 100644 --- a/src/gui/qgsnewgeopackagelayerdialog.cpp +++ b/src/gui/qgsnewgeopackagelayerdialog.cpp @@ -31,11 +31,13 @@ #include "qgshelp.h" #include "qgsogrutils.h" #include "qgsgui.h" +#include "qgsproviderconnectionmodel.h" #include #include #include #include +#include #include #include @@ -129,6 +131,15 @@ QgsNewGeoPackageLayerDialog::QgsNewGeoPackageLayerDialog( QWidget *parent, Qt::W } checkOk(); } ); + + QgsProviderConnectionModel *ogrProviderModel = new QgsProviderConnectionModel( QStringLiteral( "ogr" ), this ); + + QCompleter *completer = new QCompleter( this ); + completer->setModel( ogrProviderModel ); + completer->setCompletionRole( QgsProviderConnectionModel::RoleUri ); + completer->setCompletionMode( QCompleter::PopupCompletion ); + completer->setFilterMode( Qt::MatchContains ); + mDatabase->lineEdit()->setCompleter( completer ); } void QgsNewGeoPackageLayerDialog::setCrs( const QgsCoordinateReferenceSystem &crs ) diff --git a/src/gui/qgsproviderconnectioncombobox.cpp b/src/gui/qgsproviderconnectioncombobox.cpp index ee53f8bec880..5de0479fa80f 100644 --- a/src/gui/qgsproviderconnectioncombobox.cpp +++ b/src/gui/qgsproviderconnectioncombobox.cpp @@ -19,6 +19,25 @@ QgsProviderConnectionComboBox::QgsProviderConnectionComboBox( const QString &provider, QWidget *parent ) : QComboBox( parent ) { + setProvider( provider ); +} + +QgsProviderConnectionComboBox::QgsProviderConnectionComboBox( QWidget *parent ) + : QComboBox( parent ) +{ +} + +void QgsProviderConnectionComboBox::setProvider( const QString &provider ) +{ + if ( mSortModel ) + { + disconnect( this, static_cast < void ( QComboBox::* )( int ) > ( &QComboBox::activated ), this, &QgsProviderConnectionComboBox::indexChanged ); + disconnect( mSortModel, &QAbstractItemModel::rowsInserted, this, &QgsProviderConnectionComboBox::rowsChanged ); + disconnect( mSortModel, &QAbstractItemModel::rowsRemoved, this, &QgsProviderConnectionComboBox::rowsChanged ); + delete mSortModel; + delete mModel; + } + mModel = new QgsProviderConnectionModel( provider, this ); mSortModel = new QgsProviderConnectionComboBoxSortModel( this ); diff --git a/src/gui/qgsproviderconnectioncombobox.h b/src/gui/qgsproviderconnectioncombobox.h index e400bf59b22c..05ed8f5cd1e7 100644 --- a/src/gui/qgsproviderconnectioncombobox.h +++ b/src/gui/qgsproviderconnectioncombobox.h @@ -55,11 +55,26 @@ class GUI_EXPORT QgsProviderConnectionComboBox : public QComboBox /** * Constructor for QgsProviderConnectionComboBox, for the specified \a provider. * - * \warning The provider must support the connection API methods in its QgsProviderMetadata implementation - * in order for the model to work correctly. + * \warning The provider must support the connection API methods in its QgsProviderMetadata implementation + * in order for the model to work correctly. */ explicit QgsProviderConnectionComboBox( const QString &provider, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + /** + * Constructor for QgsProviderConnectionComboBox. + * + * \note The combo box will not show any connection until setProvider() is called. + */ + explicit QgsProviderConnectionComboBox( QWidget *parent = nullptr ) SIP_SKIP; + + /** + * Sets the provider to be used. + * + * \warning The provider must support the connection API methods in its QgsProviderMetadata implementation + * in order for the model to work correctly. + */ + void setProvider( const QString &provider ); + /** * Sets whether an optional empty connection ("not set") option is present in the combobox. * \see allowEmptyConnection() diff --git a/src/gui/qgsproviderguiregistry.cpp b/src/gui/qgsproviderguiregistry.cpp index 743ac78aa520..f927378f66ab 100644 --- a/src/gui/qgsproviderguiregistry.cpp +++ b/src/gui/qgsproviderguiregistry.cpp @@ -24,6 +24,7 @@ #include "qgslogger.h" #include "qgsgdalguiprovider.h" #include "qgsogrguiprovider.h" +#include "qgsvectortileproviderguimetadata.h" #ifdef HAVE_STATIC_PROVIDERS #include "qgswmsprovidergui.h" @@ -66,6 +67,9 @@ void QgsProviderGuiRegistry::loadStaticProviders( ) QgsProviderGuiMetadata *ogr = new QgsOgrGuiProviderMetadata(); mProviders[ ogr->key() ] = ogr; + QgsProviderGuiMetadata *vt = new QgsVectorTileProviderGuiMetadata(); + mProviders[ vt->key() ] = vt; + #ifdef HAVE_STATIC_PROVIDERS QgsProviderGuiMetadata *wms = new QgsWmsProviderGuiMetadata(); mProviders[ wms->key() ] = wms; diff --git a/src/gui/qgsproxystyle.h b/src/gui/qgsproxystyle.h index cc7c58435f70..c4bf1523b187 100644 --- a/src/gui/qgsproxystyle.h +++ b/src/gui/qgsproxystyle.h @@ -57,6 +57,8 @@ class GUI_EXPORT QgsAppStyle : public QProxyStyle explicit QgsAppStyle( const QString &base ); QPixmap generatedIconPixmap( QIcon::Mode iconMode, const QPixmap &pixmap, const QStyleOption *opt ) const override; + QString baseStyle() const { return mBaseStyle; } + /** * Returns a new QgsAppStyle instance, with the same base style as this instance. * diff --git a/src/gui/qgsrelationeditorwidget.cpp b/src/gui/qgsrelationeditorwidget.cpp index e3902c1ada7a..9ce6d86b3008 100644 --- a/src/gui/qgsrelationeditorwidget.cpp +++ b/src/gui/qgsrelationeditorwidget.cpp @@ -285,7 +285,7 @@ void QgsRelationEditorWidget::initDualView( QgsVectorLayer *layer, const QgsFeat text = tr( "Add Polygon Feature" ); } - if ( text.isEmpty() ) + if ( text.isEmpty() || !mEditorContext.mapCanvas() || !mEditorContext.cadDockWidget() ) { mAddFeatureGeometryButton->setVisible( false ); } @@ -368,8 +368,17 @@ void QgsRelationEditorWidget::setRelations( const QgsRelation &relation, const Q void QgsRelationEditorWidget::setEditorContext( const QgsAttributeEditorContext &context ) { mEditorContext = context; - mMapToolDigitize.reset( new QgsMapToolDigitizeFeature( context.mapCanvas(), context.cadDockWidget() ) ); - mMapToolDigitize->setButton( mAddFeatureGeometryButton ); + + if ( context.mapCanvas() && context.cadDockWidget() ) + { + mMapToolDigitize.reset( new QgsMapToolDigitizeFeature( context.mapCanvas(), context.cadDockWidget() ) ); + mMapToolDigitize->setButton( mAddFeatureGeometryButton ); + } +} + +QgsAttributeEditorContext QgsRelationEditorWidget::editorContext() const +{ + return mEditorContext; } QgsIFeatureSelectionManager *QgsRelationEditorWidget::featureSelectionManager() @@ -448,6 +457,9 @@ void QgsRelationEditorWidget::addFeatureGeometry() layer = mRelation.referencingLayer(); mMapToolDigitize->setLayer( layer ); + + // window is always on top, so we hide it to digitize without seeing it + window()->setVisible( false ); setMapTool( mMapToolDigitize ); connect( mMapToolDigitize, &QgsMapToolDigitizeFeature::digitizingCompleted, this, &QgsRelationEditorWidget::onDigitizingCompleted ); @@ -984,6 +996,7 @@ void QgsRelationEditorWidget::onKeyPressed( QKeyEvent *e ) void QgsRelationEditorWidget::mapToolDeactivated() { + window()->setVisible( true ); window()->raise(); window()->activateWindow(); diff --git a/src/gui/qgsrelationeditorwidget.h b/src/gui/qgsrelationeditorwidget.h index 9ae5a97f437e..91669f3728ae 100644 --- a/src/gui/qgsrelationeditorwidget.h +++ b/src/gui/qgsrelationeditorwidget.h @@ -134,9 +134,17 @@ class GUI_EXPORT QgsRelationEditorWidget : public QgsCollapsibleGroupBox /** * Sets the editor \a context + * \note if context cadDockWidget is null, it won't be possible to digitize + * the geometry of a referencing feature from this widget */ void setEditorContext( const QgsAttributeEditorContext &context ); + /** + * Returns the attribute editor context. + * \since QGIS 3.14 + */ + QgsAttributeEditorContext editorContext( ) const; + /** * The feature selection manager is responsible for the selected features * which are currently being edited. diff --git a/src/gui/qgsscalewidget.cpp b/src/gui/qgsscalewidget.cpp index 8de2bcb903fe..fabb14d13732 100644 --- a/src/gui/qgsscalewidget.cpp +++ b/src/gui/qgsscalewidget.cpp @@ -18,6 +18,12 @@ #include "qgsapplication.h" #include "qgsscalewidget.h" #include "qgsmapcanvas.h" +#include "qgsproject.h" +#include "qgslayoutmanager.h" +#include "qgslayoutitemmap.h" +#include "qgsprintlayout.h" + +#include QgsScaleWidget::QgsScaleWidget( QWidget *parent ) : QWidget( parent ) @@ -32,6 +38,12 @@ QgsScaleWidget::QgsScaleWidget( QWidget *parent ) mCurrentScaleButton = new QToolButton( this ); mCurrentScaleButton->setToolTip( tr( "Set to current canvas scale" ) ); mCurrentScaleButton->setIcon( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMapIdentification.svg" ) ) ); + + mMenu = new QMenu( this ); + mCurrentScaleButton->setMenu( mMenu ); + mCurrentScaleButton->setPopupMode( QToolButton::MenuButtonPopup ); + connect( mMenu, &QMenu::aboutToShow, this, &QgsScaleWidget::menuAboutToShow ); + layout->addWidget( mCurrentScaleButton ); mCurrentScaleButton->hide(); @@ -83,3 +95,42 @@ void QgsScaleWidget::setScale( double scale ) { mScaleComboBox->setScale( scale ); } + +void QgsScaleWidget::menuAboutToShow() +{ + mMenu->clear(); + + double scale = mCanvas->scale(); + QAction *canvasScaleAction = new QAction( QgsApplication::getThemeIcon( QStringLiteral( "/mActionMapIdentification.svg" ) ), + tr( "Current Canvas Scale (1:%1)" ).arg( qgsDoubleToString( scale, 0 ) ), mMenu ); + connect( canvasScaleAction, &QAction::triggered, this, [this, scale] { setScale( scale ); } ); + mMenu->addAction( canvasScaleAction ); + + bool first = true; + if ( QgsLayoutManager *manager = QgsProject::instance()->layoutManager() ) + { + const QList layouts = manager->printLayouts(); + for ( const QgsPrintLayout *layout : layouts ) + { + QList< QgsLayoutItemMap * > maps; + layout->layoutItems( maps ); + if ( maps.empty() ) + continue; + + if ( first ) + mMenu->addSeparator(); + + first = false; + + QMenu *layoutMenu = new QMenu( layout->name(), mMenu ); + for ( const QgsLayoutItemMap *map : qgis::as_const( maps ) ) + { + scale = map->scale(); + QAction *mapScaleAction = new QAction( tr( "%1 (1:%2)" ).arg( map->displayName(), qgsDoubleToString( scale, 0 ) ), mMenu ); + connect( mapScaleAction, &QAction::triggered, this, [this, scale] { setScale( scale ); } ); + layoutMenu->addAction( mapScaleAction ); + } + mMenu->addMenu( layoutMenu ); + } + } +} diff --git a/src/gui/qgsscalewidget.h b/src/gui/qgsscalewidget.h index 23b956d7f44c..9796dfdb546f 100644 --- a/src/gui/qgsscalewidget.h +++ b/src/gui/qgsscalewidget.h @@ -184,10 +184,15 @@ class GUI_EXPORT QgsScaleWidget : public QWidget */ void scaleChanged( double scale ); + private slots: + + void menuAboutToShow(); + private: QgsScaleComboBox *mScaleComboBox = nullptr; QToolButton *mCurrentScaleButton = nullptr; QgsMapCanvas *mCanvas = nullptr; + QMenu *mMenu = nullptr; bool mShowCurrentScaleButton = false; }; diff --git a/src/gui/qgsstyleitemslistwidget.cpp b/src/gui/qgsstyleitemslistwidget.cpp index 4fd03da7302a..c2cd54fa8617 100644 --- a/src/gui/qgsstyleitemslistwidget.cpp +++ b/src/gui/qgsstyleitemslistwidget.cpp @@ -86,6 +86,7 @@ QgsStyleItemsListWidget::QgsStyleItemsListWidget( QWidget *parent ) double treeIconSize = Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 2; #endif mSymbolTreeView->setIconSize( QSize( static_cast< int >( treeIconSize ), static_cast< int >( treeIconSize ) ) ); + mSymbolTreeView->setMinimumHeight( mSymbolTreeView->fontMetrics().height() * 6 ); viewSymbols->setSelectionBehavior( QAbstractItemView::SelectRows ); mSymbolTreeView->setSelectionMode( viewSymbols->selectionMode() ); diff --git a/src/gui/qgssymbolbutton.cpp b/src/gui/qgssymbolbutton.cpp index 972ce5a192f6..511bc23fa2d7 100644 --- a/src/gui/qgssymbolbutton.cpp +++ b/src/gui/qgssymbolbutton.cpp @@ -120,7 +120,7 @@ void QgsSymbolButton::showSettingsDialog() } else { - QgsSymbolSelectorDialog dialog( newSymbol, QgsStyle::defaultStyle(), mLayer, nullptr ); + QgsSymbolSelectorDialog dialog( newSymbol, QgsStyle::defaultStyle(), mLayer, this ); dialog.setWindowTitle( mDialogTitle ); dialog.setContext( symbolContext ); if ( dialog.exec() ) diff --git a/src/gui/qgssymbollayerselectionwidget.cpp b/src/gui/qgssymbollayerselectionwidget.cpp index b7368a9d8a04..fe27527221df 100644 --- a/src/gui/qgssymbollayerselectionwidget.cpp +++ b/src/gui/qgssymbollayerselectionwidget.cpp @@ -37,6 +37,7 @@ QgsSymbolLayerSelectionWidget::QgsSymbolLayerSelectionWidget( QWidget *parent ) // place the tree in a layout QVBoxLayout *vbox = new QVBoxLayout(); + vbox->setContentsMargins( 0, 0, 0, 0 ); vbox->addWidget( mTree ); setLayout( vbox ); diff --git a/src/gui/qgstaskmanagerwidget.cpp b/src/gui/qgstaskmanagerwidget.cpp index 393f647e04ef..e485076686c5 100644 --- a/src/gui/qgstaskmanagerwidget.cpp +++ b/src/gui/qgstaskmanagerwidget.cpp @@ -621,6 +621,9 @@ QgsTaskManagerStatusBarWidget::QgsTaskManagerStatusBarWidget( QgsTaskManager *ma connect( manager, &QgsTaskManager::allTasksFinished, this, &QgsTaskManagerStatusBarWidget::allFinished ); connect( manager, &QgsTaskManager::finalTaskProgressChanged, this, &QgsTaskManagerStatusBarWidget::overallProgressChanged ); connect( manager, &QgsTaskManager::countActiveTasksChanged, this, &QgsTaskManagerStatusBarWidget::countActiveTasksChanged ); + + if ( manager->countActiveTasks() ) + showButton(); } QSize QgsTaskManagerStatusBarWidget::sizeHint() const diff --git a/src/gui/qgstemporalcontrollerdockwidget.cpp b/src/gui/qgstemporalcontrollerwidget.cpp similarity index 64% rename from src/gui/qgstemporalcontrollerdockwidget.cpp rename to src/gui/qgstemporalcontrollerwidget.cpp index a0e189438d29..4c75b4ed2385 100644 --- a/src/gui/qgstemporalcontrollerdockwidget.cpp +++ b/src/gui/qgstemporalcontrollerwidget.cpp @@ -1,5 +1,5 @@ /*************************************************************************** - qgstemporalcontrollerdockwidget.cpp + qgstemporalcontrollerwidget.cpp ------------------------------ begin : February 2020 copyright : (C) 2020 by Samweli Mwakisambwe @@ -15,20 +15,19 @@ * * ***************************************************************************/ -#include "qgstemporalcontrollerdockwidget.h" +#include "qgstemporalcontrollerwidget.h" #include "qgsgui.h" #include "qgsproject.h" #include "qgsprojecttimesettings.h" #include "qgstemporalnavigationobject.h" #include "qgstemporalmapsettingswidget.h" +#include "qgstemporalutils.h" +#include "qgsmaplayertemporalproperties.h" -#include "qgstemporalmapsettingsdialog.h" - -QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString &name, QWidget *parent ) - : QgsDockWidget( parent ) +QgsTemporalControllerWidget::QgsTemporalControllerWidget( QWidget *parent ) + : QgsPanelWidget( parent ) { setupUi( this ); - setWindowTitle( name ); mNavigationObject = new QgsTemporalNavigationObject( this ); @@ -48,25 +47,26 @@ QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString mStopButton->setChecked( state == QgsTemporalNavigationObject::Idle ); } ); - connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerDockWidget::updateTemporalExtent ); - connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerDockWidget::updateTemporalExtent ); - connect( mSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerDockWidget::updateFrameDuration ); - connect( mTimeStepsComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerDockWidget::updateFrameDuration ); - connect( mSlider, &QSlider::valueChanged, this, &QgsTemporalControllerDockWidget::timeSlider_valueChanged ); + connect( mStartDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::updateTemporalExtent ); + connect( mEndDateTime, &QDateTimeEdit::dateTimeChanged, this, &QgsTemporalControllerWidget::updateTemporalExtent ); + connect( mSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration ); + connect( mTimeStepsComboBox, qgis::overload::of( &QComboBox::currentIndexChanged ), this, &QgsTemporalControllerWidget::updateFrameDuration ); + connect( mSlider, &QSlider::valueChanged, this, &QgsTemporalControllerWidget::timeSlider_valueChanged ); + + mSpinBox->setClearValue( 1 ); - connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerDockWidget::updateSlider ); + connect( mNavigationObject, &QgsTemporalNavigationObject::updateTemporalRange, this, &QgsTemporalControllerWidget::updateSlider ); - connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerDockWidget::settings_clicked ); - connect( mSetToProjectTimeButton, &QPushButton::clicked, this, &QgsTemporalControllerDockWidget::setDatesToProjectTime ); + connect( mSettings, &QPushButton::clicked, this, &QgsTemporalControllerWidget::settings_clicked ); + connect( mSetToProjectTimeButton, &QPushButton::clicked, this, &QgsTemporalControllerWidget::setDatesToProjectTime ); QgsDateTimeRange range; if ( QgsProject::instance()->timeSettings() ) range = QgsProject::instance()->timeSettings()->temporalRange(); - QLocale locale; - mStartDateTime->setDisplayFormat( locale.dateTimeFormat( QLocale::ShortFormat ) ); - mEndDateTime->setDisplayFormat( locale.dateTimeFormat( QLocale::ShortFormat ) ); + mStartDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); + mEndDateTime->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); if ( range.begin().isValid() && range.end().isValid() ) { @@ -74,7 +74,10 @@ QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString mEndDateTime->setDateTime( range.end() ); } - mSetToProjectTimeButton->setToolTip( tr( "Set datetimes inputs to match project time" ) ); + mSetToProjectTimeButton->setToolTip( tr( "Match time range to project. \n" + "If a project has no explicit time range set, \n" + "then the range will be calculated based on the \n" + "minimum and maximum dates from any temporal-enabled layers." ) ); for ( QgsUnitTypes::TemporalUnit u : { @@ -110,10 +113,12 @@ QgsTemporalControllerDockWidget::QgsTemporalControllerDockWidget( const QString updateFrameDuration(); - connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerDockWidget::setWidgetStateFromProject ); + connect( QgsProject::instance(), &QgsProject::readProject, this, &QgsTemporalControllerWidget::setWidgetStateFromProject ); + connect( QgsProject::instance(), &QgsProject::layersAdded, this, &QgsTemporalControllerWidget::onLayersAdded ); + connect( QgsProject::instance(), &QgsProject::cleared, this, &QgsTemporalControllerWidget::onProjectCleared ); } -void QgsTemporalControllerDockWidget::updateTemporalExtent() +void QgsTemporalControllerWidget::updateTemporalExtent() { QgsDateTimeRange temporalExtent = QgsDateTimeRange( mStartDateTime->dateTime(), mEndDateTime->dateTime() ); @@ -122,7 +127,7 @@ void QgsTemporalControllerDockWidget::updateTemporalExtent() mSlider->setValue( 0 ); } -void QgsTemporalControllerDockWidget::updateFrameDuration() +void QgsTemporalControllerWidget::updateFrameDuration() { if ( mBlockSettingUpdates ) return; @@ -136,7 +141,7 @@ void QgsTemporalControllerDockWidget::updateFrameDuration() mSlider->setRange( 0, mNavigationObject->totalFrameCount() - 1 ); } -void QgsTemporalControllerDockWidget::setWidgetStateFromProject() +void QgsTemporalControllerWidget::setWidgetStateFromProject() { mBlockSettingUpdates++; mTimeStepsComboBox->setCurrentIndex( mTimeStepsComboBox->findData( QgsProject::instance()->timeSettings()->timeStepUnit() ) ); @@ -147,64 +152,96 @@ void QgsTemporalControllerDockWidget::setWidgetStateFromProject() mNavigationObject->setFramesPerSecond( QgsProject::instance()->timeSettings()->framesPerSecond() ); } -void QgsTemporalControllerDockWidget::updateSlider( const QgsDateTimeRange &range ) +void QgsTemporalControllerWidget::onLayersAdded() +{ + if ( !mHasTemporalLayersLoaded ) + { + QVector layers = QgsProject::instance()->layers(); + for ( QgsMapLayer *layer : layers ) + { + if ( layer->temporalProperties() ) + mHasTemporalLayersLoaded |= layer->temporalProperties()->isActive(); + } + + if ( mHasTemporalLayersLoaded ) + setDatesToProjectTime(); + } +} + +void QgsTemporalControllerWidget::onProjectCleared() +{ + mHasTemporalLayersLoaded = false; + mStartDateTime->setDateTime( QDateTime( QDate::currentDate(), QTime( 0, 0, 0, Qt::UTC ) ) ); + mEndDateTime->setDateTime( mStartDateTime->dateTime() ); + updateTemporalExtent(); +} + +void QgsTemporalControllerWidget::updateSlider( const QgsDateTimeRange &range ) { whileBlocking( mSlider )->setValue( mNavigationObject->currentFrameNumber() ); updateRangeLabel( range ); } -void QgsTemporalControllerDockWidget::updateRangeLabel( const QgsDateTimeRange &range ) +void QgsTemporalControllerWidget::updateRangeLabel( const QgsDateTimeRange &range ) { - QLocale locale; mCurrentRangeLabel->setText( tr( "%1 to %2" ).arg( - range.begin().toString( locale.dateTimeFormat( QLocale::NarrowFormat ) ), - range.end().toString( locale.dateTimeFormat( QLocale::NarrowFormat ) ) ) ); + range.begin().toString( "yyyy-MM-dd HH:mm:ss" ), + range.end().toString( "yyyy-MM-dd HH:mm:ss" ) ) ); } -QgsTemporalController *QgsTemporalControllerDockWidget::temporalController() +QgsTemporalController *QgsTemporalControllerWidget::temporalController() { return mNavigationObject; } -void QgsTemporalControllerDockWidget::settings_clicked() +void QgsTemporalControllerWidget::settings_clicked() { - QgsTemporalMapSettingsDialog dialog( this ); - dialog.mapSettingsWidget()->setFrameRateValue( mNavigationObject->framesPerSecond() ); + QgsTemporalMapSettingsWidget *settingsWidget = new QgsTemporalMapSettingsWidget( this ); + settingsWidget->setFrameRateValue( mNavigationObject->framesPerSecond() ); - if ( dialog.exec() ) + connect( settingsWidget, &QgsTemporalMapSettingsWidget::frameRateChanged, this, [ = ]( double rate ) { // save new settings into project - QgsProject::instance()->timeSettings()->setFramesPerSecond( dialog.mapSettingsWidget()->frameRateValue() ); - - mNavigationObject->setFramesPerSecond( QgsProject::instance()->timeSettings()->framesPerSecond() ); - } + QgsProject::instance()->timeSettings()->setFramesPerSecond( rate ); + mNavigationObject->setFramesPerSecond( rate ); + } ); + openPanel( settingsWidget ); } -void QgsTemporalControllerDockWidget::timeSlider_valueChanged( int value ) +void QgsTemporalControllerWidget::timeSlider_valueChanged( int value ) { mNavigationObject->setCurrentFrameNumber( value ); } -void QgsTemporalControllerDockWidget::setDatesToProjectTime() +void QgsTemporalControllerWidget::setDatesToProjectTime() { QgsDateTimeRange range; + + // by default try taking the project's fixed temporal extent if ( QgsProject::instance()->timeSettings() ) range = QgsProject::instance()->timeSettings()->temporalRange(); + // if that's not set, calculate the extent from the project's layers + if ( !range.begin().isValid() || !range.end().isValid() ) + { + range = QgsTemporalUtils::calculateTemporalRangeForProject( QgsProject::instance() ); + } + if ( range.begin().isValid() && range.end().isValid() ) { mStartDateTime->setDateTime( range.begin() ); mEndDateTime->setDateTime( range.end() ); + updateTemporalExtent(); } } -void QgsTemporalControllerDockWidget::setDateInputsEnable( bool enabled ) +void QgsTemporalControllerWidget::setDateInputsEnable( bool enabled ) { mStartDateTime->setEnabled( enabled ); mEndDateTime->setEnabled( enabled ); } -void QgsTemporalControllerDockWidget::updateButtonsEnable( bool enabled ) +void QgsTemporalControllerWidget::updateButtonsEnable( bool enabled ) { mPreviousButton->setEnabled( enabled ); mNextButton->setEnabled( enabled ); diff --git a/src/gui/qgstemporalcontrollerdockwidget.h b/src/gui/qgstemporalcontrollerwidget.h similarity index 79% rename from src/gui/qgstemporalcontrollerdockwidget.h rename to src/gui/qgstemporalcontrollerwidget.h index 07e58d1a2732..c94c2cedc05d 100644 --- a/src/gui/qgstemporalcontrollerdockwidget.h +++ b/src/gui/qgstemporalcontrollerwidget.h @@ -1,5 +1,5 @@ /*************************************************************************** - qgstemporalcontrollerdockwidget.h + qgstemporalcontrollerwidget.h --------------- begin : February 2020 copyright : (C) 2020 by Samweli Mwakisambwe @@ -15,37 +15,34 @@ * * ***************************************************************************/ -#ifndef QGSTEMPORALCONTROLLERDOCKWIDGET_H -#define QGSTEMPORALCONTROLLERDOCKWIDGET_H +#ifndef QGSTEMPORALCONTROLLERWIDGET_H +#define QGSTEMPORALCONTROLLERWIDGET_H -#include "ui_qgstemporalcontrollerdockwidgetbase.h" +#include "ui_qgstemporalcontrollerwidgetbase.h" -#include "qgsdockwidget.h" #include "qgis_gui.h" #include "qgsrange.h" class QgsMapLayer; class QgsTemporalNavigationObject; -class QgsTemporalMapSettingsWidget; -class QgsTemporalMapSettingsDialog; class QgsTemporalController; class QgsInterval; /** * \ingroup gui - * The QgsTemporalControllerDockWidget class + * A widget for controlling playback properties of a QgsTemporalController. * * \since QGIS 3.14 */ -class GUI_EXPORT QgsTemporalControllerDockWidget : public QgsDockWidget, private Ui::QgsTemporalControllerDockWidgetBase +class GUI_EXPORT QgsTemporalControllerWidget : public QgsPanelWidget, private Ui::QgsTemporalControllerWidgetBase { Q_OBJECT public: /** - * Constructor for QgsTemporalControllerDockWidget, with the specified \a parent widget. + * Constructor for QgsTemporalControllerWidget, with the specified \a parent widget. */ - QgsTemporalControllerDockWidget( const QString &name, QWidget *parent SIP_TRANSFERTHIS = nullptr ); + QgsTemporalControllerWidget( QWidget *parent SIP_TRANSFERTHIS = nullptr ); /** * Returns the temporal controller object used by this object in navigation. @@ -71,6 +68,8 @@ class GUI_EXPORT QgsTemporalControllerDockWidget : public QgsDockWidget, private int mBlockSettingUpdates = 0; + bool mHasTemporalLayersLoaded = false; + private slots: /** @@ -111,7 +110,8 @@ class GUI_EXPORT QgsTemporalControllerDockWidget : public QgsDockWidget, private void setWidgetStateFromProject(); - + void onLayersAdded(); + void onProjectCleared(); }; -#endif // QGSTEMPORALCONTROLLERDOCKWIDGET_H +#endif // QGSTEMPORALCONTROLLERWIDGET_H diff --git a/src/gui/qgstemporalmapsettingsdialog.cpp b/src/gui/qgstemporalmapsettingsdialog.cpp deleted file mode 100644 index 0e67feb38251..000000000000 --- a/src/gui/qgstemporalmapsettingsdialog.cpp +++ /dev/null @@ -1,48 +0,0 @@ -/*************************************************************************** - qgstemporalmapsettingsdialog.cpp - --------------- - begin : March 2020 - copyright : (C) 2020 by Samweli Mwakisambwe - email : samweli at kartoza 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 "qgstemporalmapsettingsdialog.h" -#include "qgsgui.h" -#include "qgstemporalmapsettingswidget.h" - -///@cond PRIVATE - -QgsTemporalMapSettingsDialog::QgsTemporalMapSettingsDialog( QWidget *parent, Qt::WindowFlags flags ) - : QDialog( parent, flags ) -{ - QVBoxLayout *vl = new QVBoxLayout( ); - - mTemporalMapSettingsWidget = new QgsTemporalMapSettingsWidget( this ); - - vl->addWidget( mTemporalMapSettingsWidget, 1 ); - - QDialogButtonBox *box = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel ); - vl->addWidget( box ); - setLayout( vl ); - - connect( box, &QDialogButtonBox::accepted, this, &QgsTemporalMapSettingsDialog::accept ); - connect( box, &QDialogButtonBox::rejected, this, &QgsTemporalMapSettingsDialog::reject ); - - setWindowTitle( tr( "Temporal Map Settings" ) ); -} - -QgsTemporalMapSettingsWidget *QgsTemporalMapSettingsDialog::mapSettingsWidget() -{ - return mTemporalMapSettingsWidget; -} - -///@endcond diff --git a/src/gui/qgstemporalmapsettingswidget.cpp b/src/gui/qgstemporalmapsettingswidget.cpp index a0f3f208e138..01e93e621276 100644 --- a/src/gui/qgstemporalmapsettingswidget.cpp +++ b/src/gui/qgstemporalmapsettingswidget.cpp @@ -18,13 +18,17 @@ #include "qgstemporalmapsettingswidget.h" #include "qgsgui.h" #include "qgis.h" + ///@cond PRIVATE QgsTemporalMapSettingsWidget::QgsTemporalMapSettingsWidget( QWidget *parent ) - : QWidget( parent ) + : QgsPanelWidget( parent ) { setupUi( this ); + setPanelTitle( tr( "Temporal Settings" ) ); + + mFrameSpinBox->setClearValue( 1 ); - connect( mFrameSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalMapSettingsWidget::frameRateChange ); + connect( mFrameSpinBox, qgis::overload::of( &QDoubleSpinBox::valueChanged ), this, &QgsTemporalMapSettingsWidget::frameRateChanged ); } double QgsTemporalMapSettingsWidget::frameRateValue() @@ -37,9 +41,4 @@ void QgsTemporalMapSettingsWidget::setFrameRateValue( double value ) mFrameSpinBox->setValue( value ); } -void QgsTemporalMapSettingsWidget::frameRateChange() -{ - emit frameRateChanged(); -} - ///@endcond diff --git a/src/gui/qgstemporalmapsettingswidget.h b/src/gui/qgstemporalmapsettingswidget.h index d42ab3778c0e..03f19d73e218 100644 --- a/src/gui/qgstemporalmapsettingswidget.h +++ b/src/gui/qgstemporalmapsettingswidget.h @@ -25,7 +25,7 @@ #define SIP_NO_FILE ///@cond PRIVATE -class GUI_EXPORT QgsTemporalMapSettingsWidget : public QWidget, private Ui::QgsTemporalMapSettingsWidgetBase +class GUI_EXPORT QgsTemporalMapSettingsWidget : public QgsPanelWidget, private Ui::QgsTemporalMapSettingsWidgetBase { Q_OBJECT public: @@ -50,18 +50,9 @@ class GUI_EXPORT QgsTemporalMapSettingsWidget : public QWidget, private Ui::QgsT signals: /** - * Emitted when frame rate value on the spin box has changed. + * Emitted when frame \a rate value on the spin box has changed. */ - void frameRateChanged(); - - private slots: - - /** - * Emits frame rate change signal. - * - * \see frameRateChanged() - */ - void frameRateChange(); + void frameRateChanged( double rate ); }; diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index ddae287d0302..68f2c511b2f6 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -493,6 +493,11 @@ void QgsTextFormatWidget::initWidget() { updateShadowFrameStatus(); } ); + connect( mCalloutDrawDDBtn, &QgsPropertyOverrideButton::activated, this, &QgsTextFormatWidget::updateCalloutFrameStatus ); + connect( mCalloutsDrawCheckBox, &QCheckBox::stateChanged, this, [ = ]( int ) + { + updateCalloutFrameStatus(); + } ); mGeometryGeneratorType->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mIconPolygonLayer.svg" ) ), tr( "Polygon / MultiPolygon" ), QgsWkbTypes::GeometryType::PolygonGeometry ); mGeometryGeneratorType->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mIconLineLayer.svg" ) ), tr( "LineString / MultiLineString" ), QgsWkbTypes::GeometryType::LineGeometry ); @@ -500,6 +505,8 @@ void QgsTextFormatWidget::initWidget() // set correct initial tab to match displayed setting page whileBlocking( mOptionsTab )->setCurrentIndex( mLabelStackedWidget->currentIndex() ); + mOptionsTab->tabBar()->setUsesScrollButtons( true ); + if ( mMapCanvas ) { @@ -533,7 +540,12 @@ void QgsTextFormatWidget::setWidgetMode( QgsTextFormatWidget::Mode mode ) mOptionsTab->removeTab( 8 ); mOptionsTab->removeTab( 7 ); mOptionsTab->removeTab( 6 ); - mOptionsTab->removeTab( 5 ); + mOptionsTab->removeTab( 3 ); + mLabelStackedWidget->removeWidget( mLabelPage_Rendering ); + mLabelStackedWidget->removeWidget( mLabelPage_Callouts ); + mLabelStackedWidget->removeWidget( mLabelPage_Mask ); + mLabelStackedWidget->removeWidget( mLabelPage_Placement ); + mLabelStackedWidget->setCurrentIndex( 0 ); frameLabelWith->hide(); mDirectSymbolsFrame->hide(); @@ -795,6 +807,8 @@ void QgsTextFormatWidget::populateDataDefinedButtons() registerDataDefinedButton( mZIndexDDBtn, QgsPalLayerSettings::ZIndex ); registerDataDefinedButton( mCalloutDrawDDBtn, QgsPalLayerSettings::CalloutDraw ); + mCalloutDrawDDBtn->registerCheckedWidget( mCalloutsDrawCheckBox ); + registerDataDefinedButton( mLabelAllPartsDDBtn, QgsPalLayerSettings::LabelAllParts ); } @@ -1727,6 +1741,11 @@ void QgsTextFormatWidget::updateShadowFrameStatus() mShadowFrame->setEnabled( mShadowDrawDDBtn->isActive() || mShadowDrawChkBx->isChecked() ); } +void QgsTextFormatWidget::updateCalloutFrameStatus() +{ + mCalloutFrame->setEnabled( mCalloutDrawDDBtn->isActive() || mCalloutsDrawCheckBox->isChecked() ); +} + void QgsTextFormatWidget::setFormatFromStyle( const QString &name, QgsStyle::StyleEntity type ) { switch ( type ) diff --git a/src/gui/qgstextformatwidget.h b/src/gui/qgstextformatwidget.h index 554c1b854b95..3d2dd6bce909 100644 --- a/src/gui/qgstextformatwidget.h +++ b/src/gui/qgstextformatwidget.h @@ -304,6 +304,7 @@ class GUI_EXPORT QgsTextFormatWidget : public QWidget, public QgsExpressionConte void updateShapeFrameStatus(); void updateBufferFrameStatus(); void updateShadowFrameStatus(); + void updateCalloutFrameStatus(); }; diff --git a/src/gui/raster/qgscolorrampshaderwidget.cpp b/src/gui/raster/qgscolorrampshaderwidget.cpp index c88557c181f4..21195aa1bc83 100644 --- a/src/gui/raster/qgscolorrampshaderwidget.cpp +++ b/src/gui/raster/qgscolorrampshaderwidget.cpp @@ -94,6 +94,7 @@ QgsColorRampShaderWidget::QgsColorRampShaderWidget( QWidget *parent ) resetClassifyButton(); connect( mClassificationModeComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::classify ); + connect( mColorInterpolationComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsColorRampShaderWidget::classify ); connect( mClassifyButton, &QPushButton::clicked, this, &QgsColorRampShaderWidget::classify ); connect( btnColorRamp, &QgsColorRampButton::colorRampChanged, this, &QgsColorRampShaderWidget::applyColorRamp ); connect( mNumberOfEntriesSpinBox, static_cast < void ( QSpinBox::* )( int ) > ( &QSpinBox::valueChanged ), this, &QgsColorRampShaderWidget::classify ); @@ -265,7 +266,6 @@ void QgsColorRampShaderWidget::setUnitFromLabels() } } - void QgsColorRampShaderWidget::mAddEntryButton_clicked() { QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget ); @@ -337,6 +337,7 @@ void QgsColorRampShaderWidget::classify() connect( newItem, &QgsTreeWidgetItemObject::itemEdited, this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited ); } + mClipCheckBox->setChecked( colorRampShader->clip() ); autoLabel(); @@ -348,7 +349,6 @@ void QgsColorRampShaderWidget::mClassificationModeComboBox_currentIndexChanged( QgsColorRampShader::ClassificationMode mode = static_cast< QgsColorRampShader::ClassificationMode >( mClassificationModeComboBox->itemData( index ).toInt() ); mNumberOfEntriesSpinBox->setEnabled( mode != QgsColorRampShader::Continuous ); emit classificationModeChanged( mode ); - } void QgsColorRampShaderWidget::applyColorRamp() @@ -388,7 +388,7 @@ void QgsColorRampShaderWidget::applyColorRamp() double value = currentItem->text( ValueColumn ).toDouble(); double position = ( value - mMin ) / ( mMax - mMin ); - currentItem->setData( ColorColumn, Qt::EditRole, ramp->color( position ) ); + whileBlocking( static_cast( currentItem ) )->setData( ColorColumn, Qt::EditRole, ramp->color( position ) ); } emit widgetChanged(); @@ -414,7 +414,6 @@ void QgsColorRampShaderWidget::populateColormapTreeWidget( const QListsetChecked( colorRampShader.clip() ); + whileBlocking( mColorInterpolationComboBox )->setCurrentIndex( mColorInterpolationComboBox->findData( colorRampShader.colorRampType() ) ); + mColorInterpolationComboBox_currentIndexChanged( mColorInterpolationComboBox->currentIndex() ); + whileBlocking( mClassificationModeComboBox )->setCurrentIndex( mClassificationModeComboBox->findData( colorRampShader.classificationMode() ) ); + mClassificationModeComboBox_currentIndexChanged( mClassificationModeComboBox->currentIndex() ); + whileBlocking( mNumberOfEntriesSpinBox )->setValue( colorRampShader.colorRampItemList().count() ); // some default + if ( colorRampShader.sourceColorRamp() ) { - btnColorRamp->setColorRamp( colorRampShader.sourceColorRamp() ); + whileBlocking( btnColorRamp )->setColorRamp( colorRampShader.sourceColorRamp() ); } else { @@ -656,26 +666,7 @@ void QgsColorRampShaderWidget::setFromShader( const QgsColorRampShader &colorRam btnColorRamp->setColorRampFromName( defaultPalette ); } - mColorInterpolationComboBox->setCurrentIndex( mColorInterpolationComboBox->findData( colorRampShader.colorRampType() ) ); - - mColormapTreeWidget->clear(); - const QList colorRampItemList = colorRampShader.colorRampItemList(); - QList::const_iterator it = colorRampItemList.constBegin(); - for ( ; it != colorRampItemList.end(); ++it ) - { - QgsTreeWidgetItemObject *newItem = new QgsTreeWidgetItemObject( mColormapTreeWidget ); - newItem->setText( ValueColumn, QString::number( it->value, 'g', 15 ) ); - newItem->setData( ColorColumn, Qt::EditRole, it->color ); - newItem->setText( LabelColumn, it->label ); - newItem->setFlags( Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsSelectable ); - connect( newItem, &QgsTreeWidgetItemObject::itemEdited, - this, &QgsColorRampShaderWidget::mColormapTreeWidget_itemEdited ); - } - setUnitFromLabels(); - - mClipCheckBox->setChecked( colorRampShader.clip() ); - mClassificationModeComboBox->setCurrentIndex( mClassificationModeComboBox->findData( colorRampShader.classificationMode() ) ); - mNumberOfEntriesSpinBox->setValue( colorRampShader.colorRampItemList().count() ); // some default + emit widgetChanged(); } void QgsColorRampShaderWidget::mColorInterpolationComboBox_currentIndexChanged( int index ) diff --git a/src/gui/raster/qgsrasterbandcombobox.cpp b/src/gui/raster/qgsrasterbandcombobox.cpp index 1ae90784b2d9..3fd4f8c15a76 100644 --- a/src/gui/raster/qgsrasterbandcombobox.cpp +++ b/src/gui/raster/qgsrasterbandcombobox.cpp @@ -161,7 +161,7 @@ void QgsRasterBandComboBox::setShowNotSetOption( bool show, const QString &strin setLayer( mLayer ); } -QString QgsRasterBandComboBox::displayBandName( QgsRasterDataProvider *provider, int band ) const +QString QgsRasterBandComboBox::displayBandName( QgsRasterDataProvider *provider, int band ) { if ( !provider ) return QString(); diff --git a/src/gui/raster/qgsrasterbandcombobox.h b/src/gui/raster/qgsrasterbandcombobox.h index e5f5a26b8706..b6491a2a9bae 100644 --- a/src/gui/raster/qgsrasterbandcombobox.h +++ b/src/gui/raster/qgsrasterbandcombobox.h @@ -69,6 +69,11 @@ class GUI_EXPORT QgsRasterBandComboBox : public QComboBox */ void setShowNotSetOption( bool show, const QString &string = QString() ); + /** + * Returns a user-friendly band name for the specified \a band. + */ + static QString displayBandName( QgsRasterDataProvider *provider, int band ); + public slots: /** @@ -99,7 +104,6 @@ class GUI_EXPORT QgsRasterBandComboBox : public QComboBox QString mNotSetString; int mPrevBand = -1; - QString displayBandName( QgsRasterDataProvider *provider, int band ) const; }; diff --git a/src/gui/raster/qgsrastercontourrendererwidget.cpp b/src/gui/raster/qgsrastercontourrendererwidget.cpp new file mode 100644 index 000000000000..07236e6ebbd7 --- /dev/null +++ b/src/gui/raster/qgsrastercontourrendererwidget.cpp @@ -0,0 +1,83 @@ +/*************************************************************************** + qgsrastercontourrendererwidget.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsrastercontourrendererwidget.h" + +#include "qgsrastercontourrenderer.h" +#include "qgsrasterlayer.h" + + +QgsRasterContourRendererWidget::QgsRasterContourRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent ) + : QgsRasterRendererWidget( layer, extent ) +{ + setupUi( this ); + + mContourSymbolButton->setSymbolType( QgsSymbol::Line ); + mIndexContourSymbolButton->setSymbolType( QgsSymbol::Line ); + + mInputBandComboBox->setLayer( mRasterLayer ); + + if ( !mRasterLayer ) + { + return; + } + QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); + if ( !provider ) + { + return; + } + + const QgsRasterContourRenderer *rcr = dynamic_cast( mRasterLayer->renderer() ); + if ( rcr ) + { + mInputBandComboBox->setBand( rcr->inputBand() ); + mContourIntervalSpinBox->setValue( rcr->contourInterval() ); + mIndexContourIntervalSpinBox->setValue( rcr->contourIndexInterval() ); + mDownscaleSpinBox->setValue( rcr->downscale() ); + if ( rcr->contourSymbol() ) + mContourSymbolButton->setSymbol( rcr->contourSymbol()->clone() ); + if ( rcr->contourIndexSymbol() ) + mIndexContourSymbolButton->setSymbol( rcr->contourIndexSymbol()->clone() ); + } + + connect( mInputBandComboBox, &QgsRasterBandComboBox::bandChanged, this, &QgsRasterRendererWidget::widgetChanged ); + connect( mContourIntervalSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsRasterRendererWidget::widgetChanged ); + connect( mIndexContourIntervalSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsRasterRendererWidget::widgetChanged ); + connect( mDownscaleSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsRasterRendererWidget::widgetChanged ); + connect( mContourSymbolButton, &QgsSymbolButton::changed, this, &QgsRasterRendererWidget::widgetChanged ); + connect( mIndexContourSymbolButton, &QgsSymbolButton::changed, this, &QgsRasterRendererWidget::widgetChanged ); +} + +QgsRasterRenderer *QgsRasterContourRendererWidget::renderer() +{ + if ( !mRasterLayer ) + { + return nullptr; + } + QgsRasterDataProvider *provider = mRasterLayer->dataProvider(); + if ( !provider ) + { + return nullptr; + } + + QgsRasterContourRenderer *renderer = new QgsRasterContourRenderer( provider ); + renderer->setInputBand( mInputBandComboBox->currentBand() ); + renderer->setContourInterval( mContourIntervalSpinBox->value() ); + renderer->setContourIndexInterval( mIndexContourIntervalSpinBox->value() ); + renderer->setDownscale( mDownscaleSpinBox->value() ); + renderer->setContourSymbol( mContourSymbolButton->clonedSymbol() ); + renderer->setContourIndexSymbol( mIndexContourSymbolButton->clonedSymbol() ); + return renderer; +} diff --git a/src/gui/raster/qgsrastercontourrendererwidget.h b/src/gui/raster/qgsrastercontourrendererwidget.h new file mode 100644 index 000000000000..2549632e3c96 --- /dev/null +++ b/src/gui/raster/qgsrastercontourrendererwidget.h @@ -0,0 +1,42 @@ +/*************************************************************************** + qgsrastercontourrendererwidget.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSRASTERCONTOURRENDERERWIDGET_H +#define QGSRASTERCONTOURRENDERERWIDGET_H + +#include "qgsrasterrendererwidget.h" +#include "qgis_sip.h" +#include "ui_qgsrastercontourrendererwidget.h" +#include "qgis_gui.h" + +/** + * \ingroup gui + * Configuration widget for QgsRasterContourRenderer + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsRasterContourRendererWidget : public QgsRasterRendererWidget, private Ui::QgsRasterContourRendererWidget +{ + Q_OBJECT + public: + //! Constructs the widget + QgsRasterContourRendererWidget( QgsRasterLayer *layer, const QgsRectangle &extent = QgsRectangle() ); + + //! Widget creation function (mainly for the use by the renderer registry) + static QgsRasterRendererWidget *create( QgsRasterLayer *layer, const QgsRectangle &extent ) SIP_FACTORY { return new QgsRasterContourRendererWidget( layer, extent ); } + + QgsRasterRenderer *renderer() override; +}; + +#endif // QGSRASTERCONTOURRENDERERWIDGET_H diff --git a/src/gui/raster/qgsrasterlayerproperties.cpp b/src/gui/raster/qgsrasterlayerproperties.cpp index f2cf46a72550..e4191b49167e 100644 --- a/src/gui/raster/qgsrasterlayerproperties.cpp +++ b/src/gui/raster/qgsrasterlayerproperties.cpp @@ -38,6 +38,7 @@ #include "qgspalettedrendererwidget.h" #include "qgsproject.h" #include "qgsrasterbandstats.h" +#include "qgsrastercontourrendererwidget.h" #include "qgsrasterdataprovider.h" #include "qgsrasterhistogramwidget.h" #include "qgsrasteridentifyresult.h" @@ -58,8 +59,10 @@ #include "qgsmaplayerlegend.h" #include "qgsfileutils.h" #include "qgswebview.h" +#include "qgsvectorlayer.h" #include "qgsrasterlayertemporalpropertieswidget.h" +#include "qgsprojecttimesettings.h" #include #include @@ -115,6 +118,13 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv initOptionsBase( false ); connect( buttonBox, &QDialogButtonBox::helpRequested, this, &QgsRasterLayerProperties::showHelp ); + connect( mSetEndAsStartStaticButton, &QPushButton::clicked, this, &QgsRasterLayerProperties::setEndAsStartStaticButton_clicked ); + connect( mProjectTemporalRange, &QRadioButton::toggled, this, &QgsRasterLayerProperties::passProjectTemporalRange_toggled ); + connect( mStaticTemporalRange, &QRadioButton::toggled, this, &QgsRasterLayerProperties::staticTemporalRange_toggled ); + + connect( mStaticTemporalRange, &QRadioButton::toggled, mStaticWmstFrame, &QWidget::setEnabled ); + connect( mReferenceTime, &QCheckBox::toggled, mWmstReferenceTimeFrame, &QWidget::setEnabled ); + mBtnStyle = new QPushButton( tr( "Style" ) ); QMenu *menuStyle = new QMenu( this ); menuStyle->addAction( tr( "Load Style…" ), this, &QgsRasterLayerProperties::loadStyle_clicked ); @@ -256,7 +266,7 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv // We can calculate histogram for all data sources but estimated only if // size is unknown - could also be enabled if well supported (estimated histogram - // and and let user know that it is estimated) + // and let user know that it is estimated) if ( !provider || !( provider->capabilities() & QgsRasterDataProvider::Size ) ) { // disable Histogram tab completely @@ -266,16 +276,62 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv QVBoxLayout *layout = new QVBoxLayout( metadataFrame ); layout->setMargin( 0 ); mMetadataWidget = new QgsMetadataWidget( this, mRasterLayer ); - mMetadataWidget->layout()->setContentsMargins( -1, 0, -1, 0 ); + mMetadataWidget->layout()->setContentsMargins( 0, 0, 0, 0 ); mMetadataWidget->setMapCanvas( mMapCanvas ); layout->addWidget( mMetadataWidget ); metadataFrame->setLayout( layout ); QVBoxLayout *temporalLayout = new QVBoxLayout( temporalFrame ); - temporalLayout->setContentsMargins( -1, 0, -1, 0 ); + temporalLayout->setContentsMargins( 0, 0, 0, 0 ); mTemporalWidget = new QgsRasterLayerTemporalPropertiesWidget( this, mRasterLayer ); temporalLayout->addWidget( mTemporalWidget ); + setSourceStaticTimeState(); + mWmstGroup->setVisible( mRasterLayer->providerType() == QLatin1String( "wms" ) && mRasterLayer->dataProvider() && mRasterLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + + // This group is used to define the temporal capabilities of the PG raster layer + if ( mRasterLayer->dataProvider() && mRasterLayer->providerType() == QLatin1String( "postgresraster" ) ) + { + mPostgresRasterTemporalGroup->setEnabled( true ); + mPostgresRasterTemporalGroup->setVisible( true ); + mPostgresRasterTemporalGroup->setChecked( false ); + const QgsFields fields { mRasterLayer->dataProvider()->fields() }; + mPostgresRasterTemporalFieldComboBox->setFields( fields ); + mPostgresRasterTemporalFieldComboBox->setFilters( QgsFieldProxyModel::Filter::Date | + QgsFieldProxyModel::Filter::DateTime | + QgsFieldProxyModel::Filter::String ); + mPostgresRasterTemporalFieldComboBox->setAllowEmptyFieldName( true ); + connect( mPostgresRasterTemporalFieldComboBox, &QgsFieldComboBox::fieldChanged, this, [ = ]( const QString & fieldName ) + { + mPostgresRasterDefaultTime->setEnabled( ! fieldName.isEmpty() ); + } ); + mPostgresRasterDefaultTime->setAllowNull( true ); + mPostgresRasterDefaultTime->setEmpty(); + if ( mRasterLayer->dataProvider()->uri().hasParam( QStringLiteral( "temporalFieldIndex" ) ) ) + { + bool ok; + const int fieldIdx { mRasterLayer->dataProvider()->uri().param( QStringLiteral( "temporalFieldIndex" ) ).toInt( &ok ) }; + if ( ok && fields.exists( fieldIdx ) ) + { + mPostgresRasterTemporalGroup->setChecked( true ); + mPostgresRasterTemporalFieldComboBox->setField( fields.field( fieldIdx ).name() ); + if ( mRasterLayer->dataProvider()->uri().hasParam( QStringLiteral( "temporalDefaultTime" ) ) ) + { + const QDateTime defaultDateTime { QDateTime::fromString( mRasterLayer->dataProvider()->uri().param( QStringLiteral( "temporalDefaultTime" ) ), Qt::DateFormat::ISODate ) }; + if ( defaultDateTime.isValid() ) + { + mPostgresRasterDefaultTime->setDateTime( defaultDateTime ); + } + } + } + } + } + else + { + mPostgresRasterTemporalGroup->setEnabled( false ); + mPostgresRasterTemporalGroup->setVisible( false ); + } + QgsDebugMsg( "Setting crs to " + mRasterLayer->crs().toWkt( QgsCoordinateReferenceSystem::WKT2_2018 ) ); QgsDebugMsg( "Setting crs to " + mRasterLayer->crs().userFriendlyIdentifier() ); mCrsSelector->setCrs( mRasterLayer->crs() ); @@ -398,6 +454,7 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandpseudocolor" ), QgsSingleBandPseudoColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); //fill available renderers into combo box QgsRasterRendererRegistryEntry entry; @@ -489,6 +546,39 @@ QgsRasterLayerProperties::QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanv title += QStringLiteral( " (%1)" ).arg( mRasterLayer->styleManager()->currentStyle() ); restoreOptionsBaseUi( title ); optionsStackedWidget_CurrentChanged( mOptionsStackedWidget->currentIndex() ); + + //Add help page references + mOptsPage_Information->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#information-properties" ) ); + mOptsPage_Source->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#source-properties" ) ); + mOptsPage_Style->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#symbology-properties" ) ); + mOptsPage_Transparency->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#transparency-properties" ) ); + + if ( mOptsPage_Histogram ) + mOptsPage_Histogram->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#histogram-properties" ) ); + + mOptsPage_Rendering->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#rendering-properties" ) ); + + if ( mOptsPage_Pyramids ) + mOptsPage_Pyramids->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#pyramids-properties" ) ); + + mOptsPage_Metadata->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#metadata-properties" ) ); + mOptsPage_Legend->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#legend-properties" ) ); + mOptsPage_Server->setProperty( "helpPage", QStringLiteral( "working_with_raster/raster_properties.html#server-properties" ) ); +} + +void QgsRasterLayerProperties::setCurrentPage( const QString &page ) +{ + //find the page with a matching widget name + for ( int idx = 0; idx < mOptionsStackedWidget->count(); ++idx ) + { + QWidget *currentPage = mOptionsStackedWidget->widget( idx ); + if ( currentPage->objectName() == page ) + { + //found the page, set it as current + mOptionsStackedWidget->setCurrentIndex( idx ); + return; + } + } } void QgsRasterLayerProperties::setupTransparencyTable( int nBands ) @@ -1035,6 +1125,43 @@ void QgsRasterLayerProperties::apply() //set the blend mode for the layer mRasterLayer->setBlendMode( mBlendModeComboBox->blendMode() ); + updateSourceStaticTime(); + + // Update temporal field + if ( mRasterLayer->dataProvider() ) + { + QgsDataSourceUri uri { mRasterLayer->dataProvider()->uri() }; + if ( mPostgresRasterTemporalGroup->isEnabled() && + mPostgresRasterTemporalGroup->isChecked() && + ! mPostgresRasterTemporalFieldComboBox->currentField().isEmpty() ) + { + const QString originaUri { uri.uri() }; + const int fieldIdx { mRasterLayer->dataProvider()->fields().lookupField( mPostgresRasterTemporalFieldComboBox->currentField() ) }; + uri.removeParam( QStringLiteral( "temporalFieldIndex" ) ); + uri.removeParam( QStringLiteral( "temporalDefaultTime" ) ); + if ( fieldIdx >= 0 ) + { + uri.setParam( QStringLiteral( "temporalFieldIndex" ), QString::number( fieldIdx ) ); + if ( mPostgresRasterDefaultTime->dateTime().isValid() ) + { + QDateTime defaultDateTime { mPostgresRasterDefaultTime->dateTime() }; + const QTime defaultTime { defaultDateTime.time() }; + // Set secs to 0 + defaultDateTime.setTime( { defaultTime.hour(), defaultTime.minute(), 0 } ); + uri.setParam( QStringLiteral( "temporalDefaultTime" ), defaultDateTime.toString( Qt::DateFormat::ISODate ) ); + } + if ( uri.uri( ) != originaUri ) + mRasterLayer->setDataSource( uri.uri(), mRasterLayer->name(), mRasterLayer->providerType(), QgsDataProvider::ProviderOptions() ); + } + } + else if ( uri.hasParam( QStringLiteral( "temporalFieldIndex" ) ) ) + { + uri.removeParam( QStringLiteral( "temporalFieldIndex" ) ); + uri.removeParam( QStringLiteral( "temporalDefaultTime" ) ); + mRasterLayer->setDataSource( uri.uri(), mRasterLayer->name(), mRasterLayer->providerType(), QgsDataProvider::ProviderOptions() ); + } + } + // Update temporal properties mTemporalWidget->saveTemporalProperties(); @@ -1113,6 +1240,167 @@ void QgsRasterLayerProperties::apply() QgsProject::instance()->setDirty( true ); }//apply +void QgsRasterLayerProperties::updateSourceStaticTime() +{ + if ( mWmstGroup->isEnabled() && + mRasterLayer->dataProvider() && + mRasterLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ) + { + QgsDataSourceUri uri { mRasterLayer->dataProvider()->uri() }; + + if ( mStaticTemporalRange->isChecked() ) + { + QString time = mStartStaticDateTimeEdit->dateTime().toString( Qt::ISODateWithMs ) + '/' + + mEndStaticDateTimeEdit->dateTime().toString( Qt::ISODateWithMs ); + uri.removeParam( QStringLiteral( "time" ) ); + uri.setParam( QStringLiteral( "time" ), time ); + + uri.removeParam( QStringLiteral( "temporalSource" ) ); + uri.setParam( QStringLiteral( "temporalSource" ), QLatin1String( "provider" ) ); + } + + if ( mProjectTemporalRange->isChecked() ) + { + QgsDateTimeRange range; + + if ( QgsProject::instance()->timeSettings() ) + range = QgsProject::instance()->timeSettings()->temporalRange(); + if ( range.begin().isValid() && range.end().isValid() ) + { + QString time = range.begin().toString( Qt::ISODateWithMs ) + "/" + + range.end().toString( Qt::ISODateWithMs ); + + uri.removeParam( QStringLiteral( "time" ) ); + uri.setParam( QStringLiteral( "time" ), time ); + + uri.removeParam( QStringLiteral( "temporalSource" ) ); + uri.setParam( QStringLiteral( "temporalSource" ), QLatin1String( "project" ) ); + } + } + + if ( mReferenceTime->isChecked() ) + { + QString reference_time = mReferenceDateTimeEdit->dateTime().toString( Qt::ISODateWithMs ); + uri.removeParam( QStringLiteral( "reference_time" ) ); + uri.setParam( QStringLiteral( "reference_time" ), reference_time ); + } + const QLatin1String enableTime = mDisableTime->isChecked() ? QLatin1String( "no" ) : QLatin1String( "yes" ); + + uri.removeParam( QStringLiteral( "enableTime" ) ); + uri.setParam( QStringLiteral( "enableTime" ), enableTime ); + + mRasterLayer->setDataSource( uri.uri(), mRasterLayer->name(), mRasterLayer->providerType(), QgsDataProvider::ProviderOptions() ); + + mRasterLayer->temporalProperties()->setIntervalHandlingMethod( static_cast< QgsRasterDataProviderTemporalCapabilities::IntervalHandlingMethod >( + mFetchModeComboBox->currentData().toInt() ) ); + } +} + +void QgsRasterLayerProperties::setSourceStaticTimeState() +{ + if ( mRasterLayer->dataProvider() && mRasterLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ) + { + const QgsDateTimeRange availableProviderRange = mRasterLayer->dataProvider()->temporalCapabilities()->availableTemporalRange(); + const QgsDateTimeRange availableReferenceRange = mRasterLayer->dataProvider()->temporalCapabilities()->availableReferenceTemporalRange(); + + QgsDataSourceUri uri { mRasterLayer->dataProvider()->uri() }; + + mStartStaticDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); + mEndStaticDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); + mReferenceDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); + + // setup maximum extents for widgets, based on provider's capabilities + if ( availableProviderRange.begin().isValid() && availableProviderRange.end().isValid() ) + { + mStartStaticDateTimeEdit->setDateTimeRange( availableProviderRange.begin(), + availableProviderRange.end() ); + mStartStaticDateTimeEdit->setDateTime( availableProviderRange.begin() ); + mEndStaticDateTimeEdit->setDateTimeRange( availableProviderRange.begin(), + availableProviderRange.end() ); + mEndStaticDateTimeEdit->setDateTime( availableProviderRange.end() ); + } + if ( availableReferenceRange.begin().isValid() && availableReferenceRange.end().isValid() ) + { + mReferenceDateTimeEdit->setDateTimeRange( availableReferenceRange.begin(), + availableReferenceRange.end() ); + mReferenceDateTimeEdit->setDateTime( availableReferenceRange.begin() ); + } + + const QString time = uri.param( QStringLiteral( "time" ) ); + if ( !time.isEmpty() ) + { + QStringList parts = time.split( '/' ); + mStartStaticDateTimeEdit->setDateTime( QDateTime::fromString( parts.at( 0 ), Qt::ISODateWithMs ) ); + mEndStaticDateTimeEdit->setDateTime( QDateTime::fromString( parts.at( 1 ), Qt::ISODateWithMs ) ); + } + + const QString referenceTimeExtent = uri.param( QStringLiteral( "referenceTimeDimensionExtent" ) ); + + mReferenceTime->setEnabled( !referenceTimeExtent.isEmpty() ); + mReferenceDateTimeEdit->setVisible( !referenceTimeExtent.isEmpty() ); + + QString referenceTimeLabelText = referenceTimeExtent.isEmpty() ? + tr( "There is no reference time in the layer's capabilities." ) : QString(); + mReferenceTimeLabel->setText( referenceTimeLabelText ); + + const QString referenceTime = uri.param( QStringLiteral( "reference_time" ) ); + + if ( !referenceTime.isEmpty() && !referenceTimeExtent.isEmpty() ) + { + if ( referenceTime.contains( '/' ) ) + { + QStringList parts = time.split( '/' ); + mReferenceDateTimeEdit->setDateTime( QDateTime::fromString( parts.at( 0 ), Qt::ISODateWithMs ) ); + } + else + mReferenceDateTimeEdit->setDateTime( QDateTime::fromString( referenceTime, Qt::ISODateWithMs ) ); + } + + mFetchModeComboBox->addItem( tr( "Use Whole Temporal Range" ), QgsRasterDataProviderTemporalCapabilities::MatchUsingWholeRange ); + mFetchModeComboBox->addItem( tr( "Match to Start of Range" ), QgsRasterDataProviderTemporalCapabilities::MatchExactUsingStartOfRange ); + mFetchModeComboBox->addItem( tr( "Match to End of Range" ), QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange ); + mFetchModeComboBox->addItem( tr( "Closest Match to Start of Range" ), QgsRasterDataProviderTemporalCapabilities::FindClosestMatchToStartOfRange ); + mFetchModeComboBox->addItem( tr( "Closest Match to End of Range" ), QgsRasterDataProviderTemporalCapabilities::FindClosestMatchToEndOfRange ); + mFetchModeComboBox->setCurrentIndex( mFetchModeComboBox->findData( mRasterLayer->temporalProperties()->intervalHandlingMethod() ) ); + + const QString temporalSource = uri.param( QStringLiteral( "temporalSource" ) ); + const QString enableTime = uri.param( QStringLiteral( "enableTime" ) ); + + if ( temporalSource == QLatin1String( "provider" ) ) + mStaticTemporalRange->setChecked( !time.isEmpty() ); + else if ( temporalSource == QLatin1String( "project" ) ) + mProjectTemporalRange->setChecked( !time.isEmpty() ); + + mDisableTime->setChecked( enableTime == QLatin1String( "no" ) ); + } +} + +void QgsRasterLayerProperties::staticTemporalRange_toggled( bool checked ) +{ + if ( checked ) + { + mLabel->clear(); + } +} + +void QgsRasterLayerProperties::passProjectTemporalRange_toggled( bool checked ) +{ + if ( checked ) + { + QgsDateTimeRange range; + if ( QgsProject::instance()->timeSettings() ) + range = QgsProject::instance()->timeSettings()->temporalRange(); + + if ( range.begin().isValid() && range.end().isValid() ) + mLabel->setText( tr( "Project temporal range is set from %1 to %2" ).arg( + range.begin().toString( "yyyy-MM-dd HH:mm:ss" ), + range.end().toString( "yyyy-MM-dd HH:mm:ss" ) + ) ); + else + mLabel->setText( tr( "Project temporal range is not valid, can't use it here" ) ); + } +} + void QgsRasterLayerProperties::mLayerOrigNameLineEd_textEdited( const QString &text ) { leDisplayName->setText( mRasterLayer->formatLayerName( text ) ); @@ -1509,28 +1797,8 @@ void QgsRasterLayerProperties::aboutToShowStyleMenu() // this should be unified with QgsVectorLayerProperties::aboutToShowStyleMenu() QMenu *m = qobject_cast( sender() ); - if ( !m ) - return; - - // first get rid of previously added style manager actions (they are dynamic) - bool gotFirstSeparator = false; - QList actions = m->actions(); - for ( int i = 0; i < actions.count(); ++i ) - { - if ( actions[i]->isSeparator() ) - { - if ( gotFirstSeparator ) - { - // remove all actions after second separator (including it) - while ( actions.count() != i ) - delete actions.takeAt( i ); - break; - } - else - gotFirstSeparator = true; - } - } + QgsMapLayerStyleGuiUtils::instance()->removesExtraMenuSeparators( m ); // re-add style manager actions! m->addSeparator(); QgsMapLayerStyleGuiUtils::instance()->addStyleManagerActions( m, mRasterLayer ); @@ -1583,6 +1851,11 @@ void QgsRasterLayerProperties::optionsStackedWidget_CurrentChanged( int index ) } } +void QgsRasterLayerProperties::setEndAsStartStaticButton_clicked() +{ + mEndStaticDateTimeEdit->setDateTime( mStartStaticDateTimeEdit->dateTime() ); +} + void QgsRasterLayerProperties::pbnImportTransparentPixelValues_clicked() { int myLineCounter = 0; @@ -2108,5 +2381,14 @@ void QgsRasterLayerProperties::onCancel() void QgsRasterLayerProperties::showHelp() { - QgsHelp::openHelp( QStringLiteral( "working_with_raster/raster_properties.html" ) ); + const QVariant helpPage = mOptionsStackedWidget->currentWidget()->property( "helpPage" ); + + if ( helpPage.isValid() ) + { + QgsHelp::openHelp( helpPage.toString() ); + } + else + { + QgsHelp::openHelp( QStringLiteral( "working_with_raster/raster_properties.html" ) ); + } } diff --git a/src/gui/raster/qgsrasterlayerproperties.h b/src/gui/raster/qgsrasterlayerproperties.h index 3f2a954f9300..411f8dd38fc5 100644 --- a/src/gui/raster/qgsrasterlayerproperties.h +++ b/src/gui/raster/qgsrasterlayerproperties.h @@ -75,6 +75,13 @@ class GUI_EXPORT QgsRasterLayerProperties : public QgsOptionsDialogBase, private */ QgsRasterLayerProperties( QgsMapLayer *lyr, QgsMapCanvas *canvas, QWidget *parent = nullptr, Qt::WindowFlags = QgsGuiUtils::ModalDialogFlags ); + /** + * Sets the dialog \a page (by object name) to show. + * + * \since QGIS 3.14 + */ + void setCurrentPage( const QString &page ); + protected slots: //! \brief auto slot executed when the active page in the main widget stack is changed void optionsStackedWidget_CurrentChanged( int index ) override SIP_SKIP ; @@ -104,6 +111,15 @@ class GUI_EXPORT QgsRasterLayerProperties : public QgsOptionsDialogBase, private //! \brief slot executed when user presses "Remove Selected Row" button on the transparency page void pbnRemoveSelectedRow_clicked(); + //! \brief slot executed when user "Set end same as start" button on time options in source page. + void setEndAsStartStaticButton_clicked(); + + //! \brief slot executed when user "Pass provider temporal range" radio button on time options in source page. + void passProjectTemporalRange_toggled( bool checked ); + + //! \brief slot executed when user "Static time range" radio button on time options in source page. + void staticTemporalRange_toggled( bool checked ); + /** * \brief slot executed when the single band radio button is pressed. * \brief slot executed when the reset null value to file default icon is selected @@ -218,6 +234,18 @@ class GUI_EXPORT QgsRasterLayerProperties : public QgsOptionsDialogBase, private */ void updateTemporalProperties(); + /** + * Updates the layers date source URI with the new time. + * + */ + void updateSourceStaticTime(); + + /** + * Initialiaze the layers static time inputs state. + * + */ + void setSourceStaticTimeState(); + void setupTransparencyTable( int nBands ); //! \brief Clear the current transparency table and populate the table with the correct types for current drawing mode and data type diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp index f60250c59582..98c3c1ba4d0c 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.cpp @@ -27,265 +27,52 @@ QgsRasterLayerTemporalPropertiesWidget::QgsRasterLayerTemporalPropertiesWidget( : QWidget( parent ) , mLayer( layer ) { + Q_ASSERT( mLayer ); setupUi( this ); - connect( mSetEndAsStartNormalButton, &QPushButton::clicked, this, &QgsRasterLayerTemporalPropertiesWidget::setEndAsStartNormalButton_clicked ); - connect( mSetEndAsStartReferenceButton, &QPushButton::clicked, this, &QgsRasterLayerTemporalPropertiesWidget::setEndAsStartReferenceButton_clicked ); - connect( mResetDatesButton, &QPushButton::clicked, this, &QgsRasterLayerTemporalPropertiesWidget::resetDatesButton_clicked ); - connect( mLayerRadioButton, &QRadioButton::toggled, this, &QgsRasterLayerTemporalPropertiesWidget::layerRadioButton_toggled ); - connect( mProjectRadioButton, &QRadioButton::toggled, this, &QgsRasterLayerTemporalPropertiesWidget::projectRadioButton_toggled ); - connect( mReferenceCheckBox, &QCheckBox::clicked, this, &QgsRasterLayerTemporalPropertiesWidget::referenceCheckBox_clicked ); + connect( mModeFixedRangeRadio, &QRadioButton::toggled, mFixedTimeRangeFrame, &QWidget::setEnabled ); init(); } void QgsRasterLayerTemporalPropertiesWidget::init() { - setInputWidgetState( TemporalDimension::BiTemporal, false ); - setDateTimeInputsLimit(); - setDateTimeInputsLocale(); + mStartTemporalDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); - mFetchModeComboBox->addItem( tr( "Use Whole Temporal Range" ), QgsRasterDataProviderTemporalCapabilities::MatchUsingWholeRange ); - mFetchModeComboBox->addItem( tr( "Match to Start of Range" ), QgsRasterDataProviderTemporalCapabilities::MatchExactUsingStartOfRange ); - mFetchModeComboBox->addItem( tr( "Match to End of Range" ), QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange ); + mEndTemporalDateTimeEdit->setDisplayFormat( "yyyy-MM-dd HH:mm:ss" ); - if ( QgsRasterLayerTemporalProperties *temporalProperties = mLayer->temporalProperties() ) - mFetchModeComboBox->setCurrentIndex( mFetchModeComboBox->findData( temporalProperties->intervalHandlingMethod() ) ); - else - mFetchModeComboBox->setCurrentIndex( mFetchModeComboBox->findData( QgsRasterDataProviderTemporalCapabilities::MatchExactUsingStartOfRange ) ); - - if ( mLayer->temporalProperties()->temporalSource() == QgsMapLayerTemporalProperties::TemporalSource::Project ) - mProjectRadioButton->setChecked( true ); - - updateRangeLabel( mLabel ); - - mSetEndAsStartNormalButton->setToolTip( tr( "Set the end datetime same as the start datetime" ) ); - mSetEndAsStartReferenceButton->setToolTip( tr( "Set the end datetime same as the start datetime" ) ); - mResetDatesButton->setToolTip( tr( "Reset the start and end datetime inputs" ) ); - mDisableTime->setToolTip( "Use only the date in the datetime inputs to update the temporal range" ); -} - -void QgsRasterLayerTemporalPropertiesWidget::setInputWidgetState( TemporalDimension dimension, bool enabled ) -{ - if ( dimension == TemporalDimension::NormalTemporal ) - { - mStartTemporalDateTimeEdit->setEnabled( enabled ); - mEndTemporalDateTimeEdit->setEnabled( enabled ); - mSetEndAsStartNormalButton->setEnabled( enabled ); - } - else if ( dimension == TemporalDimension::BiTemporal ) - { - mStartReferenceDateTimeEdit->setEnabled( enabled ); - mEndReferenceDateTimeEdit->setEnabled( enabled ); - mSetEndAsStartReferenceButton->setEnabled( enabled ); - } - -} - -void QgsRasterLayerTemporalPropertiesWidget::setDateTimeInputsLocale() -{ - QLocale locale; - mStartTemporalDateTimeEdit->setDisplayFormat( - locale.dateTimeFormat( QLocale::ShortFormat ) ); - mEndTemporalDateTimeEdit->setDisplayFormat( - locale.dateTimeFormat( QLocale::ShortFormat ) ); - mStartReferenceDateTimeEdit->setDisplayFormat( - locale.dateTimeFormat( QLocale::ShortFormat ) ); - mEndReferenceDateTimeEdit->setDisplayFormat( - locale.dateTimeFormat( QLocale::ShortFormat ) ); -} - -void QgsRasterLayerTemporalPropertiesWidget::setDateTimeInputsLimit() -{ - QgsRasterLayer *layer = qobject_cast( mLayer ); - if ( layer && layer->temporalProperties() ) - { - QgsDateTimeRange fixedRange = layer->temporalProperties()->fixedTemporalRange(); - QgsDateTimeRange fixedReferenceRange = layer->temporalProperties()->fixedReferenceTemporalRange(); - - QgsDateTimeRange range = layer->temporalProperties()->temporalRange(); - QgsDateTimeRange referenceRange = layer->temporalProperties()->referenceTemporalRange(); - - // Set initial date time input values to the layers temporal range only if the - // ranges are valid - - mStartTemporalDateTimeEdit->setDateTimeRange( fixedRange.begin(), fixedRange.end() ); - mEndTemporalDateTimeEdit->setDateTimeRange( fixedRange.begin(), fixedRange.end() ); - mStartReferenceDateTimeEdit->setDateTimeRange( fixedReferenceRange.begin(), fixedReferenceRange.end() ); - mEndReferenceDateTimeEdit->setDateTimeRange( fixedReferenceRange.begin(), fixedReferenceRange.end() ); - - if ( range.begin().isValid() && range.end().isValid() ) - { - mStartTemporalDateTimeEdit->setDateTime( range.begin() ); - mEndTemporalDateTimeEdit->setDateTime( range.end() ); - } - else - { - if ( fixedRange.begin().isValid() && fixedRange.end().isValid() ) - { - mStartTemporalDateTimeEdit->setDateTime( fixedRange.begin() ); - mEndTemporalDateTimeEdit->setDateTime( fixedRange.end() ); - } - } - - if ( referenceRange.begin().isValid() && referenceRange.end().isValid() ) - { - mStartReferenceDateTimeEdit->setDateTime( referenceRange.begin() ); - mEndReferenceDateTimeEdit->setDateTime( referenceRange.end() ); - } - else - { - if ( fixedReferenceRange.begin().isValid() && fixedReferenceRange.end().isValid() ) - { - mStartReferenceDateTimeEdit->setDateTime( fixedReferenceRange.begin() ); - mEndReferenceDateTimeEdit->setDateTime( fixedReferenceRange.end() ); - } - } - } -} - -void QgsRasterLayerTemporalPropertiesWidget::updateRangeLabel( QLabel *label ) -{ - QLocale locale; - if ( mLayer->temporalProperties()->temporalSource() == - QgsMapLayerTemporalProperties::TemporalSource::Layer ) - { - if ( mLayer->type() == QgsMapLayerType::RasterLayer ) - { - QgsRasterLayer *rasterLayer = qobject_cast ( mLayer ); - QgsDateTimeRange range = rasterLayer->temporalProperties()->temporalRange(); - - if ( range.begin().isValid() && range.end().isValid() ) - label->setText( tr( "Current layer range: %1 to %2" ).arg( - range.begin().toString( locale.dateTimeFormat() ), - range.end().toString( locale.dateTimeFormat() ) ) ); - else - label->setText( tr( "Layer temporal range is not set" ) ); - } - } - else if ( mLayer->temporalProperties()->temporalSource() == - QgsMapLayerTemporalProperties::TemporalSource::Project ) - { - QgsDateTimeRange range = QgsProject::instance()->timeSettings()->temporalRange(); - - if ( range.begin().isValid() && range.end().isValid() ) - label->setText( tr( "Project time range is from %1 to %2 " ).arg( - range.begin().toString( locale.dateTimeFormat() ), - range.end().toString( locale.dateTimeFormat() ) ) ); - else - label->setText( tr( "Temporal range from the Project time settings is invalid, change it before using it here." ) ); - } -} - -void QgsRasterLayerTemporalPropertiesWidget::setEndAsStartNormalButton_clicked() -{ - mEndTemporalDateTimeEdit->setDateTime( mStartTemporalDateTimeEdit->dateTime() ); - updateRangeLabel( mLabel ); -} - -void QgsRasterLayerTemporalPropertiesWidget::setEndAsStartReferenceButton_clicked() -{ - mEndReferenceDateTimeEdit->setDateTime( mStartReferenceDateTimeEdit->dateTime() ); -} - -void QgsRasterLayerTemporalPropertiesWidget::layerRadioButton_toggled( bool checked ) -{ - if ( checked ) + mTemporalGroupBox->setChecked( mLayer->temporalProperties()->isActive() ); + switch ( mLayer->temporalProperties()->mode() ) { - mLayer->temporalProperties()->setTemporalSource( - QgsMapLayerTemporalProperties::TemporalSource::Layer ); - updateRangeLabel( mLabel ); + case QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider: + mModeAutomaticRadio->setChecked( true ); + break; + case QgsRasterLayerTemporalProperties::ModeFixedTemporalRange: + mModeFixedRangeRadio->setChecked( true ); + break; } -} -void QgsRasterLayerTemporalPropertiesWidget::projectRadioButton_toggled( bool checked ) -{ - if ( checked ) - { - mLayer->temporalProperties()->setTemporalSource( - QgsMapLayerTemporalProperties::TemporalSource::Project ); - updateRangeLabel( mLabel ); - } -} + mStartTemporalDateTimeEdit->setDateTime( mLayer->temporalProperties()->fixedTemporalRange().begin() ); + mEndTemporalDateTimeEdit->setDateTime( mLayer->temporalProperties()->fixedTemporalRange().end() ); -void QgsRasterLayerTemporalPropertiesWidget::resetDatesButton_clicked() -{ - QgsRasterLayer *layer = qobject_cast( mLayer ); - QgsDateTimeRange layerFixedRange; - - if ( layer && layer->temporalProperties() ) - layerFixedRange = layer->temporalProperties()->fixedTemporalRange(); - if ( layerFixedRange.begin().isValid() && layerFixedRange.end().isValid() ) + if ( !mLayer->dataProvider() || !mLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ) { - mStartTemporalDateTimeEdit->setDateTime( layerFixedRange.begin() ); - mEndTemporalDateTimeEdit->setDateTime( layerFixedRange.end() ); + mModeAutomaticRadio->setEnabled( false ); + mModeAutomaticRadio->setChecked( false ); + mModeFixedRangeRadio->setChecked( true ); } - else - mLabel->setText( tr( "Cannot reset dates - no temporal metadata " - "is available for this layer" ) ); -} -void QgsRasterLayerTemporalPropertiesWidget::referenceCheckBox_clicked() -{ - if ( mReferenceCheckBox->isChecked() ) - setInputWidgetState( TemporalDimension::BiTemporal, true ); - else - setInputWidgetState( TemporalDimension::BiTemporal, false ); } void QgsRasterLayerTemporalPropertiesWidget::saveTemporalProperties() { - if ( mLayerRadioButton->isChecked() ) - { - if ( mLayer->type() == QgsMapLayerType::RasterLayer ) - { - QgsDateTimeRange normalRange = QgsDateTimeRange( mStartTemporalDateTimeEdit->dateTime(), - mEndTemporalDateTimeEdit->dateTime() ); - QgsRasterLayer *rasterLayer = qobject_cast( mLayer ); - - if ( rasterLayer && rasterLayer->temporalProperties() ) - { - rasterLayer->temporalProperties()->setTemporalRange( normalRange ); - rasterLayer->temporalProperties()->setTemporalSource( QgsMapLayerTemporalProperties::TemporalSource::Layer ); + mLayer->temporalProperties()->setIsActive( mTemporalGroupBox->isChecked() ); - rasterLayer->temporalProperties()->setIntervalHandlingMethod( static_cast< QgsRasterDataProviderTemporalCapabilities::IntervalHandlingMethod >( mFetchModeComboBox->currentData().toInt() ) ); - - if ( mReferenceCheckBox->isChecked() ) - { - QgsDateTimeRange referenceRange = QgsDateTimeRange( mStartReferenceDateTimeEdit->dateTime(), - mEndReferenceDateTimeEdit->dateTime() ); - rasterLayer->temporalProperties()->setReferenceTemporalRange( referenceRange ); - } - } - } - } + QgsDateTimeRange normalRange = QgsDateTimeRange( mStartTemporalDateTimeEdit->dateTime(), + mEndTemporalDateTimeEdit->dateTime() ); - if ( mProjectRadioButton->isChecked() ) - { - if ( mLayer->type() == QgsMapLayerType::RasterLayer ) - { - QgsRasterLayer *rasterLayer = qobject_cast ( mLayer ); - QgsDateTimeRange projectRange; - - if ( QgsProject::instance()->timeSettings() ) - projectRange = QgsProject::instance()->timeSettings()->temporalRange(); - - if ( rasterLayer && rasterLayer->temporalProperties() ) - { - if ( !projectRange.begin().isValid() || !projectRange.end().isValid() ) - return; - - rasterLayer->temporalProperties()->setTemporalRange( projectRange ); - rasterLayer->temporalProperties()->setTemporalSource( QgsMapLayerTemporalProperties::TemporalSource::Project ); - - rasterLayer->temporalProperties()->setIntervalHandlingMethod( static_cast< QgsRasterDataProviderTemporalCapabilities::IntervalHandlingMethod >( mFetchModeComboBox->currentData().toInt() ) ); - - if ( mReferenceCheckBox->isChecked() ) - { - QgsDateTimeRange referenceRange = QgsDateTimeRange( mStartReferenceDateTimeEdit->dateTime(), - mEndReferenceDateTimeEdit->dateTime() ); - rasterLayer->temporalProperties()->setReferenceTemporalRange( referenceRange ); - } - } - } - } + if ( mModeAutomaticRadio->isChecked() ) + mLayer->temporalProperties()->setMode( QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider ); + else if ( mModeFixedRangeRadio->isChecked() ) + mLayer->temporalProperties()->setMode( QgsRasterLayerTemporalProperties::ModeFixedTemporalRange ); + mLayer->temporalProperties()->setFixedTemporalRange( normalRange ); } diff --git a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h index ccab3a0d6cb2..b4ff14d90a60 100644 --- a/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h +++ b/src/gui/raster/qgsrasterlayertemporalpropertieswidget.h @@ -58,81 +58,5 @@ class GUI_EXPORT QgsRasterLayerTemporalPropertiesWidget : public QWidget, privat */ QgsRasterLayer *mLayer = nullptr; - /** - * Mode used to determine if temporal properties dimensional status. - */ - enum TemporalDimension - { - NormalTemporal, //! When temporal properties have single temporal dimension. - - /** - * When temporal properties have bi-temporal dimension, - * eg. have normal time and reference time or reference time only. - */ - BiTemporal - }; - - private slots: - - /** - * Sets the input end datetime the same as start datetime input. - **/ - void setEndAsStartNormalButton_clicked(); - - /** - * Sets the reference input end datetime the same as start datetime input, from - * advance options. - */ - void setEndAsStartReferenceButton_clicked(); - - /** - * Handles actions to follow when layer radio button is toggled. - **/ - void layerRadioButton_toggled( bool checked ); - - /** - * Updates the ui states to show current project temporal range, which is - * intended to be assigned to the layer - **/ - void projectRadioButton_toggled( bool checked ); - - /** - * Resets the datetimes inputs to the layer's fixed temporal range. - **/ - void resetDatesButton_clicked(); - - /** - * Enabled inputs in reference datetimes group. - **/ - void referenceCheckBox_clicked(); - - /** - * Sets the input widgets enable state in this temporal widget. - * - * \param dimension determine to either enable normal time or reference time. - * \param enabled new enable status - */ - void setInputWidgetState( TemporalDimension dimension, bool enabled ); - - /** - * Updates the range label with current set datetime range. - * - **/ - void updateRangeLabel( QLabel *label ); - - /** - * Sets the temporal date time inputs with layer's lower and upper - * temporal range limits. - * - **/ - void setDateTimeInputsLimit(); - - /** - * Sets the temporal date time inputs with the default - * locale from the system. - * - **/ - void setDateTimeInputsLocale(); - }; #endif // QGSRASTERLAYERTEMPORALPROPERTIESWIDGET_H diff --git a/src/gui/raster/qgsrastertransparencywidget.cpp b/src/gui/raster/qgsrastertransparencywidget.cpp index d75f3448229b..95e50cdc7167 100644 --- a/src/gui/raster/qgsrastertransparencywidget.cpp +++ b/src/gui/raster/qgsrastertransparencywidget.cpp @@ -120,11 +120,11 @@ void QgsRasterTransparencyWidget::syncToLayer() QgsDebugMsg( QStringLiteral( "noDataRangeList.size = %1" ).arg( noDataRangeList.size() ) ); if ( !noDataRangeList.isEmpty() ) { - leNoDataValue->insert( QgsRasterBlock::printValue( noDataRangeList.value( 0 ).min() ) ); + leNoDataValue->setText( QgsRasterBlock::printValue( noDataRangeList.value( 0 ).min() ) ); } else { - leNoDataValue->insert( QString() ); + leNoDataValue->setText( QString() ); } populateTransparencyTable( mRasterLayer->renderer() ); diff --git a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp index 9356ab6ff033..af75f1a83fec 100644 --- a/src/gui/raster/qgsrendererrasterpropertieswidget.cpp +++ b/src/gui/raster/qgsrendererrasterpropertieswidget.cpp @@ -18,6 +18,7 @@ #include "qgsmapcanvas.h" #include "qgsbrightnesscontrastfilter.h" #include "qgshuesaturationfilter.h" +#include "qgsrastercontourrendererwidget.h" #include "qgsrasterlayer.h" #include "qgsrasterrendererwidget.h" #include "qgsrasterrendererregistry.h" @@ -47,6 +48,7 @@ static void _initRendererWidgetFunctions() QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandpseudocolor" ), QgsSingleBandPseudoColorRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "singlebandgray" ), QgsSingleBandGrayRendererWidget::create ); QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "hillshade" ), QgsHillshadeRendererWidget::create ); + QgsApplication::rasterRendererRegistry()->insertWidgetFunction( QStringLiteral( "contour" ), QgsRasterContourRendererWidget::create ); sInitialized = true; } @@ -338,7 +340,7 @@ void QgsRendererRasterPropertiesWidget::setRendererWidget( const QString &render { if ( rendererEntry.widgetCreateFunction ) // Single band color data renderer e.g. has no widget { - QgsDebugMsg( QStringLiteral( "renderer has widgetCreateFunction" ) ); + QgsDebugMsgLevel( QStringLiteral( "renderer has widgetCreateFunction" ), 3 ); // Current canvas extent (used to calc min/max) in layer CRS QgsRectangle myExtent = mMapCanvas->mapSettings().outputExtentToLayerExtent( mRasterLayer, mMapCanvas->extent() ); if ( oldWidget ) diff --git a/src/gui/symbology/qgssymbollayerwidget.cpp b/src/gui/symbology/qgssymbollayerwidget.cpp index e92a4e833542..b655834de53c 100644 --- a/src/gui/symbology/qgssymbollayerwidget.cpp +++ b/src/gui/symbology/qgssymbollayerwidget.cpp @@ -30,6 +30,7 @@ #include "qgssymbollayerutils.h" #include "qgscolorramp.h" #include "qgscolorrampbutton.h" +#include "qgsfontutils.h" #include "qgsgradientcolorrampdialog.h" #include "qgsproperty.h" #include "qgsstyle.h" //for symbol selector dialog @@ -2336,7 +2337,7 @@ void QgsSvgMarkerSymbolLayerWidget::populateIcons( const QModelIndex &idx ) QString path = idx.data( Qt::UserRole + 1 ).toString(); QAbstractItemModel *oldModel = viewImages->model(); - QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( viewImages, path ); + QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( viewImages, path, mIconSize ); viewImages->setModel( m ); delete oldModel; @@ -2708,6 +2709,14 @@ QgsSVGFillSymbolLayerWidget::QgsSVGFillSymbolLayerWidget( QgsVectorLayer *vl, QW mSvgStrokeWidthUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); mSvgTreeView->setHeaderHidden( true ); + +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) + mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().width( 'X' ) * 4 ) ) ); +#else + mIconSize = std::max( 30, static_cast< int >( std::round( Qgis::UI_SCALE_FACTOR * fontMetrics().horizontalAdvance( 'X' ) * 4 ) ) ); +#endif + mSvgListView->setGridSize( QSize( mIconSize * 1.2, mIconSize * 1.2 ) ); + insertIcons(); mRotationSpinBox->setClearValue( 0.0 ); @@ -2832,7 +2841,7 @@ void QgsSVGFillSymbolLayerWidget::insertIcons() } oldModel = mSvgListView->model(); - QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mSvgListView ); + QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mSvgListView, mIconSize ); mSvgListView->setModel( m ); delete oldModel; } @@ -2842,7 +2851,7 @@ void QgsSVGFillSymbolLayerWidget::populateIcons( const QModelIndex &idx ) QString path = idx.data( Qt::UserRole + 1 ).toString(); QAbstractItemModel *oldModel = mSvgListView->model(); - QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mSvgListView, path ); + QgsSvgSelectorListModel *m = new QgsSvgSelectorListModel( mSvgListView, path, mIconSize ); mSvgListView->setModel( m ); delete oldModel; @@ -3308,6 +3317,7 @@ QgsFontMarkerSymbolLayerWidget::QgsFontMarkerSymbolLayerWidget( QgsVectorLayer * mSizeDDBtn->setSymbol( mAssistantPreviewSymbol ); connect( cboFont, &QFontComboBox::currentFontChanged, this, &QgsFontMarkerSymbolLayerWidget::setFontFamily ); + connect( mFontStyleComboBox, static_cast( &QComboBox::currentIndexChanged ), this, &QgsFontMarkerSymbolLayerWidget::mFontStyleComboBox_currentIndexChanged ); connect( spinSize, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsFontMarkerSymbolLayerWidget::setSize ); connect( cboJoinStyle, static_cast( &QComboBox::currentIndexChanged ), this, &QgsFontMarkerSymbolLayerWidget::penJoinStyleChanged ); connect( btnColor, &QgsColorButton::colorChanged, this, &QgsFontMarkerSymbolLayerWidget::setColor ); @@ -3330,9 +3340,15 @@ void QgsFontMarkerSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) // layer type is correct, we can do the cast mLayer = static_cast( layer ); - QFont layerFont( mLayer->fontFamily() ); + mRefFont.setFamily( mLayer->fontFamily() ); + mRefFont.setStyleName( QgsFontUtils::translateNamedStyle( mLayer->fontStyle() ) ); + + mFontStyleComboBox->blockSignals( true ); + populateFontStyleComboBox(); + mFontStyleComboBox->blockSignals( false ); + // set values - whileBlocking( cboFont )->setCurrentFont( layerFont ); + whileBlocking( cboFont )->setCurrentFont( mRefFont ); whileBlocking( spinSize )->setValue( mLayer->size() ); whileBlocking( btnColor )->setColor( mLayer->color() ); whileBlocking( btnStrokeColor )->setColor( mLayer->strokeColor() ); @@ -3340,14 +3356,14 @@ void QgsFontMarkerSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) whileBlocking( spinAngle )->setValue( mLayer->angle() ); widgetChar->blockSignals( true ); - widgetChar->setFont( layerFont ); + widgetChar->setFont( mRefFont ); if ( mLayer->character().length() == 1 ) { widgetChar->setCharacter( mLayer->character().at( 0 ) ); } widgetChar->blockSignals( false ); whileBlocking( mCharLineEdit )->setText( mLayer->character() ); - mCharPreview->setFont( layerFont ); + mCharPreview->setFont( mRefFont ); //block whileBlocking( spinOffsetX )->setValue( mLayer->offset().x() ); @@ -3374,6 +3390,8 @@ void QgsFontMarkerSymbolLayerWidget::setSymbolLayer( QgsSymbolLayer *layer ) whileBlocking( mHorizontalAnchorComboBox )->setCurrentIndex( mLayer->horizontalAnchorPoint() ); whileBlocking( mVerticalAnchorComboBox )->setCurrentIndex( mLayer->verticalAnchorPoint() ); + registerDataDefinedButton( mFontFamilyDDBtn, QgsSymbolLayer::PropertyFontFamily ); + registerDataDefinedButton( mFontStyleDDBtn, QgsSymbolLayer::PropertyFontStyle ); registerDataDefinedButton( mSizeDDBtn, QgsSymbolLayer::PropertySize ); registerDataDefinedButton( mRotationDDBtn, QgsSymbolLayer::PropertyAngle ); registerDataDefinedButton( mColorDDBtn, QgsSymbolLayer::PropertyFillColor ); @@ -3395,10 +3413,27 @@ QgsSymbolLayer *QgsFontMarkerSymbolLayerWidget::symbolLayer() void QgsFontMarkerSymbolLayerWidget::setFontFamily( const QFont &font ) { - mLayer->setFontFamily( font.family() ); - widgetChar->setFont( font ); - mCharPreview->setFont( font ); - emit changed(); + if ( mLayer ) + { + mLayer->setFontFamily( font.family() ); + mRefFont.setFamily( font.family() ); + widgetChar->setFont( mRefFont ); + mCharPreview->setFont( mRefFont ); + populateFontStyleComboBox(); + emit changed(); + } +} + +void QgsFontMarkerSymbolLayerWidget::setFontStyle( const QString &style ) +{ + if ( mLayer ) + { + QgsFontUtils::updateFontViaStyle( mRefFont, style ); + mLayer->setFontStyle( QgsFontUtils::untranslateNamedStyle( style ) ); + widgetChar->setFont( mRefFont ); + mCharPreview->setFont( mRefFont ); + emit changed(); + } } void QgsFontMarkerSymbolLayerWidget::setColor( const QColor &color ) @@ -3517,6 +3552,39 @@ void QgsFontMarkerSymbolLayerWidget::mStrokeWidthUnitWidget_changed() } } +void QgsFontMarkerSymbolLayerWidget::populateFontStyleComboBox() +{ + mFontStyleComboBox->clear(); + QStringList styles = mFontDB.styles( mRefFont.family() ); + const auto constStyles = styles; + for ( const QString &style : constStyles ) + { + mFontStyleComboBox->addItem( style ); + } + + QString targetStyle = mFontDB.styleString( mRefFont ); + if ( !styles.contains( targetStyle ) ) + { + QFont f = QFont( mRefFont.family() ); + targetStyle = QFontInfo( f ).styleName(); + mRefFont.setStyleName( targetStyle ); + } + int curIndx = 0; + int stylIndx = mFontStyleComboBox->findText( targetStyle ); + if ( stylIndx > -1 ) + { + curIndx = stylIndx; + } + + mFontStyleComboBox->setCurrentIndex( curIndx ); +} + +void QgsFontMarkerSymbolLayerWidget::mFontStyleComboBox_currentIndexChanged( int index ) +{ + Q_UNUSED( index ); + setFontStyle( mFontStyleComboBox->currentText() ); +} + void QgsFontMarkerSymbolLayerWidget::mHorizontalAnchorComboBox_currentIndexChanged( int index ) { if ( mLayer ) @@ -3619,7 +3687,7 @@ QgsRasterMarkerSymbolLayerWidget::QgsRasterMarkerSymbolLayerWidget( QgsVectorLay connect( mLockAspectRatio, static_cast < void ( QgsRatioLockButton::* )( bool ) > ( &QgsRatioLockButton::lockChanged ), this, &QgsRasterMarkerSymbolLayerWidget::setLockAspectRatio ); mSizeUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits - << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); + << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches << QgsUnitTypes::RenderPercentage ); mOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); @@ -3890,7 +3958,7 @@ QgsRasterFillSymbolLayerWidget::QgsRasterFillSymbolLayerWidget( QgsVectorLayer * connect( mWidthSpinBox, static_cast < void ( QDoubleSpinBox::* )( double ) > ( &QDoubleSpinBox::valueChanged ), this, &QgsRasterFillSymbolLayerWidget::mWidthSpinBox_valueChanged ); mWidthUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits - << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); + << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches << QgsUnitTypes::RenderPercentage ); mOffsetUnitWidget->setUnits( QgsUnitTypes::RenderUnitList() << QgsUnitTypes::RenderMillimeters << QgsUnitTypes::RenderMetersInMapUnits << QgsUnitTypes::RenderMapUnits << QgsUnitTypes::RenderPixels << QgsUnitTypes::RenderPoints << QgsUnitTypes::RenderInches ); diff --git a/src/gui/symbology/qgssymbollayerwidget.h b/src/gui/symbology/qgssymbollayerwidget.h index f992a081d344..5029c4fb5563 100644 --- a/src/gui/symbology/qgssymbollayerwidget.h +++ b/src/gui/symbology/qgssymbollayerwidget.h @@ -793,6 +793,9 @@ class GUI_EXPORT QgsSVGFillSymbolLayerWidget : public QgsSymbolLayerWidget, priv void mStrokeWidthSpinBox_valueChanged( double d ); void mTextureWidthUnitWidget_changed(); void mSvgStrokeWidthUnitWidget_changed(); + + private: + int mIconSize = 30; }; ////////// @@ -1000,11 +1003,22 @@ class GUI_EXPORT QgsFontMarkerSymbolLayerWidget : public QgsSymbolLayerWidget, p CharacterWidget *widgetChar = nullptr; private slots: + + /** + * Sets the font \a style. + * \since QGIS 3.14 + */ + void setFontStyle( const QString &style ); + void setOffset(); void mSizeUnitWidget_changed(); void mOffsetUnitWidget_changed(); void mStrokeWidthUnitWidget_changed(); void mStrokeWidthSpinBox_valueChanged( double d ); + + void populateFontStyleComboBox(); + void mFontStyleComboBox_currentIndexChanged( int index ); + void mHorizontalAnchorComboBox_currentIndexChanged( int index ); void mVerticalAnchorComboBox_currentIndexChanged( int index ); void penJoinStyleChanged(); @@ -1014,6 +1028,9 @@ class GUI_EXPORT QgsFontMarkerSymbolLayerWidget : public QgsSymbolLayerWidget, p std::shared_ptr< QgsMarkerSymbol > mAssistantPreviewSymbol; + QFont mRefFont; + QFontDatabase mFontDB; + }; ////////// diff --git a/src/gui/symbology/qgssymbolselectordialog.cpp b/src/gui/symbology/qgssymbolselectordialog.cpp index 26ab0a88a80d..a3fa66024ad5 100644 --- a/src/gui/symbology/qgssymbolselectordialog.cpp +++ b/src/gui/symbology/qgssymbolselectordialog.cpp @@ -772,6 +772,7 @@ QgsSymbolSelectorDialog::QgsSymbolSelectorDialog( QgsSymbol *symbol, QgsStyle *s : QDialog( parent ) { setLayout( new QVBoxLayout() ); + mSelectorWidget = new QgsSymbolSelectorWidget( symbol, style, vl, this ); mButtonBox = new QDialogButtonBox( QDialogButtonBox::Cancel | QDialogButtonBox::Help | QDialogButtonBox::Ok ); diff --git a/src/gui/tableeditor/qgstableeditordialog.cpp b/src/gui/tableeditor/qgstableeditordialog.cpp index 7c9698bcf92b..d0f26e8db553 100644 --- a/src/gui/tableeditor/qgstableeditordialog.cpp +++ b/src/gui/tableeditor/qgstableeditordialog.cpp @@ -21,6 +21,9 @@ #include "qgspanelwidgetstack.h" #include "qgstableeditorformattingwidget.h" +#include +#include + QgsTableEditorDialog::QgsTableEditorDialog( QWidget *parent ) : QMainWindow( parent ) { @@ -98,6 +101,10 @@ QgsTableEditorDialog::QgsTableEditorDialog( QWidget *parent ) addDockWidget( Qt::RightDockWidgetArea, mPropertiesDock ); + mActionImportFromClipboard->setEnabled( !QApplication::clipboard()->text().isEmpty() ); + connect( QApplication::clipboard(), &QClipboard::dataChanged, this, [ = ]() { mActionImportFromClipboard->setEnabled( !QApplication::clipboard()->text().isEmpty() ); } ); + + connect( mActionImportFromClipboard, &QAction::triggered, this, &QgsTableEditorDialog::setTableContentsFromClipboard ); connect( mActionClose, &QAction::triggered, this, &QMainWindow::close ); connect( mActionInsertRowsAbove, &QAction::triggered, mTableWidget, &QgsTableEditorWidget::insertRowsAbove ); connect( mActionInsertRowsBelow, &QAction::triggered, mTableWidget, &QgsTableEditorWidget::insertRowsBelow ); @@ -116,6 +123,42 @@ QgsTableEditorDialog::QgsTableEditorDialog( QWidget *parent ) } ); } +bool QgsTableEditorDialog::setTableContentsFromClipboard() +{ + if ( QApplication::clipboard()->text().isEmpty() ) + return false; + + if ( QMessageBox::question( this, tr( "Import Content From Clipboard" ), + tr( "Importing content from clipboard will overwrite current table content. Are you sure?" ) ) != QMessageBox::Yes ) + return false; + + QgsTableContents contents; + const QStringList lines = QApplication::clipboard()->text().split( '\n' ); + for ( const QString &line : lines ) + { + if ( !line.isEmpty() ) + { + QgsTableRow row; + const QStringList cells = line.split( '\t' ); + for ( const QString &text : cells ) + { + QgsTableCell cell( text ); + row << cell; + } + contents << row; + } + } + + if ( !contents.isEmpty() ) + { + setTableContents( contents ); + emit tableChanged(); + return true; + } + + return false; +} + void QgsTableEditorDialog::setTableContents( const QgsTableContents &contents ) { mBlockSignals = true; diff --git a/src/gui/tableeditor/qgstableeditordialog.h b/src/gui/tableeditor/qgstableeditordialog.h index 220659108178..06486383f24e 100644 --- a/src/gui/tableeditor/qgstableeditordialog.h +++ b/src/gui/tableeditor/qgstableeditordialog.h @@ -54,6 +54,15 @@ class GUI_EXPORT QgsTableEditorDialog : public QMainWindow, private Ui::QgsTable */ void setTableContents( const QgsTableContents &contents ); + /** + * Parses the clipboard text into contents to show in the editor widget. + * \returns TRUE if the clipboard was successfully parsed + * + * \see tableContents() + */ + + bool setTableContentsFromClipboard(); + /** * Returns the current contents of the editor widget table. * diff --git a/src/gui/vector/qgsattributesformproperties.cpp b/src/gui/vector/qgsattributesformproperties.cpp index 73dd36c0725b..b35458d0b165 100644 --- a/src/gui/vector/qgsattributesformproperties.cpp +++ b/src/gui/vector/qgsattributesformproperties.cpp @@ -658,7 +658,8 @@ void QgsAttributesFormProperties::addTabOrGroupButton() tabList.append( QgsAddTabOrGroup::TabPair( itemData.name(), *it ) ); } } - QgsAddTabOrGroup addTabOrGroup( mLayer, tabList, this ); + QTreeWidgetItem *currentItem = mFormLayoutTree->selectedItems().at( 0 ); + QgsAddTabOrGroup addTabOrGroup( mLayer, tabList, currentItem, this ); if ( !addTabOrGroup.exec() ) return; diff --git a/src/gui/vector/qgsfieldcalculator.cpp b/src/gui/vector/qgsfieldcalculator.cpp index edb7bb674317..5ae621d1fa83 100644 --- a/src/gui/vector/qgsfieldcalculator.cpp +++ b/src/gui/vector/qgsfieldcalculator.cpp @@ -13,6 +13,9 @@ * * ***************************************************************************/ +#include + + #include "qgsfieldcalculator.h" #include "qgsdistancearea.h" #include "qgsexpression.h" @@ -30,7 +33,6 @@ #include "qgsexpressioncontextutils.h" #include "qgsvectorlayerjoinbuffer.h" -#include // FTC = FieldTypeCombo constexpr int FTC_TYPE_ROLE_IDX = 0; @@ -66,10 +68,6 @@ QgsFieldCalculator::QgsFieldCalculator( QgsVectorLayer *vl, QWidget *parent ) expContext.lastScope()->addVariable( QgsExpressionContextScope::StaticVariable( QStringLiteral( "row_number" ), 1, true ) ); expContext.setHighlightedVariables( QStringList() << QStringLiteral( "row_number" ) ); - builder->setLayer( vl ); - builder->loadFieldNames(); - builder->setExpressionContext( expContext ); - populateFields(); populateOutputFieldTypes(); @@ -151,17 +149,18 @@ QgsFieldCalculator::QgsFieldCalculator( QgsVectorLayer *vl, QWidget *parent ) mOnlyUpdateSelectedCheckBox->setEnabled( mCanChangeAttributeValue && hasselection ); mOnlyUpdateSelectedCheckBox->setText( tr( "Only update %1 selected features" ).arg( vl->selectedFeatureCount() ) ); - builder->loadRecent( QStringLiteral( "fieldcalc" ) ); - builder->loadUserExpressions( ); + builder->initWithLayer( vl, expContext, QStringLiteral( "fieldcalc" ) ); mInfoIcon->setPixmap( style()->standardPixmap( QStyle::SP_MessageBoxInformation ) ); + setWindowTitle( tr( "%1 — Field Calculator" ).arg( mVectorLayer->name() ) ); + setOkButtonState(); } void QgsFieldCalculator::accept() { - builder->saveToRecent( QStringLiteral( "fieldcalc" ) ); + builder->expressionTree()->saveToRecent( builder->expressionText(), QStringLiteral( "fieldcalc" ) ); if ( !mVectorLayer ) return; diff --git a/src/gui/vector/qgsjoindialog.cpp b/src/gui/vector/qgsjoindialog.cpp index 6f3e437e86ef..15c5af371d0c 100644 --- a/src/gui/vector/qgsjoindialog.cpp +++ b/src/gui/vector/qgsjoindialog.cpp @@ -65,6 +65,7 @@ QgsJoinDialog::QgsJoinDialog( QgsVectorLayer *layer, QList alread connect( mJoinLayerComboBox, &QgsMapLayerComboBox::layerChanged, this, &QgsJoinDialog::checkDefinitionValid ); connect( mJoinFieldComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsJoinDialog::checkDefinitionValid ); connect( mTargetFieldComboBox, &QgsFieldComboBox::fieldChanged, this, &QgsJoinDialog::checkDefinitionValid ); + connect( mEditableJoinLayer, &QGroupBox::toggled, this, &QgsJoinDialog::editableJoinLayerChanged ); checkDefinitionValid(); } @@ -108,6 +109,8 @@ void QgsJoinDialog::setJoinInfo( const QgsVectorLayerJoinInfo &joinInfo ) } } } + + editableJoinLayerChanged(); } QgsVectorLayerJoinInfo QgsJoinDialog::joinInfo() const @@ -201,3 +204,20 @@ void QgsJoinDialog::checkDefinitionValid() && mJoinFieldComboBox->currentIndex() != -1 && mTargetFieldComboBox->currentIndex() != -1 ); } + +void QgsJoinDialog::editableJoinLayerChanged() +{ + if ( mEditableJoinLayer->isChecked() ) + { + mCacheInMemoryCheckBox->setEnabled( false ); + mCacheInMemoryCheckBox->setToolTip( tr( "Caching can not be enabled if editable join layer is enabled" ) ); + mCacheEnabled = mCacheInMemoryCheckBox->isChecked(); + mCacheInMemoryCheckBox->setChecked( false ); + } + else + { + mCacheInMemoryCheckBox->setEnabled( true ); + mCacheInMemoryCheckBox->setToolTip( QString() ); + mCacheInMemoryCheckBox->setChecked( mCacheEnabled ); + } +} diff --git a/src/gui/vector/qgsjoindialog.h b/src/gui/vector/qgsjoindialog.h index 6789fb463c84..746069613c78 100644 --- a/src/gui/vector/qgsjoindialog.h +++ b/src/gui/vector/qgsjoindialog.h @@ -47,9 +47,14 @@ class GUI_EXPORT QgsJoinDialog: public QDialog, private Ui::QgsJoinDialogBase void checkDefinitionValid(); + void editableJoinLayerChanged(); + private: //! Target layer QgsVectorLayer *mLayer = nullptr; + + // Temporary storage for "cache" setting since the checkbox may be temporarily disabled + bool mCacheEnabled = false; }; diff --git a/src/gui/vector/qgssourcefieldsproperties.cpp b/src/gui/vector/qgssourcefieldsproperties.cpp index f4b746b2c339..fb7b608380e3 100644 --- a/src/gui/vector/qgssourcefieldsproperties.cpp +++ b/src/gui/vector/qgssourcefieldsproperties.cpp @@ -20,6 +20,9 @@ #include "qgsproject.h" #include "qgsapplication.h" #include "qgsexpressioncontextutils.h" +#include "qgsgui.h" +#include "qgsnative.h" + QgsSourceFieldsProperties::QgsSourceFieldsProperties( QgsVectorLayer *layer, QWidget *parent ) : QWidget( parent ) @@ -151,21 +154,26 @@ void QgsSourceFieldsProperties::attributeAdded( int idx ) setRow( row, idx, fields.at( idx ) ); mFieldsList->setCurrentCell( row, idx ); + QColor expressionColor = QColor( 103, 0, 243, 44 ); + QColor joinColor = QColor( 0, 243, 79, 44 ); + QColor defaultColor = QColor( 252, 255, 79, 44 ); + for ( int i = 0; i < mFieldsList->columnCount(); i++ ) { switch ( mLayer->fields().fieldOrigin( idx ) ) { case QgsFields::OriginExpression: if ( i == 7 ) continue; - mFieldsList->item( row, i )->setBackground( QColor( 200, 200, 255 ) ); + mFieldsList->item( row, i )->setBackground( expressionColor ); break; case QgsFields::OriginJoin: - mFieldsList->item( row, i )->setBackground( QColor( 200, 255, 200 ) ); + mFieldsList->item( row, i )->setBackground( joinColor ); break; default: - mFieldsList->item( row, i )->setBackground( QColor( 255, 255, 200 ) ); + if ( defaultColor.isValid() ) + mFieldsList->item( row, i )->setBackground( defaultColor ); break; } } @@ -349,12 +357,12 @@ void QgsSourceFieldsProperties::deleteAttributeClicked() } if ( !expressionFields.isEmpty() ) - mLayer->deleteAttributes( expressionFields.toList() ); + mLayer->deleteAttributes( expressionFields.values() ); if ( !providerFields.isEmpty() ) { mLayer->beginEditCommand( tr( "Deleted attributes" ) ); - if ( mLayer->deleteAttributes( providerFields.toList() ) ) + if ( mLayer->deleteAttributes( providerFields.values() ) ) mLayer->endEditCommand(); else mLayer->destroyEditCommand(); @@ -414,7 +422,7 @@ void QgsSourceFieldsProperties::updateButtons() QgsVectorDataProvider *provider = mLayer->dataProvider(); if ( !provider ) return; - const int cap = provider->capabilities(); + const QgsVectorDataProvider::Capabilities cap = provider->capabilities(); mToggleEditingButton->setEnabled( ( cap & QgsVectorDataProvider::ChangeAttributeValues ) && !mLayer->readOnly() ); diff --git a/src/gui/vector/qgsvectorlayerloadstyledialog.cpp b/src/gui/vector/qgsvectorlayerloadstyledialog.cpp index 518e71ac774e..13c6411bfa48 100644 --- a/src/gui/vector/qgsvectorlayerloadstyledialog.cpp +++ b/src/gui/vector/qgsvectorlayerloadstyledialog.cpp @@ -265,7 +265,7 @@ void QgsVectorLayerLoadStyleDialog::deleteStyleFromDB() if ( !msgError.isNull() ) { QgsDebugMsg( opInfo + " failed." ); - QMessageBox::warning(this, opInfo, tr( "%1: fail. %2" ).arg( opInfo, msgError ) ); + QMessageBox::warning( this, opInfo, tr( "%1: fail. %2" ).arg( opInfo, msgError ) ); } else { @@ -282,7 +282,7 @@ void QgsVectorLayerLoadStyleDialog::deleteStyleFromDB() int sectionLimit = mLayer->listStylesInDatabase( ids, names, descriptions, errorMsg ); if ( !errorMsg.isNull() ) { - QMessageBox::warning(this, tr( "Error occurred while retrieving styles from database" ), errorMsg ); + QMessageBox::warning( this, tr( "Error occurred while retrieving styles from database" ), errorMsg ); } else { diff --git a/src/gui/vector/qgsvectorlayerproperties.cpp b/src/gui/vector/qgsvectorlayerproperties.cpp index 251d3fbbe725..aeb6d768cf96 100644 --- a/src/gui/vector/qgsvectorlayerproperties.cpp +++ b/src/gui/vector/qgsvectorlayerproperties.cpp @@ -89,7 +89,7 @@ QgsVectorLayerProperties::QgsVectorLayerProperties( QgsMapCanvas *canvas, - QgsMessageBar* messageBar, + QgsMessageBar *messageBar, QgsVectorLayer *lyr, QWidget *parent, Qt::WindowFlags fl @@ -415,7 +415,7 @@ QgsVectorLayerProperties::QgsVectorLayerProperties( mAuxiliaryLayerActionExport = new QAction( tr( "Export" ), this ); menu->addAction( mAuxiliaryLayerActionExport ); - connect( mAuxiliaryLayerActionExport, &QAction::triggered, this, [=]{ emit exportAuxiliaryLayer( mLayer->auxiliaryLayer() ); } ); + connect( mAuxiliaryLayerActionExport, &QAction::triggered, this, [ = ] { emit exportAuxiliaryLayer( mLayer->auxiliaryLayer() ); } ); mAuxiliaryStorageActions->setMenu( menu ); @@ -1255,7 +1255,7 @@ void QgsVectorLayerProperties::saveMultipleStylesAs() else { mMessageBar->pushMessage( infoWindowTitle, tr( "Style '%1' saved" ).arg( styleName ), - Qgis::Info, timeout ); + Qgis::Info, timeout ); } break; } @@ -1271,30 +1271,9 @@ void QgsVectorLayerProperties::saveMultipleStylesAs() void QgsVectorLayerProperties::aboutToShowStyleMenu() { // this should be unified with QgsRasterLayerProperties::aboutToShowStyleMenu() - QMenu *m = qobject_cast( sender() ); - if ( !m ) - return; - - // first get rid of previously added style manager actions (they are dynamic) - bool gotFirstSeparator = false; - QList actions = m->actions(); - for ( int i = 0; i < actions.count(); ++i ) - { - if ( actions[i]->isSeparator() ) - { - if ( gotFirstSeparator ) - { - // remove all actions after second separator (including it) - while ( actions.count() != i ) - delete actions.takeAt( i ); - break; - } - else - gotFirstSeparator = true; - } - } + QgsMapLayerStyleGuiUtils::instance()->removesExtraMenuSeparators( m ); // re-add style manager actions! m->addSeparator(); QgsMapLayerStyleGuiUtils::instance()->addStyleManagerActions( m, mLayer ); diff --git a/src/gui/vectortile/qgsvectortilebasicrendererwidget.cpp b/src/gui/vectortile/qgsvectortilebasicrendererwidget.cpp new file mode 100644 index 000000000000..8f0bc954b6ff --- /dev/null +++ b/src/gui/vectortile/qgsvectortilebasicrendererwidget.cpp @@ -0,0 +1,456 @@ +/*************************************************************************** + qgsvectortilebasicrendererwidget.cpp + -------------------------------------- + Date : April 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortilebasicrendererwidget.h" + +#include "qgsguiutils.h" +#include "qgssymbollayerutils.h" +#include "qgsvectortilebasicrenderer.h" +#include "qgsvectortilelayer.h" +#include "qgssymbolselectordialog.h" +#include "qgsstyle.h" + +#include +#include +#include + + +///@cond PRIVATE + +class QgsVectorTileBasicRendererListModel : public QAbstractListModel +{ + public: + QgsVectorTileBasicRendererListModel( QgsVectorTileBasicRenderer *r, QObject *parent = nullptr ); + + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role ) override; + + bool removeRows( int row, int count, const QModelIndex &parent = QModelIndex() ) override; + + void insertStyle( int row, const QgsVectorTileBasicRendererStyle &style ); + + // drag'n'drop support + Qt::DropActions supportedDropActions() const override; + QStringList mimeTypes() const override; + QMimeData *mimeData( const QModelIndexList &indexes ) const override; + bool dropMimeData( const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent ) override; + + private: + QgsVectorTileBasicRenderer *mRenderer = nullptr; +}; + + +QgsVectorTileBasicRendererListModel::QgsVectorTileBasicRendererListModel( QgsVectorTileBasicRenderer *r, QObject *parent ) + : QAbstractListModel( parent ) + , mRenderer( r ) +{ +} + +int QgsVectorTileBasicRendererListModel::rowCount( const QModelIndex &parent ) const +{ + if ( parent.isValid() ) + return 0; + + return mRenderer->styles().count(); +} + +int QgsVectorTileBasicRendererListModel::columnCount( const QModelIndex & ) const +{ + return 5; +} + +QVariant QgsVectorTileBasicRendererListModel::data( const QModelIndex &index, int role ) const +{ + if ( index.row() < 0 || index.row() >= mRenderer->styles().count() ) + return QVariant(); + + const QList styles = mRenderer->styles(); + const QgsVectorTileBasicRendererStyle &style = styles[index.row()]; + + switch ( role ) + { + case Qt::DisplayRole: + { + if ( index.column() == 0 ) + return style.styleName(); + else if ( index.column() == 1 ) + return style.layerName().isEmpty() ? tr( "(all layers)" ) : style.layerName(); + else if ( index.column() == 2 ) + return style.minZoomLevel() >= 0 ? style.minZoomLevel() : QVariant(); + else if ( index.column() == 3 ) + return style.maxZoomLevel() >= 0 ? style.maxZoomLevel() : QVariant(); + else if ( index.column() == 4 ) + return style.filterExpression().isEmpty() ? tr( "(no filter)" ) : style.filterExpression(); + + break; + } + + case Qt::EditRole: + { + if ( index.column() == 0 ) + return style.styleName(); + else if ( index.column() == 1 ) + return style.layerName(); + else if ( index.column() == 2 ) + return style.minZoomLevel(); + else if ( index.column() == 3 ) + return style.maxZoomLevel(); + else if ( index.column() == 4 ) + return style.filterExpression(); + + break; + } + + case Qt::DecorationRole: + { + if ( index.column() == 0 && style.symbol() ) + { + const int iconSize = QgsGuiUtils::scaleIconSize( 16 ); + return QgsSymbolLayerUtils::symbolPreviewIcon( style.symbol(), QSize( iconSize, iconSize ) ); + } + break; + } + + case Qt::CheckStateRole: + { + if ( index.column() != 0 ) + return QVariant(); + return style.isEnabled() ? Qt::Checked : Qt::Unchecked; + } + + } + return QVariant(); +} + +QVariant QgsVectorTileBasicRendererListModel::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( orientation == Qt::Horizontal && role == Qt::DisplayRole && section >= 0 && section < 5 ) + { + QStringList lst; + lst << tr( "Label" ) << tr( "Layer" ) << tr( "Min. zoom" ) << tr( "Max. zoom" ) << tr( "Filter" ); + return lst[section]; + } + + return QVariant(); +} + +Qt::ItemFlags QgsVectorTileBasicRendererListModel::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return Qt::ItemIsDropEnabled; + + Qt::ItemFlag checkable = ( index.column() == 0 ? Qt::ItemIsUserCheckable : Qt::NoItemFlags ); + + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | + Qt::ItemIsEditable | checkable | + Qt::ItemIsDragEnabled; +} + +bool QgsVectorTileBasicRendererListModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() ) + return false; + + QgsVectorTileBasicRendererStyle style = mRenderer->style( index.row() ); + + if ( role == Qt::CheckStateRole ) + { + style.setEnabled( value.toInt() == Qt::Checked ); + mRenderer->setStyle( index.row(), style ); + emit dataChanged( index, index ); + return true; + } + + if ( role == Qt::EditRole ) + { + if ( index.column() == 0 ) + style.setStyleName( value.toString() ); + else if ( index.column() == 1 ) + style.setLayerName( value.toString() ); + else if ( index.column() == 2 ) + style.setMinZoomLevel( value.toInt() ); + else if ( index.column() == 3 ) + style.setMaxZoomLevel( value.toInt() ); + else if ( index.column() == 4 ) + style.setFilterExpression( value.toString() ); + + mRenderer->setStyle( index.row(), style ); + emit dataChanged( index, index ); + return true; + } + + return false; +} + +bool QgsVectorTileBasicRendererListModel::removeRows( int row, int count, const QModelIndex &parent ) +{ + QList styles = mRenderer->styles(); + + if ( row < 0 || row >= styles.count() ) + return false; + + beginRemoveRows( parent, row, row + count - 1 ); + + for ( int i = 0; i < count; i++ ) + { + if ( row < styles.count() ) + { + styles.removeAt( row ); + } + } + + mRenderer->setStyles( styles ); + + endRemoveRows(); + return true; +} + +void QgsVectorTileBasicRendererListModel::insertStyle( int row, const QgsVectorTileBasicRendererStyle &style ) +{ + beginInsertRows( QModelIndex(), row, row ); + + QList styles = mRenderer->styles(); + styles.insert( row, style ); + mRenderer->setStyles( styles ); + + endInsertRows(); +} + +Qt::DropActions QgsVectorTileBasicRendererListModel::supportedDropActions() const +{ + return Qt::MoveAction; +} + +QStringList QgsVectorTileBasicRendererListModel::mimeTypes() const +{ + QStringList types; + types << QStringLiteral( "application/vnd.text.list" ); + return types; +} + +QMimeData *QgsVectorTileBasicRendererListModel::mimeData( const QModelIndexList &indexes ) const +{ + QMimeData *mimeData = new QMimeData(); + QByteArray encodedData; + + QDataStream stream( &encodedData, QIODevice::WriteOnly ); + + const auto constIndexes = indexes; + for ( const QModelIndex &index : constIndexes ) + { + // each item consists of several columns - let's add it with just first one + if ( !index.isValid() || index.column() != 0 ) + continue; + + QgsVectorTileBasicRendererStyle style = mRenderer->style( index.row() ); + + QDomDocument doc; + QDomElement rootElem = doc.createElement( QStringLiteral( "vector_tile_basic_renderer_style_mime" ) ); + style.writeXml( rootElem, QgsReadWriteContext() ); + doc.appendChild( rootElem ); + + stream << doc.toString( -1 ); + } + + mimeData->setData( QStringLiteral( "application/vnd.text.list" ), encodedData ); + return mimeData; +} + +bool QgsVectorTileBasicRendererListModel::dropMimeData( const QMimeData *data, + Qt::DropAction action, int row, int column, const QModelIndex &parent ) +{ + Q_UNUSED( column ) + + if ( action == Qt::IgnoreAction ) + return true; + + if ( !data->hasFormat( QStringLiteral( "application/vnd.text.list" ) ) ) + return false; + + if ( parent.column() > 0 ) + return false; + + QByteArray encodedData = data->data( QStringLiteral( "application/vnd.text.list" ) ); + QDataStream stream( &encodedData, QIODevice::ReadOnly ); + int rows = 0; + + if ( row == -1 ) + { + // the item was dropped at a parent - we may decide where to put the items - let's append them + row = rowCount( parent ); + } + + while ( !stream.atEnd() ) + { + QString text; + stream >> text; + + QDomDocument doc; + if ( !doc.setContent( text ) ) + continue; + QDomElement rootElem = doc.documentElement(); + if ( rootElem.tagName() != QLatin1String( "vector_tile_basic_renderer_style_mime" ) ) + continue; + + QgsVectorTileBasicRendererStyle style; + style.readXml( rootElem, QgsReadWriteContext() ); + + insertStyle( row + rows, style ); + ++rows; + } + return true; +} + + +// + + +QgsVectorTileBasicRendererWidget::QgsVectorTileBasicRendererWidget( QgsVectorTileLayer *layer, QgsMapCanvas *canvas, QgsMessageBar *messageBar, QWidget *parent ) + : QgsMapLayerConfigWidget( layer, canvas, parent ) + , mVTLayer( layer ) + , mMessageBar( messageBar ) +{ + setupUi( this ); + layout()->setContentsMargins( 0, 0, 0, 0 ); + + if ( layer->renderer() && layer->renderer()->type() == QStringLiteral( "basic" ) ) + { + mRenderer.reset( static_cast( layer->renderer()->clone() ) ); + } + else + { + mRenderer.reset( new QgsVectorTileBasicRenderer() ); + } + + mModel = new QgsVectorTileBasicRendererListModel( mRenderer.get(), viewStyles ); + viewStyles->setModel( mModel ); + + QMenu *menuAddRule = new QMenu( btnAddRule ); + menuAddRule->addAction( tr( "Marker" ), this, [this] { addStyle( QgsWkbTypes::PointGeometry ); } ); + menuAddRule->addAction( tr( "Line" ), this, [this] { addStyle( QgsWkbTypes::LineGeometry ); } ); + menuAddRule->addAction( tr( "Fill" ), this, [this] { addStyle( QgsWkbTypes::PolygonGeometry ); } ); + btnAddRule->setMenu( menuAddRule ); + + connect( btnEditRule, &QPushButton::clicked, this, &QgsVectorTileBasicRendererWidget::editStyle ); + connect( btnRemoveRule, &QAbstractButton::clicked, this, &QgsVectorTileBasicRendererWidget::removeStyle ); + + connect( viewStyles, &QAbstractItemView::doubleClicked, this, &QgsVectorTileBasicRendererWidget::editStyleAtIndex ); + + connect( mModel, &QAbstractItemModel::dataChanged, this, &QgsPanelWidget::widgetChanged ); + connect( mModel, &QAbstractItemModel::rowsInserted, this, &QgsPanelWidget::widgetChanged ); + connect( mModel, &QAbstractItemModel::rowsRemoved, this, &QgsPanelWidget::widgetChanged ); +} + +QgsVectorTileBasicRendererWidget::~QgsVectorTileBasicRendererWidget() = default; + +void QgsVectorTileBasicRendererWidget::apply() +{ + mVTLayer->setRenderer( mRenderer->clone() ); +} + +void QgsVectorTileBasicRendererWidget::addStyle( QgsWkbTypes::GeometryType geomType ) +{ + QgsVectorTileBasicRendererStyle style( QString(), QString(), geomType ); + style.setSymbol( QgsSymbol::defaultSymbol( geomType ) ); + + int rows = mModel->rowCount(); + mModel->insertStyle( rows, style ); + viewStyles->selectionModel()->setCurrentIndex( mModel->index( rows, 0 ), QItemSelectionModel::ClearAndSelect ); +} + +void QgsVectorTileBasicRendererWidget::editStyle() +{ + editStyleAtIndex( viewStyles->selectionModel()->currentIndex() ); +} + +void QgsVectorTileBasicRendererWidget::editStyleAtIndex( const QModelIndex &index ) +{ + QgsVectorTileBasicRendererStyle style = mRenderer->style( index.row() ); + + if ( !style.symbol() ) + return; + + std::unique_ptr< QgsSymbol > symbol( style.symbol()->clone() ); + + QgsSymbolWidgetContext context; + context.setMapCanvas( mMapCanvas ); + context.setMessageBar( mMessageBar ); + + QgsVectorLayer *vectorLayer = nullptr; // TODO: have a temporary vector layer with sub-layer's fields? + + QgsPanelWidget *panel = QgsPanelWidget::findParentPanel( this ); + if ( panel && panel->dockMode() ) + { + QgsSymbolSelectorWidget *dlg = new QgsSymbolSelectorWidget( symbol.release(), QgsStyle::defaultStyle(), vectorLayer, panel ); + dlg->setContext( context ); + dlg->setPanelTitle( style.styleName() ); + connect( dlg, &QgsPanelWidget::widgetChanged, this, &QgsVectorTileBasicRendererWidget::updateSymbolsFromWidget ); + connect( dlg, &QgsPanelWidget::panelAccepted, this, &QgsVectorTileBasicRendererWidget::cleanUpSymbolSelector ); + openPanel( dlg ); + } + else + { + QgsSymbolSelectorDialog dlg( symbol.get(), QgsStyle::defaultStyle(), vectorLayer, panel ); + dlg.setContext( context ); + if ( !dlg.exec() || !symbol ) + { + return; + } + + style.setSymbol( symbol.release() ); + mRenderer->setStyle( index.row(), style ); + emit widgetChanged(); + } +} + +void QgsVectorTileBasicRendererWidget::updateSymbolsFromWidget() +{ + int index = viewStyles->selectionModel()->currentIndex().row(); + QgsVectorTileBasicRendererStyle style = mRenderer->style( index ); + + QgsSymbolSelectorWidget *dlg = qobject_cast( sender() ); + style.setSymbol( dlg->symbol()->clone() ); + + mRenderer->setStyle( index, style ); + emit widgetChanged(); +} + +void QgsVectorTileBasicRendererWidget::cleanUpSymbolSelector( QgsPanelWidget *container ) +{ + QgsSymbolSelectorWidget *dlg = qobject_cast( container ); + if ( !dlg ) + return; + + delete dlg->symbol(); +} + +void QgsVectorTileBasicRendererWidget::removeStyle() +{ + QItemSelection sel = viewStyles->selectionModel()->selection(); + const auto constSel = sel; + for ( const QItemSelectionRange &range : constSel ) + { + if ( range.isValid() ) + mModel->removeRows( range.top(), range.bottom() - range.top() + 1, range.parent() ); + } + // make sure that the selection is gone + viewStyles->selectionModel()->clear(); +} + +///@endcond diff --git a/src/gui/vectortile/qgsvectortilebasicrendererwidget.h b/src/gui/vectortile/qgsvectortilebasicrendererwidget.h new file mode 100644 index 000000000000..b7a020201626 --- /dev/null +++ b/src/gui/vectortile/qgsvectortilebasicrendererwidget.h @@ -0,0 +1,71 @@ +/*************************************************************************** + qgsvectortilebasicrendererwidget.h + -------------------------------------- + Date : April 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEBASICRENDERERWIDGET_H +#define QGSVECTORTILEBASICRENDERERWIDGET_H + +#include "qgsmaplayerconfigwidget.h" + +#include "ui_qgsvectortilebasicrendererwidget.h" + +#include "qgswkbtypes.h" + +#include + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsVectorTileBasicRenderer; +class QgsVectorTileBasicRendererListModel; +class QgsVectorTileLayer; +class QgsMapCanvas; +class QgsMessageBar; + +/** + * \ingroup gui + * Styling widget for basic renderer of vector tile layer + * + * \since QGIS 3.14 + */ +class GUI_EXPORT QgsVectorTileBasicRendererWidget : public QgsMapLayerConfigWidget, private Ui::QgsVectorTileBasicRendererWidget +{ + Q_OBJECT + public: + QgsVectorTileBasicRendererWidget( QgsVectorTileLayer *layer, QgsMapCanvas *canvas, QgsMessageBar *messageBar, QWidget *parent = nullptr ); + ~QgsVectorTileBasicRendererWidget() override; + + public slots: + //! Applies the settings made in the dialog + void apply() override; + + private slots: + void addStyle( QgsWkbTypes::GeometryType geomType ); + void editStyle(); + void editStyleAtIndex( const QModelIndex &index ); + void removeStyle(); + + void updateSymbolsFromWidget(); + void cleanUpSymbolSelector( QgsPanelWidget *container ); + + private: + QgsVectorTileLayer *mVTLayer = nullptr; + std::unique_ptr mRenderer; + QgsVectorTileBasicRendererListModel *mModel = nullptr; + QgsMessageBar *mMessageBar = nullptr; +}; + +///@endcond + +#endif // QGSVECTORTILEBASICRENDERERWIDGET_H diff --git a/src/gui/vectortile/qgsvectortileconnectiondialog.cpp b/src/gui/vectortile/qgsvectortileconnectiondialog.cpp new file mode 100644 index 000000000000..1a4e1c606891 --- /dev/null +++ b/src/gui/vectortile/qgsvectortileconnectiondialog.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + qgsvectortileconnectiondialog.cpp + --------------------- + begin : March 2020 + copyright : (C) 2020 by Martin Dobias + email : wonder dot sk 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 "qgsvectortileconnectiondialog.h" +#include "qgsvectortileconnection.h" +#include "qgsgui.h" +#include + +///@cond PRIVATE + +QgsVectorTileConnectionDialog::QgsVectorTileConnectionDialog( QWidget *parent ) + : QDialog( parent ) +{ + setupUi( this ); + QgsGui::enableAutoGeometryRestore( this ); + + // Behavior for min and max zoom checkbox + connect( mCheckBoxZMin, &QCheckBox::toggled, mSpinZMin, &QSpinBox::setEnabled ); + connect( mCheckBoxZMax, &QCheckBox::toggled, mSpinZMax, &QSpinBox::setEnabled ); +} + +void QgsVectorTileConnectionDialog::setConnection( const QString &name, const QString &uri ) +{ + mEditName->setText( name ); + + QgsVectorTileProviderConnection::Data conn = QgsVectorTileProviderConnection::decodedUri( uri ); + mEditUrl->setText( conn.url ); + mCheckBoxZMin->setChecked( conn.zMin != -1 ); + mSpinZMin->setValue( conn.zMin != -1 ? conn.zMin : 0 ); + mCheckBoxZMax->setChecked( conn.zMax != -1 ); + mSpinZMax->setValue( conn.zMax != -1 ? conn.zMax : 14 ); +} + +QString QgsVectorTileConnectionDialog::connectionUri() const +{ + QgsVectorTileProviderConnection::Data conn; + conn.url = mEditUrl->text(); + if ( mCheckBoxZMin->isChecked() ) + conn.zMin = mSpinZMin->value(); + if ( mCheckBoxZMax->isChecked() ) + conn.zMax = mSpinZMax->value(); + return QgsVectorTileProviderConnection::encodedUri( conn ); +} + +QString QgsVectorTileConnectionDialog::connectionName() const +{ + return mEditName->text(); +} + +void QgsVectorTileConnectionDialog::accept() +{ + if ( mCheckBoxZMin->isChecked() && mCheckBoxZMax->isChecked() && mSpinZMax->value() < mSpinZMin->value() ) + { + QMessageBox::warning( this, tr( "Connection Properties" ), tr( "The maximum zoom level (%1) cannot be lower than the minimum zoom level (%2)." ).arg( mSpinZMax->value() ).arg( mSpinZMin->value() ) ); + return; + } + QDialog::accept(); +} + +///@endcond diff --git a/src/gui/vectortile/qgsvectortileconnectiondialog.h b/src/gui/vectortile/qgsvectortileconnectiondialog.h new file mode 100644 index 000000000000..7c70c24c75dd --- /dev/null +++ b/src/gui/vectortile/qgsvectortileconnectiondialog.h @@ -0,0 +1,48 @@ +/*************************************************************************** + qgsvectortileconnectiondialog.h + --------------------- + begin : March 2020 + copyright : (C) 2020 by Martin Dobias + email : wonder dot sk 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 QGSVECTORTILECONNECTIONDIALOG_H +#define QGSVECTORTILECONNECTIONDIALOG_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include + +#include "ui_qgsvectortileconnectiondialog.h" + + +class QgsVectorTileConnectionDialog : public QDialog, public Ui::QgsVectorTileConnectionDialog +{ + Q_OBJECT + public: + explicit QgsVectorTileConnectionDialog( QWidget *parent = nullptr ); + + void setConnection( const QString &name, const QString &uri ); + + QString connectionUri() const; + QString connectionName() const; + + void accept() override; + + private: + + QString mBaseKey; + QString mCredentialsBaseKey; +}; + +///@endcond + +#endif // QGSVECTORTILECONNECTIONDIALOG_H diff --git a/src/gui/vectortile/qgsvectortiledataitemguiprovider.cpp b/src/gui/vectortile/qgsvectortiledataitemguiprovider.cpp new file mode 100644 index 000000000000..6659d8ca1f94 --- /dev/null +++ b/src/gui/vectortile/qgsvectortiledataitemguiprovider.cpp @@ -0,0 +1,115 @@ +/*************************************************************************** + qgsvectortiledataitemguiprovider.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortiledataitemguiprovider.h" + +#include "qgsvectortiledataitems.h" +#include "qgsvectortileconnectiondialog.h" +#include "qgsvectortileconnection.h" +#include "qgsmanageconnectionsdialog.h" + +#include +#include + +///@cond PRIVATE + +void QgsVectorTileDataItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList &, QgsDataItemGuiContext ) +{ + if ( QgsVectorTileLayerItem *layerItem = qobject_cast< QgsVectorTileLayerItem * >( item ) ) + { + QAction *actionEdit = new QAction( tr( "Edit…" ), this ); + connect( actionEdit, &QAction::triggered, this, [layerItem] { editConnection( layerItem ); } ); + menu->addAction( actionEdit ); + + QAction *actionDelete = new QAction( tr( "Delete" ), this ); + connect( actionDelete, &QAction::triggered, this, [layerItem] { deleteConnection( layerItem ); } ); + menu->addAction( actionDelete ); + } + + if ( QgsVectorTileRootItem *rootItem = qobject_cast< QgsVectorTileRootItem * >( item ) ) + { + QAction *actionNew = new QAction( tr( "New Connection…" ), this ); + connect( actionNew, &QAction::triggered, this, [rootItem] { newConnection( rootItem ); } ); + menu->addAction( actionNew ); + + QAction *actionSaveXyzTilesServers = new QAction( tr( "Save Connections…" ), this ); + connect( actionSaveXyzTilesServers, &QAction::triggered, this, [] { saveXyzTilesServers(); } ); + menu->addAction( actionSaveXyzTilesServers ); + + QAction *actionLoadXyzTilesServers = new QAction( tr( "Load Connections…" ), this ); + connect( actionLoadXyzTilesServers, &QAction::triggered, this, [rootItem] { loadXyzTilesServers( rootItem ); } ); + menu->addAction( actionLoadXyzTilesServers ); + } +} + +void QgsVectorTileDataItemGuiProvider::editConnection( QgsDataItem *item ) +{ + QgsVectorTileConnectionDialog dlg; + QString uri = QgsVectorTileProviderConnection::encodedUri( QgsVectorTileProviderConnection::connection( item->name() ) ); + dlg.setConnection( item->name(), uri ); + if ( !dlg.exec() ) + return; + + QgsVectorTileProviderConnection::deleteConnection( item->name() ); + QgsVectorTileProviderConnection::Data conn = QgsVectorTileProviderConnection::decodedUri( dlg.connectionUri() ); + QgsVectorTileProviderConnection::addConnection( dlg.connectionName(), conn ); + + item->parent()->refreshConnections(); +} + +void QgsVectorTileDataItemGuiProvider::deleteConnection( QgsDataItem *item ) +{ + if ( QMessageBox::question( nullptr, tr( "Delete Connection" ), tr( "Are you sure you want to delete the connection “%1”?" ).arg( item->name() ), + QMessageBox::Yes | QMessageBox::No, QMessageBox::No ) != QMessageBox::Yes ) + return; + + QgsVectorTileProviderConnection::deleteConnection( item->name() ); + + item->parent()->refreshConnections(); +} + +void QgsVectorTileDataItemGuiProvider::newConnection( QgsDataItem *item ) +{ + QgsVectorTileConnectionDialog dlg; + if ( !dlg.exec() ) + return; + + QgsVectorTileProviderConnection::Data conn = QgsVectorTileProviderConnection::decodedUri( dlg.connectionUri() ); + QgsVectorTileProviderConnection::addConnection( dlg.connectionName(), conn ); + + item->refreshConnections(); +} + +void QgsVectorTileDataItemGuiProvider::saveXyzTilesServers() +{ + QgsManageConnectionsDialog dlg( nullptr, QgsManageConnectionsDialog::Export, QgsManageConnectionsDialog::XyzTiles ); + dlg.exec(); +} + +void QgsVectorTileDataItemGuiProvider::loadXyzTilesServers( QgsDataItem *item ) +{ + QString fileName = QFileDialog::getOpenFileName( nullptr, tr( "Load Connections" ), QDir::homePath(), + tr( "XML files (*.xml *.XML)" ) ); + if ( fileName.isEmpty() ) + { + return; + } + + QgsManageConnectionsDialog dlg( nullptr, QgsManageConnectionsDialog::Import, QgsManageConnectionsDialog::XyzTiles, fileName ); + dlg.exec(); + item->refreshConnections(); +} + +///@endcond diff --git a/src/gui/vectortile/qgsvectortiledataitemguiprovider.h b/src/gui/vectortile/qgsvectortiledataitemguiprovider.h new file mode 100644 index 000000000000..12e93e6362db --- /dev/null +++ b/src/gui/vectortile/qgsvectortiledataitemguiprovider.h @@ -0,0 +1,46 @@ +/*************************************************************************** + qgsvectortiledataitemguiprovider.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEDATAITEMGUIPROVIDER_H +#define QGSVECTORTILEDATAITEMGUIPROVIDER_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include "qgsdataitemguiprovider.h" + + +class QgsVectorTileDataItemGuiProvider : public QObject, public QgsDataItemGuiProvider +{ + Q_OBJECT + public: + + QString name() override { return QStringLiteral( "Vector Tiles" ); } + + void populateContextMenu( QgsDataItem *item, QMenu *menu, + const QList &selectedItems, QgsDataItemGuiContext context ) override; + + private: + static void editConnection( QgsDataItem *item ); + static void deleteConnection( QgsDataItem *item ); + static void newConnection( QgsDataItem *item ); + static void saveXyzTilesServers(); + static void loadXyzTilesServers( QgsDataItem *item ); + +}; + +///@endcond + +#endif // QGSVECTORTILEDATAITEMGUIPROVIDER_H diff --git a/src/gui/vectortile/qgsvectortileproviderguimetadata.cpp b/src/gui/vectortile/qgsvectortileproviderguimetadata.cpp new file mode 100644 index 000000000000..e95796e74a3f --- /dev/null +++ b/src/gui/vectortile/qgsvectortileproviderguimetadata.cpp @@ -0,0 +1,33 @@ +/*************************************************************************** + qgsvectortileproviderguimetadata.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgsvectortileproviderguimetadata.h" + +#include "qgsvectortiledataitemguiprovider.h" + +///@cond PRIVATE + +QgsVectorTileProviderGuiMetadata::QgsVectorTileProviderGuiMetadata() + : QgsProviderGuiMetadata( QStringLiteral( "vectortile" ) ) +{ +} + +QList QgsVectorTileProviderGuiMetadata::dataItemGuiProviders() +{ + return QList() + << new QgsVectorTileDataItemGuiProvider; +} + +///@endcond diff --git a/src/gui/vectortile/qgsvectortileproviderguimetadata.h b/src/gui/vectortile/qgsvectortileproviderguimetadata.h new file mode 100644 index 000000000000..5ea34d921378 --- /dev/null +++ b/src/gui/vectortile/qgsvectortileproviderguimetadata.h @@ -0,0 +1,38 @@ +/*************************************************************************** + qgsvectortileproviderguimetadata.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 QGSVECTORTILEPROVIDERGUIMETADATA_H +#define QGSVECTORTILEPROVIDERGUIMETADATA_H + +///@cond PRIVATE +#define SIP_NO_FILE + +#include +#include + +#include "qgsproviderguimetadata.h" + +class QgsVectorTileProviderGuiMetadata: public QgsProviderGuiMetadata +{ + public: + QgsVectorTileProviderGuiMetadata(); + + QList dataItemGuiProviders() override; + +}; + +///@endcond + +#endif // QGSVECTORTILEPROVIDERGUIMETADATA_H diff --git a/src/plugins/evis/eventbrowser/evisgenericeventbrowsergui.cpp b/src/plugins/evis/eventbrowser/evisgenericeventbrowsergui.cpp index e3e2ca390e06..22cc13fd0dfd 100644 --- a/src/plugins/evis/eventbrowser/evisgenericeventbrowsergui.cpp +++ b/src/plugins/evis/eventbrowser/evisgenericeventbrowsergui.cpp @@ -571,7 +571,7 @@ void eVisGenericEventBrowserGui::displayImage() // only change the extents if the point is beyond the current extents to minimize repaints if ( !mCanvas->extent().contains( myPoint ) ) { - mCanvas->setExtent( myRect ); + mCanvas->setExtent( myRect, true ); } mCanvas->refresh(); } diff --git a/src/plugins/geometry_checker/qgsgeometrycheckerresulttab.cpp b/src/plugins/geometry_checker/qgsgeometrycheckerresulttab.cpp index c918ea319d2f..5257f793f567 100644 --- a/src/plugins/geometry_checker/qgsgeometrycheckerresulttab.cpp +++ b/src/plugins/geometry_checker/qgsgeometrycheckerresulttab.cpp @@ -408,7 +408,7 @@ void QgsGeometryCheckerResultTab::highlightErrors( bool current ) if ( !totextent.isEmpty() ) { - mIface->mapCanvas()->setExtent( totextent ); + mIface->mapCanvas()->setExtent( totextent, true ); } mIface->mapCanvas()->refresh(); } diff --git a/src/plugins/qgisplugin.h b/src/plugins/qgisplugin.h index 5557179d1b13..3d8f3ac27aa8 100644 --- a/src/plugins/qgisplugin.h +++ b/src/plugins/qgisplugin.h @@ -214,4 +214,10 @@ typedef QString icon_t(); //! Typedef for getting the experimental status without instantiating the plugin typedef QString experimental_t(); +//! Typedef for getting the create date without instantiating the plugin +typedef QString create_date_t(); + +//! Typedef for getting the update date status without instantiating the plugin +typedef QString update_date_t(); + #endif // QGISPLUGIN_H diff --git a/src/plugins/topology/checkDock.cpp b/src/plugins/topology/checkDock.cpp index 5e2082327640..1b12dd022adf 100644 --- a/src/plugins/topology/checkDock.cpp +++ b/src/plugins/topology/checkDock.cpp @@ -205,7 +205,7 @@ void checkDock::errorListClicked( const QModelIndex &index ) QgsRectangle r = mErrorList.at( row )->boundingBox(); r.scale( 1.5 ); QgsMapCanvas *canvas = qgsInterface->mapCanvas(); - canvas->setExtent( r ); + canvas->setExtent( r, true ); canvas->refresh(); mFixBox->clear(); diff --git a/src/providers/arcgisrest/qgsafssourceselect.cpp b/src/providers/arcgisrest/qgsafssourceselect.cpp index 91d74aed661d..dcbed0c30793 100644 --- a/src/providers/arcgisrest/qgsafssourceselect.cpp +++ b/src/providers/arcgisrest/qgsafssourceselect.cpp @@ -179,7 +179,7 @@ void QgsAfsSourceSelect::buildQuery( const QgsOwsConnection &connection, const Q //add available attributes to expression builder QgsExpressionBuilderWidget *w = d.expressionBuilder(); - w->loadFieldNames( provider.fields() ); + w->initWithFields( provider.fields() ); if ( d.exec() == QDialog::Accepted ) { diff --git a/src/providers/mdal/qgsmdalprovider.cpp b/src/providers/mdal/qgsmdalprovider.cpp index d9e44b52b848..d6d34aa0873f 100644 --- a/src/providers/mdal/qgsmdalprovider.cpp +++ b/src/providers/mdal/qgsmdalprovider.cpp @@ -22,6 +22,7 @@ #include "qgslogger.h" #include "qgsapplication.h" #include "qgsmdaldataitems.h" +#include "qgsmeshdataprovidertemporalcapabilities.h" const QString QgsMdalProvider::MDAL_PROVIDER_KEY = QStringLiteral( "mdal" ); @@ -50,6 +51,7 @@ QgsCoordinateReferenceSystem QgsMdalProvider::crs() const QgsMdalProvider::QgsMdalProvider( const QString &uri, const ProviderOptions &options ) : QgsMeshDataProvider( uri, options ) { + temporalCapabilities()->setTemporalUnit( QgsUnitTypes::TemporalHours ); loadData(); } @@ -307,12 +309,44 @@ void QgsMdalProvider::loadData() { QByteArray curi = dataSourceUri().toUtf8(); mMeshH = MDAL_LoadMesh( curi.constData() ); + temporalCapabilities()->clear(); + if ( mMeshH ) { const QString proj = MDAL_M_projection( mMeshH ); if ( !proj.isEmpty() ) mCrs.createFromString( proj ); + + int dsGroupCount = MDAL_M_datasetGroupCount( mMeshH ); + for ( int i = 0; i < dsGroupCount; ++i ) + addGroupToTemporalCapabilities( i ); + } +} + + +void QgsMdalProvider::addGroupToTemporalCapabilities( int indexGroup ) +{ + if ( !mMeshH ) + return; + QgsMeshDataProviderTemporalCapabilities *tempCap = temporalCapabilities(); + tempCap->setHasTemporalCapabilities( true ); + tempCap->setHasTemporalCapabilities( true ); + QgsMeshDatasetGroupMetadata dsgMetadata = datasetGroupMetadata( indexGroup ); + QDateTime refTime = dsgMetadata.referenceTime(); + refTime.setTimeSpec( Qt::UTC ); //For now provider don't support time zone and return always in local time, force UTC + tempCap->addGroupReferenceDateTime( indexGroup, refTime ); + int dsCount = datasetCount( indexGroup ); + + if ( dsgMetadata.isTemporal() ) + { + for ( int dsi = 0; dsi < dsCount; ++dsi ) + { + QgsMeshDatasetMetadata dsMeta = datasetMetadata( QgsMeshDatasetIndex( indexGroup, dsi ) ); + if ( dsMeta.isValid() ) + tempCap->addDatasetTime( indexGroup, dsMeta.time() ); + } } + } void QgsMdalProvider::reloadProviderData() @@ -322,11 +356,16 @@ void QgsMdalProvider::reloadProviderData() loadData(); + int datasetCountBeforeAdding = datasetGroupCount(); + if ( mMeshH ) for ( auto uri : mExtraDatasetUris ) { std::string str = uri.toStdString(); MDAL_M_LoadDatasets( mMeshH, str.c_str() ); + int datasetCount = datasetGroupCount(); + for ( ; datasetCountBeforeAdding < datasetCount; datasetCountBeforeAdding++ ) + addGroupToTemporalCapabilities( datasetCountBeforeAdding ); } } @@ -468,8 +507,11 @@ bool QgsMdalProvider::addDataset( const QString &uri ) else { mExtraDatasetUris << uri; - emit datasetGroupsAdded( datasetGroupCount() - datasetCount ); + int datasetCountAfterAdding = datasetGroupCount(); + emit datasetGroupsAdded( datasetCountAfterAdding - datasetCount ); emit dataChanged(); + for ( ; datasetCount < datasetCountAfterAdding; datasetCount++ ) + addGroupToTemporalCapabilities( datasetCount ); return true; // Ok } } @@ -539,6 +581,8 @@ QgsMeshDatasetGroupMetadata QgsMdalProvider::datasetGroupMetadata( int groupInde QString referenceTimeString( MDAL_G_referenceTime( group ) ); QDateTime referenceTime = QDateTime::fromString( referenceTimeString, Qt::ISODate ); + bool isTemporal = MDAL_G_datasetCount( group ) > 1; + QgsMeshDatasetGroupMetadata meta( name, isScalar, @@ -547,6 +591,7 @@ QgsMeshDatasetGroupMetadata QgsMdalProvider::datasetGroupMetadata( int groupInde max, maximumVerticalLevels, referenceTime, + isTemporal, metadata ); diff --git a/src/providers/mdal/qgsmdalprovider.h b/src/providers/mdal/qgsmdalprovider.h index 1a01be3eda3c..6cc35d00ddf3 100644 --- a/src/providers/mdal/qgsmdalprovider.h +++ b/src/providers/mdal/qgsmdalprovider.h @@ -111,6 +111,7 @@ class QgsMdalProvider : public QgsMeshDataProvider QVector edges( ) const; QVector faces( ) const; void loadData(); + void addGroupToTemporalCapabilities( int indexGroup ); MeshH mMeshH; QStringList mExtraDatasetUris; QgsCoordinateReferenceSystem mCrs; diff --git a/src/providers/mssql/CMakeLists.txt b/src/providers/mssql/CMakeLists.txt index ed82b1590f07..e31b05db7309 100644 --- a/src/providers/mssql/CMakeLists.txt +++ b/src/providers/mssql/CMakeLists.txt @@ -1,6 +1,7 @@ SET(MSSQL_SRCS qgsmssqlconnection.cpp qgsmssqlprovider.cpp + qgsmssqlproviderconnection.cpp qgsmssqlgeometryparser.cpp qgsmssqltablemodel.cpp qgsmssqldataitems.cpp diff --git a/src/providers/mssql/qgsmssqlconnection.cpp b/src/providers/mssql/qgsmssqlconnection.cpp index a2e8c778f022..4f276d778219 100644 --- a/src/providers/mssql/qgsmssqlconnection.cpp +++ b/src/providers/mssql/qgsmssqlconnection.cpp @@ -361,6 +361,68 @@ bool QgsMssqlConnection::isSystemSchema( const QString &schema ) return sSystemSchemas.contains( schema ); } +QgsDataSourceUri QgsMssqlConnection::connUri( const QString &connName ) +{ + + QgsSettings settings; + + const QString key = "/MSSQL/connections/" + connName; + + const QString service = settings.value( key + "/service" ).toString(); + const QString host = settings.value( key + "/host" ).toString(); + const QString database = settings.value( key + "/database" ).toString(); + const QString username = settings.value( key + "/username" ).toString(); + const QString password = settings.value( key + "/password" ).toString(); + + const bool useGeometryColumns { QgsMssqlConnection::geometryColumnsOnly( connName ) }; + const bool useEstimatedMetadata { QgsMssqlConnection::useEstimatedMetadata( connName ) }; + const bool allowGeometrylessTables { QgsMssqlConnection::allowGeometrylessTables( connName ) }; + const bool disableGeometryHandling { QgsMssqlConnection::isInvalidGeometryHandlingDisabled( connName ) }; + + QgsDataSourceUri uri; + if ( !service.isEmpty() ) + { + uri.setConnection( service, database, username, password ); + } + else + { + uri.setConnection( host, QString(), database, username, password ); + } + + uri.setParam( QStringLiteral( "geometryColumnsOnly" ), useGeometryColumns ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); + uri.setUseEstimatedMetadata( useEstimatedMetadata ); + uri.setParam( QStringLiteral( "allowGeometrylessTables" ), allowGeometrylessTables ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); + uri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), disableGeometryHandling ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); + + if ( settings.value( QStringLiteral( "saveUsername" ) ).isValid() ) + { + const bool saveUsername { settings.value( QStringLiteral( "saveUsername" ) ).toBool() }; + uri.setParam( QStringLiteral( "saveUsername" ), saveUsername ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); + if ( ! saveUsername ) + { + uri.setUsername( QString() ); + } + } + if ( settings.value( QStringLiteral( "savePassword" ) ).isValid() ) + { + const bool savePassword { settings.value( QStringLiteral( "savePassword" ) ).toBool() }; + uri.setParam( QStringLiteral( "savePassword" ), savePassword ? QStringLiteral( "true" ) : QStringLiteral( "false" ) ); + if ( ! savePassword ) + { + uri.setPassword( QString() ); + } + } + + return uri; +} + +QStringList QgsMssqlConnection::connectionList() +{ + QgsSettings settings; + settings.beginGroup( QStringLiteral( "MSSQL/connections" ) ); + return settings.childGroups(); +} + QString QgsMssqlConnection::dbConnectionName( const QString &name ) { // Starting with Qt 5.11, sharing the same connection between threads is not allowed. diff --git a/src/providers/mssql/qgsmssqlconnection.h b/src/providers/mssql/qgsmssqlconnection.h index c4833f5ffc79..16e71b9e5a0c 100644 --- a/src/providers/mssql/qgsmssqlconnection.h +++ b/src/providers/mssql/qgsmssqlconnection.h @@ -21,6 +21,8 @@ #include #include +#include "qgsdatasourceuri.h" + class QString; class QSqlDatabase; @@ -151,6 +153,17 @@ class QgsMssqlConnection */ static bool isSystemSchema( const QString &schema ); + /** + * Reads a connection named \a connName from the settings and returns the datasource uri + */ + static QgsDataSourceUri connUri( const QString &connName ); + + /** + * Reads MSSQL connections from the settings and return a list of connection names + */ + static QStringList connectionList(); + + private: /** diff --git a/src/providers/mssql/qgsmssqldataitems.cpp b/src/providers/mssql/qgsmssqldataitems.cpp index 3b35348f688a..6ca90efd734d 100644 --- a/src/providers/mssql/qgsmssqldataitems.cpp +++ b/src/providers/mssql/qgsmssqldataitems.cpp @@ -96,7 +96,7 @@ void QgsMssqlConnectionItem::stop() void QgsMssqlConnectionItem::refresh() { - QgsDebugMsg( "mPath = " + mPath ); + QgsDebugMsgLevel( "mPath = " + mPath, 3 ); stop(); // read up the schemas and layers from database @@ -514,7 +514,7 @@ QString QgsMssqlLayerItem::createUri() mDisableInvalidGeometryHandling = QgsMssqlConnection::isInvalidGeometryHandlingDisabled( connItem->name() ); uri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), mDisableInvalidGeometryHandling ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); - QgsDebugMsg( QStringLiteral( "layer uri: %1" ).arg( uri.uri() ) ); + QgsDebugMsgLevel( QStringLiteral( "layer uri: %1" ).arg( uri.uri() ), 3 ); return uri.uri(); } @@ -658,3 +658,4 @@ bool QgsMssqlSchemaItem::layerCollection() const { return true; } + diff --git a/src/providers/mssql/qgsmssqlgeomcolumntypethread.cpp b/src/providers/mssql/qgsmssqlgeomcolumntypethread.cpp index 1c24885db85b..c03c12ce27b6 100644 --- a/src/providers/mssql/qgsmssqlgeomcolumntypethread.cpp +++ b/src/providers/mssql/qgsmssqlgeomcolumntypethread.cpp @@ -55,21 +55,20 @@ void QgsMssqlGeomColumnTypeThread::run() if ( !mStopped ) { - QString table; - table = QStringLiteral( "%1[%2]" ) - .arg( layerProperty.schemaName.isEmpty() ? QString() : QStringLiteral( "[%1]." ).arg( layerProperty.schemaName ), - layerProperty.tableName ); - - QString query = QString( "SELECT %3" - " UPPER([%1].STGeometryType())," - " [%1].STSrid" - " FROM %2" - " WHERE [%1] IS NOT NULL %4" - " GROUP BY [%1].STGeometryType(), [%1].STSrid" ) - .arg( layerProperty.geometryColName, - table, - mUseEstimatedMetadata ? "TOP 1" : "", - layerProperty.sql.isEmpty() ? QString() : QStringLiteral( " AND %1" ).arg( layerProperty.sql ) ); + const QString table = QStringLiteral( "%1[%2]" ) + .arg( layerProperty.schemaName.isEmpty() ? QString() : QStringLiteral( "[%1]." ).arg( layerProperty.schemaName ), + layerProperty.tableName ); + + const QString query = QStringLiteral( "SELECT %3" + " UPPER([%1].STGeometryType())," + " [%1].STSrid" + " FROM %2" + " WHERE [%1] IS NOT NULL %4" + " GROUP BY [%1].STGeometryType(), [%1].STSrid" ) + .arg( layerProperty.geometryColName, + table, + mUseEstimatedMetadata ? "TOP 1" : "", + layerProperty.sql.isEmpty() ? QString() : QStringLiteral( " AND %1" ).arg( layerProperty.sql ) ); // issue the sql query QSqlDatabase db = QgsMssqlConnection::getDatabase( mService, mHost, mDatabase, mUsername, mPassword ); diff --git a/src/providers/mssql/qgsmssqlprovider.cpp b/src/providers/mssql/qgsmssqlprovider.cpp index 9d5c2c30794f..4d0322ab139b 100644 --- a/src/providers/mssql/qgsmssqlprovider.cpp +++ b/src/providers/mssql/qgsmssqlprovider.cpp @@ -17,6 +17,7 @@ #include "qgsmssqlprovider.h" #include "qgsmssqlconnection.h" +#include "qgsmssqlproviderconnection.h" #include #include @@ -490,6 +491,11 @@ QString QgsMssqlProvider::quotedValue( const QVariant &value ) } } +QString QgsMssqlProvider::quotedIdentifier( const QString &value ) +{ + return QStringLiteral( "[%1]" ).arg( value ); +} + QString QgsMssqlProvider::defaultValueClause( int fieldId ) const { return mDefaultValues.value( fieldId, QString() ); @@ -2050,6 +2056,31 @@ QList QgsMssqlProviderMetadata::dataItemProviders() const return providers; } +QMap QgsMssqlProviderMetadata::connections( bool cached ) +{ + return connectionsProtected( cached ); +} + +QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &name ) +{ + return new QgsMssqlProviderConnection( name ); +} + +QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration ) +{ + return new QgsMssqlProviderConnection( uri, configuration ); +} + +void QgsMssqlProviderMetadata::deleteConnection( const QString &name ) +{ + deleteConnectionProtected( name ); +} + +void QgsMssqlProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name ) +{ + saveConnectionProtected( conn, name ); +} + QgsVectorLayerExporter::ExportError QgsMssqlProviderMetadata::createEmptyLayer( const QString &uri, const QgsFields &fields, @@ -2389,6 +2420,130 @@ QString QgsMssqlProviderMetadata::getStyleById( const QString &uri, QString styl return style; } +QVariantMap QgsMssqlProviderMetadata::decodeUri( const QString &uri ) +{ + const QgsDataSourceUri dsUri { uri }; + QVariantMap uriParts; + + if ( ! dsUri.database().isEmpty() ) + uriParts[ QStringLiteral( "dbname" ) ] = dsUri.database(); + if ( ! dsUri.host().isEmpty() ) + uriParts[ QStringLiteral( "host" ) ] = dsUri.host(); + if ( ! dsUri.port().isEmpty() ) + uriParts[ QStringLiteral( "port" ) ] = dsUri.port(); + if ( ! dsUri.service().isEmpty() ) + uriParts[ QStringLiteral( "service" ) ] = dsUri.service(); + if ( ! dsUri.username().isEmpty() ) + uriParts[ QStringLiteral( "username" ) ] = dsUri.username(); + if ( ! dsUri.password().isEmpty() ) + uriParts[ QStringLiteral( "password" ) ] = dsUri.password(); + + // Supported? + //if ( ! dsUri.authConfigId().isEmpty() ) + // uriParts[ QStringLiteral( "authcfg" ) ] = dsUri.authConfigId(); + + if ( dsUri.wkbType() != QgsWkbTypes::Type::Unknown ) + uriParts[ QStringLiteral( "type" ) ] = dsUri.wkbType(); + + // Supported? + // uriParts[ QStringLiteral( "selectatid" ) ] = dsUri.selectAtIdDisabled(); + + if ( ! dsUri.table().isEmpty() ) + uriParts[ QStringLiteral( "table" ) ] = dsUri.table(); + if ( ! dsUri.schema().isEmpty() ) + uriParts[ QStringLiteral( "schema" ) ] = dsUri.schema(); + if ( ! dsUri.keyColumn().isEmpty() ) + uriParts[ QStringLiteral( "key" ) ] = dsUri.keyColumn(); + if ( ! dsUri.srid().isEmpty() ) + uriParts[ QStringLiteral( "srid" ) ] = dsUri.srid(); + + uriParts[ QStringLiteral( "estimatedmetadata" ) ] = dsUri.useEstimatedMetadata(); + + // is this supported? + // uriParts[ QStringLiteral( "sslmode" ) ] = dsUri.sslMode(); + + if ( ! dsUri.sql().isEmpty() ) + uriParts[ QStringLiteral( "sql" ) ] = dsUri.sql(); + if ( ! dsUri.geometryColumn().isEmpty() ) + uriParts[ QStringLiteral( "geometrycolumn" ) ] = dsUri.geometryColumn(); + + // From configuration + static const QStringList configurationParameters + { + QStringLiteral( "geometryColumnsOnly" ), + QStringLiteral( "allowGeometrylessTables" ), + QStringLiteral( "saveUsername" ), + QStringLiteral( "savePassword" ), + QStringLiteral( "estimatedMetadata" ), + QStringLiteral( "disableInvalidGeometryHandling" ), + }; + + for ( const auto &configParam : configurationParameters ) + { + if ( dsUri.hasParam( configParam ) ) + { + uriParts[ configParam ] = dsUri.param( configParam ); + } + } + + return uriParts; +} + +QString QgsMssqlProviderMetadata::encodeUri( const QVariantMap &parts ) +{ + QgsDataSourceUri dsUri; + if ( parts.contains( QStringLiteral( "dbname" ) ) ) + dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() ); + // Also accepts "database" + if ( parts.contains( QStringLiteral( "database" ) ) ) + dsUri.setDatabase( parts.value( QStringLiteral( "database" ) ).toString() ); + // Supported? + //if ( parts.contains( QStringLiteral( "port" ) ) ) + // dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() ); + if ( parts.contains( QStringLiteral( "host" ) ) ) + dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() ); + if ( parts.contains( QStringLiteral( "service" ) ) ) + dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() ); + if ( parts.contains( QStringLiteral( "username" ) ) ) + dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() ); + if ( parts.contains( QStringLiteral( "password" ) ) ) + dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() ); + // Supported? + //if ( parts.contains( QStringLiteral( "authcfg" ) ) ) + // dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() ); + if ( parts.contains( QStringLiteral( "type" ) ) ) + dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast( parts.value( QStringLiteral( "type" ) ).toInt() ) ) ); + // Supported? + //if ( parts.contains( QStringLiteral( "selectatid" ) ) ) + // dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() ); + if ( parts.contains( QStringLiteral( "table" ) ) ) + dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() ); + if ( parts.contains( QStringLiteral( "schema" ) ) ) + dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() ); + if ( parts.contains( QStringLiteral( "key" ) ) ) + dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() ); + if ( parts.contains( QStringLiteral( "srid" ) ) ) + dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() ); + if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) ) + dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() ); + // Supported? + //if ( parts.contains( QStringLiteral( "sslmode" ) ) ) + // dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast( parts.value( QStringLiteral( "sslmode" ) ).toInt( ) ) ) ); + if ( parts.contains( QStringLiteral( "sql" ) ) ) + dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() ); + // Supported? + //if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) ) + // dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() ); + if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) ) + dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() ); + if ( parts.contains( QStringLiteral( "disableInvalidGeometryHandling" ) ) ) + dsUri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), parts.value( QStringLiteral( "disableInvalidGeometryHandling" ) ).toString() ); + if ( parts.contains( QStringLiteral( "allowGeometrylessTables" ) ) ) + dsUri.setParam( QStringLiteral( "allowGeometrylessTables" ), parts.value( QStringLiteral( "allowGeometrylessTables" ) ).toString() ); + if ( parts.contains( QStringLiteral( "geometryColumnsOnly" ) ) ) + dsUri.setParam( QStringLiteral( "geometryColumnsOnly" ), parts.value( QStringLiteral( "geometryColumnsOnly" ) ).toString() ); + return dsUri.uri(); +} QGISEXTERN QgsProviderMetadata *providerMetadataFactory() { diff --git a/src/providers/mssql/qgsmssqlprovider.h b/src/providers/mssql/qgsmssqlprovider.h index 9984043d3e8d..12c4cf8ea381 100644 --- a/src/providers/mssql/qgsmssqlprovider.h +++ b/src/providers/mssql/qgsmssqlprovider.h @@ -25,6 +25,7 @@ #include #include +#include #include #include #include @@ -128,6 +129,7 @@ class QgsMssqlProvider final: public QgsVectorDataProvider //! Convert values to quoted values for database work * static QString quotedValue( const QVariant &value ); + static QString quotedIdentifier( const QString &value ); QString defaultValueClause( int fieldId ) const override; @@ -249,6 +251,20 @@ class QgsMssqlProviderMetadata final: public QgsProviderMetadata const QMap *options ) override; QgsMssqlProvider *createProvider( const QString &uri, const QgsDataProvider::ProviderOptions &options ) override; virtual QList< QgsDataItemProvider * > dataItemProviders() const override; + + // Connections API + QMap connections( bool cached = true ) override; + QgsAbstractProviderConnection *createConnection( const QString &name ) override; + QgsAbstractProviderConnection *createConnection( const QString &uri, const QVariantMap &configuration ) override; + void deleteConnection( const QString &name ) override; + void saveConnection( const QgsAbstractProviderConnection *createConnection, const QString &name ) override; + + // Data source URI API + QVariantMap decodeUri( const QString &uri ) override; + QString encodeUri( const QVariantMap &parts ) override; + }; + + #endif // QGSMSSQLPROVIDER_H diff --git a/src/providers/mssql/qgsmssqlproviderconnection.cpp b/src/providers/mssql/qgsmssqlproviderconnection.cpp new file mode 100644 index 000000000000..57edbfc3142c --- /dev/null +++ b/src/providers/mssql/qgsmssqlproviderconnection.cpp @@ -0,0 +1,485 @@ +/*************************************************************************** + qgsmssqlproviderconnection.cpp - QgsMssqlProviderConnection + + --------------------- + begin : 10.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 + +#include "qgsmssqlproviderconnection.h" +#include "qgsmssqlconnection.h" +#include "qgssettings.h" +#include "qgsmssqlprovider.h" +#include "qgsexception.h" +#include "qgsapplication.h" +#include "qgsmessagelog.h" + + +const QStringList QgsMssqlProviderConnection::EXTRA_CONNECTION_PARAMETERS +{ + QStringLiteral( "geometryColumnsOnly" ), + QStringLiteral( "allowGeometrylessTables" ), + QStringLiteral( "disableInvalidGeometryHandling" ), + QStringLiteral( "saveUsername" ), + QStringLiteral( "savePassword" ), +}; + +QgsMssqlProviderConnection::QgsMssqlProviderConnection( const QString &name ) + : QgsAbstractDatabaseProviderConnection( name ) +{ + // Remove the sql and table empty parts + setUri( QgsMssqlConnection::connUri( name ).uri() ); + setDefaultCapabilities(); +} + +QgsMssqlProviderConnection::QgsMssqlProviderConnection( const QString &uri, const QVariantMap &configuration ): + QgsAbstractDatabaseProviderConnection( QString(), configuration ) +{ + // Additional connection information + const QgsDataSourceUri inputUri( uri ); + QgsDataSourceUri currentUri { QgsDataSourceUri( uri ).connectionInfo( false ) }; + + if ( inputUri.hasParam( QStringLiteral( "estimatedMetadata" ) ) ) + { + currentUri.setUseEstimatedMetadata( inputUri.param( QStringLiteral( "estimatedMetadata" ) ) == QStringLiteral( "true" ) + || inputUri.param( QStringLiteral( "estimatedMetadata" ) ) == '1' ); + } + + for ( const auto ¶m : EXTRA_CONNECTION_PARAMETERS ) + { + if ( inputUri.hasParam( param ) ) + { + currentUri.setParam( param, inputUri.param( param ) ); + } + } + + setUri( currentUri.uri() ); + setDefaultCapabilities(); +} + +void QgsMssqlProviderConnection::setDefaultCapabilities() +{ + // TODO: we might check at this point if the user actually has the privileges and return + // properly filtered capabilities instead of all of them + mCapabilities = + { + Capability::DropVectorTable, + Capability::CreateVectorTable, + Capability::DropSchema, + Capability::CreateSchema, + Capability::ExecuteSql, + Capability::Tables, + Capability::Schemas, + Capability::Spatial, + Capability::TableExists + }; +} + +void QgsMssqlProviderConnection::dropTablePrivate( const QString &schema, const QString &name ) const +{ + // Drop all constraints and delete the table + const QString sql { QStringLiteral( R"raw( + DECLARE @database nvarchar(50) + DECLARE @table nvarchar(50) + DECLARE @schema nvarchar(50) + + set @database = N%1 + set @table = N%2 + set @schema = N%3 + + DECLARE @sql nvarchar(255) + WHILE EXISTS(select * from INFORMATION_SCHEMA.TABLE_CONSTRAINTS where constraint_catalog = @database and table_name = @table AND table_schema = @schema ) + BEGIN + select @sql = 'ALTER TABLE ' + @table + ' DROP CONSTRAINT ' + CONSTRAINT_NAME + from INFORMATION_SCHEMA.TABLE_CONSTRAINTS + where constraint_catalog = @database and + table_name = @table and table_schema = @schema + exec sp_executesql @sql + END + + DROP TABLE %5.%4 + + if exists (select * from INFORMATION_SCHEMA.TABLES where TABLE_NAME = 'geometry_columns' ) + DELETE FROM geometry_columns WHERE f_table_schema = @schema AND f_table_name = @table + + )raw" ) + .arg( QgsMssqlProvider::quotedValue( QStringLiteral( "master" ) ), // in my testing docker, it is 'master' instead of QgsMssqlProvider::quotedValue( QgsDataSourceUri( uri() ).database() ), + QgsMssqlProvider::quotedValue( name ), + QgsMssqlProvider::quotedValue( schema ), + QgsMssqlProvider::quotedIdentifier( name ), + QgsMssqlProvider::quotedIdentifier( schema ) ) }; + + executeSqlPrivate( sql ); +} + +void QgsMssqlProviderConnection::createVectorTable( const QString &schema, + const QString &name, + const QgsFields &fields, + QgsWkbTypes::Type wkbType, + const QgsCoordinateReferenceSystem &srs, + bool overwrite, + const QMap *options ) const +{ + + checkCapability( Capability::CreateVectorTable ); + + QgsDataSourceUri newUri { uri() }; + newUri.setSchema( schema ); + newUri.setTable( name ); + // Set geometry column if it's not aspatial + if ( wkbType != QgsWkbTypes::Type::Unknown && wkbType != QgsWkbTypes::Type::NoGeometry ) + { + newUri.setGeometryColumn( options->value( QStringLiteral( "geometryColumn" ), QStringLiteral( "geom" ) ).toString() ); + } + QMap map; + QString errCause; + QgsVectorLayerExporter::ExportError errCode = QgsMssqlProvider::createEmptyLayer( + newUri.uri(), + fields, + wkbType, + srs, + overwrite, + &map, + &errCause, + options + ); + if ( errCode != QgsVectorLayerExporter::ExportError::NoError ) + { + throw QgsProviderConnectionException( QObject::tr( "An error occurred while creating the vector layer: %1" ).arg( errCause ) ); + } +} + +QString QgsMssqlProviderConnection::tableUri( const QString &schema, const QString &name ) const +{ + const auto tableInfo { table( schema, name ) }; + QgsDataSourceUri dsUri( uri() ); + dsUri.setTable( name ); + dsUri.setSchema( schema ); + return dsUri.uri( false ); +} + +void QgsMssqlProviderConnection::dropVectorTable( const QString &schema, const QString &name ) const +{ + checkCapability( Capability::DropVectorTable ); + dropTablePrivate( schema, name ); +} + + +void QgsMssqlProviderConnection::createSchema( const QString &schemaName ) const +{ + checkCapability( Capability::CreateSchema ); + executeSqlPrivate( QStringLiteral( "CREATE SCHEMA %1" ) + .arg( QgsMssqlProvider::quotedIdentifier( schemaName ) ) ); + +} + +void QgsMssqlProviderConnection::dropSchema( const QString &schemaName, bool force ) const +{ + checkCapability( Capability::DropSchema ); + // We need to delete all tables first! + // Note: there might be more linked objects to drop but MSSQL sucks so let's stick to the + // easiest case. + if ( force ) + { + const auto schemaTables { tables( schemaName ) }; + for ( const auto &t : schemaTables ) + { + dropTablePrivate( schemaName, t.tableName() ); + } + } + executeSqlPrivate( QStringLiteral( "DROP SCHEMA %1" ) + .arg( QgsMssqlProvider::quotedIdentifier( schemaName ) ) ); +} + +QList QgsMssqlProviderConnection::executeSql( const QString &sql ) const +{ + checkCapability( Capability::ExecuteSql ); + return executeSqlPrivate( sql ); +} + +QList QgsMssqlProviderConnection::executeSqlPrivate( const QString &sql, bool resolveTypes ) const +{ + const QgsDataSourceUri dsUri { uri() }; + QList results; + // connect to database + QSqlDatabase db = QgsMssqlConnection::getDatabase( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() ); + + if ( !QgsMssqlConnection::openDatabase( db ) ) + { + throw QgsProviderConnectionException( QObject::tr( "Connection to %1 failed: %2" ) + .arg( uri() ) + .arg( db.lastError().text() ) ); + } + else + { + //qDebug() << "MSSQL QUERY:" << sql; + QSqlQuery q = QSqlQuery( db ); + q.setForwardOnly( true ); + + if ( ! q.exec( sql ) ) + { + const QString errorMessage { q.lastError().text() }; + throw QgsProviderConnectionException( QObject::tr( "SQL error: %1 \n %2" ) + .arg( sql ) + .arg( errorMessage ) ); + + } + + if ( q.isActive() ) + { + const QSqlRecord rec { q.record() }; + const int numCols { rec.count() }; + while ( q.next() ) + { + QVariantList row; + for ( int col = 0; col < numCols; ++col ) + { + if ( resolveTypes ) + { + row.push_back( q.value( col ) ); + } + else + { + row.push_back( q.value( col ).toString() ); + } + } + results.push_back( row ); + } + } + + } + return results; +} + +QList QgsMssqlProviderConnection::tables( const QString &schema, const TableFlags &flags ) const +{ + checkCapability( Capability::Tables ); + QList tables; + + const QgsDataSourceUri dsUri { uri() }; + + // Defaults to false + const bool useGeometryColumnsOnly { dsUri.hasParam( QStringLiteral( "geometryColumnsOnly" ) ) + &&( dsUri.param( QStringLiteral( "geometryColumnsOnly" ) ) == QStringLiteral( "true" ) + || dsUri.param( QStringLiteral( "geometryColumnsOnly" ) ) == '1' ) }; + + // Defaults to true + const bool useEstimatedMetadata { ! dsUri.hasParam( QStringLiteral( "estimatedMetadata" ) ) + || ( dsUri.param( QStringLiteral( "estimatedMetadata" ) ) == QStringLiteral( "true" ) + || dsUri.param( QStringLiteral( "estimatedMetadata" ) ) == '1' ) }; + + // Defaults to true because we want to list all tables if flags are not set + bool allowGeometrylessTables; + if ( flags == 0 ) + { + allowGeometrylessTables = true; + } + else + { + allowGeometrylessTables = flags.testFlag( TableFlag::Aspatial ); + } + + QString query { QStringLiteral( "SELECT " ) }; + + if ( useGeometryColumnsOnly ) + { + query += QStringLiteral( "f_table_schema, f_table_name, f_geometry_column, srid, geometry_type, 0 FROM geometry_columns WHERE f_table_schema = N%1" ) + .arg( QgsMssqlProvider::quotedValue( schema ) ); + } + else + { + query += QStringLiteral( R"raw( + sys.schemas.name, sys.objects.name, sys.columns.name, null, 'GEOMETRY', CASE WHEN sys.objects.type = 'V' THEN 1 ELSE 0 END + FROM sys.columns + JOIN sys.types + ON sys.columns.system_type_id = sys.types.system_type_id AND sys.columns.user_type_id = sys.types.user_type_id + JOIN sys.objects + ON sys.objects.object_id = sys.columns.object_id + JOIN sys.schemas + ON sys.objects.schema_id = sys.schemas.schema_id + WHERE + sys.schemas.name = N%1 + AND (sys.types.name = 'geometry' OR sys.types.name = 'geography') + AND (sys.objects.type = 'U' OR sys.objects.type = 'V') + )raw" ) + .arg( QgsMssqlProvider::quotedValue( schema ) ); + } + + if ( allowGeometrylessTables ) + { + query += QStringLiteral( R"raw( + UNION ALL SELECT sys.schemas.name, sys.objects.name, null, null, 'NONE', + CASE WHEN sys.objects.type = 'V' THEN 1 ELSE 0 END + FROM sys.objects + JOIN sys.schemas + ON sys.objects.schema_id = sys.schemas.schema_id + WHERE + sys.schemas.name = N%1 + AND NOT EXISTS + (SELECT * + FROM sys.columns sc1 JOIN sys.types ON sc1.system_type_id = sys.types.system_type_id + WHERE (sys.types.name = 'geometry' OR sys.types.name = 'geography') + AND sys.objects.object_id = sc1.object_id ) + AND (sys.objects.type = 'U' OR sys.objects.type = 'V') + )raw" ) + .arg( QgsMssqlProvider::quotedValue( schema ) ); + } + + const QList results { executeSqlPrivate( query, false ) }; + for ( const auto &row : results ) + { + Q_ASSERT( row.count( ) == 6 ); + QgsMssqlProviderConnection::TableProperty table; + table.setSchema( row[0].toString() ); + table.setTableName( row[1].toString() ); + table.setGeometryColumn( row[2].toString() ); + //const QVariant srid { row[3] }; + //const QVariant type { row[4] }; // GEOMETRY|GEOGRAPHY + if ( row[5].toBool() ) + table.setFlag( QgsMssqlProviderConnection::TableFlag::View ); + + int geomColCount { 0 }; + + if ( ! table.geometryColumn().isEmpty() ) + { + // Fetch geom cols + const QString geomColSql + { + QStringLiteral( R"raw( + SELECT %4 UPPER( %1.STGeometryType()), %1.STSrid + FROM %2.%3 + WHERE %1 IS NOT NULL + GROUP BY %1.STGeometryType(), %1.STSrid + )raw" ) + .arg( QgsMssqlProvider::quotedIdentifier( table.geometryColumn() ), + QgsMssqlProvider::quotedIdentifier( table.schema() ), + QgsMssqlProvider::quotedIdentifier( table.tableName() ), + useEstimatedMetadata ? "TOP 1" : "" ) }; + + // This may fail for invalid geometries + try + { + const auto geomColResults { executeSqlPrivate( geomColSql ) }; + for ( const auto &row : geomColResults ) + { + table.addGeometryColumnType( QgsWkbTypes::parseType( row[0].toString() ), + QgsCoordinateReferenceSystem::fromEpsgId( row[1].toLongLong( ) ) ); + ++geomColCount; + } + } + catch ( QgsProviderConnectionException &ex ) + { + QgsMessageLog::logMessage( QObject::tr( "Error retrieving geometry type for '%1' on table %2.%3:\n%4" ) + .arg( table.geometryColumn(), + QgsMssqlProvider::quotedIdentifier( table.schema() ), + QgsMssqlProvider::quotedIdentifier( table.tableName() ), + ex.what() ), + QStringLiteral( "MSSQL" ), Qgis::MessageLevel::Warning ); + } + + } + else + { + // Add an invalid column + table.addGeometryColumnType( QgsWkbTypes::Type::NoGeometry, + QgsCoordinateReferenceSystem() ); + table.setFlag( QgsMssqlProviderConnection::TableFlag::Aspatial ); + } + + table.setGeometryColumnCount( geomColCount ); + tables.push_back( table ); + } + return tables; +} + +QStringList QgsMssqlProviderConnection::schemas( ) const +{ + checkCapability( Capability::Schemas ); + QStringList schemas; + const QgsDataSourceUri dsUri { uri() }; + const QString sql { QStringLiteral( + R"raw( + SELECT s.name AS schema_name, + s.schema_id, + u.name AS schema_owner + FROM sys.schemas s + INNER JOIN sys.sysusers u + ON u.uid = s.principal_id + WHERE u.issqluser = 1 + AND u.name NOT IN ('sys', 'guest', 'INFORMATION_SCHEMA') + )raw" )}; + const QList result { executeSqlPrivate( sql, false ) }; + for ( const auto &row : result ) + { + if ( row.size() > 0 ) + schemas.push_back( row.at( 0 ).toString() ); + } + return schemas; +} + + +void QgsMssqlProviderConnection::store( const QString &name ) const +{ + // TODO: move this to class configuration? + const QString baseKey = QStringLiteral( "/MSSQL/connections/" ); + // delete the original entry first + remove( name ); + + QgsSettings settings; + settings.beginGroup( baseKey ); + settings.beginGroup( name ); + + // From URI + const QgsDataSourceUri dsUri { uri() }; + settings.setValue( "service", dsUri.service() ); + settings.setValue( "host", dsUri.host() ); + settings.setValue( "database", dsUri.database() ); + settings.setValue( "username", dsUri.username() ); + settings.setValue( "password", dsUri.password() ); + settings.setValue( "estimatedMetadata", dsUri.useEstimatedMetadata() ); + + for ( const auto ¶m : EXTRA_CONNECTION_PARAMETERS ) + { + if ( dsUri.hasParam( param ) ) + { + settings.setValue( param, dsUri.param( param ) == QStringLiteral( "true" ) + || dsUri.param( param ) == '1' ); + + } + } + + // From configuration + const auto config { configuration().keys() }; + for ( const auto &p : config ) + { + settings.setValue( p, configuration().value( p ) == QStringLiteral( "true" ) + || configuration().value( p ) == '1' ); + } + + settings.endGroup(); + settings.endGroup(); +} + +void QgsMssqlProviderConnection::remove( const QString &name ) const +{ + const QString baseKey = QStringLiteral( "/MSSQL/connections/" ); + QgsSettings settings; + settings.remove( baseKey + name ); +} + +QIcon QgsMssqlProviderConnection::icon() const +{ + return QgsApplication::getThemeIcon( QStringLiteral( "mIconMssql.svg" ) ); +} + diff --git a/src/providers/mssql/qgsmssqlproviderconnection.h b/src/providers/mssql/qgsmssqlproviderconnection.h new file mode 100644 index 000000000000..ff1f18d3750d --- /dev/null +++ b/src/providers/mssql/qgsmssqlproviderconnection.h @@ -0,0 +1,62 @@ +/*************************************************************************** + qgsmssqlproviderconnection.h - QgsMssqlProviderConnection + + --------------------- + begin : 10.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSMSSQLPROVIDERCONNECTION_H +#define QGSMSSQLPROVIDERCONNECTION_H +#include "qgsabstractdatabaseproviderconnection.h" + + +class QgsMssqlProviderConnection : public QgsAbstractDatabaseProviderConnection + +{ + public: + + QgsMssqlProviderConnection( const QString &name ); + QgsMssqlProviderConnection( const QString &uri, const QVariantMap &configuration ); + + // QgsAbstractProviderConnection interface + + public: + + void createVectorTable( const QString &schema, + const QString &name, + const QgsFields &fields, + QgsWkbTypes::Type wkbType, + const QgsCoordinateReferenceSystem &srs, bool overwrite, + const QMap *options ) const override; + + QString tableUri( const QString &schema, const QString &name ) const override; + void dropVectorTable( const QString &schema, const QString &name ) const override; + void createSchema( const QString &name ) const override; + void dropSchema( const QString &name, bool force = false ) const override; + QList executeSql( const QString &sql ) const override; + QList tables( const QString &schema, + const TableFlags &flags = nullptr ) const override; + QStringList schemas( ) const override; + void store( const QString &name ) const override; + void remove( const QString &name ) const override; + QIcon icon() const override; + + private: + + QList executeSqlPrivate( const QString &sql, bool resolveTypes = true ) const; + void setDefaultCapabilities(); + void dropTablePrivate( const QString &schema, const QString &name ) const; + void renameTablePrivate( const QString &schema, const QString &name, const QString &newName ) const; + + static const QStringList EXTRA_CONNECTION_PARAMETERS; +}; + +#endif // QGSMSSQLPROVIDERCONNECTION_H diff --git a/src/providers/oracle/ocispatial/cmake/FindOCI.cmake b/src/providers/oracle/ocispatial/cmake/FindOCI.cmake index c6e5991aeb7e..5c8bf2d0db76 100644 --- a/src/providers/oracle/ocispatial/cmake/FindOCI.cmake +++ b/src/providers/oracle/ocispatial/cmake/FindOCI.cmake @@ -15,6 +15,7 @@ FIND_PATH(OCI_INCLUDE_DIR oci.h ${ORACLE_INCLUDEDIR} $ENV{OSGEO4W_ROOT}/include $ENV{ORACLE_HOME}/rdbms/public + $ENV{ORACLE_HOME}/include ) FIND_LIBRARY(OCI_LIBRARY clntsh oci diff --git a/src/providers/postgres/qgspgnewconnection.cpp b/src/providers/postgres/qgspgnewconnection.cpp index d787e8163319..fcaa53b510d1 100644 --- a/src/providers/postgres/qgspgnewconnection.cpp +++ b/src/providers/postgres/qgspgnewconnection.cpp @@ -211,6 +211,7 @@ void QgsPgNewConnection::testConnection() if ( conn->pgVersion() < 90500 ) { cb_projectsInDatabase->setEnabled( false ); + cb_projectsInDatabase->setChecked( false ); cb_projectsInDatabase->setToolTip( tr( "Saving projects in databases not available for PostgreSQL databases earlier than 9.5" ) ); } else diff --git a/src/providers/postgres/qgspgtablemodel.cpp b/src/providers/postgres/qgspgtablemodel.cpp index c38f2129261c..af2bd84d6cc4 100644 --- a/src/providers/postgres/qgspgtablemodel.cpp +++ b/src/providers/postgres/qgspgtablemodel.cpp @@ -468,7 +468,7 @@ QString QgsPgTableModel::layerURI( const QModelIndex &index, const QString &conn uri.setWkbType( wkbType ); uri.setSrid( srid ); uri.disableSelectAtId( !selectAtId ); - uri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), checkPrimaryKeyUnicity ? QLatin1Literal( "1" ) : QLatin1Literal( "0" ) ); + uri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), checkPrimaryKeyUnicity ? QLatin1String( "1" ) : QLatin1String( "0" ) ); QgsDebugMsg( QStringLiteral( "returning uri %1" ).arg( uri.uri( false ) ) ); return uri.uri( false ); diff --git a/src/providers/postgres/qgspostgresconn.cpp b/src/providers/postgres/qgspostgresconn.cpp index 20b5702877eb..4500baa6747f 100644 --- a/src/providers/postgres/qgspostgresconn.cpp +++ b/src/providers/postgres/qgspostgresconn.cpp @@ -168,7 +168,7 @@ QgsPostgresConn *QgsPostgresConn::connectDb( const QString &conninfo, bool reado if ( connections.contains( conninfo ) ) { - QgsDebugMsg( QStringLiteral( "Using cached connection for %1" ).arg( conninfo ) ); + QgsDebugMsgLevel( QStringLiteral( "Using cached connection for %1" ).arg( conninfo ), 2 ); connections[conninfo]->mRef++; return connections[conninfo]; } @@ -219,7 +219,7 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s , mLock( QMutex::Recursive ) { - QgsDebugMsg( QStringLiteral( "New PostgreSQL connection for " ) + conninfo ); + QgsDebugMsgLevel( QStringLiteral( "New PostgreSQL connection for " ) + conninfo, 2 ); // expand connectionInfo QgsDataSourceUri uri( conninfo ); @@ -299,7 +299,7 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s if ( !password.isEmpty() ) uri.setPassword( password ); - QgsDebugMsg( "Connecting to " + uri.connectionInfo( false ) ); + QgsDebugMsgLevel( "Connecting to " + uri.connectionInfo( false ), 2 ); QString connectString = uri.connectionInfo(); addDefaultTimeout( connectString ); mConn = PQconnectdb( connectString.toLocal8Bit() ); @@ -321,11 +321,11 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s } //set client encoding to Unicode because QString uses UTF-8 anyway - QgsDebugMsg( QStringLiteral( "setting client encoding to UNICODE" ) ); + QgsDebugMsgLevel( QStringLiteral( "setting client encoding to UNICODE" ), 2 ); int errcode = PQsetClientEncoding( mConn, QStringLiteral( "UNICODE" ).toLocal8Bit() ); if ( errcode == 0 ) { - QgsDebugMsg( QStringLiteral( "encoding successfully set" ) ); + QgsDebugMsgLevel( QStringLiteral( "encoding successfully set" ), 2 ); } else if ( errcode == -1 ) { @@ -336,7 +336,7 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s QgsMessageLog::logMessage( tr( "undefined return value from encoding setting" ), tr( "PostGIS" ) ); } - QgsDebugMsg( QStringLiteral( "Connection to the database was successful" ) ); + QgsDebugMsgLevel( QStringLiteral( "Connection to the database was successful" ), 2 ); deduceEndian(); @@ -345,7 +345,7 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s { /* Check to see if we have GEOS support and if not, warn the user about the problems they will see :) */ - QgsDebugMsg( QStringLiteral( "Checking for GEOS support" ) ); + QgsDebugMsgLevel( QStringLiteral( "Checking for GEOS support" ), 3 ); if ( !hasGEOS() ) { @@ -353,7 +353,7 @@ QgsPostgresConn::QgsPostgresConn( const QString &conninfo, bool readOnly, bool s } else { - QgsDebugMsg( QStringLiteral( "GEOS support available!" ) ); + QgsDebugMsgLevel( QStringLiteral( "GEOS support available!" ), 3 ); } } @@ -434,7 +434,7 @@ void QgsPostgresConn::addColumnInfo( QgsPostgresLayerProperty &layerProperty, co .arg( supportedSpatialTypes().join( ',' ) ) .arg( quotedIdentifier( schemaName ), quotedIdentifier( viewName ) ); - QgsDebugMsg( "getting column info: " + sql ); + QgsDebugMsgLevel( "getting column info: " + sql, 2 ); QgsPostgresResult colRes( PQexec( sql ) ); layerProperty.pkCols.clear(); @@ -446,7 +446,6 @@ void QgsPostgresConn::addColumnInfo( QgsPostgresLayerProperty &layerProperty, co { if ( fetchPkCandidates ) { - //QgsDebugMsg( colRes.PQgetvalue( i, 0 ) ); layerProperty.pkCols << colRes.PQgetvalue( i, 0 ); } @@ -471,8 +470,6 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP QgsPostgresResult result; QString query; - //QgsDebugMsg( QStringLiteral( "Entering." ) ); - mLayersSupported.clear(); for ( int i = SctGeometry; i <= SctRaster; ++i ) @@ -592,14 +589,10 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP query += QStringLiteral( " ORDER BY 2,1,3" ); - QgsDebugMsg( "getting table info from layer registries: " + query ); + QgsDebugMsgLevel( "getting table info from layer registries: " + query, 2 ); result = PQexec( query, true ); - if ( result.PQresultStatus() != PGRES_TUPLES_OK ) - { - PQexecNR( QStringLiteral( "COMMIT" ) ); - return false; - } - + // NOTE: we intentionally continue if the query fails + // (for example because PostGIS is not installed) for ( int idx = 0; idx < result.PQntuples(); idx++ ) { QString tableName = result.PQgetvalue( idx, 0 ); @@ -644,13 +637,13 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP } #if 0 - QgsDebugMsg( QStringLiteral( "%1 : %2.%3.%4: %5 %6 %7 %8" ) - .arg( gtableName ) - .arg( schemaName ).arg( tableName ).arg( column ) - .arg( type ) - .arg( srid ) - .arg( relkind ) - .arg( dim ) ); + QgsDebugMsgLevel( QStringLiteral( "%1 : %2.%3.%4: %5 %6 %7 %8" ) + .arg( gtableName ) + .arg( schemaName ).arg( tableName ).arg( column ) + .arg( type ) + .arg( srid ) + .arg( relkind ) + .arg( dim ), 2 ); #endif QgsPostgresLayerProperty layerProperty; @@ -683,7 +676,7 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP if ( isView && layerProperty.pkCols.empty() ) { - //QgsDebugMsg( QStringLiteral( "no key columns found." ) ); + //QgsDebugMsgLevel( QStringLiteral( "no key columns found." ), 2 ); continue; } @@ -743,7 +736,7 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP } } - QgsDebugMsg( "getting spatial table info from pg_catalog: " + sql ); + QgsDebugMsgLevel( "getting spatial table info from pg_catalog: " + sql, 2 ); result = PQexec( sql ); @@ -772,8 +765,6 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP bool isForeignTable = relkind == QLatin1String( "f" ); QString comment = result.PQgetvalue( i, 5 ); // table comment - //QgsDebugMsg( QStringLiteral( "%1.%2.%3: %4" ).arg( schemaName ).arg( tableName ).arg( column ).arg( relkind ) ); - QgsPostgresLayerProperty layerProperty; layerProperty.types = QList() << QgsWkbTypes::Unknown; layerProperty.srids = QList() << std::numeric_limits::min(); @@ -818,7 +809,7 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP if ( isView && layerProperty.pkCols.empty() ) { - //QgsDebugMsg( QStringLiteral( "no key columns found." ) ); + //QgsDebugMsgLevel( QStringLiteral( "no key columns found." ), 2 ); continue; } @@ -857,7 +848,7 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP sql += QStringLiteral( " GROUP BY 1,2,3,4" ); - QgsDebugMsg( "getting non-spatial table info: " + sql ); + QgsDebugMsgLevel( "getting non-spatial table info: " + sql, 2 ); result = PQexec( sql ); @@ -880,8 +871,6 @@ bool QgsPostgresConn::getTableInfo( bool searchGeometryColumnsOnly, bool searchP bool isMaterializedView = relkind == QLatin1String( "m" ); bool isForeignTable = relkind == QLatin1String( "f" ); - //QgsDebugMsg( QStringLiteral( "%1.%2: %3" ).arg( schema ).arg( table ).arg( relkind ) ); - QgsPostgresLayerProperty layerProperty; layerProperty.types = QList() << QgsWkbTypes::NoGeometry; layerProperty.srids = QList() << std::numeric_limits::min(); @@ -947,8 +936,6 @@ bool QgsPostgresConn::supportedLayers( QVector &layers layers = mLayersSupported; - //QgsDebugMsg( QStringLiteral( "Exiting." ) ); - return true; } @@ -1035,7 +1022,7 @@ QString QgsPostgresConn::postgisVersion() const mPostgisVersionInfo = result.PQgetvalue( 0, 0 ); - QgsDebugMsg( "PostGIS version info: " + mPostgisVersionInfo ); + QgsDebugMsgLevel( "PostGIS version info: " + mPostgisVersionInfo, 2 ); QStringList postgisParts = mPostgisVersionInfo.split( ' ', QString::SkipEmptyParts ); @@ -1057,8 +1044,8 @@ QString QgsPostgresConn::postgisVersion() const { result = PQexec( QStringLiteral( "SELECT postgis_geos_version()" ) ); mGeosAvailable = result.PQntuples() == 1 && !result.PQgetisnull( 0, 0 ); - QgsDebugMsg( QStringLiteral( "geos:%1 proj:%2" ) - .arg( mGeosAvailable ? result.PQgetvalue( 0, 0 ) : "none" ) ); + QgsDebugMsgLevel( QStringLiteral( "geos:%1 proj:%2" ) + .arg( mGeosAvailable ? result.PQgetvalue( 0, 0 ) : "none" ), 2 ); } else { @@ -1074,7 +1061,7 @@ QString QgsPostgresConn::postgisVersion() const } // checking for topology support - QgsDebugMsg( QStringLiteral( "Checking for topology support" ) ); + QgsDebugMsgLevel( QStringLiteral( "Checking for topology support" ), 2 ); mTopologyAvailable = false; if ( mPostgisVersionMajor > 1 ) { @@ -1099,18 +1086,18 @@ QString QgsPostgresConn::postgisVersion() const if ( mTopologyAvailable ) { - QgsDebugMsg( QStringLiteral( "Topology support available :)" ) ); + QgsDebugMsgLevel( QStringLiteral( "Topology support available :)" ), 2 ); } else { - QgsDebugMsg( QStringLiteral( "Topology support not available :(" ) ); + QgsDebugMsgLevel( QStringLiteral( "Topology support not available :(" ), 2 ); } mGotPostgisVersion = true; if ( mPostgresqlVersion >= 90000 ) { - QgsDebugMsg( QStringLiteral( "Checking for pointcloud support" ) ); + QgsDebugMsgLevel( QStringLiteral( "Checking for pointcloud support" ), 2 ); result = PQexec( QStringLiteral( R"( SELECT has_table_privilege(c.oid, 'select') @@ -1126,11 +1113,11 @@ WHERE c.relnamespace = n.oid if ( result.PQntuples() >= 1 && result.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) ) { mPointcloudAvailable = true; - QgsDebugMsg( QStringLiteral( "Pointcloud support available!" ) ); + QgsDebugMsgLevel( QStringLiteral( "Pointcloud support available!" ), 2 ); } } - QgsDebugMsg( QStringLiteral( "Checking for raster support" ) ); + QgsDebugMsgLevel( QStringLiteral( "Checking for raster support" ), 2 ); if ( mPostgisVersionMajor >= 2 ) { result = PQexec( QStringLiteral( R"( @@ -1145,7 +1132,7 @@ WHERE c.relnamespace = n.oid if ( result.PQntuples() >= 1 && result.PQgetvalue( 0, 0 ) == QLatin1String( "t" ) ) { mRasterAvailable = true; - QgsDebugMsg( QStringLiteral( "Raster support available!" ) ); + QgsDebugMsgLevel( QStringLiteral( "Raster support available!" ), 2 ); } } @@ -1539,7 +1526,7 @@ qint64 QgsPostgresConn::getBinaryInt( QgsPostgresResult &queryResult, int row, i buf += QStringLiteral( "%1 " ).arg( *( unsigned char * )( p + i ), 0, 16, QLatin1Char( ' ' ) ); } - QgsDebugMsg( QStringLiteral( "int in hex:%1" ).arg( buf ) ); + QgsDebugMsgLevel( QStringLiteral( "int in hex:%1" ).arg( buf ), 2 ); } #endif @@ -1654,12 +1641,12 @@ void QgsPostgresConn::deduceEndian() QgsPostgresResult res( PQexec( QStringLiteral( "select regclass('pg_class')::oid" ) ) ); QString oidValue = res.PQgetvalue( 0, 0 ); - QgsDebugMsg( QStringLiteral( "Creating binary cursor" ) ); + QgsDebugMsgLevel( QStringLiteral( "Creating binary cursor" ), 2 ); // get the same value using a binary cursor openCursor( QStringLiteral( "oidcursor" ), QStringLiteral( "select regclass('pg_class')::oid" ) ); - QgsDebugMsg( QStringLiteral( "Fetching a record and attempting to get check endian-ness" ) ); + QgsDebugMsgLevel( QStringLiteral( "Fetching a record and attempting to get check endian-ness" ), 2 ); res = PQexec( QStringLiteral( "fetch forward 1 from oidcursor" ) ); @@ -1669,8 +1656,8 @@ void QgsPostgresConn::deduceEndian() // get the oid value from the binary cursor qint64 oid = getBinaryInt( res, 0, 0 ); - QgsDebugMsg( QStringLiteral( "Got oid of %1 from the binary cursor" ).arg( oid ) ); - QgsDebugMsg( QStringLiteral( "First oid is %1" ).arg( oidValue ) ); + QgsDebugMsgLevel( QStringLiteral( "Got oid of %1 from the binary cursor" ).arg( oid ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "First oid is %1" ).arg( oidValue ), 2 ); // compare the two oid values to determine if we need to do an endian swap if ( oid != oidValue.toLongLong() ) @@ -1756,7 +1743,7 @@ void QgsPostgresConn::retrieveLayerTypes( QVector &l } } - QgsDebugMsg( "Raster srids query: " + sql ); + QgsDebugMsgLevel( "Raster srids query: " + sql, 2 ); query += sql; } else // vectors @@ -1816,7 +1803,7 @@ void QgsPostgresConn::retrieveLayerTypes( QVector &l sql += " FROM " + table; - QgsDebugMsg( "Geometry types,srids and dims query: " + sql ); + QgsDebugMsgLevel( "Geometry types,srids and dims query: " + sql, 2 ); query += sql; } @@ -2036,6 +2023,26 @@ void QgsPostgresConn::postgisWkbType( QgsWkbTypes::Type wkbType, QString &geomet geometryType = QStringLiteral( "MULTIPOLYGON" ); break; + case QgsWkbTypes::CircularString: + geometryType = QStringLiteral( "CIRCULARSTRING" ); + break; + + case QgsWkbTypes::CompoundCurve: + geometryType = QStringLiteral( "COMPOUNDCURVE" ); + break; + + case QgsWkbTypes::CurvePolygon: + geometryType = QStringLiteral( "CURVEPOLYGON" ); + break; + + case QgsWkbTypes::MultiCurve: + geometryType = QStringLiteral( "MULTICURVE" ); + break; + + case QgsWkbTypes::MultiSurface: + geometryType = QStringLiteral( "MULTISURFACE" ); + break; + case QgsWkbTypes::Unknown: geometryType = QStringLiteral( "GEOMETRY" ); break; @@ -2208,7 +2215,7 @@ void QgsPostgresConn::setSelectedConnection( const QString &name ) QgsDataSourceUri QgsPostgresConn::connUri( const QString &connName ) { - QgsDebugMsg( "theConnName = " + connName ); + QgsDebugMsgLevel( "theConnName = " + connName, 2 ); QgsSettings settings; diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index b45743a1dbf9..3373ca828433 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -97,7 +97,7 @@ QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOpti , mShared( new QgsPostgresSharedData ) { - QgsDebugMsg( QStringLiteral( "URI: %1 " ).arg( uri ) ); + QgsDebugMsgLevel( QStringLiteral( "URI: %1 " ).arg( uri ), 2 ); mUri = QgsDataSourceUri( uri ); @@ -151,12 +151,12 @@ QgsPostgresProvider::QgsPostgresProvider( QString const &uri, const ProviderOpti mUseEstimatedMetadata = mUri.useEstimatedMetadata(); mSelectAtIdDisabled = mUri.selectAtIdDisabled(); - QgsDebugMsg( QStringLiteral( "Connection info is %1" ).arg( mUri.connectionInfo( false ) ) ); - QgsDebugMsg( QStringLiteral( "Geometry column is: %1" ).arg( mGeometryColumn ) ); - QgsDebugMsg( QStringLiteral( "Schema is: %1" ).arg( mSchemaName ) ); - QgsDebugMsg( QStringLiteral( "Table name is: %1" ).arg( mTableName ) ); - QgsDebugMsg( QStringLiteral( "Query is: %1" ).arg( mQuery ) ); - QgsDebugMsg( QStringLiteral( "Where clause is: %1" ).arg( mSqlWhereClause ) ); + QgsDebugMsgLevel( QStringLiteral( "Connection info is %1" ).arg( mUri.connectionInfo( false ) ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( mGeometryColumn ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( mSchemaName ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( mTableName ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Query is: %1" ).arg( mQuery ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Where clause is: %1" ).arg( mSqlWhereClause ), 2 ); // no table/query passed, the provider could be used to get tables if ( mQuery.isEmpty() ) @@ -315,7 +315,7 @@ QgsPostgresProvider::~QgsPostgresProvider() { disconnectDb(); - QgsDebugMsg( QStringLiteral( "deconstructing." ) ); + QgsDebugMsgLevel( QStringLiteral( "deconstructing." ), 3 ); } @@ -521,7 +521,7 @@ void QgsPostgresProvider::appendPkParams( QgsFeatureId featureId, QStringList &p } } - QgsDebugMsg( QStringLiteral( "keys params: %1" ).arg( params.join( "; " ) ) ); + QgsDebugMsgLevel( QStringLiteral( "keys params: %1" ).arg( params.join( "; " ) ), 2 ); } break; @@ -761,7 +761,7 @@ bool QgsPostgresProvider::loadFields() if ( !mIsQuery ) { - QgsDebugMsg( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ) ); + QgsDebugMsgLevel( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ), 2 ); // Get the relation oid for use in later queries QString sql = QStringLiteral( "SELECT regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); @@ -1218,7 +1218,7 @@ void QgsPostgresProvider::setEditorWidgets() bool QgsPostgresProvider::hasSufficientPermsAndCapabilities() { - QgsDebugMsg( QStringLiteral( "Checking for permissions on the relation" ) ); + QgsDebugMsgLevel( QStringLiteral( "Checking for permissions on the relation" ), 2 ); QgsPostgresResult testAccess; if ( !mIsQuery ) @@ -1417,22 +1417,22 @@ bool QgsPostgresProvider::determinePrimaryKey() if ( !mIsQuery ) { sql = QStringLiteral( "SELECT count(*) FROM pg_inherits WHERE inhparent=%1::regclass" ).arg( quotedValue( mQuery ) ); - QgsDebugMsg( QStringLiteral( "Checking whether %1 is a parent table" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Checking whether %1 is a parent table" ).arg( sql ), 2 ); QgsPostgresResult res( connectionRO()->PQexec( sql ) ); bool isParentTable( res.PQntuples() == 0 || res.PQgetvalue( 0, 0 ).toInt() > 0 ); sql = QStringLiteral( "SELECT indexrelid FROM pg_index WHERE indrelid=%1::regclass AND (indisprimary OR indisunique) ORDER BY CASE WHEN indisprimary THEN 1 ELSE 2 END LIMIT 1" ).arg( quotedValue( mQuery ) ); - QgsDebugMsg( QStringLiteral( "Retrieving first primary or unique index: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Retrieving first primary or unique index: %1" ).arg( sql ), 2 ); res = connectionRO()->PQexec( sql ); - QgsDebugMsg( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 ); QStringList log; // no primary or unique indices found if ( res.PQntuples() == 0 ) { - QgsDebugMsg( QStringLiteral( "Relation has no primary key -- investigating alternatives" ) ); + QgsDebugMsgLevel( QStringLiteral( "Relation has no primary key -- investigating alternatives" ), 2 ); // Two options here. If the relation is a table, see if there is // an oid column that can be used instead. @@ -1443,7 +1443,7 @@ bool QgsPostgresProvider::determinePrimaryKey() if ( type == Relkind::OrdinaryTable || type == Relkind::PartitionedTable ) { - QgsDebugMsg( QStringLiteral( "Relation is a table. Checking to see if it has an oid column." ) ); + QgsDebugMsgLevel( QStringLiteral( "Relation is a table. Checking to see if it has an oid column." ), 2 ); mPrimaryKeyAttrs.clear(); mPrimaryKeyType = PktUnknown; @@ -1515,9 +1515,9 @@ bool QgsPostgresProvider::determinePrimaryKey() QString indrelid = res.PQgetvalue( 0, 0 ); sql = QStringLiteral( "SELECT attname,attnotnull FROM pg_index,pg_attribute WHERE indexrelid=%1 AND indrelid=attrelid AND pg_attribute.attnum=any(pg_index.indkey)" ).arg( indrelid ); - QgsDebugMsg( "Retrieving key columns: " + sql ); + QgsDebugMsgLevel( "Retrieving key columns: " + sql, 2 ); res = connectionRO()->PQexec( sql ); - QgsDebugMsg( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "Got %1 rows." ).arg( res.PQntuples() ), 2 ); bool mightBeNull = false; QString primaryKey; @@ -1539,7 +1539,7 @@ bool QgsPostgresProvider::determinePrimaryKey() int idx = fieldNameIndex( name ); if ( idx == -1 ) { - QgsDebugMsg( "Skipping " + name ); + QgsDebugMsgLevel( "Skipping " + name, 2 ); continue; } QgsField fld = mAttributeFields.at( idx ); @@ -2115,7 +2115,7 @@ void QgsPostgresProvider::dropOrphanedTopoGeoms() quotedIdentifier( mTableName ) ) ; - QgsDebugMsg( "TopoGeom orphans cleanup query: " + sql ); + QgsDebugMsgLevel( "TopoGeom orphans cleanup query: " + sql, 2 ); connectionRW()->PQexecNR( sql ); } @@ -2263,7 +2263,7 @@ bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist, Flags flags ) QString fieldname = mAttributeFields.at( idx ).name(); QString fieldTypeName = mAttributeFields.at( idx ).typeName(); - QgsDebugMsg( "Checking field against: " + fieldname ); + QgsDebugMsgLevel( "Checking field against: " + fieldname, 2 ); if ( fieldname.isEmpty() || fieldname == mGeometryColumn ) continue; @@ -2376,7 +2376,7 @@ bool QgsPostgresProvider::addFeatures( QgsFeatureList &flist, Flags flags ) } } - QgsDebugMsg( QStringLiteral( "prepare addfeatures: %1" ).arg( insert ) ); + QgsDebugMsgLevel( QStringLiteral( "prepare addfeatures: %1" ).arg( insert ), 2 ); QgsPostgresResult stmt( conn->PQprepare( QStringLiteral( "addfeatures" ), insert, fieldId.size() + offset - 1, nullptr ) ); if ( stmt.PQresultStatus() != PGRES_COMMAND_OK ) @@ -2519,7 +2519,7 @@ bool QgsPostgresProvider::deleteFeatures( const QgsFeatureIds &id ) { QString sql = QStringLiteral( "DELETE FROM %1 WHERE %2" ) .arg( mQuery, whereClause( *it ) ); - QgsDebugMsg( "delete sql: " + sql ); + QgsDebugMsgLevel( "delete sql: " + sql, 2 ); //send DELETE statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); @@ -2579,7 +2579,7 @@ bool QgsPostgresProvider::truncate() conn->begin(); QString sql = QStringLiteral( "TRUNCATE %1" ).arg( mQuery ); - QgsDebugMsg( "truncate sql: " + sql ); + QgsDebugMsgLevel( "truncate sql: " + sql, 2 ); //send truncate statement and do error handling QgsPostgresResult result( conn->PQexec( sql ) ); @@ -3001,7 +3001,7 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m mQuery, pkParamWhereClause( 1 ) ); - QgsDebugMsg( "getting old topogeometry id: " + getid ); + QgsDebugMsgLevel( "getting old topogeometry id: " + getid, 2 ); result = connectionRO()->PQprepare( QStringLiteral( "getid" ), getid, 1, nullptr ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) @@ -3017,7 +3017,7 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m .arg( mQuery, quotedIdentifier( mGeometryColumn ), pkParamWhereClause( 2 ) ); - QgsDebugMsg( "TopoGeom swap: " + replace ); + QgsDebugMsgLevel( "TopoGeom swap: " + replace, 2 ); result = conn->PQprepare( QStringLiteral( "replacetopogeom" ), replace, 2, nullptr ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { @@ -3036,7 +3036,7 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m pkParamWhereClause( 2 ) ); } - QgsDebugMsg( "updating: " + update ); + QgsDebugMsgLevel( "updating: " + update, 2 ); result = conn->PQprepare( QStringLiteral( "updatefeatures" ), update, 2, nullptr ); if ( result.PQresultStatus() != PGRES_COMMAND_OK && result.PQresultStatus() != PGRES_TUPLES_OK ) @@ -3046,13 +3046,13 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m throw PGException( result ); } - QgsDebugMsg( QStringLiteral( "iterating over the map of changed geometries..." ) ); + QgsDebugMsgLevel( QStringLiteral( "iterating over the map of changed geometries..." ), 2 ); for ( QgsGeometryMap::const_iterator iter = geometry_map.constBegin(); iter != geometry_map.constEnd(); ++iter ) { - QgsDebugMsg( "iterating over feature id " + FID_TO_STRING( iter.key() ) ); + QgsDebugMsgLevel( "iterating over feature id " + FID_TO_STRING( iter.key() ), 2 ); // Save the id of the current topogeometry long old_tg_id = -1; @@ -3069,7 +3069,7 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m } // TODO: watch out for NULL, handle somehow old_tg_id = result.PQgetvalue( 0, 0 ).toLong(); - QgsDebugMsg( QStringLiteral( "Old TG id is %1" ).arg( old_tg_id ) ); + QgsDebugMsgLevel( QStringLiteral( "Old TG id is %1" ).arg( old_tg_id ), 2 ); } QStringList params; @@ -3106,7 +3106,7 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m .arg( old_tg_id ) .arg( mTopoLayerInfo.layerId ) .arg( new_tg_id ); - QgsDebugMsg( "relation swap: " + replace ); + QgsDebugMsgLevel( "relation swap: " + replace, 2 ); result = conn->PQexec( replace ); if ( result.PQresultStatus() != PGRES_COMMAND_OK ) { @@ -3144,7 +3144,7 @@ bool QgsPostgresProvider::changeGeometryValues( const QgsGeometryMap &geometry_m conn->unlock(); - QgsDebugMsg( QStringLiteral( "leaving." ) ); + QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 ); return returnvalue; } @@ -3293,7 +3293,7 @@ bool QgsPostgresProvider::changeFeatures( const QgsChangedAttributesMap &attr_ma conn->unlock(); - QgsDebugMsg( QStringLiteral( "leaving." ) ); + QgsDebugMsgLevel( QStringLiteral( "leaving." ), 4 ); return returnvalue; } @@ -3312,6 +3312,19 @@ QgsVectorDataProvider::Capabilities QgsPostgresProvider::capabilities() const return mEnabledCapabilities; } +QgsFeatureSource::SpatialIndexPresence QgsPostgresProvider::hasSpatialIndex() const +{ + QgsPostgresProviderConnection conn( mUri.uri(), QVariantMap() ); + try + { + return conn.spatialIndexExists( mUri.schema(), mUri.table(), mUri.geometryColumn() ) ? SpatialIndexPresent : SpatialIndexNotPresent; + } + catch ( QgsProviderConnectionException & ) + { + return SpatialIndexUnknown; + } +} + bool QgsPostgresProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount ) { if ( theSQL.trimmed() == mSqlWhereClause ) @@ -3414,14 +3427,14 @@ long QgsPostgresProvider::featureCount() const sql = QStringLiteral( "SELECT count(*) FROM %1%2" ).arg( mQuery, filterWhereClause() ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); - QgsDebugMsg( "number of features as text: " + result.PQgetvalue( 0, 0 ) ); + QgsDebugMsgLevel( "number of features as text: " + result.PQgetvalue( 0, 0 ), 2 ); num = result.PQgetvalue( 0, 0 ).toLong(); } mShared->setFeaturesCounted( num ); - QgsDebugMsg( "number of features: " + QString::number( num ) ); + QgsDebugMsgLevel( "number of features: " + QString::number( num ), 2 ); return num; } @@ -3502,7 +3515,7 @@ QgsRectangle QgsPostgresProvider::extent() const } else { - QgsDebugMsg( QStringLiteral( "no column statistics for %1.%2.%3" ).arg( mSchemaName, mTableName, mGeometryColumn ) ); + QgsDebugMsgLevel( QStringLiteral( "no column statistics for %1.%2.%3" ).arg( mSchemaName, mTableName, mGeometryColumn ), 2 ); } } @@ -3524,7 +3537,7 @@ QgsRectangle QgsPostgresProvider::extent() const if ( !ext.isEmpty() ) { - QgsDebugMsg( "Got extents using: " + sql ); + QgsDebugMsgLevel( "Got extents using: " + sql, 2 ); QRegExp rx( "\\((.+) (.+),(.+) (.+)\\)" ); if ( ext.contains( rx ) ) @@ -3542,7 +3555,7 @@ QgsRectangle QgsPostgresProvider::extent() const } } - QgsDebugMsg( "Set extents to: " + mLayerExtent.toString() ); + QgsDebugMsgLevel( "Set extents to: " + mLayerExtent.toString(), 2 ); } return mLayerExtent; @@ -3574,7 +3587,7 @@ bool QgsPostgresProvider::getGeometryDetails() { sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 0" ).arg( quotedIdentifier( mGeometryColumn ), mQuery ); - QgsDebugMsg( QStringLiteral( "Getting geometry column: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); if ( PGRES_TUPLES_OK == result.PQresultStatus() ) @@ -3630,7 +3643,7 @@ bool QgsPostgresProvider::getGeometryDetails() } } - QString detectedType = mRequestedGeomType == QgsWkbTypes::Unknown ? QString() : QgsPostgresConn::postgisWkbTypeName( mRequestedGeomType ); + QString detectedType; QString detectedSrid = mRequestedSrid; if ( !schemaName.isEmpty() ) { @@ -3640,14 +3653,13 @@ bool QgsPostgresProvider::getGeometryDetails() quotedValue( geomCol ), quotedValue( schemaName ) ); - QgsDebugMsg( QStringLiteral( "Getting geometry column: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Getting geometry column: %1" ).arg( sql ), 2 ); result = connectionRO()->PQexec( sql ); - QgsDebugMsg( QStringLiteral( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "Geometry column query returned %1 rows" ).arg( result.PQntuples() ), 2 ); if ( result.PQntuples() == 1 ) { - QString dt = result.PQgetvalue( 0, 0 ); - if ( dt != "GEOMETRY" ) detectedType = dt; + detectedType = result.PQgetvalue( 0, 0 ); QString dim = result.PQgetvalue( 0, 2 ); if ( dim == QLatin1String( "3" ) && !detectedType.endsWith( 'M' ) ) @@ -3656,7 +3668,7 @@ bool QgsPostgresProvider::getGeometryDetails() detectedType += QLatin1String( "ZM" ); QString ds = result.PQgetvalue( 0, 1 ); - if ( ds != "0" ) detectedSrid = ds; + if ( ds != QLatin1String( "0" ) ) detectedSrid = ds; mSpatialColType = SctGeometry; } else @@ -3672,9 +3684,9 @@ bool QgsPostgresProvider::getGeometryDetails() quotedValue( geomCol ), quotedValue( schemaName ) ); - QgsDebugMsg( QStringLiteral( "Getting geography column: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Getting geography column: %1" ).arg( sql ), 2 ); result = connectionRO()->PQexec( sql, false ); - QgsDebugMsg( QStringLiteral( "Geography column query returned %1" ).arg( result.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "Geography column query returned %1" ).arg( result.PQntuples() ), 2 ); if ( result.PQntuples() == 1 ) { @@ -3705,9 +3717,9 @@ bool QgsPostgresProvider::getGeometryDetails() quotedValue( geomCol ), quotedValue( schemaName ) ); - QgsDebugMsg( QStringLiteral( "Getting TopoGeometry column: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Getting TopoGeometry column: %1" ).arg( sql ), 2 ); result = connectionRO()->PQexec( sql, false ); - QgsDebugMsg( QStringLiteral( "TopoGeometry column query returned %1" ).arg( result.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "TopoGeometry column query returned %1" ).arg( result.PQntuples() ), 2 ); if ( result.PQntuples() == 1 ) { @@ -3729,9 +3741,9 @@ bool QgsPostgresProvider::getGeometryDetails() quotedValue( geomCol ), quotedValue( schemaName ) ); - QgsDebugMsg( QStringLiteral( "Getting pointcloud column: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Getting pointcloud column: %1" ).arg( sql ), 2 ); result = connectionRO()->PQexec( sql, false ); - QgsDebugMsg( QStringLiteral( "Pointcloud column query returned %1" ).arg( result.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "Pointcloud column query returned %1" ).arg( result.PQntuples() ), 2 ); if ( result.PQntuples() == 1 ) { @@ -3755,9 +3767,9 @@ bool QgsPostgresProvider::getGeometryDetails() .arg( quotedValue( tableName ), quotedValue( geomCol ), quotedValue( schemaName ) ); - QgsDebugMsg( QStringLiteral( "Getting column datatype: %1" ).arg( sql ) ); + QgsDebugMsgLevel( QStringLiteral( "Getting column datatype: %1" ).arg( sql ), 2 ); result = connectionRO()->PQexec( sql, false ); - QgsDebugMsg( QStringLiteral( "Column datatype query returned %1" ).arg( result.PQntuples() ) ); + QgsDebugMsgLevel( QStringLiteral( "Column datatype query returned %1" ).arg( result.PQntuples() ), 2 ); if ( result.PQntuples() == 1 ) { geomColType = result.PQgetvalue( 0, 0 ); @@ -3888,10 +3900,10 @@ bool QgsPostgresProvider::getGeometryDetails() } } - QgsDebugMsg( QStringLiteral( "Detected SRID is %1" ).arg( mDetectedSrid ) ); - QgsDebugMsg( QStringLiteral( "Requested SRID is %1" ).arg( mRequestedSrid ) ); - QgsDebugMsg( QStringLiteral( "Detected type is %1" ).arg( mDetectedGeomType ) ); - QgsDebugMsg( QStringLiteral( "Requested type is %1" ).arg( mRequestedGeomType ) ); + QgsDebugMsgLevel( QStringLiteral( "Detected SRID is %1" ).arg( mDetectedSrid ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Requested SRID is %1" ).arg( mRequestedSrid ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Detected type is %1" ).arg( mDetectedGeomType ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Requested type is %1" ).arg( mRequestedGeomType ), 2 ); mValid = ( mDetectedGeomType != QgsWkbTypes::Unknown || mRequestedGeomType != QgsWkbTypes::Unknown ) && ( !mDetectedSrid.isEmpty() || !mRequestedSrid.isEmpty() ); @@ -3899,7 +3911,7 @@ bool QgsPostgresProvider::getGeometryDetails() if ( !mValid ) return false; - QgsDebugMsg( QStringLiteral( "Spatial column type is %1" ).arg( QgsPostgresConn::displayStringForGeomType( mSpatialColType ) ) ); + QgsDebugMsgLevel( QStringLiteral( "Spatial column type is %1" ).arg( QgsPostgresConn::displayStringForGeomType( mSpatialColType ) ), 2 ); return mValid; } @@ -4068,10 +4080,10 @@ QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const } schemaTableName += quotedIdentifier( tableName ); - QgsDebugMsg( QStringLiteral( "Connection info is: %1" ).arg( dsUri.connectionInfo( false ) ) ); - QgsDebugMsg( QStringLiteral( "Geometry column is: %1" ).arg( geometryColumn ) ); - QgsDebugMsg( QStringLiteral( "Schema is: %1" ).arg( schemaName ) ); - QgsDebugMsg( QStringLiteral( "Table name is: %1" ).arg( tableName ) ); + QgsDebugMsgLevel( QStringLiteral( "Connection info is: %1" ).arg( dsUri.connectionInfo( false ) ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Geometry column is: %1" ).arg( geometryColumn ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Schema is: %1" ).arg( schemaName ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Table name is: %1" ).arg( tableName ), 2 ); // create the table QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false ); @@ -4259,7 +4271,7 @@ QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const } conn->unref(); - QgsDebugMsg( QStringLiteral( "layer %1 created" ).arg( schemaTableName ) ); + QgsDebugMsgLevel( QStringLiteral( "layer %1 created" ).arg( schemaTableName ), 2 ); // use the provider to edit the table dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey ); @@ -4274,7 +4286,7 @@ QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const return QgsVectorLayerExporter::ErrInvalidLayer; } - QgsDebugMsg( QStringLiteral( "layer loaded" ) ); + QgsDebugMsgLevel( QStringLiteral( "layer loaded" ), 2 ); // add fields to the layer if ( oldToNewAttrIdxMap ) @@ -4294,7 +4306,7 @@ QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const { //the "lowercaseFieldNames" option does not affect the name of the geometry column, so we perform //this test before converting the field name to lowercase - QgsDebugMsg( QStringLiteral( "Found a field with the same name of the geometry column. Skip it!" ) ); + QgsDebugMsgLevel( QStringLiteral( "Found a field with the same name of the geometry column. Skip it!" ), 2 ); continue; } @@ -4334,11 +4346,11 @@ QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const return QgsVectorLayerExporter::ErrAttributeTypeUnsupported; } - QgsDebugMsg( QStringLiteral( "creating field #%1 -> #%2 name %3 type %4 typename %5 width %6 precision %7" ) - .arg( fldIdx ).arg( offset ) - .arg( fld.name(), QVariant::typeToName( fld.type() ), fld.typeName() ) - .arg( fld.length() ).arg( fld.precision() ) - ); + QgsDebugMsgLevel( QStringLiteral( "creating field #%1 -> #%2 name %3 type %4 typename %5 width %6 precision %7" ) + .arg( fldIdx ).arg( offset ) + .arg( fld.name(), QVariant::typeToName( fld.type() ), fld.typeName() ) + .arg( fld.length() ).arg( fld.precision() ), 2 + ); flist.append( fld ); if ( oldToNewAttrIdxMap ) @@ -4353,7 +4365,7 @@ QgsVectorLayerExporter::ExportError QgsPostgresProvider::createEmptyLayer( const return QgsVectorLayerExporter::ErrAttributeCreationFailed; } - QgsDebugMsg( QStringLiteral( "Done creating fields" ) ); + QgsDebugMsgLevel( QStringLiteral( "Done creating fields" ), 2 ); } return QgsVectorLayerExporter::NoError; } @@ -5228,7 +5240,7 @@ void QgsPostgresSharedData::ensureFeaturesCountedAtLeast( long fetched ) */ if ( mFeaturesCounted > 0 && mFeaturesCounted < fetched ) { - QgsDebugMsg( QStringLiteral( "feature count adjusted from %1 to %2" ).arg( mFeaturesCounted ).arg( fetched ) ); + QgsDebugMsgLevel( QStringLiteral( "feature count adjusted from %1 to %2" ).arg( mFeaturesCounted ).arg( fetched ), 2 ); mFeaturesCounted = fetched; } } @@ -5354,6 +5366,8 @@ QVariantMap QgsPostgresProviderMetadata::decodeUri( const QString &uri ) uriParts[ QStringLiteral( "service" ) ] = dsUri.service(); if ( ! dsUri.username().isEmpty() ) uriParts[ QStringLiteral( "username" ) ] = dsUri.username(); + if ( ! dsUri.password().isEmpty() ) + uriParts[ QStringLiteral( "password" ) ] = dsUri.password(); if ( ! dsUri.authConfigId().isEmpty() ) uriParts[ QStringLiteral( "authcfg" ) ] = dsUri.authConfigId(); if ( dsUri.wkbType() != QgsWkbTypes::Type::Unknown ) diff --git a/src/providers/postgres/qgspostgresprovider.h b/src/providers/postgres/qgspostgresprovider.h index bc90e4f441b0..51b794fb507c 100644 --- a/src/providers/postgres/qgspostgresprovider.h +++ b/src/providers/postgres/qgspostgresprovider.h @@ -192,6 +192,7 @@ class QgsPostgresProvider final: public QgsVectorDataProvider bool setSubsetString( const QString &theSQL, bool updateFeatureCount = true ) override; bool supportsSubsetString() const override { return true; } QgsVectorDataProvider::Capabilities capabilities() const override; + SpatialIndexPresence hasSpatialIndex() const override; /** * The Postgres provider does its own transforms so we return diff --git a/src/providers/postgres/qgspostgresproviderconnection.cpp b/src/providers/postgres/qgspostgresproviderconnection.cpp index 08827ff017c6..169faf7c5b5b 100644 --- a/src/providers/postgres/qgspostgresproviderconnection.cpp +++ b/src/providers/postgres/qgspostgresproviderconnection.cpp @@ -62,7 +62,10 @@ void QgsPostgresProviderConnection::setDefaultCapabilities() Capability::Tables, Capability::Schemas, Capability::Spatial, - Capability::TableExists + Capability::TableExists, + Capability::CreateSpatialIndex, + Capability::SpatialIndexExists, + Capability::DeleteSpatialIndex }; } @@ -322,6 +325,73 @@ void QgsPostgresProviderConnection::vacuum( const QString &schema, const QString .arg( QgsPostgresConn::quotedIdentifier( name ) ) ); } +void QgsPostgresProviderConnection::createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options ) const +{ + if ( options.geometryColumnName.isEmpty() ) + throw QgsProviderConnectionException( QObject::tr( "Geometry column name not specified while creating spatial index" ) ); + + checkCapability( Capability::CreateSpatialIndex ); + + const QString indexName = QStringLiteral( "sidx_%1_%2" ).arg( name, options.geometryColumnName ); + executeSql( QStringLiteral( "CREATE INDEX %1 ON %2.%3 USING GIST (%4);" ) + .arg( indexName, + QgsPostgresConn::quotedIdentifier( schema ), + QgsPostgresConn::quotedIdentifier( name ), + QgsPostgresConn::quotedIdentifier( options.geometryColumnName ) ) ); +} + +bool QgsPostgresProviderConnection::spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const +{ + checkCapability( Capability::SpatialIndexExists ); + + const QList res = executeSql( QStringLiteral( R"""(SELECT COUNT(*) + FROM pg_class t, pg_class i, pg_namespace ns, pg_index ix, pg_attribute a + WHERE + t.oid=ix.indrelid + AND t.relnamespace=ns.oid + AND i.oid=ix.indexrelid + AND a.attrelid=t.oid + AND a.attnum=ANY(ix.indkey) + AND t.relkind='r' + AND ns.nspname=%1 + AND t.relname=%2 + AND a.attname=%3; + )""" ).arg( + QgsPostgresConn::quotedValue( schema ), + QgsPostgresConn::quotedValue( name ), + QgsPostgresConn::quotedValue( geometryColumn ) ) ); + return !res.isEmpty() && !res.at( 0 ).isEmpty() && res.at( 0 ).at( 0 ).toBool(); +} + +void QgsPostgresProviderConnection::deleteSpatialIndex( const QString &schema, const QString &name, const QString &geometryColumn ) const +{ + checkCapability( Capability::DeleteSpatialIndex ); + + const QList res = executeSql( QStringLiteral( R"""(SELECT i.relname + FROM pg_class t, pg_class i, pg_namespace ns, pg_index ix, pg_attribute a + WHERE + t.oid=ix.indrelid + AND t.relnamespace=ns.oid + AND i.oid=ix.indexrelid + AND a.attrelid=t.oid + AND a.attnum=ANY(ix.indkey) + AND t.relkind='r' + AND ns.nspname=%1 + AND t.relname=%2 + AND a.attname=%3; + )""" ).arg( + QgsPostgresConn::quotedValue( schema ), + QgsPostgresConn::quotedValue( name ), + QgsPostgresConn::quotedValue( geometryColumn ) ) ); + if ( res.isEmpty() ) + throw QgsProviderConnectionException( QObject::tr( "No spatial index exists for %1.%2" ).arg( schema, name ) ); + + const QString indexName = res.at( 0 ).at( 0 ).toString(); + + executeSql( QStringLiteral( "DROP INDEX %1.%2" ).arg( QgsPostgresConn::quotedIdentifier( schema ), + QgsPostgresConn::quotedIdentifier( indexName ) ) ); +} + QList QgsPostgresProviderConnection::tables( const QString &schema, const TableFlags &flags ) const { checkCapability( Capability::Tables ); diff --git a/src/providers/postgres/qgspostgresproviderconnection.h b/src/providers/postgres/qgspostgresproviderconnection.h index 4b9275c2b0c6..7edb458f6f10 100644 --- a/src/providers/postgres/qgspostgresproviderconnection.h +++ b/src/providers/postgres/qgspostgresproviderconnection.h @@ -47,6 +47,9 @@ class QgsPostgresProviderConnection : public QgsAbstractDatabaseProviderConnecti void renameSchema( const QString &name, const QString &newName ) const override; QList executeSql( const QString &sql ) const override; void vacuum( const QString &schema, const QString &name ) const override; + void createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options = QgsAbstractDatabaseProviderConnection::SpatialIndexOptions() ) const override; + bool spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const override; + void deleteSpatialIndex( const QString &schema, const QString &name, const QString &geometryColumn ) const override; QList tables( const QString &schema, const TableFlags &flags = nullptr ) const override; QStringList schemas( ) const override; diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp index 17a6acd8f1cc..a827d3657162 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.cpp +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.cpp @@ -114,6 +114,9 @@ QgsPostgresRasterProvider::QgsPostgresRasterProvider( const QString &uri, const QStringLiteral( "PostGIS" ), Qgis::Warning ); } + mLayerMetadata.setType( QStringLiteral( "dataset" ) ); + mLayerMetadata.setCrs( crs() ); + mValid = true; } @@ -143,6 +146,12 @@ QgsPostgresRasterProvider::QgsPostgresRasterProvider( const QgsPostgresRasterPro , mTileHeight( other.mTileHeight ) , mScaleX( other.mScaleX ) , mScaleY( other.mScaleY ) + , mTemporalFieldIndex( other.mTemporalFieldIndex ) + , mTemporalDefaultTime( other.mTemporalDefaultTime ) + , mAttributeFields( other.mAttributeFields ) + , mIdentityFields( other.mIdentityFields ) + , mDefaultValues( other.mDefaultValues ) + , mDataComment( other.mDataComment ) , mDetectedSrid( other.mDetectedSrid ) , mRequestedSrid( other.mRequestedSrid ) , mConnectionRO( other.mConnectionRO ) @@ -228,10 +237,10 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE const bool isSingleValue { width == 1 && height == 1 }; QString tableToQuery { mQuery }; - QString whereAnd; - if ( ! mSqlWhereClause.isEmpty() ) + QString whereAnd { subsetStringWithTemporalRange() }; + if ( ! whereAnd.isEmpty() ) { - whereAnd = QStringLiteral( "%1 AND " ).arg( mSqlWhereClause ); + whereAnd = whereAnd.append( QStringLiteral( " AND " ) ); } // Identify @@ -361,7 +370,7 @@ bool QgsPostgresRasterProvider::readBlock( int bandNo, const QgsRectangle &viewE unsigned int overviewFactor { 1 }; // no overview // Cannot use overviews if there is a where condition - if ( mSqlWhereClause.isEmpty() ) + if ( whereAnd.isEmpty() ) { const auto ovKeys { mOverViews.keys( ) }; QList::const_reverse_iterator rit { ovKeys.rbegin() }; @@ -552,6 +561,8 @@ QVariantMap QgsPostgresRasterProviderMetadata::decodeUri( const QString &uri ) { QStringLiteral( "sslmode" ), dsUri.sslMode() }, { QStringLiteral( "sql" ), dsUri.sql() }, { QStringLiteral( "geometrycolumn" ), dsUri.geometryColumn() }, + { QStringLiteral( "temporalFieldIndex" ), dsUri.param( QStringLiteral( "temporalFieldIndex" ) ) }, + { QStringLiteral( "temporalDefaultTime" ), dsUri.param( QStringLiteral( "temporalDefaultTime" ) ) }, }; } @@ -643,10 +654,11 @@ QString QgsPostgresRasterProvider::htmlMetadata() const QVariantMap additionalInformation { { tr( "Is Tiled" ), mIsTiled }, - { tr( "Where Clause SQL" ), mSqlWhereClause }, + { tr( "Where Clause SQL" ), subsetString() }, { tr( "Pixel Size" ), QStringLiteral( "%1, %2" ).arg( mScaleX ).arg( mScaleY ) }, { tr( "Overviews" ), overviews }, { tr( "Primary Keys SQL" ), pkSql() }, + { tr( "Temporal Column" ), mTemporalFieldIndex >= 0 && mAttributeFields.exists( mTemporalFieldIndex ) ? mAttributeFields.field( mTemporalFieldIndex ).name() : QString() }, }; return dumpVariantMap( additionalInformation, tr( "Additional information" ) ); } @@ -691,10 +703,32 @@ QString QgsPostgresRasterProvider::subsetString() const return mSqlWhereClause; } +QString QgsPostgresRasterProvider::defaultTimeSubsetString( const QDateTime &defaultTime ) const +{ + if ( defaultTime.isValid( ) && + mTemporalFieldIndex >= 0 && + mAttributeFields.exists( mTemporalFieldIndex ) ) + { + const QgsField temporalField { mAttributeFields.field( mTemporalFieldIndex ) }; + const QString typeCast { temporalField.type() != QVariant::DateTime ? QStringLiteral( "::timestamp" ) : QString() }; + const QString temporalFieldName { temporalField.name() }; + return { QStringLiteral( "%1%2 = %3" ) + .arg( quotedIdentifier( temporalFieldName ), + typeCast, + quotedValue( defaultTime.toString( Qt::DateFormat::ISODate ) ) ) }; + } + else + { + return QString(); + } +} + bool QgsPostgresRasterProvider::setSubsetString( const QString &subset, bool updateFeatureCount ) { Q_UNUSED( updateFeatureCount ) + const QString oldSql { mSqlWhereClause }; + mSqlWhereClause = subset; // Recalculate extent and other metadata calling init() if ( !init() ) @@ -715,6 +749,64 @@ bool QgsPostgresRasterProvider::setSubsetString( const QString &subset, bool upd return true; } +QString QgsPostgresRasterProvider::subsetStringWithTemporalRange() const +{ + // Temporal + if ( mTemporalFieldIndex >= 0 && mAttributeFields.exists( mTemporalFieldIndex ) ) + { + const QgsField temporalField { mAttributeFields.field( mTemporalFieldIndex ) }; + const QString typeCast { temporalField.type() != QVariant::DateTime ? QStringLiteral( "::timestamp" ) : QString() }; + const QString temporalFieldName { temporalField.name() }; + + if ( temporalCapabilities()->hasTemporalCapabilities() ) + { + QString temporalClause; + const QgsTemporalRange requestedRange { temporalCapabilities()->requestedTemporalRange() }; + if ( ! requestedRange.isEmpty() && ! requestedRange.isInfinite() ) + { + if ( requestedRange.isInstant() ) + { + temporalClause = QStringLiteral( "%1%2 = %3" ) + .arg( quotedIdentifier( temporalFieldName ), + typeCast, + quotedValue( requestedRange.begin().toString( Qt::DateFormat::ISODate ) ) ); + } + else + { + if ( requestedRange.begin().isValid() ) + { + temporalClause = QStringLiteral( "%1%2 %3 %4" ) + .arg( quotedIdentifier( temporalFieldName ), + typeCast, + requestedRange.includeBeginning() ? ">=" : ">", + quotedValue( requestedRange.begin().toString( Qt::DateFormat::ISODate ) ) ); + } + if ( requestedRange.end().isValid() ) + { + if ( ! temporalClause.isEmpty() ) + { + temporalClause.append( QStringLiteral( " AND " ) ); + } + temporalClause.append( QStringLiteral( "%1%2 %3 %4" ) + .arg( quotedIdentifier( temporalFieldName ), + typeCast, + requestedRange.includeEnd() ? "<=" : "<", + quotedValue( requestedRange.end().toString( Qt::DateFormat::ISODate ) ) ) ); + } + } + return mSqlWhereClause.isEmpty() ? temporalClause : QStringLiteral( "%1 AND (%2)" ).arg( mSqlWhereClause, temporalClause ); + } + const QString defaultTimeSubset { defaultTimeSubsetString( mTemporalDefaultTime ) }; + if ( ! defaultTimeSubset.isEmpty() ) + { + return mSqlWhereClause.isEmpty() ? defaultTimeSubset : QStringLiteral( "%1 AND (%2)" ).arg( mSqlWhereClause, defaultTimeSubset ); + } + } + } + return mSqlWhereClause; +} + + void QgsPostgresRasterProvider::disconnectDb() { if ( mConnectionRO ) @@ -733,7 +825,7 @@ void QgsPostgresRasterProvider::disconnectDb() bool QgsPostgresRasterProvider::init() { - // WARNING: multiple failure return points! + // WARNING: multiple failure and return points! if ( !determinePrimaryKey() ) { @@ -745,9 +837,10 @@ bool QgsPostgresRasterProvider::init() // unless: // - it is a query layer (unsupported at the moment) // - use estimated metadata is false - // - there is a WHERE condition + // - there is a WHERE condition (except for temporal default value ) // If previous conditions are not met or the first method fail try to fetch information // directly from the raster data. This can be very slow. + // Note that a temporal filter set as temporal default value does not count as a WHERE condition // utility to get data type from string, used in both branches auto pixelTypeFromString = [ ]( const QString & t ) -> Qgis::DataType @@ -799,7 +892,7 @@ bool QgsPostgresRasterProvider::init() // /////////////////////////////////////////////////////////////////// // First method: get information from metadata - if ( ! mIsQuery && mUseEstimatedMetadata && mSqlWhereClause.isEmpty() ) + if ( ! mIsQuery && mUseEstimatedMetadata && subsetString().isEmpty() ) { try { @@ -901,7 +994,7 @@ bool QgsPostgresRasterProvider::init() "FROM %2 WHERE %3" ) .arg( quotedIdentifier( mRasterColumn ) ) .arg( mQuery ) - .arg( mSqlWhereClause.isEmpty() ? "'t'" : mSqlWhereClause ) }; + .arg( subsetString().isEmpty() ? "'t'" : subsetString() ) }; QgsPostgresResult extentResult( connectionRO()->PQexec( extentSql ) ); const QByteArray extentHexAscii { extentResult.PQgetvalue( 0, 0 ).toLatin1() }; @@ -914,7 +1007,7 @@ bool QgsPostgresRasterProvider::init() mExtent = p.boundingBox(); - // Size + // Tile size mTileWidth = result.PQgetvalue( 0, 6 ).toInt( &ok ); if ( ! ok ) @@ -952,8 +1045,7 @@ bool QgsPostgresRasterProvider::init() // Detect overviews findOverviews(); - - return true; + return initFieldsAndTemporal( ); } else { @@ -1020,28 +1112,47 @@ bool QgsPostgresRasterProvider::init() } QString where; - if ( ! mSqlWhereClause.isEmpty() ) + if ( ! subsetString().isEmpty() ) { - where = QStringLiteral( "WHERE %1" ).arg( mSqlWhereClause ); + where = QStringLiteral( "WHERE %1" ).arg( subsetString() ); } - const QString sql { QStringLiteral( "SELECT ENCODE( ST_AsBinary( ST_Envelope( foo.bar) ), 'hex'), ( ST_Metadata( foo.bar ) ).* " - "FROM ( SELECT ST_Union ( %1 ) AS bar FROM %2 %3) AS foo" ) - .arg( quotedIdentifier( mRasterColumn ) ) - .arg( tableToQuery ) - .arg( where )}; + // If we dropped here from the fast track because there was something wrong reading metadata + // we can safely assume that the raster is NOT tiled and add LIMIT 1 in the query below to + // speed things up. + + // Fastest SQL: fetch all metadata in one pass + // 0 1 3 3 4 5 6 7 8 9 10 11 12 13 14 + // encode | upperleftx | upperlefty | width | height | scalex | scaley | skewx | skewy | srid | numbands | pixeltype | nodatavalue | isoutdb | path + const QString sql { QStringLiteral( R"( + WITH cte_filtered_raster AS ( SELECT %1 AS filtered_rast FROM %2 %3 ), + cte_rast AS ( SELECT ST_Union( cte_filtered_raster.filtered_rast ) AS united_raster FROM cte_filtered_raster ), + cte_bandno AS ( SELECT * FROM generate_series(1, ST_NumBands ( ( SELECT cte_rast.united_raster FROM cte_rast ) ) ) AS bandno ), + cte_band AS ( SELECT ST_Band( united_raster, bandno ) AS band FROM cte_rast, cte_bandno ) + SELECT ENCODE( ST_AsBinary( ST_Envelope( band ) ), 'hex'), + (ST_Metadata( band )).*, + (ST_BandMetadata( band )).* + FROM cte_band + )" ).arg( quotedIdentifier( mRasterColumn ), + tableToQuery, + where.isEmpty() &&mUseEstimatedMetadata ? QStringLiteral( "LIMIT 1" ) : where ) }; QgsDebugMsgLevel( QStringLiteral( "Raster information sql: %1" ).arg( sql ), 4 ); QgsPostgresResult result( connectionRO()->PQexec( sql ) ); if ( PGRES_TUPLES_OK == result.PQresultStatus() && result.PQntuples() > 0 ) { - bool ok; - QgsPolygon p; - // envelope | upperleftx | upperlefty | width | height | scalex | scaley | skewx | skewy | srid | numbands + // These may have been filled with defaults in the fast track + mSrcNoDataValue.clear(); + mSrcHasNoDataValue.clear(); + mUseSrcNoDataValue.clear(); + mBandCount = result.PQntuples(); + + bool ok; // Extent + QgsPolygon p; try { QgsConstWkbPtr ptr { QByteArray::fromHex( result.PQgetvalue( 0, 0 ).toLatin1() ) }; @@ -1061,7 +1172,7 @@ bool QgsPostgresRasterProvider::init() mExtent = p.boundingBox(); - // Size + // Tile size (in this path the raster is considered untiled, so this is actually the whole size mTileWidth = result.PQgetvalue( 0, 3 ).toInt( &ok ); if ( ! ok ) @@ -1098,10 +1209,10 @@ bool QgsPostgresRasterProvider::init() return false; } - // Compute raster size - mHeight = static_cast( mExtent.height() / std::abs( mScaleY ) ); - mWidth = static_cast( mExtent.width() / std::abs( mScaleX ) ); - mIsTiled = ( mWidth != mTileWidth ) || ( mHeight != mTileHeight ); + // Compute raster size, it is untiled so just take tile dimensions + mHeight = mTileHeight; + mWidth = mTileWidth; + mIsTiled = false; mCrs = QgsCoordinateReferenceSystem(); // FIXME: from Nyall's comment: @@ -1121,71 +1232,582 @@ bool QgsPostgresRasterProvider::init() } mDetectedSrid = result.PQgetvalue( 0, 9 ); - mBandCount = result.PQgetvalue( 0, 10 ).toInt( &ok ); - if ( ! ok ) + + // Fetch band data types + for ( int rowNumber = 0; rowNumber < result.PQntuples(); ++rowNumber ) { - QgsMessageLog::logMessage( tr( "Cannot convert band count '%1' to int" ).arg( result.PQgetvalue( 0, 10 ) ), - QStringLiteral( "PostGIS" ), Qgis::Critical ); - return false; + Qgis::DataType type { pixelTypeFromString( result.PQgetvalue( rowNumber, 11 ) ) }; + + if ( type == Qgis::DataType::UnknownDataType ) + { + QgsMessageLog::logMessage( tr( "Unsupported data type: '%1'" ).arg( result.PQgetvalue( rowNumber, 11 ) ), + QStringLiteral( "PostGIS" ), Qgis::Critical ); + return false; + } + + mDataTypes.push_back( type ); + mDataSizes.push_back( QgsRasterBlock::typeSize( type ) ); + double nodataValue { result.PQgetvalue( rowNumber, 12 ).toDouble( &ok ) }; + + if ( ! ok ) + { + QgsMessageLog::logMessage( tr( "Cannot convert nodata value '%1' to double, default to: %2" ) + .arg( result.PQgetvalue( rowNumber, 2 ) ) + .arg( std::numeric_limits::min() ), QStringLiteral( "PostGIS" ), Qgis::Info ); + nodataValue = std::numeric_limits::min(); + } + + mSrcNoDataValue.append( nodataValue ); + mSrcHasNoDataValue.append( true ); + mUseSrcNoDataValue.append( true ); } + mIsOutOfDb = result.PQgetvalue( 0, 13 ) == 't'; + } + else + { + QgsMessageLog::logMessage( tr( "An error occurred while fetching raster metadata" ), + QStringLiteral( "PostGIS" ), Qgis::Critical ); + return false; + } + return initFieldsAndTemporal( ); +} - // Fetch band data types - for ( int band = 1; band <= mBandCount; ++band ) +bool QgsPostgresRasterProvider::initFieldsAndTemporal( ) +{ + // Populate fields + if ( ! loadFields() ) + { + QgsMessageLog::logMessage( tr( "An error occurred while fetching raster fields information" ), + QStringLiteral( "PostGIS" ), Qgis::Critical ); + return false; + } + + QString where; + if ( ! subsetString().isEmpty() ) + { + where = QStringLiteral( "WHERE %1" ).arg( subsetString() ); + } + + // Temporal capabilities + // Setup temporal properties for layer, do not fail if something goes wrong but log a warning + if ( mUri.hasParam( QStringLiteral( "temporalFieldIndex" ) ) ) + { + bool ok; + const int temporalFieldIndex { mUri.param( QStringLiteral( "temporalFieldIndex" ) ).toInt( &ok ) }; + if ( ok && mAttributeFields.exists( temporalFieldIndex ) ) { - // pixeltype | nodatavalue | isoutdb | path - const QString sql { QStringLiteral( "SELECT ( ST_BandMetadata( foo.bar, %1 ) ).* " - "FROM ( SELECT ST_Union ( ST_Band ( %2, %1 ) ) AS bar FROM %3 %4 ) AS foo" ) - .arg( band ) - .arg( quotedIdentifier( mRasterColumn ) ) + const QString temporalFieldName { mAttributeFields.field( temporalFieldIndex ).name() }; + // Calculate the range + const QString sql { QStringLiteral( "SELECT MIN(%1::timestamp), MAX(%1::timestamp) " + "FROM %2 %3" ) + .arg( quotedIdentifier( temporalFieldName ) ) .arg( mQuery ) .arg( where )}; QgsPostgresResult result( connectionRO()->PQexec( sql ) ); - if ( PGRES_TUPLES_OK == result.PQresultStatus() && result.PQntuples() > 0 ) + + if ( PGRES_TUPLES_OK == result.PQresultStatus() && result.PQntuples() == 1 ) + { + const QDateTime minTime { QDateTime::fromString( result.PQgetvalue( 0, 0 ), Qt::DateFormat::ISODate ) }; + const QDateTime maxTime { QDateTime::fromString( result.PQgetvalue( 0, 1 ), Qt::DateFormat::ISODate ) }; + if ( minTime.isValid() && maxTime.isValid() && !( minTime > maxTime ) ) + { + mTemporalFieldIndex = temporalFieldIndex; + temporalCapabilities()->setHasTemporalCapabilities( true ); + temporalCapabilities()->setAvailableTemporalRange( { minTime, maxTime } ); + temporalCapabilities()->setIntervalHandlingMethod( QgsRasterDataProviderTemporalCapabilities::FindClosestMatchToStartOfRange ); + QgsDebugMsgLevel( QStringLiteral( "Raster temporal range for field %1: %2 - %3" ).arg( QString::number( mTemporalFieldIndex ), minTime.toString(), maxTime.toString() ), 3 ); + + if ( mUri.hasParam( QStringLiteral( "temporalDefaultTime" ) ) ) + { + const QDateTime defaultDateTime { QDateTime::fromString( mUri.param( QStringLiteral( "temporalDefaultTime" ) ), Qt::DateFormat::ISODate ) }; + if ( defaultDateTime.isValid() ) + { + mTemporalDefaultTime = defaultDateTime; + } + else + { + QgsMessageLog::logMessage( tr( "Invalid default date in raster temporal capabilities for field %1: %2" ).arg( temporalFieldName, mUri.param( QStringLiteral( "temporalDefaultTime" ) ) ), + QStringLiteral( "PostGIS" ), Qgis::Warning ); + } + } + } + else + { + QgsMessageLog::logMessage( tr( "Invalid temporal range in raster temporal capabilities for field %1: %2 - %3" ).arg( temporalFieldName, minTime.toString(), maxTime.toString() ), + QStringLiteral( "PostGIS" ), Qgis::Warning ); + } + } + else + { + QgsMessageLog::logMessage( tr( "An error occurred while fetching raster temporal capabilities for field: %1" ).arg( temporalFieldName ), + QStringLiteral( "PostGIS" ), Qgis::Warning ); + + } + } + else + { + QgsMessageLog::logMessage( tr( "Invalid field index for raster temporal capabilities: %1" ) + .arg( QString::number( temporalFieldIndex ) ), + QStringLiteral( "PostGIS" ), Qgis::Warning ); + } + } + return true; +} + +bool QgsPostgresRasterProvider::loadFields() +{ + + if ( !mIsQuery ) + { + QgsDebugMsgLevel( QStringLiteral( "Loading fields for table %1" ).arg( mTableName ), 2 ); + + // Get the relation oid for use in later queries + QString sql = QStringLiteral( "SELECT regclass(%1)::oid" ).arg( quotedValue( mQuery ) ); + QgsPostgresResult tresult( connectionRO()->PQexec( sql ) ); + QString tableoid = tresult.PQgetvalue( 0, 0 ); + + // Get the table description + sql = QStringLiteral( "SELECT description FROM pg_description WHERE objoid=%1 AND objsubid=0" ).arg( tableoid ); + tresult = connectionRO()->PQexec( sql ); + if ( tresult.PQntuples() > 0 ) + { + mDataComment = tresult.PQgetvalue( 0, 0 ); + mLayerMetadata.setAbstract( mDataComment ); + } + } + else + { + // Not supported for now + return true; + } + + // Populate the field vector for this layer. The field vector contains + // field name, type, length, and precision (if numeric) + QString sql = QStringLiteral( "SELECT * FROM %1 LIMIT 0" ).arg( mQuery ); + + QgsPostgresResult result( connectionRO()->PQexec( sql ) ); + + // Collect type info + sql = QStringLiteral( "SELECT oid,typname,typtype,typelem,typlen FROM pg_type" ); + QgsPostgresResult typeResult( connectionRO()->PQexec( sql ) ); + + QMap typeMap; + for ( int i = 0; i < typeResult.PQntuples(); ++i ) + { + PGTypeInfo typeInfo = + { + /* typeName = */ typeResult.PQgetvalue( i, 1 ), + /* typeType = */ typeResult.PQgetvalue( i, 2 ), + /* typeElem = */ typeResult.PQgetvalue( i, 3 ), + /* typeLen = */ typeResult.PQgetvalue( i, 4 ).toInt() + }; + typeMap.insert( typeResult.PQgetvalue( i, 0 ).toUInt(), typeInfo ); + } + + + QMap > fmtFieldTypeMap, descrMap, defValMap, identityMap; + QMap > attTypeIdMap; + QMap > notNullMap, uniqueMap; + if ( result.PQnfields() > 0 ) + { + // Collect table oids + QSet tableoids; + for ( int i = 0; i < result.PQnfields(); i++ ) + { + Oid tableoid = result.PQftable( i ); + if ( tableoid > 0 ) + { + tableoids.insert( tableoid ); + } + } + + if ( !tableoids.isEmpty() ) + { + QStringList tableoidsList; + const auto constTableoids = tableoids; + for ( Oid tableoid : constTableoids ) + { + tableoidsList.append( QString::number( tableoid ) ); + } + + QString tableoidsFilter = '(' + tableoidsList.join( QStringLiteral( "," ) ) + ')'; + + // Collect formatted field types + sql = QStringLiteral( + "SELECT attrelid, attnum, pg_catalog.format_type(atttypid,atttypmod), pg_catalog.col_description(attrelid,attnum), pg_catalog.pg_get_expr(adbin,adrelid), atttypid, attnotnull::int, indisunique::int%1" + " FROM pg_attribute" + " LEFT OUTER JOIN pg_attrdef ON attrelid=adrelid AND attnum=adnum" + + // find unique constraints if present. Text cast required to handle int2vector comparison. Distinct required as multiple unique constraints may exist + " LEFT OUTER JOIN ( SELECT DISTINCT indrelid, indkey, indisunique FROM pg_index WHERE indisunique ) uniq ON attrelid=indrelid AND attnum::text=indkey::text " + + " WHERE attrelid IN %2" + ).arg( connectionRO()->pgVersion() >= 100000 ? QStringLiteral( ", attidentity" ) : QString() ).arg( tableoidsFilter ); + + QgsPostgresResult fmtFieldTypeResult( connectionRO()->PQexec( sql ) ); + for ( int i = 0; i < fmtFieldTypeResult.PQntuples(); ++i ) { + Oid attrelid = fmtFieldTypeResult.PQgetvalue( i, 0 ).toUInt(); + int attnum = fmtFieldTypeResult.PQgetvalue( i, 1 ).toInt(); // Int2 + QString formatType = fmtFieldTypeResult.PQgetvalue( i, 2 ); + QString descr = fmtFieldTypeResult.PQgetvalue( i, 3 ); + QString defVal = fmtFieldTypeResult.PQgetvalue( i, 4 ); + Oid attType = fmtFieldTypeResult.PQgetvalue( i, 5 ).toUInt(); + bool attNotNull = fmtFieldTypeResult.PQgetvalue( i, 6 ).toInt(); + bool uniqueConstraint = fmtFieldTypeResult.PQgetvalue( i, 7 ).toInt(); + QString attIdentity = connectionRO()->pgVersion() >= 100000 ? fmtFieldTypeResult.PQgetvalue( i, 8 ) : " "; + fmtFieldTypeMap[attrelid][attnum] = formatType; + descrMap[attrelid][attnum] = descr; + defValMap[attrelid][attnum] = defVal; + attTypeIdMap[attrelid][attnum] = attType; + notNullMap[attrelid][attnum] = attNotNull; + uniqueMap[attrelid][attnum] = uniqueConstraint; + identityMap[attrelid][attnum] = attIdentity.isEmpty() ? " " : attIdentity; + } + } + } + + QSet fields; + mAttributeFields.clear(); + mIdentityFields.clear(); + for ( int i = 0; i < result.PQnfields(); i++ ) + { + QString fieldName = result.PQfname( i ); + if ( fieldName == mRasterColumn ) + continue; + + Oid fldtyp = result.PQftype( i ); + int fldMod = result.PQfmod( i ); + int fieldPrec = -1; + Oid tableoid = result.PQftable( i ); + int attnum = result.PQftablecol( i ); + Oid atttypid = attTypeIdMap[tableoid][attnum]; + + const PGTypeInfo &typeInfo = typeMap.value( fldtyp ); + QString fieldTypeName = typeInfo.typeName; + QString fieldTType = typeInfo.typeType; + int fieldSize = typeInfo.typeLen; + + bool isDomain = ( typeMap.value( atttypid ).typeType == QLatin1String( "d" ) ); + + QString formattedFieldType = fmtFieldTypeMap[tableoid][attnum]; + QString originalFormattedFieldType = formattedFieldType; + if ( isDomain ) + { + // get correct formatted field type for domain + sql = QStringLiteral( "SELECT format_type(%1, %2)" ).arg( fldtyp ).arg( fldMod ); + QgsPostgresResult fmtFieldModResult( connectionRO()->PQexec( sql ) ); + if ( fmtFieldModResult.PQntuples() > 0 ) + { + formattedFieldType = fmtFieldModResult.PQgetvalue( 0, 0 ); + } + } - Qgis::DataType type { pixelTypeFromString( result.PQgetvalue( 0, 0 ) ) }; + QString fieldComment = descrMap[tableoid][attnum]; - if ( type == Qgis::DataType::UnknownDataType ) + QVariant::Type fieldType; + QVariant::Type fieldSubType = QVariant::Invalid; + + if ( fieldTType == QLatin1String( "b" ) ) + { + bool isArray = fieldTypeName.startsWith( '_' ); + + if ( isArray ) + fieldTypeName = fieldTypeName.mid( 1 ); + + if ( fieldTypeName == QLatin1String( "int8" ) || fieldTypeName == QLatin1String( "serial8" ) ) + { + fieldType = QVariant::LongLong; + fieldSize = -1; + fieldPrec = 0; + } + else if ( fieldTypeName == QLatin1String( "int2" ) || fieldTypeName == QLatin1String( "int4" ) || + fieldTypeName == QLatin1String( "oid" ) || fieldTypeName == QLatin1String( "serial" ) ) + { + fieldType = QVariant::Int; + fieldSize = -1; + fieldPrec = 0; + } + else if ( fieldTypeName == QLatin1String( "real" ) || fieldTypeName == QLatin1String( "double precision" ) || + fieldTypeName == QLatin1String( "float4" ) || fieldTypeName == QLatin1String( "float8" ) ) + { + fieldType = QVariant::Double; + fieldSize = -1; + fieldPrec = -1; + } + else if ( fieldTypeName == QLatin1String( "numeric" ) ) + { + fieldType = QVariant::Double; + + if ( formattedFieldType == QLatin1String( "numeric" ) || formattedFieldType.isEmpty() ) { - QgsMessageLog::logMessage( tr( "Unsupported data type: '%1'" ).arg( result.PQgetvalue( 0, 0 ) ), - QStringLiteral( "PostGIS" ), Qgis::Critical ); - return false; + fieldSize = -1; + fieldPrec = -1; } + else + { + QRegExp re( "numeric\\((\\d+),(\\d+)\\)" ); + if ( re.exactMatch( formattedFieldType ) ) + { + fieldSize = re.cap( 1 ).toInt(); + fieldPrec = re.cap( 2 ).toInt(); + } + else if ( formattedFieldType != QLatin1String( "numeric" ) ) + { + QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" ) + .arg( formattedFieldType, + fieldName ), + tr( "PostGIS" ) ); + fieldSize = -1; + fieldPrec = -1; + } + } + } + else if ( fieldTypeName == QLatin1String( "varchar" ) ) + { + fieldType = QVariant::String; + + QRegExp re( "character varying\\((\\d+)\\)" ); + if ( re.exactMatch( formattedFieldType ) ) + { + fieldSize = re.cap( 1 ).toInt(); + } + else + { + fieldSize = -1; + } + } + else if ( fieldTypeName == QLatin1String( "date" ) ) + { + fieldType = QVariant::Date; + fieldSize = -1; + } + else if ( fieldTypeName == QLatin1String( "time" ) ) + { + fieldType = QVariant::Time; + fieldSize = -1; + } + else if ( fieldTypeName == QLatin1String( "timestamp" ) ) + { + fieldType = QVariant::DateTime; + fieldSize = -1; + } + else if ( fieldTypeName == QLatin1String( "bytea" ) ) + { + fieldType = QVariant::ByteArray; + fieldSize = -1; + } + else if ( fieldTypeName == QLatin1String( "text" ) || + fieldTypeName == QLatin1String( "citext" ) || + fieldTypeName == QLatin1String( "geometry" ) || + fieldTypeName == QLatin1String( "inet" ) || + fieldTypeName == QLatin1String( "money" ) || + fieldTypeName == QLatin1String( "ltree" ) || + fieldTypeName == QLatin1String( "uuid" ) || + fieldTypeName == QLatin1String( "xml" ) || + fieldTypeName.startsWith( QLatin1String( "time" ) ) || + fieldTypeName.startsWith( QLatin1String( "date" ) ) ) + { + fieldType = QVariant::String; + fieldSize = -1; + } + else if ( fieldTypeName == QLatin1String( "bpchar" ) ) + { + // although postgres internally uses "bpchar", this is exposed to users as character in postgres + fieldTypeName = QStringLiteral( "character" ); - mDataTypes.push_back( type ); - mDataSizes.push_back( QgsRasterBlock::typeSize( type ) ); - double nodataValue { result.PQgetvalue( 0, 1 ).toDouble( &ok ) }; + fieldType = QVariant::String; - if ( ! ok ) + QRegExp re( "character\\((\\d+)\\)" ); + if ( re.exactMatch( formattedFieldType ) ) { - QgsMessageLog::logMessage( tr( "Cannot convert nodata value '%1' to double, default to: %2" ) - .arg( result.PQgetvalue( 0, 1 ) ) - .arg( std::numeric_limits::min() ), QStringLiteral( "PostGIS" ), Qgis::Info ); - nodataValue = std::numeric_limits::min(); + fieldSize = re.cap( 1 ).toInt(); } + else + { + QgsDebugMsg( QStringLiteral( "Unexpected formatted field type '%1' for field %2" ) + .arg( formattedFieldType, + fieldName ) ); + fieldSize = -1; + fieldPrec = -1; + } + } + else if ( fieldTypeName == QLatin1String( "char" ) ) + { + fieldType = QVariant::String; - mSrcNoDataValue.append( nodataValue ); - mSrcHasNoDataValue.append( true ); - mUseSrcNoDataValue.append( true ); - mIsOutOfDb = result.PQgetvalue( 0, 2 ) == 't'; + QRegExp re( "char\\((\\d+)\\)" ); + if ( re.exactMatch( formattedFieldType ) ) + { + fieldSize = re.cap( 1 ).toInt(); + } + else + { + QgsMessageLog::logMessage( tr( "Unexpected formatted field type '%1' for field %2" ) + .arg( formattedFieldType, + fieldName ) ); + fieldSize = -1; + fieldPrec = -1; + } + } + else if ( fieldTypeName == QLatin1String( "hstore" ) || fieldTypeName == QLatin1String( "json" ) || fieldTypeName == QLatin1String( "jsonb" ) ) + { + fieldType = QVariant::Map; + fieldSubType = QVariant::String; + fieldSize = -1; + } + else if ( fieldTypeName == QLatin1String( "bool" ) ) + { + // enum + fieldType = QVariant::Bool; + fieldSize = -1; } else { - QgsMessageLog::logMessage( tr( "An error occurred while fetching raster band metadata" ), - QStringLiteral( "PostGIS" ), Qgis::Critical ); - return false; + // be tolerant in case of views: this might be a field used as a key + const QgsPostgresProvider::Relkind type = relkind(); + if ( ( type == QgsPostgresProvider::Relkind::View || type == QgsPostgresProvider::Relkind::MaterializedView ) + && parseUriKey( mUri.keyColumn( ) ).contains( fieldName ) ) + { + // Assume it is convertible to text + fieldType = QVariant::String; + fieldSize = -1; + } + else + { + QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTType ), tr( "PostGIS" ) ); + continue; + } + } + + if ( isArray ) + { + fieldTypeName = '_' + fieldTypeName; + fieldSubType = fieldType; + fieldType = ( fieldType == QVariant::String ? QVariant::StringList : QVariant::List ); + fieldSize = -1; } } + else if ( fieldTType == QLatin1String( "e" ) ) + { + // enum + fieldType = QVariant::String; + fieldSize = -1; + } + else + { + QgsMessageLog::logMessage( tr( "Field %1 ignored, because of unsupported type %2" ).arg( fieldName, fieldTType ), tr( "PostGIS" ) ); + continue; + } + + if ( fields.contains( fieldName ) ) + { + QgsMessageLog::logMessage( tr( "Duplicate field %1 found\n" ).arg( fieldName ), tr( "PostGIS" ) ); + return false; + } + + fields << fieldName; + + if ( isDomain ) + { + //field was defined using domain, so use domain type name for fieldTypeName + fieldTypeName = originalFormattedFieldType; + } + + // If this is an identity field with constraints and there is no default, let's look for a sequence: + // we might have a default value created by a sequence named __seq + if ( ! identityMap[tableoid ][ attnum ].isEmpty() + && notNullMap[tableoid][ attnum ] + && uniqueMap[tableoid][attnum] + && defValMap[tableoid][attnum].isEmpty() ) + { + const QString seqName { mTableName + '_' + fieldName + QStringLiteral( "_seq" ) }; + const QString seqSql { QStringLiteral( "SELECT c.oid " + " FROM pg_class c " + " LEFT JOIN pg_namespace n " + " ON ( n.oid = c.relnamespace ) " + " WHERE c.relkind = 'S' " + " AND c.relname = %1 " + " AND n.nspname = %2" ) + .arg( quotedValue( seqName ) ) + .arg( quotedValue( mSchemaName ) ) + }; + QgsPostgresResult seqResult( connectionRO()->PQexec( seqSql ) ); + if ( seqResult.PQntuples() == 1 ) + { + defValMap[tableoid][attnum] = QStringLiteral( "nextval(%1::regclass)" ).arg( quotedIdentifier( seqName ) ); + } + } + + mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] ); + + QgsField newField = QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType ); + + QgsFieldConstraints constraints; + if ( notNullMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' ) + constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); + if ( uniqueMap[tableoid][attnum] || ( mPrimaryKeyAttrs.size() == 1 && mPrimaryKeyAttrs[0] == i ) || identityMap[tableoid][attnum] != ' ' ) + constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); + newField.setConstraints( constraints ); + + mIdentityFields.insert( mAttributeFields.size(), identityMap[tableoid][attnum][0].toLatin1() ); + mAttributeFields.append( newField ); + } + + return true; +} + +/* static */ +QStringList QgsPostgresRasterProvider::parseUriKey( const QString &key ) +{ + if ( key.isEmpty() ) return QStringList(); + + QStringList cols; + + // remove quotes from key list + if ( key.startsWith( '"' ) && key.endsWith( '"' ) ) + { + int i = 1; + QString col; + while ( i < key.size() ) + { + if ( key[i] == '"' ) + { + if ( i + 1 < key.size() && key[i + 1] == '"' ) + { + i++; + } + else + { + cols << col; + col.clear(); + + if ( ++i == key.size() ) + break; + + Q_ASSERT( key[i] == ',' ); + i++; + Q_ASSERT( key[i] == '"' ); + i++; + col.clear(); + continue; + } + } + + col += key[i++]; + } + } + else if ( key.contains( ',' ) ) + { + cols = key.split( ',' ); } else { - QgsMessageLog::logMessage( tr( "An error occurred while fetching raster metadata" ), - QStringLiteral( "PostGIS" ), Qgis::Critical ); - return false; + cols << key; } - return true; + return cols; } QgsPostgresProvider::Relkind QgsPostgresRasterProvider::relkind() const @@ -1247,6 +1869,9 @@ bool QgsPostgresRasterProvider::determinePrimaryKey() // unique indices, so we catch them as well. QString sql; + + mPrimaryKeyAttrs.clear(); + if ( !mIsQuery ) { sql = QStringLiteral( "SELECT count(*) FROM pg_inherits WHERE inhparent=%1::regclass" ).arg( quotedValue( mQuery ) ); @@ -1454,7 +2079,7 @@ void QgsPostgresRasterProvider::determinePrimaryKeyFromUriKeyColumn() QString QgsPostgresRasterProvider::pkSql() { - Q_ASSERT( ! mPrimaryKeyAttrs.isEmpty() ); + Q_ASSERT_X( ! mPrimaryKeyAttrs.isEmpty(), "QgsPostgresRasterProvider::pkSql()", "No PK is defined!" ); if ( mPrimaryKeyAttrs.count( ) > 1 ) { QStringList pkeys; @@ -1467,6 +2092,10 @@ QString QgsPostgresRasterProvider::pkSql() return quotedIdentifier( mPrimaryKeyAttrs.first() ); } +QString QgsPostgresRasterProvider::dataComment() const +{ + return mDataComment; +} void QgsPostgresRasterProvider::findOverviews() { @@ -1575,7 +2204,7 @@ QgsRasterBandStats QgsPostgresRasterProvider::bandStatistics( int bandNo, int st double statsRatio { pixelsRatio }; // Decide if overviews can be used here - if ( mSqlWhereClause.isEmpty() && ! mIsQuery && mIsTiled && extent.isEmpty() ) + if ( subsetString().isEmpty() && ! mIsQuery && mIsTiled && extent.isEmpty() ) { const unsigned int desiredOverviewFactor { static_cast( 1.0 / sqrt( pixelsRatio ) ) }; const auto ovKeys { mOverViews.keys( ) }; @@ -1600,10 +2229,10 @@ QgsRasterBandStats QgsPostgresRasterProvider::bandStatistics( int bandNo, int st .arg( quotedValue( extent.asWktPolygon() ) ) .arg( mCrs.postgisSrid() ) }; - if ( ! mSqlWhereClause.isEmpty() ) + if ( ! subsetString().isEmpty() ) { - where.append( where.isEmpty() ? QStringLiteral( "WHERE %1" ).arg( mSqlWhereClause ) : - QStringLiteral( " AND %1" ).arg( mSqlWhereClause ) ); + where.append( where.isEmpty() ? QStringLiteral( "WHERE %1" ).arg( subsetString() ) : + QStringLiteral( " AND %1" ).arg( subsetString() ) ); } const QString sql { QStringLiteral( "SELECT (ST_SummaryStatsAgg( %1, %2, TRUE, %3 )).* " @@ -1659,3 +2288,8 @@ QGISEXTERN QgsProviderMetadata *providerMetadataFactory() QgsPostgresRasterProviderException::QgsPostgresRasterProviderException( const QString &msg ) : message( msg ) {} + +QgsFields QgsPostgresRasterProvider::fields() const +{ + return mAttributeFields; +} diff --git a/src/providers/postgres/raster/qgspostgresrasterprovider.h b/src/providers/postgres/raster/qgspostgresrasterprovider.h index 86873fe72120..438fc247138e 100644 --- a/src/providers/postgres/raster/qgspostgresrasterprovider.h +++ b/src/providers/postgres/raster/qgspostgresrasterprovider.h @@ -66,6 +66,7 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider virtual QString lastErrorTitle() override; virtual QString lastError() override; int capabilities() const override; + QgsFields fields() const override; // QgsRasterInterface interface int xSize() const override; @@ -90,7 +91,7 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider QString mRasterColumn; //! Name of the schema QString mSchemaName; - //! SQL statement used to limit the features retrieved + //! SQL statement used to limit the features retrieved (subset string) QString mSqlWhereClause; //! Rectangle that contains the extent (bounding box) of the layer mutable QgsRectangle mExtent; @@ -126,6 +127,21 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider double mScaleX = 0; //! Scale y double mScaleY = 0; + //! Temporal field index + int mTemporalFieldIndex = -1; + //! Temporal default time + QDateTime mTemporalDefaultTime; + //! Keep track of fields + QgsFields mAttributeFields; + //! Keeps track of identity fields + QHash mIdentityFields; + //! Keeps track of default values + QHash mDefaultValues; + //! Data comment + QString mDataComment; + //! Layer metadata + QgsLayerMetadata mLayerMetadata; + QString mDetectedSrid; //!< Spatial reference detected in the database QString mRequestedSrid; //!< Spatial reference requested in the uri @@ -157,10 +173,19 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider QString subsetString() const override; bool setSubsetString( const QString &subset, bool updateFeatureCount = true ) override; + //! Subset string with temporal range from request (if any) + QString subsetStringWithTemporalRange() const; + + //! Subset string with only the temporal default time part + QString defaultTimeSubsetString( const QDateTime &defaultTime ) const; + bool hasSufficientPermsAndCapabilities(); void disconnectDb(); //! Initialize the raster by fetching metadata and creating spatial indexes. bool init(); + //! Initialize fields and temporal capabilities + bool initFieldsAndTemporal(); + //! Search for overviews and store a map void findOverviews(); //! Initialize spatial indexes @@ -170,7 +195,7 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider static QString quotedJsonValue( const QVariant &value ) { return QgsPostgresConn::quotedJsonValue( value ); } static QString quotedByteaValue( const QVariant &value ); QgsPostgresProvider::Relkind relkind() const; - + bool loadFields(); /** * Determine the fields making up the primary key @@ -190,8 +215,30 @@ class QgsPostgresRasterProvider : public QgsRasterDataProvider */ QString pkSql(); + /** + * Returns table comment + */ + QString dataComment() const override; + + + /** + * Private struct for column type information + */ + struct PGTypeInfo + { + QString typeName; + QString typeType; + QString typeElem; + int typeLen; + }; + + QStringList parseUriKey( const QString &key ); + + public: }; + + struct QgsPostgresRasterProviderException: public std::exception { diff --git a/src/providers/postgres/raster/qgspostgresrastershareddata.cpp b/src/providers/postgres/raster/qgspostgresrastershareddata.cpp index 4522296d2f76..4099259898dc 100644 --- a/src/providers/postgres/raster/qgspostgresrastershareddata.cpp +++ b/src/providers/postgres/raster/qgspostgresrastershareddata.cpp @@ -39,29 +39,31 @@ QgsPostgresRasterSharedData::TilesResponse QgsPostgresRasterSharedData::tiles( c { QMutexLocker locker( &mMutex ); + const QString cacheKey { keyFromRequest( request ) }; + QgsPostgresRasterSharedData::TilesResponse result; // First check for index existence - if ( mSpatialIndexes.find( request.overviewFactor ) == mSpatialIndexes.end() ) + if ( mSpatialIndexes.find( cacheKey ) == mSpatialIndexes.end() ) { // Create the index - mSpatialIndexes.emplace( request.overviewFactor, new QgsGenericSpatialIndex() ); - mTiles.emplace( request.overviewFactor, std::map>() ); - mLoadedIndexBounds[ request.overviewFactor] = QgsGeometry(); + mSpatialIndexes.emplace( cacheKey, new QgsGenericSpatialIndex() ); + mTiles.emplace( cacheKey, std::map>() ); + mLoadedIndexBounds[ cacheKey] = QgsGeometry(); } // Now check if the requested extent was completely downloaded const QgsGeometry requestedRect { QgsGeometry::fromRect( request.extent ) }; // Fast track for first tile (where index is empty) - if ( mLoadedIndexBounds[ request.overviewFactor].isNull() ) + if ( mLoadedIndexBounds[ cacheKey ].isNull() ) { return fetchTilesIndexAndData( requestedRect, request ); } - else if ( ! mLoadedIndexBounds[ request.overviewFactor].contains( requestedRect ) ) + else if ( ! mLoadedIndexBounds[ cacheKey].contains( requestedRect ) ) { // Fetch index - const QgsGeometry geomDiff { requestedRect.difference( mLoadedIndexBounds[ request.overviewFactor] ) }; + const QgsGeometry geomDiff { requestedRect.difference( mLoadedIndexBounds[ cacheKey ] ) }; if ( ! fetchTilesIndex( geomDiff.isEmpty() ? requestedRect : geomDiff, request ) ) { return result; @@ -72,7 +74,7 @@ QgsPostgresRasterSharedData::TilesResponse QgsPostgresRasterSharedData::tiles( c QStringList missingTileIds; // Get intersecting tiles from the index - mSpatialIndexes[ request.overviewFactor ]->intersects( request.extent, [ & ]( Tile * tilePtr ) -> bool + mSpatialIndexes[ cacheKey ]->intersects( request.extent, [ & ]( Tile * tilePtr ) -> bool { if ( tilePtr->data.size() == 0 ) { @@ -139,7 +141,7 @@ QgsPostgresRasterSharedData::TilesResponse QgsPostgresRasterSharedData::tiles( c int dataRead; GByte *binaryData { CPLHexToBinary( dataResult.PQgetvalue( row, 1 ).toLatin1().constData(), &dataRead ) }; - Tile const *tilePtr { setTileData( request.overviewFactor, tileId, QByteArray::fromRawData( reinterpret_cast( binaryData ), dataRead ) ) }; + Tile const *tilePtr { setTileData( cacheKey, tileId, QByteArray::fromRawData( reinterpret_cast( binaryData ), dataRead ) ) }; CPLFree( binaryData ); if ( ! tilePtr ) @@ -185,16 +187,16 @@ void QgsPostgresRasterSharedData::invalidateCache() } -QgsPostgresRasterSharedData::Tile const *QgsPostgresRasterSharedData::setTileData( unsigned int overviewFactor, TileIdType tileId, const QByteArray &data ) +QgsPostgresRasterSharedData::Tile const *QgsPostgresRasterSharedData::setTileData( const QString &cacheKey, TileIdType tileId, const QByteArray &data ) { Q_ASSERT( ! data.isEmpty() ); - if ( mTiles.find( overviewFactor ) == mTiles.end() || - mTiles[ overviewFactor ].find( tileId ) == mTiles[ overviewFactor ].end() ) + if ( mTiles.find( cacheKey ) == mTiles.end() || + mTiles[ cacheKey ].find( tileId ) == mTiles[ cacheKey ].end() ) { return nullptr; } - Tile *const tile { mTiles[ overviewFactor ][ tileId ].get() }; + Tile *const tile { mTiles[ cacheKey ][ tileId ].get() }; const QVariantMap parsedData { QgsPostgresRasterUtils::parseWkb( data ) }; for ( int bandCnt = 1; bandCnt <= tile->numBands; ++bandCnt ) { @@ -203,6 +205,11 @@ QgsPostgresRasterSharedData::Tile const *QgsPostgresRasterSharedData::setTileDat return tile; } +QString QgsPostgresRasterSharedData::keyFromRequest( const QgsPostgresRasterSharedData::TilesRequest &request ) +{ + return QStringLiteral( "%1 - %2" ).arg( QString::number( request.overviewFactor ), request.whereClause ); +} + bool QgsPostgresRasterSharedData::fetchTilesIndex( const QgsGeometry &requestPolygon, const TilesRequest &request ) { const QString indexSql { QStringLiteral( "SELECT %1, (ST_Metadata( %2 )).* FROM %3 " @@ -224,13 +231,15 @@ bool QgsPostgresRasterSharedData::fetchTilesIndex( const QgsGeometry &requestPol return false; } - if ( mLoadedIndexBounds[ request.overviewFactor ].isNull() ) + const QString cacheKey { keyFromRequest( request ) }; + + if ( mLoadedIndexBounds[ cacheKey ].isNull() ) { - mLoadedIndexBounds[ request.overviewFactor ] = requestPolygon; + mLoadedIndexBounds[ cacheKey ] = requestPolygon; } else { - mLoadedIndexBounds[ request.overviewFactor ] = mLoadedIndexBounds[ request.overviewFactor ].combine( requestPolygon ); + mLoadedIndexBounds[ cacheKey ] = mLoadedIndexBounds[ cacheKey ].combine( requestPolygon ); } for ( int i = 0; i < result.PQntuples(); ++i ) @@ -238,7 +247,7 @@ bool QgsPostgresRasterSharedData::fetchTilesIndex( const QgsGeometry &requestPol // rid | upperleftx | upperlefty | width | height | scalex | scaley | skewx | skewy | srid | numbands const TileIdType tileId { result.PQgetvalue( i, 0 ) }; - if ( mTiles[ request.overviewFactor ].find( tileId ) == mTiles[ request.overviewFactor ].end() ) + if ( mTiles[ cacheKey ].find( tileId ) == mTiles[ cacheKey ].end() ) { const double upperleftx { result.PQgetvalue( i, 1 ).toDouble() }; const double upperlefty { result.PQgetvalue( i, 2 ).toDouble() }; @@ -266,15 +275,18 @@ bool QgsPostgresRasterSharedData::fetchTilesIndex( const QgsGeometry &requestPol skewy, numbands ); - mSpatialIndexes[ request.overviewFactor ]->insert( tile.get(), tile->extent ); - mTiles[ request.overviewFactor ][ tileId ] = std::move( tile ); - //qDebug() << "Tile added:" << request.overviewFactor << " ID: " << tileId << "extent " << extent.toString( 4 ) << upperleftx << upperlefty << tileWidth << tileHeight << extent.width() << extent.height(); + mSpatialIndexes[ cacheKey ]->insert( tile.get(), tile->extent ); + mTiles[ cacheKey ][ tileId ] = std::move( tile ); + QgsDebugMsgLevel( QStringLiteral( "Tile added: %1, ID: %2" ) + .arg( cacheKey ) + .arg( tileId ), 3 ); + //qDebug() << "Tile added:" << cacheKey << " ID: " << tileId << "extent " << extent.toString( 4 ) << upperleftx << upperlefty << tileWidth << tileHeight << extent.width() << extent.height(); } else { QgsDebugMsgLevel( QStringLiteral( "Tile already indexed: %1, ID: %2" ) - .arg( request.overviewFactor ) - .arg( tileId ), 2 ); + .arg( cacheKey ) + .arg( tileId ), 3 ); } } return true; @@ -302,14 +314,16 @@ QgsPostgresRasterSharedData::TilesResponse QgsPostgresRasterSharedData::fetchTil return response; } - mLoadedIndexBounds[ request.overviewFactor ] = requestPolygon; + const QString cacheKey { keyFromRequest( request ) }; + + mLoadedIndexBounds[ cacheKey ] = requestPolygon; for ( int row = 0; row < dataResult.PQntuples(); ++row ) { // rid | upperleftx | upperlefty | width | height | scalex | scaley | skewx | skewy | srid | numbands | data const TileIdType tileId { dataResult.PQgetvalue( row, 0 ) }; - if ( mTiles[ request.overviewFactor ].find( tileId ) == mTiles[ request.overviewFactor ].end() ) + if ( mTiles[ cacheKey ].find( tileId ) == mTiles[ cacheKey ].end() ) { const double upperleftx { dataResult.PQgetvalue( row, 1 ).toDouble() }; const double upperlefty { dataResult.PQgetvalue( row, 2 ).toDouble() }; @@ -347,7 +361,7 @@ QgsPostgresRasterSharedData::TilesResponse QgsPostgresRasterSharedData::fetchTil { tile->data.emplace_back( parsedData[ QStringLiteral( "band%1" ).arg( bandCnt ) ].toByteArray() ); } - mSpatialIndexes[ request.overviewFactor ]->insert( tile.get(), tile->extent ); + mSpatialIndexes[ cacheKey ]->insert( tile.get(), tile->extent ); response.tiles.push_back( TileBand { @@ -367,14 +381,16 @@ QgsPostgresRasterSharedData::TilesResponse QgsPostgresRasterSharedData::fetchTil response.extent.combineExtentWith( tile->extent ); - mTiles[ request.overviewFactor ][ tileId ] = std::move( tile ); - - //qDebug() << "Tile data added:" << request.overviewFactor << " ID: " << tileId << "extent " << extent.toString( 4 ) << upperleftx << upperlefty << tileWidth << tileHeight << extent.width() << extent.height(); + mTiles[ cacheKey ][ tileId ] = std::move( tile ); + QgsDebugMsgLevel( QStringLiteral( "Tile added: %1, ID: %2" ) + .arg( cacheKey ) + .arg( tileId ), 3 ); + //qDebug() << "Tile data added:" << cacheKey << " ID: " << tileId << "extent " << extent.toString( 4 ) << upperleftx << upperlefty << tileWidth << tileHeight << extent.width() << extent.height(); } else { QgsDebugMsgLevel( QStringLiteral( "Tile and data already indexed: %1, ID: %2" ) - .arg( request.overviewFactor ) + .arg( cacheKey ) .arg( tileId ), 2 ); } } diff --git a/src/providers/postgres/raster/qgspostgresrastershareddata.h b/src/providers/postgres/raster/qgspostgresrastershareddata.h index 5ac1529193f9..12390b3f2cb7 100644 --- a/src/providers/postgres/raster/qgspostgresrastershareddata.h +++ b/src/providers/postgres/raster/qgspostgresrastershareddata.h @@ -97,6 +97,9 @@ class QgsPostgresRasterSharedData */ void invalidateCache(); + //! Generates the cache key from the request + static QString keyFromRequest( const TilesRequest &request ); + private: //! Protect access to tiles @@ -150,17 +153,20 @@ class QgsPostgresRasterSharedData bool fetchTilesIndex( const QgsGeometry &requestPolygon, const TilesRequest &request ); //! Fast track for first fetch TilesResponse fetchTilesIndexAndData( const QgsGeometry &requestPolygon, const TilesRequest &request ); - Tile const *setTileData( unsigned int overviewFactor, TileIdType tileId, const QByteArray &data ); + Tile const *setTileData( const QString &cacheKey, TileIdType tileId, const QByteArray &data ); - // Note: cannot be a smart pointer because spatial index cannot be copied - //! Tile caches, index is the overview factor (1 is the full resolution data) - std::map*> mSpatialIndexes; + /** + * Tile caches, index is a key generated from the overview factor (1 is the full resolution data) + * and the where clause + * \note cannot be a smart pointer because spatial index cannot be copied + */ + std::map*> mSpatialIndexes; //! Memory manager for owned tiles (and for tileId access) - std::map>> mTiles; + std::map>> mTiles; //! Keeps track of loaded index bounds - std::map mLoadedIndexBounds; + std::map mLoadedIndexBounds; }; diff --git a/src/providers/spatialite/CMakeLists.txt b/src/providers/spatialite/CMakeLists.txt index 8f282ee45f2b..9b78d7e8e514 100644 --- a/src/providers/spatialite/CMakeLists.txt +++ b/src/providers/spatialite/CMakeLists.txt @@ -13,6 +13,7 @@ SET(SPATIALITE_SRCS qgsspatialitefeatureiterator.cpp qgsspatialitetablemodel.cpp qgsspatialiteproviderconnection.cpp + qgsspatialitetransaction.cpp ) IF (WITH_GUI) diff --git a/src/providers/spatialite/qgsspatialiteconnection.h b/src/providers/spatialite/qgsspatialiteconnection.h index 08b34cb0792b..328b935c4099 100644 --- a/src/providers/spatialite/qgsspatialiteconnection.h +++ b/src/providers/spatialite/qgsspatialiteconnection.h @@ -158,7 +158,13 @@ class QgsSqliteHandle { mIsValid = false; } + + /** + * Returns a possibly cached SQLite DB object from \a path, if \a shared is FALSE + * the DB will not be searched in the cache and a new READ ONLY connection will be returned. + */ static QgsSqliteHandle *openDb( const QString &dbPath, bool shared = true ); + static bool checkMetadata( sqlite3 *handle ); static void closeDb( QgsSqliteHandle *&handle ); diff --git a/src/providers/spatialite/qgsspatialitedataitemguiprovider.h b/src/providers/spatialite/qgsspatialitedataitemguiprovider.h index 4c31394baa21..22ea9175eb8c 100644 --- a/src/providers/spatialite/qgsspatialitedataitemguiprovider.h +++ b/src/providers/spatialite/qgsspatialitedataitemguiprovider.h @@ -26,7 +26,7 @@ class QgsSpatiaLiteDataItemGuiProvider : public QObject, public QgsDataItemGuiPr Q_OBJECT public: - QString name() override { return QStringLiteral( "SPATIALITE" ); } + QString name() override { return QStringLiteral( "spatialite" ); } void populateContextMenu( QgsDataItem *item, QMenu *menu, const QList &selectedItems, QgsDataItemGuiContext context ) override; diff --git a/src/providers/spatialite/qgsspatialitedataitems.cpp b/src/providers/spatialite/qgsspatialitedataitems.cpp index 36a11515900b..9c25918486c9 100644 --- a/src/providers/spatialite/qgsspatialitedataitems.cpp +++ b/src/providers/spatialite/qgsspatialitedataitems.cpp @@ -61,7 +61,7 @@ bool SpatiaLiteUtils::deleteLayer( const QString &dbPath, const QString &tableNa } QgsSLLayerItem::QgsSLLayerItem( QgsDataItem *parent, const QString &name, const QString &path, const QString &uri, LayerType layerType ) - : QgsLayerItem( parent, name, path, uri, layerType, QStringLiteral( "SPATIALITE" ) ) + : QgsLayerItem( parent, name, path, uri, layerType, QStringLiteral( "spatialite" ) ) { mCapabilities |= Delete; setState( Populated ); // no children are expected @@ -70,7 +70,7 @@ QgsSLLayerItem::QgsSLLayerItem( QgsDataItem *parent, const QString &name, const // ------ QgsSLConnectionItem::QgsSLConnectionItem( QgsDataItem *parent, const QString &name, const QString &path ) - : QgsDataCollectionItem( parent, name, path, QStringLiteral( "SPATIALITE" ) ) + : QgsDataCollectionItem( parent, name, path, QStringLiteral( "spatialite" ) ) { mDbPath = QgsSpatiaLiteConnection::connectionPath( name ); mToolTip = mDbPath; @@ -162,7 +162,7 @@ bool QgsSLConnectionItem::equal( const QgsDataItem *other ) // --------------------------------------------------------------------------- QgsSLRootItem::QgsSLRootItem( QgsDataItem *parent, const QString &name, const QString &path ) - : QgsDataCollectionItem( parent, name, path, QStringLiteral( "SPATIALITE" ) ) + : QgsDataCollectionItem( parent, name, path, QStringLiteral( "spatialite" ) ) { mCapabilities |= Fast; mIconName = QStringLiteral( "mIconSpatialite.svg" ); @@ -276,7 +276,7 @@ bool SpatiaLiteUtils::createDb( const QString &dbPath, QString &errCause ) QString QgsSpatiaLiteDataItemProvider::name() { - return QStringLiteral( "SPATIALITE" ); + return QStringLiteral( "spatialite" ); } QString QgsSpatiaLiteDataItemProvider::dataProviderKey() const diff --git a/src/providers/spatialite/qgsspatialitefeatureiterator.cpp b/src/providers/spatialite/qgsspatialitefeatureiterator.cpp index 1dbc84961dc4..407b4c677ce3 100644 --- a/src/providers/spatialite/qgsspatialitefeatureiterator.cpp +++ b/src/providers/spatialite/qgsspatialitefeatureiterator.cpp @@ -29,7 +29,16 @@ QgsSpatiaLiteFeatureIterator::QgsSpatiaLiteFeatureIterator( QgsSpatiaLiteFeatureSource *source, bool ownSource, const QgsFeatureRequest &request ) : QgsAbstractFeatureIteratorFromSource( source, ownSource, request ) { - mHandle = QgsSpatiaLiteConnPool::instance()->acquireConnection( mSource->mSqlitePath, request.timeout(), request.requestMayBeNested() ); + + mSqliteHandle = source->transactionHandle(); + if ( ! mSqliteHandle ) + { + mHandle = QgsSpatiaLiteConnPool::instance()->acquireConnection( mSource->mSqlitePath, request.timeout(), request.requestMayBeNested() ); + if ( mHandle ) + { + mSqliteHandle = mHandle->handle(); + } + } mFetchGeometry = !mSource->mGeometryColumn.isNull() && !( mRequest.flags() & QgsFeatureRequest::NoGeometry ); mHasPrimaryKey = !mSource->mPrimaryKey.isEmpty(); @@ -290,9 +299,10 @@ bool QgsSpatiaLiteFeatureIterator::close() iteratorClosed(); - if ( !mHandle ) + mClosed = true; + + if ( !mSqliteHandle ) { - mClosed = true; return false; } @@ -302,19 +312,24 @@ bool QgsSpatiaLiteFeatureIterator::close() sqliteStatement = nullptr; } - QgsSpatiaLiteConnPool::instance()->releaseConnection( mHandle ); - mHandle = nullptr; + if ( mHandle ) + { + QgsSpatiaLiteConnPool::instance()->releaseConnection( mHandle ); + mHandle = nullptr; + } + mSqliteHandle = nullptr; mClosed = true; return true; } + //// bool QgsSpatiaLiteFeatureIterator::prepareStatement( const QString &whereClause, long limit, const QString &orderBy ) { - if ( !mHandle ) + if ( !mSqliteHandle ) return false; try @@ -359,10 +374,10 @@ bool QgsSpatiaLiteFeatureIterator::prepareStatement( const QString &whereClause, QgsDebugMsgLevel( sql, 4 ); - if ( sqlite3_prepare_v2( mHandle->handle(), sql.toUtf8().constData(), -1, &sqliteStatement, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &sqliteStatement, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( QObject::tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mHandle->handle() ) ), QObject::tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( QObject::tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), QObject::tr( "SpatiaLite" ) ); return false; } } @@ -489,7 +504,7 @@ bool QgsSpatiaLiteFeatureIterator::getFeature( sqlite3_stmt *stmt, QgsFeature &f if ( ret != SQLITE_ROW ) { // some unexpected error occurred - QgsMessageLog::logMessage( QObject::tr( "SQLite error getting feature: %1" ).arg( QString::fromUtf8( sqlite3_errmsg( mHandle->handle() ) ) ), QObject::tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( QObject::tr( "SQLite error getting feature: %1" ).arg( QString::fromUtf8( sqlite3_errmsg( mSqliteHandle ) ) ), QObject::tr( "SpatiaLite" ) ); return false; } @@ -642,6 +657,7 @@ QgsSpatiaLiteFeatureSource::QgsSpatiaLiteFeatureSource( const QgsSpatiaLiteProvi , mSpatialIndexMbrCache( p->mSpatialIndexMbrCache ) , mSqlitePath( p->mSqlitePath ) , mCrs( p->crs() ) + , mTransactionHandle( p->transaction() ? p->sqliteHandle() : nullptr ) { } @@ -649,3 +665,8 @@ QgsFeatureIterator QgsSpatiaLiteFeatureSource::getFeatures( const QgsFeatureRequ { return QgsFeatureIterator( new QgsSpatiaLiteFeatureIterator( this, false, request ) ); } + +sqlite3 *QgsSpatiaLiteFeatureSource::transactionHandle() +{ + return mTransactionHandle; +} diff --git a/src/providers/spatialite/qgsspatialitefeatureiterator.h b/src/providers/spatialite/qgsspatialitefeatureiterator.h index c1b980625966..57e49e90c9e0 100644 --- a/src/providers/spatialite/qgsspatialitefeatureiterator.h +++ b/src/providers/spatialite/qgsspatialitefeatureiterator.h @@ -34,6 +34,8 @@ class QgsSpatiaLiteFeatureSource final: public QgsAbstractFeatureSource QgsFeatureIterator getFeatures( const QgsFeatureRequest &request ) override; + sqlite3 *transactionHandle(); + private: QString mGeometryColumn; QString mSubsetString; @@ -49,6 +51,7 @@ class QgsSpatiaLiteFeatureSource final: public QgsAbstractFeatureSource bool mSpatialIndexMbrCache; QString mSqlitePath; QgsCoordinateReferenceSystem mCrs; + sqlite3 *mTransactionHandle = nullptr; friend class QgsSpatiaLiteFeatureIterator; friend class QgsSpatiaLiteExpressionCompiler; @@ -81,8 +84,10 @@ class QgsSpatiaLiteFeatureIterator final: public QgsAbstractFeatureIteratorFromS QVariant getFeatureAttribute( sqlite3_stmt *stmt, int ic, QVariant::Type type, QVariant::Type subType ); void getFeatureGeometry( sqlite3_stmt *stmt, int ic, QgsFeature &feature ); - //! wrapper of the SQLite database connection + //! QGIS wrapper of the SQLite database connection QgsSqliteHandle *mHandle = nullptr; + //! The low level connection + sqlite3 *mSqliteHandle = nullptr; /** * SQLite statement handle diff --git a/src/providers/spatialite/qgsspatialiteprovider.cpp b/src/providers/spatialite/qgsspatialiteprovider.cpp index ad94ca5407e1..6cad148619c5 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.cpp +++ b/src/providers/spatialite/qgsspatialiteprovider.cpp @@ -31,6 +31,7 @@ email : a.furieri@lqt.it #include "qgsfeedback.h" #include "qgsspatialitedataitems.h" #include "qgsspatialiteconnection.h" +#include "qgsspatialitetransaction.h" #include "qgsspatialiteproviderconnection.h" #include "qgsjsonutils.h" @@ -51,7 +52,7 @@ const QString QgsSpatiaLiteProvider::SPATIALITE_KEY = QStringLiteral( "spatialit const QString QgsSpatiaLiteProvider::SPATIALITE_DESCRIPTION = QStringLiteral( "SpatiaLite data provider" ); static const QString SPATIALITE_ARRAY_PREFIX = QStringLiteral( "json" ); static const QString SPATIALITE_ARRAY_SUFFIX = QStringLiteral( "list" ); - +QAtomicInt QgsSpatiaLiteProvider::sSavepointId = 0; bool QgsSpatiaLiteProvider::convertField( QgsField &field ) { @@ -459,8 +460,9 @@ QgsSpatiaLiteProvider::QgsSpatiaLiteProvider( QString const &uri, const Provider mPrimaryKey = anUri.keyColumn(); mQuery = mTableName; - // trying to open the SQLite DB + // Retrieve a shared connection mHandle = QgsSqliteHandle::openDb( mSqlitePath ); + if ( !mHandle ) { return; @@ -474,7 +476,7 @@ QgsSpatiaLiteProvider::QgsSpatiaLiteProvider( QString const &uri, const Provider for ( const auto &pragma : pragmaList ) { char *errMsg = nullptr; - int ret = sqlite3_exec( mSqliteHandle, ( "PRAGMA " + pragma ).toUtf8(), nullptr, nullptr, &errMsg ); + int ret = exec_sql( QStringLiteral( "PRAGMA %1" ).arg( pragma ), errMsg ); if ( ret != SQLITE_OK ) { QgsDebugMsg( QStringLiteral( "PRAGMA " ) + pragma + QString( " failed : %1" ).arg( errMsg ? errMsg : "" ) ); @@ -539,6 +541,7 @@ QgsSpatiaLiteProvider::QgsSpatiaLiteProvider( QString const &uri, const Provider mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures; mEnabledCapabilities |= QgsVectorDataProvider::AddAttributes; mEnabledCapabilities |= QgsVectorDataProvider::CreateAttributeIndex; + mEnabledCapabilities |= QgsVectorDataProvider::TransactionSupport; } if ( lyr ) @@ -880,7 +883,7 @@ void QgsSpatiaLiteProvider::fetchConstraints() int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return; } @@ -929,7 +932,7 @@ void QgsSpatiaLiteProvider::fetchConstraints() int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return; } @@ -1036,7 +1039,7 @@ QString QgsSpatiaLiteProvider::defaultValueClause( int fieldIndex ) const return mDefaultValueClause.value( fieldIndex, QString() ); } -void QgsSpatiaLiteProvider::handleError( const QString &sql, char *errorMessage, bool rollback ) +void QgsSpatiaLiteProvider::handleError( const QString &sql, char *errorMessage, const QString &savepointId ) { QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errorMessage ? errorMessage : tr( "unknown cause" ) ), tr( "SpatiaLite" ) ); // unexpected error @@ -1045,13 +1048,24 @@ void QgsSpatiaLiteProvider::handleError( const QString &sql, char *errorMessage, sqlite3_free( errorMessage ); } - if ( rollback ) + if ( ! savepointId.isEmpty() ) { // ROLLBACK after some previous error - ( void )sqlite3_exec( mSqliteHandle, "ROLLBACK", nullptr, nullptr, nullptr ); + ( void )exec_sql( QStringLiteral( "ROLLBACK TRANSACTION TO \"%1\"" ).arg( savepointId ) ); } } +int QgsSpatiaLiteProvider::exec_sql( const QString &sql, char *errMsg ) +{ + // Use transaction's handle (if any) + return sqlite3_exec( sqliteHandle(), sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); +} + +sqlite3 *QgsSpatiaLiteProvider::sqliteHandle() const +{ + return mTransaction && mTransaction->sqliteHandle() ? mTransaction->sqliteHandle() : mSqliteHandle; +} + void QgsSpatiaLiteProvider::loadFields() { int ret; @@ -1075,10 +1089,10 @@ void QgsSpatiaLiteProvider::loadFields() sql = QStringLiteral( "PRAGMA table_info(%1)" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return; } if ( rows < 1 ) @@ -1135,10 +1149,10 @@ void QgsSpatiaLiteProvider::loadFields() { sql = QStringLiteral( "select * from %1 limit 1" ).arg( mQuery ); - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); return; } @@ -1206,7 +1220,7 @@ void QgsSpatiaLiteProvider::determineViewPrimaryKey() int rows; int columns; char *errMsg = nullptr; - int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + int ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK ) { if ( rows > 0 ) @@ -1229,15 +1243,15 @@ QStringList QgsSpatiaLiteProvider::tablePrimaryKeys( const QString &tableName ) int rows; int columns; char *errMsg = nullptr; - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else { - int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + int ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK ) { for ( int row = 1; row <= rows; ++row ) @@ -1275,7 +1289,7 @@ bool QgsSpatiaLiteProvider::hasTriggers() sql = QStringLiteral( "SELECT * FROM sqlite_master WHERE type='trigger' AND tbl_name=%1" ) .arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); sqlite3_free_table( results ); return ( ret == SQLITE_OK && rows > 0 ); } @@ -1288,7 +1302,7 @@ bool QgsSpatiaLiteProvider::hasRowid() // table without rowid column QString sql = QStringLiteral( "SELECT rowid FROM %1 WHERE 0" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); char *errMsg = nullptr; - return sqlite3_exec( mSqliteHandle, sql.toUtf8(), nullptr, nullptr, &errMsg ) == SQLITE_OK; + return exec_sql( sql, errMsg ) == SQLITE_OK; } @@ -3719,7 +3733,7 @@ QVariant QgsSpatiaLiteProvider::minimumValue( int index ) const sql += " WHERE ( " + mSubsetString + ')'; } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errMsg ? errMsg : tr( "unknown cause" ) ), tr( "SpatiaLite" ) ); @@ -3782,7 +3796,7 @@ QVariant QgsSpatiaLiteProvider::maximumValue( int index ) const sql += " WHERE ( " + mSubsetString + ')'; } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errMsg ? errMsg : tr( "unknown cause" ) ), tr( "SpatiaLite" ) ); @@ -3852,10 +3866,10 @@ QSet QgsSpatiaLiteProvider::uniqueValues( int index, int limit ) const } // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else { @@ -3891,7 +3905,7 @@ QSet QgsSpatiaLiteProvider::uniqueValues( int index, int limit ) const } else { - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); sqlite3_finalize( stmt ); return uniqueValues; } @@ -3932,10 +3946,10 @@ QStringList QgsSpatiaLiteProvider::uniqueStringsMatching( int index, const QStri } // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else @@ -3965,7 +3979,7 @@ QStringList QgsSpatiaLiteProvider::uniqueStringsMatching( int index, const QStri } else { - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); sqlite3_finalize( stmt ); return results; } @@ -4020,7 +4034,9 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) QgsAttributes attributevec = flist[0].attributes(); - ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret == SQLITE_OK ) { toCommit = true; @@ -4078,7 +4094,7 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) sql += ')'; // SQLite prepared statement - ret = sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ); + ret = sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ); if ( ret == SQLITE_OK ) { @@ -4185,14 +4201,14 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) // update feature id if ( !( flags & QgsFeatureSink::FastInsert ) ) { - feature->setId( sqlite3_last_insert_rowid( mSqliteHandle ) ); + feature->setId( sqlite3_last_insert_rowid( sqliteHandle( ) ) ); } mNumberFeatures++; } else { // some unexpected error occurred - const char *err = sqlite3_errmsg( mSqliteHandle ); + const char *err = sqlite3_errmsg( sqliteHandle( ) ); errMsg = ( char * ) sqlite3_malloc( ( int ) strlen( err ) + 1 ); strcpy( errMsg, err ); break; @@ -4202,7 +4218,7 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) if ( ret == SQLITE_DONE || ret == SQLITE_ROW ) { - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); } } // BEGIN statement @@ -4218,9 +4234,14 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) if ( toCommit ) { // ROLLBACK after some previous error - ( void )sqlite3_exec( mSqliteHandle, "ROLLBACK", nullptr, nullptr, nullptr ); + ( void )exec_sql( QStringLiteral( "ROLLBACK TRANSACTION TO SAVEPOINT \"%1\"" ).arg( savepointId ) ); } } + else + { + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + } return ret == SQLITE_OK; } @@ -4243,10 +4264,12 @@ bool QgsSpatiaLiteProvider::createAttributeIndex( int field ) QString sql; QString fieldName; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4256,43 +4279,62 @@ bool QgsSpatiaLiteProvider::createAttributeIndex( int field ) .arg( createIndexName( mTableName, fieldName ), mTableName, QgsSqliteUtils::quotedIdentifier( fieldName ) ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql, errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } +QgsFeatureSource::SpatialIndexPresence QgsSpatiaLiteProvider::hasSpatialIndex() const +{ + QgsDataSourceUri u = uri(); + QgsSpatiaLiteProviderConnection conn( u.uri(), QVariantMap() ); + try + { + return conn.spatialIndexExists( u.schema(), u.table(), u.geometryColumn() ) ? SpatialIndexPresent : SpatialIndexNotPresent; + } + catch ( QgsProviderConnectionException & ) + { + return SpatialIndexUnknown; + } +} + bool QgsSpatiaLiteProvider::deleteFeatures( const QgsFeatureIds &id ) { sqlite3_stmt *stmt = nullptr; char *errMsg = nullptr; QString sql; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } sql = QStringLiteral( "DELETE FROM %1 WHERE %2=?" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ), QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ); // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - pushError( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ) ); + pushError( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ) ); return false; } else @@ -4316,10 +4358,10 @@ bool QgsSpatiaLiteProvider::deleteFeatures( const QgsFeatureIds &id ) else { // some unexpected error occurred - const char *err = sqlite3_errmsg( mSqliteHandle ); + const char *err = sqlite3_errmsg( sqliteHandle( ) ); errMsg = ( char * ) sqlite3_malloc( ( int ) strlen( err ) + 1 ); strcpy( errMsg, err ); - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } @@ -4327,13 +4369,16 @@ bool QgsSpatiaLiteProvider::deleteFeatures( const QgsFeatureIds &id ) sqlite3_finalize( stmt ); - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4342,28 +4387,33 @@ bool QgsSpatiaLiteProvider::truncate() char *errMsg = nullptr; QString sql; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } sql = QStringLiteral( "DELETE FROM %1" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql, errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4375,10 +4425,12 @@ bool QgsSpatiaLiteProvider::addAttributes( const QList &attributes ) if ( attributes.isEmpty() ) return true; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4388,27 +4440,30 @@ bool QgsSpatiaLiteProvider::addAttributes( const QList &attributes ) .arg( mTableName, iter->name(), iter->typeName() ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql, errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } - gaiaStatisticsInvalidate( mSqliteHandle, mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); - update_layer_statistics( mSqliteHandle, mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); + gaiaStatisticsInvalidate( sqliteHandle( ), mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); + update_layer_statistics( sqliteHandle( ), mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); // reload columns loadFields(); + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4420,10 +4475,12 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap if ( attr_map.isEmpty() ) return true; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4490,7 +4547,7 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap auto msgPtr { static_cast( sqlite3_malloc( errM.length() + 1 ) ) }; strcpy( static_cast( msgPtr ), errM.toStdString().c_str() ); errMsg = msgPtr; - handleError( jRepr, errMsg, true ); + handleError( jRepr, errMsg, savepointId ); return false; } } @@ -4507,21 +4564,24 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap } sql += QStringLiteral( " WHERE %1=%2" ).arg( QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ).arg( fid ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql.toUtf8(), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4531,10 +4591,12 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry char *errMsg = nullptr; QString sql; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4546,10 +4608,10 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry .arg( QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ); // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else { @@ -4577,10 +4639,10 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry else { // some unexpected error occurred - const char *err = sqlite3_errmsg( mSqliteHandle ); + const char *err = sqlite3_errmsg( sqliteHandle( ) ); errMsg = ( char * ) sqlite3_malloc( ( int ) strlen( err ) + 1 ); strcpy( errMsg, err ); - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } @@ -4588,12 +4650,16 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry sqlite3_finalize( stmt ); - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4692,7 +4758,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() "WHERE lower(name) = lower(%1) " "AND type in ('table', 'view') " ).arg( QgsSqliteUtils::quotedString( mTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { QString type = QString( results[ columns + 0 ] ); @@ -4737,7 +4803,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() QgsSqliteUtils::quotedIdentifier( alias ) ); sql = QStringLiteral( "SELECT 0, %1 FROM %2 LIMIT 1" ).arg( QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); // Try to find a PK or try to use ROWID if ( ret == SQLITE_OK && rows == 1 ) @@ -4747,7 +4813,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() // 1. find the table that provides geometry // String containing the name of the table that provides the geometry if the layer data source is based on a query QString queryGeomTableName; - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) == SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) == SQLITE_OK ) { queryGeomTableName = sqlite3_column_table_name( stmt, 1 ); } @@ -4801,7 +4867,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() // Try first without any injection or manipulation sql = QStringLiteral( "SELECT %1, %2 FROM %3 LIMIT 1" ).arg( QgsSqliteUtils::quotedIdentifier( pks.first( ) ), QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mPrimaryKey = pks.first( ); @@ -4814,7 +4880,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() .arg( QgsSqliteUtils::quotedIdentifier( tableIdentifier ) ) .arg( pks.first() ) ) ); sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 1" ).arg( pk ).arg( newSql ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mQuery = newSql; @@ -4830,7 +4896,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() if ( ! queryGeomTableName.isEmpty() ) { sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( QgsSqliteUtils::quotedIdentifier( queryGeomTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK || rows != 1 ) { queryGeomTableName = QString(); @@ -4844,7 +4910,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() .arg( QgsSqliteUtils::quotedIdentifier( tableIdentifier ), QStringLiteral( "ROWID" ) ) ) ); sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( newSql ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mQuery = newSql; @@ -4883,7 +4949,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() .arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { if ( errMsg && strcmp( errMsg, "no such table: geometry_columns_auth" ) == 0 ) @@ -4892,7 +4958,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() sql = QStringLiteral( "SELECT 0 FROM geometry_columns WHERE upper(f_table_name) = upper(%1) and upper(f_geometry_column) = upper(%2)" ) .arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); } } if ( ret == SQLITE_OK && rows == 1 ) @@ -4922,7 +4988,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() " WHERE view_name=%1 and view_geometry=%2" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mViewBased = true; @@ -4942,7 +5008,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() " WHERE virt_name=%1 and virt_geometry=%2" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mVShapeBased = true; @@ -5052,10 +5118,10 @@ void QgsSpatiaLiteProvider::getViewSpatialIndexName() "FROM views_geometry_columns " "WHERE upper(view_name) = upper(%1) and upper(view_geometry) = upper(%2)" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); } if ( rows < 1 ) ; @@ -5104,7 +5170,7 @@ bool QgsSpatiaLiteProvider::getTableGeometryDetails() mIndexGeometry = mGeometryColumn; QString sql; - if ( ! versionIsAbove( mSqliteHandle, 3, 1 ) ) + if ( ! versionIsAbove( sqliteHandle( ), 3, 1 ) ) { sql = QString( "SELECT type, srid, spatial_index_enabled, coord_dimension FROM geometry_columns" " WHERE upper(f_table_name) = upper(%1) and upper(f_geometry_column) = upper(%2)" ).arg( QgsSqliteUtils::quotedString( mTableName ), @@ -5117,10 +5183,10 @@ bool QgsSpatiaLiteProvider::getTableGeometryDetails() QgsSqliteUtils::quotedString( mGeometryColumn ) ); } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5194,7 +5260,7 @@ bool QgsSpatiaLiteProvider::getTableGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5216,10 +5282,10 @@ bool QgsSpatiaLiteProvider::getViewGeometryDetails() " WHERE upper(view_name) = upper(%1) and upper(view_geometry) = upper(%2)" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5274,7 +5340,7 @@ bool QgsSpatiaLiteProvider::getViewGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5294,10 +5360,10 @@ bool QgsSpatiaLiteProvider::getVShapeGeometryDetails() " WHERE virt_name=%1 and virt_geometry=%2" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5341,7 +5407,7 @@ bool QgsSpatiaLiteProvider::getVShapeGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5362,7 +5428,7 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() // get stuff from the relevant column instead. This may (will?) // fail if there is no data in the relevant table. - QString sql = QStringLiteral( "select srid(%1), geometrytype(%1) from %2" ) + QString sql = QStringLiteral( "SELECT srid(%1), geometrytype(%1) FROM %2" ) .arg( QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); @@ -5374,10 +5440,10 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() sql += QLatin1String( " limit 1" ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5398,23 +5464,23 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() if ( fType == QLatin1String( "GEOMETRY" ) ) { // check to see if there is a unique geometry type - sql = QString( "select distinct " - "case" - " when geometrytype(%1) IN ('POINT','MULTIPOINT') THEN 'POINT'" - " when geometrytype(%1) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" - " when geometrytype(%1) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" - " end " - "from %2" ) + sql = QString( "SELECT DISTINCT " + "CASE" + " WHEN geometrytype(%1) IN ('POINT','MULTIPOINT') THEN 'POINT'" + " WHEN geometrytype(%1) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" + " WHEN geometrytype(%1) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" + " END " + "FROM %2" ) .arg( QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); if ( !mSubsetString.isEmpty() ) sql += " where " + mSubsetString; - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5459,7 +5525,7 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5477,10 +5543,10 @@ bool QgsSpatiaLiteProvider::getSridDetails() QString sql = QStringLiteral( "SELECT auth_name||':'||auth_srid,proj4text FROM spatial_ref_sys WHERE srid=%1" ).arg( mSrid ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5536,10 +5602,10 @@ bool QgsSpatiaLiteProvider::getTableSummary() sql += " WHERE ( " + mSubsetString + ')'; } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5654,6 +5720,17 @@ QList QgsSpatiaLiteProvider::searchLayers( const QList( transaction ); +} + +QgsTransaction *QgsSpatiaLiteProvider::transaction( ) const +{ + return static_cast( mTransaction ); +} QList QgsSpatiaLiteProvider::discoverRelations( const QgsVectorLayer *self, const QList &layers ) const { @@ -5663,7 +5740,7 @@ QList QgsSpatiaLiteProvider::discoverRelations( const QgsVectorLaye int rows; int columns; char *errMsg = nullptr; - int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + int ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK ) { int nbFound = 0; @@ -6126,7 +6203,21 @@ QList< QgsDataItemProvider * > QgsSpatiaLiteProviderMetadata::dataItemProviders( return providers; } - +QgsTransaction *QgsSpatiaLiteProviderMetadata::createTransaction( const QString &connString ) +{ + const QgsDataSourceUri dsUri{ connString }; + // Cannot use QgsSpatiaLiteConnPool::instance()->acquireConnection( dsUri.database() ) }; + // because it will return a read only connection, use the (cached) connection from the + // layers instead. + QgsSqliteHandle *ds { QgsSqliteHandle::openDb( dsUri.database() ) }; + if ( !ds ) + { + QgsMessageLog::logMessage( QObject::tr( "Cannot open transaction on %1, since it is is not currently opened" ).arg( connString ), + QObject::tr( "spatialite" ), Qgis::Critical ); + return nullptr; + } + return new QgsSpatiaLiteTransaction( connString, ds ); +} QMap QgsSpatiaLiteProviderMetadata::connections( bool cached ) { diff --git a/src/providers/spatialite/qgsspatialiteprovider.h b/src/providers/spatialite/qgsspatialiteprovider.h index 73da94d09431..a9ec8c128bf3 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.h +++ b/src/providers/spatialite/qgsspatialiteprovider.h @@ -43,6 +43,8 @@ class QgsField; class QgsSqliteHandle; class QgsSpatiaLiteFeatureIterator; +class QgsSpatiaLiteTransaction; +class QgsTransaction; #include "qgsdatasourceuri.h" @@ -124,6 +126,7 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider QVariant defaultValue( int fieldId ) const override; bool skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint constraint, const QVariant &value = QVariant() ) const override; bool createAttributeIndex( int field ) override; + SpatialIndexPresence hasSpatialIndex() const override; /** * The SpatiaLite provider does its own transforms so we return @@ -219,6 +222,12 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider */ static QList searchLayers( const QList &layers, const QString &connectionInfo, const QString &tableName ); + QgsSpatiaLiteTransaction *mTransaction = nullptr; + + QgsTransaction *transaction() const override; + + void setTransaction( QgsTransaction *transaction ) override; + QgsFields mAttributeFields; //! Map of field index to default value SQL fragments @@ -324,6 +333,10 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider //! SpatiaLite minor version int mSpatialiteVersionMinor = 0; + //! Internal transaction handling (for addFeatures etc.) + int mSavepointId; + static QAtomicInt sSavepointId; + /** * internal utility functions used to handle common SQLite tasks */ @@ -381,7 +394,17 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider /** * Handles an error encountered while executing an sql statement. */ - void handleError( const QString &sql, char *errorMessage, bool rollback = false ); + void handleError( const QString &sql, char *errorMessage, const QString &savepointId ); + + /** + * Sqlite exec sql wrapper for SQL logging + */ + int exec_sql( const QString &sql, char *errMsg = nullptr ); + + /** + * Returns the sqlite handle to be used, if we are inside a transaction it will be the transaction's handle + */ + sqlite3 *sqliteHandle( ) const; friend class QgsSpatiaLiteFeatureSource; @@ -420,14 +443,14 @@ class QgsSpatiaLiteProviderMetadata final: public QgsProviderMetadata QgsAbstractProviderConnection *createConnection( const QString &name ) override; void deleteConnection( const QString &name ) override; void saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) override; + QgsTransaction *createTransaction( const QString &connString ) override; protected: QgsAbstractProviderConnection *createConnection( const QString &uri, const QVariantMap &configuration ) override; - - }; + // clazy:excludeall=qstring-allocations #endif diff --git a/src/providers/spatialite/qgsspatialiteproviderconnection.cpp b/src/providers/spatialite/qgsspatialiteproviderconnection.cpp index 131b11760495..260c14333d56 100644 --- a/src/providers/spatialite/qgsspatialiteproviderconnection.cpp +++ b/src/providers/spatialite/qgsspatialiteproviderconnection.cpp @@ -161,17 +161,17 @@ void QgsSpatiaLiteProviderConnection::renameVectorTable( const QString &schema, QString sql( QStringLiteral( "ALTER TABLE %1 RENAME TO %2" ) .arg( QgsSqliteUtils::quotedIdentifier( name ), QgsSqliteUtils::quotedIdentifier( newName ) ) ); - executeSqlPrivate( sql ); + executeSqlDirect( sql ); sql = QStringLiteral( "UPDATE geometry_columns SET f_table_name = lower(%2) WHERE lower(f_table_name) = lower(%1)" ) .arg( QgsSqliteUtils::quotedString( name ), QgsSqliteUtils::quotedString( newName ) ); - executeSqlPrivate( sql ); + executeSqlDirect( sql ); sql = QStringLiteral( "UPDATE layer_styles SET f_table_name = lower(%2) WHERE f_table_name = lower(%1)" ) .arg( QgsSqliteUtils::quotedString( name ), QgsSqliteUtils::quotedString( newName ) ); try { - executeSqlPrivate( sql ); + executeSqlDirect( sql ); } catch ( QgsProviderConnectionException &ex ) { @@ -193,9 +193,33 @@ void QgsSpatiaLiteProviderConnection::vacuum( const QString &schema, const QStri { QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); } - executeSqlPrivate( QStringLiteral( "VACUUM" ) ); + executeSqlDirect( QStringLiteral( "VACUUM" ) ); } +void QgsSpatiaLiteProviderConnection::createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options ) const +{ + Q_UNUSED( name ) + checkCapability( Capability::Vacuum ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + executeSqlPrivate( QStringLiteral( "SELECT CreateSpatialIndex(%1, %2)" ).arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( ( options.geometryColumnName ) ) ) ); +} + +bool QgsSpatiaLiteProviderConnection::spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const +{ + checkCapability( Capability::CreateSpatialIndex ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + const QList res = executeSqlPrivate( QStringLiteral( "SELECT spatial_index_enabled FROM geometry_columns WHERE lower(f_table_name) = lower(%1) AND lower(f_geometry_column) = lower(%2)" ) + .arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( geometryColumn ) ) ); + return !res.isEmpty() && !res.at( 0 ).isEmpty() && res.at( 0 ).at( 0 ).toInt() == 1; +} QList QgsSpatiaLiteProviderConnection::tables( const QString &schema, const TableFlags &flags ) const { @@ -268,7 +292,7 @@ QList QgsSpatiaLiteProviderConne QgsSpatiaLiteProviderConnection::TableProperty property; property.setTableName( tableName ); // Create a layer and get information from it - std::unique_ptr< QgsVectorLayer > vl = qgis::make_unique( dsUri.uri(), QString(), QLatin1Literal( "spatialite" ) ); + std::unique_ptr< QgsVectorLayer > vl = qgis::make_unique( dsUri.uri(), QString(), QLatin1String( "spatialite" ) ); if ( vl->isValid() ) { if ( vl->isSpatial() ) @@ -334,6 +358,8 @@ void QgsSpatiaLiteProviderConnection::setDefaultCapabilities() Capability::Spatial, Capability::TableExists, Capability::ExecuteSql, + Capability::CreateSpatialIndex, + Capability::SpatialIndexExists, }; } @@ -391,6 +417,24 @@ QList QgsSpatiaLiteProviderConnection::executeSqlPrivate( const QS return results; } +bool QgsSpatiaLiteProviderConnection::executeSqlDirect( const QString &sql ) const +{ + sqlite3_database_unique_ptr database; + int result = database.open( pathFromUri() ); + if ( result != SQLITE_OK ) + { + throw QgsProviderConnectionException( QObject::tr( "Error executing SQL %1: %2" ).arg( sql ).arg( database.errorMessage() ) ); + } + + QString errorMessage; + result = database.exec( sql, errorMessage ); + if ( result != SQLITE_OK ) + { + throw QgsProviderConnectionException( QObject::tr( "Error executing SQL %1: %2" ).arg( sql ).arg( errorMessage ) ); + } + return true; +} + QString QgsSpatiaLiteProviderConnection::pathFromUri() const { const QgsDataSourceUri dsUri( uri() ); diff --git a/src/providers/spatialite/qgsspatialiteproviderconnection.h b/src/providers/spatialite/qgsspatialiteproviderconnection.h index 131d56da8a3c..5695ba1d959f 100644 --- a/src/providers/spatialite/qgsspatialiteproviderconnection.h +++ b/src/providers/spatialite/qgsspatialiteproviderconnection.h @@ -40,6 +40,8 @@ class QgsSpatiaLiteProviderConnection : public QgsAbstractDatabaseProviderConnec void renameVectorTable( const QString &schema, const QString &name, const QString &newName ) const override; QList> executeSql( const QString &sql ) const override; void vacuum( const QString &schema, const QString &name ) const override; + void createSpatialIndex( const QString &schema, const QString &name, const QgsAbstractDatabaseProviderConnection::SpatialIndexOptions &options = QgsAbstractDatabaseProviderConnection::SpatialIndexOptions() ) const override; + bool spatialIndexExists( const QString &schema, const QString &name, const QString &geometryColumn ) const override; QList tables( const QString &schema = QString(), const TableFlags &flags = nullptr ) const override; QIcon icon() const override; @@ -50,6 +52,9 @@ class QgsSpatiaLiteProviderConnection : public QgsAbstractDatabaseProviderConnec //! Use GDAL to execute SQL QList executeSqlPrivate( const QString &sql ) const; + //! Executes SQL directly using sqlite3 -- avoids the extra consistency checks which GDAL requires when opening a spatialite database + bool executeSqlDirect( const QString &sql ) const; + //! extract the path from the DS URI (which is in "PG" form: 'dbname=\'/path_to.sqlite\' table="table_name" (geom_col_name)') QString pathFromUri() const; diff --git a/src/providers/spatialite/qgsspatialitetransaction.cpp b/src/providers/spatialite/qgsspatialitetransaction.cpp new file mode 100644 index 000000000000..ff8f323831a1 --- /dev/null +++ b/src/providers/spatialite/qgsspatialitetransaction.cpp @@ -0,0 +1,95 @@ +/*************************************************************************** + qgsspatialitetransaction.cpp - QgsSpatialiteTransaction + + --------------------- + begin : 30.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgsspatialitetransaction.h" +#include "qgslogger.h" +#include + +///@cond PRIVATE + +QAtomicInt QgsSpatiaLiteTransaction::sSavepointId = 0; + +QgsSpatiaLiteTransaction::QgsSpatiaLiteTransaction( const QString &connString, QgsSqliteHandle *sharedHandle ) + : QgsTransaction( connString ) + , mSharedHandle( sharedHandle ) +{ + if ( mSharedHandle ) + mSqliteHandle = mSharedHandle->handle(); + mSavepointId = ++sSavepointId; +} + +bool QgsSpatiaLiteTransaction::beginTransaction( QString &error, int /* statementTimeout */ ) +{ + return executeSql( QStringLiteral( "BEGIN" ), error ); +} + +bool QgsSpatiaLiteTransaction::commitTransaction( QString &error ) +{ + return executeSql( QStringLiteral( "COMMIT" ), error ); +} + +bool QgsSpatiaLiteTransaction::rollbackTransaction( QString &error ) +{ + return executeSql( QStringLiteral( "ROLLBACK" ), error ); +} + +bool QgsSpatiaLiteTransaction::executeSql( const QString &sql, QString &errorMsg, bool isDirty, const QString &name ) +{ + + if ( ! mSqliteHandle ) + { + QgsDebugMsg( QStringLiteral( "Spatialite handle is not set" ) ); + return false; + } + + if ( isDirty ) + { + createSavepoint( errorMsg ); + if ( ! errorMsg.isEmpty() ) + { + QgsDebugMsg( errorMsg ); + return false; + } + } + + char *errMsg = nullptr; + if ( sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ) != SQLITE_OK ) + { + if ( isDirty ) + { + rollbackToSavepoint( savePoints().last(), errorMsg ); + } + errorMsg = QStringLiteral( "%1\n%2" ).arg( errMsg, errorMsg ); + QgsDebugMsg( errMsg ); + sqlite3_free( errMsg ); + return false; + } + + if ( isDirty ) + { + dirtyLastSavePoint(); + emit dirtied( sql, name ); + } + + QgsDebugMsg( QStringLiteral( "... ok" ) ); + return true; +} + +sqlite3 *QgsSpatiaLiteTransaction::sqliteHandle() const +{ + return mSqliteHandle; +} +///@endcond diff --git a/src/providers/spatialite/qgsspatialitetransaction.h b/src/providers/spatialite/qgsspatialitetransaction.h new file mode 100644 index 000000000000..daf8e767cb96 --- /dev/null +++ b/src/providers/spatialite/qgsspatialitetransaction.h @@ -0,0 +1,64 @@ +/*************************************************************************** + qgsspatialitetransaction.h - QgsSpatialiteTransaction + + --------------------- + begin : 30.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 QGSSPATIALITETRANSACTION_H +#define QGSSPATIALITETRANSACTION_H + +#include "qgstransaction.h" +#include "qgsspatialiteconnection.h" +#include "qgis_sip.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsSpatiaLiteTransaction : public QgsTransaction +{ + Q_OBJECT + + public: + explicit QgsSpatiaLiteTransaction( const QString &connString, QgsSqliteHandle *sharedHandle ); + + /** + * Executes the SQL query in database. + * + * \param sql The SQL query to execute + * \param error The error or an empty string if none + * \param isDirty True to add an undo/redo command in the edition buffer, false otherwise + * \param name Name of the operation ( only used if `isDirty` is true) + */ + bool executeSql( const QString &sql, QString &error, bool isDirty = false, const QString &name = QString() ) override; + + /** + * Returns the (possibly NULL) sqlite handle + */ + sqlite3 *sqliteHandle() const; + + private: + + QgsSqliteHandle *mSharedHandle = nullptr; + int mSavepointId; + + //! SQLite handle + sqlite3 *mSqliteHandle = nullptr; + + bool beginTransaction( QString &error, int statementTimeout ) override; + bool commitTransaction( QString &error ) override; + bool rollbackTransaction( QString &error ) override; + + static QAtomicInt sSavepointId; +}; + +///@endcond +#endif // QGSSPATIALITETRANSACTION_H diff --git a/src/providers/virtual/qgsvirtuallayersqlitemodule.cpp b/src/providers/virtual/qgsvirtuallayersqlitemodule.cpp index 752c5a111186..36d32157d185 100644 --- a/src/providers/virtual/qgsvirtuallayersqlitemodule.cpp +++ b/src/providers/virtual/qgsvirtuallayersqlitemodule.cpp @@ -195,9 +195,6 @@ struct VTable mFields = mLayer ? mLayer->fields() : mProvider->fields(); QStringList sqlFields; - // add a hidden field for rtree filtering - sqlFields << QStringLiteral( "_search_frame_ HIDDEN BLOB" ); - const auto constMFields = mFields; for ( const QgsField &field : constMFields ) { @@ -229,12 +226,15 @@ struct VTable // we are using them to set the geometry type and srid // these will be reused by the provider when it will introspect the query to detect types sqlFields << QStringLiteral( "geometry geometry(%1,%2)" ).arg( provider->wkbType() ).arg( provider->crs().postgisSrid() ); + + // add a hidden field for rtree filtering + sqlFields << QStringLiteral( "_search_frame_ HIDDEN BLOB" ); } QgsAttributeList pkAttributeIndexes = provider->pkAttributeIndexes(); if ( pkAttributeIndexes.size() == 1 ) { - mPkColumn = pkAttributeIndexes.at( 0 ) + 1; + mPkColumn = pkAttributeIndexes.at( 0 ); } mCreationStr = "CREATE TABLE vtable (" + sqlFields.join( QStringLiteral( "," ) ) + ")"; @@ -491,8 +491,8 @@ int vtableBestIndex( sqlite3_vtab *pvtab, sqlite3_index_info *indexInfo ) // request for filter with a comparison operator if ( ( indexInfo->aConstraint[i].usable ) && - ( indexInfo->aConstraint[i].iColumn > 0 ) && - ( indexInfo->aConstraint[i].iColumn <= vtab->fields().count() ) && + ( indexInfo->aConstraint[i].iColumn >= 0 ) && + ( indexInfo->aConstraint[i].iColumn < vtab->fields().count() ) && ( ( indexInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_EQ ) || // if no PK ( indexInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_GT ) || ( indexInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_LE ) || @@ -508,7 +508,7 @@ int vtableBestIndex( sqlite3_vtab *pvtab, sqlite3_index_info *indexInfo ) indexInfo->idxNum = 3; // expression filter indexInfo->estimatedCost = 2.0; // probably better than no index - QString expr = QgsExpression::quotedColumnRef( vtab->fields().at( indexInfo->aConstraint[i].iColumn - 1 ).name() ); + QString expr = QgsExpression::quotedColumnRef( vtab->fields().at( indexInfo->aConstraint[i].iColumn ).name() ); switch ( indexInfo->aConstraint[i].op ) { case SQLITE_INDEX_CONSTRAINT_EQ: @@ -546,7 +546,8 @@ int vtableBestIndex( sqlite3_vtab *pvtab, sqlite3_index_info *indexInfo ) // request for rtree filtering if ( ( indexInfo->aConstraint[i].usable ) && - ( 0 == indexInfo->aConstraint[i].iColumn ) && + // request on _search_frame_ column + ( vtab->fields().count() + 1 == indexInfo->aConstraint[i].iColumn ) && ( indexInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_EQ ) ) { indexInfo->aConstraintUsage[i].argvIndex = 1; @@ -596,9 +597,12 @@ int vtableFilter( sqlite3_vtab_cursor *cursor, int idxNum, const char *idxStr, i { // rtree filter const char *blob = reinterpret_cast< const char * >( sqlite3_value_blob( argv[0] ) ); - int bytes = sqlite3_value_bytes( argv[0] ); - QgsRectangle r( spatialiteBlobBbox( blob, bytes ) ); - request.setFilterRect( r ); + if ( blob ) + { + int bytes = sqlite3_value_bytes( argv[0] ); + QgsRectangle r( spatialiteBlobBbox( blob, bytes ) ); + request.setFilterRect( r ); + } } else if ( idxNum == 3 ) { @@ -658,13 +662,9 @@ int vtableRowId( sqlite3_vtab_cursor *cursor, sqlite3_int64 *outRowid ) int vtableColumn( sqlite3_vtab_cursor *cursor, sqlite3_context *ctxt, int idx ) { VTableCursor *c = reinterpret_cast( cursor ); - if ( idx == 0 ) - { - // _search_frame_, return null - sqlite3_result_null( ctxt ); - return SQLITE_OK; - } - if ( idx == c->nColumns() + 1 ) + + // geometry column + if ( idx == c->nColumns() ) { QPair g = c->currentGeometry(); if ( !g.first ) @@ -673,7 +673,15 @@ int vtableColumn( sqlite3_vtab_cursor *cursor, sqlite3_context *ctxt, int idx ) sqlite3_result_blob( ctxt, g.first, g.second, deleteGeometryBlob ); return SQLITE_OK; } - QVariant v = c->currentAttribute( idx - 1 ); + + // _search_frame_, return null + if ( idx == c->nColumns() + 1 ) + { + sqlite3_result_null( ctxt ); + return SQLITE_OK; + } + + QVariant v = c->currentAttribute( idx ); if ( v.isNull() ) { sqlite3_result_null( ctxt ); diff --git a/src/providers/wfs/qgsbackgroundcachedfeatureiterator.cpp b/src/providers/wfs/qgsbackgroundcachedfeatureiterator.cpp index 9b404a6a1ba7..c9bbdf3c3d3c 100644 --- a/src/providers/wfs/qgsbackgroundcachedfeatureiterator.cpp +++ b/src/providers/wfs/qgsbackgroundcachedfeatureiterator.cpp @@ -257,25 +257,26 @@ void QgsThreadedFeatureDownloader::run() // ------------------------- -static QgsFeatureRequest addSubsetToFeatureRequest( const QgsFeatureRequest &requestIn, - const QgsBackgroundCachedSharedData *shared ) -{ - if ( shared->clientSideFilterExpression().isEmpty() ) - { - return requestIn; - } - QgsFeatureRequest requestOut( requestIn ); - requestOut.combineFilterExpression( shared->clientSideFilterExpression() ); - return requestOut; -} - QgsBackgroundCachedFeatureIterator::QgsBackgroundCachedFeatureIterator( QgsBackgroundCachedFeatureSource *source, bool ownSource, std::shared_ptr shared, const QgsFeatureRequest &request ) - : QgsAbstractFeatureIteratorFromSource( source, ownSource, addSubsetToFeatureRequest( request, shared.get() ) ) + : QgsAbstractFeatureIteratorFromSource( source, ownSource, request ) , mShared( shared ) { + if ( !shared->clientSideFilterExpression().isEmpty() ) + { + // backup current request because combine filter expression will remove the fid(s) filtering + if ( mRequest.filterType() == QgsFeatureRequest::FilterFid || mRequest.filterType() == QgsFeatureRequest::FilterFids ) + { + mAdditionalRequest = QgsFeatureRequest( shared->clientSideFilterExpression() ); + } + else + { + mRequest.combineFilterExpression( shared->clientSideFilterExpression() ); + } + } + if ( mRequest.destinationCrs().isValid() && mRequest.destinationCrs() != mShared->sourceCrs() ) { mTransform = QgsCoordinateTransform( mShared->sourceCrs(), mRequest.destinationCrs(), mRequest.transformContext() ); @@ -601,6 +602,11 @@ bool QgsBackgroundCachedFeatureIterator::fetchFeature( QgsFeature &f ) continue; } + if ( !mAdditionalRequest.acceptFeature( cachedFeature ) ) + { + continue; + } + copyFeature( cachedFeature, f, true ); geometryToDestinationCrs( f, mTransform ); @@ -692,6 +698,11 @@ bool QgsBackgroundCachedFeatureIterator::fetchFeature( QgsFeature &f ) continue; } + if ( !mAdditionalRequest.acceptFeature( feat ) ) + { + continue; + } + copyFeature( feat, f, false ); return true; } diff --git a/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h b/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h index 531b399ec9df..735a8206301f 100644 --- a/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h +++ b/src/providers/wfs/qgsbackgroundcachedfeatureiterator.h @@ -320,6 +320,9 @@ class QgsBackgroundCachedFeatureIterator final: public QObject, QgsCoordinateTransform mTransform; QgsRectangle mFilterRect; + //! typically to save a FilterFid/FilterFids request that will not be captured by mRequest + QgsFeatureRequest mAdditionalRequest; + ///////////////// METHODS //! Translate mRequest to a request compatible of the Spatialite cache diff --git a/src/providers/wms/CMakeLists.txt b/src/providers/wms/CMakeLists.txt index 834bef7dd077..331f7c982c51 100644 --- a/src/providers/wms/CMakeLists.txt +++ b/src/providers/wms/CMakeLists.txt @@ -1,5 +1,4 @@ SET (WMS_SRCS - qgsmbtilesreader.cpp qgswmscapabilities.cpp qgswmsprovider.cpp qgswmsconnection.cpp diff --git a/src/providers/wms/qgswmscapabilities.cpp b/src/providers/wms/qgswmscapabilities.cpp index 5037b019be7f..dd840eeab3a7 100644 --- a/src/providers/wms/qgswmscapabilities.cpp +++ b/src/providers/wms/qgswmscapabilities.cpp @@ -85,7 +85,7 @@ bool QgsWmsSettings::parseUri( const QString &uriString ) if ( uri.param( QStringLiteral( "type" ) ) == QLatin1String( "wmst" ) ) { mIsTemporal = true; - mTemporalExtent = uri.param( QStringLiteral( "time" ) ); + mTemporalExtent = uri.param( QStringLiteral( "timeDimensionExtent" ) ); mTimeDimensionExtent = parseTemporalExtent( mTemporalExtent ); if ( mTimeDimensionExtent.datesResolutionList.first().dates.dateTimes.size() > 0 ) @@ -100,9 +100,9 @@ bool QgsWmsSettings::parseUri( const QString &uriString ) mDateTimes = dateTimesFromExtent( mTimeDimensionExtent ); - if ( uri.param( QStringLiteral( "reference_time" ) ) != QString() ) + if ( uri.param( QStringLiteral( "referenceTimeDimensionExtent" ) ) != QString() ) { - QString referenceExtent = uri.param( QStringLiteral( "reference_time" ) ); + QString referenceExtent = uri.param( QStringLiteral( "referenceTimeDimensionExtent" ) ); mReferenceTimeDimensionExtent = parseTemporalExtent( referenceExtent ); @@ -295,6 +295,7 @@ QList QgsWmsSettings::dateTimesFromExtent( QgsWmstDimensionExtent dim { QDateTime first = QDateTime( pair.dates.dateTimes.at( 0 ) ); QDateTime last = QDateTime( pair.dates.dateTimes.at( 1 ) ); + dates.append( first ); while ( first < last ) { @@ -334,6 +335,37 @@ QDateTime QgsWmsSettings::addTime( QDateTime dateTime, QgsWmstResolution resolut return resultDateTime; } +QDateTime QgsWmsSettings::findLeastClosestDateTime( QDateTime dateTime ) const +{ + QDateTime closest = dateTime; + long long min = std::numeric_limits::max(); + + if ( !dateTime.isValid() || mDateTimes.contains( dateTime ) ) + return closest; + + for ( QDateTime current : mDateTimes ) + { + if ( !current.isValid() ) + continue; + + long long difference = dateTime.secsTo( current ); + + // The datetimes list is sorted, if difference is increasing or + // it is above zero means search is now looking for greater than + // datetimes then search will have to stop. + if ( difference > 0 || std::abs( difference ) > min ) + break; + + if ( std::abs( difference ) < min ) + { + min = std::abs( difference ); + closest = current; + } + } + + return closest; +} + QgsWmstResolution QgsWmsSettings::parseWmstResolution( QString item ) { QgsWmstResolution resolution; @@ -416,10 +448,10 @@ QgsWmstResolution QgsWmsSettings::parseWmstResolution( QString item ) QDateTime QgsWmsSettings::parseWmstDateTimes( QString item ) { - // Standard item will have YYYY-MM-DDThh:mm:ss.SSSZ + // Standard item will have YYYY-MM-DDTHH:mm:ss.SSSZ // format a Qt::ISODateWithMs - QString format = "YYYY-MM-DDThh:mm:ss.SSSZ"; + QString format = "yyyy-MM-ddTHH:mm:ss.SSSZ"; // Check if it does not have time part if ( !item.contains( 'T' ) ) diff --git a/src/providers/wms/qgswmscapabilities.h b/src/providers/wms/qgswmscapabilities.h index bd92c9613dd4..e0c80a8069ba 100644 --- a/src/providers/wms/qgswmscapabilities.h +++ b/src/providers/wms/qgswmscapabilities.h @@ -785,6 +785,14 @@ class QgsWmsSettings QDateTime addTime( QDateTime dateTime, QgsWmstResolution resolution ); + /** + * Finds the least closest datetime from list of available datetimes + * with the given \a dateTime. + * + * Returns the passed \a dateTime if it is found in the available datetimes. + */ + QDateTime findLeastClosestDateTime( QDateTime dateTime ) const; + protected: QgsWmsParserSettings mParserSettings; diff --git a/src/providers/wms/qgswmsdataitems.cpp b/src/providers/wms/qgswmsdataitems.cpp index c8b85cd576ac..7ecdf7e7d3c7 100644 --- a/src/providers/wms/qgswmsdataitems.cpp +++ b/src/providers/wms/qgswmsdataitems.cpp @@ -267,14 +267,89 @@ bool QgsWMSConnectionItem::equal( const QgsDataItem *other ) } // --------------------------------------------------------------------------- - -QgsWMSLayerCollectionItem::QgsWMSLayerCollectionItem( QgsDataItem *parent, QString name, QString path, const QgsWmsCapabilitiesProperty &capabilitiesProperty, const QgsDataSourceUri &dataSourceUri, const QgsWmsLayerProperty &layerProperty ) - : QgsDataCollectionItem( parent, name, path, QStringLiteral( "WMS" ) ) - , mCapabilitiesProperty( capabilitiesProperty ) +QgsWMSItemBase::QgsWMSItemBase( const QgsWmsCapabilitiesProperty &capabilitiesProperty, const QgsDataSourceUri &dataSourceUri, const QgsWmsLayerProperty &layerProperty ) + : mCapabilitiesProperty( capabilitiesProperty ) , mDataSourceUri( dataSourceUri ) , mLayerProperty( layerProperty ) +{ +} + +QString QgsWMSItemBase::createUri() +{ + if ( mLayerProperty.name.isEmpty() ) + return QString(); // layer collection + + // Number of styles must match number of layers + mDataSourceUri.setParam( QStringLiteral( "layers" ), mLayerProperty.name ); + QString style = !mLayerProperty.style.isEmpty() ? mLayerProperty.style.at( 0 ).name : QString(); + mDataSourceUri.setParam( QStringLiteral( "styles" ), style ); + + // Check for layer dimensions + for ( const QgsWmsDimensionProperty &dimension : qgis::as_const( mLayerProperty.dimensions ) ) + { + // add temporal dimensions only + if ( dimension.name == QLatin1String( "time" ) || dimension.name == QLatin1String( "reference_time" ) ) + { + QString name = dimension.name == QLatin1String( "time" ) ? QString( "timeDimensionExtent" ) : QString( "referenceTimeDimensionExtent" ); + + if ( !( mDataSourceUri.param( QLatin1String( "type" ) ) == QLatin1String( "wmst" ) ) ) + mDataSourceUri.setParam( QLatin1String( "type" ), QLatin1String( "wmst" ) ); + mDataSourceUri.setParam( name, dimension.extent ); + } + } + + // WMS-T defaults settings + if ( mDataSourceUri.param( QLatin1String( "type" ) ) == QLatin1String( "wmst" ) ) + { + mDataSourceUri.setParam( QLatin1String( "temporalSource" ), QLatin1String( "provider" ) ); + mDataSourceUri.setParam( QLatin1String( "enableTime" ), QLatin1String( "yes" ) ); + } + + QString format; + // get first supported by qt and server + QVector formats( QgsWmsProvider::supportedFormats() ); + const auto constFormats = formats; + for ( const QgsWmsSupportedFormat &f : constFormats ) + { + if ( mCapabilitiesProperty.capability.request.getMap.format.indexOf( f.format ) >= 0 ) + { + format = f.format; + break; + } + } + mDataSourceUri.setParam( QStringLiteral( "format" ), format ); + + QString crs; + // get first known if possible + QgsCoordinateReferenceSystem testCrs; + for ( const QString &c : qgis::as_const( mLayerProperty.crs ) ) + { + testCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( c ); + if ( testCrs.isValid() ) + { + crs = c; + break; + } + } + if ( crs.isEmpty() && !mLayerProperty.crs.isEmpty() ) + { + crs = mLayerProperty.crs[0]; + } + mDataSourceUri.setParam( QStringLiteral( "crs" ), crs ); + //uri = rasterLayerPath + "|layers=" + layers.join( "," ) + "|styles=" + styles.join( "," ) + "|format=" + format + "|crs=" + crs; + + return mDataSourceUri.encodedUri(); +} + + +// --------------------------------------------------------------------------- + +QgsWMSLayerCollectionItem::QgsWMSLayerCollectionItem( QgsDataItem *parent, QString name, QString path, const QgsWmsCapabilitiesProperty &capabilitiesProperty, const QgsDataSourceUri &dataSourceUri, const QgsWmsLayerProperty &layerProperty ) + : QgsDataCollectionItem( parent, name, path, QStringLiteral( "wms" ) ) + , QgsWMSItemBase( capabilitiesProperty, dataSourceUri, layerProperty ) { mIconName = QStringLiteral( "mIconWms.svg" ); + mUri = createUri(); // Populate everything, it costs nothing, all info about layers is collected for ( const QgsWmsLayerProperty &layerProperty : qgis::as_const( mLayerProperty.layer ) ) @@ -334,17 +409,35 @@ bool QgsWMSLayerCollectionItem::equal( const QgsDataItem *other ) } } - return ( mPath == otherCollectionItem->mPath && mName == otherCollectionItem->mName ); } +bool QgsWMSLayerCollectionItem::hasDragEnabled() const +{ + if ( !mLayerProperty.name.isEmpty() ) + return true; + return false; +} + +QgsMimeDataUtils::Uri QgsWMSLayerCollectionItem::mimeUri() const +{ + QgsMimeDataUtils::Uri u; + + u.layerType = QStringLiteral( "raster" ); + u.providerKey = providerKey(); + u.name = name(); + u.uri = mUri; + u.supportedCrs = mLayerProperty.crs; + u.supportedFormats = mCapabilitiesProperty.capability.request.getMap.format; + + return u; +} + // --------------------------------------------------------------------------- QgsWMSLayerItem::QgsWMSLayerItem( QgsDataItem *parent, QString name, QString path, const QgsWmsCapabilitiesProperty &capabilitiesProperty, const QgsDataSourceUri &dataSourceUri, const QgsWmsLayerProperty &layerProperty ) : QgsLayerItem( parent, name, path, QString(), QgsLayerItem::Raster, QStringLiteral( "wms" ) ) - , mCapabilitiesProperty( capabilitiesProperty ) - , mDataSourceUri( dataSourceUri ) - , mLayerProperty( layerProperty ) + , QgsWMSItemBase( capabilitiesProperty, dataSourceUri, layerProperty ) { mSupportedCRS = mLayerProperty.crs; mSupportFormats = mCapabilitiesProperty.capability.request.getMap.format; @@ -355,64 +448,6 @@ QgsWMSLayerItem::QgsWMSLayerItem( QgsDataItem *parent, QString name, QString pat setState( Populated ); } -QString QgsWMSLayerItem::createUri() -{ - if ( mLayerProperty.name.isEmpty() ) - return QString(); // layer collection - - // Number of styles must match number of layers - mDataSourceUri.setParam( QStringLiteral( "layers" ), mLayerProperty.name ); - QString style = !mLayerProperty.style.isEmpty() ? mLayerProperty.style.at( 0 ).name : QString(); - mDataSourceUri.setParam( QStringLiteral( "styles" ), style ); - - // Check for layer dimensions - for ( const QgsWmsDimensionProperty &dimension : qgis::as_const( mLayerProperty.dimensions ) ) - { - // add temporal dimensions only - if ( dimension.name == QLatin1String( "time" ) || dimension.name == QLatin1String( "reference_time" ) ) - { - if ( !( mDataSourceUri.param( QLatin1String( "type" ) ) == QLatin1String( "wmst" ) ) ) - mDataSourceUri.setParam( QLatin1String( "type" ), QLatin1String( "wmst" ) ); - mDataSourceUri.setParam( dimension.name, dimension.extent ); - } - } - - QString format; - // get first supported by qt and server - QVector formats( QgsWmsProvider::supportedFormats() ); - const auto constFormats = formats; - for ( const QgsWmsSupportedFormat &f : constFormats ) - { - if ( mCapabilitiesProperty.capability.request.getMap.format.indexOf( f.format ) >= 0 ) - { - format = f.format; - break; - } - } - mDataSourceUri.setParam( QStringLiteral( "format" ), format ); - - QString crs; - // get first known if possible - QgsCoordinateReferenceSystem testCrs; - for ( const QString &c : qgis::as_const( mLayerProperty.crs ) ) - { - testCrs = QgsCoordinateReferenceSystem::fromOgcWmsCrs( c ); - if ( testCrs.isValid() ) - { - crs = c; - break; - } - } - if ( crs.isEmpty() && !mLayerProperty.crs.isEmpty() ) - { - crs = mLayerProperty.crs[0]; - } - mDataSourceUri.setParam( QStringLiteral( "crs" ), crs ); - //uri = rasterLayerPath + "|layers=" + layers.join( "," ) + "|styles=" + styles.join( "," ) + "|format=" + format + "|crs=" + crs; - - return mDataSourceUri.encodedUri(); -} - bool QgsWMSLayerItem::equal( const QgsDataItem *other ) { if ( type() != other->type() ) @@ -428,7 +463,6 @@ bool QgsWMSLayerItem::equal( const QgsDataItem *other ) if ( !mLayerProperty.equal( otherLayer->mLayerProperty ) ) return false; - return ( mPath == otherLayer->mPath && mName == otherLayer->mName ); } diff --git a/src/providers/wms/qgswmsdataitems.h b/src/providers/wms/qgswmsdataitems.h index 0c93ae1ecaa4..44251e01d76a 100644 --- a/src/providers/wms/qgswmsdataitems.h +++ b/src/providers/wms/qgswmsdataitems.h @@ -41,12 +41,36 @@ class QgsWMSConnectionItem : public QgsDataCollectionItem QgsWmsCapabilitiesDownload *mCapabilitiesDownload = nullptr; }; +/** + * Base class which contains similar basic attributes and functions needed by the + * wms collection layers and child layers. + * + */ +class QgsWMSItemBase +{ + public: + QgsWMSItemBase( const QgsWmsCapabilitiesProperty &capabilitiesProperty, + const QgsDataSourceUri &dataSourceUri, + const QgsWmsLayerProperty &layerProperty ); + + QString createUri(); + + //! Stores GetCapabilities response + QgsWmsCapabilitiesProperty mCapabilitiesProperty; + + //! Stores WMS connection information + QgsDataSourceUri mDataSourceUri; + + //! WMS Layer properties, can be inherited by subsidiary layers + QgsWmsLayerProperty mLayerProperty; +}; + /** * \brief WMS Layer Collection. * - * This collection contains a WMS Layer element that can enclose other layers + * This collection contains a WMS Layer element that can enclose other layers. */ -class QgsWMSLayerCollectionItem : public QgsDataCollectionItem +class QgsWMSLayerCollectionItem : public QgsDataCollectionItem, public QgsWMSItemBase { Q_OBJECT public: @@ -57,14 +81,13 @@ class QgsWMSLayerCollectionItem : public QgsDataCollectionItem bool equal( const QgsDataItem *other ) override; - //! Stores GetCapabilities response - QgsWmsCapabilitiesProperty mCapabilitiesProperty; + bool hasDragEnabled() const override; - //! Stores WMS connection information - QgsDataSourceUri mDataSourceUri; + QgsMimeDataUtils::Uri mimeUri() const override; - //! WMS Layer properties, can be inherited by subsidiary layers - QgsWmsLayerProperty mLayerProperty; + protected: + //! The URI + QString mUri; // QgsDataItem interface public: @@ -73,7 +96,7 @@ class QgsWMSLayerCollectionItem : public QgsDataCollectionItem // WMS Layers may be nested, so that they may be both QgsDataCollectionItem and QgsLayerItem // We have to use QgsDataCollectionItem and support layer methods if necessary -class QgsWMSLayerItem : public QgsLayerItem +class QgsWMSLayerItem : public QgsLayerItem, public QgsWMSItemBase { Q_OBJECT public: @@ -83,11 +106,7 @@ class QgsWMSLayerItem : public QgsLayerItem const QgsWmsLayerProperty &layerProperty ); bool equal( const QgsDataItem *other ) override; - QString createUri(); - QgsWmsCapabilitiesProperty mCapabilitiesProperty; - QgsDataSourceUri mDataSourceUri; - QgsWmsLayerProperty mLayerProperty; }; class QgsWMTSLayerItem : public QgsLayerItem diff --git a/src/providers/wms/qgswmsprovider.cpp b/src/providers/wms/qgswmsprovider.cpp index 291de55bff5b..55a7663b15f5 100644 --- a/src/providers/wms/qgswmsprovider.cpp +++ b/src/providers/wms/qgswmsprovider.cpp @@ -1082,25 +1082,54 @@ QUrl QgsWmsProvider::createRequestUrlWMS( const QgsRectangle &viewExtent, int pi void QgsWmsProvider::addWmstParameters( QUrlQuery &query ) { QgsDateTimeRange range = temporalCapabilities()->requestedTemporalRange(); - QString format = "yyyy-MM-ddThh:mm:ssZ"; + QString format { QStringLiteral( "yyyy-MM-ddThh:mm:ssZ" ) }; + QgsDataSourceUri uri { dataSourceUri() }; - switch ( temporalCapabilities()->intervalHandlingMethod() ) + if ( range.isInfinite() ) { - case QgsRasterDataProviderTemporalCapabilities::MatchUsingWholeRange: - break; - case QgsRasterDataProviderTemporalCapabilities::MatchExactUsingStartOfRange: - range = QgsDateTimeRange( range.begin(), range.begin() ); - break; - case QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange: - range = QgsDateTimeRange( range.end(), range.end() ); - break; + if ( uri.hasParam( QStringLiteral( "time" ) ) && + !uri.param( QStringLiteral( "time" ) ).isEmpty() ) + { + QString time = uri.param( QStringLiteral( "time" ) ); + QStringList timeParts = time.split( '/' ); + + QDateTime start = QDateTime::fromString( timeParts.at( 0 ), Qt::ISODateWithMs ); + QDateTime end = QDateTime::fromString( timeParts.at( 1 ), Qt::ISODateWithMs ); + + range = QgsDateTimeRange( start, end ); + } } - if ( !temporalCapabilities()->isTimeEnabled() ) + if ( uri.param( QStringLiteral( "enableTime" ) ) == QLatin1String( "no" ) ) format = "yyyy-MM-dd"; if ( range.begin().isValid() && range.end().isValid() ) { + switch ( temporalCapabilities()->intervalHandlingMethod() ) + { + case QgsRasterDataProviderTemporalCapabilities::MatchUsingWholeRange: + break; + case QgsRasterDataProviderTemporalCapabilities::MatchExactUsingStartOfRange: + range = QgsDateTimeRange( range.begin(), range.begin() ); + break; + case QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange: + range = QgsDateTimeRange( range.end(), range.end() ); + break; + case QgsRasterDataProviderTemporalCapabilities::FindClosestMatchToStartOfRange: + { + QDateTime dateTimeStart = mSettings.findLeastClosestDateTime( range.begin() ); + range = QgsDateTimeRange( dateTimeStart, dateTimeStart ); + break; + } + + case QgsRasterDataProviderTemporalCapabilities::FindClosestMatchToEndOfRange: + { + QDateTime dateTimeEnd = mSettings.findLeastClosestDateTime( range.end() ); + range = QgsDateTimeRange( dateTimeEnd, dateTimeEnd ); + break; + } + } + if ( range.begin() == range.end() ) setQueryItem( query, QStringLiteral( "TIME" ), range.begin().toString( format ) ); @@ -1113,23 +1142,19 @@ void QgsWmsProvider::addWmstParameters( QUrlQuery &query ) setQueryItem( query, QStringLiteral( "TIME" ), extent ); } } + // If the data provider has bi-temporal properties and they are enabled - if ( temporalCapabilities()->isReferenceEnable() ) + if ( uri.hasParam( QStringLiteral( "reference_time" ) ) && + !uri.param( QStringLiteral( "reference_time" ) ).isEmpty() ) { - QgsDateTimeRange referenceRange = temporalCapabilities()->requestedReferenceTemporalRange(); - if ( referenceRange.begin().isValid() && referenceRange.end().isValid() ) - { - if ( referenceRange.begin() == referenceRange.end() ) - setQueryItem( query, QStringLiteral( "DIM_REFERENCE_TIME" ), - referenceRange.begin().toString( format ) ); - else - { - QString extent = referenceRange.begin().toString( format ); - extent.append( "/" ); - extent.append( referenceRange.end().toString( format ) ); + QString time = uri.param( QStringLiteral( "reference_time" ) ); - setQueryItem( query, QStringLiteral( "DIM_REFERENCE_TIME" ), extent ); - } + QDateTime dateTime = QDateTime::fromString( time, Qt::ISODateWithMs ); + + if ( dateTime.isValid() ) + { + setQueryItem( query, QStringLiteral( "DIM_REFERENCE_TIME" ), + dateTime.toString( format ) ); } } } diff --git a/src/providers/wms/qgswmssourceselect.cpp b/src/providers/wms/qgswmssourceselect.cpp index f69286df53de..4f519e87ed02 100644 --- a/src/providers/wms/qgswmssourceselect.cpp +++ b/src/providers/wms/qgswmssourceselect.cpp @@ -277,6 +277,7 @@ void QgsWMSSourceSelect::clear() bool QgsWMSSourceSelect::populateLayerList( const QgsWmsCapabilities &capabilities ) { const QVector layers = capabilities.supportedLayers(); + mLayerProperties = layers; bool first = true; QSet alreadyAddedLabels; @@ -515,6 +516,8 @@ void QgsWMSSourceSelect::addButtonClicked() collectSelectedLayers( layers, styles, titles ); crs = mCRS; format = mFormats[ mImageFormatGroup->checkedId()].format; + + collectDimensions( layers, uri ); } else { @@ -1020,6 +1023,39 @@ void QgsWMSSourceSelect::collectSelectedLayers( QStringList &layers, QStringList } } +void QgsWMSSourceSelect::collectDimensions( QStringList &layers, QgsDataSourceUri &uri ) +{ + for ( const QgsWmsLayerProperty layerProperty : mLayerProperties ) + { + if ( layerProperty.name == layers.join( ',' ) ) + { + // Check for layer dimensions + for ( const QgsWmsDimensionProperty &dimension : qgis::as_const( layerProperty.dimensions ) ) + { + // add temporal dimensions only + if ( dimension.name == QLatin1String( "time" ) || + dimension.name == QLatin1String( "reference_time" ) ) + { + QString name = dimension.name == QLatin1String( "time" ) ? + QStringLiteral( "timeDimensionExtent" ) : QStringLiteral( "referenceTimeDimensionExtent" ); + + if ( !( uri.param( QLatin1String( "type" ) ) == QLatin1String( "wmst" ) ) ) + uri.setParam( QLatin1String( "type" ), QLatin1String( "wmst" ) ); + uri.setParam( name, dimension.extent ); + } + } + + // WMS-T defaults settings + if ( uri.param( QLatin1String( "type" ) ) == QLatin1String( "wmst" ) ) + { + uri.setParam( QLatin1String( "temporalSource" ), QLatin1String( "provider" ) ); + uri.setParam( QLatin1String( "enableTime" ), QLatin1String( "yes" ) ); + } + + } + } +} + QString QgsWMSSourceSelect::selectedImageEncoding() { // TODO: Match this hard coded list to the list of formats Qt reports it can actually handle. diff --git a/src/providers/wms/qgswmssourceselect.h b/src/providers/wms/qgswmssourceselect.h index 207c13f7763d..8bdca3808e36 100644 --- a/src/providers/wms/qgswmssourceselect.h +++ b/src/providers/wms/qgswmssourceselect.h @@ -178,6 +178,13 @@ class QgsWMSSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsWM void enableLayersForCrs( QTreeWidgetItem *item ); void collectSelectedLayers( QStringList &layers, QStringList &styles, QStringList &titles ); + + /** + * Collects the available dimensions from the WMS layers and adds them + * to the passed \a uri. + */ + void collectDimensions( QStringList &layers, QgsDataSourceUri &uri ); + QString selectedImageEncoding(); QList mCurrentSelection; @@ -185,6 +192,9 @@ class QgsWMSSourceSelect : public QgsAbstractDataSourceWidget, private Ui::QgsWM QList mTileLayers; + //! Stores all the layers properties from the service capabilities. + QVector mLayerProperties; + private slots: void btnSearch_clicked(); void btnAddWMS_clicked(); diff --git a/src/quickgui/CMakeLists.txt b/src/quickgui/CMakeLists.txt index 268161a644c3..2b31d4599f20 100644 --- a/src/quickgui/CMakeLists.txt +++ b/src/quickgui/CMakeLists.txt @@ -120,13 +120,6 @@ GENERATE_EXPORT_HEADER( ) # Installation -INSTALL(TARGETS qgis_quick - RUNTIME DESTINATION ${QGIS_BIN_DIR} - LIBRARY DESTINATION ${QGIS_LIB_DIR} - ARCHIVE DESTINATION ${QGIS_LIB_DIR} - FRAMEWORK DESTINATION ${QGIS_FW_SUBDIR} - PUBLIC_HEADER DESTINATION ${QGIS_INCLUDE_DIR}) - IF(NOT APPLE) INSTALL(FILES ${CMAKE_CURRENT_BINARY_DIR}/qgis_quick.h ${QGIS_QUICK_GUI_HDRS} ${QGIS_QUICK_GUI_MOC_HDRS} DESTINATION ${QGIS_INCLUDE_DIR}) ELSE(NOT APPLE) @@ -151,6 +144,13 @@ ELSE(NOT APPLE) ) ENDIF(NOT APPLE) +INSTALL(TARGETS qgis_quick + RUNTIME DESTINATION ${QGIS_BIN_DIR} + LIBRARY DESTINATION ${QGIS_LIB_DIR} + ARCHIVE DESTINATION ${QGIS_LIB_DIR} + FRAMEWORK DESTINATION ${QGIS_FW_SUBDIR} + PUBLIC_HEADER DESTINATION ${QGIS_INCLUDE_DIR}) + ############################################################ # qgis_quick_plugin module (QML) library ADD_SUBDIRECTORY(plugin) diff --git a/src/server/qgsserverquerystringparameter.cpp b/src/server/qgsserverquerystringparameter.cpp index a37305e74236..e862ec2a4fee 100644 --- a/src/server/qgsserverquerystringparameter.cpp +++ b/src/server/qgsserverquerystringparameter.cpp @@ -123,6 +123,10 @@ json QgsServerQueryStringParameter::data() const { dataType = "string"; } + else if ( dataType == "double" ) + { + dataType = "number"; + } return { { "name", nameString }, diff --git a/src/server/services/wfs/qgswfsdescribefeaturetype.cpp b/src/server/services/wfs/qgswfsdescribefeaturetype.cpp index 4faf3d60798b..0a0dcdab77ef 100644 --- a/src/server/services/wfs/qgswfsdescribefeaturetype.cpp +++ b/src/server/services/wfs/qgswfsdescribefeaturetype.cpp @@ -111,7 +111,7 @@ namespace QgsWfs QStringList typeNameList; QDomDocument queryDoc; QString errorMsg; - if ( queryDoc.setContent( parameters.value( QStringLiteral( "REQUEST_BODY" ) ), true, &errorMsg ) ) + if ( queryDoc.setContent( request.data(), true, &errorMsg ) ) { //read doc QDomElement queryDocElem = queryDoc.documentElement(); diff --git a/src/server/services/wfs/qgswfsgetfeature.cpp b/src/server/services/wfs/qgswfsgetfeature.cpp index d90a86fe663f..f1661fee3da7 100644 --- a/src/server/services/wfs/qgswfsgetfeature.cpp +++ b/src/server/services/wfs/qgswfsgetfeature.cpp @@ -103,7 +103,7 @@ namespace QgsWfs QDomDocument doc; QString errorMsg; - if ( doc.setContent( mRequestParameters.value( QStringLiteral( "REQUEST_BODY" ) ), true, &errorMsg ) ) + if ( doc.setContent( request.data(), true, &errorMsg ) ) { QDomElement docElem = doc.documentElement(); aRequest = parseGetFeatureRequestBody( docElem, project ); diff --git a/src/server/services/wfs/qgswfstransaction.cpp b/src/server/services/wfs/qgswfstransaction.cpp index a83bcce3dfd4..fa224fe7abd2 100644 --- a/src/server/services/wfs/qgswfstransaction.cpp +++ b/src/server/services/wfs/qgswfstransaction.cpp @@ -72,7 +72,7 @@ namespace QgsWfs QDomDocument doc; QString errorMsg; - if ( doc.setContent( parameters.value( QStringLiteral( "REQUEST_BODY" ) ), true, &errorMsg ) ) + if ( doc.setContent( request.data(), true, &errorMsg ) ) { QDomElement docElem = doc.documentElement(); aRequest = parseTransactionRequestBody( docElem, project ); diff --git a/src/server/services/wfs/qgswfstransaction_1_0_0.cpp b/src/server/services/wfs/qgswfstransaction_1_0_0.cpp index 21a042f4114d..82d69547b318 100644 --- a/src/server/services/wfs/qgswfstransaction_1_0_0.cpp +++ b/src/server/services/wfs/qgswfstransaction_1_0_0.cpp @@ -70,7 +70,7 @@ namespace QgsWfs QDomDocument doc; QString errorMsg; - if ( doc.setContent( parameters.value( QStringLiteral( "REQUEST_BODY" ) ), true, &errorMsg ) ) + if ( doc.setContent( request.data(), true, &errorMsg ) ) { QDomElement docElem = doc.documentElement(); aRequest = parseTransactionRequestBody( docElem, project ); diff --git a/src/server/services/wfs3/qgswfs3handlers.cpp b/src/server/services/wfs3/qgswfs3handlers.cpp index 890b324576b4..7e283758c57a 100644 --- a/src/server/services/wfs3/qgswfs3handlers.cpp +++ b/src/server/services/wfs3/qgswfs3handlers.cpp @@ -1031,9 +1031,13 @@ json QgsWfs3CollectionsItemsHandler::schema( const QgsServerApiContext &context "201", { { "description", "A new feature was successfully added to the collection" } }, + }, + { "403", { { "description", "Forbidden: the operation requested was not authorized" } }, + }, + { "500", { { "description", "Posted data could not be parsed correctly or another error occurred" } } @@ -1963,12 +1967,16 @@ json QgsWfs3CollectionsFeatureHandler::schema( const QgsServerApiContext &contex "200", { { "description", "The feature was successfully updated" } }, + }, + { "403", { { "description", "Forbidden: the operation requested was not authorized" } }, + }, + { "500", { { "description", "Posted data could not be parsed correctly or another error occurred" } - } + }, }, { "default", defaultResponse() } } @@ -1987,12 +1995,16 @@ json QgsWfs3CollectionsFeatureHandler::schema( const QgsServerApiContext &contex "200", { { "description", "The feature was successfully updated" } }, + }, + { "403", { { "description", "Forbidden: the operation requested was not authorized" } }, + }, + { "500", { { "description", "Posted data could not be parsed correctly or another error occurred" } - } + }, }, { "default", defaultResponse() } } @@ -2011,9 +2023,13 @@ json QgsWfs3CollectionsFeatureHandler::schema( const QgsServerApiContext &contex "201", { { "description", "The feature was successfully deleted from the collection" } }, + }, + { "403", { { "description", "Forbidden: the operation requested was not authorized" } }, + }, + { "500", { { "description", "Posted data could not be parsed correctly or another error occurred" } } diff --git a/src/server/services/wms/qgslayerrestorer.cpp b/src/server/services/wms/qgslayerrestorer.cpp index f144f6792507..3574a7e21466 100644 --- a/src/server/services/wms/qgslayerrestorer.cpp +++ b/src/server/services/wms/qgslayerrestorer.cpp @@ -73,6 +73,7 @@ QgsLayerRestorer::QgsLayerRestorer( const QList &layers ) } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } @@ -128,6 +129,7 @@ QgsLayerRestorer::~QgsLayerRestorer() } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/server/services/wms/qgswmsdescribelayer.cpp b/src/server/services/wms/qgswmsdescribelayer.cpp index eef78646bd3a..eb6944fbebb4 100644 --- a/src/server/services/wms/qgswmsdescribelayer.cpp +++ b/src/server/services/wms/qgswmsdescribelayer.cpp @@ -191,6 +191,7 @@ namespace QgsWms } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 9f365e6b7331..6234bf21f83f 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -891,6 +891,7 @@ namespace QgsWms if ( projectSettings ) { layerElem.setAttribute( QStringLiteral( "visible" ), treeNode->isVisible() ); + layerElem.setAttribute( QStringLiteral( "expanded" ), treeNode->isExpanded() ); } if ( treeNode->nodeType() == QgsLayerTreeNode::NodeGroup ) @@ -1956,6 +1957,7 @@ namespace QgsWms } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/server/services/wms/qgswmsparameters.cpp b/src/server/services/wms/qgswmsparameters.cpp index 43b2634f8edc..00d1d85b15db 100644 --- a/src/server/services/wms/qgswmsparameters.cpp +++ b/src/server/services/wms/qgswmsparameters.cpp @@ -59,7 +59,7 @@ namespace QgsWms if ( !ok ) { - const QString msg = QString( "%1 ('%2') cannot be converted into a list of geometries" ).arg( name( mName ), toString(), typeName() ); + const QString msg = QString( "%1 ('%2') cannot be converted into a list of geometries" ).arg( name( mName ), toString() ); QgsServerParameterDefinition::raiseError( msg ); } @@ -73,7 +73,7 @@ namespace QgsWms if ( !ok ) { - const QString msg = QString( "%1 ('%2') cannot be converted into a rectangle" ).arg( name( mName ), toString(), typeName() ); + const QString msg = QString( "%1 ('%2') cannot be converted into a rectangle" ).arg( name( mName ), toString() ); QgsServerParameterDefinition::raiseError( msg ); } @@ -143,7 +143,7 @@ namespace QgsWms if ( !ok ) { - const QString msg = QString( "%1 ('%2') cannot be converted into a list of colors" ).arg( name( mName ), toString(), typeName() ); + const QString msg = QString( "%1 ('%2') cannot be converted into a list of colors" ).arg( name( mName ), toString() ); QgsServerParameterDefinition::raiseError( msg ); } @@ -157,7 +157,7 @@ namespace QgsWms if ( !ok ) { - const QString msg = QString( "%1 ('%2') cannot be converted into a list of int" ).arg( name( mName ), toString(), typeName() ); + const QString msg = QString( "%1 ('%2') cannot be converted into a list of int" ).arg( name( mName ), toString() ); QgsServerParameterDefinition::raiseError( msg ); } @@ -171,7 +171,7 @@ namespace QgsWms if ( !ok ) { - const QString msg = QString( "%1 ('%2') cannot be converted into a list of float" ).arg( name( mName ), toString(), typeName() ); + const QString msg = QString( "%1 ('%2') cannot be converted into a list of float" ).arg( name( mName ), toString() ); QgsServerParameterDefinition::raiseError( msg ); } diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 4c0890e6f6a2..b1446e6162a1 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -2530,7 +2530,7 @@ namespace QgsWms // create vector layer const QgsVectorLayer::LayerOptions options { QgsProject::instance()->transformContext() }; - std::unique_ptr layer = qgis::make_unique( url, param.mName, QLatin1Literal( "memory" ), options ); + std::unique_ptr layer = qgis::make_unique( url, param.mName, QLatin1String( "memory" ), options ); if ( !layer->isValid() ) { continue; @@ -2725,6 +2725,7 @@ namespace QgsWms } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/ui/3d/map3dconfigwidget.ui b/src/ui/3d/map3dconfigwidget.ui index edb1b0d5501e..af8c951d3eaf 100644 --- a/src/ui/3d/map3dconfigwidget.ui +++ b/src/ui/3d/map3dconfigwidget.ui @@ -37,7 +37,7 @@ 0 0 539 - 693 + 696 @@ -63,6 +63,23 @@ + + + + Elevation + + + + + + + + + + Tile resolution + + + @@ -70,6 +87,13 @@ + + + + Type + + + @@ -80,17 +104,17 @@ - - + + - Skirt height + Vertical scale - - - - false + + + + Skirt height @@ -110,44 +134,6 @@ - - - - Vertical scale - - - - - - - Type - - - - - - - Elevation - - - - - - - Map theme - - - - - - - Tile resolution - - - - - - @@ -392,7 +378,6 @@ spinTerrainScale spinTerrainResolution spinTerrainSkirtHeight - cboTerrainMapTheme groupTerrainShading spinMapResolution spinScreenError diff --git a/src/ui/attributeformconfig/qgsattributetypeedit.ui b/src/ui/attributeformconfig/qgsattributetypeedit.ui index bdae8b1ba4a4..fca23243ef73 100644 --- a/src/ui/attributeformconfig/qgsattributetypeedit.ui +++ b/src/ui/attributeformconfig/qgsattributetypeedit.ui @@ -184,36 +184,65 @@ Defaults - - + + Default value - - + + - + Preview - + Qt::StrongFocus - - + + - Preview + - + + + + + + + + 0 + 0 + + + + + + + :/images/themes/default/mIconWarning.svg + + + + + + + Using fields in a default value expression only works if "Apply default value on update" is checked. + + + + + + + <p>With this option checked, the default value will not only be used when the feature is first created, but also whenever a feature's attribute or geometry is changed.</p><p>This is often useful for a last_modified timestamp or to record the username that last modified the feature.</p> @@ -274,8 +303,10 @@ leConstraintExpressionDescription mCheckBoxEnforceExpression mExpressionWidget - mApplyDefaultValueOnUpdateCheckBox - + + + + diff --git a/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui b/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui index 7f2f5f591e98..3602d4579741 100644 --- a/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui +++ b/src/ui/editorwidgets/qgsvaluerelationconfigdlgbase.ui @@ -7,14 +7,14 @@ 0 0 427 - 474 + 489 Form - - + + @@ -32,7 +32,7 @@ Qt::RightToLeft - + :/images/themes/default/mIconExpression.svg:/images/themes/default/mIconExpression.svg @@ -52,6 +52,23 @@ + + + + Number of columns + + + + + + + + + + Select layer, key column and value column + + + @@ -62,33 +79,40 @@ - - + + + + + + + + - Select layer, key column and value column + Allow multiple selections - + Allow NULL value - - - - - - - + Use completer + + + + Order by value + + + @@ -99,6 +123,12 @@ + + + + + + @@ -109,35 +139,15 @@ - - - - Order by value - - - - - - - - - - Number of columns - - - - - - - - + + - Allow multiple selections + Description column - - + + @@ -152,6 +162,12 @@ QComboBox
    qgsmaplayercombobox.h
    + + QgsFieldExpressionWidget + QWidget +
    qgsfieldexpressionwidget.h
    + 1 +
    mLayerName @@ -164,8 +180,6 @@ mEditExpression mFilterExpression - - - + diff --git a/src/ui/labeling/qgslabelpropertydialogbase.ui b/src/ui/labeling/qgslabelpropertydialogbase.ui index fa080507d3b9..56d49bba7700 100644 --- a/src/ui/labeling/qgslabelpropertydialogbase.ui +++ b/src/ui/labeling/qgslabelpropertydialogbase.ui @@ -113,11 +113,7 @@ - - - false - - + diff --git a/src/ui/layout/qgslayoutmarkerwidgetbase.ui b/src/ui/layout/qgslayoutmarkerwidgetbase.ui new file mode 100644 index 000000000000..8513c4ac2e61 --- /dev/null +++ b/src/ui/layout/qgslayoutmarkerwidgetbase.ui @@ -0,0 +1,204 @@ + + + QgsLayoutMarkerWidgetBase + + + + 0 + 0 + 308 + 272 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + padding: 2px; font-weight: bold; background-color: rgb(200, 200, 200); + + + Marker + + + + + + + true + + + + + 0 + 0 + 306 + 247 + + + + + + + Qt::StrongFocus + + + Main Properties + + + composeritem + + + false + + + + + + Symbol + + + + + + + + 0 + 0 + + + + Change… + + + + + + + + + + Qt::StrongFocus + + + Marker Rotation + + + composeritem + + + false + + + + + + Offset + + + + + + + + + + Sync with map + + + + + + + true + + + ° + + + -360.000000000000000 + + + 360.000000000000000 + + + + + + + North alignment + + + + + + + + + + + + + + + + + + QgsScrollArea + QScrollArea +
    qgsscrollarea.h
    + 1 +
    + + QgsCollapsibleGroupBoxBasic + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1 +
    + + QgsDoubleSpinBox + QDoubleSpinBox +
    qgsdoublespinbox.h
    +
    + + QgsSymbolButton + QToolButton +
    qgssymbolbutton.h
    +
    + + QgsLayoutItemComboBox + QComboBox +
    qgslayoutitemcombobox.h
    +
    +
    + + groupBox + scrollArea + mShapeStyleButton + + + +
    diff --git a/src/ui/layout/qgslayoutpicturewidgetbase.ui b/src/ui/layout/qgslayoutpicturewidgetbase.ui index eb2e545b4b67..c3fef8527765 100644 --- a/src/ui/layout/qgslayoutpicturewidgetbase.ui +++ b/src/ui/layout/qgslayoutpicturewidgetbase.ui @@ -6,8 +6,8 @@ 0 0 - 334 - 572 + 536 + 500 @@ -60,203 +60,239 @@ 0 - 0 - 318 - 928 + -4 + 520 + 881 - - - Qt::StrongFocus + + + SVG image - - Main Properties + + true - - composeritem + + + + + + Raster image - - false + + + + + + QFrame::NoFrame - - - - - Image source - - - - - - - - - - - - - 0 - 0 - - - - - 30 - 32767 - - - - - - - - - - - - - - - - - - - - Resize mode + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::StrongFocus - - - - - - - Placement - + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + - - - - - - - 0 - 0 - + + + QFrame::NoFrame - - Qt::StrongFocus - - - Search Directories + + QFrame::Plain - - composeritem - - - true - - - - - - - true - - - - Loading previews… - - - Qt::AlignCenter - - - - - - - - 0 - 200 - - - - false - - - QAbstractItemView::DragDrop - - - QListView::Free - - - QListView::LeftToRight - - - true - - - QListView::Adjust - - - - 30 - 30 - - - - QListView::IconMode - - - true + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::StrongFocus - - - - Image search paths - + + + + QFrame::NoFrame + + + QFrame::Plain + + + + 0 + + + 0 + + + 0 + + + 0 + + - - - - QComboBox::AdjustToMinimumContentsLength + + + + Qt::Horizontal + + + + + + SVG Groups + + + + + + + + 3 + 1 + + + + + + + + + + + + SVG Image + + + + + + + + 5 + 1 + + + + + 0 + 250 + + + + QAbstractItemView::NoEditTriggers + + + + 32 + 32 + + + + QListView::Static + + + QListView::Adjust + + + QListView::Batched + + + 2 + + + + 36 + 36 + + + + QListView::IconMode + + + true + + + true + + + + + - - - - - - Remove - - - - - - - Add… - - - - - - + SVG Parameters @@ -397,6 +433,44 @@ + + + + Qt::StrongFocus + + + Size and Placement + + + composeritem + + + false + + + + + + + + + Placement + + + + + + + Resize mode + + + + + + + + + @@ -515,26 +589,36 @@ QComboBox
    qgslayoutitemcombobox.h
    + + QgsImageSourceLineEdit + QWidget +
    qgsfilecontentsourcelineedit.h
    + 1 +
    + + QgsSvgSourceLineEdit + QWidget +
    qgsfilecontentsourcelineedit.h
    + 1 +
    scrollArea - mPreviewGroupBox - mPictureLineEdit - mPictureBrowseButton - mSourceDDBtn - mResizeModeComboBox - mAnchorPointComboBox - mSearchDirectoriesGroupBox - mPreviewListWidget - mSearchDirectoriesComboBox - mRemoveDirectoryButton - mAddDirectoryButton + mRadioSVG + mRadioRaster + mImageSourceLineEdit + viewGroups + viewImages + mSvgSourceLineEdit mFillColorButton mFillColorDDBtn mStrokeColorButton mStrokeColorDDBtn mStrokeWidthSpinBox mStrokeWidthDDBtn + mPreviewGroupBox + mResizeModeComboBox + mAnchorPointComboBox mRotationGroupBox mPictureRotationSpinBox mRotationFromComposerMapCheckBox diff --git a/src/ui/layout/qgslayoutscalebarwidgetbase.ui b/src/ui/layout/qgslayoutscalebarwidgetbase.ui index 577d61851ba7..316677944ec3 100644 --- a/src/ui/layout/qgslayoutscalebarwidgetbase.ui +++ b/src/ui/layout/qgslayoutscalebarwidgetbase.ui @@ -60,9 +60,9 @@ 0 - -337 + -307 440 - 973 + 944 @@ -361,76 +361,40 @@ true - - - + + + + + + + + + - Horizontal label placement + Primary fill - - - - - + + 0 0 - - - - - mm - - - 0.010000000000000 - - - 0.100000000000000 - - - 0.200000000000000 - - - false - - - - - - - - - - - - - - - - - Box margin - - - - - - Join style + Line Style… - - + + - - + + - Cap style + Alignment @@ -444,30 +408,24 @@
    - - + + - Line width + Label margin - - + + - Alignment + Secondary fill - - - - - - - - + + - Label margin + Horizontal label placement @@ -478,6 +436,13 @@
    + + + + Box margin + + + @@ -488,187 +453,59 @@ -
    -
    -
    - - - - Qt::StrongFocus - - - Fonts and Colors - - - composeritem - - - true - - - - - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - 0 - 0 - + + + + Line style + + + + Font - - + + + + + 0 + 0 + + - Fill color + Fill Style… - - + + + + + 0 + 0 + + - Line color + Fill Style… - - - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - + + + + + 0 + 0 + + - Secondary fill color + Font - - - - - - - 120 - 0 - - - - - 120 - 16777215 - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - @@ -686,12 +523,6 @@
    qgsscrollarea.h
    1 - - QgsColorButton - QToolButton -
    qgscolorbutton.h
    - 1 -
    QgsCollapsibleGroupBoxBasic QGroupBox @@ -703,6 +534,11 @@ QDoubleSpinBox
    qgsdoublespinbox.h
    + + QgsLayoutItemComboBox + QComboBox +
    qgslayoutitemcombobox.h
    +
    QgsFontButton QToolButton @@ -713,30 +549,15 @@ QSpinBox
    qgsspinbox.h
    - - QgsLayoutItemComboBox - QComboBox -
    qgslayoutitemcombobox.h
    -
    - - QgsPropertyOverrideButton - QToolButton -
    qgspropertyoverridebutton.h
    -
    QgsAlignmentComboBox QComboBox
    qgsalignmentcombobox.h
    - QgsPenJoinStyleComboBox - QComboBox -
    qgspenstylecombobox.h
    -
    - - QgsPenCapStyleComboBox - QComboBox -
    qgspenstylecombobox.h
    + QgsSymbolButton + QToolButton +
    qgssymbolbutton.h
    @@ -761,21 +582,13 @@ groupBox_5 mBoxSizeSpinBox mLabelBarSpaceSpinBox - mLineWidthSpinBox - mLineWidthDDBtn - mLineJoinStyleCombo - mLineCapStyleCombo + mLabelVerticalPlacementComboBox + mLabelHorizontalPlacementComboBox + mFillSymbol1Button + mFillSymbol2Button + mLineStyleButton mAlignmentComboBox - groupBox_4 mFontButton - mFillColorButton - mFillColorDDBtn - mFillColor2Button - mFillColor2DDBtn - mStrokeColorButton - mLineColorDDBtn - mLabelHorizontalPlacementComboBox - mLabelVerticalPlacementComboBox diff --git a/src/ui/mesh/qgsmeshlayerpropertiesbase.ui b/src/ui/mesh/qgsmeshlayerpropertiesbase.ui index 2f6c636da2e3..2da5ba71305a 100644 --- a/src/ui/mesh/qgsmeshlayerpropertiesbase.ui +++ b/src/ui/mesh/qgsmeshlayerpropertiesbase.ui @@ -140,6 +140,15 @@ :/images/themes/default/propertyicons/rendering.svg:/images/themes/default/propertyicons/rendering.svg
    + + + Temporal + + + + :/images/themes/default/propertyicons/temporal.svg:/images/themes/default/propertyicons/temporal.svg + +
    @@ -178,8 +187,11 @@ 0 + + QFrame::Plain + - 0 + 1 @@ -216,7 +228,7 @@ 0 0 643 - 729 + 727 @@ -300,6 +312,23 @@ border-radius: 2px;
    + + + + Static Dataset + + + + + + + + 0 + 0 + + + + @@ -357,7 +386,7 @@ border-radius: 2px;
    - + @@ -435,6 +464,169 @@ border-radius: 2px; + + + + + + false + + + Qt::AlignCenter + + + true + + + QAbstractSpinBox::NoButtons + + + + + + + + 24 + 24 + + + + Reload from provider + + + + + + + :/images/themes/default/mActionRefresh.svg:/images/themes/default/mActionRefresh.svg + + + + + + + Layer Time Reference + + + + + + + Provider Time Settings + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Time unit + + + + + + + + + + + + + Qt::AlignCenter + + + true + + + Qt::UTC + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Layer End Time + + + + + + + false + + + false + + + true + + + Qt::AlignCenter + + + true + + + QAbstractSpinBox::NoButtons + + + false + + + false + + + Qt::UTC + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Layer Start Time + + + + + @@ -474,7 +666,7 @@ border-radius: 2px; Qt::Horizontal - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok @@ -490,6 +682,12 @@ border-radius: 2px;
    qgscollapsiblegroupbox.h
    1 + + QgsScrollArea + QScrollArea +
    qgsscrollarea.h
    + 1 +
    QgsFilterLineEdit QLineEdit @@ -502,9 +700,9 @@ border-radius: 2px; 1 - QgsScrollArea - QScrollArea -
    qgsscrollarea.h
    + QgsMeshStaticDatasetWidget + QWidget +
    qgsmeshstaticdatasetwidget.h
    1
    diff --git a/src/ui/mesh/qgsmeshrendereractivedatasetwidgetbase.ui b/src/ui/mesh/qgsmeshrendereractivedatasetwidgetbase.ui index 2f136ed44ee5..786e54304cd2 100644 --- a/src/ui/mesh/qgsmeshrendereractivedatasetwidgetbase.ui +++ b/src/ui/mesh/qgsmeshrendereractivedatasetwidgetbase.ui @@ -46,104 +46,10 @@ - - - - - - |< - - - true - - - - - - - Qt::Horizontal - - - QSlider::TicksBelow - - - 1 - - - - - - - true - - - QComboBox::NoInsert - - - - - - - > - - - true - - - - - - - Time Format Options - - - ... - - - - :/images/themes/default/console/iconSettingsConsole.svg:/images/themes/default/console/iconSettingsConsole.svg - - - true - - - - - - - >| - - - true - - - - - - - < - - - true - - - - - - - P - - - true - - - - - - Metadata + Dataset group Metadata diff --git a/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui b/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui index e09b2ad4ba87..956418c2aa78 100644 --- a/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui +++ b/src/ui/mesh/qgsmeshrenderervectorsettingswidgetbase.ui @@ -6,8 +6,8 @@ 0 0 - 322 - 760 + 376 + 1000 @@ -62,8 +62,24 @@ Line Width and Color - - + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + @@ -73,9 +89,86 @@ - + + + + Coloring Method + + + + + + + Color Ramp Shader + + + + + + + + + + :/images/themes/default/mActionRefresh.svg:/images/themes/default/mActionRefresh.svg + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + Max + + + + + + + + + + Min + + + + + + + + + + + 0 + 0 + + + + + + + + + + + + Color + + + @@ -573,7 +666,21 @@ QComboBox
    qgsunitselectionwidget.h
    + + QgsColorRampShaderWidget + QWidget +
    raster/qgscolorrampshaderwidget.h
    + 1 +
    + + QgsCollapsibleGroupBox + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1 +
    - + + + diff --git a/src/ui/mesh/qgsmeshstaticdatasetwidgetbase.ui b/src/ui/mesh/qgsmeshstaticdatasetwidgetbase.ui new file mode 100644 index 000000000000..cc73ebe1f504 --- /dev/null +++ b/src/ui/mesh/qgsmeshstaticdatasetwidgetbase.ui @@ -0,0 +1,81 @@ + + + QgsMeshStaticDatasetWidget + + + + 0 + 0 + 400 + 128 + + + + Form + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Vector Dataset Group + + + + + + + Scalar Dataset Group + + + + + + + + + + + 0 + 0 + + + + Qt::Vertical + + + + + + + Name + + + + + + + Name + + + + + + + + diff --git a/src/ui/mesh/qgsmeshtimeformatdialog.ui b/src/ui/mesh/qgsmeshtimeformatdialog.ui deleted file mode 100644 index 27a7b9ad6ce7..000000000000 --- a/src/ui/mesh/qgsmeshtimeformatdialog.ui +++ /dev/null @@ -1,367 +0,0 @@ - - - QgsMeshTimeFormatDialog - - - - 0 - 0 - 271 - 460 - - - - Time Display Options - - - Reload from layer - - - - - - 1 - - - - Use absolute time - - - - - Use relative time - - - - - - - - true - - - Use Absolute Time - - - false - - - false - - - false - - - - - - Reference date/time - - - - - - - 0 - - - - - - 0 - 0 - 0 - 2019 - 1 - 1 - - - - true - - - - - - - - 24 - 24 - - - - Qt::NoFocus - - - - - - - :/images/themes/default/mActionRefresh.svg:/images/themes/default/mActionRefresh.svg - - - - - - - - - Date/time format - - - - - - - false - - - - dd.MM.yyyy hh:mm:ss - - - - - dd.MM.yyyy hh:mm - - - - - dd.MM.yyyy hh - - - - - dd.MM.yyyy - - - - - dd/MM/yyyy hh:mm:ss - - - - - dd/MM/yyyy hh:mm - - - - - dd/MM/yyyy hh - - - - - dd/MM/yyyy - - - - - MM/dd/yyyy hh:mm:ss - - - - - MM/dd/yyyy hh:mm - - - - - MM/dd/yyyy hh - - - - - MM/dd/yyyy - - - - - - - - - - - Use Relative Time - - - - - - - - - Offset by - - - - - - - hours - - - 1 - - - -100000.000000000000000 - - - 100000.000000000000000 - - - - - - - - - - - - - Time format - - - - - - - false - - - - hh:mm:ss - - - - - hh:mm:ss.zzz - - - - - hh - - - - - d hh:mm:ss - - - - - d hh - - - - - d - - - - - ss - - - - - - - - - - - - - - Dataset Playback - - - - - - - - - Interval - - - - - - - sec - - - 1 - - - 1.000000000000000 - - - 10.000000000000000 - - - 3.000000000000000 - - - - - - - - - - - - - Provider Time Settings - - - - - - - seconds - - - - - minutes - - - - - hours - - - - - days - - - - - - - - Time unit - - - - - - - - - - - - - - shaftLengthMethodChanged() - apply() - - diff --git a/src/ui/numericformats/qgsfractionnumericformatwidgetbase.ui b/src/ui/numericformats/qgsfractionnumericformatwidgetbase.ui new file mode 100644 index 000000000000..6a4d75e3b0bb --- /dev/null +++ b/src/ui/numericformats/qgsfractionnumericformatwidgetbase.ui @@ -0,0 +1,108 @@ + + + QgsFractionNumericFormatWidgetBase + + + + 0 + 0 + 288 + 289 + + + + Form + + + + + + Use the unicode superscript and subscript representation of numbers, e.g. ¹⁷/₂₃ + + + Use Unicode super/subscript + + + + + + + Show thousands separator + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Show plus sign + + + + + + + 1 + + + Default + + + + + + + Thousands separator + + + + + + + Use dedicated Unicode characters for specific fractions (where they exist), e.g. ½ or ¾ + + + Use dedicated Unicode characters + + + + + + + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    + + QgsFilterLineEdit + QLineEdit +
    qgsfilterlineedit.h
    +
    +
    + + mUseUnicodeSupersubscriptCheckBox + mUseDedicatedUnicodeCheckBox + mShowThousandsCheckBox + mShowPlusCheckBox + mThousandsLineEdit + + + +
    diff --git a/src/ui/processing/qgsmodeldesignerdialogbase.ui b/src/ui/processing/qgsmodeldesignerdialogbase.ui index 7b6e9363adcf..2cedd9ab038e 100644 --- a/src/ui/processing/qgsmodeldesignerdialogbase.ui +++ b/src/ui/processing/qgsmodeldesignerdialogbase.ui @@ -70,7 +70,7 @@ - + &View @@ -80,16 +80,23 @@ + + &Edit + + + + + - + @@ -610,6 +617,36 @@ Del + + + true + + + Enable Snapping + + + + + Snap Selected Components to Grid + + + + + + :/images/themes/default/mActionSelectAll.svg:/images/themes/default/mActionSelectAll.svg + + + Select All + + + Ctrl+A + + + + + Add Group Box + + diff --git a/src/ui/processing/qgsprocessingalgorithmdialogbase.ui b/src/ui/processing/qgsprocessingalgorithmdialogbase.ui index 73a5d54555b4..32c5c249ce4e 100644 --- a/src/ui/processing/qgsprocessingalgorithmdialogbase.ui +++ b/src/ui/processing/qgsprocessingalgorithmdialogbase.ui @@ -58,6 +58,9 @@ 0 + + + @@ -114,7 +117,7 @@ - + :/images/themes/default/mActionFileSave.svg:/images/themes/default/mActionFileSave.svg @@ -131,7 +134,7 @@ - + :/images/themes/default/mActionEditCopy.svg:/images/themes/default/mActionEditCopy.svg @@ -148,7 +151,7 @@ - + :/images/themes/default/console/iconClearConsole.svg:/images/themes/default/console/iconClearConsole.svg @@ -211,43 +214,20 @@ Qt::Horizontal - QDialogButtonBox::Close|QDialogButtonBox::Help|QDialogButtonBox::Ok + QDialogButtonBox::Close|QDialogButtonBox::Help|QDialogButtonBox::Ok|QDialogButtonBox::Yes - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + QgsPanelWidgetStack + QWidget +
    qgspanelwidgetstack.h
    + 1 +
    +
    + diff --git a/src/ui/processing/qgsprocessingdestinationwidgetbase.ui b/src/ui/processing/qgsprocessingdestinationwidgetbase.ui new file mode 100644 index 000000000000..3848bd9b2aac --- /dev/null +++ b/src/ui/processing/qgsprocessingdestinationwidgetbase.ui @@ -0,0 +1,63 @@ + + + QgsProcessingDestinationWidgetBase + + + + 0 + 0 + 249 + 27 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + + QgsHighlightableLineEdit + QLineEdit +
    qgshighlightablelineedit.h
    +
    +
    + + +
    diff --git a/python/plugins/processing/ui/enummodelerwidgetbase.ui b/src/ui/processing/qgsprocessingenummodelerwidgetbase.ui similarity index 64% rename from python/plugins/processing/ui/enummodelerwidgetbase.ui rename to src/ui/processing/qgsprocessingenummodelerwidgetbase.ui index 539d36f727f8..fd379d8d77a0 100644 --- a/python/plugins/processing/ui/enummodelerwidgetbase.ui +++ b/src/ui/processing/qgsprocessingenummodelerwidgetbase.ui @@ -1,7 +1,7 @@ - Form - + QgsProcessingEnumModelerWidgetBase + 0 @@ -27,13 +27,17 @@ 0 - + Remove item + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + @@ -50,27 +54,35 @@ - + Add item + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + - + Clear all + + + :/images/themes/default/console/iconClearConsole.svg:/images/themes/default/console/iconClearConsole.svg + - + QAbstractItemView::InternalMove @@ -83,7 +95,7 @@ - + Allow multiple selection @@ -91,6 +103,8 @@ - + + + diff --git a/src/ui/processing/qgsprocessingfeaturesourceoptionsbase.ui b/src/ui/processing/qgsprocessingfeaturesourceoptionsbase.ui new file mode 100644 index 000000000000..70f7968bd424 --- /dev/null +++ b/src/ui/processing/qgsprocessingfeaturesourceoptionsbase.ui @@ -0,0 +1,121 @@ + + + QgsProcessingFeatureSourceOptionsBase + + + + 0 + 0 + 448 + 197 + + + + Form + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 448 + 197 + + + + + + + Invalid feature filtering + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + If set, allows the default method for handling features with invalid geometries to be overridden + + + + + + + If set, limits the maximum number of features which will be processed from this source + + + 999999999 + + + + + + + Limit features processed + + + + + + + + + + + + QgsScrollArea + QScrollArea +
    qgsscrollarea.h
    + 1 +
    + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    + + QgsSpinBox + QSpinBox +
    qgsspinbox.h
    +
    +
    + + +
    diff --git a/python/plugins/processing/ui/matrixmodelerwidgetbase.ui b/src/ui/processing/qgsprocessingmatrixmodelerwidgetbase.ui similarity index 58% rename from python/plugins/processing/ui/matrixmodelerwidgetbase.ui rename to src/ui/processing/qgsprocessingmatrixmodelerwidgetbase.ui index 39ade6d26151..3e7d6b6670b6 100644 --- a/python/plugins/processing/ui/matrixmodelerwidgetbase.ui +++ b/src/ui/processing/qgsprocessingmatrixmodelerwidgetbase.ui @@ -1,7 +1,7 @@ - Form - + QgsProcessingMatrixModelerWidgetBase + 0 @@ -27,7 +27,7 @@ 0 - + QAbstractItemView::SelectRows @@ -37,23 +37,31 @@ - + Remove row + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + - + Clear all + + + :/images/themes/default/console/iconClearConsole.svg:/images/themes/default/console/iconClearConsole.svg + @@ -70,44 +78,58 @@ - + Fixed number of rows - + Add row + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + - + Add column + + + :/images/themes/default/mActionNewAttribute.svg:/images/themes/default/mActionNewAttribute.svg + - + Remove column + + + :/images/themes/default/mActionDeleteAttribute.svg:/images/themes/default/mActionDeleteAttribute.svg + - + + + diff --git a/src/ui/processing/qgsprocessingmultipleselectiondialogbase.ui b/src/ui/processing/qgsprocessingmultipleselectiondialogbase.ui index d82385335fd8..bce108734d61 100644 --- a/src/ui/processing/qgsprocessingmultipleselectiondialogbase.ui +++ b/src/ui/processing/qgsprocessingmultipleselectiondialogbase.ui @@ -1,7 +1,7 @@ QgsProcessingMultipleSelectionDialogBase - + 0 @@ -45,45 +45,20 @@ Qt::Vertical - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::Ok + + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    +
    - - - mButtonBox - accepted() - QgsProcessingMultipleSelectionDialogBase - accept() - - - 248 - 254 - - - 157 - 274 - - - - - mButtonBox - rejected() - QgsProcessingMultipleSelectionDialogBase - reject() - - - 316 - 260 - - - 286 - 274 - - - - +
    diff --git a/python/plugins/processing/ui/widgetParametersPanel.ui b/src/ui/processing/qgsprocessingparameterswidgetbase.ui similarity index 72% rename from python/plugins/processing/ui/widgetParametersPanel.ui rename to src/ui/processing/qgsprocessingparameterswidgetbase.ui index e4fc9da5f765..12078cdfe945 100644 --- a/python/plugins/processing/ui/widgetParametersPanel.ui +++ b/src/ui/processing/qgsprocessingparameterswidgetbase.ui @@ -1,7 +1,7 @@ - Form - + QgsProcessingParametersWidgetBase + 0 @@ -46,7 +46,19 @@ 90 - + + + 0 + + + 0 + + + 0 + + + 0 + @@ -55,7 +67,7 @@ true - + @@ -81,13 +93,19 @@ QgsScrollArea QScrollArea -
    qgis.gui
    +
    qgsscrollarea.h
    + 1 +
    + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    1
    QgsCollapsibleGroupBox QGroupBox -
    qgis.gui
    +
    qgscollapsiblegroupbox.h
    1
    diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index b9e4bf5f4039..cab30b4a31d7 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -151,6 +151,8 @@ + + @@ -292,6 +294,7 @@ + @@ -542,7 +545,6 @@ true - @@ -1208,6 +1210,18 @@ Deselect Features from All Layers + + + Ctrl+Alt+A + + + + + + :/images/themes/default/mActionDeselectActiveLayer.svg:/images/themes/default/mActionDeselectActiveLayer.svg + + + Deselect Features from the Current Active Layer Ctrl+Shift+A @@ -2664,6 +2678,20 @@ Acts on the currently active layer only. Hide Selected Layers + + + + :/images/themes/default/mActionToggleSelectedLayers.svg:/images/themes/default/mActionToggleSelectedLayers.svg + + + Toggle Selected Layers + + + + + Toggle Selected Layers Independently + + diff --git a/src/ui/qgsdatasourceselectdialog.ui b/src/ui/qgsdatasourceselectdialog.ui index d7849a3da86b..ddf8d190f479 100644 --- a/src/ui/qgsdatasourceselectdialog.ui +++ b/src/ui/qgsdatasourceselectdialog.ui @@ -1,7 +1,7 @@ QgsDataSourceSelectDialog - + 0 @@ -18,168 +18,155 @@ 4 - 4 + 0 - 4 + 0 - 4 + 0 - 4 + 0 - - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 24 - 24 - + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + false + + + + + + + + + + + 0 + + + 0 + + + 0 - - false + + 0 - - - - - - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - + + 0 + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 2 - - QFrame::NoFrame + + 2 - - QFrame::Raised + + 0 - - - 2 - - - 2 - - - 0 - - - 2 - - - 0 - - - - - Options - - - - - - - :/images/themes/default/mActionOptions.svg:/images/themes/default/mActionOptions.svg - - - QToolButton::InstantPopup - - - Qt::ToolButtonIconOnly - - - true - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 0 - 0 - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - + + 2 + + + 0 + + + + + Options + + + + + + + :/images/themes/default/mActionOptions.svg:/images/themes/default/mActionOptions.svg + + + QToolButton::InstantPopup + + + Qt::ToolButtonIconOnly + + + true + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 0 + 0 + + + + + + + + + + + + + + - @@ -219,7 +206,6 @@ Collapse All - @@ -232,43 +218,16 @@ QTreeView
    qgsbrowsertreeview.h
    + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    +
    - + + - - - buttonBox - accepted() - QgsDataSourceSelectDialog - accept() - - - 248 - 254 - - - 157 - 274 - - - - - buttonBox - rejected() - QgsDataSourceSelectDialog - reject() - - - 316 - 260 - - - 286 - 274 - - - - +
    diff --git a/src/ui/qgsdevtoolswidgetbase.ui b/src/ui/qgsdevtoolswidgetbase.ui new file mode 100644 index 000000000000..dad48fbae300 --- /dev/null +++ b/src/ui/qgsdevtoolswidgetbase.ui @@ -0,0 +1,100 @@ + + + QgsDevToolsWidgetBase + + + + 0 + 0 + 500 + 496 + + + + Form + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 38 + 0 + + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoEditTriggers + + + Qt::ElideNone + + + QListView::Adjust + + + true + + + + + + + + + + + + + + diff --git a/src/ui/qgseditconditionalformatrulewidget.ui b/src/ui/qgseditconditionalformatrulewidget.ui index d3d3c9e6cbd1..bc99de8058f8 100644 --- a/src/ui/qgseditconditionalformatrulewidget.ui +++ b/src/ui/qgseditconditionalformatrulewidget.ui @@ -317,11 +317,7 @@ - - - false - - + diff --git a/src/ui/qgsexpressionbuilder.ui b/src/ui/qgsexpressionbuilder.ui index ddd0c09b744d..7494867eb2c8 100644 --- a/src/ui/qgsexpressionbuilder.ui +++ b/src/ui/qgsexpressionbuilder.ui @@ -99,48 +99,21 @@ 0
    - + - + - - - - 0 - 0 - - - - - 27 - 0 - - - - - 300 - 16777215 - - - - - 20 - 0 - - - - - 7 - 0 - + + + QFrame::NoFrame - - Qt::LeftToRight + + QFrame::Raised - - false + + 1 - + 2 @@ -157,132 +130,86 @@ 0 - - - Equal operator - - - = - - - - - - - - 0 - 0 - - - - Addition operator - - - + + + + false - - - - - Subtraction operator + Clear the expression editor - - + Clear - - - Division operator - - - / + + + false - - - - - Multiplication operator + Add current expression to user expressions - * + Save - - - Power operator - - - ^ + + + false - - - - - String Concatenation + Edit selected expression from user expressions - || + Edit - - - - 0 - 0 - - - - - 0 - 10 - + + + false - Open Bracket + Remove selected expression from user expressions - ( + Remove - + - Close Bracket + Import User Expressions - ) + Import - + - New Line + Export User Expressions - '\n' + Export - + Qt::Horizontal - 40 - 20 + 0 + 0 @@ -290,92 +217,198 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - 1 - - - - 2 + + + + + + + + + + 0 + 0 + + + + + 27 + 0 + + + + + 300 + 16777215 + + + + + 20 + 0 + + + + + 7 + 0 + + + + Qt::LeftToRight + + + false + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Equal operator - - 0 + + = - - 0 + + + + + + + 0 + 0 + - - 0 + + Addition operator - - 0 + + + - - - - false - - - Clear the expression editor - - - Clear - - - - - - - false - - - Add current expression to user expressions - - - Save - - - - - - - false - - - Remove selected expression from user expressions - - - Remove - - - - - - - - - - + + + + + + Subtraction operator + + + - + + + + + + + Division operator + + + / + + + + + + + Multiplication operator + + + * + + + + + + + Power operator + + + ^ + + + + + + + String Concatenation + + + || + + + + + + + + 0 + 0 + + + + + 0 + 10 + + + + Open Bracket + + + ( + + + + + + + Close Bracket + + + ) + + + + + + + New Line + + + '\n' + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -559,7 +592,7 @@ - + QFrame::StyledPanel @@ -801,7 +834,7 @@ Change the name of the script and save to allow QGIS to auto load on startup. - + :/images/themes/default/console/iconNewTabEditorConsole.svg:/images/themes/default/console/iconNewTabEditorConsole.svg @@ -893,7 +926,7 @@ Saved scripts are auto loaded on QGIS startup. Save and Load Functions - + :/images/themes/default/mActionStart.svg:/images/themes/default/mActionStart.svg @@ -936,9 +969,10 @@ Saved scripts are auto loaded on QGIS startup. - QgsFilterLineEdit - QLineEdit -
    qgsfilterlineedit.h
    + QgsCollapsibleGroupBox + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1
    QgsCodeEditorExpression @@ -946,6 +980,11 @@ Saved scripts are auto loaded on QGIS startup.
    qgscodeeditorexpression.h
    1
    + + QgsFilterLineEdit + QLineEdit +
    qgsfilterlineedit.h
    +
    QgsCodeEditorPython QWidget @@ -953,12 +992,14 @@ Saved scripts are auto loaded on QGIS startup. 1 - QgsCollapsibleGroupBox - QGroupBox -
    qgscollapsiblegroupbox.h
    - 1 + QgsExpressionTreeView + QTreeView +
    qgsexpressiontreeview.h
    - + + + + diff --git a/src/ui/qgsextentgroupboxwidget.ui b/src/ui/qgsextentgroupboxwidget.ui index 51738c6dea16..7115d765d4e0 100644 --- a/src/ui/qgsextentgroupboxwidget.ui +++ b/src/ui/qgsextentgroupboxwidget.ui @@ -2,169 +2,250 @@ QgsExtentGroupBoxWidget - - - 0 - 0 - 679 - 163 - + + + 0 + 0 + Form + + 0 + + + 0 + + + 0 + + + 0 + - - - - - North - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - - South - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - West - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - East - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - - - 150 - 0 - - - - Map Canvas Extent - - + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + South + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + + + + + + + North + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + East + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + West + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + Draw on Canvas + + + + + + + + 150 + 0 + + + + Current Layer Extent + + + + + + + + 150 + 0 + + + + Map Canvas Extent + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + Calculate from Layer + + + + + - - + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + - + 0 0 - - Calculate from Layer - - - + + - + 0 0 - Draw on Canvas - - - - - - - - 150 - 0 - - - - Current Layer Extent + @@ -173,6 +254,13 @@ + + + QgsHighlightableLineEdit + QLineEdit +
    qgshighlightablelineedit.h
    +
    +
    mYMaxLineEdit mXMinLineEdit diff --git a/src/ui/qgsfieldmappingwidget.ui b/src/ui/qgsfieldmappingwidget.ui new file mode 100644 index 000000000000..01c179b5857c --- /dev/null +++ b/src/ui/qgsfieldmappingwidget.ui @@ -0,0 +1,44 @@ + + + QgsFieldMappingWidget + + + + 0 + 0 + 400 + 300 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    +
    + + +
    diff --git a/src/ui/qgsidentifyresultsbase.ui b/src/ui/qgsidentifyresultsbase.ui index 0eb5eec292d4..2cafc9530489 100644 --- a/src/ui/qgsidentifyresultsbase.ui +++ b/src/ui/qgsidentifyresultsbase.ui @@ -311,7 +311,7 @@ Copy Feature
    - Copy Selected Feature to Clipboard + Copy the Identified Feature to Clipboard
    diff --git a/src/ui/qgsjoindialogbase.ui b/src/ui/qgsjoindialogbase.ui index b3dee4fe7628..52978e122f00 100644 --- a/src/ui/qgsjoindialogbase.ui +++ b/src/ui/qgsjoindialogbase.ui @@ -7,7 +7,7 @@ 0 0 505 - 487 + 576 @@ -47,7 +47,7 @@ - &Joined Fields + &Joined fields true @@ -68,7 +68,7 @@ - Custom Field &Name Prefix + Custom field &name prefix true @@ -105,7 +105,7 @@ - Cache join layer in virtual memory + Cache join layer in memory @@ -126,14 +126,14 @@ - + Dynamic form - + Edi&table join layer diff --git a/src/ui/qgsmaskingwidgetbase.ui b/src/ui/qgsmaskingwidgetbase.ui index a0e58a370ba6..bca2671bc6cd 100644 --- a/src/ui/qgsmaskingwidgetbase.ui +++ b/src/ui/qgsmaskingwidgetbase.ui @@ -6,51 +6,54 @@ 0 0 - 637 - 409 + 863 + 461 Form - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - + - - - Masked symbol layers + + + Edit mask settings - - - - - - - - - Mask sources + + true - - - - - + false + + + 9 + + + + + Masked symbol layers + + + + + + + + + + Mask sources + + + + + + + false + + + + diff --git a/src/ui/qgsnetworkloggerpanelbase.ui b/src/ui/qgsnetworkloggerpanelbase.ui new file mode 100644 index 000000000000..396609dba749 --- /dev/null +++ b/src/ui/qgsnetworkloggerpanelbase.ui @@ -0,0 +1,137 @@ + + + QgsNetworkLoggerPanelBase + + + + 0 + 0 + 700 + 629 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + false + + + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + + :/images/themes/default/mActionDeleteSelected.svg:/images/themes/default/mActionDeleteSelected.svg + + + Clear + + + Clear Log + + + + + true + + + + :/images/themes/default/mActionPlay.svg:/images/themes/default/mActionPlay.svg + + + Record Log + + + + + true + + + Show Successful Requests + + + + + true + + + Show Timeouts + + + + + + :/images/themes/default/mActionFileSave.svg:/images/themes/default/mActionFileSave.svg + + + Save Log… + + + + + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    + + QgsFilterLineEdit + QLineEdit +
    qgsfilterlineedit.h
    +
    +
    + + + + + + +
    diff --git a/src/ui/qgsnewdatabasetablenamewidget.ui b/src/ui/qgsnewdatabasetablenamewidget.ui index 3927a14f4755..d7bb81e64229 100644 --- a/src/ui/qgsnewdatabasetablenamewidget.ui +++ b/src/ui/qgsnewdatabasetablenamewidget.ui @@ -1,7 +1,7 @@ QgsNewDatabaseTableNameWidget - + 0 @@ -10,54 +10,71 @@ 629 - + - 4 + 0 - 4 + 0 - 4 + 0 4 - - - - - 24 - 24 - + + + + - - false + + name of the new table - - - - - - + + - New table name + Ok - - - - + + + + 0 - - name of the new table + + 0 + + + + + + 24 + 24 + + + + false + + + + + + + + + + + + + New table name - + true @@ -79,6 +96,12 @@
    + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    QgsBrowserTreeView QTreeView diff --git a/src/ui/qgsnewspatialitelayerdialogbase.ui b/src/ui/qgsnewspatialitelayerdialogbase.ui index 2042410ef160..f0bdab1b1e94 100644 --- a/src/ui/qgsnewspatialitelayerdialogbase.ui +++ b/src/ui/qgsnewspatialitelayerdialogbase.ui @@ -70,7 +70,7 @@ - + true @@ -448,6 +448,12 @@
    qgscollapsiblegroupbox.h
    1
    + + QgsProviderConnectionComboBox + QWidget +
    qgsproviderconnectioncombobox.h
    + 1 +
    scrollArea diff --git a/src/ui/qgsoptionsbase.ui b/src/ui/qgsoptionsbase.ui index bcc27b48b49e..35edb8e83a6e 100644 --- a/src/ui/qgsoptionsbase.ui +++ b/src/ui/qgsoptionsbase.ui @@ -606,11 +606,7 @@ - - - false - - + @@ -5447,7 +5443,7 @@ The bigger the number, the faster zooming with the mouse wheel will be. - + diff --git a/src/ui/qgspluginmanagerbase.ui b/src/ui/qgspluginmanagerbase.ui index 66a82ebb7fe1..751713356827 100644 --- a/src/ui/qgspluginmanagerbase.ui +++ b/src/ui/qgspluginmanagerbase.ui @@ -504,6 +504,32 @@ + + + + false + + + + 160 + 0 + + + + Install, reinstall or upgrade the experimental version of selected plugin + + + background-color: #EEEEBB + + + Reinstall Experimental + + + + :/images/themes/default/pluginExperimental.png:/images/themes/default/pluginExperimental.png + + + diff --git a/src/ui/qgsprojectpropertiesbase.ui b/src/ui/qgsprojectpropertiesbase.ui index 6f58008867e9..6a2198b6b311 100644 --- a/src/ui/qgsprojectpropertiesbase.ui +++ b/src/ui/qgsprojectpropertiesbase.ui @@ -245,7 +245,7 @@ - 9 + 0 @@ -274,8 +274,8 @@ 0 0 - 652 - 846 + 671 + 865 @@ -897,8 +897,8 @@ 0 0 - 663 - 164 + 587 + 167 @@ -972,8 +972,8 @@ 0 0 - 315 - 553 + 288 + 563 @@ -1548,8 +1548,8 @@ 0 0 - 191 - 55 + 177 + 56 @@ -1610,8 +1610,8 @@ 0 0 - 719 - 2767 + 643 + 2818 @@ -2881,43 +2881,39 @@ Temporal Options - - - - - color:rgb(136, 138, 133); - + + + + + M/d/yyyy h:mm AP - - + + Qt::UTC - + Start date - - - - M/d/yyyy h:mm AP - - - Qt::UTC + + + + Calculate from Layers - + End date - + M/d/yyyy h:mm AP @@ -2927,13 +2923,6 @@ - - - - Calculate from layers - - - @@ -3002,9 +2991,14 @@ - QgsCollapsibleGroupBox - QGroupBox -
    qgscollapsiblegroupbox.h
    + QgsFilterLineEdit + QLineEdit +
    qgsfilterlineedit.h
    +
    + + QgsVariableEditorWidget + QWidget +
    qgsvariableeditorwidget.h
    1
    @@ -3013,6 +3007,12 @@
    qgsscrollarea.h
    1
    + + QgsCollapsibleGroupBox + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1 +
    QgsDateTimeEdit QDateTimeEdit @@ -3024,11 +3024,6 @@
    qgscolorbutton.h
    1
    - - QgsFilterLineEdit - QLineEdit -
    qgsfilterlineedit.h
    -
    QgsOpacityWidget QWidget @@ -3041,12 +3036,6 @@
    qgscolorschemelist.h
    1
    - - QgsVariableEditorWidget - QWidget -
    qgsvariableeditorwidget.h
    - 1 -
    QgsDatumTransformTableWidget QWidget @@ -3183,6 +3172,8 @@
    + + diff --git a/src/ui/qgsrastercontourrendererwidget.ui b/src/ui/qgsrastercontourrendererwidget.ui new file mode 100644 index 000000000000..e7aec24fc8bd --- /dev/null +++ b/src/ui/qgsrastercontourrendererwidget.ui @@ -0,0 +1,159 @@ + + + QgsRasterContourRendererWidget + + + + 0 + 0 + 487 + 394 + + + + Form + + + + + + Input band + + + + + + + + + + Contour Interval + + + + + + + 1 + + + 0.100000000000000 + + + 999999.000000000000000 + + + 100.000000000000000 + + + + + + + Contour Symbol + + + + + + + + 0 + 0 + + + + + + + + + + + Index Contour Interval + + + + + + + 1 + + + 999999.000000000000000 + + + 500.000000000000000 + + + + + + + Index Contour Symbol + + + + + + + + 0 + 0 + + + + + + + + + + + Input Downscaling + + + + + + + 1 + + + 1.000000000000000 + + + 4.000000000000000 + + + + + + + + QgsRasterBandComboBox + QComboBox +
    raster/qgsrasterbandcombobox.h
    +
    + + QgsSymbolButton + QToolButton +
    qgssymbolbutton.h
    +
    + + QgsDoubleSpinBox + QDoubleSpinBox +
    qgsdoublespinbox.h
    +
    +
    + + mInputBandComboBox + mContourIntervalSpinBox + mContourSymbolButton + mIndexContourIntervalSpinBox + mIndexContourSymbolButton + mDownscaleSpinBox + + + +
    diff --git a/src/ui/qgsrasterlayerpropertiesbase.ui b/src/ui/qgsrasterlayerpropertiesbase.ui index dab8e9f836cc..181e4e95d16f 100644 --- a/src/ui/qgsrasterlayerpropertiesbase.ui +++ b/src/ui/qgsrasterlayerpropertiesbase.ui @@ -161,6 +161,18 @@ :/images/themes/default/propertyicons/rendering.svg:/images/themes/default/propertyicons/rendering.svg + + + Temporal + + + Temporal Settings + + + + :/images/themes/default/propertyicons/temporal.svg:/images/themes/default/propertyicons/temporal.svg + + Pyramids @@ -246,6 +258,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -277,9 +301,9 @@ 0 - 0 - 634 - 680 + -188 + 629 + 914 @@ -339,22 +363,280 @@ border-radius: 2px; - - - - 100 - 450 - + + + true + + + Use static WMS-T capabilities + + + true - - + + true - - QFrame::NoFrame + + + + + Time slice mode + + + + + + + Note: If the capabilities of this layer move out of this time range, the range will be reset to layer's advertised default layer time range. + + + true + + + + + + + false + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + + + Set end same as start + + + + + + + Start date + + + + + + + M/d/yyyy H:mm:ss AP + + + Qt::UTC + + + false + + + + + + + End date + + + + + + + Use dates only + + + + + + + + 2 + 3 + 57 + 2020 + 4 + 30 + + + + QDateTimeEdit::MonthSection + + + M/d/yyyy H:mm:ss AP + + + Qt::UTC + + + false + + + + + + + + + + false + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + + + + 16 + 20 + 36 + 2020 + 1 + 25 + + + + + 2020 + 1 + 25 + + + + QDateTimeEdit::MonthSection + + + M/d/yyyy H:mm:ss AP + + + false + + + Qt::UTC + + + false + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + Use static WMS-T temporal range + + + + + + + Pass project temporal range to provider + + + + + + + Reference time + + + + + + + + + + Temporal capabilities - - QFrame::Raised + + true + + + + + If the dataset contains multiple rasters belonging to a time series, specify the field that contains the time information, the type of the field can be any type that can be converted to a timestamp. + + + true + + + + + + + Default time + + + + + + + + 1 + 0 + + + + + + + + Temporal field + + + + + + + + + + <html><head/><body><p>A default value can be specified and it will be used when the temporal controller is not requesting any particular time.</p><p>It is recommended to set a default temporal value here instead of using a provider filter because the provider filter will still be active when using the temporal controller while this default value will be ignored.</p></body></html> + + + true + + + + @@ -406,11 +688,23 @@ border-radius: 2px; 0 0 - 568 - 532 + 470 + 514 + + 0 + + + 0 + + + 0 + + + 0 + @@ -994,14 +1288,20 @@ border-radius: 2px; 0 0 - 377 - 467 + 332 + 477 + + 0 + 0 + + 0 + 0 @@ -1346,9 +1646,15 @@ border-radius: 2px; + + 0 + 0 + + 0 + 0 @@ -1375,6 +1681,18 @@ border-radius: 2px; + + 0 + + + 0 + + + 0 + + + 0 + @@ -1479,6 +1797,41 @@ border-radius: 2px; + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 100 + 450 + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + @@ -1506,14 +1859,20 @@ border-radius: 2px; 0 0 - 649 + 531 205 + + 0 + 0 + + 0 + 0 @@ -1570,7 +1929,7 @@ border-radius: 2px; <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;"> +</style></head><body style=" font-family:'Cantarell'; font-size:11pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Cantarell';"><br /></span></p></body></html> @@ -1651,6 +2010,18 @@ p, li { white-space: pre-wrap; } + + 0 + + + 0 + + + 0 + + + 0 + @@ -1665,6 +2036,18 @@ p, li { white-space: pre-wrap; } + + 0 + + + 0 + + + 0 + + + 0 + @@ -1706,11 +2089,23 @@ p, li { white-space: pre-wrap; } 0 0 - 382 - 685 + 325 + 667 + + 0 + + + 0 + + + 0 + + + 0 + @@ -2145,18 +2540,23 @@ p, li { white-space: pre-wrap; } - - QgsCollapsibleGroupBox - QGroupBox -
    qgscollapsiblegroupbox.h
    - 1 -
    QgsScrollArea QScrollArea
    qgsscrollarea.h
    1
    + + QgsDateTimeEdit + QDateTimeEdit +
    qgsdatetimeedit.h
    +
    + + QgsCollapsibleGroupBox + QGroupBox +
    qgscollapsiblegroupbox.h
    + 1 +
    QgsColorButton QToolButton @@ -2207,6 +2607,11 @@ p, li { white-space: pre-wrap; }
    qgswebview.h
    1
    + + QgsFieldComboBox + QComboBox +
    qgsfieldcombobox.h
    +
    mSearchLineEdit @@ -2274,6 +2679,8 @@ p, li { white-space: pre-wrap; } + + diff --git a/src/ui/qgstableeditorbase.ui b/src/ui/qgstableeditorbase.ui index e3ca1da93f5d..e7c1f38df0d1 100644 --- a/src/ui/qgstableeditorbase.ui +++ b/src/ui/qgstableeditorbase.ui @@ -100,6 +100,8 @@ File + +
    @@ -161,6 +163,11 @@ Select Column + + + Import Content from Clipboard + + Close Editor diff --git a/src/ui/qgstemporalcontrollerdockwidgetbase.ui b/src/ui/qgstemporalcontrollerdockwidgetbase.ui deleted file mode 100644 index ca08574fb69d..000000000000 --- a/src/ui/qgstemporalcontrollerdockwidgetbase.ui +++ /dev/null @@ -1,275 +0,0 @@ - - - QgsTemporalControllerDockWidgetBase - - - - 0 - 0 - 1219 - 136 - - - - gsDockWidget - - - - - - - - - - - - - :/images/themes/default/propertyicons/settings.svg:/images/themes/default/propertyicons/settings.svg - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - - - - - - - - - - :/images/themes/default/temporal_navigation/rewindToStart.svg:/images/themes/default/temporal_navigation/rewindToStart.svg - - - - - - - - - - - :/images/themes/default/temporal_navigation/previous.svg:/images/themes/default/temporal_navigation/previous.svg - - - - - - - - - - - :/images/themes/default/temporal_navigation/back.svg:/images/themes/default/temporal_navigation/back.svg - - - true - - - - - - - - - - - :/images/themes/default/temporal_navigation/pause.svg:/images/themes/default/temporal_navigation/pause.svg - - - true - - - - - - - - - - - :/images/themes/default/temporal_navigation/forward.svg:/images/themes/default/temporal_navigation/forward.svg - - - true - - - - - - - - - - - :/images/themes/default/temporal_navigation/next.svg:/images/themes/default/temporal_navigation/next.svg - - - - - - - - - - - :/images/themes/default/temporal_navigation/skipToEnd.svg:/images/themes/default/temporal_navigation/skipToEnd.svg - - - - - - - Automatically reset and repeat the animation endlessly - - - Loop - - - - - - - Time steps of - - - - - - - - - - false - - - - - - - - - - Currently set from - - - - - - - M/d/yyyy h:mm AP - - - Qt::UTC - - - - - - - to - - - - - - - M/d/yyyy h:mm AP - - - Qt::UTC - - - - - - - - - - - :/images/themes/default/propertyicons/temporal.svg:/images/themes/default/propertyicons/temporal.svg - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Qt::Horizontal - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - QgsDockWidget - QDockWidget -
    qgsdockwidget.h
    - 1 -
    - - QgsDoubleSpinBox - QDoubleSpinBox -
    qgsdoublespinbox.h
    -
    -
    - - - - -
    diff --git a/src/ui/qgstemporalcontrollerwidgetbase.ui b/src/ui/qgstemporalcontrollerwidgetbase.ui new file mode 100644 index 000000000000..c99103cccb3d --- /dev/null +++ b/src/ui/qgstemporalcontrollerwidgetbase.ui @@ -0,0 +1,314 @@ + + + QgsTemporalControllerWidgetBase + + + + 0 + 0 + 747 + 93 + + + + gsDockWidget + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Range + + + + + + + M/d/yyyy h:mm AP + + + Qt::UTC + + + + + + + to + + + + + + + M/d/yyyy h:mm AP + + + Qt::UTC + + + + + + + + + + + :/images/themes/default/mActionRefresh.svg:/images/themes/default/mActionRefresh.svg + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 40 + 20 + + + + + + + + Step + + + + + + + + 0 + 0 + + + + + 130 + 16777215 + + + + 3 + + + + + + + false + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + :/images/themes/default/propertyicons/settings.svg:/images/themes/default/propertyicons/settings.svg + + + true + + + + + + + + + + + + + + + :/images/themes/default/temporal_navigation/rewindToStart.svg:/images/themes/default/temporal_navigation/rewindToStart.svg + + + + + + + + + + + :/images/themes/default/temporal_navigation/previous.svg:/images/themes/default/temporal_navigation/previous.svg + + + + + + + + + + + :/images/themes/default/temporal_navigation/back.svg:/images/themes/default/temporal_navigation/back.svg + + + true + + + + + + + + + + + :/images/themes/default/temporal_navigation/pause.svg:/images/themes/default/temporal_navigation/pause.svg + + + true + + + + + + + + + + + :/images/themes/default/temporal_navigation/forward.svg:/images/themes/default/temporal_navigation/forward.svg + + + true + + + + + + + + + + + :/images/themes/default/temporal_navigation/next.svg:/images/themes/default/temporal_navigation/next.svg + + + + + + + + + + + :/images/themes/default/temporal_navigation/skipToEnd.svg:/images/themes/default/temporal_navigation/skipToEnd.svg + + + + + + + Automatically reset and repeat the animation endlessly + + + Loop + + + + + + + Qt::Horizontal + + + false + + + QSlider::NoTicks + + + + + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + QgsDoubleSpinBox + QDoubleSpinBox +
    qgsdoublespinbox.h
    +
    + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    + 1 +
    +
    + + + + + + +
    diff --git a/src/ui/qgstemporalmapsettingsdialogbase.ui b/src/ui/qgstemporalmapsettingsdialogbase.ui deleted file mode 100644 index 01a84931cf18..000000000000 --- a/src/ui/qgstemporalmapsettingsdialogbase.ui +++ /dev/null @@ -1,87 +0,0 @@ - - - QgsTemporalMapSettingsDialogBase - - - - 0 - 0 - 216 - 210 - - - - Dialog - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - - - - buttonBox - rejected() - QgsTemporalMapSettingsDialogBase - reject() - - - 316 - 260 - - - 286 - 274 - - - - - buttonBox - accepted() - QgsTemporalMapSettingsDialogBase - accept() - - - 248 - 254 - - - 157 - 274 - - - - - diff --git a/src/ui/qgstemporalmapsettingswidgetbase.ui b/src/ui/qgstemporalmapsettingswidgetbase.ui index b833b3cb9b30..7c94bece96a6 100644 --- a/src/ui/qgstemporalmapsettingswidgetbase.ui +++ b/src/ui/qgstemporalmapsettingswidgetbase.ui @@ -1,19 +1,31 @@ QgsTemporalMapSettingsWidgetBase - + 0 0 - 354 - 194 + 409 + 63 Form + + 0 + + + 0 + + + 0 + + + 0 + @@ -24,20 +36,7 @@
    - - - Qt::Horizontal - - - - 40 - 20 - - - - - - + @@ -69,6 +68,18 @@
    + + + QgsDoubleSpinBox + QDoubleSpinBox +
    qgsdoublespinbox.h
    +
    + + QgsPanelWidget + QWidget +
    qgspanelwidget.h
    +
    +
    diff --git a/src/ui/qgstextformatwidgetbase.ui b/src/ui/qgstextformatwidgetbase.ui index 41c8bde0ee6a..63675f99cdab 100644 --- a/src/ui/qgstextformatwidgetbase.ui +++ b/src/ui/qgstextformatwidgetbase.ui @@ -7,7 +7,7 @@ 0 0 880 - 516 + 589 @@ -694,7 +694,7 @@ - 3 + 6 @@ -723,8 +723,8 @@ 0 0 - 782 - 387 + 317 + 260 @@ -1144,9 +1144,6 @@
    - - false - QComboBox::AdjustToMinimumContentsLengthWithIcon @@ -1306,8 +1303,8 @@ font-style: italic; 0 0 - 374 - 708 + 348 + 624 @@ -2205,8 +2202,8 @@ font-style: italic; 0 0 - 308 - 308 + 284 + 273 @@ -2551,8 +2548,8 @@ font-style: italic; 0 0 - 831 - 295 + 830 + 376 @@ -2829,8 +2826,8 @@ font-style: italic; 0 0 - 474 - 786 + 816 + 696 @@ -3590,8 +3587,8 @@ font-style: italic; 0 0 - 318 - 457 + 816 + 406 @@ -3991,7 +3988,7 @@ font-style: italic;
    - + 0 @@ -4018,8 +4015,8 @@ font-style: italic; 0 0 - 156 - 185 + 830 + 376 @@ -4037,61 +4034,6 @@ font-style: italic; - - - - Callouts - - - - - - - - - - - - - - 20 - - - 0 - - - - - - - - Style - - - - - - - - - - - This callout type doesn't have any editable properties - - - Qt::AlignCenter - - - true - - - - - - - - - @@ -4105,6 +4047,13 @@ font-style: italic; + + + + Callouts + + + @@ -4118,6 +4067,56 @@ font-style: italic; + + + + + + + + + + + + 20 + + + 1 + + + + + + + + + + + + This callout type doesn't have any editable properties + + + Qt::AlignCenter + + + true + + + + + + + + + + + Style + + + + + + @@ -4166,8 +4165,8 @@ font-style: italic; 0 0 - 440 - 1154 + 431 + 1108 @@ -5991,8 +5990,8 @@ font-style: italic; 0 0 - 415 - 853 + 383 + 624 diff --git a/src/ui/qgsvectortilebasicrendererwidget.ui b/src/ui/qgsvectortilebasicrendererwidget.ui new file mode 100644 index 000000000000..a23c13e0224f --- /dev/null +++ b/src/ui/qgsvectortilebasicrendererwidget.ui @@ -0,0 +1,104 @@ + + + QgsVectorTileBasicRendererWidget + + + + 0 + 0 + 557 + 424 + + + + + + + true + + + QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + + + true + + + QAbstractItemView::InternalMove + + + QAbstractItemView::ExtendedSelection + + + false + + + + + + + + + Add rule + + + + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + QToolButton::InstantPopup + + + + + + + Remove selected rules + + + + + + + :/images/themes/default/symbologyRemove.svg:/images/themes/default/symbologyRemove.svg + + + + + + + Edit current rule + + + + + + + :/images/themes/default/symbologyEdit.svg:/images/themes/default/symbologyEdit.svg + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + diff --git a/src/ui/qgsvectortileconnectiondialog.ui b/src/ui/qgsvectortileconnectiondialog.ui new file mode 100644 index 000000000000..7049a6fd8bee --- /dev/null +++ b/src/ui/qgsvectortileconnectiondialog.ui @@ -0,0 +1,168 @@ + + + QgsVectorTileConnectionDialog + + + + 0 + 0 + 659 + 506 + + + + Vector Tiles Connection + + + + + + Connection Details + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + URL + + + + + + + + 0 + 0 + + + + 14 + + + + + + + Name + + + + + + + Name of the new connection + + + + + + + URL of the connection, {x}, {y}, and {z} will be replaced with actual values. Use {-y} for inverted y axis. + + + http://example.com/{z}/{x}/{y}.png + + + + + + + Max. Zoom Level + + + true + + + + + + + Min. Zoom Level + + + true + + + + + + + + 0 + 0 + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + mEditName + mEditUrl + mCheckBoxZMin + mSpinZMin + mCheckBoxZMax + mSpinZMax + + + + + buttonBox + accepted() + QgsVectorTileConnectionDialog + accept() + + + 224 + 381 + + + 157 + 274 + + + + + buttonBox + rejected() + QgsVectorTileConnectionDialog + reject() + + + 292 + 387 + + + 286 + 274 + + + + + diff --git a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui index 63ec14deff67..983a29d2b435 100644 --- a/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui +++ b/src/ui/raster/qgsrasterlayertemporalpropertieswidgetbase.ui @@ -6,8 +6,8 @@ 0 0 - 743 - 658 + 611 + 413 @@ -48,8 +48,8 @@ 0 0 - 743 - 658 + 611 + 413 @@ -57,13 +57,16 @@ 0 - 9 + 0 0 + + 0 + - + true @@ -74,152 +77,105 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti Temporal - - false - - - rastertransp - - + true - - - - Fetch mode - - - - - - - - + + - Use date only - - - - - - - Specify time range + Fixed time range (only show this layer if animation time is within this range) - true + false - - - - Use Project time - - + + + false - - - - QFrame::NoFrame QFrame::Raised - + + 0 + + + + 0 + + + 0 + - + - If this option is checked, project time will be used to determine time range. - - - true + Start date - - - - - - - Advanced - - - false - - - - + + M/d/yyyy h:mm AP - - false - - - - - - - Use Reference time - - - - - - - Set end same as start + + Qt::UTC - - - - Filter layer temporal properties based on the time when product model ran. + + + + M/d/yyyy h:mm AP - - true + + Qt::UTC - - - - - + + End date - - mEndReferenceDateTimeEdit - - - - - M/d/yyyy h:mm AP + + + + Qt::Horizontal - - - - - - Start date - - - mStartReferenceDateTimeEdit + + + 40 + 20 + - + - + + + + Delegates temporal handling to the data provider + + + Automatic + + + true + + + + Qt::Vertical @@ -232,95 +188,6 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - - - - - - - true - - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - 0 - - - - - - Start date - - - mStartTemporalDateTimeEdit - - - - - - - End date - - - mEndTemporalDateTimeEdit - - - - - - - M/d/yyyy h:mm AP - - - Qt::UTC - - - - - - - Set end same as start - - - - - - - Reset dates - - - - - - - Note: If the capabilities of this layer move out of this time range, the range will be reset to layer's advertised default layer time range. - - - true - - - - - - - M/d/yyyy h:mm AP - - - Qt::UTC - - - - - - @@ -332,9 +199,9 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti - QgsCollapsibleGroupBox - QGroupBox -
    qgscollapsiblegroupbox.h
    + QgsScrollArea + QScrollArea +
    qgsscrollarea.h
    1
    @@ -342,62 +209,14 @@ background: white;QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::ti QDateTimeEdit
    qgsdatetimeedit.h
    - - QgsScrollArea - QScrollArea -
    qgsscrollarea.h
    - 1 -
    scrollArea - mFetchModeComboBox - mDisableTime - mLayerRadioButton + mModeFixedRangeRadio mStartTemporalDateTimeEdit mEndTemporalDateTimeEdit - mSetEndAsStartNormalButton - mProjectRadioButton - mAdvancedGroupBox - mReferenceCheckBox - mStartReferenceDateTimeEdit - mEndReferenceDateTimeEdit - mSetEndAsStartReferenceButton mTemporalGroupBox - - - mLayerRadioButton - toggled(bool) - frame_2 - setEnabled(bool) - - - 70 - 136 - - - 115 - 181 - - - - - mProjectRadioButton - toggled(bool) - frame - setEnabled(bool) - - - 64 - 324 - - - 81 - 344 - - - - + diff --git a/src/ui/symbollayer/widget_fontmarker.ui b/src/ui/symbollayer/widget_fontmarker.ui index 05aee7985170..93db159e6b72 100644 --- a/src/ui/symbollayer/widget_fontmarker.ui +++ b/src/ui/symbollayer/widget_fontmarker.ui @@ -14,7 +14,7 @@ Form - + @@ -39,7 +39,7 @@ - + @@ -64,14 +64,14 @@ - + Size - + @@ -79,20 +79,62 @@ - + Font family - + + + + Font style + + + + + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + Available typeface styles + + + QComboBox::AdjustToContentsOnFirstShow + + + + + + + + + + + Offset - + @@ -171,28 +213,35 @@ - + + + + + + + + Anchor point - + - + Fill color - + @@ -205,7 +254,7 @@ - + Join style @@ -215,7 +264,7 @@ - + @@ -234,14 +283,14 @@ - + Rotation - + @@ -260,28 +309,28 @@ - + - + - + - + true @@ -303,42 +352,42 @@ - + - + Character(s) - + Type in characters directly, or enter a character's hexadecimal value. - + - + - + @@ -375,21 +424,21 @@ - + - + Stroke width - + @@ -434,10 +483,10 @@ - + - + @@ -473,14 +522,14 @@ - + - + Type in characters directly, or enter a character's hexadecimal value. @@ -490,7 +539,7 @@ - + Preview @@ -536,6 +585,7 @@ cboFont + mFontStyleComboBox spinSize mSizeUnitWidget mSizeDDBtn diff --git a/tests/src/3d/testqgs3drendering.cpp b/tests/src/3d/testqgs3drendering.cpp index d109ac725240..065c775e79cf 100644 --- a/tests/src/3d/testqgs3drendering.cpp +++ b/tests/src/3d/testqgs3drendering.cpp @@ -128,11 +128,10 @@ void TestQgs3DRendering::initTestCase() mLayerMeshDataset->dataProvider()->addDataset( dataDir + "/mesh/quad_and_triangle_vertex_vector.dat" ); QVERIFY( mLayerMeshDataset->isValid() ); mLayerMeshDataset->setCrs( mLayerDtm->crs() ); // this testing mesh does not have any CRS defined originally - // disable rendering of scalar 2d datasets for now - QgsMeshRendererSettings settings = mLayerMeshDataset->rendererSettings(); - settings.setActiveScalarDataset( QgsMeshDatasetIndex( 0, 0 ) ); - settings.setActiveVectorDataset( QgsMeshDatasetIndex( 2, 0 ) ); - mLayerMeshDataset->setRendererSettings( settings ); + mLayerMeshDataset->temporalProperties()->setIsActive( false ); + mLayerMeshDataset->setStaticScalarDatasetIndex( QgsMeshDatasetIndex( 0, 0 ) ); + mLayerMeshDataset->setStaticVectorDatasetIndex( QgsMeshDatasetIndex( 2, 0 ) ); + mProject->addMapLayer( mLayerMeshDataset ); mProject->addMapLayer( mLayerMeshDataset ); QgsMesh3DSymbol *symbolMesh3d = new QgsMesh3DSymbol; diff --git a/tests/src/analysis/testqgsprocessing.cpp b/tests/src/analysis/testqgsprocessing.cpp index 6ff8d70a313a..cede52b685cd 100644 --- a/tests/src/analysis/testqgsprocessing.cpp +++ b/tests/src/analysis/testqgsprocessing.cpp @@ -22,6 +22,7 @@ #include "qgsprocessingcontext.h" #include "qgsprocessingparametertype.h" #include "qgsprocessingmodelalgorithm.h" +#include "qgsprocessingmodelgroupbox.h" #include "qgsnativealgorithms.h" #include #include @@ -518,8 +519,10 @@ class TestQgsProcessing: public QObject void providerById(); void removeProvider(); void compatibleLayers(); + void encodeDecodeUriProvider(); void normalizeLayerSource(); void context(); + void feedback(); void mapLayers(); void mapLayerFromStore(); void mapLayerFromString(); @@ -529,13 +532,14 @@ class TestQgsProcessing: public QObject void createIndex(); void parseDestinationString(); void createFeatureSink(); + void source(); void parameters(); void algorithmParameters(); void algorithmOutputs(); void parameterGeneral(); void parameterBoolean(); void parameterCrs(); - void parameterLayer(); + void parameterMapLayer(); void parameterExtent(); void parameterPoint(); void parameterFile(); @@ -567,6 +571,8 @@ class TestQgsProcessing: public QObject void parameterMapTheme(); void parameterDateTime(); void parameterProviderConnection(); + void parameterDatabaseSchema(); + void parameterDatabaseTable(); void checkParamValues(); void combineLayerExtent(); void processingFeatureSource(); @@ -578,6 +584,7 @@ class TestQgsProcessing: public QObject void asPythonCommand(); void modelerAlgorithm(); void modelExecution(); + void modelBranchPruning(); void modelWithProviderWithLimitedTypes(); void modelVectorOutputIsCompatibleType(); void modelAcceptableValues(); @@ -598,6 +605,7 @@ class TestQgsProcessing: public QObject void parameterType(); void sourceTypeToString_data(); void sourceTypeToString(); + void modelSource(); private: @@ -856,6 +864,28 @@ void TestQgsProcessing::compatibleLayers() QCOMPARE( lIds, QStringList() << "R1" << "ar2" << "zz" << "V4" << "v1" << "v3" << "vvvv4" << "MX" << "mA" ); } +void TestQgsProcessing::encodeDecodeUriProvider() +{ + QString provider; + QString uri; + QCOMPARE( QgsProcessingUtils::encodeProviderKeyAndUri( QStringLiteral( "ogr" ), QStringLiteral( "/home/me/test.shp" ) ), QStringLiteral( "ogr:///home/me/test.shp" ) ); + QVERIFY( QgsProcessingUtils::decodeProviderKeyAndUri( QStringLiteral( "ogr:///home/me/test.shp" ), provider, uri ) ); + QCOMPARE( provider, QStringLiteral( "ogr" ) ); + QCOMPARE( uri, QStringLiteral( "/home/me/test.shp" ) ); + QCOMPARE( QgsProcessingUtils::encodeProviderKeyAndUri( QStringLiteral( "ogr" ), QStringLiteral( "http://mysourcem/a.json" ) ), QStringLiteral( "ogr://http://mysourcem/a.json" ) ); + QVERIFY( QgsProcessingUtils::decodeProviderKeyAndUri( QStringLiteral( "ogr://http://mysourcem/a.json" ), provider, uri ) ); + QCOMPARE( provider, QStringLiteral( "ogr" ) ); + QCOMPARE( uri, QStringLiteral( "http://mysourcem/a.json" ) ); + QCOMPARE( QgsProcessingUtils::encodeProviderKeyAndUri( QStringLiteral( "postgres" ), QStringLiteral( "host=blah blah etc" ) ), QStringLiteral( "postgres://host=blah blah etc" ) ); + QVERIFY( QgsProcessingUtils::decodeProviderKeyAndUri( QStringLiteral( "postgres://host=blah blah etc" ), provider, uri ) ); + QCOMPARE( provider, QStringLiteral( "postgres" ) ); + QCOMPARE( uri, QStringLiteral( "host=blah blah etc" ) ); + + // should reject non valid providers + QVERIFY( !QgsProcessingUtils::decodeProviderKeyAndUri( QStringLiteral( "asdasda://host=blah blah etc" ), provider, uri ) ); + QVERIFY( !QgsProcessingUtils::decodeProviderKeyAndUri( QStringLiteral( "http://mysourcem/a.json" ), provider, uri ) ); +} + void TestQgsProcessing::normalizeLayerSource() { QCOMPARE( QgsProcessingUtils::normalizeLayerSource( "data\\layers\\test.shp" ), QString( "data/layers/test.shp" ) ); @@ -1028,6 +1058,19 @@ void TestQgsProcessing::context() QVERIFY( !context2.temporaryLayerStore()->mapLayer( id ) ); } +void TestQgsProcessing::feedback() +{ + QgsProcessingFeedback f; + f.pushInfo( QStringLiteral( "info" ) ); + f.reportError( QStringLiteral( "error" ) ); + f.pushDebugInfo( QStringLiteral( "debug" ) ); + f.pushCommandInfo( QStringLiteral( "command" ) ); + f.pushConsoleInfo( QStringLiteral( "console" ) ); + + QCOMPARE( f.htmlLog(), QStringLiteral( "info
    error
    debug
    command
    console
    " ) ); + QCOMPARE( f.textLog(), QStringLiteral( "info\nerror\ndebug\ncommand\nconsole\n" ) ); +} + void TestQgsProcessing::mapLayers() { QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt @@ -1042,6 +1085,13 @@ void TestQgsProcessing::mapLayers() delete l; + // use encoded provider/uri string + l = QgsProcessingUtils::loadMapLayerFromString( QStringLiteral( "gdal://%1" ).arg( raster ), QgsCoordinateTransformContext() ); + QVERIFY( l->isValid() ); + QCOMPARE( l->type(), QgsMapLayerType::RasterLayer ); + QCOMPARE( l->name(), QStringLiteral( "landsat" ) ); + delete l; + //test with vector l = QgsProcessingUtils::loadMapLayerFromString( vector, QgsCoordinateTransformContext() ); QVERIFY( l->isValid() ); @@ -1049,6 +1099,13 @@ void TestQgsProcessing::mapLayers() QCOMPARE( l->name(), QStringLiteral( "points" ) ); delete l; + // use encoded provider/uri string + l = QgsProcessingUtils::loadMapLayerFromString( QStringLiteral( "ogr://%1" ).arg( vector ), QgsCoordinateTransformContext() ); + QVERIFY( l->isValid() ); + QCOMPARE( l->type(), QgsMapLayerType::VectorLayer ); + QCOMPARE( l->name(), QStringLiteral( "points" ) ); + delete l; + l = QgsProcessingUtils::loadMapLayerFromString( QString(), QgsCoordinateTransformContext() ); QVERIFY( !l ); l = QgsProcessingUtils::loadMapLayerFromString( QStringLiteral( "so much room for activities!" ), QgsCoordinateTransformContext() ); @@ -1356,6 +1413,12 @@ void TestQgsProcessing::features() QVERIFY( ids.isEmpty() ); QCOMPARE( source->featureCount(), 0L ); + // feature limit + params.insert( QStringLiteral( "layer" ), QVariant::fromValue( QgsProcessingFeatureSourceDefinition( layer->id(), false, 3 ) ) ); + source.reset( QgsProcessingParameters::parameterAsSource( def.get(), params, context ) ); + ids = getIds( source->getFeatures() ); + QCOMPARE( ids.size(), 3 ); + QCOMPARE( source->featureCount(), 3L ); // test that feature request is honored params.insert( QStringLiteral( "layer" ), QVariant::fromValue( QgsProcessingFeatureSourceDefinition( layer->id(), false ) ) ); @@ -1399,10 +1462,35 @@ void TestQgsProcessing::features() ids = getIds( source->getFeatures() ); QVERIFY( !encountered ); + // context wants to filter, but filtering disabled on source definition + context.setInvalidGeometryCheck( QgsFeatureRequest::GeometryAbortOnInvalid ); + context.setInvalidGeometryCallback( callback ); + params.insert( QStringLiteral( "layer" ), QVariant::fromValue( QgsProcessingFeatureSourceDefinition( polyLayer->id(), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometryNoCheck ) ) ); + + source.reset( QgsProcessingParameters::parameterAsSource( def.get(), params, context ) ); + ids = getIds( source->getFeatures() ); + QVERIFY( !encountered ); + + QgsProcessingContext context2; + // context wants to skip, source wants to abort + context2.setInvalidGeometryCheck( QgsFeatureRequest::GeometrySkipInvalid ); + params.insert( QStringLiteral( "layer" ), QVariant::fromValue( QgsProcessingFeatureSourceDefinition( polyLayer->id(), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometryAbortOnInvalid ) ) ); + source.reset( QgsProcessingParameters::parameterAsSource( def.get(), params, context ) ); + try + { + ids = getIds( source->getFeatures() ); + QVERIFY( false ); + } + catch ( QgsProcessingException & ) + {} + // equality operator QVERIFY( QgsProcessingFeatureSourceDefinition( layer->id(), true ) == QgsProcessingFeatureSourceDefinition( layer->id(), true ) ); QVERIFY( QgsProcessingFeatureSourceDefinition( layer->id(), true ) != QgsProcessingFeatureSourceDefinition( "b", true ) ); QVERIFY( QgsProcessingFeatureSourceDefinition( layer->id(), true ) != QgsProcessingFeatureSourceDefinition( layer->id(), false ) ); + QVERIFY( QgsProcessingFeatureSourceDefinition( layer->id(), true ) != QgsProcessingFeatureSourceDefinition( layer->id(), true, 5 ) ); + QVERIFY( QgsProcessingFeatureSourceDefinition( layer->id(), true, 5, nullptr ) != QgsProcessingFeatureSourceDefinition( layer->id(), true, 5, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ) ); + QVERIFY( QgsProcessingFeatureSourceDefinition( layer->id(), true, 5, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometrySkipInvalid ) != QgsProcessingFeatureSourceDefinition( layer->id(), true, 5, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometryAbortOnInvalid ) ); } void TestQgsProcessing::uniqueValues() @@ -1557,6 +1645,21 @@ void TestQgsProcessing::parseDestinationString() QVERIFY( !useWriter ); QVERIFY( extension.isEmpty() ); + // newer format + destination = QStringLiteral( "postgres://dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ); + QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension ); + QCOMPARE( providerKey, QStringLiteral( "postgres" ) ); + QCOMPARE( uri, QStringLiteral( "dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ) ); + QVERIFY( !useWriter ); + QVERIFY( extension.isEmpty() ); + //mssql + destination = QStringLiteral( "mssql://dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ); + QgsProcessingUtils::parseDestinationString( destination, providerKey, uri, layerName, format, options, useWriter, extension ); + QCOMPARE( providerKey, QStringLiteral( "mssql" ) ); + QCOMPARE( uri, QStringLiteral( "dbname='db' host=DBHOST port=5432 table=\"calcs\".\"output\" (geom) sql=" ) ); + QVERIFY( !useWriter ); + QVERIFY( extension.isEmpty() ); + // full uri shp output options.clear(); destination = QStringLiteral( "ogr:d:/test.shp" ); @@ -1697,6 +1800,8 @@ void TestQgsProcessing::createFeatureSink() prevDest = QDir::tempPath() + "/create_feature_sink2.gpkg"; sink.reset( QgsProcessingUtils::createFeatureSink( destination, context, fields, QgsWkbTypes::PointZ, QgsCoordinateReferenceSystem::fromEpsgId( 3111 ) ) ); QVERIFY( sink.get() ); + f = QgsFeature( fields ); + f.setAttributes( QgsAttributes() << "val" ); f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "PointZ(1 2 3)" ) ) ); QVERIFY( sink->addFeature( f ) ); QCOMPARE( destination, prevDest ); @@ -1709,7 +1814,36 @@ void TestQgsProcessing::createFeatureSink() QCOMPARE( layer->fields().at( 1 ).name(), QStringLiteral( "my_field" ) ); QCOMPARE( layer->fields().at( 1 ).type(), QVariant::String ); QCOMPARE( layer->featureCount(), 1L ); - + // append to existing OGR layer + QgsRemappingSinkDefinition remapDef; + remapDef.setDestinationFields( layer->fields() ); + remapDef.setDestinationCrs( layer->crs() ); + remapDef.setSourceCrs( QgsCoordinateReferenceSystem( "EPSG:4326" ) ); + remapDef.setDestinationWkbType( QgsWkbTypes::Polygon ); + remapDef.addMappedField( QStringLiteral( "my_field" ), QgsProperty::fromExpression( QStringLiteral( "field2 || @extra" ) ) ); + QgsFields fields2; + fields2.append( QgsField( "field2", QVariant::String ) ); + context.expressionContext().appendScope( new QgsExpressionContextScope() ); + context.expressionContext().scope( 0 )->setVariable( QStringLiteral( "extra" ), 2 ); + sink.reset( QgsProcessingUtils::createFeatureSink( destination, context, fields2, QgsWkbTypes::Point, QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), QVariantMap(), nullptr, &remapDef ) ); + QVERIFY( sink.get() ); + f = QgsFeature( fields2 ); + f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "Point(10 0)" ) ) ); + f.setAttributes( QgsAttributes() << "val" ); + QVERIFY( sink->addFeature( f ) ); + QCOMPARE( destination, prevDest ); + sink.reset( nullptr ); + layer = new QgsVectorLayer( destination ); + QVERIFY( layer->isValid() ); + QCOMPARE( layer->featureCount(), 2L ); + QgsFeatureIterator it = layer->getFeatures(); + QVERIFY( it.nextFeature( f ) ); + QCOMPARE( f.attributes().at( 1 ).toString(), QStringLiteral( "val" ) ); + QCOMPARE( f.geometry().asWkt( 1 ), QStringLiteral( "PointZ (1 2 3)" ) ); + QVERIFY( it.nextFeature( f ) ); + QCOMPARE( f.attributes().at( 1 ).toString(), QStringLiteral( "val2" ) ); + QCOMPARE( f.geometry().asWkt( 0 ), QStringLiteral( "Point (-10199761 -4017774)" ) ); + delete layer; //windows style path destination = "d:\\temp\\create_feature_sink.tab"; sink.reset( QgsProcessingUtils::createFeatureSink( destination, context, fields, QgsWkbTypes::Polygon, QgsCoordinateReferenceSystem::fromEpsgId( 3111 ) ) ); @@ -1754,6 +1888,93 @@ void TestQgsProcessing::createFeatureSink() QCOMPARE( layer->wkbType(), QgsWkbTypes::Polygon ); QVERIFY( layer->getFeatures().nextFeature( f ) ); QCOMPARE( f.attribute( "my_field" ).toString(), QStringLiteral( "val" ) ); + + // now append to that second layer + remapDef.setDestinationFields( layer->fields() ); + remapDef.setDestinationCrs( layer->crs() ); + + remapDef.setSourceCrs( QgsCoordinateReferenceSystem( "EPSG:4326" ) ); + remapDef.setDestinationWkbType( QgsWkbTypes::Point ); + remapDef.addMappedField( QStringLiteral( "my_field" ), QgsProperty::fromExpression( QStringLiteral( "field2 || @extra" ) ) ); + destination2 = QStringLiteral( "ogr:dbname='%1' table=\"points\" (geom) sql=" ).arg( geopackagePath ); + sink.reset( QgsProcessingUtils::createFeatureSink( destination2, context, fields2, QgsWkbTypes::PointZ, QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), QVariantMap(), nullptr, &remapDef ) ); + QVERIFY( sink.get() ); + f = QgsFeature( fields ); + f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "PointZ(3 4 5)" ) ) ); + f.setAttributes( QgsAttributes() << "v" ); + QVERIFY( sink->addFeature( f ) ); + sink.reset( nullptr ); + layer = qobject_cast< QgsVectorLayer *>( QgsProcessingUtils::mapLayerFromString( destination2, context, true ) ); + QVERIFY( layer->isValid() ); + QCOMPARE( layer->wkbType(), QgsWkbTypes::Point ); + QCOMPARE( layer->featureCount(), 2L ); + QVERIFY( layer->getFeatures().nextFeature( f ) ); + QCOMPARE( f.attribute( "my_field" ).toString(), QStringLiteral( "val2" ) ); +} + +void TestQgsProcessing::source() +{ + QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt + QgsVectorLayer *invalidLayer = new QgsVectorLayer( testDataDir + "invalidgeometries.gml", QString(), "ogr" ); + QVERIFY( invalidLayer->isValid() ); + + QgsProcessingContext context; + context.setInvalidGeometryCheck( QgsFeatureRequest::GeometryAbortOnInvalid ); + QgsProcessingFeatureSource source( invalidLayer, context ); + // expect an exception, we should be using the context's "abort on invalid" setting + QgsFeatureIterator it = source.getFeatures(); + QgsFeature f; + try + { + it.nextFeature( f ); + QVERIFY( false ); + } + catch ( QgsProcessingException & ) + { + + } + + // now try with a source overriding the context's setting + source.setInvalidGeometryCheck( QgsFeatureRequest::GeometryNoCheck ); + it = source.getFeatures(); + QVERIFY( it.nextFeature( f ) ); + QVERIFY( !f.geometry().isGeosValid() ); + // all good! + + QgsVectorLayer *polygonLayer = new QgsVectorLayer( testDataDir + "polys.shp", QString(), "ogr" ); + QVERIFY( polygonLayer->isValid() ); + + QgsProcessingFeatureSource source2( polygonLayer, context ); + QCOMPARE( source2.featureCount(), 10L ); + int i = 0; + it = source2.getFeatures(); + while ( it.nextFeature( f ) ) + i++; + QCOMPARE( i, 10 ); + + // now with a limit on features + QgsProcessingFeatureSource source3( polygonLayer, context, false, 5 ); + QCOMPARE( source3.featureCount(), 5L ); + i = 0; + it = source3.getFeatures(); + while ( it.nextFeature( f ) ) + i++; + QCOMPARE( i, 5 ); + + // feature request has a lower limit than source + it = source3.getFeatures( QgsFeatureRequest().setLimit( 2 ) ); + i = 0; + while ( it.nextFeature( f ) ) + i++; + QCOMPARE( i, 2 ); + + // feature request has a higher limit than source + it = source3.getFeatures( QgsFeatureRequest().setLimit( 12 ) ); + i = 0; + while ( it.nextFeature( f ) ) + i++; + QCOMPARE( i, 5 ); + } void TestQgsProcessing::parameters() @@ -2378,7 +2599,7 @@ void TestQgsProcessing::parameterCrs() QVERIFY( !fromCode->defaultValue().isValid() ); } -void TestQgsProcessing::parameterLayer() +void TestQgsProcessing::parameterMapLayer() { // setup a context QgsProject p; @@ -2404,6 +2625,11 @@ void TestQgsProcessing::parameterLayer() QVERIFY( !def->checkValueIsAcceptable( "" ) ); QVERIFY( !def->checkValueIsAcceptable( QVariant() ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + // should be OK QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp" ) ); // ... unless we use context, when the check that the layer actually exists is performed @@ -2464,6 +2690,42 @@ void TestQgsProcessing::parameterLayer() def.reset( dynamic_cast< QgsProcessingParameterMapLayer *>( QgsProcessingParameters::parameterFromVariantMap( map ) ) ); QVERIFY( dynamic_cast< QgsProcessingParameterMapLayer *>( def.get() ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeVectorPoint ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeVectorPoint])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer point" ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeVectorLine ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeVectorLine])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer line" ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeVectorPolygon ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeVectorPolygon])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer polygon" ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeVectorAnyGeometry])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer hasgeometry" ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeVectorPoint,QgsProcessing.TypeVectorLine])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer point line" ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorPolygon ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeVectorPoint,QgsProcessing.TypeVectorPolygon])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer point polygon" ) ); + def->setDataTypes( QList< int >() << QgsProcessing::TypeRaster << QgsProcessing::TypeVectorPoint ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMapLayer('non_optional', '', defaultValue='', types=[QgsProcessing.TypeRaster,QgsProcessing.TypeVectorPoint])" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=layer raster point" ) ); + // optional def.reset( new QgsProcessingParameterMapLayer( "optional", QString(), v1->id(), true ) ); params.insert( "optional", QVariant() ); @@ -2549,6 +2811,8 @@ void TestQgsProcessing::parameterExtent() QVERIFY( !def->checkValueIsAcceptable( QgsRectangle() ) ); QVERIFY( def->checkValueIsAcceptable( QgsReferencedRectangle( QgsRectangle( 1, 2, 3, 4 ), QgsCoordinateReferenceSystem( "EPSG:4326" ) ) ) ); QVERIFY( !def->checkValueIsAcceptable( QgsReferencedRectangle( QgsRectangle(), QgsCoordinateReferenceSystem( "EPSG:4326" ) ) ) ); + QVERIFY( def->checkValueIsAcceptable( QgsGeometry::fromRect( QgsRectangle( 1, 2, 3, 4 ) ) ) ); + QVERIFY( def->checkValueIsAcceptable( QgsGeometry::fromWkt( QStringLiteral( "LineString(10 10, 20 20)" ) ) ) ); // these checks require a context - otherwise we could potentially be referring to a layer source QVERIFY( def->checkValueIsAcceptable( "1,2,3" ) ); @@ -2725,6 +2989,20 @@ void TestQgsProcessing::parameterExtent() p.setCrs( QgsCoordinateReferenceSystem( "EPSG:3785" ) ); QCOMPARE( QgsProcessingParameters::parameterAsExtentCrs( def.get(), params, context ).authid(), QStringLiteral( "EPSG:3785" ) ); + // QgsGeometry + params.insert( "non_optional", QgsGeometry::fromRect( QgsRectangle( 13, 14, 15, 16 ) ) ); + ext = QgsProcessingParameters::parameterAsExtent( def.get(), params, context ); + QGSCOMPARENEAR( ext.xMinimum(), 13, 0.001 ); + QGSCOMPARENEAR( ext.xMaximum(), 15, 0.001 ); + QGSCOMPARENEAR( ext.yMinimum(), 14, 0.001 ); + QGSCOMPARENEAR( ext.yMaximum(), 16, 0.001 ); + // with target CRS - should make no difference, because source CRS is unknown + ext = QgsProcessingParameters::parameterAsExtent( def.get(), params, context, QgsCoordinateReferenceSystem( "EPSG:3785" ) ); + QGSCOMPARENEAR( ext.xMinimum(), 13, 0.001 ); + QGSCOMPARENEAR( ext.xMaximum(), 15, 0.001 ); + QGSCOMPARENEAR( ext.yMinimum(), 14, 0.001 ); + QGSCOMPARENEAR( ext.yMaximum(), 16, 0.001 ); + // QgsReferencedRectangle params.insert( "non_optional", QgsReferencedRectangle( QgsRectangle( 1.1, 2.2, 3.3, 4.4 ), QgsCoordinateReferenceSystem( "EPSG:4326" ) ) ); ext = QgsProcessingParameters::parameterAsExtent( def.get(), params, context ); @@ -2761,6 +3039,7 @@ void TestQgsProcessing::parameterExtent() QCOMPARE( def->valueAsPythonString( "1,2,3,4 [EPSG:4326]", context ), QStringLiteral( "'1,2,3,4 [EPSG:4326]'" ) ); QCOMPARE( def->valueAsPythonString( "uri='complex' username=\"complex\"", context ), QStringLiteral( "'uri=\\'complex\\' username=\\\"complex\\\"'" ) ); QCOMPARE( def->valueAsPythonString( QStringLiteral( "c:\\test\\new data\\test.dat" ), context ), QStringLiteral( "'c:\\\\test\\\\new data\\\\test.dat'" ) ); + QCOMPARE( def->valueAsPythonString( QgsGeometry::fromWkt( QStringLiteral( "LineString( 10 10, 20 20)" ) ), context ), QStringLiteral( "QgsGeometry.fromWkt('LineString (10 10, 20 20)')" ) ); QString pythonCode = def->asPythonString(); QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterExtent('non_optional', '', defaultValue='1,2,3,4')" ) ); @@ -3271,6 +3550,10 @@ void TestQgsProcessing::parameterLayerList() QVERIFY( !def->checkValueIsAcceptable( QStringList() << "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp", &context ) ); QVERIFY( !def->checkValueIsAcceptable( QVariantList() << "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp", &context ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); // using existing map layer ID QVariantMap params; @@ -3415,6 +3698,10 @@ void TestQgsProcessing::parameterLayerList() def.reset( new QgsProcessingParameterMultipleLayers( "optional", QString(), QgsProcessing::TypeMapLayer, QVariantList() << v1->id() << r1->publicSource(), true ) ); params.insert( "optional", QVariant() ); QCOMPARE( QgsProcessingParameters::parameterAsLayerList( def.get(), params, context ), QList< QgsMapLayer *>() << v1 << r1 ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); pythonCode = def->asPythonString(); QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMultipleLayers('optional', '', optional=True, layerType=QgsProcessing.TypeMapLayer, defaultValue=['" ) + r1->publicSource() + "'])" ); @@ -3438,6 +3725,11 @@ void TestQgsProcessing::parameterLayerList() QCOMPARE( QgsProcessingParameters::parameterAsLayerList( def.get(), params, context ), QList< QgsMapLayer *>() << v1 << r1 ); def.reset( new QgsProcessingParameterMultipleLayers( "type", QString(), QgsProcessing::TypeRaster ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + pythonCode = def->asPythonString(); QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMultipleLayers('type', '', layerType=QgsProcessing.TypeRaster, defaultValue=None)" ) ); code = def->asScriptCode(); @@ -3451,6 +3743,8 @@ void TestQgsProcessing::parameterLayerList() QCOMPARE( fromCode->layerType(), QgsProcessing::TypeRaster ); def.reset( new QgsProcessingParameterMultipleLayers( "type", QString(), QgsProcessing::TypeFile ) ); + QCOMPARE( def->createFileFilter(), QStringLiteral( "All files (*.*)" ) ); + pythonCode = def->asPythonString(); QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterMultipleLayers('type', '', layerType=QgsProcessing.TypeFile, defaultValue=None)" ) ); code = def->asScriptCode(); @@ -4039,6 +4333,11 @@ void TestQgsProcessing::parameterRasterLayer() QVERIFY( def->checkValueIsAcceptable( QVariant::fromValue( r1 ) ) ); QVERIFY( !def->checkValueIsAcceptable( QVariant::fromValue( v1 ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + // should be OK QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.tif" ) ); // ... unless we use context, when the check that the layer actually exists is performed @@ -4996,6 +5295,11 @@ void TestQgsProcessing::parameterVectorLayer() QVERIFY( def->checkValueIsAcceptable( QgsProperty::fromValue( QStringLiteral( "layer12312312" ) ) ) ); QVERIFY( !def->checkValueIsAcceptable( QgsProperty::fromValue( QString() ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + // should be OK QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp" ) ); // ... unless we use context, when the check that the layer actually exists is performed @@ -5125,6 +5429,11 @@ void TestQgsProcessing::parameterMeshLayer() // ... unless we use context, when the check that the layer actually exists is performed QVERIFY( !def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.2dm", &context ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + // using existing map layer ID QVariantMap params; params.insert( "non_optional", m1->id() ); @@ -5246,6 +5555,11 @@ void TestQgsProcessing::parameterFeatureSource() // ... unless we use context, when the check that the layer actually exists is performed QVERIFY( !def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.shp", &context ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + // using existing map layer ID QVariantMap params; params.insert( "non_optional", v1->id() ); @@ -5276,12 +5590,24 @@ void TestQgsProcessing::parameterFeatureSource() QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( "abc" ) ), context ), QStringLiteral( "'abc'" ) ); QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( v2->id() ) ), context ), QStringLiteral( "'%1'" ).arg( vector2 ) ); QCOMPARE( def->valueAsPythonString( v2->id(), context ), QStringLiteral( "'%1'" ).arg( vector2 ) ); - QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), true ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition('abc', True)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), true ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition('abc', selectedFeaturesOnly=True, featureLimit=-1, geometryCheck=QgsFeatureRequest.GeometryAbortOnInvalid)" ) ); QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ) ) ), context ), QStringLiteral( "QgsProperty.fromExpression('\"abc\" || \"def\"')" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ), true ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('\"abc\" || \"def\"'), selectedFeaturesOnly=True, featureLimit=-1, geometryCheck=QgsFeatureRequest.GeometryAbortOnInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), false, 11 ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition('abc', selectedFeaturesOnly=False, featureLimit=11, geometryCheck=QgsFeatureRequest.GeometryAbortOnInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ), false, 11 ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('\"abc\" || \"def\"'), selectedFeaturesOnly=False, featureLimit=11, geometryCheck=QgsFeatureRequest.GeometryAbortOnInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), false, -1, nullptr, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "'abc'" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ), false, -1, nullptr, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProperty.fromExpression('\"abc\" || \"def\"')" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition('abc', selectedFeaturesOnly=False, featureLimit=-1, flags=QgsProcessingFeatureSourceDefinition.FlagOverrideDefaultGeometryCheck, geometryCheck=QgsFeatureRequest.GeometrySkipInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('\"abc\" || \"def\"'), selectedFeaturesOnly=False, featureLimit=-1, flags=QgsProcessingFeatureSourceDefinition.FlagOverrideDefaultGeometryCheck, geometryCheck=QgsFeatureRequest.GeometrySkipInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition('abc', selectedFeaturesOnly=False, featureLimit=-1, flags=QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature, geometryCheck=QgsFeatureRequest.GeometrySkipInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('\"abc\" || \"def\"'), selectedFeaturesOnly=False, featureLimit=-1, flags=QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature, geometryCheck=QgsFeatureRequest.GeometrySkipInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromValue( "abc" ), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck | QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition('abc', selectedFeaturesOnly=False, featureLimit=-1, flags=QgsProcessingFeatureSourceDefinition.FlagOverrideDefaultGeometryCheck | QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature, geometryCheck=QgsFeatureRequest.GeometrySkipInvalid)" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProcessingFeatureSourceDefinition( QgsProperty::fromExpression( "\"abc\" || \"def\"" ), false, -1, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck | QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature, QgsFeatureRequest::GeometrySkipInvalid ) ), context ), QStringLiteral( "QgsProcessingFeatureSourceDefinition(QgsProperty.fromExpression('\"abc\" || \"def\"'), selectedFeaturesOnly=False, featureLimit=-1, flags=QgsProcessingFeatureSourceDefinition.FlagOverrideDefaultGeometryCheck | QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature, geometryCheck=QgsFeatureRequest.GeometrySkipInvalid)" ) ); QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProperty::fromExpression( "\"a\"=1" ) ), context ), QStringLiteral( "QgsProperty.fromExpression('\"a\"=1')" ) ); QCOMPARE( def->valueAsPythonString( QVariant::fromValue( v2 ), context ), QStringLiteral( "'%1'" ).arg( vector2 ) ); QCOMPARE( def->valueAsPythonString( "uri='complex' username=\"complex\"", context ), QStringLiteral( "'uri=\\'complex\\' username=\\\"complex\\\"'" ) ); QCOMPARE( def->valueAsPythonString( QStringLiteral( "c:\\test\\new data\\test.dat" ), context ), QStringLiteral( "'c:\\\\test\\\\new data\\\\test.dat'" ) ); + QCOMPARE( def->valueAsPythonString( QStringLiteral( "postgres://uri='complex' username=\"complex\"" ), context ), QStringLiteral( "'postgres://uri=\\'complex\\' username=\\\"complex\\\"'" ) ); QVariantMap map = def->toVariantMap(); QgsProcessingParameterFeatureSource fromMap( "x" ); @@ -5417,6 +5743,11 @@ void TestQgsProcessing::parameterFeatureSink() QVERIFY( def->generateTemporaryDestination().endsWith( QLatin1String( ".gpkg" ) ) ); QVERIFY( def->generateTemporaryDestination().startsWith( QgsProcessingUtils::tempFolder() ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + QVariantMap map = def->toVariantMap(); QgsProcessingParameterFeatureSink fromMap( "x" ); QVERIFY( fromMap.fromVariantMap( map ) ); @@ -5581,6 +5912,11 @@ void TestQgsProcessing::parameterVectorOut() QVERIFY( def->generateTemporaryDestination().endsWith( QLatin1String( ".gpkg" ) ) ); QVERIFY( def->generateTemporaryDestination().startsWith( QgsProcessingUtils::tempFolder() ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + QVariantMap map = def->toVariantMap(); QgsProcessingParameterVectorDestination fromMap( "x" ); QVERIFY( fromMap.fromVariantMap( map ) ); @@ -5764,6 +6100,11 @@ void TestQgsProcessing::parameterRasterOut() QVERIFY( def->generateTemporaryDestination().endsWith( QLatin1String( ".tif" ) ) ); QVERIFY( def->generateTemporaryDestination().startsWith( QgsProcessingUtils::tempFolder() ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.shp" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.tif" ) ) ); + QVERIFY( !def->createFileFilter().contains( QStringLiteral( "*.2dm" ) ) ); + QVERIFY( def->createFileFilter().contains( QStringLiteral( "*.*" ) ) ); + QVariantMap params; params.insert( "non_optional", "test.tif" ); QCOMPARE( QgsProcessingParameters::parameterAsOutputLayer( def.get(), params, context ), QStringLiteral( "test.tif" ) ); @@ -5884,6 +6225,8 @@ void TestQgsProcessing::parameterFileOut() QCOMPARE( def->defaultFileExtension(), QStringLiteral( "pcx" ) ); def->setFileFilter( QStringLiteral( "PCX files (*.pcx *.picx);;BMP files (*.bmp)" ) ); QCOMPARE( def->defaultFileExtension(), QStringLiteral( "pcx" ) ); + QCOMPARE( def->createFileFilter(), QStringLiteral( "PCX files (*.pcx *.picx);;BMP files (*.bmp);;All files (*.*)" ) ); + def->setFileFilter( QString() ); QCOMPARE( def->defaultFileExtension(), QStringLiteral( "file" ) ); QVERIFY( def->generateTemporaryDestination().endsWith( QLatin1String( ".file" ) ) ); @@ -5904,6 +6247,8 @@ void TestQgsProcessing::parameterFileOut() QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.txt" ) ); QVERIFY( def->checkValueIsAcceptable( "c:/Users/admin/Desktop/roads_clipped_transformed_v1_reprojected_final_clipped_aAAA.txt", &context ) ); + QCOMPARE( def->createFileFilter(), QStringLiteral( "All files (*.*)" ) ); + QVariantMap params; params.insert( "non_optional", "test.txt" ); QCOMPARE( QgsProcessingParameters::parameterAsFileOutput( def.get(), params, context ), QStringLiteral( "test.txt" ) ); @@ -7005,6 +7350,177 @@ void TestQgsProcessing::parameterProviderConnection() QVERIFY( def->checkValueIsAcceptable( QVariant() ) ); // should be valid, falls back to valid default } +void TestQgsProcessing::parameterDatabaseSchema() +{ + QgsProcessingContext context; + + // not optional! + std::unique_ptr< QgsProcessingParameterDatabaseSchema > def( new QgsProcessingParameterDatabaseSchema( "non_optional", QString(), QString(), QVariant(), false ) ); + QVERIFY( def->checkValueIsAcceptable( 1 ) ); + QVERIFY( def->checkValueIsAcceptable( "test" ) ); + QVERIFY( !def->checkValueIsAcceptable( "" ) ); + QVERIFY( !def->checkValueIsAcceptable( QVariant() ) ); + + // string + QVariantMap params; + params.insert( "non_optional", QString( "a" ) ); + QCOMPARE( QgsProcessingParameters::parameterAsSchema( def.get(), params, context ), QStringLiteral( "a" ) ); + + QCOMPARE( def->valueAsPythonString( QVariant(), context ), QStringLiteral( "None" ) ); + QCOMPARE( def->valueAsPythonString( QStringLiteral( "abc" ), context ), QStringLiteral( "'abc'" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProperty::fromExpression( "\"a\"=1" ) ), context ), QStringLiteral( "QgsProperty.fromExpression('\"a\"=1')" ) ); + QCOMPARE( def->valueAsPythonString( "probably\'invalid\"schema", context ), QStringLiteral( "'probably\\'invalid\\\"schema'" ) ); + + QString pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseSchema('non_optional', '', connectionParameterName='', defaultValue=None)" ) ); + + QString code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=databaseschema" ) ); + std::unique_ptr< QgsProcessingParameterDatabaseSchema > fromCode( dynamic_cast< QgsProcessingParameterDatabaseSchema * >( QgsProcessingParameters::parameterFromScriptCode( code ) ) ); + QVERIFY( fromCode.get() ); + QCOMPARE( fromCode->name(), def->name() ); + QCOMPARE( fromCode->description(), QStringLiteral( "non optional" ) ); + QCOMPARE( fromCode->flags(), def->flags() ); + QCOMPARE( fromCode->defaultValue(), def->defaultValue() ); + QCOMPARE( fromCode->parentConnectionParameterName(), def->parentConnectionParameterName() ); + + QVERIFY( def->dependsOnOtherParameters().isEmpty() ); + def->setParentConnectionParameterName( "my_parent" ); + QCOMPARE( def->dependsOnOtherParameters(), QStringList() << QStringLiteral( "my_parent" ) ); + + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseSchema('non_optional', '', connectionParameterName='my_parent', defaultValue=None)" ) ); + + code = def->asScriptCode(); + fromCode.reset( dynamic_cast< QgsProcessingParameterDatabaseSchema * >( QgsProcessingParameters::parameterFromScriptCode( code ) ) ); + QVERIFY( fromCode.get() ); + QCOMPARE( fromCode->name(), def->name() ); + QCOMPARE( fromCode->description(), QStringLiteral( "non optional" ) ); + QCOMPARE( fromCode->flags(), def->flags() ); + QCOMPARE( fromCode->defaultValue(), def->defaultValue() ); + QCOMPARE( fromCode->parentConnectionParameterName(), def->parentConnectionParameterName() ); + + // optional + def.reset( new QgsProcessingParameterDatabaseSchema( "optional", QString(), QString(), QStringLiteral( "def" ), true ) ); + QVERIFY( def->checkValueIsAcceptable( 1 ) ); + QVERIFY( def->checkValueIsAcceptable( "test" ) ); + QVERIFY( def->checkValueIsAcceptable( "" ) ); + QVERIFY( def->checkValueIsAcceptable( QVariant() ) ); + + params.insert( "optional", QVariant() ); + QCOMPARE( QgsProcessingParameters::parameterAsSchema( def.get(), params, context ), QStringLiteral( "def" ) ); + + // optional, no default + def.reset( new QgsProcessingParameterDatabaseSchema( "optional", QString(), QString(), QVariant(), true ) ); + params.insert( "optional", QVariant() ); + QVERIFY( QgsProcessingParameters::parameterAsSchema( def.get(), params, context ).isEmpty() ); + + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseSchema('optional', '', optional=True, connectionParameterName='', defaultValue=None)" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##optional=optional databaseschema" ) ); + fromCode.reset( dynamic_cast< QgsProcessingParameterDatabaseSchema * >( QgsProcessingParameters::parameterFromScriptCode( code ) ) ); + QVERIFY( fromCode.get() ); + QCOMPARE( fromCode->name(), def->name() ); + QCOMPARE( fromCode->description(), QStringLiteral( "optional" ) ); + QCOMPARE( fromCode->flags(), def->flags() ); + QCOMPARE( fromCode->defaultValue(), def->defaultValue() ); + QCOMPARE( fromCode->parentConnectionParameterName(), def->parentConnectionParameterName() ); +} + +void TestQgsProcessing::parameterDatabaseTable() +{ + QgsProcessingContext context; + + // not optional! + std::unique_ptr< QgsProcessingParameterDatabaseTable > def( new QgsProcessingParameterDatabaseTable( "non_optional", QString(), QString(), QString(), QVariant(), false ) ); + QVERIFY( def->checkValueIsAcceptable( 1 ) ); + QVERIFY( def->checkValueIsAcceptable( "test" ) ); + QVERIFY( !def->checkValueIsAcceptable( "" ) ); + QVERIFY( !def->checkValueIsAcceptable( QVariant() ) ); + + // string + QVariantMap params; + params.insert( "non_optional", QString( "a" ) ); + QCOMPARE( QgsProcessingParameters::parameterAsDatabaseTableName( def.get(), params, context ), QStringLiteral( "a" ) ); + + QCOMPARE( def->valueAsPythonString( QVariant(), context ), QStringLiteral( "None" ) ); + QCOMPARE( def->valueAsPythonString( QStringLiteral( "abc" ), context ), QStringLiteral( "'abc'" ) ); + QCOMPARE( def->valueAsPythonString( QVariant::fromValue( QgsProperty::fromExpression( "\"a\"=1" ) ), context ), QStringLiteral( "QgsProperty.fromExpression('\"a\"=1')" ) ); + QCOMPARE( def->valueAsPythonString( "probably\'invalid\"schema", context ), QStringLiteral( "'probably\\'invalid\\\"schema'" ) ); + + QString pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseTable('non_optional', '', connectionParameterName='', schemaParameterName='', defaultValue=None)" ) ); + + QString code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##non_optional=databasetable none none" ) ); + std::unique_ptr< QgsProcessingParameterDatabaseTable > fromCode( dynamic_cast< QgsProcessingParameterDatabaseTable * >( QgsProcessingParameters::parameterFromScriptCode( code ) ) ); + QVERIFY( fromCode.get() ); + QCOMPARE( fromCode->name(), def->name() ); + QCOMPARE( fromCode->description(), QStringLiteral( "non optional" ) ); + QCOMPARE( fromCode->flags(), def->flags() ); + QCOMPARE( fromCode->defaultValue(), def->defaultValue() ); + QCOMPARE( fromCode->parentConnectionParameterName(), def->parentConnectionParameterName() ); + QCOMPARE( fromCode->parentSchemaParameterName(), def->parentSchemaParameterName() ); + + QVERIFY( def->dependsOnOtherParameters().isEmpty() ); + def->setParentConnectionParameterName( "my_parent" ); + QCOMPARE( def->dependsOnOtherParameters(), QStringList() << QStringLiteral( "my_parent" ) ); + def->setParentSchemaParameterName( "my_schema" ); + QCOMPARE( def->dependsOnOtherParameters(), QStringList() << QStringLiteral( "my_parent" ) << QStringLiteral( "my_schema" ) ); + + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseTable('non_optional', '', connectionParameterName='my_parent', schemaParameterName='my_schema', defaultValue=None)" ) ); + + code = def->asScriptCode(); + fromCode.reset( dynamic_cast< QgsProcessingParameterDatabaseTable * >( QgsProcessingParameters::parameterFromScriptCode( code ) ) ); + QVERIFY( fromCode.get() ); + QCOMPARE( fromCode->name(), def->name() ); + QCOMPARE( fromCode->description(), QStringLiteral( "non optional" ) ); + QCOMPARE( fromCode->flags(), def->flags() ); + QCOMPARE( fromCode->defaultValue(), def->defaultValue() ); + QCOMPARE( fromCode->parentConnectionParameterName(), def->parentConnectionParameterName() ); + QCOMPARE( fromCode->parentSchemaParameterName(), def->parentSchemaParameterName() ); + + // optional + def.reset( new QgsProcessingParameterDatabaseTable( "optional", QString(), QString(), QString(), QStringLiteral( "def" ), true ) ); + QVERIFY( def->checkValueIsAcceptable( 1 ) ); + QVERIFY( def->checkValueIsAcceptable( "test" ) ); + QVERIFY( def->checkValueIsAcceptable( "" ) ); + QVERIFY( def->checkValueIsAcceptable( QVariant() ) ); + + params.insert( "optional", QVariant() ); + QCOMPARE( QgsProcessingParameters::parameterAsDatabaseTableName( def.get(), params, context ), QStringLiteral( "def" ) ); + + // optional, no default + def.reset( new QgsProcessingParameterDatabaseTable( "optional", QString(), QString(), QString(), QVariant(), true ) ); + params.insert( "optional", QVariant() ); + QVERIFY( QgsProcessingParameters::parameterAsDatabaseTableName( def.get(), params, context ).isEmpty() ); + + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseTable('optional', '', optional=True, connectionParameterName='', schemaParameterName='', defaultValue=None)" ) ); + code = def->asScriptCode(); + QCOMPARE( code, QStringLiteral( "##optional=optional databasetable none none" ) ); + fromCode.reset( dynamic_cast< QgsProcessingParameterDatabaseTable * >( QgsProcessingParameters::parameterFromScriptCode( code ) ) ); + QVERIFY( fromCode.get() ); + QCOMPARE( fromCode->name(), def->name() ); + QCOMPARE( fromCode->description(), QStringLiteral( "optional" ) ); + QCOMPARE( fromCode->flags(), def->flags() ); + QCOMPARE( fromCode->defaultValue(), def->defaultValue() ); + QCOMPARE( fromCode->parentConnectionParameterName(), def->parentConnectionParameterName() ); + QCOMPARE( fromCode->parentSchemaParameterName(), def->parentSchemaParameterName() ); + + // allow new table names + def.reset( new QgsProcessingParameterDatabaseTable( "new", QString(), QStringLiteral( "con" ), QStringLiteral( "schema" ), QVariant(), false, true ) ); + + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterDatabaseTable('new', '', allowNewTableNames=True, connectionParameterName='con', schemaParameterName='schema', defaultValue=None)" ) ); + QVariantMap var = def->toVariantMap(); + def.reset( dynamic_cast( QgsProcessingParameters::parameterFromVariantMap( var ) ) ); + QCOMPARE( def->parentConnectionParameterName(), QStringLiteral( "con" ) ); + QCOMPARE( def->parentSchemaParameterName(), QStringLiteral( "schema" ) ); + QVERIFY( def->allowNewTableNames() ); +} void TestQgsProcessing::parameterDateTime() { @@ -7403,17 +7919,33 @@ void TestQgsProcessing::combineLayerExtent() void TestQgsProcessing::processingFeatureSource() { QString sourceString = QStringLiteral( "test.shp" ); - QgsProcessingFeatureSourceDefinition fs( sourceString, true ); + QgsProcessingFeatureSourceDefinition fs( sourceString, true, 21, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck, QgsFeatureRequest::GeometrySkipInvalid ); QCOMPARE( fs.source.staticValue().toString(), sourceString ); QVERIFY( fs.selectedFeaturesOnly ); + QCOMPARE( fs.featureLimit, 21LL ); + QCOMPARE( fs.flags, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ); + QCOMPARE( fs.geometryCheck, QgsFeatureRequest::GeometrySkipInvalid ); // test storing QgsProcessingFeatureSource in variant and retrieving QVariant fsInVariant = QVariant::fromValue( fs ); QVERIFY( fsInVariant.isValid() ); + // test converting to variant map and back + QVariant res = fs.toVariant(); + QgsProcessingFeatureSourceDefinition dd; + QVERIFY( dd.loadVariant( res.toMap() ) ); + QCOMPARE( dd.source.staticValue().toString(), sourceString ); + QVERIFY( dd.selectedFeaturesOnly ); + QCOMPARE( dd.featureLimit, 21LL ); + QCOMPARE( dd.flags, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ); + QCOMPARE( dd.geometryCheck, QgsFeatureRequest::GeometrySkipInvalid ); + QgsProcessingFeatureSourceDefinition fromVar = qvariant_cast( fsInVariant ); QCOMPARE( fromVar.source.staticValue().toString(), sourceString ); QVERIFY( fromVar.selectedFeaturesOnly ); + QCOMPARE( fromVar.featureLimit, 21LL ); + QCOMPARE( fromVar.flags, QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ); + QCOMPARE( fromVar.geometryCheck, QgsFeatureRequest::GeometrySkipInvalid ); // test evaluating parameter as source QgsVectorLayer *layer = new QgsVectorLayer( "Point", "v1", "memory" ); @@ -7467,8 +7999,15 @@ void TestQgsProcessing::processingFeatureSink() QString sinkString( QStringLiteral( "test.shp" ) ); QgsProject p; QgsProcessingOutputLayerDefinition fs( sinkString, &p ); + QgsRemappingSinkDefinition remap; + QVERIFY( !fs.useRemapping() ); + remap.setDestinationWkbType( QgsWkbTypes::Point ); + fs.setRemappingDefinition( remap ); + QVERIFY( fs.useRemapping() ); + QCOMPARE( fs.sink.staticValue().toString(), sinkString ); QCOMPARE( fs.destinationProject, &p ); + QCOMPARE( fs.remappingDefinition().destinationWkbType(), QgsWkbTypes::Point ); // test storing QgsProcessingFeatureSink in variant and retrieving QVariant fsInVariant = QVariant::fromValue( fs ); @@ -7477,13 +8016,14 @@ void TestQgsProcessing::processingFeatureSink() QgsProcessingOutputLayerDefinition fromVar = qvariant_cast( fsInVariant ); QCOMPARE( fromVar.sink.staticValue().toString(), sinkString ); QCOMPARE( fromVar.destinationProject, &p ); + QCOMPARE( fromVar.remappingDefinition().destinationWkbType(), QgsWkbTypes::Point ); // test evaluating parameter as sink QgsProcessingContext context; context.setProject( &p ); // first using static string definition - std::unique_ptr< QgsProcessingParameterDefinition > def( new QgsProcessingParameterFeatureSink( QStringLiteral( "layer" ) ) ); + std::unique_ptr< QgsProcessingParameterFeatureSink > def( new QgsProcessingParameterFeatureSink( QStringLiteral( "layer" ) ) ); QVariantMap params; params.insert( QStringLiteral( "layer" ), QgsProcessingOutputLayerDefinition( "memory:test", nullptr ) ); QString dest; @@ -7551,6 +8091,22 @@ void TestQgsProcessing::processingFeatureSink() params.insert( QStringLiteral( "layer" ), QVariant() ); sink.reset( QgsProcessingParameters::parameterAsSink( def.get(), params, QgsFields(), QgsWkbTypes::Point, QgsCoordinateReferenceSystem( "EPSG:3113" ), context, dest ) ); QVERIFY( sink.get() ); + + // appendable + def->setSupportsAppend( true ); + QVERIFY( def->supportsAppend() ); + QString pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterFeatureSink('layer', '', optional=True, type=QgsProcessing.TypeMapLayer, createByDefault=True, supportsAppend=True, defaultValue='memory:defaultlayer')" ) ); + + QVariantMap val = def->toVariantMap(); + QgsProcessingParameterFeatureSink fromMap( "x" ); + QVERIFY( fromMap.fromVariantMap( val ) ); + QVERIFY( fromMap.supportsAppend() ); + + def->setSupportsAppend( false ); + QVERIFY( !def->supportsAppend() ); + pythonCode = def->asPythonString(); + QCOMPARE( pythonCode, QStringLiteral( "QgsProcessingParameterFeatureSink('layer', '', optional=True, type=QgsProcessing.TypeMapLayer, createByDefault=True, defaultValue='memory:defaultlayer')" ) ); } void TestQgsProcessing::algorithmScope() @@ -7788,16 +8344,45 @@ void TestQgsProcessing::modelerAlgorithm() QCOMPARE( comment.position(), QPointF( 11, 14 ) ); comment.setDescription( QStringLiteral( "a comment" ) ); QCOMPARE( comment.description(), QStringLiteral( "a comment" ) ); + comment.setColor( QColor( 123, 45, 67 ) ); + QCOMPARE( comment.color(), QColor( 123, 45, 67 ) ); std::unique_ptr< QgsProcessingModelComment > commentClone( comment.clone() ); QCOMPARE( commentClone->toVariant(), comment.toVariant() ); QCOMPARE( commentClone->size(), QSizeF( 9, 8 ) ); QCOMPARE( commentClone->position(), QPointF( 11, 14 ) ); QCOMPARE( commentClone->description(), QStringLiteral( "a comment" ) ); + QCOMPARE( commentClone->color(), QColor( 123, 45, 67 ) ); QgsProcessingModelComment comment2; comment2.loadVariant( comment.toVariant().toMap() ); QCOMPARE( comment2.size(), QSizeF( 9, 8 ) ); QCOMPARE( comment2.position(), QPointF( 11, 14 ) ); QCOMPARE( comment2.description(), QStringLiteral( "a comment" ) ); + QCOMPARE( comment2.color(), QColor( 123, 45, 67 ) ); + + // group boxes + QgsProcessingModelGroupBox groupBox; + groupBox.setSize( QSizeF( 9, 8 ) ); + QCOMPARE( groupBox.size(), QSizeF( 9, 8 ) ); + groupBox.setPosition( QPointF( 11, 14 ) ); + QCOMPARE( groupBox.position(), QPointF( 11, 14 ) ); + groupBox.setDescription( QStringLiteral( "a comment" ) ); + QCOMPARE( groupBox.description(), QStringLiteral( "a comment" ) ); + groupBox.setColor( QColor( 123, 45, 67 ) ); + QCOMPARE( groupBox.color(), QColor( 123, 45, 67 ) ); + std::unique_ptr< QgsProcessingModelGroupBox > groupClone( groupBox.clone() ); + QCOMPARE( groupClone->toVariant(), groupBox.toVariant() ); + QCOMPARE( groupClone->size(), QSizeF( 9, 8 ) ); + QCOMPARE( groupClone->position(), QPointF( 11, 14 ) ); + QCOMPARE( groupClone->description(), QStringLiteral( "a comment" ) ); + QCOMPARE( groupClone->color(), QColor( 123, 45, 67 ) ); + QCOMPARE( groupClone->uuid(), groupBox.uuid() ); + QgsProcessingModelGroupBox groupBox2; + groupBox2.loadVariant( groupBox.toVariant().toMap() ); + QCOMPARE( groupBox2.size(), QSizeF( 9, 8 ) ); + QCOMPARE( groupBox2.position(), QPointF( 11, 14 ) ); + QCOMPARE( groupBox2.description(), QStringLiteral( "a comment" ) ); + QCOMPARE( groupBox2.color(), QColor( 123, 45, 67 ) ); + QCOMPARE( groupBox2.uuid(), groupBox.uuid() ); QMap< QString, QString > friendlyOutputNames; QgsProcessingModelChildAlgorithm child( QStringLiteral( "some_id" ) ); @@ -7971,6 +8556,17 @@ void TestQgsProcessing::modelerAlgorithm() QCOMPARE( alg.shortDescription(), QStringLiteral( "short" ) ); QCOMPARE( alg.helpUrl(), QStringLiteral( "url" ) ); + QVERIFY( alg.groupBoxes().isEmpty() ); + alg.addGroupBox( groupBox ); + QCOMPARE( alg.groupBoxes().size(), 1 ); + QCOMPARE( alg.groupBoxes().at( 0 ).uuid(), groupBox.uuid() ); + QCOMPARE( alg.groupBoxes().at( 0 ).uuid(), groupBox.uuid() ); + alg.removeGroupBox( QStringLiteral( "a" ) ); + QCOMPARE( alg.groupBoxes().size(), 1 ); + alg.removeGroupBox( groupBox.uuid() ); + QVERIFY( alg.groupBoxes().isEmpty() ); + + QVariantMap lastParams; lastParams.insert( QStringLiteral( "a" ), 2 ); lastParams.insert( QStringLiteral( "b" ), 4 ); @@ -7985,6 +8581,21 @@ void TestQgsProcessing::modelerAlgorithm() a2.setDescription( QStringLiteral( "alg2" ) ); a2.setPosition( QPointF( 112, 131 ) ); a2.setSize( QSizeF( 44, 55 ) ); + a2.comment()->setSize( QSizeF( 111, 222 ) ); + a2.comment()->setPosition( QPointF( 113, 114 ) ); + a2.comment()->setDescription( QStringLiteral( "c" ) ); + a2.comment()->setColor( QColor( 255, 254, 253 ) ); + QgsProcessingModelOutput oo; + oo.setPosition( QPointF( 312, 331 ) ); + oo.setSize( QSizeF( 344, 355 ) ); + oo.comment()->setSize( QSizeF( 311, 322 ) ); + oo.comment()->setPosition( QPointF( 313, 314 ) ); + oo.comment()->setDescription( QStringLiteral( "c3" ) ); + oo.comment()->setColor( QColor( 155, 14, 353 ) ); + QMap< QString, QgsProcessingModelOutput > a2Outs; + a2Outs.insert( QStringLiteral( "out1" ), oo ); + a2.setModelOutputs( a2Outs ); + algs.insert( QStringLiteral( "a" ), a1 ); algs.insert( QStringLiteral( "b" ), a2 ); alg.setChildAlgorithms( algs ); @@ -7995,10 +8606,28 @@ void TestQgsProcessing::modelerAlgorithm() QgsProcessingModelChildAlgorithm a2other; a2other.setChildId( QStringLiteral( "b" ) ); a2other.setDescription( QStringLiteral( "alg2 other" ) ); + QgsProcessingModelOutput oo2; + QMap< QString, QgsProcessingModelOutput > a2Outs2; + a2Outs2.insert( QStringLiteral( "out1" ), oo2 ); + a2other.setModelOutputs( a2Outs2 ); + a2other.copyNonDefinitionPropertiesFromModel( &alg ); QCOMPARE( a2other.description(), QStringLiteral( "alg2 other" ) ); QCOMPARE( a2other.position(), QPointF( 112, 131 ) ); QCOMPARE( a2other.size(), QSizeF( 44, 55 ) ); + QCOMPARE( a2other.comment()->size(), QSizeF( 111, 222 ) ); + QCOMPARE( a2other.comment()->position(), QPointF( 113, 114 ) ); + // should not be copied + QCOMPARE( a2other.comment()->description(), QString() ); + QVERIFY( !a2other.comment()->color().isValid() ); + + QCOMPARE( a2other.modelOutput( QStringLiteral( "out1" ) ).position(), QPointF( 312, 331 ) ); + QCOMPARE( a2other.modelOutput( QStringLiteral( "out1" ) ).size(), QSizeF( 344, 355 ) ); + QCOMPARE( a2other.modelOutput( QStringLiteral( "out1" ) ).comment()->size(), QSizeF( 311, 322 ) ); + QCOMPARE( a2other.modelOutput( QStringLiteral( "out1" ) ).comment()->position(), QPointF( 313, 314 ) ); + // should be copied for outputs + QCOMPARE( a2other.modelOutput( QStringLiteral( "out1" ) ).comment()->description(), QStringLiteral( "c3" ) ); + QCOMPARE( a2other.modelOutput( QStringLiteral( "out1" ) ).comment()->color(), QColor( 155, 14, 353 ) ); QgsProcessingModelChildAlgorithm a3; a3.setChildId( QStringLiteral( "c" ) ); @@ -8275,6 +8904,7 @@ void TestQgsProcessing::modelerAlgorithm() QgsProcessingModelAlgorithm alg5( "test", "testGroup" ); alg5.helpContent().insert( "author", "me" ); alg5.helpContent().insert( "usage", "run" ); + alg5.addGroupBox( groupBox ); QVariantMap variables; variables.insert( QStringLiteral( "v1" ), 5 ); variables.insert( QStringLiteral( "v2" ), QStringLiteral( "aabbccd" ) ); @@ -8334,6 +8964,13 @@ void TestQgsProcessing::modelerAlgorithm() QCOMPARE( alg6.helpContent(), alg5.helpContent() ); QCOMPARE( alg6.variables(), variables ); QCOMPARE( alg6.designerParameterValues(), lastParams ); + + QCOMPARE( alg6.groupBoxes().size(), 1 ); + QCOMPARE( alg6.groupBoxes().at( 0 ).size(), QSizeF( 9, 8 ) ); + QCOMPARE( alg6.groupBoxes().at( 0 ).position(), QPointF( 11, 14 ) ); + QCOMPARE( alg6.groupBoxes().at( 0 ).description(), QStringLiteral( "a comment" ) ); + QCOMPARE( alg6.groupBoxes().at( 0 ).color(), QColor( 123, 45, 67 ) ); + QgsProcessingModelChildAlgorithm alg6c1 = alg6.childAlgorithm( "cx1" ); QCOMPARE( alg6c1.childId(), QStringLiteral( "cx1" ) ); QCOMPARE( alg6c1.algorithmId(), QStringLiteral( "buffer" ) ); @@ -8656,6 +9293,23 @@ void TestQgsProcessing::modelExecution() QCOMPARE( outDef.sink.staticValue().toString(), QStringLiteral( "memory:" ) ); QCOMPARE( params.count(), 3 ); // don't want FAIL_OUTPUT set! + // a child with an static output value + QgsProcessingModelChildAlgorithm alg2c4; + alg2c4.setChildId( "cx4" ); + alg2c4.setAlgorithmId( "native:extractbyexpression" ); + alg2c4.addParameterSources( "OUTPUT", QgsProcessingModelChildParameterSources() << QgsProcessingModelChildParameterSource::fromStaticValue( "STATIC" ) ); + model2.addChildAlgorithm( alg2c4 ); + params = model2.parametersForChildAlgorithm( model2.childAlgorithm( "cx4" ), modelInputs, childResults, expContext ); + QCOMPARE( params.value( "OUTPUT" ).toString(), QStringLiteral( "STATIC" ) ); + model2.removeChildAlgorithm( "cx4" ); + // expression based output value + alg2c4.addParameterSources( "OUTPUT", QgsProcessingModelChildParameterSources() << QgsProcessingModelChildParameterSource::fromExpression( "'A' || 'B'" ) ); + model2.addChildAlgorithm( alg2c4 ); + params = model2.parametersForChildAlgorithm( model2.childAlgorithm( "cx4" ), modelInputs, childResults, expContext ); + QCOMPARE( params.value( "OUTPUT" ).toString(), QStringLiteral( "AB" ) ); + model2.removeChildAlgorithm( "cx4" ); + + variables = model2.variablesForChildAlgorithm( "cx3", context ); QCOMPARE( variables.count(), 16 ); QCOMPARE( variables.value( "DIST" ).source.source(), QgsProcessingModelChildParameterSource::ModelParameter ); @@ -8732,7 +9386,7 @@ void TestQgsProcessing::modelExecution() " param = QgsProcessingParameterCrs('CRS', '', defaultValue=QgsCoordinateReferenceSystem('EPSG:28355'))\n" " param.setFlags(param.flags() | QgsProcessingParameterDefinition.FlagAdvanced)\n" " self.addParameter(param)\n" - " self.addParameter(QgsProcessingParameterFeatureSink('MyModelOutput', 'my model output', type=QgsProcessing.TypeVectorPolygon, createByDefault=True, defaultValue=None))\n" + " self.addParameter(QgsProcessingParameterFeatureSink('MyModelOutput', 'my model output', type=QgsProcessing.TypeVectorPolygon, createByDefault=True, supportsAppend=True, defaultValue=None))\n" " self.addParameter(QgsProcessingParameterFeatureSink('cx3:MY_OUT', '', type=QgsProcessing.TypeVectorAnyGeometry, createByDefault=True, defaultValue=None))\n" "\n" " def processAlgorithm(self, parameters, context, model_feedback):\n" @@ -8796,6 +9450,134 @@ void TestQgsProcessing::modelExecution() QCOMPARE( actualParts, expectedParts ); } +void TestQgsProcessing::modelBranchPruning() +{ + QgsVectorLayer *layer3111 = new QgsVectorLayer( "Point?crs=epsg:3111", "v1", "memory" ); + QgsProject p; + p.addMapLayer( layer3111 ); + + QString testDataDir = QStringLiteral( TEST_DATA_DIR ) + '/'; //defined in CmakeLists.txt + QString raster1 = testDataDir + "landsat_4326.tif"; + QFileInfo fi1( raster1 ); + QgsRasterLayer *r1 = new QgsRasterLayer( fi1.filePath(), "R1" ); + QVERIFY( r1->isValid() ); + p.addMapLayer( r1 ); + + QgsProcessingContext context; + context.setProject( &p ); + + // test that model branches are trimmed for algorithms which return the FlagPruneModelBranchesBasedOnAlgorithmResults flag + QgsProcessingModelAlgorithm model1; + + // first add the filter by layer type alg + QgsProcessingModelChildAlgorithm algc1; + algc1.setChildId( "filter" ); + algc1.setAlgorithmId( "native:filterlayersbytype" ); + QgsProcessingModelParameter param; + param.setParameterName( QStringLiteral( "LAYER" ) ); + model1.addModelParameter( new QgsProcessingParameterMapLayer( QStringLiteral( "LAYER" ) ), param ); + algc1.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromModelParameter( QStringLiteral( "LAYER" ) ) ); + model1.addChildAlgorithm( algc1 ); + + //then create some branches which come off this, depending on the layer type + QgsProcessingModelChildAlgorithm algc2; + algc2.setChildId( "buffer" ); + algc2.setAlgorithmId( "native:buffer" ); + algc2.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "filter" ), QStringLiteral( "VECTOR" ) ) ); + QMap outputsc2; + QgsProcessingModelOutput outc2( "BUFFER_OUTPUT" ); + outc2.setChildOutputName( "OUTPUT" ); + outputsc2.insert( QStringLiteral( "BUFFER_OUTPUT" ), outc2 ); + algc2.setModelOutputs( outputsc2 ); + model1.addChildAlgorithm( algc2 ); + // ...we want a complex branch, so add some more bits to the branch + QgsProcessingModelChildAlgorithm algc3; + algc3.setChildId( "buffer2" ); + algc3.setAlgorithmId( "native:buffer" ); + algc3.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "buffer" ), QStringLiteral( "OUTPUT" ) ) ); + QMap outputsc3; + QgsProcessingModelOutput outc3( "BUFFER2_OUTPUT" ); + outc3.setChildOutputName( "OUTPUT" ); + outputsc3.insert( QStringLiteral( "BUFFER2_OUTPUT" ), outc3 ); + algc3.setModelOutputs( outputsc3 ); + model1.addChildAlgorithm( algc3 ); + QgsProcessingModelChildAlgorithm algc4; + algc4.setChildId( "buffer3" ); + algc4.setAlgorithmId( "native:buffer" ); + algc4.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "buffer" ), QStringLiteral( "OUTPUT" ) ) ); + QMap outputsc4; + QgsProcessingModelOutput outc4( "BUFFER3_OUTPUT" ); + outc4.setChildOutputName( "OUTPUT" ); + outputsc4.insert( QStringLiteral( "BUFFER3_OUTPUT" ), outc4 ); + algc4.setModelOutputs( outputsc4 ); + model1.addChildAlgorithm( algc4 ); + + // now add some bits to the raster branch + QgsProcessingModelChildAlgorithm algr2; + algr2.setChildId( "fill2" ); + algr2.setAlgorithmId( "native:fillnodata" ); + algr2.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "filter" ), QStringLiteral( "RASTER" ) ) ); + QMap outputsr2; + QgsProcessingModelOutput outr2( "RASTER_OUTPUT" ); + outr2.setChildOutputName( "OUTPUT" ); + outputsr2.insert( QStringLiteral( "RASTER_OUTPUT" ), outr2 ); + algr2.setModelOutputs( outputsr2 ); + model1.addChildAlgorithm( algr2 ); + + // some more bits on the raster branch + QgsProcessingModelChildAlgorithm algr3; + algr3.setChildId( "fill3" ); + algr3.setAlgorithmId( "native:fillnodata" ); + algr3.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "fill2" ), QStringLiteral( "OUTPUT" ) ) ); + QMap outputsr3; + QgsProcessingModelOutput outr3( "RASTER_OUTPUT2" ); + outr3.setChildOutputName( "OUTPUT" ); + outputsr3.insert( QStringLiteral( "RASTER_OUTPUT2" ), outr3 ); + algr3.setModelOutputs( outputsr3 ); + model1.addChildAlgorithm( algr3 ); + + QgsProcessingModelChildAlgorithm algr4; + algr4.setChildId( "fill4" ); + algr4.setAlgorithmId( "native:fillnodata" ); + algr4.addParameterSources( QStringLiteral( "INPUT" ), QList< QgsProcessingModelChildParameterSource >() << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "fill2" ), QStringLiteral( "OUTPUT" ) ) ); + QMap outputsr4; + QgsProcessingModelOutput outr4( "RASTER_OUTPUT3" ); + outr4.setChildOutputName( "OUTPUT" ); + outputsr4.insert( QStringLiteral( "RASTER_OUTPUT3" ), outr4 ); + algr4.setModelOutputs( outputsr4 ); + model1.addChildAlgorithm( algr4 ); + + QgsProcessingFeedback feedback; + QVariantMap params; + // vector input + params.insert( QStringLiteral( "LAYER" ), QStringLiteral( "v1" ) ); + params.insert( QStringLiteral( "buffer:BUFFER_OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + params.insert( QStringLiteral( "buffer2:BUFFER2_OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + params.insert( QStringLiteral( "buffer3:BUFFER3_OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + params.insert( QStringLiteral( "fill2:RASTER_OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + params.insert( QStringLiteral( "fill3:RASTER_OUTPUT2" ), QgsProcessing::TEMPORARY_OUTPUT ); + params.insert( QStringLiteral( "fill4:RASTER_OUTPUT3" ), QgsProcessing::TEMPORARY_OUTPUT ); + QVariantMap results = model1.run( params, context, &feedback ); + // we should get the vector branch outputs only + QVERIFY( !results.value( QStringLiteral( "buffer:BUFFER_OUTPUT" ) ).toString().isEmpty() ); + QVERIFY( !results.value( QStringLiteral( "buffer2:BUFFER2_OUTPUT" ) ).toString().isEmpty() ); + QVERIFY( !results.value( QStringLiteral( "buffer3:BUFFER3_OUTPUT" ) ).toString().isEmpty() ); + QVERIFY( !results.contains( QStringLiteral( "fill2:RASTER_OUTPUT" ) ) ); + QVERIFY( !results.contains( QStringLiteral( "fill3:RASTER_OUTPUT2" ) ) ); + QVERIFY( !results.contains( QStringLiteral( "fill4:RASTER_OUTPUT3" ) ) ); + + // raster input + params.insert( QStringLiteral( "LAYER" ), QStringLiteral( "R1" ) ); + results = model1.run( params, context, &feedback ); + // we should get the raster branch outputs only + QVERIFY( !results.value( QStringLiteral( "fill2:RASTER_OUTPUT" ) ).toString().isEmpty() ); + QVERIFY( !results.value( QStringLiteral( "fill3:RASTER_OUTPUT2" ) ).toString().isEmpty() ); + QVERIFY( !results.value( QStringLiteral( "fill4:RASTER_OUTPUT3" ) ).toString().isEmpty() ); + QVERIFY( !results.contains( QStringLiteral( "buffer:BUFFER_OUTPUT" ) ) ); + QVERIFY( !results.contains( QStringLiteral( "buffer2:BUFFER2_OUTPUT" ) ) ); + QVERIFY( !results.contains( QStringLiteral( "buffer3:BUFFER3_OUTPUT" ) ) ); +} + void TestQgsProcessing::modelWithProviderWithLimitedTypes() { QgsApplication::processingRegistry()->addProvider( new DummyProvider4() ); @@ -9419,6 +10201,27 @@ void TestQgsProcessing::convertCompatible() QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); QCOMPARE( layerName, QString() ); + // feature limit, will force export + params.insert( QStringLiteral( "source" ), QgsProcessingFeatureSourceDefinition( layer->id(), false, 2 ) ); + out = QgsProcessingParameters::parameterAsCompatibleSourceLayerPath( def.get(), params, context, QStringList() << "shp", QString( "shp" ), &feedback ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".shp" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); + QgsVectorLayer *subset = new QgsVectorLayer( out ); + QVERIFY( subset->isValid() ); + QCOMPARE( subset->featureCount(), 2L ); + delete subset; + + out = QgsProcessingParameters::parameterAsCompatibleSourceLayerPathAndLayerName( def.get(), params, context, QStringList() << "shp", QString( "shp" ), &feedback, &layerName ); + QVERIFY( out != layer->source() ); + QVERIFY( out.endsWith( ".shp" ) ); + QVERIFY( out.startsWith( QgsProcessingUtils::tempFolder() ) ); + QCOMPARE( layerName, QString() ); + subset = new QgsVectorLayer( out ); + QVERIFY( subset->isValid() ); + QCOMPARE( subset->featureCount(), 2L ); + delete subset; + // vector layer as default def.reset( new QgsProcessingParameterFeatureSource( QStringLiteral( "source" ), QString(), QList(), QVariant::fromValue( layer ) ) ); params.remove( QStringLiteral( "source" ) ); @@ -9726,5 +10529,34 @@ void TestQgsProcessing::sourceTypeToString() QCOMPARE( QgsProcessing::sourceTypeToString( sourceType ), expected ); } +void TestQgsProcessing::modelSource() +{ + QgsProcessingModelChildParameterSource source; + source.setExpression( QStringLiteral( "expression" ) ); + source.setExpressionText( QStringLiteral( "expression string" ) ); + source.setOutputName( QStringLiteral( "output name " ) ); + source.setStaticValue( QString( "value" ) ); + source.setOutputChildId( QStringLiteral( "output child id" ) ); + source.setParameterName( QStringLiteral( "parameter name" ) ); + source.setSource( QgsProcessingModelChildParameterSource::ChildOutput ); + + QByteArray ba; + QDataStream ds( &ba, QIODevice::ReadWrite ); + ds << source; + + ds.device()->seek( 0 ); + + QgsProcessingModelChildParameterSource res; + ds >> res; + + QCOMPARE( res.expression(), QStringLiteral( "expression" ) ); + QCOMPARE( res.expressionText(), QStringLiteral( "expression string" ) ); + QCOMPARE( res.outputName(), QStringLiteral( "output name " ) ); + QCOMPARE( res.staticValue().toString(), QString( "value" ) ); + QCOMPARE( res.outputChildId(), QStringLiteral( "output child id" ) ); + QCOMPARE( res.parameterName(), QStringLiteral( "parameter name" ) ); + QCOMPARE( res.source(), QgsProcessingModelChildParameterSource::ChildOutput ); +} + QGSTEST_MAIN( TestQgsProcessing ) #include "testqgsprocessing.moc" diff --git a/tests/src/analysis/testqgsprocessingalgs.cpp b/tests/src/analysis/testqgsprocessingalgs.cpp index 638dc7fb05cb..f8904462b95d 100644 --- a/tests/src/analysis/testqgsprocessingalgs.cpp +++ b/tests/src/analysis/testqgsprocessingalgs.cpp @@ -93,6 +93,8 @@ class TestQgsProcessingAlgs: public QObject void rasterLogicOp_data(); void rasterLogicOp(); + void roundRasterValues_data(); + void roundRasterValues(); void layoutMapExtent(); @@ -112,6 +114,10 @@ class TestQgsProcessingAlgs: public QObject void raiseException(); void raiseWarning(); + void filterByLayerType(); + + void saveLog(); + private: QString mPointLayerPath; @@ -1447,6 +1453,230 @@ void TestQgsProcessingAlgs::rasterLogicOp() } } + +void TestQgsProcessingAlgs::roundRasterValues_data() +{ + QTest::addColumn( "inputRaster" ); + QTest::addColumn( "expectedRaster" ); + QTest::addColumn( "inputBand" ); + QTest::addColumn( "roundingDirection" ); + QTest::addColumn( "decimals" ); + QTest::addColumn( "baseN" ); + + /* + * Testcase 1 + * + * Integer Raster Layer + * band = 1 + * roundingDirection = nearest + * decimals = 2 + */ + QTest::newRow( "testcase 1" ) + << "/raster/dem.tif" + << QStringLiteral( "/roundRasterValues_testcase1.tif" ) //no output expected: can't round integer + << 1 + << 1 + << 2 + << 10; + + /* + * Testcase 2 + * + * WGS84 dem + * band = 1 + * roundingDirection = up + * decimals = 2 + */ + QTest::newRow( "testcase 2" ) + << "/raster/dem.tif" + << QStringLiteral( "/roundRasterValues_testcase2.tif" ) + << 1 + << 0 + << 2 + << 10; + + /* + * Testcase 3 + * + * WGS84 dem + * band = 1 + * roundingDirection = down + * decimals = 1 + */ + QTest::newRow( "testcase 3" ) + << "/raster/dem.tif" + << QStringLiteral( "/roundRasterValues_testcase3.tif" ) + << 1 + << 2 + << 1 + << 10; + + /* + * Testcase 4 + * + * WGS84 dem + * band = 1 + * roundingDirection = nearest + * decimals = -1 + */ + QTest::newRow( "testcase 4" ) + << "/raster/dem.tif" + << QStringLiteral( "/roundRasterValues_testcase4.tif" ) + << 1 + << 1 + << -1 + << 10; + + /* + * Testcase 5 + * + * WGS84 dem + * band = 1 + * roundingDirection = up + * decimals = -1 + */ + QTest::newRow( "testcase 5" ) + << "/raster/dem.tif" + << QStringLiteral( "/roundRasterValues_testcase5.tif" ) + << 1 + << 0 + << -1 + << 10; + + /* + * Testcase 6 + * + * WGS84 dem + * band = 1 + * roundingDirection = down + * decimals = -1 + */ + QTest::newRow( "testcase 6" ) + << "/raster/dem.tif" + << QStringLiteral( "/roundRasterValues_testcase6.tif" ) + << 1 + << 2 + << -1 + << 10; + + /* + * Testcase 7 + * + * WGS84 int + * band = 1 + * roundingDirection = nearest + * decimals = 2 + */ + QTest::newRow( "testcase 7" ) + << "/raster/band1_int16_noct_epsg4326.tif" + << QStringLiteral( "/roundRasterValues_testcase7.tif" ) + << 1 + << 1 + << -1 + << 10; + + /* + * Testcase 8 + * + * WGS84 int + * band = 1 + * roundingDirection = nearest + * decimals = -1 + */ + QTest::newRow( "testcase 8" ) + << "/raster/band1_int16_noct_epsg4326.tif" + << QStringLiteral( "/roundRasterValues_testcase8.tif" ) + << 1 + << 1 + << -1 + << 10; + +} + +void TestQgsProcessingAlgs::roundRasterValues() +{ + QFETCH( QString, inputRaster ); + QFETCH( QString, expectedRaster ); + QFETCH( int, inputBand ); + QFETCH( int, roundingDirection ); + QFETCH( int, decimals ); + QFETCH( int, baseN ); + + //prepare input params + QgsProject p; + std::unique_ptr< QgsProcessingAlgorithm > alg( QgsApplication::processingRegistry()->createAlgorithmById( QStringLiteral( "native:roundrastervalues" ) ) ); + + QString myDataPath( TEST_DATA_DIR ); //defined in CmakeLists.txt + + std::unique_ptr inputRasterLayer = qgis::make_unique< QgsRasterLayer >( myDataPath + inputRaster, "inputDataset", "gdal" ); + + //set project crs and ellipsoid from input layer + p.setCrs( inputRasterLayer->crs(), true ); + + //set project after layer has been added so that transform context/ellipsoid from crs is also set + std::unique_ptr< QgsProcessingContext > context = qgis::make_unique< QgsProcessingContext >(); + context->setProject( &p ); + + QVariantMap parameters; + + parameters.insert( QStringLiteral( "INPUT" ), myDataPath + inputRaster ); + parameters.insert( QStringLiteral( "BAND" ), inputBand ); + parameters.insert( QStringLiteral( "ROUNDING_DIRECTION" ), roundingDirection ); + parameters.insert( QStringLiteral( "DECIMAL_PLACES" ), decimals ); + parameters.insert( QStringLiteral( "BASE_N" ), baseN ); + parameters.insert( QStringLiteral( "OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + + //prepare expectedRaster + std::unique_ptr expectedRasterLayer = qgis::make_unique< QgsRasterLayer >( myDataPath + "/control_images/roundRasterValues/" + expectedRaster, "expectedDataset", "gdal" ); + std::unique_ptr< QgsRasterInterface > expectedInterface( expectedRasterLayer->dataProvider()->clone() ); + QgsRasterIterator expectedIter( expectedInterface.get() ); + expectedIter.startRasterRead( 1, expectedRasterLayer->width(), expectedRasterLayer->height(), expectedInterface->extent() ); + + //run alg... + + bool ok = false; + QgsProcessingFeedback feedback; + QVariantMap results; + + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + + //...and check results with expected datasets + std::unique_ptr outputRaster = qgis::make_unique< QgsRasterLayer >( results.value( QStringLiteral( "OUTPUT" ) ).toString(), "output", "gdal" ); + std::unique_ptr< QgsRasterInterface > outputInterface( outputRaster->dataProvider()->clone() ); + + QCOMPARE( outputRaster->width(), expectedRasterLayer->width() ); + QCOMPARE( outputRaster->height(), expectedRasterLayer->height() ); + + QgsRasterIterator outputIter( outputInterface.get() ); + outputIter.startRasterRead( 1, outputRaster->width(), outputRaster->height(), outputInterface->extent() ); + int outputIterLeft = 0; + int outputIterTop = 0; + int outputIterCols = 0; + int outputIterRows = 0; + int expectedIterLeft = 0; + int expectedIterTop = 0; + int expectedIterCols = 0; + int expectedIterRows = 0; + + std::unique_ptr< QgsRasterBlock > outputRasterBlock; + std::unique_ptr< QgsRasterBlock > expectedRasterBlock; + + while ( outputIter.readNextRasterPart( 1, outputIterCols, outputIterRows, outputRasterBlock, outputIterLeft, outputIterTop ) && + expectedIter.readNextRasterPart( 1, expectedIterCols, expectedIterRows, expectedRasterBlock, expectedIterLeft, expectedIterTop ) ) + { + for ( int row = 0; row < expectedIterRows; row++ ) + { + for ( int column = 0; column < expectedIterCols; column++ ) + { + double expectedValue = expectedRasterBlock->value( row, column ); + double outputValue = outputRasterBlock->value( row, column ); + QCOMPARE( outputValue, expectedValue ); + } + } + } +} + void TestQgsProcessingAlgs::layoutMapExtent() { std::unique_ptr< QgsProcessingAlgorithm > alg( QgsApplication::processingRegistry()->createAlgorithmById( QStringLiteral( "native:printlayoutmapextenttolayer" ) ) ); @@ -2339,5 +2569,73 @@ void TestQgsProcessingAlgs::raiseWarning() QCOMPARE( feedback.errors, QStringList() << QStringLiteral( "you mighta screwed up boy, but i aint so sure" ) ); } +void TestQgsProcessingAlgs::filterByLayerType() +{ + QgsProject p; + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "Point?crs=epsg:4326&field=pk:int&field=col1:string" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ); + QVERIFY( vl->isValid() ); + p.addMapLayer( vl ); + // raster layer + QgsRasterLayer *rl = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/tenbytenraster.asc", QStringLiteral( "rl" ) ); + QVERIFY( rl->isValid() ); + p.addMapLayer( rl ); + + + std::unique_ptr< QgsProcessingAlgorithm > alg( QgsApplication::processingRegistry()->createAlgorithmById( QStringLiteral( "native:filterlayersbytype" ) ) ); + QVERIFY( alg != nullptr ); + + QVariantMap parameters; + // vector input + parameters.insert( QStringLiteral( "INPUT" ), QStringLiteral( "vl" ) ); + + bool ok = false; + std::unique_ptr< QgsProcessingContext > context = qgis::make_unique< QgsProcessingContext >(); + context->setProject( &p ); + QgsProcessingFeedback feedback; + QVariantMap results; + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + QVERIFY( !results.value( QStringLiteral( "VECTOR" ) ).toString().isEmpty() ); + QVERIFY( !results.contains( QStringLiteral( "RASTER" ) ) ); + + // raster input + parameters.insert( QStringLiteral( "INPUT" ), QStringLiteral( "rl" ) ); + ok = false; + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + QVERIFY( !results.value( QStringLiteral( "RASTER" ) ).toString().isEmpty() ); + QVERIFY( !results.contains( QStringLiteral( "VECTOR" ) ) ); +} + +void TestQgsProcessingAlgs::saveLog() +{ + std::unique_ptr< QgsProcessingAlgorithm > alg( QgsApplication::processingRegistry()->createAlgorithmById( QStringLiteral( "native:savelog" ) ) ); + QVERIFY( alg != nullptr ); + + QVariantMap parameters; + parameters.insert( QStringLiteral( "OUTPUT" ), QgsProcessing::TEMPORARY_OUTPUT ); + + bool ok = false; + std::unique_ptr< QgsProcessingContext > context = qgis::make_unique< QgsProcessingContext >(); + QgsProcessingFeedback feedback; + feedback.reportError( QStringLiteral( "test" ) ); + QVariantMap results; + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + + QVERIFY( !results.value( QStringLiteral( "OUTPUT" ) ).toString().isEmpty() ); + QFile file( results.value( QStringLiteral( "OUTPUT" ) ).toString() ); + QVERIFY( file.open( QFile::ReadOnly | QIODevice::Text ) ); + QCOMPARE( QString( file.readAll() ), QStringLiteral( "test\n" ) ); + + parameters.insert( QStringLiteral( "USE_HTML" ), true ); + results = alg->run( parameters, *context, &feedback, &ok ); + QVERIFY( ok ); + QVERIFY( !results.value( QStringLiteral( "OUTPUT" ) ).toString().isEmpty() ); + QFile file2( results.value( QStringLiteral( "OUTPUT" ) ).toString() ); + QVERIFY( file2.open( QFile::ReadOnly | QIODevice::Text ) ); + QCOMPARE( QString( file2.readAll() ), QStringLiteral( "test
    " ) ); +} + QGSTEST_MAIN( TestQgsProcessingAlgs ) #include "testqgsprocessingalgs.moc" diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index 3e11e387dad8..c869f34856e3 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -129,6 +129,7 @@ ADD_QGIS_TEST(maptoolmovefeaturetest testqgsmaptoolmovefeature.cpp) ADD_QGIS_TEST(maptoolellipsetest testqgsmaptoolellipse.cpp) ADD_QGIS_TEST(maptoolrectangletest testqgsmaptoolrectangle.cpp) ADD_QGIS_TEST(maptoolregularpolygontest testqgsmaptoolregularpolygon.cpp) +ADD_QGIS_TEST(maptoolsplitpartstest testqgsmaptoolsplitparts.cpp) ADD_QGIS_TEST(measuretool testqgsmeasuretool.cpp) ADD_QGIS_TEST(vertextool testqgsvertextool.cpp) ADD_QGIS_TEST(vectorlayersaveasdialogtest testqgsvectorlayersaveasdialog.cpp) diff --git a/tests/src/app/testqgsapplocatorfilters.cpp b/tests/src/app/testqgsapplocatorfilters.cpp index efd78e506b46..a66a2c48a5ba 100644 --- a/tests/src/app/testqgsapplocatorfilters.cpp +++ b/tests/src/app/testqgsapplocatorfilters.cpp @@ -38,6 +38,7 @@ class TestQgsAppLocatorFilters : public QObject void testLayouts(); void testSearchActiveLayer(); void testSearchAllLayers(); + void testSearchAllLayersPrioritizeExactMatch(); private: QgisApp *mQgisApp = nullptr; @@ -221,6 +222,39 @@ void TestQgsAppLocatorFilters::testSearchAllLayers() QgsProject::instance()->removeAllMapLayers(); } +void TestQgsAppLocatorFilters::testSearchAllLayersPrioritizeExactMatch() +{ + QString layerDef = QStringLiteral( "Point?crs=epsg:4326&field=pk:integer&field=my_text:string&field=my_number:integer&key=pk" ); + QgsVectorLayer *l1 = new QgsVectorLayer( layerDef, QStringLiteral( "Layer 1" ), QStringLiteral( "memory" ) ); + + QgsProject::instance()->addMapLayers( QList< QgsMapLayer *>() << l1 ); + + QgsFeature f1; + f1.setAttributes( QVector() << 100 << "A nice feature" << 100 ); + f1.setGeometry( QgsGeometry::fromWkt( "Point (-71.123 78.23)" ) ); + QgsFeature f2; + f2.setAttributes( QVector() << 101 << "Something crazy" << 3 ); + f2.setGeometry( QgsGeometry::fromWkt( "Point (-72.123 78.23)" ) ); + QgsFeature f3; + f3.setAttributes( QVector() << 102 << "Another feature" << 1 ); + f3.setGeometry( QgsGeometry::fromWkt( "Point (-73.123 78.23)" ) ); + + l1->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ); + + QgsAllLayersFeaturesLocatorFilter filter; + QgsLocatorContext context; + context.usingPrefix = true; // Searching for short strings is only available with prefix + + l1->setDisplayExpression( QStringLiteral( "\"my_number\"" ) ); + + QList< QgsLocatorResult > results = gatherResults( &filter, QStringLiteral( "1" ), context ); + QCOMPARE( results.count(), 2 ); + QCOMPARE( results.first().displayString, QStringLiteral( "1" ) ); + QCOMPARE( results.last().displayString, QStringLiteral( "100" ) ); + + QgsProject::instance()->removeAllMapLayers(); +} + QList TestQgsAppLocatorFilters::gatherResults( QgsLocatorFilter *filter, const QString &string, const QgsLocatorContext &context ) { QSignalSpy spy( filter, &QgsLocatorFilter::resultFetched ); diff --git a/tests/src/app/testqgsattributetable.cpp b/tests/src/app/testqgsattributetable.cpp index e86423db3628..d05f93e45674 100644 --- a/tests/src/app/testqgsattributetable.cpp +++ b/tests/src/app/testqgsattributetable.cpp @@ -53,6 +53,7 @@ class TestQgsAttributeTable : public QObject void testSelectedOnTop(); void testSortByDisplayExpression(); void testOrderColumn(); + void testFilteredFeatures(); private: QgisApp *mQgisApp = nullptr; @@ -405,5 +406,85 @@ void TestQgsAttributeTable::testOrderColumn() QCOMPARE( filterModel->sortColumn(), 2 ); } +void TestQgsAttributeTable::testFilteredFeatures() +{ + std::unique_ptr< QgsVectorLayer> tempLayer( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3111&field=pk:int&field=col1:int&field=col2:int" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) ); + QVERIFY( tempLayer->isValid() ); + + QgsFeature f1( tempLayer->dataProvider()->fields(), 1 ); + f1.setAttribute( 0, 1 ); + f1.setAttribute( 1, 2 ); + QgsFeature f2( tempLayer->dataProvider()->fields(), 2 ); + f2.setAttribute( 0, 2 ); + f2.setAttribute( 1, 4 ); + QgsFeature f3( tempLayer->dataProvider()->fields(), 3 ); + f3.setAttribute( 0, 3 ); + f3.setAttribute( 1, 6 ); + QgsFeature f4( tempLayer->dataProvider()->fields(), 4 ); + f4.setAttribute( 0, 4 ); + f4.setAttribute( 1, 8 ); + + QVERIFY( tempLayer->dataProvider()->addFeatures( QgsFeatureList() << f1 << f2 << f3 ) ); + + std::unique_ptr< QgsAttributeTableDialog > dlg( new QgsAttributeTableDialog( tempLayer.get(), QgsAttributeTableFilterModel::ShowAll ) ); + + // show all (three features) + dlg->mFeatureFilterWidget->filterShowAll(); + QCOMPARE( dlg->mMainView->featureCount(), 3 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 ); + + // add a feature + tempLayer->startEditing(); + QVERIFY( tempLayer->addFeatures( QgsFeatureList() << f4 ) ); + //still show all (four features) + QCOMPARE( tempLayer->featureCount(), 4L ); + QCOMPARE( dlg->mMainView->featureCount(), 4 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 4 ); + + // bigger 5 (two of four features) + dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>5" ), QgsAttributeForm::ReplaceFilter, true ); + QCOMPARE( dlg->mMainView->featureCount(), 4 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 ); + // bigger 7 (one of four features) + dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>7" ), QgsAttributeForm::ReplaceFilter, true ); + QCOMPARE( dlg->mMainView->featureCount(), 4 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 1 ); + // bigger 9 (no of four features) + dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1>9" ), QgsAttributeForm::ReplaceFilter, true ); + QCOMPARE( dlg->mMainView->featureCount(), 4 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 0 ); + + //add two features + QgsFeature f5( tempLayer->dataProvider()->fields(), 5 ); + f5.setAttribute( 0, 5 ); + f5.setAttribute( 1, 10 ); + QgsFeature f6( tempLayer->dataProvider()->fields(), 6 ); + f6.setAttribute( 0, 6 ); + f6.setAttribute( 1, 12 ); + QVERIFY( tempLayer->addFeatures( QgsFeatureList() << f5 << f6 ) ); + tempLayer->commitChanges(); + + //no filter change -> now two of six features + QCOMPARE( dlg->mMainView->featureCount(), 6 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 ); + + //remove a feature not affecting the filter + tempLayer->startEditing(); + QVERIFY( tempLayer->deleteFeature( f2.id() ) ); + //no filter change -> now two of five features + QCOMPARE( dlg->mMainView->featureCount(), 5 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 2 ); + + //remove a feature affecting the filter + QVERIFY( tempLayer->deleteFeature( f5.id() ) ); + //no filter change -> now one of four features + QCOMPARE( dlg->mMainView->featureCount(), 4 ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 1 ); + + // smaller 11 (three of four features) + dlg->mFeatureFilterWidget->setFilterExpression( QStringLiteral( "col1<11" ), QgsAttributeForm::ReplaceFilter, true ); + QCOMPARE( dlg->mMainView->filteredFeatureCount(), 3 ); +} + QGSTEST_MAIN( TestQgsAttributeTable ) #include "testqgsattributetable.moc" diff --git a/tests/src/app/testqgsmaptoolidentifyaction.cpp b/tests/src/app/testqgsmaptoolidentifyaction.cpp index fc4dfd198407..79e56f1da6ed 100644 --- a/tests/src/app/testqgsmaptoolidentifyaction.cpp +++ b/tests/src/app/testqgsmaptoolidentifyaction.cpp @@ -620,9 +620,8 @@ void TestQgsMapToolIdentifyAction::identifyMesh() tempLayer->createMapRenderer( context ); // only scalar dataset - QgsMeshRendererSettings settings = tempLayer->rendererSettings(); - settings.setActiveScalarDataset( QgsMeshDatasetIndex( 0, 0 ) ); - tempLayer->setRendererSettings( settings ); + tempLayer->temporalProperties()->setIsActive( false ); + tempLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex( 0, 0 ) ); QList results; results = testIdentifyMesh( tempLayer, 500, 500 ); @@ -633,9 +632,8 @@ void TestQgsMapToolIdentifyAction::identifyMesh() QCOMPARE( results[0].mAttributes[ QStringLiteral( "Scalar Value" )], QStringLiteral( "42" ) ); // scalar + vector same - settings.setActiveScalarDataset( QgsMeshDatasetIndex( 1, 0 ) ); - settings.setActiveVectorDataset( QgsMeshDatasetIndex( 1, 0 ) ); - tempLayer->setRendererSettings( settings ); + tempLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex( 1, 0 ) ); + tempLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex( 1, 0 ) ); results = testIdentifyMesh( tempLayer, 500, 500 ); QCOMPARE( results.size(), 1 ); QCOMPARE( results[0].mAttributes[ QStringLiteral( "Vector Value" )], QStringLiteral( "no data" ) ); @@ -646,9 +644,8 @@ void TestQgsMapToolIdentifyAction::identifyMesh() QCOMPARE( results[0].mAttributes[ QStringLiteral( "Vector y-component" )], QStringLiteral( "2.4" ) ); // scalar + vector different - settings.setActiveScalarDataset( QgsMeshDatasetIndex( 0, 0 ) ); - settings.setActiveVectorDataset( QgsMeshDatasetIndex( 1, 0 ) ); - tempLayer->setRendererSettings( settings ); + tempLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex( 0, 0 ) ); + tempLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex( 1, 0 ) ); results = testIdentifyMesh( tempLayer, 2400, 2400 ); QCOMPARE( results.size(), 2 ); QCOMPARE( results[0].mAttributes[ QStringLiteral( "Scalar Value" )], QStringLiteral( "42" ) ); @@ -657,9 +654,8 @@ void TestQgsMapToolIdentifyAction::identifyMesh() QCOMPARE( results[1].mAttributes[ QStringLiteral( "Vector y-component" )], QStringLiteral( "2.4" ) ); // only vector - settings.setActiveScalarDataset( QgsMeshDatasetIndex() ); - settings.setActiveVectorDataset( QgsMeshDatasetIndex( 1, 0 ) ); - tempLayer->setRendererSettings( settings ); + tempLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex() ); + tempLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex( 1, 0 ) ); results = testIdentifyMesh( tempLayer, 2400, 2400 ); QCOMPARE( results.size(), 1 ); QCOMPARE( results[0].mAttributes[ QStringLiteral( "Vector Magnitude" )], QStringLiteral( "3" ) ); diff --git a/tests/src/app/testqgsmaptoolsplitparts.cpp b/tests/src/app/testqgsmaptoolsplitparts.cpp new file mode 100644 index 000000000000..3939c9ed9abd --- /dev/null +++ b/tests/src/app/testqgsmaptoolsplitparts.cpp @@ -0,0 +1,139 @@ +/*************************************************************************** + testqgsmaptoolsplitparts.cpp + ------------------------ + Date : February 2020 + Copyright : (C) 2020 by Loïc Bartoletti + Email : loic.bartoletti@oslandia.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 "qgstest.h" + +#include "qgisapp.h" +#include "qgsgeometry.h" +#include "qgsmapcanvas.h" +#include "qgssettings.h" +#include "qgsvectorlayer.h" +#include "qgsmaptoolsplitparts.h" +#include "qgsgeometryutils.h" + +#include "testqgsmaptoolutils.h" + +class TestQgsMapToolSplitParts : public QObject +{ + Q_OBJECT + + public: + TestQgsMapToolSplitParts(); + + private slots: + void initTestCase(); + void cleanupTestCase(); + + void testSplitMultiLineString(); + + private: + QPoint mapToPoint( double x, double y ); + QgisApp *mQgisApp = nullptr; + QgsMapCanvas *mCanvas = nullptr; + QgsVectorLayer *mMultiLineStringLayer = nullptr; + QgsFeature lineF1, lineF2; +}; + +TestQgsMapToolSplitParts::TestQgsMapToolSplitParts() = default; + + +//runs before all tests +void TestQgsMapToolSplitParts::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + + mQgisApp = new QgisApp(); + + mCanvas = new QgsMapCanvas(); + mCanvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3946" ) ) ); + + // make testing layers + mMultiLineStringLayer = new QgsVectorLayer( QStringLiteral( "MultiLineString?crs=EPSG:3946" ), QStringLiteral( "layer multiline" ), QStringLiteral( "memory" ) ); + QVERIFY( mMultiLineStringLayer->isValid() ); + mMultiLineStringLayer->startEditing(); + lineF1.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "MultiLineString ((0 0, 10 0))" ) ) ); + lineF2.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "MultiLineString ((0 5, 10 5),(10 5, 15 5))" ) ) ); + mMultiLineStringLayer->addFeature( lineF1 ); + mMultiLineStringLayer->addFeature( lineF2 ); + + mCanvas->setFrameStyle( QFrame::NoFrame ); + mCanvas->resize( 50, 50 ); + mCanvas->setExtent( QgsRectangle( 0, 0, 10, 10 ) ); + mCanvas->show(); // to make the canvas resize + mCanvas->hide(); + // Disable flaky tests on windows... + // QCOMPARE( mCanvas->mapSettings().outputSize(), QSize( 50, 50 ) ); + // QCOMPARE( mCanvas->mapSettings().visibleExtent(), QgsRectangle( 0, 0, 10, 10 ) ); + + QgsProject::instance()->addMapLayers( QList() << mMultiLineStringLayer ); + + // set layers in canvas + mCanvas->setLayers( QList() << mMultiLineStringLayer ); +} + +void TestQgsMapToolSplitParts::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +QPoint TestQgsMapToolSplitParts::mapToPoint( double x, double y ) +{ + + QgsPointXY mapPoint = mCanvas->mapSettings().mapToPixel().transform( x, y ); + + return QPoint( std::round( mapPoint.x() ), std::round( mapPoint.y() ) ); +} + +void TestQgsMapToolSplitParts::testSplitMultiLineString() +{ + + mCanvas->setCurrentLayer( mMultiLineStringLayer ); + QgsMapToolSplitParts *mapTool = new QgsMapToolSplitParts( mCanvas ) ; + mCanvas->setMapTool( mapTool ); + + std::unique_ptr< QgsMapMouseEvent > event( new QgsMapMouseEvent( + mCanvas, + QEvent::MouseButtonRelease, + mapToPoint( 4, 7 ), + Qt::LeftButton + ) ); + mapTool->cadCanvasReleaseEvent( event.get() ); + event.reset( new QgsMapMouseEvent( + mCanvas, + QEvent::MouseButtonRelease, + mapToPoint( 4, -1 ), + Qt::LeftButton + ) ); + mapTool->cadCanvasReleaseEvent( event.get() ); + + event.reset( new QgsMapMouseEvent( + mCanvas, + QEvent::MouseButtonRelease, + mapToPoint( 4, -1 ), + Qt::RightButton + ) ); + mapTool->cadCanvasReleaseEvent( event.get() ); + + + QCOMPARE( mMultiLineStringLayer->featureCount(), ( long )2 ); + QCOMPARE( mMultiLineStringLayer->getFeature( lineF1.id() ).geometry().asWkt(), QStringLiteral( "MultiLineString ((0 0, 4 0),(4 0, 10 0))" ) ); + QCOMPARE( mMultiLineStringLayer->getFeature( lineF2.id() ).geometry().asWkt(), QStringLiteral( "MultiLineString ((0 5, 4 5),(4 5, 10 5),(10 5, 15 5))" ) ); + + mMultiLineStringLayer->rollBack(); +} + +QGSTEST_MAIN( TestQgsMapToolSplitParts ) +#include "testqgsmaptoolsplitparts.moc" diff --git a/tests/src/auth/testqgsauthoauth2method.cpp b/tests/src/auth/testqgsauthoauth2method.cpp index 6144f12a4fb9..62bbfc6ab0b5 100644 --- a/tests/src/auth/testqgsauthoauth2method.cpp +++ b/tests/src/auth/testqgsauthoauth2method.cpp @@ -465,8 +465,8 @@ void TestQgsAuthOAuth2Method::testDynamicRegistration() { qApp->processEvents(); } - QCOMPARE( dlg.leClientId->text(), QLatin1Literal( "___QGIS_ROCKS___@www.qgis.org" ) ); - QCOMPARE( dlg.leClientSecret->text(), QLatin1Literal( "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___" ) ); + QCOMPARE( dlg.leClientId->text(), QLatin1String( "___QGIS_ROCKS___@www.qgis.org" ) ); + QCOMPARE( dlg.leClientSecret->text(), QLatin1String( "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___" ) ); } @@ -499,8 +499,8 @@ void TestQgsAuthOAuth2Method::testDynamicRegistrationJwt() { qApp->processEvents(); } - QCOMPARE( dlg.leClientId->text(), QLatin1Literal( "___QGIS_ROCKS___@www.qgis.org" ) ); - QCOMPARE( dlg.leClientSecret->text(), QLatin1Literal( "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___" ) ); + QCOMPARE( dlg.leClientId->text(), QLatin1String( "___QGIS_ROCKS___@www.qgis.org" ) ); + QCOMPARE( dlg.leClientSecret->text(), QLatin1String( "___QGIS_ROCKS______QGIS_ROCKS______QGIS_ROCKS___" ) ); } diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index be74141a7b23..69794ecdde00 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -31,6 +31,7 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/classification ${CMAKE_SOURCE_DIR}/src/core/mesh + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/test ${CMAKE_BINARY_DIR}/src/core @@ -204,6 +205,7 @@ SET(TESTS testqgsrastermarker.cpp testqgsrasteriterator.cpp testqgsrasterblock.cpp + testqgsrastercontourrenderer.cpp testqgsrasterdataprovidertemporalcapabilities.cpp testqgsrasterlayer.cpp testqgsrasterlayertemporalproperties.cpp @@ -227,6 +229,7 @@ SET(TESTS testqgstaskmanager.cpp testqgstemporalproperty.cpp testqgstemporalrangeobject.cpp + testqgstemporalnavigationobject.cpp testqgstracer.cpp testqgstriangularmesh.cpp testqgsfontutils.cpp @@ -237,6 +240,7 @@ SET(TESTS testqgsvectorlayerjoinbuffer.cpp testqgsvectorlayer.cpp testqgsvectorlayerutils.cpp + testqgsvectortilelayer.cpp testqgsziputils.cpp testziplayer.cpp testqgslayerdefinition.cpp diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 6166add9a1fd..fd394bd8f315 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -94,6 +94,8 @@ class TestQgsExpression: public QObject mPointsLayer->setAttributionUrl( QStringLiteral( "attribution url" ) ); mPointsLayer->setMinimumScale( 500 ); mPointsLayer->setMaximumScale( 1000 ); + mPointsLayer->setMapTipTemplate( QStringLiteral( "Maptip with class = [% \"Class\" %]" ) ); + mPointsLayer->setDisplayExpression( QStringLiteral( "'Display expression with class = ' || \"Class\"" ) ); mPointsLayerMetadata = new QgsVectorLayer( pointFileInfo.filePath(), pointFileInfo.completeBaseName() + "_metadata", QStringLiteral( "ogr" ) ); @@ -1411,6 +1413,11 @@ class TestQgsExpression: public QObject QTest::newRow( "right associativity" ) << "(2^3)^2" << false << QVariant( 64. ); QTest::newRow( "left associativity" ) << "1-(2-1)" << false << QVariant( 0 ); + // eval_template tests + QTest::newRow( "eval_template" ) << QStringLiteral( "eval_template(\'this is a [% \\'template\\' || \\'!\\' %]\')" ) << false << QVariant( "this is a template!" ); + QTest::newRow( "eval_template string" ) << QStringLiteral( "eval_template('string')" ) << false << QVariant( "string" ); + QTest::newRow( "eval_template expression" ) << QStringLiteral( "eval_template('a' || ' string')" ) << false << QVariant( "a string" ); + // layer_property tests QTest::newRow( "layer_property no layer" ) << "layer_property('','title')" << false << QVariant(); QTest::newRow( "layer_property bad layer" ) << "layer_property('bad','title')" << false << QVariant(); @@ -1428,10 +1435,12 @@ class TestQgsExpression: public QObject QTest::newRow( "layer_property source" ) << QStringLiteral( "layer_property('%1','source')" ).arg( mPointsLayer->name() ) << false << QVariant( mPointsLayer->publicSource() ); QTest::newRow( "layer_property min_scale" ) << QStringLiteral( "layer_property('%1','min_scale')" ).arg( mPointsLayer->name() ) << false << QVariant( mPointsLayer->minimumScale() ); QTest::newRow( "layer_property max_scale" ) << QStringLiteral( "layer_property('%1','max_scale')" ).arg( mPointsLayer->name() ) << false << QVariant( mPointsLayer->maximumScale() ); + QTest::newRow( "layer_property is_editable" ) << QStringLiteral( "layer_property('%1','is_editable')" ).arg( mPointsLayer->name() ) << false << QVariant( mPointsLayer->isEditable() ); QTest::newRow( "layer_property crs" ) << QStringLiteral( "layer_property('%1','crs')" ).arg( mPointsLayer->name() ) << false << QVariant( "EPSG:4326" ); QTest::newRow( "layer_property crs_description" ) << QStringLiteral( "layer_property('%1','crs_description')" ).arg( mPointsLayer->name() ) << false << QVariant( "WGS 84" ); QTest::newRow( "layer_property crs_definition" ) << QStringLiteral( "layer_property('%1','crs_definition')" ).arg( mPointsLayer->name() ) << false << QVariant( "+proj=longlat +datum=WGS84 +no_defs" ); QTest::newRow( "layer_property extent" ) << QStringLiteral( "geom_to_wkt(layer_property('%1','extent'))" ).arg( mPointsLayer->name() ) << false << QVariant( "Polygon ((-118.88888889 22.80020704, -83.33333333 22.80020704, -83.33333333 46.87198068, -118.88888889 46.87198068, -118.88888889 22.80020704))" ); + QTest::newRow( "layer_property distance_units" ) << QStringLiteral( "layer_property('%1','distance_units')" ).arg( mPointsLayer->name() ) << false << QVariant( "degrees" ); QTest::newRow( "layer_property type" ) << QStringLiteral( "layer_property('%1','type')" ).arg( mPointsLayer->name() ) << false << QVariant( "Vector" ); QTest::newRow( "layer_property storage_type" ) << QStringLiteral( "layer_property('%1','storage_type')" ).arg( mPointsLayer->name() ) << false << QVariant( "ESRI Shapefile" ); QTest::newRow( "layer_property geometry_type" ) << QStringLiteral( "layer_property('%1','geometry_type')" ).arg( mPointsLayer->name() ) << false << QVariant( "Point" ); @@ -1978,6 +1987,8 @@ class TestQgsExpression: public QObject QTest::newRow( "range" ) << "range(\"col1\")" << false << QVariant( 18.0 ); QTest::newRow( "minority" ) << "minority(\"col3\")" << false << QVariant( 1 ); QTest::newRow( "majority" ) << "majority(\"col3\")" << false << QVariant( 2 ); + QTest::newRow( "minority string" ) << "minority(\"col2\")" << false << QVariant( "test" ); + QTest::newRow( "majority string" ) << "majority(\"col2\")" << false << QVariant( "" ); QTest::newRow( "q1" ) << "q1(\"col1\")" << false << QVariant( 2.5 ); QTest::newRow( "q3" ) << "q3(\"col1\")" << false << QVariant( 6.5 ); QTest::newRow( "iqr" ) << "iqr(\"col1\")" << false << QVariant( 4 ); @@ -2015,6 +2026,63 @@ class TestQgsExpression: public QObject QTest::newRow( "group by with null value" ) << "sum(\"col1\", \"col4\")" << false << QVariant( 8 ); } + void maptip_display_data() + { + QTest::addColumn( "string" ); + QTest::addColumn( "feature" ); + QTest::addColumn( "layer" ); + QTest::addColumn( "evalError" ); + QTest::addColumn( "result" ); + + QgsFeature firstFeature = mPointsLayer->getFeature( 1 ); + QgsVectorLayer *noLayer = nullptr; + + QTest::newRow( "display not evaluated" ) << QStringLiteral( "display_expression(@layer_id, $currentfeature, False)" ) << firstFeature << mPointsLayer << false << QVariant( "'Display expression with class = ' || \"Class\"" ); + QTest::newRow( "display wrong layer" ) << QStringLiteral( "display_expression()" ) << firstFeature << noLayer << true << QVariant(); + QTest::newRow( "display wrong feature" ) << QStringLiteral( "display_expression()" ) << QgsFeature() << mPointsLayer << true << QVariant(); + + QTest::newRow( "maptip wrong feature" ) << QStringLiteral( "maptip()" ) << QgsFeature() << mPointsLayer << true << QVariant(); + QTest::newRow( "maptip wrong layer" ) << QStringLiteral( "maptip()" ) << firstFeature << noLayer << true << QVariant(); + QTest::newRow( "maptip not evaluated" ) << QStringLiteral( "maptip(@layer_id, $currentfeature, False)" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = [% \"Class\" %]" ); + + QTest::newRow( "maptip with 2 params" ) << QStringLiteral( "maptip(@layer_id, $currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = Biplane" ); + QTest::newRow( "maptip with 1 param" ) << QStringLiteral( "maptip($currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = Biplane" ); + QTest::newRow( "maptip with 0 param" ) << QStringLiteral( "maptip()" ) << firstFeature << mPointsLayer << false << QVariant( "Maptip with class = Biplane" ); + + QTest::newRow( "display with 2 params" ) << QStringLiteral( "display_expression(@layer_id, $currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Display expression with class = Biplane" ); + QTest::newRow( "display with 1 param" ) << QStringLiteral( "display_expression($currentfeature)" ) << firstFeature << mPointsLayer << false << QVariant( "Display expression with class = Biplane" ); + QTest::newRow( "display with 0 param" ) << QStringLiteral( "display_expression()" ) << firstFeature << mPointsLayer << false << QVariant( "Display expression with class = Biplane" ); + } + + void maptip_display() + { + QFETCH( QString, string ); + QFETCH( QgsFeature, feature ); + QFETCH( QgsVectorLayer *, layer ); + QFETCH( bool, evalError ); + QFETCH( QVariant, result ); + + QgsExpressionContext context; + context.appendScope( QgsExpressionContextUtils::globalScope() ); + context.appendScope( QgsExpressionContextUtils::projectScope( QgsProject::instance() ) ); + if ( layer ) + { + //layer->setDisplayExpression( QStringLiteral( "Display expression with class = ' || Class" ) ); + context.appendScope( QgsExpressionContextUtils::layerScope( layer ) ); + } + context.setFeature( feature ); + + QgsExpression exp( string ); + exp.prepare( &context ); + + if ( exp.hasParserError() ) + qDebug() << exp.parserErrorString(); + QCOMPARE( exp.hasParserError(), false ); + QVariant res = exp.evaluate( &context ); + QCOMPARE( exp.hasEvalError(), evalError ); + QCOMPARE( res.toString(), result.toString() ); + } + void selection() { QFETCH( QgsFeatureIds, selectedFeatures ); diff --git a/tests/src/core/testqgsexpressioncontext.cpp b/tests/src/core/testqgsexpressioncontext.cpp index 411ccfcd6056..e08fe92645ef 100644 --- a/tests/src/core/testqgsexpressioncontext.cpp +++ b/tests/src/core/testqgsexpressioncontext.cpp @@ -625,7 +625,7 @@ void TestQgsExpressionContext::globalScope() void TestQgsExpressionContext::projectScope() { - QgsProject *project = QgsProject::instance(); + QgsProject project; QgsProjectMetadata md; md.setTitle( QStringLiteral( "project title" ) ); md.setAuthor( QStringLiteral( "project author" ) ); @@ -636,13 +636,13 @@ void TestQgsExpressionContext::projectScope() keywords.insert( QStringLiteral( "voc1" ), QStringList() << "a" << "b" ); keywords.insert( QStringLiteral( "voc2" ), QStringList() << "c" << "d" ); md.setKeywords( keywords ); - project->setMetadata( md ); + project.setMetadata( md ); - QgsExpressionContextUtils::setProjectVariable( project, QStringLiteral( "test" ), "testval" ); - QgsExpressionContextUtils::setProjectVariable( project, QStringLiteral( "testdouble" ), 5.2 ); + QgsExpressionContextUtils::setProjectVariable( &project, QStringLiteral( "test" ), "testval" ); + QgsExpressionContextUtils::setProjectVariable( &project, QStringLiteral( "testdouble" ), 5.2 ); QgsExpressionContext context; - QgsExpressionContextScope *scope = QgsExpressionContextUtils::projectScope( project ); + QgsExpressionContextScope *scope = QgsExpressionContextUtils::projectScope( &project ); context << scope; QCOMPARE( scope->name(), tr( "Project" ) ); @@ -664,18 +664,70 @@ void TestQgsExpressionContext::projectScope() QgsExpression expProject( QStringLiteral( "var('test')" ) ); QCOMPARE( expProject.evaluate( &context ).toString(), QString( "testval" ) ); + // layers + QVERIFY( context.variable( "layers" ).isValid() ); + QVERIFY( context.variable( "layer_ids" ).isValid() ); + QVERIFY( context.variable( "layers" ).toList().isEmpty() ); + QVERIFY( context.variable( "layer_ids" ).toList().isEmpty() ); + + // add layer + QgsVectorLayer *vectorLayer = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer&field=col2:integer&field=col3:integer" ), QStringLiteral( "test layer" ), QStringLiteral( "memory" ) ); + QgsVectorLayer *vectorLayer2 = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer&field=col2:integer&field=col3:integer" ), QStringLiteral( "test layer" ), QStringLiteral( "memory" ) ); + QgsVectorLayer *vectorLayer3 = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer&field=col2:integer&field=col3:integer" ), QStringLiteral( "test layer" ), QStringLiteral( "memory" ) ); + project.addMapLayer( vectorLayer ); + QgsExpressionContextScope *projectScope = QgsExpressionContextUtils::projectScope( &project ); + QCOMPARE( projectScope->variable( "layers" ).toList().size(), 1 ); + QCOMPARE( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 0 ) ), vectorLayer ); + QCOMPARE( projectScope->variable( "layer_ids" ).toList().size(), 1 ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer->id() ) ); + delete projectScope; + project.addMapLayer( vectorLayer2 ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); + QCOMPARE( projectScope->variable( "layers" ).toList().size(), 2 ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 0 ) ) ) ) ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 1 ) ) ) ) ); + QCOMPARE( projectScope->variable( "layer_ids" ).toList().size(), 2 ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer->id() ) ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer2->id() ) ); + delete projectScope; + project.addMapLayers( QList< QgsMapLayer * >() << vectorLayer3 ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); + QCOMPARE( projectScope->variable( "layers" ).toList().size(), 3 ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 0 ) ) ) ) ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 1 ) ) ) ) ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 2 ) ) ) ) ); + QCOMPARE( projectScope->variable( "layer_ids" ).toList().size(), 3 ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer->id() ) ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer2->id() ) ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer3->id() ) ); + delete projectScope; + project.removeMapLayer( vectorLayer ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); + QCOMPARE( projectScope->variable( "layers" ).toList().size(), 2 ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 0 ) ) ) ) ); + QVERIFY( project.mapLayers().values().contains( qobject_cast< QgsMapLayer * >( qvariant_cast< QObject * >( projectScope->variable( "layers" ).toList().at( 1 ) ) ) ) ); + QCOMPARE( projectScope->variable( "layer_ids" ).toList().size(), 2 ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer2->id() ) ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().contains( vectorLayer3->id() ) ); + delete projectScope; + project.removeMapLayers( QList< QgsMapLayer * >() << vectorLayer2 << vectorLayer3 ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); + QVERIFY( projectScope->variable( "layers" ).toList().isEmpty() ); + QVERIFY( projectScope->variable( "layer_ids" ).toList().isEmpty() ); + delete projectScope; + //test clearing project variables - QgsExpressionContextScope *projectScope = QgsExpressionContextUtils::projectScope( project ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); QVERIFY( projectScope->hasVariable( "test" ) ); - QgsProject::instance()->clear(); + project.clear(); delete projectScope; - projectScope = QgsExpressionContextUtils::projectScope( project ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); QVERIFY( !projectScope->hasVariable( "test" ) ); //test a preset project variable - QgsProject::instance()->setTitle( QStringLiteral( "test project" ) ); + project.setTitle( QStringLiteral( "test project" ) ); delete projectScope; - projectScope = QgsExpressionContextUtils::projectScope( project ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); QCOMPARE( projectScope->variable( "project_title" ).toString(), QString( "test project" ) ); delete projectScope; @@ -683,8 +735,8 @@ void TestQgsExpressionContext::projectScope() QVariantMap vars; vars.insert( QStringLiteral( "newvar1" ), QStringLiteral( "val1" ) ); vars.insert( QStringLiteral( "newvar2" ), QStringLiteral( "val2" ) ); - QgsExpressionContextUtils::setProjectVariables( project, vars ); - projectScope = QgsExpressionContextUtils::projectScope( project ); + QgsExpressionContextUtils::setProjectVariables( &project, vars ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); QVERIFY( !projectScope->hasVariable( "test" ) ); QCOMPARE( projectScope->variable( "newvar1" ).toString(), QString( "val1" ) ); @@ -692,11 +744,11 @@ void TestQgsExpressionContext::projectScope() delete projectScope; //test removeProjectVariable - QgsExpressionContextUtils::setProjectVariable( project, QStringLiteral( "key" ), "value" ); - projectScope = QgsExpressionContextUtils::projectScope( project ); + QgsExpressionContextUtils::setProjectVariable( &project, QStringLiteral( "key" ), "value" ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); QVERIFY( projectScope->hasVariable( "key" ) ); - QgsExpressionContextUtils::removeProjectVariable( project, QStringLiteral( "key" ) ); - projectScope = QgsExpressionContextUtils::projectScope( project ); + QgsExpressionContextUtils::removeProjectVariable( &project, QStringLiteral( "key" ) ); + projectScope = QgsExpressionContextUtils::projectScope( &project ); QVERIFY( !projectScope->hasVariable( "key" ) ); delete projectScope; projectScope = nullptr; @@ -710,7 +762,7 @@ void TestQgsExpressionContext::projectScope() colorList << qMakePair( QColor( 30, 60, 20 ), QStringLiteral( "murky depths of hades" ) ); s.setColors( colorList ); QgsExpressionContext contextColors; - contextColors << QgsExpressionContextUtils::projectScope( project ); + contextColors << QgsExpressionContextUtils::projectScope( QgsProject::instance() ); QgsExpression expProjectColor( QStringLiteral( "project_color('murky depths of hades')" ) ); QCOMPARE( expProjectColor.evaluate( &contextColors ).toString(), QString( "30,60,20" ) ); diff --git a/tests/src/core/testqgsfield.cpp b/tests/src/core/testqgsfield.cpp index 2de70329c7e5..5ea636e8539d 100644 --- a/tests/src/core/testqgsfield.cpp +++ b/tests/src/core/testqgsfield.cpp @@ -48,6 +48,7 @@ class TestQgsField: public QObject void dataStream(); void displayName(); void displayNameWithAlias(); + void displayType(); void editorWidgetSetup(); void collection(); @@ -752,6 +753,24 @@ void TestQgsField::displayNameWithAlias() } +void TestQgsField::displayType() +{ + QgsField field; + field.setTypeName( QStringLiteral( "numeric" ) ); + QCOMPARE( field.displayType(), QString( "numeric" ) ); + field.setLength( 20 ); + QCOMPARE( field.displayType(), QString( "numeric(20)" ) ); + field.setPrecision( 10 ); + field.setPrecision( 10 ); + QCOMPARE( field.displayType(), QString( "numeric(20, 10)" ) ); + QCOMPARE( field.displayType( true ), QString( "numeric(20, 10) NULL" ) ); + QgsFieldConstraints constraints; + constraints.setConstraint( QgsFieldConstraints::ConstraintUnique ); + field.setConstraints( constraints ); + QCOMPARE( field.displayType( true ), QString( "numeric(20, 10) NULL UNIQUE" ) ); +} + + void TestQgsField::editorWidgetSetup() { QgsField field; diff --git a/tests/src/core/testqgsfontmarker.cpp b/tests/src/core/testqgsfontmarker.cpp index 753abbc41dbe..9e47e25569e1 100644 --- a/tests/src/core/testqgsfontmarker.cpp +++ b/tests/src/core/testqgsfontmarker.cpp @@ -20,6 +20,7 @@ #include #include #include +#include //qgis includes... #include @@ -54,8 +55,10 @@ class TestQgsFontMarkerSymbol : public QObject void cleanup() {} // will be called after every testfunction. void fontMarkerSymbol(); + void fontMarkerSymbolStyle(); void fontMarkerSymbolStroke(); void bounds(); + void fontMarkerSymbolDataDefinedProperties(); private: bool mTestHasError = false ; @@ -78,6 +81,7 @@ void TestQgsFontMarkerSymbol::initTestCase() QgsApplication::init(); QgsApplication::initQgis(); QgsApplication::showSettings(); + QgsFontUtils::loadStandardTestFonts( QStringList() << QStringLiteral( "Bold" ) ); //create some objects that will be used in all tests... QString myDataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt @@ -134,6 +138,49 @@ void TestQgsFontMarkerSymbol::fontMarkerSymbol() QVERIFY( imageCheck( "fontmarker" ) ); } +void TestQgsFontMarkerSymbol::fontMarkerSymbolStyle() +{ + mReport += QLatin1String( "

    Font marker symbol style layer test

    \n" ); + + QgsFontUtils::loadStandardTestFonts( QStringList() << QStringLiteral( "Bold" ) << QStringLiteral( "Oblique" ) ); + mFontMarkerLayer->setColor( Qt::blue ); + QFont font = QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ); + mFontMarkerLayer->setFontFamily( font.family() ); + mFontMarkerLayer->setFontStyle( QStringLiteral( "Oblique" ) ); + mFontMarkerLayer->setCharacter( QChar( 'A' ) ); + mFontMarkerLayer->setSize( 12 ); + QVERIFY( imageCheck( "fontmarker_style" ) ); + + // Loading both Bold and Oblique in the initTestCase() function creates inconsistent results on windows and linux, this is a workaround + QFontDatabase fontDb; + fontDb.removeAllApplicationFonts(); + QgsFontUtils::loadStandardTestFonts( QStringList() << QStringLiteral( "Bold" ) ); +} + +void TestQgsFontMarkerSymbol::fontMarkerSymbolDataDefinedProperties() +{ + mReport += QLatin1String( "

    Font marker symbol data defined properties layer test

    \n" ); + + QgsFontUtils::loadStandardTestFonts( QStringList() << QStringLiteral( "Bold" ) << QStringLiteral( "Oblique" ) ); + mFontMarkerLayer->setColor( Qt::blue ); + QFont font = QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ); + mFontMarkerLayer->setFontFamily( font.family() ); + mFontMarkerLayer->setFontStyle( QStringLiteral( "Bold" ) ); + mFontMarkerLayer->setDataDefinedProperty( QgsSymbolLayer::PropertyFontStyle, QgsProperty::fromExpression( QStringLiteral( "'Oblique'" ) ) ); + mFontMarkerLayer->setCharacter( QChar( 'Z' ) ); + mFontMarkerLayer->setDataDefinedProperty( QgsSymbolLayer::PropertyCharacter, QgsProperty::fromExpression( QStringLiteral( "'A'" ) ) ); + mFontMarkerLayer->setSize( 12 ); + mFontMarkerLayer->setDataDefinedProperty( QgsSymbolLayer::PropertySize, QgsProperty::fromExpression( QStringLiteral( "12" ) ) ); + QVERIFY( imageCheck( "fontmarker_datadefinedproperties" ) ); + + mFontMarkerLayer->setDataDefinedProperties( QgsPropertyCollection() ); + + // Loading both Bold and Oblique in the initTestCase() function creates inconsistent results on windows and linux, this is a workaround + QFontDatabase fontDb; + fontDb.removeAllApplicationFonts(); + QgsFontUtils::loadStandardTestFonts( QStringList() << QStringLiteral( "Bold" ) ); +} + void TestQgsFontMarkerSymbol::fontMarkerSymbolStroke() { mFontMarkerLayer->setColor( Qt::blue ); diff --git a/tests/src/core/testqgsgeometry.cpp b/tests/src/core/testqgsgeometry.cpp index 9e16a31da81b..36dc4cf29565 100644 --- a/tests/src/core/testqgsgeometry.cpp +++ b/tests/src/core/testqgsgeometry.cpp @@ -12263,6 +12263,21 @@ void TestQgsGeometry::multiPoint() pCast2.fromWkt( QStringLiteral( "MultiPointZM(PointZM(0 1 1 2))" ) ); QVERIFY( QgsMultiPoint().cast( &pCast2 ) ); + // bounding box + QgsMultiPoint boundingBox; + boundingBox.addGeometry( new QgsPoint( 0, 0 ) ); + QCOMPARE( boundingBox.boundingBox(), QgsRectangle( 0, 0, 0, 0 ) ); + boundingBox.addGeometry( new QgsPoint( 1, 2 ) ); + QCOMPARE( boundingBox.boundingBox(), QgsRectangle( 0, 0, 1, 2 ) ); + QgsMultiPoint boundingBox2; + QCOMPARE( boundingBox2.boundingBox(), QgsRectangle( 0, 0, 0, 0 ) ); + boundingBox2.addGeometry( new QgsPoint( 1, 2 ) ); + QCOMPARE( boundingBox2.boundingBox(), QgsRectangle( 1, 2, 1, 2 ) ); + boundingBox2.addGeometry( new QgsPoint( 10, 3 ) ); + QCOMPARE( boundingBox2.boundingBox(), QgsRectangle( 1, 2, 10, 3 ) ); + boundingBox2.addGeometry( new QgsPoint( 0, 0 ) ); + QCOMPARE( boundingBox2.boundingBox(), QgsRectangle( 0, 0, 10, 3 ) ); + //boundary //multipoints have no boundary defined diff --git a/tests/src/core/testqgslayertree.cpp b/tests/src/core/testqgslayertree.cpp index 9d4da03bbc8e..28f11b4c044a 100644 --- a/tests/src/core/testqgslayertree.cpp +++ b/tests/src/core/testqgslayertree.cpp @@ -55,6 +55,7 @@ class TestQgsLayerTree : public QObject void testUtilsCollectMapLayers(); void testUtilsCountMapLayers(); void testSymbolText(); + void testNodeDepth(); private: @@ -764,6 +765,39 @@ void TestQgsLayerTree::testSymbolText() delete root; } +void TestQgsLayerTree::testNodeDepth() +{ + QCOMPARE( mRoot->depth(), 0 ); + QgsLayerTreeNode *secondGroup = mRoot->children()[1]; + QCOMPARE( secondGroup->depth(), 1 ); + + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "Point?field=col1:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ); + QVERIFY( vl->isValid() ); + + QgsLayerTreeLayer *n = new QgsLayerTreeLayer( vl->id(), vl->name() ); + mRoot->addChildNode( n ); + QCOMPARE( n->depth(), 1 ); + + QgsLayerTreeGroup *g1 = mRoot->addGroup( QStringLiteral( "g1" ) ); + QCOMPARE( g1->depth(), 1 ); + QgsLayerTreeLayer *n1 = n->clone(); + g1->addChildNode( n1 ); + QCOMPARE( n1->depth(), 2 ); + QgsLayerTreeGroup *g2 = g1->addGroup( QStringLiteral( "g2" ) ); + QCOMPARE( g2->depth(), 2 ); + QgsLayerTreeLayer *n2 = n->clone(); + g2->addChildNode( n2 ); + QCOMPARE( n2->depth(), 3 ); + QgsLayerTreeGroup *g3 = g2->addGroup( QStringLiteral( "g3" ) ); + QCOMPARE( g3->depth(), 3 ); + QgsLayerTreeLayer *n3 = n->clone(); + g3->addChildNode( n3 ); + QCOMPARE( n3->depth(), 4 ); + + mRoot->removeChildNode( n ); + mRoot->removeChildNode( g1 ); + delete vl; +} QGSTEST_MAIN( TestQgsLayerTree ) #include "testqgslayertree.moc" diff --git a/tests/src/core/testqgslayoutpicture.cpp b/tests/src/core/testqgslayoutpicture.cpp index 7c5e5770948f..646341d33b49 100644 --- a/tests/src/core/testqgslayoutpicture.cpp +++ b/tests/src/core/testqgslayoutpicture.cpp @@ -37,6 +37,8 @@ class TestQgsLayoutPicture : public QObject void cleanup();// will be called after every testfunction. void pictureRender(); + void pictureRaster(); + void pictureSvg(); void pictureRotation(); //test if picture pictureRotation is functioning void pictureItemRotation(); //test if composer picture item rotation is functioning @@ -131,6 +133,39 @@ void TestQgsLayoutPicture::pictureRender() mLayout->removeItem( mPicture ); } +void TestQgsLayoutPicture::pictureRaster() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemPicture *p = new QgsLayoutItemPicture( &l ); + p->setPicturePath( mPngImage, QgsLayoutItemPicture::FormatRaster ); + p->attemptSetSceneRect( QRectF( 70, 70, 100, 100 ) ); + p->setFrameEnabled( true ); + + l.addLayoutItem( p ); + + QgsLayoutChecker checker( QStringLiteral( "composerpicture_render" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "composer_picture" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + +void TestQgsLayoutPicture::pictureSvg() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemPicture *p = new QgsLayoutItemPicture( &l ); + p->setResizeMode( QgsLayoutItemPicture::Zoom ); + p->setPicturePath( mSvgImage, QgsLayoutItemPicture::FormatSVG ); + p->attemptSetSceneRect( QRectF( 70, 70, 100, 100 ) ); + p->setFrameEnabled( true ); + + l.addLayoutItem( p ); + + QgsLayoutChecker checker( QStringLiteral( "composerpicture_svg_zoom" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "composer_picture" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + void TestQgsLayoutPicture::pictureRotation() { //test picture rotation diff --git a/tests/src/core/testqgslayoutscalebar.cpp b/tests/src/core/testqgslayoutscalebar.cpp index 16ae4bb5a576..5ab824e24167 100644 --- a/tests/src/core/testqgslayoutscalebar.cpp +++ b/tests/src/core/testqgslayoutscalebar.cpp @@ -29,7 +29,11 @@ #include "qgsproject.h" #include "qgspallabeling.h" #include "qgsbasicnumericformat.h" - +#include "qgslinesymbollayer.h" +#include "qgslayoutmanager.h" +#include "qgsprintlayout.h" +#include "qgsfillsymbollayer.h" +#include "qgshollowscalebarrenderer.h" #include #include #include "qgstest.h" @@ -47,15 +51,24 @@ class TestQgsLayoutScaleBar : public QObject void init();// will be called before each testfunction is executed. void cleanup();// will be called after every testfunction. void singleBox(); + void singleBoxLineSymbol(); + void singleBoxFillSymbol(); void singleBoxLabelBelowSegment(); void singleBoxAlpha(); void doubleBox(); + void doubleBoxLineSymbol(); + void doubleBoxFillSymbol(); void doubleBoxLabelCenteredSegment(); void numeric(); void tick(); + void tickLineSymbol(); void dataDefined(); + void oldDataDefinedProject(); void textFormat(); void numericFormat(); + void steppedLine(); + void hollow(); + void hollowDefaults(); private: QString mReport; @@ -124,7 +137,9 @@ void TestQgsLayoutScaleBar::singleBox() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); scalebar->setStyle( QStringLiteral( "Single Box" ) ); @@ -133,6 +148,93 @@ void TestQgsLayoutScaleBar::singleBox() QVERIFY( checker.testLayout( mReport, 0, 0 ) ); } +void TestQgsLayoutScaleBar::singleBoxLineSymbol() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 4 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + + lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 2 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 255, 0 ) ); + lineSymbol->appendSymbolLayer( lineSymbolLayer.release() ); + + scalebar->setLineSymbol( lineSymbol.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "Single Box" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_singlebox_linesymbol" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + +void TestQgsLayoutScaleBar::singleBoxFillSymbol() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + fillSymbolLayer->setColor2( QColor( 255, 255, 0 ) ); + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + scalebar->setFillSymbol( fillSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2 = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer2 = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer2->setColor( QColor( 0, 255, 0 ) ); + fillSymbolLayer2->setColor2( QColor( 255, 255, 255 ) ); + fillSymbol2->changeSymbolLayer( 0, fillSymbolLayer2.release() ); + scalebar->setAlternateFillSymbol( fillSymbol2.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "Single Box" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_singlebox_fillsymbol" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + void TestQgsLayoutScaleBar::singleBoxLabelBelowSegment() { QgsLayout l( QgsProject::instance() ); @@ -153,7 +255,9 @@ void TestQgsLayoutScaleBar::singleBoxLabelBelowSegment() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP scalebar->setLabelVerticalPlacement( QgsScaleBarSettings::LabelBelowSegment ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); @@ -186,13 +290,17 @@ void TestQgsLayoutScaleBar::singleBoxAlpha() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP scalebar->setStyle( QStringLiteral( "Single Box" ) ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setFillColor( QColor( 255, 0, 0, 100 ) ); scalebar->setFillColor2( QColor( 0, 255, 0, 50 ) ); scalebar->setLineColor( QColor( 0, 0, 255, 150 ) ); scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_singlebox_alpha" ), &l ); @@ -222,12 +330,13 @@ void TestQgsLayoutScaleBar::doubleBox() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); - scalebar->setFillColor( Qt::black ); scalebar->setFillColor2( Qt::white ); scalebar->setLineColor( Qt::black ); scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP scalebar->setStyle( QStringLiteral( "Double Box" ) ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); @@ -236,6 +345,93 @@ void TestQgsLayoutScaleBar::doubleBox() QVERIFY( checker.testLayout( mReport, 0, 0 ) ); } +void TestQgsLayoutScaleBar::doubleBoxLineSymbol() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 4 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + + lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 2 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 255, 0 ) ); + lineSymbol->appendSymbolLayer( lineSymbolLayer.release() ); + + scalebar->setLineSymbol( lineSymbol.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "Double Box" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_doublebox_linesymbol" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + +void TestQgsLayoutScaleBar::doubleBoxFillSymbol() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + fillSymbolLayer->setColor2( QColor( 255, 255, 0 ) ); + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + scalebar->setFillSymbol( fillSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2 = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer2 = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer2->setColor( QColor( 0, 255, 0 ) ); + fillSymbolLayer2->setColor2( QColor( 255, 255, 255 ) ); + fillSymbol2->changeSymbolLayer( 0, fillSymbolLayer2.release() ); + scalebar->setAlternateFillSymbol( fillSymbol2.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "Double Box" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_doublebox_fillsymbol" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + void TestQgsLayoutScaleBar::doubleBoxLabelCenteredSegment() { QgsLayout l( QgsProject::instance() ); @@ -258,12 +454,13 @@ void TestQgsLayoutScaleBar::doubleBoxLabelCenteredSegment() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 3 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); - scalebar->setFillColor( Qt::black ); scalebar->setFillColor2( Qt::white ); scalebar->setLineColor( Qt::black ); scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP scalebar->setStyle( QStringLiteral( "Double Box" ) ); scalebar->setLabelVerticalPlacement( QgsScaleBarSettings::LabelBelowSegment ); @@ -297,7 +494,9 @@ void TestQgsLayoutScaleBar::numeric() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP QFont newFont = QgsFontUtils::getStandardTestFont( QStringLiteral( "Bold" ) ); newFont.setPointSizeF( 12 ); @@ -332,16 +531,64 @@ void TestQgsLayoutScaleBar::tick() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); scalebar->setStyle( QStringLiteral( "Line Ticks Up" ) ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_tick" ), &l ); checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); QVERIFY( checker.testLayout( mReport, 0, 0 ) ); } +void TestQgsLayoutScaleBar::tickLineSymbol() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 4 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + + lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 2 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 255, 0 ) ); + lineSymbol->appendSymbolLayer( lineSymbolLayer.release() ); + + scalebar->setLineSymbol( lineSymbol.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "Line Ticks Up" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_tick_linesymbol" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + void TestQgsLayoutScaleBar::dataDefined() { QgsLayout l( QgsProject::instance() ); @@ -362,8 +609,16 @@ void TestQgsLayoutScaleBar::dataDefined() scalebar->setUnitsPerSegment( 2000 ); scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); - scalebar->setHeight( 5 ); - scalebar->setLineWidth( 1.0 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 1 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 0, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + scalebar->setLineSymbol( lineSymbol.release() ); + scalebar->setStyle( QStringLiteral( "Single Box" ) ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setNumberDecimalPlaces( 0 ); @@ -372,21 +627,41 @@ void TestQgsLayoutScaleBar::dataDefined() newFont.setPointSizeF( 12 ); scalebar->setTextFormat( QgsTextFormat::fromQFont( newFont ) ); - scalebar->setStyle( QStringLiteral( "Numeric" ) ); - QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_numeric" ), &l ); - checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); - bool result = checker.testLayout( mReport, 0, 0 ); - QVERIFY( result ); - + // this is the deprecated way of doing this -- the new way is using data defined properties on the scalebar line symbol. + // so this test is to ensure old projects/api use works correctly scalebar->dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarFillColor, QgsProperty::fromExpression( QStringLiteral( "'red'" ) ) ); scalebar->dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarFillColor2, QgsProperty::fromExpression( QStringLiteral( "'blue'" ) ) ); scalebar->dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarLineColor, QgsProperty::fromExpression( QStringLiteral( "'yellow'" ) ) ); - scalebar->dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarLineWidth, QgsProperty::fromExpression( QStringLiteral( "1.2" ) ) ); + scalebar->dataDefinedProperties().setProperty( QgsLayoutObject::ScalebarLineWidth, QgsProperty::fromExpression( QStringLiteral( "1.2*3" ) ) ); scalebar->refreshDataDefinedProperty(); - QCOMPARE( scalebar->brush().color().name(), QColor( 255, 0, 0 ).name() ); - QCOMPARE( scalebar->brush2().color().name(), QColor( 0, 0, 255 ).name() ); - QCOMPARE( scalebar->pen().color().name(), QColor( 255, 255, 0 ).name() ); - QCOMPARE( scalebar->pen().widthF(), 1.2 ); + + QgsLayoutChecker checker2( QStringLiteral( "layoutscalebar_datadefined" ), &l ); + checker2.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker2.testLayout( mReport, 0, 0 ) ); +} + +void TestQgsLayoutScaleBar::oldDataDefinedProject() +{ + QgsProject project; + // read a project with the older data defined line width and color + project.read( QStringLiteral( TEST_DATA_DIR ) + "/layouts/scalebar_old_datadefined.qgs" ); + QgsLayout *l = project.layoutManager()->printLayouts().at( 0 ); + QList< QgsLayoutItemScaleBar * > scaleBars; + l->layoutItems( scaleBars ); + QgsLayoutItemScaleBar *scaleBar = scaleBars.at( 0 ); + + // ensure the deprecated scalebar datadefined properties were automatically copied to the scalebar's line symbol + QgsLineSymbol *ls = scaleBar->lineSymbol(); + QgsSimpleLineSymbolLayer *sll = dynamic_cast< QgsSimpleLineSymbolLayer * >( ls->symbolLayer( 0 ) ); + + QVERIFY( sll->dataDefinedProperties().property( QgsSymbolLayer::PropertyStrokeWidth ).isActive() ); + QCOMPARE( sll->dataDefinedProperties().property( QgsSymbolLayer::PropertyStrokeWidth ).asExpression(), QStringLiteral( "3" ) ); + QVERIFY( sll->dataDefinedProperties().property( QgsSymbolLayer::PropertyStrokeColor ).isActive() ); + QCOMPARE( sll->dataDefinedProperties().property( QgsSymbolLayer::PropertyStrokeColor ).asExpression(), QStringLiteral( "'red'" ) ); + + // deprecated properties should be gone + QVERIFY( !scaleBar->dataDefinedProperties().property( QgsLayoutObject::ScalebarLineColor ).isActive() ); + QVERIFY( !scaleBar->dataDefinedProperties().property( QgsLayoutObject::ScalebarLineWidth ).isActive() ); } void TestQgsLayoutScaleBar::textFormat() @@ -408,7 +683,9 @@ void TestQgsLayoutScaleBar::textFormat() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP scalebar->setStyle( QStringLiteral( "Single Box" ) ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); @@ -441,7 +718,9 @@ void TestQgsLayoutScaleBar::numericFormat() scalebar->setNumberOfSegmentsLeft( 0 ); scalebar->setNumberOfSegments( 2 ); scalebar->setHeight( 5 ); + Q_NOWARN_DEPRECATED_PUSH scalebar->setLineWidth( 1.0 ); + Q_NOWARN_DEPRECATED_POP scalebar->setStyle( QStringLiteral( "Single Box" ) ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( true ); dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowPlusSign( true ); @@ -456,6 +735,155 @@ void TestQgsLayoutScaleBar::numericFormat() QVERIFY( checker.testLayout( mReport, 0, 0 ) ); } +void TestQgsLayoutScaleBar::steppedLine() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 4 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + + lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 2 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 255, 0 ) ); + lineSymbol->appendSymbolLayer( lineSymbolLayer.release() ); + + scalebar->setLineSymbol( lineSymbol.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "stepped" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_stepped" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + +void TestQgsLayoutScaleBar::hollow() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 150, 150 ) ); + map->setFrameEnabled( true ); + l.addLayoutItem( map ); + map->setExtent( QgsRectangle( 17.923, 30.160, 18.023, 30.260 ) ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + scalebar->attemptSetSceneRect( QRectF( 20, 180, 50, 20 ) ); + l.addLayoutItem( scalebar ); + scalebar->setLinkedMap( map ); + scalebar->setTextFormat( QgsTextFormat::fromQFont( QgsFontUtils::getStandardTestFont() ) ); + scalebar->setUnits( QgsUnitTypes::DistanceMeters ); + scalebar->setUnitsPerSegment( 2000 ); + scalebar->setNumberOfSegmentsLeft( 2 ); + scalebar->setNumberOfSegments( 2 ); + scalebar->setHeight( 20 ); + + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 4 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + + lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 2 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 255, 0 ) ); + lineSymbol->appendSymbolLayer( lineSymbolLayer.release() ); + + scalebar->setLineSymbol( lineSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + fillSymbolLayer->setColor2( QColor( 255, 255, 0 ) ); + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + scalebar->setFillSymbol( fillSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2 = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer2 = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer2->setColor( QColor( 0, 255, 0 ) ); + fillSymbolLayer2->setColor2( QColor( 255, 255, 255 ) ); + fillSymbol2->changeSymbolLayer( 0, fillSymbolLayer2.release() ); + scalebar->setAlternateFillSymbol( fillSymbol2.release() ); + + dynamic_cast< QgsBasicNumericFormat *>( const_cast< QgsNumericFormat * >( scalebar->numericFormat() ) )->setShowThousandsSeparator( false ); + + scalebar->setStyle( QStringLiteral( "hollow" ) ); + QgsLayoutChecker checker( QStringLiteral( "layoutscalebar_hollow" ), &l ); + checker.setControlPathPrefix( QStringLiteral( "layout_scalebar" ) ); + QVERIFY( checker.testLayout( mReport, 0, 0 ) ); +} + +void TestQgsLayoutScaleBar::hollowDefaults() +{ + QgsLayout l( QgsProject::instance() ); + + QgsLayoutItemScaleBar *scalebar = new QgsLayoutItemScaleBar( &l ); + + // apply random symbols + std::unique_ptr< QgsLineSymbol > lineSymbol = qgis::make_unique< QgsLineSymbol >(); + std::unique_ptr< QgsSimpleLineSymbolLayer > lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 4 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + lineSymbol->changeSymbolLayer( 0, lineSymbolLayer.release() ); + + lineSymbolLayer = qgis::make_unique< QgsSimpleLineSymbolLayer >(); + lineSymbolLayer->setWidth( 2 ); + lineSymbolLayer->setWidthUnit( QgsUnitTypes::RenderMillimeters ); + lineSymbolLayer->setColor( QColor( 255, 255, 0 ) ); + lineSymbol->appendSymbolLayer( lineSymbolLayer.release() ); + + scalebar->setLineSymbol( lineSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer->setColor( QColor( 255, 0, 0 ) ); + fillSymbolLayer->setColor2( QColor( 255, 255, 0 ) ); + fillSymbol->changeSymbolLayer( 0, fillSymbolLayer.release() ); + scalebar->setFillSymbol( fillSymbol.release() ); + + std::unique_ptr< QgsFillSymbol > fillSymbol2 = qgis::make_unique< QgsFillSymbol >(); + std::unique_ptr< QgsGradientFillSymbolLayer > fillSymbolLayer2 = qgis::make_unique< QgsGradientFillSymbolLayer >(); + fillSymbolLayer2->setColor( QColor( 0, 255, 0 ) ); + fillSymbolLayer2->setColor2( QColor( 255, 255, 255 ) ); + fillSymbol2->changeSymbolLayer( 0, fillSymbolLayer2.release() ); + scalebar->setAlternateFillSymbol( fillSymbol2.release() ); + + // reset to renderer defaults + QgsHollowScaleBarRenderer renderer; + scalebar->applyDefaultRendererSettings( &renderer ); + // should be reset to "null" fill symbols + QCOMPARE( dynamic_cast< QgsSimpleFillSymbolLayer * >( scalebar->fillSymbol()->symbolLayer( 0 ) )->brushStyle(), Qt::NoBrush ); + QCOMPARE( dynamic_cast< QgsSimpleFillSymbolLayer * >( scalebar->alternateFillSymbol()->symbolLayer( 0 ) )->brushStyle(), Qt::NoBrush ); + // stroke should be unchanged + QCOMPARE( dynamic_cast< QgsSimpleLineSymbolLayer * >( scalebar->lineSymbol()->symbolLayer( 0 ) )->color(), QColor( 255, 0, 0 ) ); + +} + QGSTEST_MAIN( TestQgsLayoutScaleBar ) #include "testqgslayoutscalebar.moc" diff --git a/tests/src/core/testqgslayouttable.cpp b/tests/src/core/testqgslayouttable.cpp index e1ef3493fe84..b5cabb55b448 100644 --- a/tests/src/core/testqgslayouttable.cpp +++ b/tests/src/core/testqgslayouttable.cpp @@ -79,6 +79,8 @@ class TestQgsLayoutTable : public QObject void conditionalFormatting(); //test rendering with conditional formatting void dataDefinedSource(); void wrappedText(); + void testBaseSort(); + void testExpressionSort(); private: QgsVectorLayer *mVectorLayer = nullptr; @@ -1582,5 +1584,52 @@ void TestQgsLayoutTable::wrappedText() QVERIFY( !wrapText.endsWith( "\naliqua" ) ); } + +void TestQgsLayoutTable::testBaseSort() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemAttributeTable *table = new QgsLayoutItemAttributeTable( &l ); + table->setVectorLayer( mVectorLayer ); + table->setDisplayOnlyVisibleFeatures( false ); + table->setMaximumNumberOfFeatures( 1 ); + table->columns().at( 2 )->setSortByRank( 1 ); + table->columns().at( 2 )->setSortOrder( Qt::DescendingOrder ); + table->refresh(); + + QVector expectedRows; + QStringList row; + row << QStringLiteral( "Jet" ) << QStringLiteral( "100" ) << QStringLiteral( "20" ) << QStringLiteral( "3" ) << QStringLiteral( "0" ) << QStringLiteral( "3" ); + expectedRows.append( row ); + row.clear(); + + //retrieve rows and check + compareTable( table, expectedRows ); +} + +void TestQgsLayoutTable::testExpressionSort() +{ + QgsLayout l( QgsProject::instance() ); + l.initializeDefaults(); + QgsLayoutItemAttributeTable *table = new QgsLayoutItemAttributeTable( &l ); + table->setVectorLayer( mVectorLayer ); + table->setDisplayOnlyVisibleFeatures( false ); + table->setMaximumNumberOfFeatures( 1 ); + table->columns().at( 0 )->setAttribute( "Heading * -1" ); + table->columns().at( 0 )->setHeading( "exp" ); + table->columns().at( 0 )->setSortByRank( 1 ); + table->columns().at( 0 )->setSortOrder( Qt::AscendingOrder ); + table->refresh(); + + QVector expectedRows; + QStringList row; + row << QStringLiteral( "-340" ) << QStringLiteral( "340" ) << QStringLiteral( "1" ) << QStringLiteral( "3" ) << QStringLiteral( "3" ) << QStringLiteral( "6" ); + expectedRows.append( row ); + row.clear(); + + //retrieve rows and check + compareTable( table, expectedRows ); +} + QGSTEST_MAIN( TestQgsLayoutTable ) #include "testqgslayouttable.moc" diff --git a/tests/src/core/testqgsmeshlayer.cpp b/tests/src/core/testqgsmeshlayer.cpp index b82a3c178406..52f6b616fce6 100644 --- a/tests/src/core/testqgsmeshlayer.cpp +++ b/tests/src/core/testqgsmeshlayer.cpp @@ -27,6 +27,9 @@ #include "qgsproject.h" #include "qgstriangularmesh.h" #include "qgsmeshlayerutils.h" +#include "qgsmeshlayertemporalproperties.h" + +#include "qgsmeshdataprovidertemporalcapabilities.h" /** * \ingroup UnitTests @@ -45,6 +48,7 @@ class TestQgsMeshLayer : public QObject QgsMeshLayer *mMdalLayer = nullptr; QgsMeshLayer *mMemory1DLayer = nullptr; QgsMeshLayer *mMdal1DLayer = nullptr; + QgsMeshLayer *mMdal3DLayer = nullptr; private slots: void initTestCase();// will be called before the first testfunction is executed. @@ -72,8 +76,7 @@ class TestQgsMeshLayer : public QObject void test_read_vertex_scalar_dataset_with_inactive_face(); void test_extent(); - void test_time_format_data(); - void test_time_format(); + void test_temporal(); void test_reload(); void test_reload_extra_dataset(); @@ -104,13 +107,16 @@ void TestQgsMeshLayer::initTestCase() mMemoryLayer = new QgsMeshLayer( readFile( "/quad_and_triangle.txt" ), "Triangle and Quad Memory", "mesh_memory" ); QVERIFY( mMemoryLayer->isValid() ); QCOMPARE( mMemoryLayer->dataProvider()->extraDatasets().count(), 0 ); + QVERIFY( !mMemoryLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + QVERIFY( !mMemoryLayer->temporalProperties()->isActive() ); mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_bed_elevation.txt" ) ); mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_vertex_scalar.txt" ) ); mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_vertex_vector.txt" ) ); mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_face_scalar.txt" ) ); mMemoryLayer->dataProvider()->addDataset( readFile( "/quad_and_triangle_face_vector.txt" ) ); QCOMPARE( mMemoryLayer->dataProvider()->extraDatasets().count(), 5 ); - + QVERIFY( mMemoryLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + QVERIFY( mMemoryLayer->temporalProperties()->isActive() ); QgsProject::instance()->addMapLayers( QList() << mMemoryLayer ); @@ -118,6 +124,7 @@ void TestQgsMeshLayer::initTestCase() QString uri( mDataDir + "/quad_and_triangle.2dm" ); mMdalLayer = new QgsMeshLayer( uri, "Triangle and Quad MDAL", "mdal" ); QCOMPARE( mMdalLayer->dataProvider()->datasetGroupCount(), 1 ); //bed elevation is already in the 2dm + QVERIFY( mMdalLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_vertex_scalar.dat" ); mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_vertex_vector.dat" ); QCOMPARE( mMdalLayer->dataProvider()->extraDatasets().count(), 2 ); @@ -127,6 +134,8 @@ void TestQgsMeshLayer::initTestCase() mMdalLayer->dataProvider()->addDataset( mDataDir + "/quad_and_triangle_els_face_vector.dat" ); QVERIFY( mMdalLayer->isValid() ); + QVERIFY( mMemoryLayer->temporalProperties()->isActive() ); + QgsProject::instance()->addMapLayers( QList() << mMdalLayer ); @@ -134,12 +143,16 @@ void TestQgsMeshLayer::initTestCase() mMemory1DLayer = new QgsMeshLayer( readFile( "/lines.txt" ), "Lines Memory", "mesh_memory" ); QVERIFY( mMemory1DLayer->isValid() ); QCOMPARE( mMemory1DLayer->dataProvider()->extraDatasets().count(), 0 ); + QVERIFY( !mMemory1DLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + QVERIFY( !mMemory1DLayer->temporalProperties()->isActive() ); mMemory1DLayer->dataProvider()->addDataset( readFile( "/lines_bed_elevation.txt" ) ); mMemory1DLayer->dataProvider()->addDataset( readFile( "/lines_vertex_scalar.txt" ) ); mMemory1DLayer->dataProvider()->addDataset( readFile( "/lines_vertex_vector.txt" ) ); mMemory1DLayer->dataProvider()->addDataset( readFile( "/lines_els_scalar.txt" ) ); mMemory1DLayer->dataProvider()->addDataset( readFile( "/lines_els_vector.txt" ) ); QCOMPARE( mMemory1DLayer->dataProvider()->extraDatasets().count(), 5 ); + QVERIFY( mMemory1DLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + QVERIFY( mMemory1DLayer->temporalProperties()->isActive() ); QgsProject::instance()->addMapLayers( QList() << mMemory1DLayer ); @@ -148,9 +161,14 @@ void TestQgsMeshLayer::initTestCase() uri = QString( mDataDir + "/lines.2dm" ); mMdal1DLayer = new QgsMeshLayer( uri, "Lines MDAL", "mdal" ); QCOMPARE( mMdal1DLayer->dataProvider()->datasetGroupCount(), 1 ); //bed elevation is already in the 2dm + QVERIFY( mMemory1DLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + QVERIFY( mMemory1DLayer->temporalProperties()->isActive() ); mMdal1DLayer->dataProvider()->addDataset( mDataDir + "/lines_vertex_scalar.dat" ); mMdal1DLayer->dataProvider()->addDataset( mDataDir + "/lines_vertex_vector.dat" ); QCOMPARE( mMdal1DLayer->dataProvider()->extraDatasets().count(), 2 ); + QVERIFY( mMemory1DLayer->dataProvider()->temporalCapabilities()->hasTemporalCapabilities() ); + QVERIFY( mMemory1DLayer->temporalProperties()->isActive() ); + //The face dataset is recognized by "_els_" in the filename for this format mMdal1DLayer->dataProvider()->addDataset( mDataDir + "/lines_els_scalar.dat" ); @@ -159,6 +177,15 @@ void TestQgsMeshLayer::initTestCase() QVERIFY( mMdal1DLayer->isValid() ); QgsProject::instance()->addMapLayers( QList() << mMdal1DLayer ); + + + // MDAL 3D Layer + uri = QString( mDataDir + "/trap_steady_05_3D.nc" ); + mMdal3DLayer = new QgsMeshLayer( uri, "Lines MDAL", "mdal" ); + + QVERIFY( mMdal3DLayer->isValid() ); + QgsProject::instance()->addMapLayers( + QList() << mMdal3DLayer ); } void TestQgsMeshLayer::cleanupTestCase() @@ -685,69 +712,6 @@ void TestQgsMeshLayer::test_write_read_project() QVERIFY( layers.size() == 2 ); } -void TestQgsMeshLayer::test_time_format_data() -{ - QTest::addColumn( "settings" ); - QTest::addColumn( "hours" ); - QTest::addColumn( "expectedTime" ); - - QTest::newRow( "rel1" ) << QgsMeshTimeSettings( 0, "hh:mm:ss.zzz" ) << 0.0 << QString( "00:00:00.000" ); - QTest::newRow( "rel2" ) << QgsMeshTimeSettings( 0, "hh:mm:ss" ) << 0.0 << QString( "00:00:00" ); - QTest::newRow( "rel3" ) << QgsMeshTimeSettings( 0, "d hh:mm:ss" ) << 0.0 << QString( "0 d 00:00:00" ); - QTest::newRow( "rel4" ) << QgsMeshTimeSettings( 0, "d hh" ) << 0.0 << QString( "0 d 0" ); - QTest::newRow( "rel5" ) << QgsMeshTimeSettings( 0, "d" ) << 0.0 << QString( "0" ); - QTest::newRow( "rel6" ) << QgsMeshTimeSettings( 0, "hh" ) << 0.0 << QString( "0" ); - QTest::newRow( "rel7" ) << QgsMeshTimeSettings( 0, "ss" ) << 0.0 << QString( "0" ); - QTest::newRow( "rel8" ) << QgsMeshTimeSettings( 0, "some-invalid-format" ) << 0.0 << QString( "0" ); - - QTest::newRow( "rel9" ) << QgsMeshTimeSettings( 100.11111, "hh:mm:ss.zzz" ) << 0.0 << QString( "100:06:39.996" ); - QTest::newRow( "rel10" ) << QgsMeshTimeSettings( 0, "hh:mm:ss.zzz" ) << 100.11111 << QString( "100:06:39.996" ); - QTest::newRow( "rel11" ) << QgsMeshTimeSettings( 0, "hh:mm:ss" ) << 100.11111 << QString( "100:06:39" ); - QTest::newRow( "rel12" ) << QgsMeshTimeSettings( 0, "d hh:mm:ss" ) << 100.11111 << QString( "4 d 04:06:39" ); - QTest::newRow( "rel13" ) << QgsMeshTimeSettings( 0, "d hh" ) << 100.11111 << QString( "4 d 4" ); - QTest::newRow( "rel14" ) << QgsMeshTimeSettings( 0, "d" ) << 100.11111 << QString( "4" ); - QTest::newRow( "rel15" ) << QgsMeshTimeSettings( 0, "hh" ) << 100.11111 << QString( "100.111" ); - QTest::newRow( "rel16" ) << QgsMeshTimeSettings( 0, "ss" ) << 100.11111 << QString( "360399" ); - QTest::newRow( "rel17" ) << QgsMeshTimeSettings( 0, "some-invalid-format" ) << 100.11111 << QString( "100.111" ); - - QDateTime dt = QDateTime::fromString( "2019-03-21 11:01:02", "yyyy-MM-dd HH:mm:ss" ); - QTest::newRow( "abs1" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy hh:mm:ss" ) << 0.0 << QString( "21.03.2019 11:01:02" ); - QTest::newRow( "abs2" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy hh:mm" ) << 0.0 << QString( "21.03.2019 11:01" ); - QTest::newRow( "abs3" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy hh" ) << 0.0 << QString( "21.03.2019 11" ); - QTest::newRow( "abs4" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy" ) << 0.0 << QString( "21.03.2019" ); - QTest::newRow( "abs5" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy hh:mm:ss" ) << 0.0 << QString( "21/03/2019 11:01:02" ); - QTest::newRow( "abs6" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy hh:mm" ) << 0.0 << QString( "21/03/2019 11:01" ); - QTest::newRow( "abs7" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy hh" ) << 0.0 << QString( "21/03/2019 11" ); - QTest::newRow( "abs8" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy" ) << 0.0 << QString( "21/03/2019" ); - QTest::newRow( "abs9" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy hh:mm:ss" ) << 0.0 << QString( "03/21/2019 11:01:02" ); - QTest::newRow( "abs10" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy hh:mm" ) << 0.0 << QString( "03/21/2019 11:01" ); - QTest::newRow( "abs11" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy hh" ) << 0.0 << QString( "03/21/2019 11" ); - QTest::newRow( "abs12" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy" ) << 0.0 << QString( "03/21/2019" ); - - QTest::newRow( "abs13" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy hh:mm:ss" ) << 100.11111 << QString( "25.03.2019 15:07:41" ); - QTest::newRow( "abs14" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy hh:mm" ) << 100.11111 << QString( "25.03.2019 15:07" ); - QTest::newRow( "abs15" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy hh" ) << 100.11111 << QString( "25.03.2019 15" ); - QTest::newRow( "abs16" ) << QgsMeshTimeSettings( dt, "dd.MM.yyyy" ) << 100.11111 << QString( "25.03.2019" ); - QTest::newRow( "abs17" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy hh:mm:ss" ) << 100.11111 << QString( "25/03/2019 15:07:41" ); - QTest::newRow( "abs18" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy hh:mm" ) << 100.11111 << QString( "25/03/2019 15:07" ); - QTest::newRow( "abs19" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy hh" ) << 100.11111 << QString( "25/03/2019 15" ); - QTest::newRow( "abs20" ) << QgsMeshTimeSettings( dt, "dd/MM/yyyy" ) << 100.11111 << QString( "25/03/2019" ); - QTest::newRow( "abs21" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy hh:mm:ss" ) << 100.11111 << QString( "03/25/2019 15:07:41" ); - QTest::newRow( "abs22" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy hh:mm" ) << 100.11111 << QString( "03/25/2019 15:07" ); - QTest::newRow( "abs23" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy hh" ) << 100.11111 << QString( "03/25/2019 15" ); - QTest::newRow( "abs24" ) << QgsMeshTimeSettings( dt, "MM/dd/yyyy" ) << 100.11111 << QString( "03/25/2019" ); -} - -void TestQgsMeshLayer::test_time_format() -{ - QFETCH( QgsMeshTimeSettings, settings ); - QFETCH( double, hours ); - QFETCH( QString, expectedTime ); - - QString time = QgsMeshLayerUtils::formatTime( hours, settings ); - QCOMPARE( time, expectedTime ); -} - void TestQgsMeshLayer::test_reload() { //init file for the test @@ -920,12 +884,9 @@ void TestQgsMeshLayer::test_reload_extra_dataset() void TestQgsMeshLayer::test_mesh_simplification() { - // Init files for the test - QgsMeshLayer layer( mDataDir + "/trap_steady_05_3D.nc", "MDAL layer", "mdal" ); - QgsCoordinateTransform invalidTransform; - layer.updateTriangularMesh( invalidTransform ); - QgsTriangularMesh *baseMesh = layer.triangularMesh(); + mMdal3DLayer->updateTriangularMesh( invalidTransform ); + QgsTriangularMesh *baseMesh = mMdal3DLayer->triangularMesh(); QCOMPARE( baseMesh->triangles().count(), 640 ); @@ -947,5 +908,67 @@ void TestQgsMeshLayer::test_mesh_simplification() delete m; } +void TestQgsMeshLayer::test_temporal() +{ + qint64 relativeTime_0 = -1000; + qint64 relativeTime_1 = 0; + qint64 relativeTime_2 = 1000 * 60 * 60 * 0.3; + qint64 relativeTime_3 = 1000 * 60 * 60 * 0.5; + qint64 relativeTime_4 = 1000 * 60 * 60 * 1.5; + qint64 relativeTime_5 = 1000 * 60 * 60 * 2; + // Mesh memory provider + QgsMeshDataProviderTemporalCapabilities *tempCap = mMemoryLayer->dataProvider()->temporalCapabilities(); + // Static dataset + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_0, relativeTime_1 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_1, relativeTime_2 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_2, relativeTime_3 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_3, relativeTime_4 ).dataset(), 0 ); + // Temporal dataset + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_0, relativeTime_1 ).dataset(), -1 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_1, relativeTime_2 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_2, relativeTime_3 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_3, relativeTime_4 ).dataset(), 1 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_4, relativeTime_5 ).dataset(), -1 ); + + // Mesh MDAL provider with internal dataset + tempCap = mMdalLayer->dataProvider()->temporalCapabilities(); + // Static dataset + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_0, relativeTime_1 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_1, relativeTime_2 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_2, relativeTime_3 ).dataset(), 0 ); + // Temporal dataset + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_0, relativeTime_1 ).dataset(), -1 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_1, relativeTime_2 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_2, relativeTime_3 ).dataset(), 0 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_3, relativeTime_4 ).dataset(), 1 ); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 1, relativeTime_4, relativeTime_5 ).dataset(), -1 ); + + //Mesh MDAL provider with reference time + tempCap = mMdal3DLayer->dataProvider()->temporalCapabilities(); + QCOMPARE( tempCap->datasetIndexFromRelativeTimeRange( 0, relativeTime_0, relativeTime_1 ).dataset(), 0 ); + QDateTime begin = QDateTime( QDate( 1990, 1, 1 ), QTime( 0, 0, 0 ), Qt::UTC ); + QDateTime end = QDateTime( QDate( 1990, 1, 1 ), QTime( 6, 0, 1, 938 ), Qt::UTC ); + QCOMPARE( tempCap->timeExtent(), QgsDateTimeRange( begin, end ) ); + + QDateTime time_1 = QDateTime( QDate( 1990, 1, 1 ), QTime( 3, 0, 0 ), Qt::UTC ); + QDateTime time_2 = time_1.addSecs( 300 ); + QgsMeshRendererSettings settings = mMdal3DLayer->rendererSettings(); + // Static dataset (bed elevation) + settings.setActiveScalarDatasetGroup( 0 ); //static dataset (bed elevation) + mMdal3DLayer->setRendererSettings( settings ); + QCOMPARE( mMdal3DLayer->activeScalarDatasetAtTime( QgsDateTimeRange( time_1, time_2 ) ).dataset(), 0 ); + // Attempt to next dataset + QCOMPARE( mMdal3DLayer->activeScalarDatasetAtTime( QgsDateTimeRange( time_1.addSecs( 400 ), time_2.addSecs( 400 ) ) ).dataset(), 0 ); + + // Temporal dataset + settings.setActiveScalarDatasetGroup( 1 ); + mMdal3DLayer->setRendererSettings( settings ); + QCOMPARE( mMdal3DLayer->activeScalarDatasetAtTime( QgsDateTimeRange( time_1, time_2 ) ).dataset(), 18 ); + // Next dataset + QCOMPARE( mMdal3DLayer->activeScalarDatasetAtTime( QgsDateTimeRange( time_1.addSecs( 400 ), time_2.addSecs( 400 ) ) ).dataset(), 19 ); + + mMdal3DLayer->temporalProperties(); +} + QGSTEST_MAIN( TestQgsMeshLayer ) #include "testqgsmeshlayer.moc" diff --git a/tests/src/core/testqgsmeshlayerrenderer.cpp b/tests/src/core/testqgsmeshlayerrenderer.cpp index 97f3a914a79e..21183b4a219f 100644 --- a/tests/src/core/testqgsmeshlayerrenderer.cpp +++ b/tests/src/core/testqgsmeshlayerrenderer.cpp @@ -79,6 +79,7 @@ class TestQgsMeshRenderer : public QObject void test_vertex_scalar_dataset_rendering(); void test_vertex_vector_dataset_rendering(); + void test_vertex_vector_dataset_colorRamp_rendering(); void test_face_scalar_dataset_rendering(); void test_face_scalar_dataset_interpolated_neighbour_average_rendering(); void test_face_vector_dataset_rendering(); @@ -87,7 +88,9 @@ class TestQgsMeshRenderer : public QObject void test_face_vector_on_user_grid_streamlines(); void test_vertex_vector_on_user_grid(); void test_vertex_vector_on_user_grid_streamlines(); + void test_vertex_vector_on_user_grid_streamlines_colorRamp(); void test_vertex_vector_traces(); + void test_vertex_vector_traces_colorRamp(); void test_stacked_3d_mesh_single_level_averaging(); void test_simplified_triangular_mesh_rendering(); @@ -97,40 +100,44 @@ class TestQgsMeshRenderer : public QObject void TestQgsMeshRenderer::init() { QgsMeshRendererSettings rendererSettings = mMemory1DLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset(); - rendererSettings.setActiveVectorDataset(); rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setEdgeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setAveragingMethod( nullptr ); mMemory1DLayer->setRendererSettings( rendererSettings ); + mMemory1DLayer->temporalProperties()->setIsActive( false ); + mMemory1DLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex() ); + mMemory1DLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex() ); rendererSettings = mMemoryLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset(); - rendererSettings.setActiveVectorDataset(); rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setEdgeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setAveragingMethod( nullptr ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->temporalProperties()->setIsActive( false ); + mMemoryLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex() ); + mMemoryLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex() ); rendererSettings = mMdalLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset(); - rendererSettings.setActiveVectorDataset(); rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setEdgeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setAveragingMethod( nullptr ); mMdalLayer->setRendererSettings( rendererSettings ); + mMdalLayer->temporalProperties()->setIsActive( false ); + mMdalLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex() ); + mMdalLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex() ); rendererSettings = mMdal3DLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset(); - rendererSettings.setActiveVectorDataset(); rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setEdgeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setAveragingMethod( nullptr ); mMdal3DLayer->setRendererSettings( rendererSettings ); + mMdal3DLayer->temporalProperties()->setIsActive( false ); + mMdal3DLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex() ); + mMdal3DLayer->setStaticVectorDatasetIndex( QgsMeshDatasetIndex() ); } void TestQgsMeshRenderer::initTestCase() @@ -185,6 +192,14 @@ void TestQgsMeshRenderer::initTestCase() QCOMPARE( lst.count(), 52 ); QCOMPARE( lst.at( 0 ).value, 1. ); // min group value QCOMPARE( lst.at( lst.count() - 1 ).value, 4. ); // max group value + + ds = QgsMeshDatasetIndex( 1, 0 ); + QgsMeshRendererVectorSettings vectorSettings = mMemoryLayer->rendererSettings().vectorSettings( ds.group() ); + shader = vectorSettings.colorRampShader(); + lst = shader.colorRampItemList(); + QCOMPARE( lst.count(), 52 ); + QVERIFY( fabs( lst.at( 0 ).value - 1.41421356237 ) < 0.000001 ); // min group value + QCOMPARE( lst.at( lst.count() - 1 ).value, 5. ); // max group value } void TestQgsMeshRenderer::cleanupTestCase() @@ -268,8 +283,8 @@ void TestQgsMeshRenderer::test_1d_vertex_scalar_dataset_rendering() QVERIFY( metadata.name() == "VertexScalarDataset" ); QgsMeshRendererSettings rendererSettings = mMemory1DLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( ds ); mMemory1DLayer->setRendererSettings( rendererSettings ); + mMemory1DLayer->setStaticScalarDatasetIndex( ds ); QVERIFY( imageCheck( "lines_vertex_scalar_dataset", mMemory1DLayer ) ); } @@ -286,8 +301,8 @@ void TestQgsMeshRenderer::test_1d_vertex_vector_dataset_rendering() arrowSettings.setMinShaftLength( 15 ); settings.setArrowsSettings( arrowSettings ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemory1DLayer->setRendererSettings( rendererSettings ); + mMemory1DLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "lines_vertex_vector_dataset", mMemory1DLayer ) ); } @@ -299,8 +314,8 @@ void TestQgsMeshRenderer::test_1d_face_scalar_dataset_rendering() QVERIFY( metadata.name() == "EdgeScalarDataset" ); QgsMeshRendererSettings rendererSettings = mMemory1DLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( ds ); mMemory1DLayer->setRendererSettings( rendererSettings ); + mMemory1DLayer->setStaticScalarDatasetIndex( ds ); QVERIFY( imageCheck( "lines_edge_scalar_dataset", mMemory1DLayer ) ); } @@ -312,8 +327,8 @@ void TestQgsMeshRenderer::test_1d_face_vector_dataset_rendering() QVERIFY( metadata.name() == "EdgeVectorDataset" ); QgsMeshRendererSettings rendererSettings = mMemory1DLayer->rendererSettings(); - rendererSettings.setActiveVectorDataset( ds ); mMemory1DLayer->setRendererSettings( rendererSettings ); + mMemory1DLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "lines_edge_vector_dataset", mMemory1DLayer ) ); } @@ -325,8 +340,8 @@ void TestQgsMeshRenderer::test_vertex_scalar_dataset_rendering() QVERIFY( metadata.name() == "VertexScalarDataset" ); QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticScalarDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset", mMemoryLayer ) ); } @@ -343,12 +358,31 @@ void TestQgsMeshRenderer::test_vertex_vector_dataset_rendering() arrowSettings.setMinShaftLength( 15 ); settings.setArrowsSettings( arrowSettings ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset", mMemoryLayer ) ); } +void TestQgsMeshRenderer::test_vertex_vector_dataset_colorRamp_rendering() +{ + QgsMeshDatasetIndex ds( 1, 0 ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); + const QgsMeshDatasetGroupMetadata metadata = mMemoryLayer->dataProvider()->datasetGroupMetadata( ds ); + QVERIFY( metadata.name() == "VertexVectorDataset" ); + + QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); + QgsMeshRendererVectorSettings settings = rendererSettings.vectorSettings( ds.group() ); + QgsMeshRendererVectorArrowSettings arrowSettings = settings.arrowSettings(); + arrowSettings.setMinShaftLength( 15 ); + settings.setColoringMethod( QgsMeshRendererVectorSettings::ColorRamp ); + settings.setArrowsSettings( arrowSettings ); + rendererSettings.setVectorSettings( ds.group(), settings ); + mMemoryLayer->setRendererSettings( rendererSettings ); + + QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_dataset_colorRamp", mMemoryLayer ) ); +} + void TestQgsMeshRenderer::test_face_scalar_dataset_rendering() { QgsMeshDatasetIndex ds( 2, 0 ); @@ -356,8 +390,8 @@ void TestQgsMeshRenderer::test_face_scalar_dataset_rendering() QVERIFY( metadata.name() == "FaceScalarDataset" ); QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticScalarDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_face_scalar_dataset", mMemoryLayer ) ); } @@ -369,11 +403,11 @@ void TestQgsMeshRenderer::test_face_scalar_dataset_interpolated_neighbour_averag QVERIFY( metadata.name() == "FaceScalarDataset" ); QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( ds ); auto scalarRendererSettings = rendererSettings.scalarSettings( 2 ); - scalarRendererSettings.setDataInterpolationMethod( QgsMeshRendererScalarSettings::NeighbourAverage ); + scalarRendererSettings.setDataResamplingMethod( QgsMeshRendererScalarSettings::NeighbourAverage ); rendererSettings.setScalarSettings( 2, scalarRendererSettings ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticScalarDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_face_scalar_interpolated_neighbour_average_dataset", mMemoryLayer ) ); } @@ -386,8 +420,8 @@ void TestQgsMeshRenderer::test_face_vector_dataset_rendering() QVERIFY( metadata.name() == "FaceVectorDataset" ); QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_face_vector_dataset", mMemoryLayer ) ); } @@ -399,8 +433,8 @@ void TestQgsMeshRenderer::test_vertex_scalar_dataset_with_inactive_face_renderin QVERIFY( metadata.name() == "VertexScalarDatasetWithInactiveFace1" ); QgsMeshRendererSettings rendererSettings = mMdalLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( ds ); mMdalLayer->setRendererSettings( rendererSettings ); + mMdalLayer->setStaticScalarDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_vertex_scalar_dataset_with_inactive_face", mMdalLayer ) ); } @@ -419,8 +453,8 @@ void TestQgsMeshRenderer::test_face_vector_on_user_grid() settings.setLineWidth( 0.8 ); settings.setSymbology( QgsMeshRendererVectorSettings::Arrows ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_face_vector_user_grid_dataset", mMemoryLayer ) ); } @@ -439,8 +473,8 @@ void TestQgsMeshRenderer::test_face_vector_on_user_grid_streamlines() settings.setLineWidth( 0.8 ); settings.setSymbology( QgsMeshRendererVectorSettings::Streamlines ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_face_vector_user_grid_dataset_streamlines", mMemoryLayer ) ); } @@ -458,9 +492,10 @@ void TestQgsMeshRenderer::test_vertex_vector_on_user_grid() settings.setUserGridCellHeight( 40 ); settings.setLineWidth( 0.9 ); settings.setSymbology( QgsMeshRendererVectorSettings::Arrows ); + settings.setColoringMethod( QgsMeshRendererVectorSettings::SingleColor ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset", mMemoryLayer ) ); } @@ -477,14 +512,36 @@ void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_streamlines() settings.setUserGridCellWidth( 60 ); settings.setUserGridCellHeight( 40 ); settings.setLineWidth( 0.9 ); + settings.setColoringMethod( QgsMeshRendererVectorSettings::SingleColor ); settings.setSymbology( QgsMeshRendererVectorSettings::Streamlines ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines", mMemoryLayer ) ); } +void TestQgsMeshRenderer::test_vertex_vector_on_user_grid_streamlines_colorRamp() +{ + QgsMeshDatasetIndex ds( 1, 0 ); + const QgsMeshDatasetGroupMetadata metadata = mMemoryLayer->dataProvider()->datasetGroupMetadata( ds ); + QVERIFY( metadata.name() == "VertexVectorDataset" ); + + QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); + QgsMeshRendererVectorSettings settings = rendererSettings.vectorSettings( ds.group() ); + settings.setOnUserDefinedGrid( true ); + settings.setUserGridCellWidth( 60 ); + settings.setUserGridCellHeight( 40 ); + settings.setLineWidth( 0.9 ); + settings.setColoringMethod( QgsMeshRendererVectorSettings::ColorRamp ); + settings.setSymbology( QgsMeshRendererVectorSettings::Streamlines ); + rendererSettings.setVectorSettings( ds.group(), settings ); + mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); + + QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp", mMemoryLayer ) ); +} + void TestQgsMeshRenderer::test_vertex_vector_traces() { QgsMeshDatasetIndex ds( 1, 0 ); @@ -497,6 +554,7 @@ void TestQgsMeshRenderer::test_vertex_vector_traces() settings.setUserGridCellWidth( 60 ); settings.setUserGridCellHeight( 40 ); settings.setLineWidth( 1 ); + settings.setColoringMethod( QgsMeshRendererVectorSettings::SingleColor ); settings.setSymbology( QgsMeshRendererVectorSettings::Traces ); QgsMeshRendererVectorTracesSettings tracesSetting = settings.tracesSettings(); @@ -505,12 +563,39 @@ void TestQgsMeshRenderer::test_vertex_vector_traces() tracesSetting.setMaximumTailLengthUnit( QgsUnitTypes::RenderPixels ); settings.setTracesSettings( tracesSetting ); rendererSettings.setVectorSettings( ds.group(), settings ); - rendererSettings.setActiveVectorDataset( ds ); mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_traces", mMemoryLayer ) ); } +void TestQgsMeshRenderer::test_vertex_vector_traces_colorRamp() +{ + QgsMeshDatasetIndex ds( 1, 0 ); + const QgsMeshDatasetGroupMetadata metadata = mMemoryLayer->dataProvider()->datasetGroupMetadata( ds ); + QVERIFY( metadata.name() == "VertexVectorDataset" ); + + QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); + QgsMeshRendererVectorSettings settings = rendererSettings.vectorSettings( ds.group() ); + settings.setOnUserDefinedGrid( true ); + settings.setUserGridCellWidth( 60 ); + settings.setUserGridCellHeight( 40 ); + settings.setLineWidth( 1 ); + settings.setColoringMethod( QgsMeshRendererVectorSettings::ColorRamp ); + + settings.setSymbology( QgsMeshRendererVectorSettings::Traces ); + QgsMeshRendererVectorTracesSettings tracesSetting = settings.tracesSettings(); + tracesSetting.setParticlesCount( -1 ); + tracesSetting.setMaximumTailLength( 40 ); + tracesSetting.setMaximumTailLengthUnit( QgsUnitTypes::RenderPixels ); + settings.setTracesSettings( tracesSetting ); + rendererSettings.setVectorSettings( ds.group(), settings ); + mMemoryLayer->setRendererSettings( rendererSettings ); + mMemoryLayer->setStaticVectorDatasetIndex( ds ); + + QVERIFY( imageCheck( "quad_and_triangle_vertex_vector_traces_colorRamp", mMemoryLayer ) ); +} + void TestQgsMeshRenderer::test_signals() { QSignalSpy spy1( mMemoryLayer, &QgsMapLayer::rendererChanged ); @@ -518,7 +603,7 @@ void TestQgsMeshRenderer::test_signals() QSignalSpy spy3( mMemoryLayer, &QgsMapLayer::legendChanged ); QgsMeshRendererSettings rendererSettings = mMemoryLayer->rendererSettings(); - rendererSettings.setActiveScalarDataset( QgsMeshDatasetIndex( 1, 0 ) ); + mMemoryLayer->setStaticScalarDatasetIndex( QgsMeshDatasetIndex( 1, 0 ) ); mMemoryLayer->setRendererSettings( rendererSettings ); QCOMPARE( spy1.count(), 1 ); @@ -528,13 +613,16 @@ void TestQgsMeshRenderer::test_signals() void TestQgsMeshRenderer::test_stacked_3d_mesh_single_level_averaging() { + QgsMeshDatasetIndex ds( 1, 3 ); + mMdal3DLayer->setStaticScalarDatasetIndex( ds ); QgsMeshRendererSettings rendererSettings = mMdal3DLayer->rendererSettings(); // we want to set active scalar dataset one defined on 3d mesh - QgsMeshDatasetIndex ds( 1, 3 ); QgsMeshDatasetGroupMetadata metadata = mMdal3DLayer->dataProvider()->datasetGroupMetadata( ds ); QVERIFY( metadata.name() == "temperature" ); QVERIFY( metadata.maximumVerticalLevelsCount() == 10 ); - rendererSettings.setActiveScalarDataset( ds ); + QgsMeshRendererScalarSettings scalarSettings = rendererSettings.scalarSettings( ds.group() ); + scalarSettings.setDataResamplingMethod( QgsMeshRendererScalarSettings::None ); + rendererSettings.setScalarSettings( ds.group(), scalarSettings ); // want to set active vector dataset one defined on 3d mesh ds = QgsMeshDatasetIndex( 6, 3 ); metadata = mMdal3DLayer->dataProvider()->datasetGroupMetadata( ds ); @@ -550,13 +638,13 @@ void TestQgsMeshRenderer::test_stacked_3d_mesh_single_level_averaging() vectorSettings.setLineWidth( 1 ); vectorSettings.setArrowsSettings( arrowSettings ); rendererSettings.setVectorSettings( ds.group(), vectorSettings ); - rendererSettings.setActiveVectorDataset( ds ); // switch off mesh renderings rendererSettings.setNativeMeshSettings( QgsMeshRendererMeshSettings() ); rendererSettings.setTriangularMeshSettings( QgsMeshRendererMeshSettings() ); std::unique_ptr method( new QgsMeshMultiLevelsAveragingMethod( 1, true ) ); rendererSettings.setAveragingMethod( method.get() ); mMdal3DLayer->setRendererSettings( rendererSettings ); + mMdal3DLayer->setStaticVectorDatasetIndex( ds ); QVERIFY( imageCheck( "stacked_3d_mesh_single_level_averaging", mMdal3DLayer ) ); } diff --git a/tests/src/core/testqgsogrprovider.cpp b/tests/src/core/testqgsogrprovider.cpp index 86d6dd78455a..ed147bb41254 100644 --- a/tests/src/core/testqgsogrprovider.cpp +++ b/tests/src/core/testqgsogrprovider.cpp @@ -100,7 +100,7 @@ void TestQgsOgrProvider::setupProxy() settings.setValue( QStringLiteral( "proxy/proxyPassword" ), QStringLiteral( "password" ) ); settings.setValue( QStringLiteral( "proxy/proxyExcludedUrls" ), QStringLiteral( "http://www.myhost.com|http://www.myotherhost.com" ) ); QgsNetworkAccessManager::instance()->setupDefaultProxyAndCache(); - QgsVectorLayer vl( mTestDataDir + '/' + QStringLiteral( "lines.shp" ), QStringLiteral( "proxy_test" ), QLatin1Literal( "ogr" ) ); + QgsVectorLayer vl( mTestDataDir + '/' + QStringLiteral( "lines.shp" ), QStringLiteral( "proxy_test" ), QLatin1String( "ogr" ) ); QVERIFY( vl.isValid() ); const char *proxyConfig = CPLGetConfigOption( "GDAL_HTTP_PROXY", nullptr ); QCOMPARE( proxyConfig, "myproxyhostname.com:1234" ); @@ -116,7 +116,7 @@ void TestQgsOgrProvider::setupProxy() settings.setValue( QStringLiteral( "proxy/proxyUser" ), QStringLiteral( "username" ) ); settings.remove( QStringLiteral( "proxy/proxyPassword" ) ); QgsNetworkAccessManager::instance()->setupDefaultProxyAndCache(); - QgsVectorLayer vl( mTestDataDir + '/' + QStringLiteral( "lines.shp" ), QStringLiteral( "proxy_test" ), QLatin1Literal( "ogr" ) ); + QgsVectorLayer vl( mTestDataDir + '/' + QStringLiteral( "lines.shp" ), QStringLiteral( "proxy_test" ), QLatin1String( "ogr" ) ); QVERIFY( vl.isValid() ); const char *proxyConfig = CPLGetConfigOption( "GDAL_HTTP_PROXY", nullptr ); QCOMPARE( proxyConfig, "myproxyhostname.com" ); @@ -167,7 +167,7 @@ class ReadVectorLayer : public QThread void run() override { - QgsVectorLayer *vl2 = new QgsVectorLayer( _filePath, QStringLiteral( "thread_test" ), QLatin1Literal( "ogr" ) ); + QgsVectorLayer *vl2 = new QgsVectorLayer( _filePath, QStringLiteral( "thread_test" ), QLatin1String( "ogr" ) ); QgsFeature f; QVERIFY( vl2->getFeatures().nextFeature( f ) ); diff --git a/tests/src/core/testqgsproject.cpp b/tests/src/core/testqgsproject.cpp index 8684ffc01f9a..b0aa01a52863 100644 --- a/tests/src/core/testqgsproject.cpp +++ b/tests/src/core/testqgsproject.cpp @@ -49,6 +49,7 @@ class TestQgsProject : public QObject void testSetGetCrs(); void testEmbeddedLayerGroupFromQgz(); void projectSaveUser(); + void testCrsExpressions(); }; void TestQgsProject::init() @@ -605,6 +606,51 @@ void TestQgsProject::testSetGetCrs() ellipsoidChangedSpy.clear(); } +void TestQgsProject::testCrsExpressions() +{ + QgsProject p; + QVariant r; + + p.setCrs( QgsCoordinateReferenceSystem::fromEpsgId( 4326 ) ); + + QgsExpressionContext c = p.createExpressionContext(); + + QgsExpression e2( QStringLiteral( "@project_crs" ) ); + r = e2.evaluate( &c ); + QCOMPARE( r.toString(), QString( "EPSG:4326" ) ); + + QgsExpression e3( QStringLiteral( "@project_crs_definition" ) ); + r = e3.evaluate( &c ); + QCOMPARE( r.toString(), QString( "+proj=longlat +datum=WGS84 +no_defs" ) ); + + QgsExpression e4( QStringLiteral( "@project_units" ) ); + r = e4.evaluate( &c ); + QCOMPARE( r.toString(), QString( "degrees" ) ); + + QgsExpression e5( QStringLiteral( "@project_crs_description" ) ); + r = e5.evaluate( &c ); + QCOMPARE( r.toString(), QString( "WGS 84" ) ); + + QgsExpression e6( QStringLiteral( "@project_crs_acronym" ) ); + r = e6.evaluate( &c ); + QCOMPARE( r.toString(), QString( "longlat" ) ); + + QgsExpression e7( QStringLiteral( "@project_crs_proj4" ) ); + r = e7.evaluate( &c ); + QCOMPARE( r.toString(), QString( "+proj=longlat +datum=WGS84 +no_defs" ) ); + + QgsExpression e8( QStringLiteral( "@project_crs_wkt" ) ); + r = e8.evaluate( &c ); + QVERIFY( r.toString().length() >= 15 ); + + QgsExpression e9( QStringLiteral( "@project_crs_ellipsoid" ) ); + r = e9.evaluate( &c ); +#if PROJ_VERSION_MAJOR>=6 + QCOMPARE( r.toString(), QString( "EPSG:7030" ) ); +#else + QCOMPARE( r.toString(), QString( "WGS84" ) ); +#endif +} QGSTEST_MAIN( TestQgsProject ) #include "testqgsproject.moc" diff --git a/tests/src/core/testqgsrastercontourrenderer.cpp b/tests/src/core/testqgsrastercontourrenderer.cpp new file mode 100644 index 000000000000..a486f091378f --- /dev/null +++ b/tests/src/core/testqgsrastercontourrenderer.cpp @@ -0,0 +1,115 @@ +/*************************************************************************** + testqgsrastercontourrenderer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgstest.h" +#include +#include + +//qgis includes... +#include "qgsapplication.h" +#include "qgsproject.h" +#include "qgsrenderchecker.h" +#include "qgsrasterlayer.h" +#include "qgsrastercontourrenderer.h" +#include "qgslinesymbollayer.h" + +/** + * \ingroup UnitTests + * This is a unit test for contour renderer + */ +class TestQgsRasterContourRenderer : public QObject +{ + Q_OBJECT + + public: + TestQgsRasterContourRenderer() = default; + + private: + QString mDataDir; + QgsRasterLayer *mLayer = nullptr; + QString mReport; + QgsMapSettings *mMapSettings = nullptr; + + bool imageCheck( const QString &testType, QgsRasterLayer *layer, QgsRectangle extent ); + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + + void test_render(); +}; + + +void TestQgsRasterContourRenderer::initTestCase() +{ + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + QgsApplication::showSettings(); + mDataDir = QString( TEST_DATA_DIR ); //defined in CmakeLists.txt + mDataDir += "/analysis"; + + mLayer = new QgsRasterLayer( mDataDir + "/dem.tif", "dem", "gdal" ); + + QgsProject::instance()->addMapLayer( mLayer ); + + mMapSettings = new QgsMapSettings(); + mMapSettings->setLayers( QList() << mLayer ); +} + +void TestQgsRasterContourRenderer::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +bool TestQgsRasterContourRenderer::imageCheck( const QString &testType, QgsRasterLayer *layer, QgsRectangle extent ) +{ + mReport += "

    " + testType + "

    \n"; + mMapSettings->setExtent( extent ); + mMapSettings->setDestinationCrs( layer->crs() ); + mMapSettings->setOutputDpi( 96 ); + QgsRenderChecker myChecker; + myChecker.setControlName( "expected_" + testType ); + myChecker.setMapSettings( *mMapSettings ); + myChecker.setColorTolerance( 15 ); + bool myResultFlag = myChecker.runTest( testType, 0 ); + mReport += myChecker.report(); + return myResultFlag; +} + +void TestQgsRasterContourRenderer::test_render() +{ + QgsSimpleLineSymbolLayer *slsl1 = new QgsSimpleLineSymbolLayer( Qt::black, 0.5 ); + QgsLineSymbol *contourSymbol = new QgsLineSymbol( QgsSymbolLayerList() << slsl1 ); + QgsSimpleLineSymbolLayer *slsl2 = new QgsSimpleLineSymbolLayer( Qt::black, 1 ); + QgsLineSymbol *contourIndexSymbol = new QgsLineSymbol( QgsSymbolLayerList() << slsl2 ); + + QgsRasterContourRenderer *renderer = new QgsRasterContourRenderer( mLayer->dataProvider() ); + renderer->setContourSymbol( contourSymbol ); + renderer->setContourInterval( 100 ); + renderer->setContourIndexSymbol( contourIndexSymbol ); + renderer->setContourIndexInterval( 500 ); + renderer->setDownscale( 10 ); + + mLayer->setRenderer( renderer ); + + QVERIFY( imageCheck( "raster_contours", mLayer, mLayer->extent().scaled( 0.5 ) ) ); +} + + +QGSTEST_MAIN( TestQgsRasterContourRenderer ) +#include "testqgsrastercontourrenderer.moc" diff --git a/tests/src/core/testqgsrasterdataprovidertemporalcapabilities.cpp b/tests/src/core/testqgsrasterdataprovidertemporalcapabilities.cpp index 80bddbe87f05..a153b1a18ff7 100644 --- a/tests/src/core/testqgsrasterdataprovidertemporalcapabilities.cpp +++ b/tests/src/core/testqgsrasterdataprovidertemporalcapabilities.cpp @@ -40,7 +40,6 @@ class TestQgsRasterDataProviderTemporalCapabilities : public QObject void checkActiveStatus(); void checkTemporalRange(); - void checkReferenceTemporalRange(); private: QgsRasterDataProviderTemporalCapabilities *temporalCapabilities = nullptr; @@ -100,7 +99,7 @@ void TestQgsRasterDataProviderTemporalCapabilities::checkTemporalRange() // Test setting out of fixed temporal range limits, should not update the temporal range. temporalCapabilities->setRequestedTemporalRange( outOfLimitsRange ); - QCOMPARE( temporalCapabilities->requestedTemporalRange(), dateTimeRange ); + QCOMPARE( temporalCapabilities->requestedTemporalRange(), outOfLimitsRange ); // Test if setting the requested temporal range with the fixed temporal range object, // will result in to setting the requested temporal range with the fixed temporal range. @@ -108,27 +107,5 @@ void TestQgsRasterDataProviderTemporalCapabilities::checkTemporalRange() QCOMPARE( temporalCapabilities->requestedTemporalRange(), fixedDateTimeRange ); } -void TestQgsRasterDataProviderTemporalCapabilities::checkReferenceTemporalRange() -{ - QgsDateTimeRange fixedDateTimeRange = QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), - QDateTime( QDate( 2020, 12, 31 ) ) ); - QgsDateTimeRange dateTimeRange = QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), - QDateTime( QDate( 2020, 3, 1 ) ) ); - QgsDateTimeRange outOfLimitsRange = QgsDateTimeRange( QDateTime( QDate( 2019, 1, 1 ) ), - QDateTime( QDate( 2021, 3, 1 ) ) ); - - temporalCapabilities->setAvailableReferenceTemporalRange( fixedDateTimeRange ); - temporalCapabilities->setRequestedReferenceTemporalRange( dateTimeRange ); - - QCOMPARE( temporalCapabilities->availableReferenceTemporalRange(), fixedDateTimeRange ); - QCOMPARE( temporalCapabilities->requestedReferenceTemporalRange(), dateTimeRange ); - - temporalCapabilities->setRequestedReferenceTemporalRange( outOfLimitsRange ); - QCOMPARE( temporalCapabilities->requestedReferenceTemporalRange(), dateTimeRange ); - - temporalCapabilities->setRequestedReferenceTemporalRange( fixedDateTimeRange ); - QCOMPARE( temporalCapabilities->requestedReferenceTemporalRange(), fixedDateTimeRange ); -} - QGSTEST_MAIN( TestQgsRasterDataProviderTemporalCapabilities ) #include "testqgsrasterdataprovidertemporalcapabilities.moc" diff --git a/tests/src/core/testqgsrasterfill.cpp b/tests/src/core/testqgsrasterfill.cpp index 125132c19a13..64add43d6676 100644 --- a/tests/src/core/testqgsrasterfill.cpp +++ b/tests/src/core/testqgsrasterfill.cpp @@ -21,7 +21,7 @@ #include #include -//qgis includes... +// qgis includes... #include #include #include @@ -31,7 +31,7 @@ #include #include #include -//qgis test includes +// qgis test includes #include "qgsmultirenderchecker.h" /** @@ -57,8 +57,15 @@ class TestQgsRasterFill : public QObject void offset(); void width(); + // Tests for percentage value of size unit. + void percentage(); + void percentageCoordinateMode(); + void percentageOffset(); + void percentageAlpha(); + void percentageWidth(); + private: - bool mTestHasError = false ; + bool mTestHasError = false; bool setQml( const QString &type ); bool imageCheck( const QString &type ); QgsMapSettings mMapSettings; @@ -112,8 +119,8 @@ void TestQgsRasterFill::initTestCase() // mMapSettings.setLayers( QList() << mpPolysLayer ); mReport += QLatin1String( "

    Raster Fill Renderer Tests

    \n" ); - } + void TestQgsRasterFill::cleanupTestCase() { QString myReportFile = QDir::tempPath() + "/qgistest.html"; @@ -183,6 +190,55 @@ void TestQgsRasterFill::width() QVERIFY( result ); } +void TestQgsRasterFill::percentage() +{ + mReport += QString( "

    Raster fill percentage (6.3 %)

    \n" ); + mRasterFill->setWidthUnit( QgsUnitTypes::RenderPercentage ); + mRasterFill->setWidth( 6.3 ); + bool result = imageCheck( QStringLiteral( "rasterfill_percentage" ) ); + QVERIFY( result ); +} + +void TestQgsRasterFill::percentageCoordinateMode() +{ + mReport += QLatin1String( "

    Raster fill percentage viewport mode

    \n" ); + mRasterFill->setWidthUnit( QgsUnitTypes::RenderPercentage ); + mRasterFill->setWidth( 6.3 ); + mRasterFill->setCoordinateMode( QgsRasterFillSymbolLayer::Viewport ); + bool result = imageCheck( QStringLiteral( "rasterfill_viewport_percentage" ) ); + QVERIFY( result ); +} + +void TestQgsRasterFill::percentageOffset() +{ + mReport += QLatin1String( "

    Raster fill percentage offset (12px; 15 px)

    \n" ); + mRasterFill->setWidthUnit( QgsUnitTypes::RenderPercentage ); + mRasterFill->setWidth( 6.3 ); + mRasterFill->setOffsetUnit( QgsUnitTypes::RenderPixels ); + mRasterFill->setOffset( QPointF( 12, 15 ) ); + bool result = imageCheck( QStringLiteral( "rasterfill_offset_percentage" ) ); + QVERIFY( result ); +} + +void TestQgsRasterFill::percentageAlpha() +{ + mReport += QLatin1String( "

    Raster fill percentage alpha (0.5)

    \n" ); + mRasterFill->setWidthUnit( QgsUnitTypes::RenderPercentage ); + mRasterFill->setWidth( 6.3 ); + mRasterFill->setOpacity( 0.5 ); + bool result = imageCheck( QStringLiteral( "rasterfill_alpha_percentage" ) ); + QVERIFY( result ); +} + +void TestQgsRasterFill::percentageWidth() +{ + mReport += QLatin1String( "

    Raster fill percentage width (3.3 %)

    \n" ); + mRasterFill->setWidthUnit( QgsUnitTypes::RenderPercentage ); + mRasterFill->setWidth( 3.3 ); + bool result = imageCheck( QStringLiteral( "rasterfill_width_percentage" ) ); + QVERIFY( result ); +} + // // Private helper functions not called directly by CTest // diff --git a/tests/src/core/testqgsrasterlayertemporalproperties.cpp b/tests/src/core/testqgsrasterlayertemporalproperties.cpp index 10b221984f84..3ca69ed0dcdc 100644 --- a/tests/src/core/testqgsrasterlayertemporalproperties.cpp +++ b/tests/src/core/testqgsrasterlayertemporalproperties.cpp @@ -39,11 +39,9 @@ class TestQgsRasterLayerTemporalProperties : public QObject void cleanup(); // will be called after every testfunction. void checkSettingTemporalRange(); - void testChangedSignal(); + void testReadWrite(); void testVisibleInTimeRange(); - private: - QgsRasterLayerTemporalProperties *temporalProperties = nullptr; }; void TestQgsRasterLayerTemporalProperties::initTestCase() @@ -60,9 +58,6 @@ void TestQgsRasterLayerTemporalProperties::initTestCase() void TestQgsRasterLayerTemporalProperties::init() { - // create a temporal property that will be used in all tests... - - temporalProperties = new QgsRasterLayerTemporalProperties(); } void TestQgsRasterLayerTemporalProperties::cleanup() @@ -76,26 +71,65 @@ void TestQgsRasterLayerTemporalProperties::cleanupTestCase() void TestQgsRasterLayerTemporalProperties::checkSettingTemporalRange() { + QgsRasterLayerTemporalProperties temporalProperties; QgsDateTimeRange dateTimeRange = QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), QDateTime( QDate( 2020, 12, 31 ) ) ); - temporalProperties->setFixedTemporalRange( dateTimeRange ); + temporalProperties.setFixedTemporalRange( dateTimeRange ); - QCOMPARE( temporalProperties->fixedTemporalRange(), dateTimeRange ); + QCOMPARE( temporalProperties.fixedTemporalRange(), dateTimeRange ); } -void TestQgsRasterLayerTemporalProperties::testChangedSignal() +void TestQgsRasterLayerTemporalProperties::testReadWrite() { - QCOMPARE( temporalProperties->temporalSource(), QgsMapLayerTemporalProperties::TemporalSource::Layer ); - QSignalSpy spy( temporalProperties, SIGNAL( changed() ) ); - - temporalProperties->setTemporalSource( QgsMapLayerTemporalProperties::TemporalSource::Layer ); - QCOMPARE( spy.count(), 0 ); - temporalProperties->setTemporalSource( QgsMapLayerTemporalProperties::TemporalSource::Project ); - QCOMPARE( spy.count(), 1 ); + QgsRasterLayerTemporalProperties temporalProperties; + + QDomImplementation DomImplementation; + QDomDocumentType documentType = + DomImplementation.createDocumentType( + QStringLiteral( "qgis" ), QStringLiteral( "http://mrcc.com/qgis.dtd" ), QStringLiteral( "SYSTEM" ) ); + QDomDocument doc( documentType ); + + QDomElement node = doc.createElement( QStringLiteral( "temp" ) ); + // read none existent node + temporalProperties.readXml( node.toElement(), QgsReadWriteContext() ); + + // must not be active! + QVERIFY( !temporalProperties.isActive() ); + + temporalProperties.setIsActive( true ); + temporalProperties.setMode( QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider ); + temporalProperties.setIntervalHandlingMethod( QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange ); + + temporalProperties.writeXml( node, doc, QgsReadWriteContext() ); + + QgsRasterLayerTemporalProperties temporalProperties2; + temporalProperties2.readXml( node, QgsReadWriteContext() ); + QVERIFY( temporalProperties2.isActive() ); + QCOMPARE( temporalProperties2.mode(), QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider ); + QCOMPARE( temporalProperties2.intervalHandlingMethod(), QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange ); + + temporalProperties.setIsActive( false ); + QDomElement node2 = doc.createElement( QStringLiteral( "temp" ) ); + temporalProperties.writeXml( node2, doc, QgsReadWriteContext() ); + QgsRasterLayerTemporalProperties temporalProperties3; + temporalProperties3.readXml( node2, QgsReadWriteContext() ); + QVERIFY( !temporalProperties3.isActive() ); + QCOMPARE( temporalProperties3.mode(), QgsRasterLayerTemporalProperties::ModeTemporalRangeFromDataProvider ); + QCOMPARE( temporalProperties3.intervalHandlingMethod(), QgsRasterDataProviderTemporalCapabilities::MatchExactUsingEndOfRange ); + + temporalProperties.setMode( QgsRasterLayerTemporalProperties::ModeFixedTemporalRange ); + temporalProperties.setFixedTemporalRange( QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), + QDateTime( QDate( 2020, 12, 31 ) ) ) ); + QDomElement node3 = doc.createElement( QStringLiteral( "temp" ) ); + temporalProperties.writeXml( node3, doc, QgsReadWriteContext() ); + QgsRasterLayerTemporalProperties temporalProperties4; + temporalProperties4.readXml( node3, QgsReadWriteContext() ); + QVERIFY( !temporalProperties4.isActive() ); + QCOMPARE( temporalProperties4.mode(), QgsRasterLayerTemporalProperties::ModeFixedTemporalRange ); + QCOMPARE( temporalProperties4.fixedTemporalRange(), QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), + QDateTime( QDate( 2020, 12, 31 ) ) ) ); - temporalProperties->setIsActive( true ); - QCOMPARE( spy.count(), 2 ); } void TestQgsRasterLayerTemporalProperties::testVisibleInTimeRange() diff --git a/tests/src/core/testqgsrastermarker.cpp b/tests/src/core/testqgsrastermarker.cpp index 0c93d0f20d18..29c8785b9f87 100644 --- a/tests/src/core/testqgsrastermarker.cpp +++ b/tests/src/core/testqgsrastermarker.cpp @@ -57,8 +57,16 @@ class TestQgsRasterMarker : public QObject void rotation(); void fixedAspectRatio(); + // Tests for percentage value of size unit. + void percentage(); + void percentageAnchor(); + void percentageAlpha(); + void percentageRotation(); + void percentageFixedAspectRatio(); + void percentageOffset(); + private: - bool mTestHasError = false ; + bool mTestHasError = false; bool imageCheck( const QString &type ); QgsMapSettings mMapSettings; @@ -105,7 +113,6 @@ void TestQgsRasterMarker::initTestCase() // and is more light weight mMapSettings.setLayers( QList() << mPointLayer ); mReport += QLatin1String( "

    Raster Marker Renderer Tests

    \n" ); - } void TestQgsRasterMarker::cleanupTestCase() @@ -143,12 +150,12 @@ void TestQgsRasterMarker::rasterMarkerSymbol() void TestQgsRasterMarker::anchor() { - mRasterMarker->setHorizontalAnchorPoint( QgsMarkerSymbolLayer::HorizontalAnchorPoint( 2 ) ); - mRasterMarker->setVerticalAnchorPoint( QgsMarkerSymbolLayer::VerticalAnchorPoint( 2 ) ); + mRasterMarker->setHorizontalAnchorPoint( QgsMarkerSymbolLayer::Right ); + mRasterMarker->setVerticalAnchorPoint( QgsMarkerSymbolLayer::Bottom ); bool result = imageCheck( QStringLiteral( "rastermarker_anchor" ) ); QVERIFY( result ); - mRasterMarker->setHorizontalAnchorPoint( QgsMarkerSymbolLayer::HorizontalAnchorPoint( 1 ) ); - mRasterMarker->setVerticalAnchorPoint( QgsMarkerSymbolLayer::VerticalAnchorPoint( 1 ) ); + mRasterMarker->setHorizontalAnchorPoint( QgsMarkerSymbolLayer::HCenter ); + mRasterMarker->setVerticalAnchorPoint( QgsMarkerSymbolLayer::VCenter ); } void TestQgsRasterMarker::alpha() @@ -175,6 +182,78 @@ void TestQgsRasterMarker::fixedAspectRatio() QVERIFY( result ); } +void TestQgsRasterMarker::percentage() +{ + mRasterMarker->setOffset( QPointF( 0, 0 ) ); + mRasterMarker->setAngle( 0.0 ); + mRasterMarker->setFixedAspectRatio( 0.0 ); + mRasterMarker->setOpacity( 1.0 ); + + mReport += QLatin1String( "

    Raster marker percentage (6.3 %)

    \n" ); + mRasterMarker->setSizeUnit( QgsUnitTypes::RenderPercentage ); + mRasterMarker->setSize( 6.3 ); + bool result = imageCheck( QStringLiteral( "rastermarker_percentage" ) ); + QVERIFY( result ); +} + +void TestQgsRasterMarker::percentageAnchor() +{ + mReport += QString( "

    Raster marker percentage anchor (Right; Bottom)

    \n" ); + mRasterMarker->setSizeUnit( QgsUnitTypes::RenderPercentage ); + mRasterMarker->setSize( 6.3 ); + mRasterMarker->setHorizontalAnchorPoint( QgsMarkerSymbolLayer::Right ); + mRasterMarker->setVerticalAnchorPoint( QgsMarkerSymbolLayer::Bottom ); + bool result = imageCheck( QStringLiteral( "rastermarker_anchor_percentage" ) ); + mRasterMarker->setHorizontalAnchorPoint( QgsMarkerSymbolLayer::HCenter ); + mRasterMarker->setVerticalAnchorPoint( QgsMarkerSymbolLayer::VCenter ); + QVERIFY( result ); +} + +void TestQgsRasterMarker::percentageAlpha() +{ + mReport += QString( "

    Raster marker percentage alpha (0.5)

    \n" ); + mRasterMarker->setSizeUnit( QgsUnitTypes::RenderPercentage ); + mRasterMarker->setSize( 6.3 ); + mRasterMarker->setOpacity( 0.5 ); + bool result = imageCheck( QStringLiteral( "rastermarker_alpha_percentage" ) ); + mRasterMarker->setOpacity( 1.0 ); + QVERIFY( result ); +} + +void TestQgsRasterMarker::percentageRotation() +{ + mReport += QString( "

    Raster marker percentage rotation (45.0)

    \n" ); + mRasterMarker->setSizeUnit( QgsUnitTypes::RenderPercentage ); + mRasterMarker->setSize( 6.3 ); + mRasterMarker->setAngle( 45.0 ); + bool result = imageCheck( QStringLiteral( "rastermarker_rotation_percentage" ) ); + mRasterMarker->setAngle( 0.0 ); + QVERIFY( result ); +} + +void TestQgsRasterMarker::percentageFixedAspectRatio() +{ + mReport += QString( "

    Raster marker percentage fixed aspect ratio (1.0)

    \n" ); + mRasterMarker->setSizeUnit( QgsUnitTypes::RenderPercentage ); + mRasterMarker->setSize( 6.3 ); + mRasterMarker->setFixedAspectRatio( 1.0 ); + bool result = imageCheck( QStringLiteral( "rastermarker_fixedaspectratio_percentage" ) ); + mRasterMarker->setFixedAspectRatio( 0.0 ); + QVERIFY( result ); +} + +void TestQgsRasterMarker::percentageOffset() +{ + mReport += QString( "

    Raster marker percentage offset (12 px; 15 px)

    \n" ); + mRasterMarker->setSizeUnit( QgsUnitTypes::RenderPercentage ); + mRasterMarker->setSize( 6.3 ); + mRasterMarker->setOffsetUnit( QgsUnitTypes::RenderPixels ); + mRasterMarker->setOffset( QPointF( 12, 15 ) ); + bool result = imageCheck( QStringLiteral( "rastermarker_offset_percentage" ) ); + mRasterMarker->setOffset( QPointF( 0, 0 ) ); + QVERIFY( result ); +} + // // Private helper functions not called directly by CTest // diff --git a/tests/src/core/testqgssnappingutils.cpp b/tests/src/core/testqgssnappingutils.cpp index e07a1808bc74..3d7fdc308e71 100644 --- a/tests/src/core/testqgssnappingutils.cpp +++ b/tests/src/core/testqgssnappingutils.cpp @@ -250,7 +250,7 @@ class TestQgsSnappingUtils : public QObject u.setMapSettings( mapSettings ); snappingConfig.setEnabled( true ); snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration ); - snappingConfig.setIndividualLayerSettings( mVL, QgsSnappingConfig::IndividualLayerSettings( true, QgsSnappingConfig::VertexFlag, 10, QgsTolerance::Pixels ) ); + snappingConfig.setIndividualLayerSettings( mVL, QgsSnappingConfig::IndividualLayerSettings( true, QgsSnappingConfig::VertexFlag, 10, QgsTolerance::Pixels, -1.0, -1.0 ) ); u.setConfig( snappingConfig ); QgsPointLocator::Match m = u.snapToMap( QPoint( 100, 100 ) ); @@ -297,7 +297,7 @@ class TestQgsSnappingUtils : public QObject QgsSnappingConfig snappingConfig = u.config(); snappingConfig.setEnabled( true ); snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration ); - QgsSnappingConfig::IndividualLayerSettings layerSettings( true, QgsSnappingConfig::VertexFlag, 0.1, QgsTolerance::ProjectUnits ); + QgsSnappingConfig::IndividualLayerSettings layerSettings( true, QgsSnappingConfig::VertexFlag, 0.1, QgsTolerance::ProjectUnits, 0.0, 0.0 ); snappingConfig.setIndividualLayerSettings( vl, layerSettings ); u.setConfig( snappingConfig ); @@ -349,7 +349,7 @@ class TestQgsSnappingUtils : public QObject QgsSnappingConfig snappingConfig = u.config(); snappingConfig.setEnabled( true ); snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration ); - QgsSnappingConfig::IndividualLayerSettings layerSettings( true, QgsSnappingConfig::VertexFlag, 0.2, QgsTolerance::ProjectUnits ); + QgsSnappingConfig::IndividualLayerSettings layerSettings( true, QgsSnappingConfig::VertexFlag, 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 ); snappingConfig.setIntersectionSnapping( true ); snappingConfig.setIndividualLayerSettings( vCurveZ.get(), layerSettings ); u.setConfig( snappingConfig ); @@ -386,7 +386,7 @@ class TestQgsSnappingUtils : public QObject QgsSnappingConfig snappingConfig = u.config(); snappingConfig.setEnabled( true ); snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration ); - QgsSnappingConfig::IndividualLayerSettings layerSettings( true, QgsSnappingConfig::VertexFlag, 0.2, QgsTolerance::ProjectUnits ); + QgsSnappingConfig::IndividualLayerSettings layerSettings( true, QgsSnappingConfig::VertexFlag, 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 ); snappingConfig.setIntersectionSnapping( true ); snappingConfig.setIndividualLayerSettings( vMulti.get(), layerSettings ); u.setConfig( snappingConfig ); @@ -424,7 +424,7 @@ class TestQgsSnappingUtils : public QObject QgsSnappingConfig snappingConfig = u.config(); snappingConfig.setEnabled( true ); snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration ); - QgsSnappingConfig::IndividualLayerSettings layerSettings( true, static_cast( QgsSnappingConfig::MiddleOfSegmentFlag | QgsSnappingConfig::CentroidFlag ), 0.2, QgsTolerance::ProjectUnits ); + QgsSnappingConfig::IndividualLayerSettings layerSettings( true, static_cast( QgsSnappingConfig::MiddleOfSegmentFlag | QgsSnappingConfig::CentroidFlag ), 0.2, QgsTolerance::ProjectUnits, 0.0, 0.0 ); snappingConfig.setIndividualLayerSettings( vSnapCentroidMiddle.get(), layerSettings ); u.setConfig( snappingConfig ); @@ -457,6 +457,78 @@ class TestQgsSnappingUtils : public QObject QCOMPARE( m.point(), QgsPointXY( 1, 0 ) ); } + void testSnapScaleDependency() + { + QgsMapSettings mapSettings; + mapSettings.setOutputSize( QSize( 100, 100 ) ); + mapSettings.setExtent( QgsRectangle( 0, 0, 1, 1 ) ); + //Cannot set a specific scale directly, so play with Dpi in map settings, default scale is now 43295.7 + mapSettings.setOutputDpi( 1 ); + QVERIFY( mapSettings.hasValidSettings() ); + + QgsSnappingUtils u; + QgsSnappingConfig snappingConfig = u.config(); + u.setMapSettings( mapSettings ); + snappingConfig.setEnabled( true ); + snappingConfig.setMode( QgsSnappingConfig::AdvancedConfiguration ); + snappingConfig.setScaleDependencyMode( QgsSnappingConfig::Disabled ); + snappingConfig.setIndividualLayerSettings( mVL, QgsSnappingConfig::IndividualLayerSettings( true, QgsSnappingConfig::VertexFlag, 10, QgsTolerance::Pixels, -1.0, -1.0 ) ); + u.setConfig( snappingConfig ); + + //No limit on scale + QgsPointLocator::Match m = u.snapToMap( QPoint( 100, 100 ) ); + QVERIFY( m.isValid() ); + QVERIFY( m.hasVertex() ); + QCOMPARE( m.point(), QgsPointXY( 1, 0 ) ); + + snappingConfig.setScaleDependencyMode( QgsSnappingConfig::Global ); + snappingConfig.setMinimumScale( 0.0 ); + snappingConfig.setMaximumScale( 0.0 ); + u.setConfig( snappingConfig ); + + //Global settings for scale limit, but scale are set to 0 -> snapping enabled + QgsPointLocator::Match m1 = u.snapToMap( QPoint( 100, 100 ) ); + QVERIFY( m1.isValid() ); + QVERIFY( m1.hasVertex() ); + + snappingConfig.setScaleDependencyMode( QgsSnappingConfig::Global ); + snappingConfig.setMinimumScale( 1000.0 ); + snappingConfig.setMaximumScale( 10000.0 ); + u.setConfig( snappingConfig ); + + //Global settings for scale limit, but scale outside min max range -> no snapping + QgsPointLocator::Match m2 = u.snapToMap( QPoint( 100, 100 ) ); + QVERIFY( m2.isValid() == false ); + QVERIFY( m2.hasVertex() == false ); + + snappingConfig.setScaleDependencyMode( QgsSnappingConfig::Global ); + snappingConfig.setMinimumScale( 1000.0 ); + snappingConfig.setMaximumScale( 100000.0 ); + u.setConfig( snappingConfig ); + + //Global settings for scale limit, scale inside min max range -> snapping enabled + QgsPointLocator::Match m3 = u.snapToMap( QPoint( 100, 100 ) ); + QVERIFY( m3.isValid() ); + QVERIFY( m3.hasVertex() ); + + snappingConfig.setScaleDependencyMode( QgsSnappingConfig::PerLayer ); + snappingConfig.setIndividualLayerSettings( mVL, QgsSnappingConfig::IndividualLayerSettings( true, QgsSnappingConfig::VertexFlag, 10, QgsTolerance::Pixels, 1000.0, 10000.0 ) ); + u.setConfig( snappingConfig ); + + //Per layer settings, but scale outside min max range of layer -> no snapping + QgsPointLocator::Match m4 = u.snapToMap( QPoint( 100, 100 ) ); + QVERIFY( m4.isValid() == false ); + QVERIFY( m4.hasVertex() == false ); + + snappingConfig.setScaleDependencyMode( QgsSnappingConfig::PerLayer ); + snappingConfig.setIndividualLayerSettings( mVL, QgsSnappingConfig::IndividualLayerSettings( true, QgsSnappingConfig::VertexFlag, 10, QgsTolerance::Pixels, 1000.0, 100000.0 ) ); + u.setConfig( snappingConfig ); + + //Per layer settings, scale inside min max range of layer -> snapping enabled + QgsPointLocator::Match m5 = u.snapToMap( QPoint( 100, 100 ) ); + QVERIFY( m5.isValid() ); + QVERIFY( m5.hasVertex() ); + } }; QGSTEST_MAIN( TestQgsSnappingUtils ) diff --git a/tests/src/core/testqgssqliteexpressioncompiler.cpp b/tests/src/core/testqgssqliteexpressioncompiler.cpp index f877dce5a36c..65de5a2d86a1 100644 --- a/tests/src/core/testqgssqliteexpressioncompiler.cpp +++ b/tests/src/core/testqgssqliteexpressioncompiler.cpp @@ -101,7 +101,15 @@ void TestQgsSQLiteExpressionCompiler::testCompiler() QCOMPARE( compiler.compile( &exp ), QgsSqlExpressionCompiler::Result::Complete ); // Check that parenthesis matches QCOMPARE( compiler.result().count( '(' ), compiler.result().count( ')' ) ); - QCOMPARE( compiler.result(), QString( "((((\"Z\" >= 0) AND (\"Bottom\" <= 1)) OR ((\"Z\" >= 1) AND (\"Bottom\" <= 2))) OR ((\"Z\" >= 2) AND (\"Bottom\" <= 3)))" ) ); + QCOMPARE( compiler.result(), QStringLiteral( "((((\"Z\" >= 0) AND (\"Bottom\" <= 1)) OR ((\"Z\" >= 1) AND (\"Bottom\" <= 2))) OR ((\"Z\" >= 2) AND (\"Bottom\" <= 3)))" ) ); + + QgsExpression ilike( QStringLiteral( "'a' ilike 'A'" ) ); + QCOMPARE( compiler.compile( &ilike ), QgsSqlExpressionCompiler::Result::Complete ); + QCOMPARE( compiler.result(), QStringLiteral( "lower('a') LIKE lower('A') ESCAPE '\\'" ) ); + + QgsExpression nilike( QStringLiteral( "'a' not ilike 'A'" ) ); + QCOMPARE( compiler.compile( &nilike ), QgsSqlExpressionCompiler::Result::Complete ); + QCOMPARE( compiler.result(), QStringLiteral( "lower('a') NOT LIKE lower('A') ESCAPE '\\'" ) ); } diff --git a/tests/src/core/testqgstaskmanager.cpp b/tests/src/core/testqgstaskmanager.cpp index 44bb423a8ae9..304549a75f0e 100644 --- a/tests/src/core/testqgstaskmanager.cpp +++ b/tests/src/core/testqgstaskmanager.cpp @@ -807,7 +807,8 @@ void TestQgsTaskManager::subTaskPartialComplete2() QCOMPARE( subsubTask->status(), QgsTask::Running ); subsubTask->finish(); - while ( subsubTask->status() == QgsTask::Running ) + while ( subsubTask->status() == QgsTask::Running + || subTask->status() == QgsTask::Running ) { QCoreApplication::processEvents(); } diff --git a/tests/src/core/testqgstemporalnavigationobject.cpp b/tests/src/core/testqgstemporalnavigationobject.cpp new file mode 100644 index 000000000000..7c02a4902c47 --- /dev/null +++ b/tests/src/core/testqgstemporalnavigationobject.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + testqgstemporalnavigationobject.cpp + --------------- + begin : April 2020 + copyright : (C) 2020 by Samweli Mwakisambwe + email : samweli at kartoza 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 "qgstest.h" +#include + +//qgis includes... +#include + +/** + * \ingroup UnitTests + * This is a unit test for the QgsTemporalNavigationObject class. + */ +class TestQgsTemporalNavigationObject : public QObject +{ + Q_OBJECT + + public: + TestQgsTemporalNavigationObject() = default; + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init(); // will be called before each testfunction is executed. + void cleanup(); // will be called after every testfunction. + + void animationState(); + void temporalExtents(); + void frameSettings(); + + private: + QgsTemporalNavigationObject *navigationObject = nullptr; +}; + +void TestQgsTemporalNavigationObject::initTestCase() +{ + // + // Runs once before any tests are run + // + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + QgsApplication::showSettings(); + +} + +void TestQgsTemporalNavigationObject::init() +{ + //create some objects that will be used in all tests... + //create a temporal object that will be used in all tests... + + navigationObject = new QgsTemporalNavigationObject(); +} + +void TestQgsTemporalNavigationObject::cleanup() +{ +} + +void TestQgsTemporalNavigationObject::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsTemporalNavigationObject::animationState() +{ + QgsDateTimeRange range = QgsDateTimeRange( + QDateTime( QDate( 2020, 1, 1 ), QTime( 8, 0, 0 ) ), + QDateTime( QDate( 2020, 10, 1 ), QTime( 8, 0, 0 ) ) + ); + navigationObject->setTemporalExtents( range ); + + navigationObject->setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalMonths ) ); + + qRegisterMetaType( "AnimationState" ); + QSignalSpy stateSignal( navigationObject, &QgsTemporalNavigationObject::stateChanged ); + + QCOMPARE( navigationObject->animationState(), QgsTemporalNavigationObject::Idle ); + + navigationObject->setAnimationState( QgsTemporalNavigationObject::Forward ); + QCOMPARE( navigationObject->animationState(), QgsTemporalNavigationObject::Forward ); + QCOMPARE( stateSignal.count(), 1 ); + + navigationObject->playBackward(); + QCOMPARE( navigationObject->animationState(), QgsTemporalNavigationObject::Reverse ); + QCOMPARE( stateSignal.count(), 2 ); + + navigationObject->playForward(); + QCOMPARE( navigationObject->animationState(), QgsTemporalNavigationObject::Forward ); + QCOMPARE( stateSignal.count(), 3 ); + + navigationObject->pause(); + QCOMPARE( navigationObject->animationState(), QgsTemporalNavigationObject::Idle ); + QCOMPARE( stateSignal.count(), 4 ); + + navigationObject->next(); + QCOMPARE( navigationObject->currentFrameNumber(), 1 ); + + navigationObject->previous(); + QCOMPARE( navigationObject->currentFrameNumber(), 0 ); + + navigationObject->skipToEnd(); + QCOMPARE( navigationObject->currentFrameNumber(), 10 ); + + navigationObject->rewindToStart(); + QCOMPARE( navigationObject->currentFrameNumber(), 0 ); + + QCOMPARE( navigationObject->isLooping(), false ); + navigationObject->setLooping( true ); + QCOMPARE( navigationObject->isLooping(), true ); + +} + +void TestQgsTemporalNavigationObject::temporalExtents() +{ + QgsDateTimeRange range = QgsDateTimeRange( + QDateTime( QDate( 2020, 1, 1 ), QTime( 8, 0, 0 ) ), + QDateTime( QDate( 2020, 12, 1 ), QTime( 8, 0, 0 ) ) + ); + navigationObject->setTemporalExtents( range ); + QCOMPARE( navigationObject->temporalExtents(), range ); + + navigationObject->setTemporalExtents( QgsDateTimeRange() ); + QCOMPARE( navigationObject->temporalExtents(), QgsDateTimeRange() ); +} + +void TestQgsTemporalNavigationObject::frameSettings() +{ + qRegisterMetaType( "QgsDateTimeRange" ); + QSignalSpy temporalRangeSignal( navigationObject, &QgsTemporalNavigationObject::updateTemporalRange ); + + QgsDateTimeRange range = QgsDateTimeRange( + QDateTime( QDate( 2020, 1, 1 ), QTime( 8, 0, 0 ) ), + QDateTime( QDate( 2020, 1, 1 ), QTime( 12, 0, 0 ) ) + ); + navigationObject->setTemporalExtents( range ); + QCOMPARE( temporalRangeSignal.count(), 1 ); + + navigationObject->setFrameDuration( QgsInterval( 1, QgsUnitTypes::TemporalHours ) ); + QCOMPARE( navigationObject->frameDuration(), QgsInterval( 1, QgsUnitTypes::TemporalHours ) ); + + QCOMPARE( navigationObject->currentFrameNumber(), 0 ); + QCOMPARE( navigationObject->totalFrameCount(), 5 ); + + navigationObject->setCurrentFrameNumber( 1 ); + QCOMPARE( navigationObject->currentFrameNumber(), 1 ); + QCOMPARE( temporalRangeSignal.count(), 2 ); + + navigationObject->setFramesPerSecond( 1 ); + QCOMPARE( navigationObject->framesPerSecond(), 1.0 ); + +} + +QGSTEST_MAIN( TestQgsTemporalNavigationObject ) +#include "testqgstemporalnavigationobject.moc" diff --git a/tests/src/core/testqgsvectortilelayer.cpp b/tests/src/core/testqgsvectortilelayer.cpp new file mode 100644 index 000000000000..00ee27d5b731 --- /dev/null +++ b/tests/src/core/testqgsvectortilelayer.cpp @@ -0,0 +1,138 @@ +/*************************************************************************** + testqgsvectortilelayer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk 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 "qgstest.h" +#include +#include + +//qgis includes... +#include "qgsapplication.h" +#include "qgsproject.h" +#include "qgsrenderchecker.h" +#include "qgstiles.h" +#include "qgsvectortilebasicrenderer.h" +#include "qgsvectortilelayer.h" + +/** + * \ingroup UnitTests + * This is a unit test for a vector tile layer + */ +class TestQgsVectorTileLayer : public QObject +{ + Q_OBJECT + + public: + TestQgsVectorTileLayer() = default; + + private: + QString mDataDir; + QgsVectorTileLayer *mLayer = nullptr; + QString mReport; + QgsMapSettings *mMapSettings = nullptr; + + bool imageCheck( const QString &testType, QgsVectorTileLayer *layer, QgsRectangle extent ); + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + + void test_basic(); + void test_render(); +}; + + +void TestQgsVectorTileLayer::initTestCase() +{ + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + QgsApplication::showSettings(); + mDataDir = QString( TEST_DATA_DIR ); //defined in CmakeLists.txt + mDataDir += "/vector_tile"; + + QgsDataSourceUri ds; + ds.setParam( "type", "xyz" ); + ds.setParam( "url", QString( "file://%1/{z}-{x}-{y}.pbf" ).arg( mDataDir ) ); + ds.setParam( "zmax", "1" ); + mLayer = new QgsVectorTileLayer( ds.encodedUri(), "Vector Tiles Test" ); + QVERIFY( mLayer->isValid() ); + + QgsProject::instance()->addMapLayer( mLayer ); + + mMapSettings = new QgsMapSettings(); + mMapSettings->setLayers( QList() << mLayer ); + + // let's have some standard style config for the layer + QColor polygonFillColor = Qt::blue; + QColor polygonStrokeColor = polygonFillColor; + polygonFillColor.setAlpha( 100 ); + double polygonStrokeWidth = DEFAULT_LINE_WIDTH * 2; + QColor lineStrokeColor = Qt::blue; + double lineStrokeWidth = DEFAULT_LINE_WIDTH * 2; + QColor pointFillColor = Qt::red; + QColor pointStrokeColor = pointFillColor; + pointFillColor.setAlpha( 100 ); + double pointSize = DEFAULT_POINT_SIZE; + + QgsVectorTileBasicRenderer *rend = new QgsVectorTileBasicRenderer; + rend->setStyles( QgsVectorTileBasicRenderer::simpleStyle( + polygonFillColor, polygonStrokeColor, polygonStrokeWidth, + lineStrokeColor, lineStrokeWidth, + pointFillColor, pointStrokeColor, pointSize ) ); + mLayer->setRenderer( rend ); // takes ownership +} + +void TestQgsVectorTileLayer::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsVectorTileLayer::test_basic() +{ + // tile fetch test + QByteArray tile0rawData = mLayer->getRawTile( QgsTileXYZ( 0, 0, 0 ) ); + QCOMPARE( tile0rawData.length(), 64822 ); + + QByteArray invalidTileRawData = mLayer->getRawTile( QgsTileXYZ( 0, 0, 99 ) ); + QCOMPARE( invalidTileRawData.length(), 0 ); +} + + +bool TestQgsVectorTileLayer::imageCheck( const QString &testType, QgsVectorTileLayer *layer, QgsRectangle extent ) +{ + mReport += "

    " + testType + "

    \n"; + mMapSettings->setExtent( extent ); + mMapSettings->setDestinationCrs( layer->crs() ); + mMapSettings->setOutputDpi( 96 ); + QgsRenderChecker myChecker; + myChecker.setControlPathPrefix( QStringLiteral( "vector_tile" ) ); + myChecker.setControlName( "expected_" + testType ); + myChecker.setMapSettings( *mMapSettings ); + myChecker.setColorTolerance( 15 ); + bool myResultFlag = myChecker.runTest( testType, 0 ); + mReport += myChecker.report(); + return myResultFlag; +} + +void TestQgsVectorTileLayer::test_render() +{ + QVERIFY( imageCheck( "render_test_basic", mLayer, mLayer->extent() ) ); +} + + +QGSTEST_MAIN( TestQgsVectorTileLayer ) +#include "testqgsvectortilelayer.moc" diff --git a/tests/src/gui/testprocessinggui.cpp b/tests/src/gui/testprocessinggui.cpp index 3ed76daa51e0..0338fcd563e0 100644 --- a/tests/src/gui/testprocessinggui.cpp +++ b/tests/src/gui/testprocessinggui.cpp @@ -40,6 +40,7 @@ #include "qgsprocessingmaplayercombobox.h" #include "qgsnativealgorithms.h" #include "processing/models/qgsprocessingmodelalgorithm.h" +#include "processing/models/qgsprocessingmodelgroupbox.h" #include "qgsxmlutils.h" #include "qgspropertyoverridebutton.h" #include "qgsprojectionselectionwidget.h" @@ -72,6 +73,13 @@ #include "qgsproviderregistry.h" #include "qgsprovidermetadata.h" #include "qgsproviderconnectioncombobox.h" +#include "qgsdatabaseschemacombobox.h" +#include "qgsdatabasetablecombobox.h" +#include "qgsprocessingoutputdestinationwidget.h" +#include "qgssettings.h" +#include "qgsprocessingfeaturesourceoptionswidget.h" +#include "qgsextentwidget.h" +#include "qgsrasterbandcombobox.h" class TestParamType : public QgsProcessingParameterDefinition { @@ -152,8 +160,6 @@ class TestWidgetFactory : public QgsProcessingParameterWidgetFactoryInterface QStringList compatibleOutputTypes() const override { return QStringList(); } - QList< int > compatibleDataTypes() const override { return QList(); } - }; @@ -191,6 +197,10 @@ class TestProcessingGui : public QObject void testFieldSelectionPanel(); void testFieldWrapper(); void testMultipleSelectionDialog(); + void testMultipleFileSelectionDialog(); + void testRasterBandSelectionPanel(); + void testBandWrapper(); + void testMultipleInputWrapper(); void testEnumSelectionPanel(); void testEnumCheckboxPanel(); void testEnumWrapper(); @@ -198,13 +208,32 @@ class TestProcessingGui : public QObject void testLayoutItemWrapper(); void testPointPanel(); void testPointWrapper(); + void testExtentWrapper(); void testColorWrapper(); void testCoordinateOperationWrapper(); void mapLayerComboBox(); + void testMapLayerWrapper(); + void testRasterLayerWrapper(); + void testVectorLayerWrapper(); + void testFeatureSourceWrapper(); + void testMeshLayerWrapper(); void paramConfigWidget(); void testMapThemeWrapper(); void testDateTimeWrapper(); void testProviderConnectionWrapper(); + void testDatabaseSchemaWrapper(); + void testDatabaseTableWrapper(); + void testOutputDefinitionWidget(); + void testOutputDefinitionWidgetVectorOut(); + void testOutputDefinitionWidgetRasterOut(); + void testOutputDefinitionWidgetFolder(); + void testOutputDefinitionWidgetFileOut(); + void testFeatureSourceOptionsWidget(); + void testVectorOutWrapper(); + void testSinkWrapper(); + void testRasterOutWrapper(); + void testFileOutWrapper(); + void testFolderOutWrapper(); private: @@ -609,9 +638,13 @@ void TestProcessingGui::testModelerWrapper() QgsProcessingModelChildAlgorithm a3( QStringLiteral( "native:buffer" ) ); a3.setDescription( QStringLiteral( "alg3" ) ); a3.setChildId( QStringLiteral( "alg3" ) ); + QgsProcessingModelChildAlgorithm a4( QStringLiteral( "native:package" ) ); + a4.setDescription( QStringLiteral( "alg4" ) ); + a4.setChildId( QStringLiteral( "alg4" ) ); algs.insert( QStringLiteral( "alg1" ), a1 ); algs.insert( QStringLiteral( "alg2" ), a2 ); algs.insert( QStringLiteral( "alg3" ), a3 ); + algs.insert( QStringLiteral( "alg4" ), a4 ); model.setChildAlgorithms( algs ); QMap pComponents; @@ -624,7 +657,8 @@ void TestProcessingGui::testModelerWrapper() model.addModelParameter( new QgsProcessingParameterBoolean( "p1", "desc" ), bool1 ); QgsProcessingModelParameter testParam( "p2" ); model.addModelParameter( new TestParamType( "test_type", "p2" ), testParam ); - + QgsProcessingModelParameter testDestParam( "p3" ); + model.addModelParameter( new QgsProcessingParameterFileDestination( "test_dest", "p3" ), testDestParam ); // try to create a parameter widget, no factories registered QgsProcessingGuiRegistry registry; QgsProcessingContext context; @@ -649,32 +683,32 @@ void TestProcessingGui::testModelerWrapper() // static value w->setWidgetValue( QgsProcessingModelChildParameterSource::fromStaticValue( true ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::StaticValue ); - QCOMPARE( w->value().staticValue().toBool(), true ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::StaticValue ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().staticValue().toBool(), true ); w->setWidgetValue( QgsProcessingModelChildParameterSource::fromStaticValue( false ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::StaticValue ); - QCOMPARE( w->value().staticValue().toBool(), false ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::StaticValue ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().staticValue().toBool(), false ); QCOMPARE( w->mStackedWidget->currentIndex(), 0 ); QCOMPARE( w->mSourceButton->toolTip(), QStringLiteral( "Value" ) ); // expression value w->setWidgetValue( QgsProcessingModelChildParameterSource::fromExpression( QStringLiteral( "1+2" ) ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::Expression ); - QCOMPARE( w->value().expression(), QStringLiteral( "1+2" ) ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::Expression ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().expression(), QStringLiteral( "1+2" ) ); QCOMPARE( w->mStackedWidget->currentIndex(), 1 ); QCOMPARE( w->mSourceButton->toolTip(), QStringLiteral( "Pre-calculated Value" ) ); // model input - should fail, because we haven't populated sources yet, and so have no compatible sources w->setWidgetValue( QgsProcessingModelChildParameterSource::fromModelParameter( QStringLiteral( "p1" ) ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::ModelParameter ); - QVERIFY( w->value().parameterName().isEmpty() ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ModelParameter ); + QVERIFY( w->value().value< QgsProcessingModelChildParameterSource>().parameterName().isEmpty() ); QCOMPARE( w->mStackedWidget->currentIndex(), 2 ); QCOMPARE( w->mSourceButton->toolTip(), QStringLiteral( "Model Input" ) ); // alg output - should fail, because we haven't populated sources yet, and so have no compatible sources w->setWidgetValue( QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "alg3" ), QStringLiteral( "OUTPUT" ) ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::ChildOutput ); - QVERIFY( w->value().outputChildId().isEmpty() ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ChildOutput ); + QVERIFY( w->value().value< QgsProcessingModelChildParameterSource>().outputChildId().isEmpty() ); QCOMPARE( w->mStackedWidget->currentIndex(), 3 ); QCOMPARE( w->mSourceButton->toolTip(), QStringLiteral( "Algorithm Output" ) ); @@ -683,15 +717,55 @@ void TestProcessingGui::testModelerWrapper() // model input w->setWidgetValue( QgsProcessingModelChildParameterSource::fromModelParameter( QStringLiteral( "p1" ) ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::ModelParameter ); - QCOMPARE( w->value().parameterName(), QStringLiteral( "p1" ) ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ModelParameter ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().parameterName(), QStringLiteral( "p1" ) ); // alg output w->setWidgetValue( QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "alg3" ), QStringLiteral( "OUTPUT" ) ) ); - QCOMPARE( w->value().source(), QgsProcessingModelChildParameterSource::ChildOutput ); - QCOMPARE( w->value().outputChildId(), QStringLiteral( "alg3" ) ); - QCOMPARE( w->value().outputName(), QStringLiteral( "OUTPUT" ) ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ChildOutput ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().outputChildId(), QStringLiteral( "alg3" ) ); + QCOMPARE( w->value().value< QgsProcessingModelChildParameterSource>().outputName(), QStringLiteral( "OUTPUT" ) ); + + // model output + delete w; + w = new QgsProcessingModelerParameterWidget( &model, "alg1", model.parameterDefinition( "test_dest" ), context ); + QCOMPARE( w->parameterDefinition()->name(), QStringLiteral( "test_dest" ) ); + // should default to being a model output for destination parameters, but with no value + QVERIFY( w->isModelOutput() ); + QCOMPARE( w->modelOutputName(), QString() ); + // set it to something else + w->setWidgetValue( QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "alg3" ), QStringLiteral( "OUTPUT" ) ) ); + QVERIFY( !w->isModelOutput() ); + // and back + w->setToModelOutput( QStringLiteral( "out" ) ); + QVERIFY( w->isModelOutput() ); + QCOMPARE( w->modelOutputName(), QStringLiteral( "out" ) ); + w->setWidgetValue( QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "alg3" ), QStringLiteral( "OUTPUT" ) ) ); + w->setToModelOutput( QString() ); + QVERIFY( w->isModelOutput() ); + QCOMPARE( w->modelOutputName(), QString() ); + // multi-source input + delete w; + const QgsProcessingAlgorithm *packageAlg = QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "native:package" ) ); + const QgsProcessingParameterDefinition *layerDef = packageAlg->parameterDefinition( QStringLiteral( "LAYERS" ) ); + + w = new QgsProcessingModelerParameterWidget( &model, "alg4", layerDef, context ); + + w->setWidgetValue( QList< QgsProcessingModelChildParameterSource>() + << QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "alg3" ), QStringLiteral( "OUTPUT" ) ) + << QgsProcessingModelChildParameterSource::fromModelParameter( QStringLiteral( "p1" ) ) + << QgsProcessingModelChildParameterSource::fromStaticValue( QStringLiteral( "something" ) ) ); + QCOMPARE( w->value().toList().count(), 3 ); + + QCOMPARE( w->value().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ChildOutput ); + QCOMPARE( w->value().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ChildOutput ); + QCOMPARE( w->value().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().outputChildId(), QStringLiteral( "alg3" ) ); + QCOMPARE( w->value().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().outputName(), QStringLiteral( "OUTPUT" ) ); + QCOMPARE( w->value().toList().at( 1 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ModelParameter ); + QCOMPARE( w->value().toList().at( 1 ).value< QgsProcessingModelChildParameterSource>().parameterName(), QStringLiteral( "p1" ) ); + QCOMPARE( w->value().toList().at( 2 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::StaticValue ); + QCOMPARE( w->value().toList().at( 2 ).value< QgsProcessingModelChildParameterSource>().staticValue().toString(), QStringLiteral( "something" ) ); delete w; } @@ -1345,6 +1419,33 @@ void TestProcessingGui::testCrsWrapper() QCOMPARE( l->toolTip(), param.toolTip() ); delete w; delete l; + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "crs" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterCrs crsParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "EPSG:4326" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "crs" ), context, widgetContext, &crsParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterCrs * >( def.get() )->defaultValue().toString(), QStringLiteral( "EPSG:4326" ) ); + crsParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + crsParam.setDefaultValue( QStringLiteral( "EPSG:3111" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "crs" ), context, widgetContext, &crsParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterCrs * >( def.get() )->defaultValue().toString(), QStringLiteral( "EPSG:3111" ) ); } void TestProcessingGui::testNumericWrapperDouble() @@ -1514,6 +1615,45 @@ void TestProcessingGui::testNumericWrapperDouble() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "number" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterNumber numParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QgsProcessingParameterNumber::Double, 1.0 ); + numParam.setMinimum( 0 ); + numParam.setMaximum( 10 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "number" ), context, widgetContext, &numParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->defaultValue().toDouble(), 1.0 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->dataType(), QgsProcessingParameterNumber::Double ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->minimum(), 0.0 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->maximum(), 10.0 ); + numParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + numParam.setDataType( QgsProcessingParameterNumber::Integer ); + numParam.setMinimum( -1 ); + numParam.setMaximum( 1 ); + numParam.setDefaultValue( 0 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "number" ), context, widgetContext, &numParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->defaultValue().toInt(), 0 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->dataType(), QgsProcessingParameterNumber::Integer ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->minimum(), -1.0 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->maximum(), 1.0 ); } void TestProcessingGui::testNumericWrapperInt() @@ -1665,6 +1805,45 @@ void TestProcessingGui::testNumericWrapperInt() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "number" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterNumber numParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QgsProcessingParameterNumber::Integer, 1 ); + numParam.setMinimum( 0 ); + numParam.setMaximum( 10 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "number" ), context, widgetContext, &numParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->defaultValue().toDouble(), 1.0 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->dataType(), QgsProcessingParameterNumber::Integer ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->minimum(), 0.0 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->maximum(), 10.0 ); + numParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + numParam.setDataType( QgsProcessingParameterNumber::Double ); + numParam.setMinimum( -2.5 ); + numParam.setMaximum( 2.5 ); + numParam.setDefaultValue( 0.5 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "number" ), context, widgetContext, &numParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->defaultValue().toDouble(), 0.5 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->dataType(), QgsProcessingParameterNumber::Double ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->minimum(), -2.5 ); + QCOMPARE( static_cast< QgsProcessingParameterNumber * >( def.get() )->maximum(), 2.5 ); } void TestProcessingGui::testDistanceWrapper() @@ -1850,6 +2029,44 @@ void TestProcessingGui::testDistanceWrapper() QCOMPARE( l->toolTip(), param.toolTip() ); delete w; delete l; + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "distance" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterDistance distParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), 1, QStringLiteral( "parent" ) ); + distParam.setMinimum( 1 ); + distParam.setMaximum( 100 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "distance" ), context, widgetContext, &distParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->defaultValue().toDouble(), 1.0 ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->minimum(), 1.0 ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->maximum(), 100.0 ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->parentParameterName(), QStringLiteral( "parent" ) ); + distParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + distParam.setParentParameterName( QString() ); + distParam.setMinimum( 10 ); + distParam.setMaximum( 12 ); + distParam.setDefaultValue( 11.5 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "distance" ), context, widgetContext, &distParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->defaultValue().toDouble(), 11.5 ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->minimum(), 10.0 ); + QCOMPARE( static_cast< QgsProcessingParameterDistance * >( def.get() )->maximum(), 12.0 ); + QVERIFY( static_cast< QgsProcessingParameterDistance * >( def.get() )->parentParameterName().isEmpty() ); } void TestProcessingGui::testScaleWrapper() @@ -1939,6 +2156,34 @@ void TestProcessingGui::testScaleWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "scale" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterScale scaleParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), 1000 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "scale" ), context, widgetContext, &scaleParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterScale * >( def.get() )->defaultValue().toDouble(), 1000.0 ); + scaleParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + scaleParam.setDefaultValue( 28356 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "scale" ), context, widgetContext, &scaleParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterScale * >( def.get() )->defaultValue().toDouble(), 28356.0 ); } void TestProcessingGui::testRangeWrapper() @@ -2072,6 +2317,37 @@ void TestProcessingGui::testRangeWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "range" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterRange rangeParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QgsProcessingParameterNumber::Integer, QStringLiteral( "0,255" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "range" ), context, widgetContext, &rangeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterRange * >( def.get() )->defaultValue().toString(), QStringLiteral( "0,255" ) ); + QCOMPARE( static_cast< QgsProcessingParameterRange * >( def.get() )->dataType(), QgsProcessingParameterNumber::Integer ); + rangeParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + rangeParam.setDataType( QgsProcessingParameterNumber::Double ); + rangeParam.setDefaultValue( QStringLiteral( "0,1" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "range" ), context, widgetContext, &rangeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterRange * >( def.get() )->defaultValue().toString(), QStringLiteral( "0,1" ) ); + QCOMPARE( static_cast< QgsProcessingParameterRange * >( def.get() )->dataType(), QgsProcessingParameterNumber::Double ); } void TestProcessingGui::testMatrixDialog() @@ -2154,6 +2430,39 @@ void TestProcessingGui::testMatrixWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "matrix" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterMatrix matrixParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), 1, false, QStringList() << "A" << "B" << "C", QVariantList() << 0 << 0 << 0 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "matrix" ), context, widgetContext, &matrixParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterMatrix * >( def.get() )->headers(), QStringList() << "A" << "B" << "C" ); + QCOMPARE( static_cast< QgsProcessingParameterMatrix * >( def.get() )->defaultValue().toStringList(), QStringList() << "0" << "0" << "0" ); + QVERIFY( !static_cast< QgsProcessingParameterMatrix * >( def.get() )->hasFixedNumberRows() ); + matrixParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + matrixParam.setHasFixedNumberRows( true ); + matrixParam.setDefaultValue( QVariantList() << 1 << 2 << 3 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "matrix" ), context, widgetContext, &matrixParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterMatrix * >( def.get() )->headers(), QStringList() << "A" << "B" << "C" ); + QCOMPARE( static_cast< QgsProcessingParameterMatrix * >( def.get() )->defaultValue().toStringList(), QStringList() << "1" << "2" << "3" ); + QVERIFY( static_cast< QgsProcessingParameterMatrix * >( def.get() )->hasFixedNumberRows() ); } void TestProcessingGui::testExpressionWrapper() @@ -2266,6 +2575,34 @@ void TestProcessingGui::testExpressionWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "expression" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterExpression exprParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QVariant(), QStringLiteral( "parent" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "expression" ), context, widgetContext, &exprParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterExpression * >( def.get() )->parentLayerParameterName(), QStringLiteral( "parent" ) ); + exprParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + exprParam.setParentLayerParameterName( QString() ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "expression" ), context, widgetContext, &exprParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QVERIFY( static_cast< QgsProcessingParameterExpression * >( def.get() )->parentLayerParameterName().isEmpty() ); } void TestProcessingGui::testFieldSelectionPanel() @@ -2634,19 +2971,59 @@ void TestProcessingGui::testFieldWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "field" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterField fieldParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "field_name" ), QStringLiteral( "parent" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "field" ), context, widgetContext, &fieldParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->defaultValue().toString(), QStringLiteral( "field_name" ) ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->parentLayerParameterName(), QStringLiteral( "parent" ) ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->dataType(), QgsProcessingParameterField::Any ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->allowMultiple(), false ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->defaultToAllFields(), false ); + fieldParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + fieldParam.setParentLayerParameterName( QString() ); + fieldParam.setAllowMultiple( true ); + fieldParam.setDefaultToAllFields( true ); + fieldParam.setDataType( QgsProcessingParameterField::String ); + fieldParam.setDefaultValue( QStringLiteral( "field_1;field_2" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "field" ), context, widgetContext, &fieldParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->defaultValue().toString(), QStringLiteral( "field_1;field_2" ) ); + QVERIFY( static_cast< QgsProcessingParameterBand * >( def.get() )->parentLayerParameterName().isEmpty() ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->dataType(), QgsProcessingParameterField::String ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->allowMultiple(), true ); + QCOMPARE( static_cast< QgsProcessingParameterField * >( def.get() )->defaultToAllFields(), true ); } void TestProcessingGui::testMultipleSelectionDialog() { QVariantList availableOptions; QVariantList selectedOptions; - std::unique_ptr< QgsProcessingMultipleSelectionDialog > dlg = qgis::make_unique< QgsProcessingMultipleSelectionDialog >( availableOptions, selectedOptions ); + std::unique_ptr< QgsProcessingMultipleSelectionPanelWidget > dlg = qgis::make_unique< QgsProcessingMultipleSelectionPanelWidget >( availableOptions, selectedOptions ); QVERIFY( dlg->selectedOptions().isEmpty() ); QCOMPARE( dlg->mModel->rowCount(), 0 ); std::unique_ptr< QgsVectorLayer > vl = qgis::make_unique< QgsVectorLayer >( QStringLiteral( "LineString" ), QStringLiteral( "x" ), QStringLiteral( "memory" ) ); availableOptions << QVariant( "aa" ) << QVariant( 15 ) << QVariant::fromValue( vl.get() ); - dlg = qgis::make_unique< QgsProcessingMultipleSelectionDialog >( availableOptions, selectedOptions ); + dlg = qgis::make_unique< QgsProcessingMultipleSelectionPanelWidget >( availableOptions, selectedOptions ); QVERIFY( dlg->selectedOptions().isEmpty() ); QCOMPARE( dlg->mModel->rowCount(), 3 ); dlg->selectAll( true ); @@ -2662,7 +3039,7 @@ void TestProcessingGui::testMultipleSelectionDialog() // additional options availableOptions.clear(); selectedOptions << QVariant( "bb" ) << QVariant( 6.6 ); - dlg = qgis::make_unique< QgsProcessingMultipleSelectionDialog >( availableOptions, selectedOptions ); + dlg = qgis::make_unique< QgsProcessingMultipleSelectionPanelWidget >( availableOptions, selectedOptions ); QCOMPARE( dlg->mModel->rowCount(), 2 ); QCOMPARE( dlg->selectedOptions(), selectedOptions ); dlg->mModel->item( 1 )->setCheckState( Qt::Unchecked ); @@ -2670,7 +3047,7 @@ void TestProcessingGui::testMultipleSelectionDialog() // mix of standard and additional options availableOptions << QVariant( 6.6 ) << QVariant( "aa" ); - dlg = qgis::make_unique< QgsProcessingMultipleSelectionDialog >( availableOptions, selectedOptions ); + dlg = qgis::make_unique< QgsProcessingMultipleSelectionPanelWidget >( availableOptions, selectedOptions ); QCOMPARE( dlg->mModel->rowCount(), 3 ); QCOMPARE( dlg->selectedOptions(), selectedOptions ); // order must be maintained! dlg->mModel->item( 2 )->setCheckState( Qt::Checked ); @@ -2679,7 +3056,7 @@ void TestProcessingGui::testMultipleSelectionDialog() // selection buttons selectedOptions.clear(); availableOptions = QVariantList() << QVariant( "a" ) << QVariant( "b" ) << QVariant( "c" ); - dlg = qgis::make_unique< QgsProcessingMultipleSelectionDialog >( availableOptions, selectedOptions ); + dlg = qgis::make_unique< QgsProcessingMultipleSelectionPanelWidget >( availableOptions, selectedOptions ); QVERIFY( dlg->selectedOptions().isEmpty() ); dlg->mSelectionList->selectionModel()->select( dlg->mModel->index( 1, 0 ), QItemSelectionModel::ClearAndSelect ); // without a multi-selection, select all/toggle options should affect all items @@ -2709,7 +3086,7 @@ void TestProcessingGui::testMultipleSelectionDialog() // text format availableOptions = QVariantList() << QVariant( "a" ) << 6 << 6.2; - dlg = qgis::make_unique< QgsProcessingMultipleSelectionDialog >( availableOptions, selectedOptions ); + dlg = qgis::make_unique< QgsProcessingMultipleSelectionPanelWidget >( availableOptions, selectedOptions ); QCOMPARE( dlg->mModel->item( 0 )->text(), QStringLiteral( "a" ) ); QCOMPARE( dlg->mModel->item( 1 )->text(), QStringLiteral( "6" ) ); QCOMPARE( dlg->mModel->item( 2 )->text(), QStringLiteral( "6.2" ) ); @@ -2723,185 +3100,254 @@ void TestProcessingGui::testMultipleSelectionDialog() } -void TestProcessingGui::testEnumSelectionPanel() +void TestProcessingGui::testMultipleFileSelectionDialog() { - QgsProcessingParameterEnum enumParam( QString(), QString(), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true ); - QgsProcessingEnumPanelWidget w( nullptr, &enumParam ); - QSignalSpy spy( &w, &QgsProcessingEnumPanelWidget::changed ); + std::unique_ptr< QgsProcessingParameterMultipleLayers > param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeRaster ); + QVariantList selectedOptions; + std::unique_ptr< QgsProcessingMultipleInputPanelWidget > dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), selectedOptions, QList() ); + QVERIFY( dlg->selectedOptions().isEmpty() ); + QCOMPARE( dlg->mModel->rowCount(), 0 ); - QCOMPARE( w.mLineEdit->text(), QStringLiteral( "0 options selected" ) ); - w.setValue( 1 ); + QgsProject::instance()->removeAllMapLayers(); + QgsVectorLayer *point = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "point" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( point ); + QgsVectorLayer *line = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "line" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( line ); + QgsVectorLayer *polygon = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "polygon" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( polygon ); + QgsVectorLayer *noGeom = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "nogeom" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( noGeom ); + QgsMeshLayer *mesh = new QgsMeshLayer( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle.2dm", QStringLiteral( "mesh" ), QStringLiteral( "mdal" ) ); + mesh->dataProvider()->addDataset( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); + QVERIFY( mesh->isValid() ); + QgsProject::instance()->addMapLayer( mesh ); + QgsRasterLayer *raster = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/raster/band1_byte_ct_epsg4326.tif", QStringLiteral( "raster" ) ); + QgsProject::instance()->addMapLayer( raster ); + + dlg->setProject( QgsProject::instance() ); + // should be filtered to raster layers only + QCOMPARE( dlg->mModel->rowCount(), 1 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "raster [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), raster->id() ); + QVERIFY( dlg->selectedOptions().isEmpty() ); + // existing value using layer id should match to project layer + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList() << raster->id(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "raster [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), raster->id() ); + QCOMPARE( dlg->selectedOptions().size(), 1 ); + QCOMPARE( dlg->selectedOptions().at( 0 ).toString(), raster->id() ); + // existing value using layer source should also match to project layer + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList() << raster->source(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "raster [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), raster->source() ); + QCOMPARE( dlg->selectedOptions().size(), 1 ); + QCOMPARE( dlg->selectedOptions().at( 0 ).toString(), raster->source() ); + // existing value using full layer path not matching a project layer should work + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList() << raster->source() << QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif", QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 2 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "raster [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), raster->source() ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 1, 0 ) ).toString(), QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 1, 0 ), Qt::UserRole ).toString(), QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" ); + QCOMPARE( dlg->selectedOptions().size(), 2 ); + QCOMPARE( dlg->selectedOptions().at( 0 ).toString(), raster->source() ); + QCOMPARE( dlg->selectedOptions().at( 1 ).toString(), QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" ); + + // should remember layer order + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList() << QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" << raster->source(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 2 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 1, 0 ) ).toString(), QStringLiteral( "raster [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 1, 0 ), Qt::UserRole ).toString(), raster->source() ); + QCOMPARE( dlg->selectedOptions().size(), 2 ); + QCOMPARE( dlg->selectedOptions().at( 0 ).toString(), QStringLiteral( TEST_DATA_DIR ) + "/landsat.tif" ); + QCOMPARE( dlg->selectedOptions().at( 1 ).toString(), raster->source() ); + + // mesh + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeMesh ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 1 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "mesh" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), mesh->id() ); + + // vector points + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeVectorPoint ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 1 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "point [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), point->id() ); + + // vector lines + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeVectorLine ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 1 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "line [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), line->id() ); + + // vector polygons + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeVectorPolygon ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 1 ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ) ).toString(), QStringLiteral( "polygon [EPSG:4326]" ) ); + QCOMPARE( dlg->mModel->data( dlg->mModel->index( 0, 0 ), Qt::UserRole ).toString(), polygon->id() ); + + // vector any type + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeVector ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 4 ); + QSet< QString > titles; + for ( int i = 0; i < dlg->mModel->rowCount(); ++i ) + titles << dlg->mModel->data( dlg->mModel->index( i, 0 ) ).toString(); + QCOMPARE( titles, QSet() << QStringLiteral( "polygon [EPSG:4326]" ) << QStringLiteral( "point [EPSG:4326]" ) << QStringLiteral( "line [EPSG:4326]" ) << QStringLiteral( "nogeom" ) ); + + // any type + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeMapLayer ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 6 ); + titles.clear(); + for ( int i = 0; i < dlg->mModel->rowCount(); ++i ) + titles << dlg->mModel->data( dlg->mModel->index( i, 0 ) ).toString(); + QCOMPARE( titles, QSet() << QStringLiteral( "polygon [EPSG:4326]" ) << QStringLiteral( "point [EPSG:4326]" ) << QStringLiteral( "line [EPSG:4326]" ) + << QStringLiteral( "nogeom" ) << QStringLiteral( "raster [EPSG:4326]" ) << QStringLiteral( "mesh" ) ); + + // files + param = qgis::make_unique< QgsProcessingParameterMultipleLayers >( QString(), QString(), QgsProcessing::TypeFile ); + dlg = qgis::make_unique< QgsProcessingMultipleInputPanelWidget >( param.get(), QVariantList(), QList() ); + dlg->setProject( QgsProject::instance() ); + QCOMPARE( dlg->mModel->rowCount(), 0 ); +} + +void TestProcessingGui::testRasterBandSelectionPanel() +{ + QgsProcessingParameterBand bandParam( QString(), QString(), QVariant(), QStringLiteral( "INPUT" ), false, true ); + QgsProcessingRasterBandPanelWidget w( nullptr, &bandParam ); + QSignalSpy spy( &w, &QgsProcessingRasterBandPanelWidget::changed ); + + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "0 bands selected" ) ); + w.setValue( QStringLiteral( "1" ) ); QCOMPARE( spy.count(), 1 ); - QCOMPARE( w.value().toList(), QVariantList() << 1 ); - QCOMPARE( w.mLineEdit->text(), QStringLiteral( "1 options selected" ) ); + QCOMPARE( w.value().toList(), QVariantList() << QStringLiteral( "1" ) ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "1 bands selected" ) ); - w.setValue( QVariantList() << 2 << 0 ); + w.setValue( QVariantList() << QStringLiteral( "2" ) << QStringLiteral( "1" ) ); QCOMPARE( spy.count(), 2 ); - QCOMPARE( w.value().toList(), QVariantList() << 2 << 0 ); - QCOMPARE( w.mLineEdit->text(), QStringLiteral( "2 options selected" ) ); + QCOMPARE( w.value().toList(), QVariantList() << QStringLiteral( "2" ) << QStringLiteral( "1" ) ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "2 bands selected" ) ); - w.setValue( QVariant() ); + w.setValue( QVariantList() << 3 << 5 << 1 ); QCOMPARE( spy.count(), 3 ); + QCOMPARE( w.value().toList(), QVariantList() << 3 << 5 << 1 ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "3 bands selected" ) ); + + w.setValue( QVariant() ); + QCOMPARE( spy.count(), 4 ); QCOMPARE( w.value().toList(), QVariantList() ); - QCOMPARE( w.mLineEdit->text(), QStringLiteral( "0 options selected" ) ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "0 bands selected" ) ); } -void TestProcessingGui::testEnumCheckboxPanel() +void TestProcessingGui::testBandWrapper() { - //single value - QgsProcessingParameterEnum param( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), false ); - QgsProcessingEnumCheckboxPanelWidget panel( nullptr, ¶m ); - QSignalSpy spy( &panel, &QgsProcessingEnumCheckboxPanelWidget::changed ); + const QgsProcessingAlgorithm *statsAlg = QgsApplication::processingRegistry()->algorithmById( QStringLiteral( "native:rasterlayerstatistics" ) ); + const QgsProcessingParameterDefinition *layerDef = statsAlg->parameterDefinition( QStringLiteral( "INPUT" ) ); - QCOMPARE( panel.value(), QVariant() ); - panel.setValue( 2 ); - QCOMPARE( spy.count(), 1 ); - QCOMPARE( panel.value().toInt(), 2 ); - QVERIFY( !panel.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel.mButtons[ 1 ]->isChecked() ); - QVERIFY( panel.mButtons[ 2 ]->isChecked() ); - panel.setValue( 0 ); - QCOMPARE( spy.count(), 2 ); - QCOMPARE( panel.value().toInt(), 0 ); - QVERIFY( panel.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel.mButtons[ 1 ]->isChecked() ); - QVERIFY( !panel.mButtons[ 2 ]->isChecked() ); - panel.mButtons[1]->setChecked( true ); - QCOMPARE( spy.count(), 4 ); - QCOMPARE( panel.value().toInt(), 1 ); - panel.setValue( QVariantList() << 2 ); - QCOMPARE( spy.count(), 5 ); - QCOMPARE( panel.value().toInt(), 2 ); - QVERIFY( !panel.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel.mButtons[ 1 ]->isChecked() ); - QVERIFY( panel.mButtons[ 2 ]->isChecked() ); + auto testWrapper = [layerDef]( QgsProcessingGui::WidgetType type ) + { + TestLayerWrapper layerWrapper( layerDef ); + QgsProject p; + QgsRasterLayer *rl = new QgsRasterLayer( TEST_DATA_DIR + QStringLiteral( "/landsat.tif" ), QStringLiteral( "x" ), QStringLiteral( "gdal" ) ); + p.addMapLayer( rl ); - // multiple value - QgsProcessingParameterEnum param2( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true ); - QgsProcessingEnumCheckboxPanelWidget panel2( nullptr, ¶m2 ); - QSignalSpy spy2( &panel2, &QgsProcessingEnumCheckboxPanelWidget::changed ); - - QCOMPARE( panel2.value().toList(), QVariantList() ); - panel2.setValue( 2 ); - QCOMPARE( spy2.count(), 1 ); - QCOMPARE( panel2.value().toList(), QVariantList() << 2 ); - QVERIFY( !panel2.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel2.mButtons[ 1 ]->isChecked() ); - QVERIFY( panel2.mButtons[ 2 ]->isChecked() ); - panel2.setValue( QVariantList() << 0 << 1 ); - QCOMPARE( spy2.count(), 2 ); - QCOMPARE( panel2.value().toList(), QVariantList() << 0 << 1 ); - QVERIFY( panel2.mButtons[ 0 ]->isChecked() ); - QVERIFY( panel2.mButtons[ 1 ]->isChecked() ); - QVERIFY( !panel2.mButtons[ 2 ]->isChecked() ); - panel2.mButtons[0]->setChecked( false ); - QCOMPARE( spy2.count(), 3 ); - QCOMPARE( panel2.value().toList(), QVariantList() << 1 ); - panel2.mButtons[2]->setChecked( true ); - QCOMPARE( spy2.count(), 4 ); - QCOMPARE( panel2.value().toList(), QVariantList() << 1 << 2 ); - panel2.deselectAll(); - QCOMPARE( spy2.count(), 5 ); - QCOMPARE( panel2.value().toList(), QVariantList() ); - panel2.selectAll(); - QCOMPARE( spy2.count(), 6 ); - QCOMPARE( panel2.value().toList(), QVariantList() << 0 << 1 << 2 ); - - // multiple value optional - QgsProcessingParameterEnum param3( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true, QVariant(), true ); - QgsProcessingEnumCheckboxPanelWidget panel3( nullptr, ¶m3 ); - QSignalSpy spy3( &panel3, &QgsProcessingEnumCheckboxPanelWidget::changed ); - - QCOMPARE( panel3.value().toList(), QVariantList() ); - panel3.setValue( 2 ); - QCOMPARE( spy3.count(), 1 ); - QCOMPARE( panel3.value().toList(), QVariantList() << 2 ); - QVERIFY( !panel3.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel3.mButtons[ 1 ]->isChecked() ); - QVERIFY( panel3.mButtons[ 2 ]->isChecked() ); - panel3.setValue( QVariantList() << 0 << 1 ); - QCOMPARE( spy3.count(), 2 ); - QCOMPARE( panel3.value().toList(), QVariantList() << 0 << 1 ); - QVERIFY( panel3.mButtons[ 0 ]->isChecked() ); - QVERIFY( panel3.mButtons[ 1 ]->isChecked() ); - QVERIFY( !panel3.mButtons[ 2 ]->isChecked() ); - panel3.mButtons[0]->setChecked( false ); - QCOMPARE( spy3.count(), 3 ); - QCOMPARE( panel3.value().toList(), QVariantList() << 1 ); - panel3.mButtons[2]->setChecked( true ); - QCOMPARE( spy3.count(), 4 ); - QCOMPARE( panel3.value().toList(), QVariantList() << 1 << 2 ); - panel3.deselectAll(); - QCOMPARE( spy3.count(), 5 ); - QCOMPARE( panel3.value().toList(), QVariantList() ); - panel3.selectAll(); - QCOMPARE( spy3.count(), 6 ); - QCOMPARE( panel3.value().toList(), QVariantList() << 0 << 1 << 2 ); - panel3.setValue( QVariantList() ); - QCOMPARE( panel3.value().toList(), QVariantList() ); - QVERIFY( !panel3.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel3.mButtons[ 1 ]->isChecked() ); - QVERIFY( !panel3.mButtons[ 2 ]->isChecked() ); - QCOMPARE( spy3.count(), 7 ); - panel3.selectAll(); - QCOMPARE( spy3.count(), 8 ); - panel3.setValue( QVariant() ); - QCOMPARE( panel3.value().toList(), QVariantList() ); - QVERIFY( !panel3.mButtons[ 0 ]->isChecked() ); - QVERIFY( !panel3.mButtons[ 1 ]->isChecked() ); - QVERIFY( !panel3.mButtons[ 2 ]->isChecked() ); - QCOMPARE( spy3.count(), 9 ); -} - -void TestProcessingGui::testEnumWrapper() -{ - auto testWrapper = []( QgsProcessingGui::WidgetType type, bool checkboxStyle = false ) - { - // non optional, single value - QgsProcessingParameterEnum param( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), false ); - QVariantMap metadata; - QVariantMap wrapperMetadata; - wrapperMetadata.insert( QStringLiteral( "useCheckBoxes" ), true ); - metadata.insert( QStringLiteral( "widget_wrapper" ), wrapperMetadata ); - if ( checkboxStyle ) - param.setMetadata( metadata ); + QgsProcessingParameterBand param( QStringLiteral( "band" ), QStringLiteral( "band" ), QVariant(), QStringLiteral( "INPUT" ) ); - QgsProcessingEnumWidgetWrapper wrapper( ¶m, type ); + QgsProcessingBandWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + ( void )w; + layerWrapper.setWidgetValue( QVariant::fromValue( rl ), context ); + wrapper.setParentLayerWrapperValue( &layerWrapper ); - QSignalSpy spy( &wrapper, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( 1, context ); + QSignalSpy spy( &wrapper, &QgsProcessingBandWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( 3, context ); QCOMPARE( spy.count(), 1 ); - QCOMPARE( wrapper.widgetValue().toInt(), 1 ); - if ( !checkboxStyle ) - { - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 1 ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); - } - else + QCOMPARE( wrapper.widgetValue().toInt(), 3 ); + + switch ( type ) { - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper.wrappedWidget() )->value().toInt(), 1 ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsRasterBandComboBox * >( wrapper.wrappedWidget() )->currentBand(), 3 ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "3" ) ); + break; } - wrapper.setWidgetValue( 0, context ); + + wrapper.setWidgetValue( QStringLiteral( "1" ), context ); QCOMPARE( spy.count(), 2 ); - QCOMPARE( wrapper.widgetValue().toInt(), 0 ); - if ( !checkboxStyle ) + QCOMPARE( wrapper.widgetValue().toInt(), 1 ); + + switch ( type ) { - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 0 ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsRasterBandComboBox * >( wrapper.wrappedWidget() )->currentBand(), 1 ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "1" ) ); + break; } - else + + delete w; + + // optional + param = QgsProcessingParameterBand( QStringLiteral( "band" ), QStringLiteral( "band" ), QVariant(), QStringLiteral( "INPUT" ), true, false ); + + QgsProcessingBandWidgetWrapper wrapper2( ¶m, type ); + + w = wrapper2.createWrappedWidget( context ); + layerWrapper.setWidgetValue( QVariant::fromValue( rl ), context ); + wrapper2.setParentLayerWrapperValue( &layerWrapper ); + QSignalSpy spy2( &wrapper2, &QgsProcessingBandWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "4" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toInt(), 4 ); + + wrapper2.setWidgetValue( QVariant(), context ); + QCOMPARE( spy2.count(), 2 ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + + switch ( type ) { - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper.wrappedWidget() )->value().toInt(), 0 ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsRasterBandComboBox * >( wrapper2.wrappedWidget() )->currentBand(), -1 ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QString() ); + break; } QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "enum" ) ); + QCOMPARE( l->text(), QStringLiteral( "band [optional]" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -2911,185 +3357,156 @@ void TestProcessingGui::testEnumWrapper() } // check signal - if ( !checkboxStyle ) - static_cast< QComboBox * >( wrapper.wrappedWidget() )->setCurrentIndex( 2 ); - else - static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper.wrappedWidget() )->setValue( 2 ); - QCOMPARE( spy.count(), 3 ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + static_cast< QgsRasterBandComboBox * >( wrapper2.wrappedWidget() )->setBand( 6 ); + break; - delete w; + case QgsProcessingGui::Modeler: + static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->setText( QStringLiteral( "6" ) ); + break; + } - // optional + QCOMPARE( spy2.count(), 3 ); - QgsProcessingParameterEnum param2( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), false, QVariant(), true ); - if ( checkboxStyle ) - param2.setMetadata( metadata ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( wrapper2.mComboBox->layer(), rl ); + break; - QgsProcessingEnumWidgetWrapper wrapper2( ¶m2, type ); + case QgsProcessingGui::Modeler: + break; + } - w = wrapper2.createWrappedWidget( context ); + // should not be owned by wrapper + QVERIFY( !wrapper2.mParentLayer.get() ); + layerWrapper.setWidgetValue( QVariant(), context ); + wrapper2.setParentLayerWrapperValue( &layerWrapper ); - QSignalSpy spy2( &wrapper2, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper2.setWidgetValue( 1, context ); - QCOMPARE( spy2.count(), 1 ); - QCOMPARE( wrapper2.widgetValue().toInt(), 1 ); - if ( !checkboxStyle ) - { - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 2 ); - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); - } - else + switch ( type ) { - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper2.wrappedWidget() )->value().toInt(), 1 ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QVERIFY( !wrapper2.mComboBox->layer() ); + break; + + case QgsProcessingGui::Modeler: + break; } - wrapper2.setWidgetValue( 0, context ); - QCOMPARE( spy2.count(), 2 ); - QCOMPARE( wrapper2.widgetValue().toInt(), 0 ); - if ( !checkboxStyle ) + + layerWrapper.setWidgetValue( rl->id(), context ); + wrapper2.setParentLayerWrapperValue( &layerWrapper ); + switch ( type ) { - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 1 ); - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QVERIFY( !wrapper2.mComboBox->layer() ); + break; + + case QgsProcessingGui::Modeler: + break; } - else + QVERIFY( !wrapper2.mParentLayer.get() ); + + // with project layer + context.setProject( &p ); + TestProcessingContextGenerator generator( context ); + wrapper2.registerProcessingContextGenerator( &generator ); + + layerWrapper.setWidgetValue( rl->id(), context ); + wrapper2.setParentLayerWrapperValue( &layerWrapper ); + switch ( type ) { - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper2.wrappedWidget() )->value().toInt(), 0 ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( wrapper2.mComboBox->layer(), rl ); + break; + + case QgsProcessingGui::Modeler: + break; } - wrapper2.setWidgetValue( QVariant(), context ); - QCOMPARE( spy2.count(), 3 ); - if ( !checkboxStyle ) + QVERIFY( !wrapper2.mParentLayer.get() ); + + // non-project layer + QString rasterFileName = TEST_DATA_DIR + QStringLiteral( "/landsat-f32-b1.tif" ); + layerWrapper.setWidgetValue( rasterFileName, context ); + wrapper2.setParentLayerWrapperValue( &layerWrapper ); + switch ( type ) { - QVERIFY( !wrapper2.widgetValue().isValid() ); - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 0 ); - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "[Not selected]" ) ); + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( wrapper2.mComboBox->layer()->publicSource(), rasterFileName ); + break; + + case QgsProcessingGui::Modeler: + break; } - // check signal - if ( !checkboxStyle ) - static_cast< QComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 2 ); - else - static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper2.wrappedWidget() )->setValue( 1 ); - QCOMPARE( spy2.count(), 4 ); + // must be owned by wrapper, or layer may be deleted while still required by wrapper + QCOMPARE( wrapper2.mParentLayer->publicSource(), rasterFileName ); delete w; - // allow multiple - QgsProcessingParameterEnum param3( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true, QVariant(), false ); - if ( checkboxStyle ) - param3.setMetadata( metadata ); + // multiple + param = QgsProcessingParameterBand( QStringLiteral( "band" ), QStringLiteral( "band" ), QVariant(), QStringLiteral( "INPUT" ), true, true ); - QgsProcessingEnumWidgetWrapper wrapper3( ¶m3, type ); + QgsProcessingBandWidgetWrapper wrapper3( ¶m, type ); w = wrapper3.createWrappedWidget( context ); - - QSignalSpy spy3( &wrapper3, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper3.setWidgetValue( 1, context ); + layerWrapper.setWidgetValue( QVariant::fromValue( rl ), context ); + wrapper3.setParentLayerWrapperValue( &layerWrapper ); + QSignalSpy spy3( &wrapper3, &QgsProcessingBandWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "5" ), context ); QCOMPARE( spy3.count(), 1 ); - QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 1 ); - if ( !checkboxStyle ) - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 1 ); - else - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 1 ); - wrapper3.setWidgetValue( 0, context ); + QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 5 ); + + wrapper3.setWidgetValue( QString(), context ); QCOMPARE( spy3.count(), 2 ); - QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 0 ); - if ( !checkboxStyle ) - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 0 ); - else - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 0 ); - wrapper3.setWidgetValue( QVariantList() << 2 << 1, context ); + QVERIFY( wrapper3.widgetValue().toString().isEmpty() ); + + wrapper3.setWidgetValue( QStringLiteral( "3;4" ), context ); QCOMPARE( spy3.count(), 3 ); - if ( !checkboxStyle ) - { - QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 2 << 1 ); - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 2 << 1 ); - } - else - { - // checkbox style isn't ordered - QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 1 << 2 ); - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 1 << 2 ); - } - // check signal - if ( !checkboxStyle ) - static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); - else - static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); + QCOMPARE( wrapper3.widgetValue().toStringList(), QStringList() << QStringLiteral( "3" ) << QStringLiteral( "4" ) ); + wrapper3.setWidgetValue( QVariantList() << 5 << 6 << 7, context ); QCOMPARE( spy3.count(), 4 ); + QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 5 << 6 << 7 ); - delete w; + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 5 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); - // allow multiple, optional - QgsProcessingParameterEnum param4( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true, QVariant(), false ); - if ( checkboxStyle ) - param4.setMetadata( metadata ); - QgsProcessingEnumWidgetWrapper wrapper4( ¶m4, type ); + // multiple non-optional + param = QgsProcessingParameterBand( QStringLiteral( "band" ), QStringLiteral( "band" ), QVariant(), QStringLiteral( "INPUT" ), false, true ); - w = wrapper4.createWrappedWidget( context ); + QgsProcessingBandWidgetWrapper wrapper4( ¶m, type ); - QSignalSpy spy4( &wrapper4, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper4.setWidgetValue( 1, context ); + w = wrapper4.createWrappedWidget( context ); + layerWrapper.setWidgetValue( QVariant::fromValue( rl ), context ); + wrapper4.setParentLayerWrapperValue( &layerWrapper ); + QSignalSpy spy4( &wrapper4, &QgsProcessingBandWidgetWrapper::widgetValueHasChanged ); + wrapper4.setWidgetValue( QStringLiteral( "5" ), context ); QCOMPARE( spy4.count(), 1 ); - QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 1 ); - if ( !checkboxStyle ) - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 1 ); - else - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 1 ); - wrapper4.setWidgetValue( 0, context ); + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 5 ); + + wrapper4.setWidgetValue( QString(), context ); QCOMPARE( spy4.count(), 2 ); - QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 0 ); - if ( !checkboxStyle ) - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 0 ); - else - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 0 ); - wrapper4.setWidgetValue( QVariantList() << 2 << 1, context ); - QCOMPARE( spy4.count(), 3 ); - if ( !checkboxStyle ) - { - QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 2 << 1 ); - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 2 << 1 ); - } - else - { - // checkbox style isn't ordered - QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 1 << 2 ); - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 1 << 2 ); - } - wrapper4.setWidgetValue( QVariantList(), context ); - QCOMPARE( spy4.count(), 4 ); - QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() ); - if ( !checkboxStyle ) - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); - else - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); + QVERIFY( wrapper4.widgetValue().toString().isEmpty() ); - wrapper4.setWidgetValue( QVariant(), context ); - QCOMPARE( spy4.count(), 5 ); - QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() ); - if ( !checkboxStyle ) - QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); - else - QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); + wrapper4.setWidgetValue( QStringLiteral( "3;4" ), context ); + QCOMPARE( spy4.count(), 3 ); + QCOMPARE( wrapper4.widgetValue().toStringList(), QStringList() << QStringLiteral( "3" ) << QStringLiteral( "4" ) ); - // check signal - if ( !checkboxStyle ) - { - static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); - QCOMPARE( spy4.count(), 6 ); - static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariant() ); - QCOMPARE( spy4.count(), 7 ); - } - else - { - static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); - QCOMPARE( spy4.count(), 6 ); - static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariant() ); - QCOMPARE( spy4.count(), 7 ); - } + wrapper4.setWidgetValue( QVariantList() << 5 << 6 << 7, context ); + QCOMPARE( spy4.count(), 4 ); + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 5 << 6 << 7 ); delete w; - }; // standard wrapper @@ -3101,66 +3518,90 @@ void TestProcessingGui::testEnumWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); - // checkbox style (not for batch or model mode!) - testWrapper( QgsProcessingGui::Standard, true ); + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "band" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + // using a parameter definition as initial values + QgsProcessingParameterBand bandParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), 1, QStringLiteral( "parent" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "band" ), context, widgetContext, &bandParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterBand * >( def.get() )->defaultValue().toString(), QStringLiteral( "1" ) ); + QCOMPARE( static_cast< QgsProcessingParameterBand * >( def.get() )->allowMultiple(), false ); + QCOMPARE( static_cast< QgsProcessingParameterBand * >( def.get() )->parentLayerParameterName(), QStringLiteral( "parent" ) ); + bandParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + bandParam.setParentLayerParameterName( QString() ); + bandParam.setAllowMultiple( true ); + bandParam.setDefaultValue( QVariantList() << 2 << 3 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "band" ), context, widgetContext, &bandParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterBand * >( def.get() )->defaultValue().toStringList(), QStringList() << "2" << "3" ); + QCOMPARE( static_cast< QgsProcessingParameterBand * >( def.get() )->allowMultiple(), true ); + QVERIFY( static_cast< QgsProcessingParameterBand * >( def.get() )->parentLayerParameterName().isEmpty() ); } -void TestProcessingGui::testLayoutWrapper() +void TestProcessingGui::testMultipleInputWrapper() { - QgsProject p; - QgsPrintLayout *l1 = new QgsPrintLayout( &p ); - l1->setName( "l1" ); - p.layoutManager()->addLayout( l1 ); - QgsPrintLayout *l2 = new QgsPrintLayout( &p ); - l2->setName( "l2" ); - p.layoutManager()->addLayout( l2 ); + QString path1 = TEST_DATA_DIR + QStringLiteral( "/landsat-f32-b1.tif" ); + QString path2 = TEST_DATA_DIR + QStringLiteral( "/landsat.tif" ); - auto testWrapper = [&p]( QgsProcessingGui::WidgetType type ) + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) { - // non optional - QgsProcessingParameterLayout param( QStringLiteral( "layout" ), QStringLiteral( "layout" ), false ); + QgsProcessingParameterMultipleLayers param( QStringLiteral( "multi" ), QStringLiteral( "multi" ), QgsProcessing::TypeVector, QVariant(), false ); - QgsProcessingLayoutWidgetWrapper wrapper( ¶m, type ); + QgsProcessingMultipleLayerWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; - context.setProject( &p ); - QgsProcessingParameterWidgetContext widgetContext; - widgetContext.setProject( &p ); - wrapper.setWidgetContext( widgetContext ); + QWidget *w = wrapper.createWrappedWidget( context ); + ( void )w; - QSignalSpy spy( &wrapper, &QgsProcessingLayoutWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( "l2", context ); + QSignalSpy spy( &wrapper, &QgsProcessingMultipleLayerWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QVariantList() << path1 << path2, context ); QCOMPARE( spy.count(), 1 ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "l2" ) ); - if ( type != QgsProcessingGui::Modeler ) - { - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 1 ); - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "l2" ) ); - } - else - { - QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "l2" ) ); - } - wrapper.setWidgetValue( "l1", context ); + QCOMPARE( wrapper.widgetValue().toList(), QVariantList() << path1 << path2 ); + QCOMPARE( static_cast< QgsProcessingMultipleLayerPanelWidget * >( wrapper.wrappedWidget() )->value().toList(), QVariantList() << path1 << path2 ); + + wrapper.setWidgetValue( path1, context ); QCOMPARE( spy.count(), 2 ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "l1" ) ); - if ( type != QgsProcessingGui::Modeler ) - { - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 0 ); - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); - } - else - { - QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "l1" ) ); - } + QCOMPARE( wrapper.widgetValue().toStringList(), QStringList() << path1 ); + QCOMPARE( static_cast< QgsProcessingMultipleLayerPanelWidget * >( wrapper.wrappedWidget() )->value().toList(), QVariantList() << path1 ); + delete w; + + // optional + param = QgsProcessingParameterMultipleLayers( QStringLiteral( "multi" ), QStringLiteral( "multi" ), QgsProcessing::TypeVector, QVariant(), true ); + + QgsProcessingMultipleLayerWidgetWrapper wrapper2( ¶m, type ); + + w = wrapper2.createWrappedWidget( context ); + QSignalSpy spy2( &wrapper2, &QgsProcessingMultipleLayerWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( path2, context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toList(), QVariantList() << path2 ); + + wrapper2.setWidgetValue( QVariant(), context ); + QCOMPARE( spy2.count(), 2 ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + QVERIFY( static_cast< QgsProcessingMultipleLayerPanelWidget * >( wrapper2.wrappedWidget() )->value().toList().isEmpty() ); QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "layout" ) ); + QCOMPARE( l->text(), QStringLiteral( "multi [optional]" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -3170,72 +3611,30 @@ void TestProcessingGui::testLayoutWrapper() } // check signal - if ( type != QgsProcessingGui::Modeler ) - { - static_cast< QComboBox * >( wrapper.wrappedWidget() )->setCurrentIndex( 1 ); - } - else - { - static_cast< QLineEdit * >( wrapper.wrappedWidget() )->setText( QStringLiteral( "aaaa" ) ); - } - QCOMPARE( spy.count(), 3 ); - - delete w; - - // optional - - QgsProcessingParameterLayout param2( QStringLiteral( "layout" ), QStringLiteral( "layout" ), QVariant(), true ); + static_cast< QgsProcessingMultipleLayerPanelWidget * >( wrapper2.wrappedWidget() )->setValue( QVariantList() << path1 ); + QCOMPARE( spy2.count(), 3 ); - QgsProcessingLayoutWidgetWrapper wrapper2( ¶m2, type ); - wrapper2.setWidgetContext( widgetContext ); - w = wrapper2.createWrappedWidget( context ); - QSignalSpy spy2( &wrapper2, &QgsProcessingLayoutWidgetWrapper::widgetValueHasChanged ); - wrapper2.setWidgetValue( "l2", context ); - QCOMPARE( spy2.count(), 1 ); - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "l2" ) ); - if ( type != QgsProcessingGui::Modeler ) - { - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 2 ); - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l2" ) ); - } - else - { - QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "l2" ) ); - } - wrapper2.setWidgetValue( "l1", context ); - QCOMPARE( spy2.count(), 2 ); - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "l1" ) ); - if ( type != QgsProcessingGui::Modeler ) - { - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 1 ); - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); - } - else - { - QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "l1" ) ); - } - wrapper2.setWidgetValue( QVariant(), context ); - QCOMPARE( spy2.count(), 3 ); - QVERIFY( !wrapper2.widgetValue().isValid() ); - if ( type != QgsProcessingGui::Modeler ) - { - QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 0 ); - QVERIFY( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentText().isEmpty() ); - } - else + if ( wrapper.type() == QgsProcessingGui::Modeler ) { - QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); + // different mix of sources + + wrapper2.setWidgetValue( QVariantList() + << QVariant::fromValue( QgsProcessingModelChildParameterSource::fromChildOutput( QStringLiteral( "alg3" ), QStringLiteral( "OUTPUT" ) ) ) + << QVariant::fromValue( QgsProcessingModelChildParameterSource::fromModelParameter( QStringLiteral( "p1" ) ) ) + << QVariant::fromValue( QgsProcessingModelChildParameterSource::fromStaticValue( QStringLiteral( "something" ) ) ), context ) ; + QCOMPARE( wrapper2.widgetValue().toList().count(), 3 ); + + QCOMPARE( wrapper2.widgetValue().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ChildOutput ); + QCOMPARE( wrapper2.widgetValue().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ChildOutput ); + QCOMPARE( wrapper2.widgetValue().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().outputChildId(), QStringLiteral( "alg3" ) ); + QCOMPARE( wrapper2.widgetValue().toList().at( 0 ).value< QgsProcessingModelChildParameterSource>().outputName(), QStringLiteral( "OUTPUT" ) ); + QCOMPARE( wrapper2.widgetValue().toList().at( 1 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::ModelParameter ); + QCOMPARE( wrapper2.widgetValue().toList().at( 1 ).value< QgsProcessingModelChildParameterSource>().parameterName(), QStringLiteral( "p1" ) ); + QCOMPARE( wrapper2.widgetValue().toList().at( 2 ).value< QgsProcessingModelChildParameterSource>().source(), QgsProcessingModelChildParameterSource::StaticValue ); + QCOMPARE( wrapper2.widgetValue().toList().at( 2 ).value< QgsProcessingModelChildParameterSource>().staticValue().toString(), QStringLiteral( "something" ) ); + delete w; } - - // check signal - if ( type != QgsProcessingGui::Modeler ) - static_cast< QComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 2 ); - else - static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->setText( QStringLiteral( "aaa" ) ); - QCOMPARE( spy2.count(), 4 ); - - delete w; }; // standard wrapper @@ -3247,68 +3646,214 @@ void TestProcessingGui::testLayoutWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); -} - -void TestProcessingGui::testLayoutItemWrapper() -{ - QgsProject p; - QgsPrintLayout *l1 = new QgsPrintLayout( &p ); - l1->setName( "l1" ); - p.layoutManager()->addLayout( l1 ); - QgsLayoutItemLabel *label1 = new QgsLayoutItemLabel( l1 ); - label1->setId( "a" ); - l1->addLayoutItem( label1 ); - QgsLayoutItemLabel *label2 = new QgsLayoutItemLabel( l1 ); - label2->setId( "b" ); - l1->addLayoutItem( label2 ); + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "multilayer" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - auto testWrapper = [&p, l1, label1, label2]( QgsProcessingGui::WidgetType type ) + // using a parameter definition as initial values + QgsProcessingParameterMultipleLayers layersParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "multilayer" ), context, widgetContext, &layersParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterMultipleLayers * >( def.get() )->layerType(), QgsProcessing::TypeVectorAnyGeometry ); + layersParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + layersParam.setLayerType( QgsProcessing::TypeRaster ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "multilayer" ), context, widgetContext, &layersParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterMultipleLayers * >( def.get() )->layerType(), QgsProcessing::TypeRaster ); +} + +void TestProcessingGui::testEnumSelectionPanel() +{ + QgsProcessingParameterEnum enumParam( QString(), QString(), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true ); + QgsProcessingEnumPanelWidget w( nullptr, &enumParam ); + QSignalSpy spy( &w, &QgsProcessingEnumPanelWidget::changed ); + + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "0 options selected" ) ); + w.setValue( 1 ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( w.value().toList(), QVariantList() << 1 ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "1 options selected" ) ); + + w.setValue( QVariantList() << 2 << 0 ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( w.value().toList(), QVariantList() << 2 << 0 ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "2 options selected" ) ); + + w.setValue( QVariant() ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( w.value().toList(), QVariantList() ); + QCOMPARE( w.mLineEdit->text(), QStringLiteral( "0 options selected" ) ); +} + +void TestProcessingGui::testEnumCheckboxPanel() +{ + //single value + QgsProcessingParameterEnum param( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), false ); + QgsProcessingEnumCheckboxPanelWidget panel( nullptr, ¶m ); + QSignalSpy spy( &panel, &QgsProcessingEnumCheckboxPanelWidget::changed ); + + QCOMPARE( panel.value(), QVariant() ); + panel.setValue( 2 ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( panel.value().toInt(), 2 ); + QVERIFY( !panel.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel.mButtons[ 1 ]->isChecked() ); + QVERIFY( panel.mButtons[ 2 ]->isChecked() ); + panel.setValue( 0 ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( panel.value().toInt(), 0 ); + QVERIFY( panel.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel.mButtons[ 1 ]->isChecked() ); + QVERIFY( !panel.mButtons[ 2 ]->isChecked() ); + panel.mButtons[1]->setChecked( true ); + QCOMPARE( spy.count(), 4 ); + QCOMPARE( panel.value().toInt(), 1 ); + panel.setValue( QVariantList() << 2 ); + QCOMPARE( spy.count(), 5 ); + QCOMPARE( panel.value().toInt(), 2 ); + QVERIFY( !panel.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel.mButtons[ 1 ]->isChecked() ); + QVERIFY( panel.mButtons[ 2 ]->isChecked() ); + + // multiple value + QgsProcessingParameterEnum param2( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true ); + QgsProcessingEnumCheckboxPanelWidget panel2( nullptr, ¶m2 ); + QSignalSpy spy2( &panel2, &QgsProcessingEnumCheckboxPanelWidget::changed ); + + QCOMPARE( panel2.value().toList(), QVariantList() ); + panel2.setValue( 2 ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( panel2.value().toList(), QVariantList() << 2 ); + QVERIFY( !panel2.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel2.mButtons[ 1 ]->isChecked() ); + QVERIFY( panel2.mButtons[ 2 ]->isChecked() ); + panel2.setValue( QVariantList() << 0 << 1 ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( panel2.value().toList(), QVariantList() << 0 << 1 ); + QVERIFY( panel2.mButtons[ 0 ]->isChecked() ); + QVERIFY( panel2.mButtons[ 1 ]->isChecked() ); + QVERIFY( !panel2.mButtons[ 2 ]->isChecked() ); + panel2.mButtons[0]->setChecked( false ); + QCOMPARE( spy2.count(), 3 ); + QCOMPARE( panel2.value().toList(), QVariantList() << 1 ); + panel2.mButtons[2]->setChecked( true ); + QCOMPARE( spy2.count(), 4 ); + QCOMPARE( panel2.value().toList(), QVariantList() << 1 << 2 ); + panel2.deselectAll(); + QCOMPARE( spy2.count(), 5 ); + QCOMPARE( panel2.value().toList(), QVariantList() ); + panel2.selectAll(); + QCOMPARE( spy2.count(), 6 ); + QCOMPARE( panel2.value().toList(), QVariantList() << 0 << 1 << 2 ); + + // multiple value optional + QgsProcessingParameterEnum param3( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true, QVariant(), true ); + QgsProcessingEnumCheckboxPanelWidget panel3( nullptr, ¶m3 ); + QSignalSpy spy3( &panel3, &QgsProcessingEnumCheckboxPanelWidget::changed ); + + QCOMPARE( panel3.value().toList(), QVariantList() ); + panel3.setValue( 2 ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( panel3.value().toList(), QVariantList() << 2 ); + QVERIFY( !panel3.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel3.mButtons[ 1 ]->isChecked() ); + QVERIFY( panel3.mButtons[ 2 ]->isChecked() ); + panel3.setValue( QVariantList() << 0 << 1 ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( panel3.value().toList(), QVariantList() << 0 << 1 ); + QVERIFY( panel3.mButtons[ 0 ]->isChecked() ); + QVERIFY( panel3.mButtons[ 1 ]->isChecked() ); + QVERIFY( !panel3.mButtons[ 2 ]->isChecked() ); + panel3.mButtons[0]->setChecked( false ); + QCOMPARE( spy3.count(), 3 ); + QCOMPARE( panel3.value().toList(), QVariantList() << 1 ); + panel3.mButtons[2]->setChecked( true ); + QCOMPARE( spy3.count(), 4 ); + QCOMPARE( panel3.value().toList(), QVariantList() << 1 << 2 ); + panel3.deselectAll(); + QCOMPARE( spy3.count(), 5 ); + QCOMPARE( panel3.value().toList(), QVariantList() ); + panel3.selectAll(); + QCOMPARE( spy3.count(), 6 ); + QCOMPARE( panel3.value().toList(), QVariantList() << 0 << 1 << 2 ); + panel3.setValue( QVariantList() ); + QCOMPARE( panel3.value().toList(), QVariantList() ); + QVERIFY( !panel3.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel3.mButtons[ 1 ]->isChecked() ); + QVERIFY( !panel3.mButtons[ 2 ]->isChecked() ); + QCOMPARE( spy3.count(), 7 ); + panel3.selectAll(); + QCOMPARE( spy3.count(), 8 ); + panel3.setValue( QVariant() ); + QCOMPARE( panel3.value().toList(), QVariantList() ); + QVERIFY( !panel3.mButtons[ 0 ]->isChecked() ); + QVERIFY( !panel3.mButtons[ 1 ]->isChecked() ); + QVERIFY( !panel3.mButtons[ 2 ]->isChecked() ); + QCOMPARE( spy3.count(), 9 ); +} + +void TestProcessingGui::testEnumWrapper() +{ + auto testWrapper = []( QgsProcessingGui::WidgetType type, bool checkboxStyle = false ) { - // non optional - QgsProcessingParameterLayoutItem param( QStringLiteral( "layout" ), QStringLiteral( "layout" ), false ); + // non optional, single value + QgsProcessingParameterEnum param( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), false ); + QVariantMap metadata; + QVariantMap wrapperMetadata; + wrapperMetadata.insert( QStringLiteral( "useCheckBoxes" ), true ); + metadata.insert( QStringLiteral( "widget_wrapper" ), wrapperMetadata ); + if ( checkboxStyle ) + param.setMetadata( metadata ); - QgsProcessingLayoutItemWidgetWrapper wrapper( ¶m, type ); + QgsProcessingEnumWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; - context.setProject( &p ); - QgsProcessingParameterWidgetContext widgetContext; - widgetContext.setProject( &p ); - wrapper.setWidgetContext( widgetContext ); QWidget *w = wrapper.createWrappedWidget( context ); - wrapper.setLayout( l1 ); - - QSignalSpy spy( &wrapper, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( "b", context ); + QSignalSpy spy( &wrapper, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( 1, context ); QCOMPARE( spy.count(), 1 ); - if ( type != QgsProcessingGui::Modeler ) + QCOMPARE( wrapper.widgetValue().toInt(), 1 ); + if ( !checkboxStyle ) { - QCOMPARE( wrapper.widgetValue().toString(), label2->uuid() ); - QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 1 ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); } else { - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "b" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper.wrappedWidget() )->value().toInt(), 1 ); } - wrapper.setWidgetValue( "a", context ); + wrapper.setWidgetValue( 0, context ); QCOMPARE( spy.count(), 2 ); - if ( type != QgsProcessingGui::Modeler ) + QCOMPARE( wrapper.widgetValue().toInt(), 0 ); + if ( !checkboxStyle ) { - QCOMPARE( wrapper.widgetValue().toString(), label1->uuid() ); - QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 0 ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); } else { - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "a" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper.wrappedWidget() )->value().toInt(), 0 ); } QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "layout" ) ); + QCOMPARE( l->text(), QStringLiteral( "enum" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -3318,72 +3863,185 @@ void TestProcessingGui::testLayoutItemWrapper() } // check signal - if ( type != QgsProcessingGui::Modeler ) - { - static_cast< QComboBox * >( wrapper.wrappedWidget() )->setCurrentIndex( 1 ); - } + if ( !checkboxStyle ) + static_cast< QComboBox * >( wrapper.wrappedWidget() )->setCurrentIndex( 2 ); else - { - static_cast< QLineEdit * >( wrapper.wrappedWidget() )->setText( QStringLiteral( "aaaa" ) ); - } + static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper.wrappedWidget() )->setValue( 2 ); QCOMPARE( spy.count(), 3 ); delete w; // optional - QgsProcessingParameterLayoutItem param2( QStringLiteral( "layout" ), QStringLiteral( "layout" ), QVariant(), QString(), -1, true ); + QgsProcessingParameterEnum param2( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), false, QVariant(), true ); + if ( checkboxStyle ) + param2.setMetadata( metadata ); + + QgsProcessingEnumWidgetWrapper wrapper2( ¶m2, type ); - QgsProcessingLayoutItemWidgetWrapper wrapper2( ¶m2, type ); - wrapper2.setWidgetContext( widgetContext ); w = wrapper2.createWrappedWidget( context ); - wrapper2.setLayout( l1 ); - QSignalSpy spy2( &wrapper2, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); - wrapper2.setWidgetValue( "b", context ); + QSignalSpy spy2( &wrapper2, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( 1, context ); QCOMPARE( spy2.count(), 1 ); - if ( type != QgsProcessingGui::Modeler ) + QCOMPARE( wrapper2.widgetValue().toInt(), 1 ); + if ( !checkboxStyle ) { - QCOMPARE( wrapper2.widgetValue().toString(), label2->uuid() ); - QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 2 ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); } else { - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "b" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper2.wrappedWidget() )->value().toInt(), 1 ); } - wrapper2.setWidgetValue( "a", context ); + wrapper2.setWidgetValue( 0, context ); QCOMPARE( spy2.count(), 2 ); - if ( type != QgsProcessingGui::Modeler ) + QCOMPARE( wrapper2.widgetValue().toInt(), 0 ); + if ( !checkboxStyle ) { - QCOMPARE( wrapper2.widgetValue().toString(), label1->uuid() ); - QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 1 ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); } else { - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "a" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper2.wrappedWidget() )->value().toInt(), 0 ); } wrapper2.setWidgetValue( QVariant(), context ); QCOMPARE( spy2.count(), 3 ); - QVERIFY( !wrapper2.widgetValue().isValid() ); - if ( type != QgsProcessingGui::Modeler ) + if ( !checkboxStyle ) { - QVERIFY( static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->currentText().isEmpty() ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 0 ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "[Not selected]" ) ); + } + + // check signal + if ( !checkboxStyle ) + static_cast< QComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 2 ); + else + static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper2.wrappedWidget() )->setValue( 1 ); + QCOMPARE( spy2.count(), 4 ); + + delete w; + + // allow multiple + QgsProcessingParameterEnum param3( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true, QVariant(), false ); + if ( checkboxStyle ) + param3.setMetadata( metadata ); + + QgsProcessingEnumWidgetWrapper wrapper3( ¶m3, type ); + + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( 1, context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 1 ); + if ( !checkboxStyle ) + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 1 ); + else + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 1 ); + wrapper3.setWidgetValue( 0, context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 0 ); + if ( !checkboxStyle ) + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 0 ); + else + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 0 ); + wrapper3.setWidgetValue( QVariantList() << 2 << 1, context ); + QCOMPARE( spy3.count(), 3 ); + if ( !checkboxStyle ) + { + QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 2 << 1 ); + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 2 << 1 ); } else { - QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); + // checkbox style isn't ordered + QCOMPARE( wrapper3.widgetValue().toList(), QVariantList() << 1 << 2 ); + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->value().toList(), QVariantList() << 1 << 2 ); + } + // check signal + if ( !checkboxStyle ) + static_cast< QgsProcessingEnumPanelWidget * >( wrapper3.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); + else + static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper3.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); + + QCOMPARE( spy3.count(), 4 ); + + delete w; + + // allow multiple, optional + QgsProcessingParameterEnum param4( QStringLiteral( "enum" ), QStringLiteral( "enum" ), QStringList() << QStringLiteral( "a" ) << QStringLiteral( "b" ) << QStringLiteral( "c" ), true, QVariant(), false ); + if ( checkboxStyle ) + param4.setMetadata( metadata ); + + QgsProcessingEnumWidgetWrapper wrapper4( ¶m4, type ); + + w = wrapper4.createWrappedWidget( context ); + + QSignalSpy spy4( &wrapper4, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper4.setWidgetValue( 1, context ); + QCOMPARE( spy4.count(), 1 ); + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 1 ); + if ( !checkboxStyle ) + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 1 ); + else + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 1 ); + wrapper4.setWidgetValue( 0, context ); + QCOMPARE( spy4.count(), 2 ); + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 0 ); + if ( !checkboxStyle ) + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 0 ); + else + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 0 ); + wrapper4.setWidgetValue( QVariantList() << 2 << 1, context ); + QCOMPARE( spy4.count(), 3 ); + if ( !checkboxStyle ) + { + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 2 << 1 ); + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 2 << 1 ); + } + else + { + // checkbox style isn't ordered + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() << 1 << 2 ); + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() << 1 << 2 ); } + wrapper4.setWidgetValue( QVariantList(), context ); + QCOMPARE( spy4.count(), 4 ); + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() ); + if ( !checkboxStyle ) + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); + else + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); + + wrapper4.setWidgetValue( QVariant(), context ); + QCOMPARE( spy4.count(), 5 ); + QCOMPARE( wrapper4.widgetValue().toList(), QVariantList() ); + if ( !checkboxStyle ) + QCOMPARE( static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); + else + QCOMPARE( static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->value().toList(), QVariantList() ); // check signal - if ( type != QgsProcessingGui::Modeler ) - static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 1 ); + if ( !checkboxStyle ) + { + static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); + QCOMPARE( spy4.count(), 6 ); + static_cast< QgsProcessingEnumPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariant() ); + QCOMPARE( spy4.count(), 7 ); + } else - static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->setText( QStringLiteral( "aaa" ) ); - QCOMPARE( spy2.count(), 4 ); + { + static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariantList() << 0 << 1 ); + QCOMPARE( spy4.count(), 6 ); + static_cast< QgsProcessingEnumCheckboxPanelWidget * >( wrapper4.wrappedWidget() )->setValue( QVariant() ); + QCOMPARE( spy4.count(), 7 ); + } delete w; + }; // standard wrapper @@ -3395,125 +4053,98 @@ void TestProcessingGui::testLayoutItemWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + // checkbox style (not for batch or model mode!) + testWrapper( QgsProcessingGui::Standard, true ); // config widget QgsProcessingParameterWidgetContext widgetContext; QgsProcessingContext context; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layoutitem" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "enum" ), context, widgetContext ); std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); // using a parameter definition as initial values - QgsProcessingParameterLayoutItem itemParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QVariant(), QStringLiteral( "parent" ) ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layoutitem" ), context, widgetContext, &itemParam ); + QgsProcessingParameterEnum enumParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringList() << "A" << "B" << "C", false, 2 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "enum" ), context, widgetContext, &enumParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QCOMPARE( static_cast< QgsProcessingParameterLayoutItem * >( def.get() )->parentLayoutParameterName(), QStringLiteral( "parent" ) ); - itemParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); - itemParam.setParentLayoutParameterName( QString() ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layoutitem" ), context, widgetContext, &itemParam ); + QCOMPARE( static_cast< QgsProcessingParameterEnum * >( def.get() )->options(), QStringList() << "A" << "B" << "C" ); + QCOMPARE( static_cast< QgsProcessingParameterEnum * >( def.get() )->defaultValue().toStringList(), QStringList() << "2" ); + QVERIFY( !static_cast< QgsProcessingParameterEnum * >( def.get() )->allowMultiple() ); + enumParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + enumParam.setAllowMultiple( true ); + enumParam.setDefaultValue( QVariantList() << 0 << 1 ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "enum" ), context, widgetContext, &enumParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); - QVERIFY( static_cast< QgsProcessingParameterLayoutItem * >( def.get() )->parentLayoutParameterName().isEmpty() ); + QCOMPARE( static_cast< QgsProcessingParameterEnum * >( def.get() )->options(), QStringList() << "A" << "B" << "C" ); + QCOMPARE( static_cast< QgsProcessingParameterEnum * >( def.get() )->defaultValue().toStringList(), QStringList() << "0" << "1" ); + QVERIFY( static_cast< QgsProcessingParameterEnum * >( def.get() )->allowMultiple() ); } -void TestProcessingGui::testPointPanel() +void TestProcessingGui::testLayoutWrapper() { - std::unique_ptr< QgsProcessingPointPanel > panel = qgis::make_unique< QgsProcessingPointPanel >( nullptr ); - QSignalSpy spy( panel.get(), &QgsProcessingPointPanel::changed ); - - panel->setValue( QgsPointXY( 100, 150 ), QgsCoordinateReferenceSystem() ); - QCOMPARE( panel->value().toString(), QStringLiteral( "100.000000,150.000000" ) ); - QCOMPARE( spy.count(), 1 ); - - panel->setValue( QgsPointXY( 200, 250 ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ); - QCOMPARE( panel->value().toString(), QStringLiteral( "200.000000,250.000000 [EPSG:3111]" ) ); - QCOMPARE( spy.count(), 2 ); - - panel->setValue( QgsPointXY( 123456.123456789, 654321.987654321 ), QgsCoordinateReferenceSystem() ); - QCOMPARE( panel->value().toString(), QStringLiteral( "123456.123457,654321.987654" ) ); - QCOMPARE( spy.count(), 3 ); - - QVERIFY( !panel->mLineEdit->showClearButton() ); - panel->setAllowNull( true ); - QVERIFY( panel->mLineEdit->showClearButton() ); - panel->clear(); - QVERIFY( !panel->value().isValid() ); - QCOMPARE( spy.count(), 4 ); - - QgsMapCanvas canvas; - canvas.setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:28356" ) ) ); - panel->setMapCanvas( &canvas ); - panel->updatePoint( QgsPointXY( 1.5, -3.5 ) ); - QCOMPARE( panel->value().toString(), QStringLiteral( "1.500000,-3.500000 [EPSG:28356]" ) ); - QCOMPARE( spy.count(), 5 ); - - panel.reset(); -} + QgsProject p; + QgsPrintLayout *l1 = new QgsPrintLayout( &p ); + l1->setName( "l1" ); + p.layoutManager()->addLayout( l1 ); + QgsPrintLayout *l2 = new QgsPrintLayout( &p ); + l2->setName( "l2" ); + p.layoutManager()->addLayout( l2 ); -void TestProcessingGui::testPointWrapper() -{ - auto testWrapper = []( QgsProcessingGui::WidgetType type ) + auto testWrapper = [&p]( QgsProcessingGui::WidgetType type ) { // non optional - QgsProcessingParameterPoint param( QStringLiteral( "point" ), QStringLiteral( "point" ), false ); + QgsProcessingParameterLayout param( QStringLiteral( "layout" ), QStringLiteral( "layout" ), false ); - QgsProcessingPointWidgetWrapper wrapper( ¶m, type ); + QgsProcessingLayoutWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; + context.setProject( &p ); + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( &p ); + wrapper.setWidgetContext( widgetContext ); QWidget *w = wrapper.createWrappedWidget( context ); - QSignalSpy spy( &wrapper, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( "1,2", context ); + QSignalSpy spy( &wrapper, &QgsProcessingLayoutWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( "l2", context ); QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "l2" ) ); if ( type != QgsProcessingGui::Modeler ) { - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1.000000,2.000000" ) ); - QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000" ) ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 1 ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "l2" ) ); } else { - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1,2" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "1,2" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "l2" ) ); } - wrapper.setWidgetValue( "1,2 [EPSG:3111]", context ); + wrapper.setWidgetValue( "l1", context ); QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "l1" ) ); if ( type != QgsProcessingGui::Modeler ) { - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); - QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); - } - else - { - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1,2 [EPSG:3111]" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "1,2 [EPSG:3111]" ) ); - } - - // check signal - if ( type != QgsProcessingGui::Modeler ) - { - static_cast< QgsProcessingPointPanel * >( wrapper.wrappedWidget() )->mLineEdit->setText( QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentIndex(), 0 ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); } else { - static_cast< QLineEdit * >( wrapper.wrappedWidget() )->setText( QStringLiteral( "aaaa" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "l1" ) ); } - QCOMPARE( spy.count(), 3 ); - QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "point" ) ); + QCOMPARE( l->text(), QStringLiteral( "layout" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -3522,78 +4153,71 @@ void TestProcessingGui::testPointWrapper() QVERIFY( !l ); } + // check signal + if ( type != QgsProcessingGui::Modeler ) + { + static_cast< QComboBox * >( wrapper.wrappedWidget() )->setCurrentIndex( 1 ); + } + else + { + static_cast< QLineEdit * >( wrapper.wrappedWidget() )->setText( QStringLiteral( "aaaa" ) ); + } + QCOMPARE( spy.count(), 3 ); + delete w; // optional - QgsProcessingParameterPoint param2( QStringLiteral( "point" ), QStringLiteral( "point" ), QVariant(), true ); + QgsProcessingParameterLayout param2( QStringLiteral( "layout" ), QStringLiteral( "layout" ), QVariant(), true ); - QgsProcessingPointWidgetWrapper wrapper2( ¶m2, type ); + QgsProcessingLayoutWidgetWrapper wrapper2( ¶m2, type ); + wrapper2.setWidgetContext( widgetContext ); w = wrapper2.createWrappedWidget( context ); - QSignalSpy spy2( &wrapper2, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); - wrapper2.setWidgetValue( "1,2", context ); + QSignalSpy spy2( &wrapper2, &QgsProcessingLayoutWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( "l2", context ); QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "l2" ) ); if ( type != QgsProcessingGui::Modeler ) { - QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000" ) ); - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1.000000,2.000000" ) ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 2 ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l2" ) ); } else { - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1,2" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "1,2" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "l2" ) ); } - - wrapper2.setWidgetValue( "1,2 [EPSG:3111]", context ); + wrapper2.setWidgetValue( "l1", context ); QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "l1" ) ); if ( type != QgsProcessingGui::Modeler ) { - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); - QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 1 ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); } else { - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1,2 [EPSG:3111]" ) ); - QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "1,2 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "l1" ) ); } wrapper2.setWidgetValue( QVariant(), context ); QCOMPARE( spy2.count(), 3 ); QVERIFY( !wrapper2.widgetValue().isValid() ); - if ( type == QgsProcessingGui::Modeler ) + if ( type != QgsProcessingGui::Modeler ) { - QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); + QCOMPARE( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentIndex(), 0 ); + QVERIFY( static_cast< QgsLayoutComboBox * >( wrapper2.wrappedWidget() )->currentText().isEmpty() ); } else - { - QVERIFY( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text().isEmpty() ); - } - wrapper2.setWidgetValue( "1,3", context ); - QCOMPARE( spy2.count(), 4 ); - wrapper2.setWidgetValue( "", context ); - QCOMPARE( spy2.count(), 5 ); - QVERIFY( !wrapper2.widgetValue().isValid() ); - if ( type == QgsProcessingGui::Modeler ) { QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); } - else - { - QVERIFY( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text().isEmpty() ); - } - // check signals - wrapper2.setWidgetValue( "1,3", context ); - QCOMPARE( spy2.count(), 6 ); - if ( type == QgsProcessingGui::Modeler ) - { - static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->clear(); - } + // check signal + if ( type != QgsProcessingGui::Modeler ) + static_cast< QComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 2 ); else - { - static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->clear(); - } - QCOMPARE( spy2.count(), 7 ); + static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->setText( QStringLiteral( "aaa" ) ); + QCOMPARE( spy2.count(), 4 ); delete w; }; @@ -3606,36 +4230,69 @@ void TestProcessingGui::testPointWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + } -void TestProcessingGui::testColorWrapper() +void TestProcessingGui::testLayoutItemWrapper() { - auto testWrapper = []( QgsProcessingGui::WidgetType type ) + QgsProject p; + QgsPrintLayout *l1 = new QgsPrintLayout( &p ); + l1->setName( "l1" ); + p.layoutManager()->addLayout( l1 ); + QgsLayoutItemLabel *label1 = new QgsLayoutItemLabel( l1 ); + label1->setId( "a" ); + l1->addLayoutItem( label1 ); + QgsLayoutItemLabel *label2 = new QgsLayoutItemLabel( l1 ); + label2->setId( "b" ); + l1->addLayoutItem( label2 ); + + auto testWrapper = [&p, l1, label1, label2]( QgsProcessingGui::WidgetType type ) { - QgsProcessingParameterColor param( QStringLiteral( "color" ), QStringLiteral( "color" ) ); + // non optional + QgsProcessingParameterLayoutItem param( QStringLiteral( "layout" ), QStringLiteral( "layout" ), false ); - QgsProcessingColorWidgetWrapper wrapper( ¶m, type ); + QgsProcessingLayoutItemWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; + context.setProject( &p ); + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( &p ); + wrapper.setWidgetContext( widgetContext ); QWidget *w = wrapper.createWrappedWidget( context ); - QSignalSpy spy( &wrapper, &QgsProcessingColorWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( QColor( 255, 0, 0 ), context ); + wrapper.setLayout( l1 ); + + QSignalSpy spy( &wrapper, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( "b", context ); QCOMPARE( spy.count(), 1 ); - QCOMPARE( wrapper.widgetValue().value< QColor >().name(), QStringLiteral( "#ff0000" ) ); - QCOMPARE( static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->color(), QColor( 255, 0, 0 ) ); - QVERIFY( !static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->showNull() ); - QVERIFY( static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->allowOpacity() ); - wrapper.setWidgetValue( QColor(), context ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( wrapper.widgetValue().toString(), label2->uuid() ); + QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); + } + else + { + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "b" ) ); + } + wrapper.setWidgetValue( "a", context ); QCOMPARE( spy.count(), 2 ); - QVERIFY( !wrapper.widgetValue().value< QColor >().isValid() ); - QVERIFY( !static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->color().isValid() ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( wrapper.widgetValue().toString(), label1->uuid() ); + QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); + } + else + { + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "a" ) ); + } QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "color" ) ); + QCOMPARE( l->text(), QStringLiteral( "layout" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -3645,34 +4302,72 @@ void TestProcessingGui::testColorWrapper() } // check signal - static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->setColor( QColor( 0, 255, 0 ) ); + if ( type != QgsProcessingGui::Modeler ) + { + static_cast< QComboBox * >( wrapper.wrappedWidget() )->setCurrentIndex( 1 ); + } + else + { + static_cast< QLineEdit * >( wrapper.wrappedWidget() )->setText( QStringLiteral( "aaaa" ) ); + } QCOMPARE( spy.count(), 3 ); - // with opacity - wrapper.setWidgetValue( QColor( 255, 0, 0, 100 ), context ); - QCOMPARE( wrapper.widgetValue().value< QColor >(), QColor( 255, 0, 0, 100 ) ); - delete w; - // with null - QgsProcessingParameterColor param2( QStringLiteral( "c2" ), QStringLiteral( "c2" ), QColor( 10, 20, 30 ), true, true ); + // optional - QgsProcessingColorWidgetWrapper wrapper2( ¶m2, type ); + QgsProcessingParameterLayoutItem param2( QStringLiteral( "layout" ), QStringLiteral( "layout" ), QVariant(), QString(), -1, true ); + + QgsProcessingLayoutItemWidgetWrapper wrapper2( ¶m2, type ); + wrapper2.setWidgetContext( widgetContext ); w = wrapper2.createWrappedWidget( context ); - QVERIFY( static_cast< QgsColorButton * >( wrapper2.wrappedWidget() )->showNull() ); - QCOMPARE( static_cast< QgsColorButton * >( wrapper2.wrappedWidget() )->color().name(), QStringLiteral( "#0a141e" ) ); + wrapper2.setLayout( l1 ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( "b", context ); + QCOMPARE( spy2.count(), 1 ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( wrapper2.widgetValue().toString(), label2->uuid() ); + QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "b" ) ); + } + else + { + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "b" ) ); + } + wrapper2.setWidgetValue( "a", context ); + QCOMPARE( spy2.count(), 2 ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( wrapper2.widgetValue().toString(), label1->uuid() ); + QCOMPARE( static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "a" ) ); + } + else + { + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "a" ) ); + } wrapper2.setWidgetValue( QVariant(), context ); + QCOMPARE( spy2.count(), 3 ); QVERIFY( !wrapper2.widgetValue().isValid() ); - wrapper2.setWidgetValue( QColor( 255, 0, 255 ), context ); - QCOMPARE( wrapper2.widgetValue().value< QColor >().name(), QStringLiteral( "#ff00ff" ) ); + if ( type != QgsProcessingGui::Modeler ) + { + QVERIFY( static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->currentText().isEmpty() ); + } + else + { + QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); + } - // no opacity - QgsProcessingParameterColor param3( QStringLiteral( "c2" ), QStringLiteral( "c2" ), QColor( 10, 20, 30 ), false, true ); + // check signal + if ( type != QgsProcessingGui::Modeler ) + static_cast< QgsLayoutItemComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 1 ); + else + static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->setText( QStringLiteral( "aaa" ) ); + QCOMPARE( spy2.count(), 4 ); - QgsProcessingColorWidgetWrapper wrapper3( ¶m3, type ); - w = wrapper3.createWrappedWidget( context ); - wrapper3.setWidgetValue( QColor( 255, 0, 0, 100 ), context ); - QCOMPARE( wrapper3.widgetValue().value< QColor >(), QColor( 255, 0, 0 ) ); + delete w; }; // standard wrapper @@ -3684,93 +4379,125 @@ void TestProcessingGui::testColorWrapper() // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); + // config widget QgsProcessingParameterWidgetContext widgetContext; QgsProcessingContext context; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "color" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layoutitem" ), context, widgetContext ); std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QVERIFY( static_cast< QgsProcessingParameterColor * >( def.get() )->opacityEnabled() ); // should default to true // using a parameter definition as initial values - QgsProcessingParameterColor colorParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QColor( 255, 0, 0, 100 ), true ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "color" ), context, widgetContext, &colorParam ); + QgsProcessingParameterLayoutItem itemParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QVariant(), QStringLiteral( "parent" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layoutitem" ), context, widgetContext, &itemParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QCOMPARE( static_cast< QgsProcessingParameterColor * >( def.get() )->defaultValue().value< QColor >(), QColor( 255, 0, 0, 100 ) ); - QVERIFY( static_cast< QgsProcessingParameterColor * >( def.get() )->opacityEnabled() ); - colorParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); - colorParam.setOpacityEnabled( false ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "color" ), context, widgetContext, &colorParam ); + QCOMPARE( static_cast< QgsProcessingParameterLayoutItem * >( def.get() )->parentLayoutParameterName(), QStringLiteral( "parent" ) ); + itemParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + itemParam.setParentLayoutParameterName( QString() ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layoutitem" ), context, widgetContext, &itemParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); - QCOMPARE( static_cast< QgsProcessingParameterColor * >( def.get() )->defaultValue().value< QColor >(), QColor( 255, 0, 0 ) ); // (no opacity!) - QVERIFY( !static_cast< QgsProcessingParameterColor * >( def.get() )->opacityEnabled() ); + QVERIFY( static_cast< QgsProcessingParameterLayoutItem * >( def.get() )->parentLayoutParameterName().isEmpty() ); } -void TestProcessingGui::testCoordinateOperationWrapper() +void TestProcessingGui::testPointPanel() +{ + std::unique_ptr< QgsProcessingPointPanel > panel = qgis::make_unique< QgsProcessingPointPanel >( nullptr ); + QSignalSpy spy( panel.get(), &QgsProcessingPointPanel::changed ); + + panel->setValue( QgsPointXY( 100, 150 ), QgsCoordinateReferenceSystem() ); + QCOMPARE( panel->value().toString(), QStringLiteral( "100.000000,150.000000" ) ); + QCOMPARE( spy.count(), 1 ); + + panel->setValue( QgsPointXY( 200, 250 ), QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3111" ) ) ); + QCOMPARE( panel->value().toString(), QStringLiteral( "200.000000,250.000000 [EPSG:3111]" ) ); + QCOMPARE( spy.count(), 2 ); + + panel->setValue( QgsPointXY( 123456.123456789, 654321.987654321 ), QgsCoordinateReferenceSystem() ); + QCOMPARE( panel->value().toString(), QStringLiteral( "123456.123457,654321.987654" ) ); + QCOMPARE( spy.count(), 3 ); + + QVERIFY( !panel->mLineEdit->showClearButton() ); + panel->setAllowNull( true ); + QVERIFY( panel->mLineEdit->showClearButton() ); + panel->clear(); + QVERIFY( !panel->value().isValid() ); + QCOMPARE( spy.count(), 4 ); + + QgsMapCanvas canvas; + canvas.setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:28356" ) ) ); + panel->setMapCanvas( &canvas ); + panel->updatePoint( QgsPointXY( 1.5, -3.5 ) ); + QCOMPARE( panel->value().toString(), QStringLiteral( "1.500000,-3.500000 [EPSG:28356]" ) ); + QCOMPARE( spy.count(), 5 ); + + panel.reset(); +} + +void TestProcessingGui::testPointWrapper() { -#if PROJ_VERSION_MAJOR>=6 auto testWrapper = []( QgsProcessingGui::WidgetType type ) { - QgsProcessingParameterCoordinateOperation param( QStringLiteral( "op" ), QStringLiteral( "op" ) ); + // non optional + QgsProcessingParameterPoint param( QStringLiteral( "point" ), QStringLiteral( "point" ), false ); - QgsProcessingCoordinateOperationWidgetWrapper wrapper( ¶m, type ); + QgsProcessingPointWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; QWidget *w = wrapper.createWrappedWidget( context ); - wrapper.setSourceCrsParameterValue( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:26745" ) ) ); - wrapper.setDestinationCrsParameterValue( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ) ); - QSignalSpy spy( &wrapper, &QgsProcessingCoordinateOperationWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ), context ); + QSignalSpy spy( &wrapper, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( "1,2", context ); QCOMPARE( spy.count(), 1 ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); - switch ( type ) + if ( type != QgsProcessingGui::Modeler ) { - case QgsProcessingGui::Standard: - { - QCOMPARE( static_cast< QgsCoordinateOperationWidget * >( wrapper.wrappedWidget() )->selectedOperation().proj, QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); - wrapper.setWidgetValue( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ), context ); - QCOMPARE( spy.count(), 2 ); - QCOMPARE( static_cast< QgsCoordinateOperationWidget * >( wrapper.wrappedWidget() )->selectedOperation().proj, QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); - - // check signal - QgsCoordinateOperationWidget::OperationDetails deets; - deets.proj = QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ); - static_cast< QgsCoordinateOperationWidget * >( wrapper.wrappedWidget() )->setSelectedOperation( deets ); - QCOMPARE( spy.count(), 3 ); - break; - } - - case QgsProcessingGui::Modeler: - case QgsProcessingGui::Batch: - { - QCOMPARE( wrapper.mLineEdit->text(), QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); - wrapper.setWidgetValue( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ), context ); - QCOMPARE( spy.count(), 2 ); - QCOMPARE( wrapper.mLineEdit->text(), QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1.000000,2.000000" ) ); + QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000" ) ); + } + else + { + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1,2" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "1,2" ) ); + } + wrapper.setWidgetValue( "1,2 [EPSG:3111]", context ); + QCOMPARE( spy.count(), 2 ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); + } + else + { + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1,2 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper.wrappedWidget() )->text(), QStringLiteral( "1,2 [EPSG:3111]" ) ); + } - // check signal - wrapper.mLineEdit->setText( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); - QCOMPARE( spy.count(), 3 ); - break; - } + // check signal + if ( type != QgsProcessingGui::Modeler ) + { + static_cast< QgsProcessingPointPanel * >( wrapper.wrappedWidget() )->mLineEdit->setText( QStringLiteral( "b" ) ); + } + else + { + static_cast< QLineEdit * >( wrapper.wrappedWidget() )->setText( QStringLiteral( "aaaa" ) ); } + QCOMPARE( spy.count(), 3 ); + QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "op" ) ); + QCOMPARE( l->text(), QStringLiteral( "point" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -3780,6 +4507,79 @@ void TestProcessingGui::testCoordinateOperationWrapper() } delete w; + + // optional + + QgsProcessingParameterPoint param2( QStringLiteral( "point" ), QStringLiteral( "point" ), QVariant(), true ); + + QgsProcessingPointWidgetWrapper wrapper2( ¶m2, type ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingLayoutItemWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( "1,2", context ); + QCOMPARE( spy2.count(), 1 ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000" ) ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1.000000,2.000000" ) ); + } + else + { + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1,2" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "1,2" ) ); + } + + wrapper2.setWidgetValue( "1,2 [EPSG:3111]", context ); + QCOMPARE( spy2.count(), 2 ); + if ( type != QgsProcessingGui::Modeler ) + { + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text(), QStringLiteral( "1.000000,2.000000 [EPSG:3111]" ) ); + } + else + { + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1,2 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text(), QStringLiteral( "1,2 [EPSG:3111]" ) ); + } + wrapper2.setWidgetValue( QVariant(), context ); + QCOMPARE( spy2.count(), 3 ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + if ( type == QgsProcessingGui::Modeler ) + { + QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); + } + else + { + QVERIFY( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text().isEmpty() ); + } + wrapper2.setWidgetValue( "1,3", context ); + QCOMPARE( spy2.count(), 4 ); + wrapper2.setWidgetValue( "", context ); + QCOMPARE( spy2.count(), 5 ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + if ( type == QgsProcessingGui::Modeler ) + { + QVERIFY( static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->text().isEmpty() ); + } + else + { + QVERIFY( static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->text().isEmpty() ); + } + + // check signals + wrapper2.setWidgetValue( "1,3", context ); + QCOMPARE( spy2.count(), 6 ); + if ( type == QgsProcessingGui::Modeler ) + { + static_cast< QLineEdit * >( wrapper2.wrappedWidget() )->clear(); + } + else + { + static_cast< QgsProcessingPointPanel * >( wrapper2.wrappedWidget() )->mLineEdit->clear(); + } + QCOMPARE( spy2.count(), 7 ); + + delete w; }; // standard wrapper @@ -3792,383 +4592,761 @@ void TestProcessingGui::testCoordinateOperationWrapper() testWrapper( QgsProcessingGui::Modeler ); // config widget - QgsProcessingParameterWidgetContext widgetContext; QgsProcessingContext context; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "coordinateoperation" ), context, widgetContext ); + QgsProcessingParameterWidgetContext widgetContext; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "point" ), context, widgetContext ); std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QVERIFY( !static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrs().isValid() ); // should default to not set - QVERIFY( !static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrs().isValid() ); // should default to not set - QVERIFY( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrsParameterName().isEmpty() ); // should default to not set - QVERIFY( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrsParameterName().isEmpty() ); // should default to not set - // using a parameter definition as initial values - QgsProcessingParameterCoordinateOperation coordParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "+proj" ), QStringLiteral( "a" ), QStringLiteral( "b" ), QStringLiteral( "EPSG:26745" ), QStringLiteral( "EPSG:4326" ), false ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "coordinateoperation" ), context, widgetContext, &coordParam ); + QgsProcessingParameterPoint pointParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "1,2" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "point" ), context, widgetContext, &pointParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->defaultValue().toString(), QStringLiteral( "+proj" ) ); - QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrsParameterName(), QStringLiteral( "a" ) ); - QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrsParameterName(), QStringLiteral( "b" ) ); - QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrs().value< QgsCoordinateReferenceSystem >( ).authid(), QStringLiteral( "EPSG:26745" ) ); - QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrs().value< QgsCoordinateReferenceSystem >( ).authid(), QStringLiteral( "EPSG:4326" ) ); - coordParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "coordinateoperation" ), context, widgetContext, &coordParam ); + QCOMPARE( static_cast< QgsProcessingParameterPoint * >( def.get() )->defaultValue().toString(), QStringLiteral( "1.000000,2.000000" ) ); + pointParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + pointParam.setDefaultValue( QStringLiteral( "4,7" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "point" ), context, widgetContext, &pointParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); -#endif + QCOMPARE( static_cast< QgsProcessingParameterPoint * >( def.get() )->defaultValue().toString(), QStringLiteral( "4.000000,7.000000" ) ); + } -void TestProcessingGui::mapLayerComboBox() +void TestProcessingGui::testExtentWrapper() { - QgsProject::instance()->removeAllMapLayers(); - QgsProcessingContext context; - context.setProject( QgsProject::instance() ); + auto testWrapper = []( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterExtent param( QStringLiteral( "extent" ), QStringLiteral( "extent" ), false ); - // feature source param - std::unique_ptr< QgsProcessingParameterDefinition > param( new QgsProcessingParameterFeatureSource( QStringLiteral( "param" ), QString() ) ); - std::unique_ptr< QgsProcessingMapLayerComboBox> combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + QgsProcessingExtentWidgetWrapper wrapper( ¶m, type ); - QSignalSpy spy( combo.get(), &QgsProcessingMapLayerComboBox::valueChanged ); - QVERIFY( !combo->value().isValid() ); - combo->setValue( QStringLiteral( "file path" ), context ); - QCOMPARE( spy.count(), 1 ); - QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 1 ); - combo->setValue( QVariant(), context ); // not possible, it's not an optional param - QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 1 ); - combo->setValue( QStringLiteral( "file path 2" ), context ); - QCOMPARE( combo->value().toString(), QStringLiteral( "file path 2" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 2 ); - combo->setValue( QStringLiteral( "file path" ), context ); - QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 3 ); - combo->setLayer( nullptr ); // not possible, not optional - QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 3 ); + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); - // project layers - QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); - QgsFeature f; - vl->dataProvider()->addFeature( f ); - QgsProject::instance()->addMapLayer( vl ); - QVERIFY( vl->isValid() ); - QgsVectorLayer *vl2 = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l2" ), QStringLiteral( "memory" ) ); - vl2->dataProvider()->addFeature( f ); - QgsProject::instance()->addMapLayer( vl2 ); - QVERIFY( vl2->isValid() ); + QSignalSpy spy( &wrapper, &QgsProcessingExtentWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( "1,2,3,4", context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1.000000000,2.000000000,3.000000000,4.000000000" ) ); + QCOMPARE( static_cast< QgsExtentWidget * >( wrapper.wrappedWidget() )->outputExtent(), QgsRectangle( 1, 3, 2, 4 ) ); - QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 3 ); + wrapper.setWidgetValue( "1,2,3,4 [EPSG:3111]", context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "1.000000000,2.000000000,3.000000000,4.000000000 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QgsExtentWidget * >( wrapper.wrappedWidget() )->outputExtent(), QgsRectangle( 1, 3, 2, 4 ) ); + QCOMPARE( static_cast< QgsExtentWidget * >( wrapper.wrappedWidget() )->outputCrs().authid(), QStringLiteral( "EPSG:3111" ) ); - combo->setLayer( vl ); - QCOMPARE( combo->currentLayer(), vl ); - QCOMPARE( combo->value().toString(), vl->id() ); - QVERIFY( combo->currentText().startsWith( vl->name() ) ); - QCOMPARE( spy.count(), 4 ); - combo->setLayer( vl ); - QCOMPARE( spy.count(), 4 ); + // check signal + static_cast< QgsExtentWidget * >( wrapper.wrappedWidget() )->setOutputExtentFromUser( QgsRectangle( 11, 22, 33, 44 ), QgsCoordinateReferenceSystem() ); + QCOMPARE( spy.count(), 3 ); - combo->setLayer( vl2 ); - QCOMPARE( combo->value().toString(), vl2->id() ); - QVERIFY( combo->currentText().startsWith( vl2->name() ) ); - QCOMPARE( spy.count(), 5 ); + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "extent" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } - combo->setValue( QStringLiteral( "file path" ), context ); - QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); - QVERIFY( !combo->currentLayer() ); - QCOMPARE( spy.count(), 6 ); + delete w; - // setting feature source def, i.e. with selection - QgsProcessingFeatureSourceDefinition sourceDef( vl2->id(), false ); - combo->setValue( sourceDef, context ); - QCOMPARE( combo->value().toString(), vl2->id() ); - QVERIFY( combo->currentText().startsWith( vl2->name() ) ); - QCOMPARE( spy.count(), 7 ); - // asking for selected features only, but no selection in layer, won't be allowed - sourceDef = QgsProcessingFeatureSourceDefinition( vl2->id(), true ); - combo->setValue( sourceDef, context ); - QCOMPARE( combo->value().toString(), vl2->id() ); - QVERIFY( combo->currentText().startsWith( vl2->name() ) ); - QCOMPARE( spy.count(), 7 ); // no change + // optional - // now make a selection in the layer, and repeat - vl2->selectAll(); - combo->setValue( sourceDef, context ); - QVERIFY( combo->value().canConvert< QgsProcessingFeatureSourceDefinition >() ); - QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().source.staticValue().toString(), vl2->id() ); - QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().selectedFeaturesOnly ); - QVERIFY( combo->currentText().startsWith( vl2->name() ) ); - QCOMPARE( spy.count(), 8 ); + QgsProcessingParameterExtent param2( QStringLiteral( "extent" ), QStringLiteral( "extent" ), QVariant(), true ); - // remove layer selection, and check result... - vl2->removeSelection(); - QCOMPARE( combo->value().toString(), vl2->id() ); - QVERIFY( combo->currentText().startsWith( vl2->name() ) ); - QCOMPARE( spy.count(), 9 ); + QgsProcessingExtentWidgetWrapper wrapper2( ¶m2, type ); + w = wrapper2.createWrappedWidget( context ); - // phew, nearly there. Let's check another variation - vl2->selectAll(); - combo->setValue( sourceDef, context ); - QVERIFY( combo->value().canConvert< QgsProcessingFeatureSourceDefinition >() ); - QCOMPARE( spy.count(), 10 ); - combo->setValue( QVariant::fromValue( vl ), context ); - QCOMPARE( combo->value().toString(), vl->id() ); - QVERIFY( combo->currentText().startsWith( vl->name() ) ); - QCOMPARE( spy.count(), 11 ); + QSignalSpy spy2( &wrapper2, &QgsProcessingExtentWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( "1,2,3,4", context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( static_cast< QgsExtentWidget * >( wrapper2.wrappedWidget() )->outputExtent(), QgsRectangle( 1, 3, 2, 4 ) ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1.000000000,2.000000000,3.000000000,4.000000000" ) ); - // one last variation - selection to selection - combo->setValue( sourceDef, context ); - QCOMPARE( spy.count(), 12 ); - QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().selectedFeaturesOnly ); - vl->selectAll(); - sourceDef = QgsProcessingFeatureSourceDefinition( vl->id(), true ); - combo->setValue( sourceDef, context ); - // except "selected only" state to remain - QVERIFY( combo->value().canConvert< QgsProcessingFeatureSourceDefinition >() ); - QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().source.staticValue().toString(), vl->id() ); - QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().selectedFeaturesOnly ); - QVERIFY( combo->currentText().startsWith( vl->name() ) ); - QCOMPARE( spy.count(), 13 ); - combo.reset(); - param.reset(); + wrapper2.setWidgetValue( "1,2,3,4 [EPSG:3111]", context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "1.000000000,2.000000000,3.000000000,4.000000000 [EPSG:3111]" ) ); + QCOMPARE( static_cast< QgsExtentWidget * >( wrapper2.wrappedWidget() )->outputExtent(), QgsRectangle( 1, 3, 2, 4 ) ); + QCOMPARE( static_cast< QgsExtentWidget * >( wrapper2.wrappedWidget() )->outputCrs().authid(), QStringLiteral( "EPSG:3111" ) ); + wrapper2.setWidgetValue( QVariant(), context ); + QCOMPARE( spy2.count(), 3 ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + QVERIFY( !static_cast< QgsExtentWidget * >( wrapper2.wrappedWidget() )->isValid() ); - // setup a project with a range of layer types - QgsProject::instance()->removeAllMapLayers(); - QgsVectorLayer *point = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); - QgsProject::instance()->addMapLayer( point ); - QgsVectorLayer *line = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); - QgsProject::instance()->addMapLayer( line ); - QgsVectorLayer *polygon = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); - QgsProject::instance()->addMapLayer( polygon ); - QgsVectorLayer *noGeom = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); - QgsProject::instance()->addMapLayer( noGeom ); - QgsMeshLayer *mesh = new QgsMeshLayer( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle.2dm", QStringLiteral( "Triangle and Quad Mdal" ), QStringLiteral( "mdal" ) ); - mesh->dataProvider()->addDataset( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); - QVERIFY( mesh->isValid() ); - QgsProject::instance()->addMapLayer( mesh ); - QgsRasterLayer *raster = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/raster/band1_byte_ct_epsg4326.tif", QStringLiteral( "band1_byte" ) ); - QgsProject::instance()->addMapLayer( raster ); + wrapper2.setWidgetValue( "1,3,4,7", context ); + QCOMPARE( spy2.count(), 4 ); + wrapper2.setWidgetValue( "", context ); + QCOMPARE( spy2.count(), 5 ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + QVERIFY( !static_cast< QgsExtentWidget * >( wrapper2.wrappedWidget() )->isValid() ); - // map layer param, all types are acceptable - param = qgis::make_unique< QgsProcessingParameterMapLayer> ( QStringLiteral( "param" ), QString() ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo->setLayer( line ); - QCOMPARE( combo->currentLayer(), line ); - combo->setLayer( polygon ); - QCOMPARE( combo->currentLayer(), polygon ); - combo->setLayer( noGeom ); - QCOMPARE( combo->currentLayer(), noGeom ); - combo->setLayer( mesh ); - QCOMPARE( combo->currentLayer(), mesh ); - combo->setLayer( raster ); - QCOMPARE( combo->currentLayer(), raster ); - combo.reset(); - param.reset(); + // check signals + wrapper2.setWidgetValue( "1,3,9,8", context ); + QCOMPARE( spy2.count(), 6 ); + static_cast< QgsExtentWidget * >( wrapper2.wrappedWidget() )->clear(); + QCOMPARE( spy2.count(), 7 ); - // raster layer param, only raster types are acceptable - param = qgis::make_unique< QgsProcessingParameterRasterLayer> ( QStringLiteral( "param" ), QString() ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - combo->setLayer( point ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( line ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( polygon ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( raster ); - QCOMPARE( combo->currentLayer(), raster ); - combo.reset(); - param.reset(); + delete w; - // mesh layer parm, only mesh types are acceptable - param = qgis::make_unique< QgsProcessingParameterMeshLayer> ( QStringLiteral( "param" ), QString() ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - combo->setLayer( point ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( line ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( polygon ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo->setLayer( mesh ); - QCOMPARE( combo->currentLayer(), mesh ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo.reset(); - param.reset(); + }; - // feature source and vector layer params - // if not specified, the default is any vector layer with geometry - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ) ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - auto param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ) ); - auto combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo2->setLayer( point ); - QCOMPARE( combo2->currentLayer(), point ); - combo->setLayer( line ); - QCOMPARE( combo->currentLayer(), line ); - combo2->setLayer( line ); - QCOMPARE( combo2->currentLayer(), line ); - combo->setLayer( polygon ); - QCOMPARE( combo->currentLayer(), polygon ); - combo2->setLayer( polygon ); - QCOMPARE( combo2->currentLayer(), polygon ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( noGeom ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( mesh ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( raster ); - QVERIFY( !combo2->currentLayer() ); - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); - // point layer - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint ); - combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo2->setLayer( point ); - QCOMPARE( combo2->currentLayer(), point ); - combo->setLayer( line ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( line ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( polygon ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( polygon ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( noGeom ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( mesh ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( raster ); - QVERIFY( !combo2->currentLayer() ); - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); - // line layer - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorLine ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorLine ); - combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( point ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( line ); - QCOMPARE( combo->currentLayer(), line ); - combo2->setLayer( line ); - QCOMPARE( combo2->currentLayer(), line ); - combo->setLayer( polygon ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( polygon ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( noGeom ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( mesh ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( raster ); - QVERIFY( !combo2->currentLayer() ); - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); - // polygon - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPolygon ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPolygon ); - combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( point ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( line ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( line ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( polygon ); - QCOMPARE( combo->currentLayer(), polygon ); - combo2->setLayer( polygon ); - QCOMPARE( combo2->currentLayer(), polygon ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( noGeom ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( mesh ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( raster ); - QVERIFY( !combo2->currentLayer() ); - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); + // config widget + QgsProcessingContext context; + QgsProcessingParameterWidgetContext widgetContext; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "extent" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - // no geom - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVector ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVector ); - combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo2->setLayer( point ); - QCOMPARE( combo2->currentLayer(), point ); - combo->setLayer( line ); - QCOMPARE( combo->currentLayer(), line ); - combo2->setLayer( line ); + // using a parameter definition as initial values + QgsProcessingParameterExtent extentParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "1,2,3,4" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "extent" ), context, widgetContext, &extentParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterExtent * >( def.get() )->defaultValue().toString(), QStringLiteral( "1.000000000,2.000000000,3.000000000,4.000000000" ) ); + extentParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + extentParam.setDefaultValue( QStringLiteral( "4,7,8,9" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "extent" ), context, widgetContext, &extentParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterExtent * >( def.get() )->defaultValue().toString(), QStringLiteral( "4.000000000,7.000000000,8.000000000,9.000000000" ) ); +} + +void TestProcessingGui::testColorWrapper() +{ + auto testWrapper = []( QgsProcessingGui::WidgetType type ) + { + QgsProcessingParameterColor param( QStringLiteral( "color" ), QStringLiteral( "color" ) ); + + QgsProcessingColorWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingColorWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QColor( 255, 0, 0 ), context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().value< QColor >().name(), QStringLiteral( "#ff0000" ) ); + QCOMPARE( static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->color(), QColor( 255, 0, 0 ) ); + QVERIFY( !static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->showNull() ); + QVERIFY( static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->allowOpacity() ); + wrapper.setWidgetValue( QColor(), context ); + QCOMPARE( spy.count(), 2 ); + QVERIFY( !wrapper.widgetValue().value< QColor >().isValid() ); + QVERIFY( !static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->color().isValid() ); + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "color" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + + // check signal + static_cast< QgsColorButton * >( wrapper.wrappedWidget() )->setColor( QColor( 0, 255, 0 ) ); + QCOMPARE( spy.count(), 3 ); + + // with opacity + wrapper.setWidgetValue( QColor( 255, 0, 0, 100 ), context ); + QCOMPARE( wrapper.widgetValue().value< QColor >(), QColor( 255, 0, 0, 100 ) ); + + delete w; + + // with null + QgsProcessingParameterColor param2( QStringLiteral( "c2" ), QStringLiteral( "c2" ), QColor( 10, 20, 30 ), true, true ); + + QgsProcessingColorWidgetWrapper wrapper2( ¶m2, type ); + w = wrapper2.createWrappedWidget( context ); + QVERIFY( static_cast< QgsColorButton * >( wrapper2.wrappedWidget() )->showNull() ); + QCOMPARE( static_cast< QgsColorButton * >( wrapper2.wrappedWidget() )->color().name(), QStringLiteral( "#0a141e" ) ); + wrapper2.setWidgetValue( QVariant(), context ); + QVERIFY( !wrapper2.widgetValue().isValid() ); + wrapper2.setWidgetValue( QColor( 255, 0, 255 ), context ); + QCOMPARE( wrapper2.widgetValue().value< QColor >().name(), QStringLiteral( "#ff00ff" ) ); + + // no opacity + QgsProcessingParameterColor param3( QStringLiteral( "c2" ), QStringLiteral( "c2" ), QColor( 10, 20, 30 ), false, true ); + + QgsProcessingColorWidgetWrapper wrapper3( ¶m3, type ); + w = wrapper3.createWrappedWidget( context ); + wrapper3.setWidgetValue( QColor( 255, 0, 0, 100 ), context ); + QCOMPARE( wrapper3.widgetValue().value< QColor >(), QColor( 255, 0, 0 ) ); + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "color" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QVERIFY( static_cast< QgsProcessingParameterColor * >( def.get() )->opacityEnabled() ); // should default to true + + // using a parameter definition as initial values + QgsProcessingParameterColor colorParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QColor( 255, 0, 0, 100 ), true ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "color" ), context, widgetContext, &colorParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterColor * >( def.get() )->defaultValue().value< QColor >(), QColor( 255, 0, 0, 100 ) ); + QVERIFY( static_cast< QgsProcessingParameterColor * >( def.get() )->opacityEnabled() ); + colorParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + colorParam.setOpacityEnabled( false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "color" ), context, widgetContext, &colorParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterColor * >( def.get() )->defaultValue().value< QColor >(), QColor( 255, 0, 0 ) ); // (no opacity!) + QVERIFY( !static_cast< QgsProcessingParameterColor * >( def.get() )->opacityEnabled() ); +} + +void TestProcessingGui::testCoordinateOperationWrapper() +{ +#if PROJ_VERSION_MAJOR>=6 + auto testWrapper = []( QgsProcessingGui::WidgetType type ) + { + QgsProcessingParameterCoordinateOperation param( QStringLiteral( "op" ), QStringLiteral( "op" ) ); + + QgsProcessingCoordinateOperationWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + wrapper.setSourceCrsParameterValue( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:26745" ) ) ); + wrapper.setDestinationCrsParameterValue( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ) ); + + QSignalSpy spy( &wrapper, &QgsProcessingCoordinateOperationWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ), context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + switch ( type ) + { + case QgsProcessingGui::Standard: + { + QCOMPARE( static_cast< QgsCoordinateOperationWidget * >( wrapper.wrappedWidget() )->selectedOperation().proj, QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + wrapper.setWidgetValue( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( static_cast< QgsCoordinateOperationWidget * >( wrapper.wrappedWidget() )->selectedOperation().proj, QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + + // check signal + QgsCoordinateOperationWidget::OperationDetails deets; + deets.proj = QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ); + static_cast< QgsCoordinateOperationWidget * >( wrapper.wrappedWidget() )->setSelectedOperation( deets ); + QCOMPARE( spy.count(), 3 ); + break; + } + + case QgsProcessingGui::Modeler: + case QgsProcessingGui::Batch: + { + QCOMPARE( wrapper.mLineEdit->text(), QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + wrapper.setWidgetValue( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.mLineEdit->text(), QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=159 +z=175 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + + // check signal + wrapper.mLineEdit->setText( QStringLiteral( "+proj=pipeline +step +proj=unitconvert +xy_in=us-ft +xy_out=m +step +inv +proj=lcc +lat_0=33.5 +lon_0=-118 +lat_1=35.4666666666667 +lat_2=34.0333333333333 +x_0=609601.219202438 +y_0=0 +ellps=clrk66 +step +proj=push +v_3 +step +proj=cart +ellps=clrk66 +step +proj=helmert +x=-8 +y=160 +z=176 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=webmerc +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84" ) ); + QCOMPARE( spy.count(), 3 ); + break; + } + } + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "op" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + + delete w; + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "coordinateoperation" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + QVERIFY( !static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrs().isValid() ); // should default to not set + QVERIFY( !static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrs().isValid() ); // should default to not set + QVERIFY( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrsParameterName().isEmpty() ); // should default to not set + QVERIFY( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrsParameterName().isEmpty() ); // should default to not set + + // using a parameter definition as initial values + QgsProcessingParameterCoordinateOperation coordParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "+proj" ), QStringLiteral( "a" ), QStringLiteral( "b" ), QStringLiteral( "EPSG:26745" ), QStringLiteral( "EPSG:4326" ), false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "coordinateoperation" ), context, widgetContext, &coordParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->defaultValue().toString(), QStringLiteral( "+proj" ) ); + QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrsParameterName(), QStringLiteral( "a" ) ); + QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrsParameterName(), QStringLiteral( "b" ) ); + QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->sourceCrs().value< QgsCoordinateReferenceSystem >( ).authid(), QStringLiteral( "EPSG:26745" ) ); + QCOMPARE( static_cast< QgsProcessingParameterCoordinateOperation * >( def.get() )->destinationCrs().value< QgsCoordinateReferenceSystem >( ).authid(), QStringLiteral( "EPSG:4326" ) ); + coordParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "coordinateoperation" ), context, widgetContext, &coordParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); +#endif +} + +void TestProcessingGui::mapLayerComboBox() +{ + QgsProject::instance()->removeAllMapLayers(); + QgsProcessingContext context; + context.setProject( QgsProject::instance() ); + + // feature source param + std::unique_ptr< QgsProcessingParameterDefinition > param( new QgsProcessingParameterFeatureSource( QStringLiteral( "param" ), QString() ) ); + std::unique_ptr< QgsProcessingMapLayerComboBox> combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + + QSignalSpy spy( combo.get(), &QgsProcessingMapLayerComboBox::valueChanged ); + QVERIFY( !combo->value().isValid() ); + combo->setValue( QStringLiteral( "file path" ), context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 1 ); + combo->setValue( QVariant(), context ); // not possible, it's not an optional param + QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 1 ); + combo->setValue( QStringLiteral( "file path 2" ), context ); + QCOMPARE( combo->value().toString(), QStringLiteral( "file path 2" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 2 ); + combo->setValue( QStringLiteral( "file path" ), context ); + QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 3 ); + combo->setLayer( nullptr ); // not possible, not optional + QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 3 ); + + // project layers + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsFeature f; + vl->dataProvider()->addFeature( f ); + QgsProject::instance()->addMapLayer( vl ); + QVERIFY( vl->isValid() ); + QgsVectorLayer *vl2 = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l2" ), QStringLiteral( "memory" ) ); + vl2->dataProvider()->addFeature( f ); + QgsProject::instance()->addMapLayer( vl2 ); + QVERIFY( vl2->isValid() ); + + QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 3 ); + + combo->setLayer( vl ); + QCOMPARE( combo->currentLayer(), vl ); + QCOMPARE( combo->value().toString(), vl->id() ); + QVERIFY( combo->currentText().startsWith( vl->name() ) ); + QCOMPARE( spy.count(), 4 ); + combo->setLayer( vl ); + QCOMPARE( spy.count(), 4 ); + + combo->setLayer( vl2 ); + QCOMPARE( combo->value().toString(), vl2->id() ); + QVERIFY( combo->currentText().startsWith( vl2->name() ) ); + QCOMPARE( spy.count(), 5 ); + + combo->setValue( QStringLiteral( "file path" ), context ); + QCOMPARE( combo->value().toString(), QStringLiteral( "file path" ) ); + QVERIFY( !combo->currentLayer() ); + QCOMPARE( spy.count(), 6 ); + + // setting feature source def, i.e. with selection + QgsProcessingFeatureSourceDefinition sourceDef( vl2->id(), false ); + combo->setValue( sourceDef, context ); + QCOMPARE( combo->value().toString(), vl2->id() ); + QVERIFY( combo->currentText().startsWith( vl2->name() ) ); + QCOMPARE( spy.count(), 7 ); + // asking for selected features only, but no selection in layer, won't be allowed + sourceDef = QgsProcessingFeatureSourceDefinition( vl2->id(), true ); + combo->setValue( sourceDef, context ); + QCOMPARE( combo->value().toString(), vl2->id() ); + QVERIFY( combo->currentText().startsWith( vl2->name() ) ); + QCOMPARE( spy.count(), 7 ); // no change + + // now make a selection in the layer, and repeat + vl2->selectAll(); + combo->setValue( sourceDef, context ); + QVERIFY( combo->value().canConvert< QgsProcessingFeatureSourceDefinition >() ); + QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().source.staticValue().toString(), vl2->id() ); + QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().selectedFeaturesOnly ); + QVERIFY( combo->currentText().startsWith( vl2->name() ) ); + QCOMPARE( spy.count(), 8 ); + + // remove layer selection, and check result... + vl2->removeSelection(); + QCOMPARE( combo->value().toString(), vl2->id() ); + QVERIFY( combo->currentText().startsWith( vl2->name() ) ); + QCOMPARE( spy.count(), 9 ); + + // phew, nearly there. Let's check another variation + vl2->selectAll(); + combo->setValue( sourceDef, context ); + QVERIFY( combo->value().canConvert< QgsProcessingFeatureSourceDefinition >() ); + QCOMPARE( spy.count(), 10 ); + combo->setValue( QVariant::fromValue( vl ), context ); + QCOMPARE( combo->value().toString(), vl->id() ); + QVERIFY( combo->currentText().startsWith( vl->name() ) ); + QCOMPARE( spy.count(), 11 ); + + // one last variation - selection to selection + combo->setValue( sourceDef, context ); + QCOMPARE( spy.count(), 12 ); + QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().selectedFeaturesOnly ); + vl->selectAll(); + sourceDef = QgsProcessingFeatureSourceDefinition( vl->id(), true ); + combo->setValue( sourceDef, context ); + // expect "selected only" state to remain + QVERIFY( combo->value().canConvert< QgsProcessingFeatureSourceDefinition >() ); + QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().source.staticValue().toString(), vl->id() ); + QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().selectedFeaturesOnly ); + QVERIFY( combo->currentText().startsWith( vl->name() ) ); + QCOMPARE( spy.count(), 13 ); + + // iterate over features + QVERIFY( !( combo->value().value< QgsProcessingFeatureSourceDefinition >().flags & QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature ) ); + sourceDef.flags |= QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature; + combo->setValue( sourceDef, context ); + QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().flags & QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature ); + sourceDef.flags = nullptr; + combo->setValue( sourceDef, context ); + QVERIFY( !( combo->value().value< QgsProcessingFeatureSourceDefinition >().flags & QgsProcessingFeatureSourceDefinition::Flag::FlagCreateIndividualOutputPerInputFeature ) ); + + // advanced settings + sourceDef.featureLimit = 67; + combo->setValue( sourceDef, context ); + QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().featureLimit, 67LL ); + sourceDef.featureLimit = -1; + combo->setValue( sourceDef, context ); + QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().featureLimit, -1LL ); + sourceDef.flags |= QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck; + sourceDef.geometryCheck = QgsFeatureRequest::GeometrySkipInvalid; + combo->setValue( sourceDef, context ); + QVERIFY( combo->value().value< QgsProcessingFeatureSourceDefinition >().flags & QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ); + QCOMPARE( combo->value().value< QgsProcessingFeatureSourceDefinition >().geometryCheck, QgsFeatureRequest::GeometrySkipInvalid ); + sourceDef.flags = nullptr; + combo->setValue( sourceDef, context ); + QVERIFY( !( combo->value().value< QgsProcessingFeatureSourceDefinition >().flags & QgsProcessingFeatureSourceDefinition::Flag::FlagOverrideDefaultGeometryCheck ) ); + + combo.reset(); + param.reset(); + + // setup a project with a range of layer types + QgsProject::instance()->removeAllMapLayers(); + QgsVectorLayer *point = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( point ); + QgsVectorLayer *line = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( line ); + QgsVectorLayer *polygon = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( polygon ); + QgsVectorLayer *noGeom = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( noGeom ); + QgsMeshLayer *mesh = new QgsMeshLayer( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle.2dm", QStringLiteral( "Triangle and Quad Mdal" ), QStringLiteral( "mdal" ) ); + mesh->dataProvider()->addDataset( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); + QVERIFY( mesh->isValid() ); + QgsProject::instance()->addMapLayer( mesh ); + QgsRasterLayer *raster = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/raster/band1_byte_ct_epsg4326.tif", QStringLiteral( "band1_byte" ) ); + QgsProject::instance()->addMapLayer( raster ); + + // map layer param, all types are acceptable + param = qgis::make_unique< QgsProcessingParameterMapLayer> ( QStringLiteral( "param" ), QString() ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo->setLayer( line ); + QCOMPARE( combo->currentLayer(), line ); + combo->setLayer( polygon ); + QCOMPARE( combo->currentLayer(), polygon ); + combo->setLayer( noGeom ); + QCOMPARE( combo->currentLayer(), noGeom ); + combo->setLayer( mesh ); + QCOMPARE( combo->currentLayer(), mesh ); + combo->setLayer( raster ); + QCOMPARE( combo->currentLayer(), raster ); + combo.reset(); + param.reset(); + + // map layer param, only point vector and raster types are acceptable + param = qgis::make_unique< QgsProcessingParameterMapLayer> ( QStringLiteral( "param" ), QString(), QVariant(), false, QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeRaster ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo->setLayer( line ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( polygon ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( raster ); + QCOMPARE( combo->currentLayer(), raster ); + combo.reset(); + param.reset(); + + // raster layer param, only raster types are acceptable + param = qgis::make_unique< QgsProcessingParameterRasterLayer> ( QStringLiteral( "param" ), QString() ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + combo->setLayer( point ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( line ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( polygon ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( raster ); + QCOMPARE( combo->currentLayer(), raster ); + combo.reset(); + param.reset(); + + // mesh layer parm, only mesh types are acceptable + param = qgis::make_unique< QgsProcessingParameterMeshLayer> ( QStringLiteral( "param" ), QString() ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + combo->setLayer( point ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( line ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( polygon ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo->setLayer( mesh ); + QCOMPARE( combo->currentLayer(), mesh ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo.reset(); + param.reset(); + + // feature source and vector layer params + // if not specified, the default is any vector layer with geometry + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ) ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + auto param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ) ); + auto combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo2->setLayer( point ); + QCOMPARE( combo2->currentLayer(), point ); + combo->setLayer( line ); + QCOMPARE( combo->currentLayer(), line ); + combo2->setLayer( line ); + QCOMPARE( combo2->currentLayer(), line ); + combo->setLayer( polygon ); + QCOMPARE( combo->currentLayer(), polygon ); + combo2->setLayer( polygon ); + QCOMPARE( combo2->currentLayer(), polygon ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( noGeom ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( mesh ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( raster ); + QVERIFY( !combo2->currentLayer() ); + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + + // point layer + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint ); + combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo2->setLayer( point ); + QCOMPARE( combo2->currentLayer(), point ); + combo->setLayer( line ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( line ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( polygon ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( polygon ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( noGeom ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( mesh ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( raster ); + QVERIFY( !combo2->currentLayer() ); + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + + // line layer + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorLine ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorLine ); + combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( point ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( line ); + QCOMPARE( combo->currentLayer(), line ); + combo2->setLayer( line ); + QCOMPARE( combo2->currentLayer(), line ); + combo->setLayer( polygon ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( polygon ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( noGeom ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( mesh ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( raster ); + QVERIFY( !combo2->currentLayer() ); + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + + // polygon + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPolygon ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPolygon ); + combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( point ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( line ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( line ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( polygon ); + QCOMPARE( combo->currentLayer(), polygon ); + combo2->setLayer( polygon ); + QCOMPARE( combo2->currentLayer(), polygon ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( noGeom ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( mesh ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( raster ); + QVERIFY( !combo2->currentLayer() ); + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + + // no geom + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVector ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVector ); + combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo2->setLayer( point ); + QCOMPARE( combo2->currentLayer(), point ); + combo->setLayer( line ); + QCOMPARE( combo->currentLayer(), line ); + combo2->setLayer( line ); QCOMPARE( combo2->currentLayer(), line ); combo->setLayer( polygon ); QCOMPARE( combo->currentLayer(), polygon ); @@ -4191,222 +5369,2716 @@ void TestProcessingGui::mapLayerComboBox() combo.reset(); param.reset(); - // any geom - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorAnyGeometry ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorAnyGeometry ); - combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo2->setLayer( point ); - QCOMPARE( combo2->currentLayer(), point ); - combo->setLayer( line ); - QCOMPARE( combo->currentLayer(), line ); - combo2->setLayer( line ); - QCOMPARE( combo2->currentLayer(), line ); - combo->setLayer( polygon ); - QCOMPARE( combo->currentLayer(), polygon ); - combo2->setLayer( polygon ); - QCOMPARE( combo2->currentLayer(), polygon ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( noGeom ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( mesh ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( raster ); - QVERIFY( !combo2->currentLayer() ); - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); + // any geom + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorAnyGeometry ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorAnyGeometry ); + combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo2->setLayer( point ); + QCOMPARE( combo2->currentLayer(), point ); + combo->setLayer( line ); + QCOMPARE( combo->currentLayer(), line ); + combo2->setLayer( line ); + QCOMPARE( combo2->currentLayer(), line ); + combo->setLayer( polygon ); + QCOMPARE( combo->currentLayer(), polygon ); + combo2->setLayer( polygon ); + QCOMPARE( combo2->currentLayer(), polygon ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( noGeom ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( mesh ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( raster ); + QVERIFY( !combo2->currentLayer() ); + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + + // combination point and line only + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); + combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo2->setLayer( point ); + QCOMPARE( combo2->currentLayer(), point ); + combo->setLayer( line ); + QCOMPARE( combo->currentLayer(), line ); + combo2->setLayer( line ); + QCOMPARE( combo2->currentLayer(), line ); + combo->setLayer( polygon ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( polygon ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( noGeom ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( noGeom ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( mesh ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( mesh ); + QVERIFY( !combo2->currentLayer() ); + combo->setLayer( raster ); + QVERIFY( !combo->currentLayer() ); + combo2->setLayer( raster ); + QVERIFY( !combo2->currentLayer() ); + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + + // optional + param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>(), QVariant(), true ); + combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo->setLayer( nullptr ); + QVERIFY( !combo->currentLayer() ); + QVERIFY( !combo->value().isValid() ); + combo->setLayer( point ); + QCOMPARE( combo->currentLayer(), point ); + combo->setValue( QVariant(), context ); + QVERIFY( !combo->currentLayer() ); + QVERIFY( !combo->value().isValid() ); + + combo2.reset(); + param2.reset(); + combo.reset(); + param.reset(); + QgsProject::instance()->removeAllMapLayers(); +} + +void TestProcessingGui::testMapLayerWrapper() +{ + // setup a project with a range of layer types + QgsProject::instance()->removeAllMapLayers(); + QgsVectorLayer *point = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( point ); + QgsVectorLayer *line = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( line ); + QgsVectorLayer *polygon = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( polygon ); + QgsVectorLayer *noGeom = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( noGeom ); + QgsMeshLayer *mesh = new QgsMeshLayer( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle.2dm", QStringLiteral( "Triangle and Quad Mdal" ), QStringLiteral( "mdal" ) ); + mesh->dataProvider()->addDataset( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); + QVERIFY( mesh->isValid() ); + QgsProject::instance()->addMapLayer( mesh ); + QgsRasterLayer *raster = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/raster/band1_byte_ct_epsg4326.tif", QStringLiteral( "band1_byte" ) ); + QgsProject::instance()->addMapLayer( raster ); + + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterMapLayer param( QStringLiteral( "layer" ), QStringLiteral( "layer" ), false ); + + QgsProcessingMapLayerWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + + // with project + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( QgsProject::instance() ); + context.setProject( QgsProject::instance() ); + + QgsProcessingMapLayerWidgetWrapper wrapper2( ¶m, type ); + wrapper2.setWidgetContext( widgetContext ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingMapLayerWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper2.setWidgetValue( QStringLiteral( "band1_byte" ), context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), raster->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte" ) ); + break; + } + + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "band1_byte" ) ); + + // check signal + static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->setLayer( polygon ); + QCOMPARE( spy2.count(), 3 ); + QCOMPARE( wrapper2.widgetValue().toString(), polygon->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1 [EPSG:4326]" ) ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); + break; + } + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "l1" ) ); + + delete w; + + // optional + QgsProcessingParameterMapLayer param2( QStringLiteral( "layer" ), QStringLiteral( "layer" ), QVariant(), true ); + QgsProcessingMapLayerWidgetWrapper wrapper3( ¶m2, type ); + wrapper3.setWidgetContext( widgetContext ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingMapLayerWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "band1_byte" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), raster->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte" ) ); + break; + } + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "layer" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layer" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterMapLayer layerParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QVariant(), false, QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layer" ), context, widgetContext, &layerParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterMapLayer * >( def.get() )->dataTypes(), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + layerParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + layerParam.setDataTypes( QList< int >() << QgsProcessing::TypeRaster << QgsProcessing::TypeVectorPoint ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "layer" ), context, widgetContext, &layerParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterMapLayer * >( def.get() )->dataTypes(), QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeRaster ); +} + +void TestProcessingGui::testRasterLayerWrapper() +{ + // setup a project + QgsProject::instance()->removeAllMapLayers(); + QgsRasterLayer *raster = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/raster/band1_byte_ct_epsg4326.tif", QStringLiteral( "band1_byte" ) ); + QgsProject::instance()->addMapLayer( raster ); + QgsRasterLayer *raster2 = new QgsRasterLayer( QStringLiteral( TEST_DATA_DIR ) + "/raster/band1_byte_ct_epsg4326.tif", QStringLiteral( "band1_byte2" ) ); + QgsProject::instance()->addMapLayer( raster2 ); + + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterRasterLayer param( QStringLiteral( "raster" ), QStringLiteral( "raster" ), false ); + + QgsProcessingMapLayerWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingMapLayerWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + + // with project + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( QgsProject::instance() ); + context.setProject( QgsProject::instance() ); + + QgsProcessingMapLayerWidgetWrapper wrapper2( ¶m, type ); + wrapper2.setWidgetContext( widgetContext ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingRasterLayerWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper2.setWidgetValue( QStringLiteral( "band1_byte" ), context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), raster->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte" ) ); + break; + } + + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "band1_byte" ) ); + + // check signal + static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->setLayer( raster2 ); + QCOMPARE( spy2.count(), 3 ); + QCOMPARE( wrapper2.widgetValue().toString(), raster2->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte2 [EPSG:4326]" ) ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte2" ) ); + break; + } + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "band1_byte2" ) ); + + delete w; + + // optional + QgsProcessingParameterRasterLayer param2( QStringLiteral( "raster" ), QStringLiteral( "raster" ), QVariant(), true ); + QgsProcessingMapLayerWidgetWrapper wrapper3( ¶m2, type ); + wrapper3.setWidgetContext( widgetContext ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingMapLayerWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "band1_byte" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), raster->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "band1_byte" ) ); + break; + } + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "raster" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); +} + +void TestProcessingGui::testVectorLayerWrapper() +{ + // setup a project with a range of vector layers + QgsProject::instance()->removeAllMapLayers(); + QgsVectorLayer *point = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "point" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( point ); + QgsVectorLayer *line = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( line ); + QgsVectorLayer *polygon = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( polygon ); + QgsVectorLayer *noGeom = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( noGeom ); + + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterVectorLayer param( QStringLiteral( "vector" ), QStringLiteral( "vector" ), QList() << QgsProcessing::TypeVector, false ); + + QgsProcessingVectorLayerWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingVectorLayerWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + + // with project + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( QgsProject::instance() ); + context.setProject( QgsProject::instance() ); + + QgsProcessingMapLayerWidgetWrapper wrapper2( ¶m, type ); + wrapper2.setWidgetContext( widgetContext ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingMapLayerWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper2.setWidgetValue( QStringLiteral( "point" ), context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), point->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "point [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "point" ) ); + break; + } + + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "point" ) ); + + // check signal + static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->setLayer( polygon ); + QCOMPARE( spy2.count(), 3 ); + QCOMPARE( wrapper2.widgetValue().toString(), polygon->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1 [EPSG:4326]" ) ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); + break; + } + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "l1" ) ); + + delete w; + + // optional + QgsProcessingParameterVectorLayer param2( QStringLiteral( "vector" ), QStringLiteral( "vector" ), QList< int >() << QgsProcessing::TypeVector, QVariant(), true ); + QgsProcessingVectorLayerWidgetWrapper wrapper3( ¶m2, type ); + wrapper3.setWidgetContext( widgetContext ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingVectorLayerWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "point" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), point->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "point [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "point" ) ); + break; + } + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "vector" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "vector" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterVectorLayer layerParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "vector" ), context, widgetContext, &layerParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterVectorLayer * >( def.get() )->dataTypes(), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + layerParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + layerParam.setDataTypes( QList< int >() << QgsProcessing::TypeVectorLine << QgsProcessing::TypeVectorPoint ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "vector" ), context, widgetContext, &layerParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterVectorLayer * >( def.get() )->dataTypes(), QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); +} + +void TestProcessingGui::testFeatureSourceWrapper() +{ + // setup a project with a range of vector layers + QgsProject::instance()->removeAllMapLayers(); + QgsVectorLayer *point = new QgsVectorLayer( QStringLiteral( "Point" ), QStringLiteral( "point" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( point ); + QgsVectorLayer *line = new QgsVectorLayer( QStringLiteral( "LineString" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( line ); + QgsVectorLayer *polygon = new QgsVectorLayer( QStringLiteral( "Polygon" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( polygon ); + QgsVectorLayer *noGeom = new QgsVectorLayer( QStringLiteral( "None" ), QStringLiteral( "l1" ), QStringLiteral( "memory" ) ); + QgsProject::instance()->addMapLayer( noGeom ); + + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterFeatureSource param( QStringLiteral( "source" ), QStringLiteral( "source" ), QList() << QgsProcessing::TypeVector, false ); + + QgsProcessingFeatureSourceWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingFeatureSourceWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + + // with project + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( QgsProject::instance() ); + context.setProject( QgsProject::instance() ); + + QgsProcessingMapLayerWidgetWrapper wrapper2( ¶m, type ); + wrapper2.setWidgetContext( widgetContext ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingMapLayerWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper2.setWidgetValue( QStringLiteral( "point" ), context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), point->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "point [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "point" ) ); + break; + } + + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "point" ) ); + + // check signal + static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->setLayer( polygon ); + QCOMPARE( spy2.count(), 3 ); + QCOMPARE( wrapper2.widgetValue().toString(), polygon->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1 [EPSG:4326]" ) ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "l1" ) ); + break; + } + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "l1" ) ); + + delete w; + + // optional + QgsProcessingParameterFeatureSource param2( QStringLiteral( "source" ), QStringLiteral( "source" ), QList< int >() << QgsProcessing::TypeVector, QVariant(), true ); + QgsProcessingFeatureSourceWidgetWrapper wrapper3( ¶m2, type ); + wrapper3.setWidgetContext( widgetContext ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingFeatureSourceWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "point" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), point->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "point [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "point" ) ); + break; + } + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "source" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "source" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + QgsProcessingParameterFeatureSource sourceParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "source" ), context, widgetContext, &sourceParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterFeatureSource * >( def.get() )->dataTypes(), QList< int >() << QgsProcessing::TypeVectorAnyGeometry ); + sourceParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + sourceParam.setDataTypes( QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "source" ), context, widgetContext, &sourceParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterFeatureSource * >( def.get() )->dataTypes(), QList< int >() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); +} + +void TestProcessingGui::testMeshLayerWrapper() +{ + // setup a project with a range of layer types + QgsProject::instance()->removeAllMapLayers(); + QgsMeshLayer *mesh = new QgsMeshLayer( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle.2dm", QStringLiteral( "mesh1" ), QStringLiteral( "mdal" ) ); + mesh->dataProvider()->addDataset( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); + QVERIFY( mesh->isValid() ); + mesh->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + QgsProject::instance()->addMapLayer( mesh ); + QgsMeshLayer *mesh2 = new QgsMeshLayer( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle.2dm", QStringLiteral( "mesh2" ), QStringLiteral( "mdal" ) ); + mesh2->dataProvider()->addDataset( QStringLiteral( TEST_DATA_DIR ) + "/mesh/quad_and_triangle_vertex_scalar_with_inactive_face.dat" ); + QVERIFY( mesh2->isValid() ); + mesh2->setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ) ); + QgsProject::instance()->addMapLayer( mesh2 ); + + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterMeshLayer param( QStringLiteral( "mesh" ), QStringLiteral( "mesh" ), false ); + + QgsProcessingMeshLayerWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingMeshLayerWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + + // with project + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( QgsProject::instance() ); + context.setProject( QgsProject::instance() ); + + QgsProcessingMapLayerWidgetWrapper wrapper2( ¶m, type ); + wrapper2.setWidgetContext( widgetContext ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingMeshLayerWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper2.setWidgetValue( QStringLiteral( "mesh2" ), context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), mesh2->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "mesh2 [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "mesh2" ) ); + break; + } + + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "mesh2" ) ); + + // check signal + static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->setLayer( mesh ); + QCOMPARE( spy2.count(), 3 ); + QCOMPARE( wrapper2.widgetValue().toString(), mesh->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "mesh1 [EPSG:4326]" ) ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "mesh1" ) ); + break; + } + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper2.wrappedWidget() )->currentLayer()->name(), QStringLiteral( "mesh1" ) ); + + delete w; + + // optional + QgsProcessingParameterMeshLayer param2( QStringLiteral( "mesh" ), QStringLiteral( "mesh" ), QVariant(), true ); + QgsProcessingMeshLayerWidgetWrapper wrapper3( ¶m2, type ); + wrapper3.setWidgetContext( widgetContext ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingMeshLayerWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "mesh2" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), mesh2->id() ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "mesh2 [EPSG:4326]" ) ); + break; + case QgsProcessingGui::Modeler: + QCOMPARE( static_cast< QgsProcessingMapLayerComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "mesh2" ) ); + break; + } + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "mesh" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); +} + +void TestProcessingGui::paramConfigWidget() +{ + QgsProcessingContext context; + QgsProcessingParameterWidgetContext widgetContext; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "string" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + + // using a parameter definition as initial values + def->setDescription( QStringLiteral( "test desc" ) ); + def->setFlags( QgsProcessingParameterDefinition::FlagOptional ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "string" ), context, widgetContext, def.get() ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + def->setFlags( QgsProcessingParameterDefinition::FlagAdvanced ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "string" ), context, widgetContext, def.get() ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); +} + +void TestProcessingGui::testMapThemeWrapper() +{ + // add some themes to the project + QgsProject p; + p.mapThemeCollection()->insert( QStringLiteral( "aa" ), QgsMapThemeCollection::MapThemeRecord() ); + p.mapThemeCollection()->insert( QStringLiteral( "bb" ), QgsMapThemeCollection::MapThemeRecord() ); + + QCOMPARE( p.mapThemeCollection()->mapThemes(), QStringList() << QStringLiteral( "aa" ) << QStringLiteral( "bb" ) ); + + auto testWrapper = [&p]( QgsProcessingGui::WidgetType type ) + { + // non optional, no existing themes + QgsProcessingParameterMapTheme param( QStringLiteral( "theme" ), QStringLiteral( "theme" ), false ); + + QgsProcessingMapThemeWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + // batch or standard mode, only valid themes can be set! + QCOMPARE( spy.count(), 0 ); + QVERIFY( !wrapper.widgetValue().isValid() ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), -1 ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 0 ); + QVERIFY( !wrapper.widgetValue().isValid() ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), -1 ); + break; + + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + + // with project + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( &p ); + + QgsProcessingMapThemeWidgetWrapper wrapper2( ¶m, type ); + wrapper2.setWidgetContext( widgetContext ); + w = wrapper2.createWrappedWidget( context ); + + QSignalSpy spy2( &wrapper2, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy2.count(), 1 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper2.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy2.count(), 2 ); + QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + + // check signal + static_cast< QComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 2 ); + QCOMPARE( spy2.count(), 3 ); + + delete w; + + // optional + QgsProcessingParameterMapTheme param2( QStringLiteral( "theme" ), QStringLiteral( "theme" ), true ); + QgsProcessingMapThemeWidgetWrapper wrapper3( ¶m2, type ); + wrapper3.setWidgetContext( widgetContext ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "theme" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + widgetContext.setProject( &p ); + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QVERIFY( !static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().isValid() ); + + // using a parameter definition as initial values + QgsProcessingParameterMapTheme themeParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "aaa" ), false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext, &themeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().toString(), QStringLiteral( "aaa" ) ); + themeParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + themeParam.setDefaultValue( QStringLiteral( "xxx" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext, &themeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().toString(), QStringLiteral( "xxx" ) ); + themeParam.setDefaultValue( QVariant() ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext, &themeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QVERIFY( !static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().isValid() ); +} + +void TestProcessingGui::testDateTimeWrapper() +{ + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional, no existing themes + QgsProcessingParameterDateTime param( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::DateTime, QVariant(), false ); + + QgsProcessingDateTimeWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); + // not a date value + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy.count(), 0 ); + // not optional, so an invalid value gets a date anyway... + QVERIFY( wrapper.widgetValue().isValid() ); + wrapper.setWidgetValue( QStringLiteral( "2019-08-07" ), context ); + QCOMPARE( spy.count(), 1 ); + QVERIFY( wrapper.widgetValue().isValid() ); + QCOMPARE( wrapper.widgetValue().toDateTime(), QDateTime( QDate( 2019, 8, 7 ) ) ); + QCOMPARE( static_cast< QgsDateTimeEdit * >( wrapper.wrappedWidget() )->dateTime(), QDateTime( QDate( 2019, 8, 7 ) ) ); + wrapper.setWidgetValue( QStringLiteral( "2019-08-07" ), context ); + QCOMPARE( spy.count(), 1 ); + + // check signal + static_cast< QgsDateTimeEdit * >( wrapper.wrappedWidget() )->setDateTime( QDateTime( QDate( 2019, 8, 9 ) ) ); + QCOMPARE( spy.count(), 2 ); + + delete w; + + // optional + QgsProcessingParameterDateTime param2( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::DateTime, QVariant(), true ); + QgsProcessingDateTimeWidgetWrapper wrapper3( ¶m2, type ); + w = wrapper3.createWrappedWidget( context ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + QSignalSpy spy3( &wrapper3, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 0 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + QVERIFY( !static_cast< QgsDateTimeEdit * >( wrapper3.wrappedWidget() )->dateTime().isValid() ); + wrapper3.setWidgetValue( QStringLiteral( "2019-03-20" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toDateTime(), QDateTime( QDate( 2019, 3, 20 ) ) ); + QCOMPARE( static_cast< QgsDateTimeEdit * >( wrapper3.wrappedWidget() )->dateTime(), QDateTime( QDate( 2019, 3, 20 ) ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 2 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + // date mode + QgsProcessingParameterDateTime param3( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::Date, QVariant(), true ); + QgsProcessingDateTimeWidgetWrapper wrapper4( ¶m3, type ); + w = wrapper4.createWrappedWidget( context ); + QVERIFY( !wrapper4.widgetValue().isValid() ); + QSignalSpy spy4( &wrapper4, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); + wrapper4.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy4.count(), 0 ); + QVERIFY( !wrapper4.widgetValue().isValid() ); + QVERIFY( !static_cast< QgsDateEdit * >( wrapper4.wrappedWidget() )->date().isValid() ); + wrapper4.setWidgetValue( QStringLiteral( "2019-03-20" ), context ); + QCOMPARE( spy4.count(), 1 ); + QCOMPARE( wrapper4.widgetValue().toDate(), QDate( 2019, 3, 20 ) ); + QCOMPARE( static_cast< QgsDateEdit * >( wrapper4.wrappedWidget() )->date(), QDate( 2019, 3, 20 ) ); + wrapper4.setWidgetValue( QDate( 2020, 1, 3 ), context ); + QCOMPARE( spy4.count(), 2 ); + QCOMPARE( wrapper4.widgetValue().toDate(), QDate( 2020, 1, 3 ) ); + QCOMPARE( static_cast< QgsDateEdit * >( wrapper4.wrappedWidget() )->date(), QDate( 2020, 1, 3 ) ); + wrapper4.setWidgetValue( QVariant(), context ); + QCOMPARE( spy4.count(), 3 ); + QVERIFY( !wrapper4.widgetValue().isValid() ); + delete w; + + // time mode + QgsProcessingParameterDateTime param4( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::Time, QVariant(), true ); + QgsProcessingDateTimeWidgetWrapper wrapper5( ¶m4, type ); + w = wrapper5.createWrappedWidget( context ); + QVERIFY( !wrapper5.widgetValue().isValid() ); + QSignalSpy spy5( &wrapper5, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); + wrapper5.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy5.count(), 0 ); + QVERIFY( !wrapper5.widgetValue().isValid() ); + QVERIFY( !static_cast< QgsTimeEdit * >( wrapper5.wrappedWidget() )->time().isValid() ); + wrapper5.setWidgetValue( QStringLiteral( "11:34:56" ), context ); + QCOMPARE( spy5.count(), 1 ); + QCOMPARE( wrapper5.widgetValue().toTime(), QTime( 11, 34, 56 ) ); + QCOMPARE( static_cast< QgsTimeEdit * >( wrapper5.wrappedWidget() )->time(), QTime( 11, 34, 56 ) ); + wrapper5.setWidgetValue( QTime( 9, 34, 56 ), context ); + QCOMPARE( spy5.count(), 2 ); + QCOMPARE( wrapper5.widgetValue().toTime(), QTime( 9, 34, 56 ) ); + QCOMPARE( static_cast< QgsTimeEdit * >( wrapper5.wrappedWidget() )->time(), QTime( 9, 34, 56 ) ); + wrapper5.setWidgetValue( QVariant(), context ); + QCOMPARE( spy5.count(), 3 ); + QVERIFY( !wrapper5.widgetValue().isValid() ); + delete w; + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "datetime" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "datetime" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QVERIFY( !static_cast< QgsProcessingParameterDateTime * >( def.get() )->defaultValue().isValid() ); + + // using a parameter definition as initial values + QgsProcessingParameterDateTime datetimeParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QgsProcessingParameterDateTime::Date, QVariant(), false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "datetime" ), context, widgetContext, &datetimeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterDateTime * >( def.get() )->dataType(), QgsProcessingParameterDateTime::Date ); + datetimeParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + datetimeParam.setDefaultValue( QStringLiteral( "xxx" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "datetime" ), context, widgetContext, &datetimeParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterDateTime * >( def.get() )->dataType(), QgsProcessingParameterDateTime::Date ); +} + +void TestProcessingGui::testProviderConnectionWrapper() +{ + // register some connections + QgsProviderMetadata *md = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) ); + QgsAbstractProviderConnection *conn = md->createConnection( QStringLiteral( "test uri" ), QVariantMap() ); + md->saveConnection( conn, QStringLiteral( "aa" ) ); + md->saveConnection( conn, QStringLiteral( "bb" ) ); + + auto testWrapper = []( QgsProcessingGui::WidgetType type ) + { + QgsProcessingParameterProviderConnection param( QStringLiteral( "conn" ), QStringLiteral( "connection" ), QStringLiteral( "ogr" ), false ); + + QgsProcessingProviderConnectionWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingProviderConnectionWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QgsProviderConnectionComboBox * >( wrapper.wrappedWidget() )->currentConnection(), QStringLiteral( "bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy.count(), 1 ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( spy.count(), 2 ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + // batch or standard mode, only valid connections can be set! + // not valid + wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); + QCOMPARE( spy.count(), 3 ); + QVERIFY( !wrapper.widgetValue().isValid() ); + QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), -1 ); + break; + + } + case QgsProcessingGui::Modeler: + // invalid connections permitted + wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( static_cast< QgsProviderConnectionComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "cc" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "cc" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 4 ); + QCOMPARE( static_cast< QgsProviderConnectionComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + break; + } + + delete w; + // optional + QgsProcessingParameterProviderConnection param2( QStringLiteral( "conn" ), QStringLiteral( "connection" ), QStringLiteral( "ogr" ), QVariant(), true ); + QgsProcessingProviderConnectionWidgetWrapper wrapper3( ¶m2, type ); + w = wrapper3.createWrappedWidget( context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); + wrapper3.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "connection" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QVERIFY( !static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().isValid() ); + + // using a parameter definition as initial values + QgsProcessingParameterProviderConnection connParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "spatialite" ), QStringLiteral( "aaa" ), false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext, &connParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().toString(), QStringLiteral( "aaa" ) ); + QCOMPARE( static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->providerId(), QStringLiteral( "spatialite" ) ); + connParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + connParam.setDefaultValue( QStringLiteral( "xxx" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext, &connParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().toString(), QStringLiteral( "xxx" ) ); + connParam.setDefaultValue( QVariant() ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext, &connParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QVERIFY( !static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().isValid() ); +} + +void TestProcessingGui::testDatabaseSchemaWrapper() +{ +#ifdef ENABLE_PGTEST + // register some connections + QgsProviderMetadata *md = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "postgres" ) ); + + QString dbConn = getenv( "QGIS_PGTEST_DB" ); + if ( dbConn.isEmpty() ) + { + dbConn = "service=\"qgis_test\""; + } + QgsAbstractProviderConnection *conn = md->createConnection( QStringLiteral( "%1 sslmode=disable" ).arg( dbConn ), QVariantMap() ); + md->saveConnection( conn, QStringLiteral( "aa" ) ); + + const QStringList schemas = dynamic_cast( conn )->schemas(); + QVERIFY( !schemas.isEmpty() ); + + auto testWrapper = [&schemas]( QgsProcessingGui::WidgetType type ) + { + QgsProcessingParameterProviderConnection connParam( QStringLiteral( "conn" ), QStringLiteral( "connection" ), QStringLiteral( "postgres" ), QVariant(), true ); + TestLayerWrapper connWrapper( &connParam ); + + QgsProcessingParameterDatabaseSchema param( QStringLiteral( "schema" ), QStringLiteral( "schema" ), QStringLiteral( "conn" ), QVariant(), false ); + + QgsProcessingDatabaseSchemaWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + // no connection associated yet + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->count(), 0 ); + + // Set the parent widget connection value + connWrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + wrapper.setParentConnectionWrapperValue( &connWrapper ); + + // now we should have schemas available + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->count(), schemas.count() ); + + QSignalSpy spy( &wrapper, &QgsProcessingDatabaseSchemaWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "qgis_test" ), context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "qgis_test" ) ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->currentSchema(), QStringLiteral( "qgis_test" ) ); + wrapper.setWidgetValue( QStringLiteral( "public" ), context ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "public" ) ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->currentSchema(), QStringLiteral( "public" ) ); + QCOMPARE( spy.count(), 2 ); + wrapper.setWidgetValue( QStringLiteral( "public" ), context ); + QCOMPARE( spy.count(), 2 ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + // batch or standard mode, only valid schemas can be set! + // not valid + wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); + QCOMPARE( spy.count(), 3 ); + QVERIFY( !wrapper.widgetValue().isValid() ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentIndex(), -1 ); + break; + + } + case QgsProcessingGui::Modeler: + // invalid schemas permitted + wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "cc" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "cc" ) ); + wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( spy.count(), 4 ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "aa" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + break; + } + + // make sure things are ok if connection is changed back to nothing + connWrapper.setWidgetValue( QVariant(), context ); + wrapper.setParentConnectionWrapperValue( &connWrapper ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->count(), 0 ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + QCOMPARE( spy.count(), 3 ); + break; + } + + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 5 ); + break; + } + QVERIFY( !wrapper.widgetValue().isValid() ); + + wrapper.setWidgetValue( QStringLiteral( "qgis_test" ), context ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + QVERIFY( !wrapper.widgetValue().isValid() ); + break; + } + + case QgsProcessingGui::Modeler: + // invalid schemas permitted + QCOMPARE( spy.count(), 6 ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "qgis_test" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "qgis_test" ) ); + + break; + } + delete w; + + connWrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + + // optional + QgsProcessingParameterDatabaseSchema param2( QStringLiteral( "schema" ), QStringLiteral( "schema" ), QStringLiteral( "conn" ), QVariant(), true ); + QgsProcessingDatabaseSchemaWidgetWrapper wrapper3( ¶m2, type ); + w = wrapper3.createWrappedWidget( context ); + + wrapper3.setParentConnectionWrapperValue( &connWrapper ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingDatabaseSchemaWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "qgis_test" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "qgis_test" ) ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper3.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "qgis_test" ) ); + wrapper3.setWidgetValue( QStringLiteral( "public" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "public" ) ); + QCOMPARE( static_cast< QgsDatabaseSchemaComboBox * >( wrapper3.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "public" ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + + delete w; + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "schema" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget + QgsProcessingParameterWidgetContext widgetContext; + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databaseschema" ), context, widgetContext ); + std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QVERIFY( !static_cast< QgsProcessingParameterDatabaseSchema * >( def.get() )->defaultValue().isValid() ); + + // using a parameter definition as initial values + QgsProcessingParameterDatabaseSchema schemaParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "connparam" ), QStringLiteral( "aaa" ), false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databaseschema" ), context, widgetContext, &schemaParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseSchema * >( def.get() )->defaultValue().toString(), QStringLiteral( "aaa" ) ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseSchema * >( def.get() )->parentConnectionParameterName(), QStringLiteral( "connparam" ) ); + schemaParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + schemaParam.setDefaultValue( QStringLiteral( "xxx" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databaseschema" ), context, widgetContext, &schemaParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); + QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseSchema * >( def.get() )->defaultValue().toString(), QStringLiteral( "xxx" ) ); + schemaParam.setDefaultValue( QVariant() ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databaseschema" ), context, widgetContext, &schemaParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QVERIFY( !static_cast< QgsProcessingParameterDatabaseSchema * >( def.get() )->defaultValue().isValid() ); +#endif +} + +void TestProcessingGui::testDatabaseTableWrapper() +{ +#ifdef ENABLE_PGTEST + // register some connections + QgsProviderMetadata *md = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "postgres" ) ); + + QString dbConn = getenv( "QGIS_PGTEST_DB" ); + if ( dbConn.isEmpty() ) + { + dbConn = "service=\"qgis_test\""; + } + QgsAbstractProviderConnection *conn = md->createConnection( QStringLiteral( "%1 sslmode=disable" ).arg( dbConn ), QVariantMap() ); + md->saveConnection( conn, QStringLiteral( "aa" ) ); + + const QList tables = dynamic_cast( conn )->tables( QStringLiteral( "qgis_test" ) ); + QStringList tableNames; + for ( const QgsAbstractDatabaseProviderConnection::TableProperty &prop : tables ) + tableNames << prop.tableName(); + + QVERIFY( !tableNames.isEmpty() ); + + auto testWrapper = [&tableNames]( QgsProcessingGui::WidgetType type ) + { + QgsProcessingParameterProviderConnection connParam( QStringLiteral( "conn" ), QStringLiteral( "connection" ), QStringLiteral( "postgres" ), QVariant(), true ); + TestLayerWrapper connWrapper( &connParam ); + QgsProcessingParameterDatabaseSchema schemaParam( QStringLiteral( "schema" ), QStringLiteral( "schema" ), QStringLiteral( "connection" ), QVariant(), true ); + TestLayerWrapper schemaWrapper( &schemaParam ); + + QgsProcessingParameterDatabaseTable param( QStringLiteral( "table" ), QStringLiteral( "table" ), QStringLiteral( "conn" ), QStringLiteral( "schema" ), QVariant(), false ); + + QgsProcessingDatabaseTableWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + // no connection associated yet + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->count(), 0 ); + + // Set the parent widget connection value + connWrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + wrapper.setParentConnectionWrapperValue( &connWrapper ); + schemaWrapper.setWidgetValue( QStringLiteral( "qgis_test" ), context ); + wrapper.setParentSchemaWrapperValue( &schemaWrapper ); + + // now we should have tables available + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->count(), tableNames.count() ); + + QSignalSpy spy( &wrapper, &QgsProcessingDatabaseTableWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "someData" ), context ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "someData" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->currentTable(), QStringLiteral( "someData" ) ); + wrapper.setWidgetValue( QStringLiteral( "some_poly_data" ), context ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "some_poly_data" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->currentTable(), QStringLiteral( "some_poly_data" ) ); + QCOMPARE( spy.count(), 2 ); + wrapper.setWidgetValue( QStringLiteral( "some_poly_data" ), context ); + QCOMPARE( spy.count(), 2 ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + // batch or standard mode, only valid tables can be set! + // not valid + wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); + QCOMPARE( spy.count(), 3 ); + QVERIFY( !wrapper.widgetValue().isValid() ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentIndex(), -1 ); + break; + + } + case QgsProcessingGui::Modeler: + // invalid tables permitted + wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "cc" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "cc" ) ); + wrapper.setWidgetValue( QStringLiteral( "someData" ), context ); + QCOMPARE( spy.count(), 4 ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "someData" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "someData" ) ); + break; + } + + // make sure things are ok if connection is changed back to nothing + connWrapper.setWidgetValue( QVariant(), context ); + wrapper.setParentConnectionWrapperValue( &connWrapper ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->count(), 0 ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + QCOMPARE( spy.count(), 3 ); + break; + } + + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 5 ); + break; + } + QVERIFY( !wrapper.widgetValue().isValid() ); + + wrapper.setWidgetValue( QStringLiteral( "some_poly_data" ), context ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + { + QVERIFY( !wrapper.widgetValue().isValid() ); + break; + } + + case QgsProcessingGui::Modeler: + // invalid tables permitted + QCOMPARE( spy.count(), 6 ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "some_poly_data" ) ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "some_poly_data" ) ); + + break; + } + delete w; + + connWrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + + // optional + QgsProcessingParameterDatabaseTable param2( QStringLiteral( "table" ), QStringLiteral( "table" ), QStringLiteral( "conn" ), QStringLiteral( "schema" ), QVariant(), true ); + QgsProcessingDatabaseTableWidgetWrapper wrapper3( ¶m2, type ); + w = wrapper3.createWrappedWidget( context ); + + wrapper3.setParentConnectionWrapperValue( &connWrapper ); + wrapper3.setParentSchemaWrapperValue( &schemaWrapper ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingDatabaseTableWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "someData" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "someData" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper3.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "someData" ) ); + wrapper3.setWidgetValue( QStringLiteral( "some_poly_data" ), context ); + QCOMPARE( spy3.count(), 2 ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "some_poly_data" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper3.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "some_poly_data" ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 3 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + + delete w; + + // allowing new table names + QgsProcessingParameterDatabaseTable param3( QStringLiteral( "table" ), QStringLiteral( "table" ), QStringLiteral( "conn" ), QStringLiteral( "schema" ), QVariant(), false, true ); + QgsProcessingDatabaseTableWidgetWrapper wrapper4( ¶m3, type ); + w = wrapper4.createWrappedWidget( context ); + + wrapper4.setParentConnectionWrapperValue( &connWrapper ); + wrapper4.setParentSchemaWrapperValue( &schemaWrapper ); + + QSignalSpy spy4( &wrapper4, &QgsProcessingDatabaseTableWidgetWrapper::widgetValueHasChanged ); + wrapper4.setWidgetValue( QStringLiteral( "someData" ), context ); + QCOMPARE( spy4.count(), 1 ); + QCOMPARE( wrapper4.widgetValue().toString(), QStringLiteral( "someData" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper4.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "someData" ) ); + wrapper4.setWidgetValue( QStringLiteral( "some_poly_data" ), context ); + QCOMPARE( spy4.count(), 2 ); + QCOMPARE( wrapper4.widgetValue().toString(), QStringLiteral( "some_poly_data" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper4.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "some_poly_data" ) ); + wrapper4.setWidgetValue( QVariant(), context ); + QCOMPARE( spy4.count(), 3 ); + QVERIFY( !wrapper4.widgetValue().isValid() ); + // should always allow non existing table names + wrapper4.setWidgetValue( QStringLiteral( "someDataxxxxxxxxxxxxxxxxxxxx" ), context ); + QCOMPARE( spy4.count(), 4 ); + QCOMPARE( wrapper4.widgetValue().toString(), QStringLiteral( "someDataxxxxxxxxxxxxxxxxxxxx" ) ); + QCOMPARE( static_cast< QgsDatabaseTableComboBox * >( wrapper4.wrappedWidget() )->comboBox()->currentText(), QStringLiteral( "someDataxxxxxxxxxxxxxxxxxxxx" ) ); - // combination point and line only - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - param2 = qgis::make_unique< QgsProcessingParameterFeatureSource> ( QStringLiteral( "param" ), QString(), QList< int>() << QgsProcessing::TypeVectorPoint << QgsProcessing::TypeVectorLine ); - combo2 = qgis::make_unique< QgsProcessingMapLayerComboBox >( param2.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo2->setLayer( point ); - QCOMPARE( combo2->currentLayer(), point ); - combo->setLayer( line ); - QCOMPARE( combo->currentLayer(), line ); - combo2->setLayer( line ); - QCOMPARE( combo2->currentLayer(), line ); - combo->setLayer( polygon ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( polygon ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( noGeom ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( noGeom ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( mesh ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( mesh ); - QVERIFY( !combo2->currentLayer() ); - combo->setLayer( raster ); - QVERIFY( !combo->currentLayer() ); - combo2->setLayer( raster ); - QVERIFY( !combo2->currentLayer() ); - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); - // optional - param = qgis::make_unique< QgsProcessingParameterVectorLayer> ( QStringLiteral( "param" ), QString(), QList< int>(), QVariant(), true ); - combo = qgis::make_unique< QgsProcessingMapLayerComboBox >( param.get() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo->setLayer( nullptr ); - QVERIFY( !combo->currentLayer() ); - QVERIFY( !combo->value().isValid() ); - combo->setLayer( point ); - QCOMPARE( combo->currentLayer(), point ); - combo->setValue( QVariant(), context ); - QVERIFY( !combo->currentLayer() ); - QVERIFY( !combo->value().isValid() ); + delete w; - combo2.reset(); - param2.reset(); - combo.reset(); - param.reset(); - QgsProject::instance()->removeAllMapLayers(); -} -void TestProcessingGui::paramConfigWidget() -{ - QgsProcessingContext context; + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "table" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); + + // config widget QgsProcessingParameterWidgetContext widgetContext; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "string" ), context, widgetContext ); + QgsProcessingContext context; + std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databasetable" ), context, widgetContext ); std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); + QVERIFY( !static_cast< QgsProcessingParameterDatabaseTable * >( def.get() )->defaultValue().isValid() ); // using a parameter definition as initial values - def->setDescription( QStringLiteral( "test desc" ) ); - def->setFlags( QgsProcessingParameterDefinition::FlagOptional ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "string" ), context, widgetContext, def.get() ); + QgsProcessingParameterDatabaseTable tableParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "connparam" ), QStringLiteral( "schemaparam" ), QStringLiteral( "aaa" ), false ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databasetable" ), context, widgetContext, &tableParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); + QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - def->setFlags( QgsProcessingParameterDefinition::FlagAdvanced ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "string" ), context, widgetContext, def.get() ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseTable * >( def.get() )->defaultValue().toString(), QStringLiteral( "aaa" ) ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseTable * >( def.get() )->parentConnectionParameterName(), QStringLiteral( "connparam" ) ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseTable * >( def.get() )->parentSchemaParameterName(), QStringLiteral( "schemaparam" ) ); + tableParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); + tableParam.setDefaultValue( QStringLiteral( "xxx" ) ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databasetable" ), context, widgetContext, &tableParam ); def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); + QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); + QCOMPARE( static_cast< QgsProcessingParameterDatabaseTable * >( def.get() )->defaultValue().toString(), QStringLiteral( "xxx" ) ); + tableParam.setDefaultValue( QVariant() ); + widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "databasetable" ), context, widgetContext, &tableParam ); + def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); + QVERIFY( !static_cast< QgsProcessingParameterDatabaseTable * >( def.get() )->defaultValue().isValid() ); +#endif } -void TestProcessingGui::testMapThemeWrapper() +void TestProcessingGui::testOutputDefinitionWidget() { - // add some themes to the project - QgsProject p; - p.mapThemeCollection()->insert( QStringLiteral( "aa" ), QgsMapThemeCollection::MapThemeRecord() ); - p.mapThemeCollection()->insert( QStringLiteral( "bb" ), QgsMapThemeCollection::MapThemeRecord() ); + QgsProcessingParameterFeatureSink sink( QStringLiteral( "test" ) ); + QgsProcessingLayerOutputDestinationWidget panel( &sink, false ); + + QSignalSpy skipSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + QVariant v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + QgsProcessingOutputLayerDefinition def; + def.sink.setStaticValue( QgsProcessing::TEMPORARY_OUTPUT ); + def.createOptions.insert( QStringLiteral( "fileEncoding" ), QStringLiteral( "utf8" ) ); + panel.setValue( def ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + + panel.setValue( QStringLiteral( "memory:" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + def.sink.setStaticValue( QStringLiteral( "memory:" ) ); + panel.setValue( def ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + + panel.setValue( QStringLiteral( "ogr:dbname='/me/a.gpkg' table=\"d\" (geom) sql=''" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "ogr:dbname='/me/a.gpkg' table=\"d\" (geom) sql=''" ) ); + QVERIFY( !panel.outputIsSkipped() ); + panel.setValue( QStringLiteral( "ogr:dbname='/me/a.gpkg' table=\"d\" (geom) sql=''" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + + panel.setValue( QStringLiteral( "postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table=\"stufff\".\"output\" (the_geom) sql=" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table=\"stufff\".\"output\" (the_geom) sql=" ) ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 2 ); + panel.setValue( QStringLiteral( "postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table=\"stufff\".\"output\" (the_geom) sql=" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 2 ); + + panel.setValue( QStringLiteral( "/home/me/test.shp" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "/home/me/test.shp" ) ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 3 ); + panel.setValue( QStringLiteral( "/home/me/test.shp" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 3 ); + panel.setValue( QStringLiteral( "/home/me/test2.shp" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 4 ); + + QgsSettings settings; + settings.setValue( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), TEST_DATA_DIR ); + panel.setValue( QStringLiteral( "test.shp" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "utf8" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), TEST_DATA_DIR + QStringLiteral( "/test.shp" ) ); + + // optional, test skipping + sink.setFlags( sink.flags() | QgsProcessingParameterDefinition::FlagOptional ); + sink.setCreateByDefault( true ); + QgsProcessingLayerOutputDestinationWidget panel2( &sink, false ); + + QSignalSpy skipSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel2.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel2.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + + panel2.setValue( QVariant() ); + v = panel2.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 1 ); + QCOMPARE( changedSpy2.count(), 1 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 2 ); + QCOMPARE( changedSpy2.count(), 2 ); + + sink.setCreateByDefault( false ); + QgsProcessingLayerOutputDestinationWidget panel3( &sink, false ); + + QSignalSpy skipSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel3.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); - QCOMPARE( p.mapThemeCollection()->mapThemes(), QStringList() << QStringLiteral( "aa" ) << QStringLiteral( "bb" ) ); + panel3.setValue( QVariant() ); + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 2 ); + QCOMPARE( changedSpy3.count(), 2 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 3 ); + QCOMPARE( changedSpy3.count(), 3 ); + + // with remapping + def = QgsProcessingOutputLayerDefinition( QStringLiteral( "test.shp" ) ); + QgsRemappingSinkDefinition remap; + QMap< QString, QgsProperty > fieldMap; + fieldMap.insert( QStringLiteral( "field1" ), QgsProperty::fromField( QStringLiteral( "source1" ) ) ); + fieldMap.insert( QStringLiteral( "field2" ), QgsProperty::fromExpression( QStringLiteral( "source || source2" ) ) ); + remap.setFieldMap( fieldMap ); + def.setRemappingDefinition( remap ); + + panel3.setValue( def ); + v = panel3.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QVERIFY( v.value< QgsProcessingOutputLayerDefinition>().useRemapping() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().remappingDefinition().fieldMap().size(), 2 ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().remappingDefinition().fieldMap().value( QStringLiteral( "field1" ) ), QgsProperty::fromField( QStringLiteral( "source1" ) ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().remappingDefinition().fieldMap().value( QStringLiteral( "field2" ) ), QgsProperty::fromExpression( QStringLiteral( "source || source2" ) ) ); + + panel3.setValue( QStringLiteral( "other.shp" ) ); + v = panel3.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QVERIFY( !v.value< QgsProcessingOutputLayerDefinition>().useRemapping() ); +} - auto testWrapper = [&p]( QgsProcessingGui::WidgetType type ) +void TestProcessingGui::testOutputDefinitionWidgetVectorOut() +{ + QgsProcessingParameterVectorDestination vector( QStringLiteral( "test" ) ); + QgsProcessingLayerOutputDestinationWidget panel( &vector, false ); + + QSignalSpy skipSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + QVariant v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + panel.setValue( QStringLiteral( "memory:" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + + panel.setValue( QStringLiteral( "ogr:dbname='/me/a.gpkg' table=\"d\" (geom) sql=''" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "ogr:dbname='/me/a.gpkg' table=\"d\" (geom) sql=''" ) ); + QVERIFY( !panel.outputIsSkipped() ); + panel.setValue( QStringLiteral( "ogr:dbname='/me/a.gpkg' table=\"d\" (geom) sql=''" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + + panel.setValue( QStringLiteral( "postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table=\"stufff\".\"output\" (the_geom) sql=" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table=\"stufff\".\"output\" (the_geom) sql=" ) ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 2 ); + panel.setValue( QStringLiteral( "postgis:dbname='oraclesux' host=10.1.1.221 port=5432 user='qgis' password='qgis' table=\"stufff\".\"output\" (the_geom) sql=" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 2 ); + + panel.setValue( QStringLiteral( "/home/me/test.shp" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "/home/me/test.shp" ) ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 3 ); + panel.setValue( QStringLiteral( "/home/me/test.shp" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 3 ); + panel.setValue( QStringLiteral( "/home/me/test2.shp" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 4 ); + + QgsSettings settings; + settings.setValue( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), TEST_DATA_DIR ); + panel.setValue( QStringLiteral( "test.shp" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), TEST_DATA_DIR + QStringLiteral( "/test.shp" ) ); + + // optional, test skipping + vector.setFlags( vector.flags() | QgsProcessingParameterDefinition::FlagOptional ); + vector.setCreateByDefault( true ); + QgsProcessingLayerOutputDestinationWidget panel2( &vector, false ); + + QSignalSpy skipSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel2.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel2.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + + panel2.setValue( QVariant() ); + v = panel2.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 1 ); + QCOMPARE( changedSpy2.count(), 1 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 2 ); + QCOMPARE( changedSpy2.count(), 2 ); + + vector.setCreateByDefault( false ); + QgsProcessingLayerOutputDestinationWidget panel3( &vector, false ); + + QSignalSpy skipSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel3.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + + panel3.setValue( QVariant() ); + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 2 ); + QCOMPARE( changedSpy3.count(), 2 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 3 ); + QCOMPARE( changedSpy3.count(), 3 ); +} + +void TestProcessingGui::testOutputDefinitionWidgetRasterOut() +{ + QgsProcessingParameterRasterDestination raster( QStringLiteral( "test" ) ); + QgsProcessingLayerOutputDestinationWidget panel( &raster, false ); + + QSignalSpy skipSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + QVariant v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + panel.setValue( QStringLiteral( "/home/me/test.tif" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QStringLiteral( "/home/me/test.tif" ) ); + QVERIFY( !panel.outputIsSkipped() ); + panel.setValue( QStringLiteral( "/home/me/test.tif" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + + QgsSettings settings; + settings.setValue( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), TEST_DATA_DIR ); + panel.setValue( QStringLiteral( "test.tif" ) ); + v = panel.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), TEST_DATA_DIR + QStringLiteral( "/test.tif" ) ); + + // optional, test skipping + raster.setFlags( raster.flags() | QgsProcessingParameterDefinition::FlagOptional ); + raster.setCreateByDefault( true ); + QgsProcessingLayerOutputDestinationWidget panel2( &raster, false ); + + QSignalSpy skipSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel2.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel2.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + + panel2.setValue( QVariant() ); + v = panel2.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 1 ); + QCOMPARE( changedSpy2.count(), 1 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 2 ); + QCOMPARE( changedSpy2.count(), 2 ); + + raster.setCreateByDefault( false ); + QgsProcessingLayerOutputDestinationWidget panel3( &raster, false ); + + QSignalSpy skipSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel3.value(); + QVERIFY( v.canConvert< QgsProcessingOutputLayerDefinition>() ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().createOptions.value( QStringLiteral( "fileEncoding" ) ).toString(), QStringLiteral( "System" ) ); + QCOMPARE( v.value< QgsProcessingOutputLayerDefinition>().sink.staticValue().toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + + panel3.setValue( QVariant() ); + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 2 ); + QCOMPARE( changedSpy3.count(), 2 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 3 ); + QCOMPARE( changedSpy3.count(), 3 ); +} + +void TestProcessingGui::testOutputDefinitionWidgetFolder() +{ + QgsProcessingParameterFolderDestination folder( QStringLiteral( "test" ) ); + QgsProcessingLayerOutputDestinationWidget panel( &folder, false ); + + QSignalSpy skipSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + QVariant v = panel.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + panel.setValue( QStringLiteral( "/home/me/" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + v = panel.value(); + QCOMPARE( v.toString(), QStringLiteral( "/home/me/" ) ); + QVERIFY( !panel.outputIsSkipped() ); + panel.setValue( QStringLiteral( "/home/me/" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + + QgsSettings settings; + settings.setValue( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), TEST_DATA_DIR ); + panel.setValue( QStringLiteral( "mystuff" ) ); + v = panel.value(); + QCOMPARE( v.toString(), TEST_DATA_DIR + QStringLiteral( "/mystuff" ) ); + + // optional, test skipping + folder.setFlags( folder.flags() | QgsProcessingParameterDefinition::FlagOptional ); + folder.setCreateByDefault( true ); + QgsProcessingLayerOutputDestinationWidget panel2( &folder, false ); + + QSignalSpy skipSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel2.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel2.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + + panel2.setValue( QVariant() ); + v = panel2.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 1 ); + QCOMPARE( changedSpy2.count(), 1 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 2 ); + QCOMPARE( changedSpy2.count(), 2 ); + + folder.setCreateByDefault( false ); + QgsProcessingLayerOutputDestinationWidget panel3( &folder, false ); + + QSignalSpy skipSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel3.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + + panel3.setValue( QVariant() ); + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 2 ); + QCOMPARE( changedSpy3.count(), 2 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 3 ); + QCOMPARE( changedSpy3.count(), 3 ); +} + +void TestProcessingGui::testOutputDefinitionWidgetFileOut() +{ + QgsProcessingParameterFileDestination file( QStringLiteral( "test" ) ); + QgsProcessingLayerOutputDestinationWidget panel( &file, false ); + + QSignalSpy skipSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy( &panel, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + QVariant v = panel.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel.outputIsSkipped() ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + panel.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 0 ); + + panel.setValue( QStringLiteral( "/home/me/test.tif" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + v = panel.value(); + QCOMPARE( v.toString(), QStringLiteral( "/home/me/test.tif" ) ); + QVERIFY( !panel.outputIsSkipped() ); + panel.setValue( QStringLiteral( "/home/me/test.tif" ) ); + QCOMPARE( skipSpy.count(), 0 ); + QCOMPARE( changedSpy.count(), 1 ); + + QgsSettings settings; + settings.setValue( QStringLiteral( "/Processing/Configuration/OUTPUTS_FOLDER" ), TEST_DATA_DIR ); + panel.setValue( QStringLiteral( "test.tif" ) ); + v = panel.value(); + QCOMPARE( v.toString(), TEST_DATA_DIR + QStringLiteral( "/test.tif" ) ); + + // optional, test skipping + file.setFlags( file.flags() | QgsProcessingParameterDefinition::FlagOptional ); + file.setCreateByDefault( true ); + QgsProcessingLayerOutputDestinationWidget panel2( &file, false ); + + QSignalSpy skipSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy2( &panel2, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel2.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel2.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 0 ); + QCOMPARE( changedSpy2.count(), 0 ); + + panel2.setValue( QVariant() ); + v = panel2.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel2.outputIsSkipped() ); + QCOMPARE( skipSpy2.count(), 1 ); + QCOMPARE( changedSpy2.count(), 1 ); + panel2.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy2.count(), 2 ); + QCOMPARE( changedSpy2.count(), 2 ); + + file.setCreateByDefault( false ); + QgsProcessingLayerOutputDestinationWidget panel3( &file, false ); + + QSignalSpy skipSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::skipOutputChanged ); + QSignalSpy changedSpy3( &panel3, &QgsProcessingLayerOutputDestinationWidget::destinationChanged ); + + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + v = panel3.value(); + QCOMPARE( v.toString(), QgsProcessing::TEMPORARY_OUTPUT ); + QVERIFY( !panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 1 ); + QCOMPARE( changedSpy3.count(), 1 ); + + panel3.setValue( QVariant() ); + v = panel3.value(); + QVERIFY( !v.isValid() ); + QVERIFY( panel3.outputIsSkipped() ); + QCOMPARE( skipSpy3.count(), 2 ); + QCOMPARE( changedSpy3.count(), 2 ); + panel3.setValue( QgsProcessing::TEMPORARY_OUTPUT ); + QCOMPARE( skipSpy3.count(), 3 ); + QCOMPARE( changedSpy3.count(), 3 ); +} + +void TestProcessingGui::testFeatureSourceOptionsWidget() +{ + QgsProcessingFeatureSourceOptionsWidget w; + QSignalSpy spy( &w, &QgsProcessingFeatureSourceOptionsWidget::widgetChanged ); + + w.setFeatureLimit( 66 ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( w.featureLimit(), 66 ); + w.setFeatureLimit( 66 ); + QCOMPARE( spy.count(), 1 ); + w.setFeatureLimit( -1 ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( w.featureLimit(), -1 ); + + w.setGeometryCheckMethod( false, QgsFeatureRequest::GeometrySkipInvalid ); + QCOMPARE( spy.count(), 2 ); + QVERIFY( !w.isOverridingInvalidGeometryCheck() ); + w.setGeometryCheckMethod( true, QgsFeatureRequest::GeometrySkipInvalid ); + QCOMPARE( spy.count(), 3 ); + QVERIFY( w.isOverridingInvalidGeometryCheck() ); + QCOMPARE( w.geometryCheckMethod(), QgsFeatureRequest::GeometrySkipInvalid ); + w.setGeometryCheckMethod( true, QgsFeatureRequest::GeometrySkipInvalid ); + QCOMPARE( spy.count(), 3 ); + w.setGeometryCheckMethod( true, QgsFeatureRequest::GeometryAbortOnInvalid ); + QCOMPARE( spy.count(), 4 ); + QVERIFY( w.isOverridingInvalidGeometryCheck() ); + QCOMPARE( w.geometryCheckMethod(), QgsFeatureRequest::GeometryAbortOnInvalid ); + w.setGeometryCheckMethod( false, QgsFeatureRequest::GeometryAbortOnInvalid ); + QVERIFY( !w.isOverridingInvalidGeometryCheck() ); + QCOMPARE( spy.count(), 5 ); +} + +void TestProcessingGui::testVectorOutWrapper() +{ + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) { - // non optional, no existing themes - QgsProcessingParameterMapTheme param( QStringLiteral( "theme" ), QStringLiteral( "theme" ), false ); + // non optional + QgsProcessingParameterVectorDestination param( QStringLiteral( "vector" ), QStringLiteral( "vector" ) ); - QgsProcessingMapThemeWidgetWrapper wrapper( ¶m, type ); + QgsProcessingVectorDestinationWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; QWidget *w = wrapper.createWrappedWidget( context ); - QSignalSpy spy( &wrapper, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); + QSignalSpy spy( &wrapper, &QgsProcessingVectorDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "/bb.shp" ), context ); switch ( type ) { case QgsProcessingGui::Standard: case QgsProcessingGui::Batch: - // batch or standard mode, only valid themes can be set! - QCOMPARE( spy.count(), 0 ); - QVERIFY( !wrapper.widgetValue().isValid() ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), -1 ); - wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); - QCOMPARE( spy.count(), 0 ); - QVERIFY( !wrapper.widgetValue().isValid() ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), -1 ); + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + wrapper.setWidgetValue( QStringLiteral( "/aa.shp" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/aa.shp" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/aa.shp" ) ); break; + } + + // check signal + static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->setValue( QStringLiteral( "/cc.shp" ) ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/cc.shp" ) ); + delete w; + + // optional + QgsProcessingParameterVectorDestination param2( QStringLiteral( "vector" ), QStringLiteral( "vector" ), QgsProcessing::TypeVector, QVariant(), true ); + QgsProcessingVectorDestinationWidgetWrapper wrapper3( ¶m2, type ); + w = wrapper3.createWrappedWidget( context ); + QSignalSpy spy3( &wrapper3, &QgsProcessingVectorDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "/bb.shp" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper3.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 2 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; + + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "vector" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; + + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + // testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); +} + +void TestProcessingGui::testSinkWrapper() +{ + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterFeatureSink param( QStringLiteral( "sink" ), QStringLiteral( "sink" ) ); + + QgsProcessingFeatureSinkWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingFeatureSinkWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "/bb.shp" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: case QgsProcessingGui::Modeler: QCOMPARE( spy.count(), 1 ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); - wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + wrapper.setWidgetValue( QStringLiteral( "/aa.shp" ), context ); QCOMPARE( spy.count(), 2 ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/aa.shp" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/aa.shp" ) ); break; } + // check signal + static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->setValue( QStringLiteral( "/cc.shp" ) ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/cc.shp" ) ); delete w; - // with project - QgsProcessingParameterWidgetContext widgetContext; - widgetContext.setProject( &p ); + // optional + QgsProcessingParameterFeatureSink param2( QStringLiteral( "sink" ), QStringLiteral( "sink" ), QgsProcessing::TypeVector, QVariant(), true ); + QgsProcessingFeatureSinkWidgetWrapper wrapper3( ¶m2, type ); + w = wrapper3.createWrappedWidget( context ); - QgsProcessingMapThemeWidgetWrapper wrapper2( ¶m, type ); - wrapper2.setWidgetContext( widgetContext ); - w = wrapper2.createWrappedWidget( context ); + QSignalSpy spy3( &wrapper3, &QgsProcessingFeatureSinkWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "/bb.shp" ), context ); + QCOMPARE( spy3.count(), 1 ); + QCOMPARE( wrapper3.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper3.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.shp" ) ); + wrapper3.setWidgetValue( QVariant(), context ); + QCOMPARE( spy3.count(), 2 ); + QVERIFY( !wrapper3.widgetValue().isValid() ); + delete w; - QSignalSpy spy2( &wrapper2, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper2.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy2.count(), 1 ); - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "bb" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); - wrapper2.setWidgetValue( QStringLiteral( "aa" ), context ); - QCOMPARE( spy2.count(), 2 ); - QCOMPARE( wrapper2.widgetValue().toString(), QStringLiteral( "aa" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper2.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + QLabel *l = wrapper.createWrappedLabel(); + if ( wrapper.type() != QgsProcessingGui::Batch ) + { + QVERIFY( l ); + QCOMPARE( l->text(), QStringLiteral( "sink" ) ); + QCOMPARE( l->toolTip(), param.toolTip() ); + delete l; + } + else + { + QVERIFY( !l ); + } + }; - // check signal - static_cast< QComboBox * >( wrapper2.wrappedWidget() )->setCurrentIndex( 2 ); - QCOMPARE( spy2.count(), 3 ); + // standard wrapper + testWrapper( QgsProcessingGui::Standard ); + + // batch wrapper + // testWrapper( QgsProcessingGui::Batch ); + + // modeler wrapper + testWrapper( QgsProcessingGui::Modeler ); +} + +void TestProcessingGui::testRasterOutWrapper() +{ + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) + { + // non optional + QgsProcessingParameterRasterDestination param( QStringLiteral( "raster" ), QStringLiteral( "raster" ) ); + + QgsProcessingRasterDestinationWidgetWrapper wrapper( ¶m, type ); + + QgsProcessingContext context; + QWidget *w = wrapper.createWrappedWidget( context ); + + QSignalSpy spy( &wrapper, &QgsProcessingRasterDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "/bb.tif" ), context ); + + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.tif" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.tif" ) ); + wrapper.setWidgetValue( QStringLiteral( "/aa.tif" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/aa.tif" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/aa.tif" ) ); + break; + } + // check signal + static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->setValue( QStringLiteral( "/cc.tif" ) ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( wrapper.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/cc.tif" ) ); delete w; // optional - QgsProcessingParameterMapTheme param2( QStringLiteral( "theme" ), QStringLiteral( "theme" ), true ); - QgsProcessingMapThemeWidgetWrapper wrapper3( ¶m2, type ); - wrapper3.setWidgetContext( widgetContext ); + QgsProcessingParameterRasterDestination param2( QStringLiteral( "raster" ), QStringLiteral( "raster" ), QVariant(), true ); + QgsProcessingRasterDestinationWidgetWrapper wrapper3( ¶m2, type ); w = wrapper3.createWrappedWidget( context ); - QSignalSpy spy3( &wrapper3, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QSignalSpy spy3( &wrapper3, &QgsProcessingRasterDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "/bb.tif" ), context ); QCOMPARE( spy3.count(), 1 ); - QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); - wrapper3.setWidgetValue( QStringLiteral( "aa" ), context ); - QCOMPARE( spy3.count(), 2 ); - QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "aa" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + QCOMPARE( wrapper3.widgetValue().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.tif" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper3.wrappedWidget() )->value().value< QgsProcessingOutputLayerDefinition >().sink.staticValue().toString(), QStringLiteral( "/bb.tif" ) ); wrapper3.setWidgetValue( QVariant(), context ); - QCOMPARE( spy3.count(), 3 ); + QCOMPARE( spy3.count(), 2 ); QVERIFY( !wrapper3.widgetValue().isValid() ); delete w; - QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "theme" ) ); + QCOMPARE( l->text(), QStringLiteral( "raster" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -4420,149 +8092,68 @@ void TestProcessingGui::testMapThemeWrapper() testWrapper( QgsProcessingGui::Standard ); // batch wrapper - testWrapper( QgsProcessingGui::Batch ); + // testWrapper( QgsProcessingGui::Batch ); // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); - - - // config widget - QgsProcessingParameterWidgetContext widgetContext; - widgetContext.setProject( &p ); - QgsProcessingContext context; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext ); - std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QVERIFY( !static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().isValid() ); - - // using a parameter definition as initial values - QgsProcessingParameterMapTheme themeParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "aaa" ), false ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext, &themeParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QCOMPARE( static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().toString(), QStringLiteral( "aaa" ) ); - themeParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); - themeParam.setDefaultValue( QStringLiteral( "xxx" ) ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext, &themeParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); - QCOMPARE( static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().toString(), QStringLiteral( "xxx" ) ); - themeParam.setDefaultValue( QVariant() ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "maptheme" ), context, widgetContext, &themeParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QVERIFY( !static_cast< QgsProcessingParameterMapTheme * >( def.get() )->defaultValue().isValid() ); } -void TestProcessingGui::testDateTimeWrapper() +void TestProcessingGui::testFileOutWrapper() { auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) { - // non optional, no existing themes - QgsProcessingParameterDateTime param( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::DateTime, QVariant(), false ); + // non optional + QgsProcessingParameterFileDestination param( QStringLiteral( "file" ), QStringLiteral( "file" ) ); - QgsProcessingDateTimeWidgetWrapper wrapper( ¶m, type ); + QgsProcessingFileDestinationWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; QWidget *w = wrapper.createWrappedWidget( context ); - QSignalSpy spy( &wrapper, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); - // not a date value - wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy.count(), 0 ); - // not optional, so an invalid value gets a date anyway... - QVERIFY( wrapper.widgetValue().isValid() ); - wrapper.setWidgetValue( QStringLiteral( "2019-08-07" ), context ); - QCOMPARE( spy.count(), 1 ); - QVERIFY( wrapper.widgetValue().isValid() ); - QCOMPARE( wrapper.widgetValue().toDateTime(), QDateTime( QDate( 2019, 8, 7 ) ) ); - QCOMPARE( static_cast< QgsDateTimeEdit * >( wrapper.wrappedWidget() )->dateTime(), QDateTime( QDate( 2019, 8, 7 ) ) ); - wrapper.setWidgetValue( QStringLiteral( "2019-08-07" ), context ); - QCOMPARE( spy.count(), 1 ); + QSignalSpy spy( &wrapper, &QgsProcessingFileDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "/bb.tif" ), context ); - // check signal - static_cast< QgsDateTimeEdit * >( wrapper.wrappedWidget() )->setDateTime( QDateTime( QDate( 2019, 8, 9 ) ) ); - QCOMPARE( spy.count(), 2 ); + switch ( type ) + { + case QgsProcessingGui::Standard: + case QgsProcessingGui::Batch: + case QgsProcessingGui::Modeler: + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "/bb.tif" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().toString(), QStringLiteral( "/bb.tif" ) ); + wrapper.setWidgetValue( QStringLiteral( "/aa.tif" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "/aa.tif" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().toString(), QStringLiteral( "/aa.tif" ) ); + break; + } + // check signal + static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->setValue( QStringLiteral( "/cc.tif" ) ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "/cc.tif" ) ); delete w; // optional - QgsProcessingParameterDateTime param2( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::DateTime, QVariant(), true ); - QgsProcessingDateTimeWidgetWrapper wrapper3( ¶m2, type ); + QgsProcessingParameterFileDestination param2( QStringLiteral( "file" ), QStringLiteral( "file" ), QString(), QVariant(), true ); + QgsProcessingFileDestinationWidgetWrapper wrapper3( ¶m2, type ); w = wrapper3.createWrappedWidget( context ); - QVERIFY( !wrapper3.widgetValue().isValid() ); - QSignalSpy spy3( &wrapper3, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); - wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy3.count(), 0 ); - QVERIFY( !wrapper3.widgetValue().isValid() ); - QVERIFY( !static_cast< QgsDateTimeEdit * >( wrapper3.wrappedWidget() )->dateTime().isValid() ); - wrapper3.setWidgetValue( QStringLiteral( "2019-03-20" ), context ); + + QSignalSpy spy3( &wrapper3, &QgsProcessingFileDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "/bb.tif" ), context ); QCOMPARE( spy3.count(), 1 ); - QCOMPARE( wrapper3.widgetValue().toDateTime(), QDateTime( QDate( 2019, 3, 20 ) ) ); - QCOMPARE( static_cast< QgsDateTimeEdit * >( wrapper3.wrappedWidget() )->dateTime(), QDateTime( QDate( 2019, 3, 20 ) ) ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "/bb.tif" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper3.wrappedWidget() )->value().toString(), QStringLiteral( "/bb.tif" ) ); wrapper3.setWidgetValue( QVariant(), context ); QCOMPARE( spy3.count(), 2 ); QVERIFY( !wrapper3.widgetValue().isValid() ); delete w; - // date mode - QgsProcessingParameterDateTime param3( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::Date, QVariant(), true ); - QgsProcessingDateTimeWidgetWrapper wrapper4( ¶m3, type ); - w = wrapper4.createWrappedWidget( context ); - QVERIFY( !wrapper4.widgetValue().isValid() ); - QSignalSpy spy4( &wrapper4, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); - wrapper4.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy4.count(), 0 ); - QVERIFY( !wrapper4.widgetValue().isValid() ); - QVERIFY( !static_cast< QgsDateEdit * >( wrapper4.wrappedWidget() )->date().isValid() ); - wrapper4.setWidgetValue( QStringLiteral( "2019-03-20" ), context ); - QCOMPARE( spy4.count(), 1 ); - QCOMPARE( wrapper4.widgetValue().toDate(), QDate( 2019, 3, 20 ) ); - QCOMPARE( static_cast< QgsDateEdit * >( wrapper4.wrappedWidget() )->date(), QDate( 2019, 3, 20 ) ); - wrapper4.setWidgetValue( QDate( 2020, 1, 3 ), context ); - QCOMPARE( spy4.count(), 2 ); - QCOMPARE( wrapper4.widgetValue().toDate(), QDate( 2020, 1, 3 ) ); - QCOMPARE( static_cast< QgsDateEdit * >( wrapper4.wrappedWidget() )->date(), QDate( 2020, 1, 3 ) ); - wrapper4.setWidgetValue( QVariant(), context ); - QCOMPARE( spy4.count(), 3 ); - QVERIFY( !wrapper4.widgetValue().isValid() ); - delete w; - - // time mode - QgsProcessingParameterDateTime param4( QStringLiteral( "datetime" ), QStringLiteral( "datetime" ), QgsProcessingParameterDateTime::Time, QVariant(), true ); - QgsProcessingDateTimeWidgetWrapper wrapper5( ¶m4, type ); - w = wrapper5.createWrappedWidget( context ); - QVERIFY( !wrapper5.widgetValue().isValid() ); - QSignalSpy spy5( &wrapper5, &QgsProcessingDateTimeWidgetWrapper::widgetValueHasChanged ); - wrapper5.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy5.count(), 0 ); - QVERIFY( !wrapper5.widgetValue().isValid() ); - QVERIFY( !static_cast< QgsTimeEdit * >( wrapper5.wrappedWidget() )->time().isValid() ); - wrapper5.setWidgetValue( QStringLiteral( "11:34:56" ), context ); - QCOMPARE( spy5.count(), 1 ); - QCOMPARE( wrapper5.widgetValue().toTime(), QTime( 11, 34, 56 ) ); - QCOMPARE( static_cast< QgsTimeEdit * >( wrapper5.wrappedWidget() )->time(), QTime( 11, 34, 56 ) ); - wrapper5.setWidgetValue( QTime( 9, 34, 56 ), context ); - QCOMPARE( spy5.count(), 2 ); - QCOMPARE( wrapper5.widgetValue().toTime(), QTime( 9, 34, 56 ) ); - QCOMPARE( static_cast< QgsTimeEdit * >( wrapper5.wrappedWidget() )->time(), QTime( 9, 34, 56 ) ); - wrapper5.setWidgetValue( QVariant(), context ); - QCOMPARE( spy5.count(), 3 ); - QVERIFY( !wrapper5.widgetValue().isValid() ); - delete w; - QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "datetime" ) ); + QCOMPARE( l->text(), QStringLiteral( "file" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -4576,120 +8167,68 @@ void TestProcessingGui::testDateTimeWrapper() testWrapper( QgsProcessingGui::Standard ); // batch wrapper - testWrapper( QgsProcessingGui::Batch ); + // testWrapper( QgsProcessingGui::Batch ); // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); - - // config widget - QgsProcessingParameterWidgetContext widgetContext; - QgsProcessingContext context; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "datetime" ), context, widgetContext ); - std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QVERIFY( !static_cast< QgsProcessingParameterDateTime * >( def.get() )->defaultValue().isValid() ); - - // using a parameter definition as initial values - QgsProcessingParameterDateTime datetimeParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QgsProcessingParameterDateTime::Date, QVariant(), false ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "datetime" ), context, widgetContext, &datetimeParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QCOMPARE( static_cast< QgsProcessingParameterDateTime * >( def.get() )->dataType(), QgsProcessingParameterDateTime::Date ); - datetimeParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); - datetimeParam.setDefaultValue( QStringLiteral( "xxx" ) ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "datetime" ), context, widgetContext, &datetimeParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); - QCOMPARE( static_cast< QgsProcessingParameterDateTime * >( def.get() )->dataType(), QgsProcessingParameterDateTime::Date ); } -void TestProcessingGui::testProviderConnectionWrapper() +void TestProcessingGui::testFolderOutWrapper() { - // register some connections - QgsProviderMetadata *md = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) ); - QgsAbstractProviderConnection *conn = md->createConnection( QStringLiteral( "test uri" ), QVariantMap() ); - md->saveConnection( conn, QStringLiteral( "aa" ) ); - md->saveConnection( conn, QStringLiteral( "bb" ) ); - - auto testWrapper = []( QgsProcessingGui::WidgetType type ) + auto testWrapper = [ = ]( QgsProcessingGui::WidgetType type ) { - QgsProcessingParameterProviderConnection param( QStringLiteral( "conn" ), QStringLiteral( "connection" ), QStringLiteral( "ogr" ), false ); + // non optional + QgsProcessingParameterFolderDestination param( QStringLiteral( "folder" ), QStringLiteral( "folder" ) ); - QgsProcessingProviderConnectionWidgetWrapper wrapper( ¶m, type ); + QgsProcessingFolderDestinationWidgetWrapper wrapper( ¶m, type ); QgsProcessingContext context; QWidget *w = wrapper.createWrappedWidget( context ); - QSignalSpy spy( &wrapper, &QgsProcessingProviderConnectionWidgetWrapper::widgetValueHasChanged ); - wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy.count(), 1 ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "bb" ) ); - QCOMPARE( static_cast< QgsProviderConnectionComboBox * >( wrapper.wrappedWidget() )->currentConnection(), QStringLiteral( "bb" ) ); - wrapper.setWidgetValue( QStringLiteral( "bb" ), context ); - QCOMPARE( spy.count(), 1 ); - wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); - QCOMPARE( spy.count(), 2 ); + QSignalSpy spy( &wrapper, &QgsProcessingFolderDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper.setWidgetValue( QStringLiteral( "/bb" ), context ); switch ( type ) { case QgsProcessingGui::Standard: case QgsProcessingGui::Batch: - { - // batch or standard mode, only valid connections can be set! - // not valid - wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); - QCOMPARE( spy.count(), 3 ); - QVERIFY( !wrapper.widgetValue().isValid() ); - QCOMPARE( static_cast< QComboBox * >( wrapper.wrappedWidget() )->currentIndex(), -1 ); - break; - - } case QgsProcessingGui::Modeler: - // invalid connections permitted - wrapper.setWidgetValue( QStringLiteral( "cc" ), context ); - QCOMPARE( spy.count(), 3 ); - QCOMPARE( static_cast< QgsProviderConnectionComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "cc" ) ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "cc" ) ); - wrapper.setWidgetValue( QStringLiteral( "aa" ), context ); - QCOMPARE( spy.count(), 4 ); - QCOMPARE( static_cast< QgsProviderConnectionComboBox * >( wrapper.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); - QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "aa" ) ); + QCOMPARE( spy.count(), 1 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "/bb" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().toString(), QStringLiteral( "/bb" ) ); + wrapper.setWidgetValue( QStringLiteral( "/aa" ), context ); + QCOMPARE( spy.count(), 2 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "/aa" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->value().toString(), QStringLiteral( "/aa" ) ); break; } + // check signal + static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper.wrappedWidget() )->setValue( QStringLiteral( "/cc" ) ); + QCOMPARE( spy.count(), 3 ); + QCOMPARE( wrapper.widgetValue().toString(), QStringLiteral( "/cc" ) ); delete w; + // optional - QgsProcessingParameterProviderConnection param2( QStringLiteral( "conn" ), QStringLiteral( "connection" ), QStringLiteral( "ogr" ), QVariant(), true ); - QgsProcessingProviderConnectionWidgetWrapper wrapper3( ¶m2, type ); + QgsProcessingParameterFolderDestination param2( QStringLiteral( "folder" ), QStringLiteral( "folder" ), QVariant(), true ); + QgsProcessingFolderDestinationWidgetWrapper wrapper3( ¶m2, type ); w = wrapper3.createWrappedWidget( context ); - QSignalSpy spy3( &wrapper3, &QgsProcessingEnumWidgetWrapper::widgetValueHasChanged ); - wrapper3.setWidgetValue( QStringLiteral( "bb" ), context ); + QSignalSpy spy3( &wrapper3, &QgsProcessingFolderDestinationWidgetWrapper::widgetValueHasChanged ); + wrapper3.setWidgetValue( QStringLiteral( "/bb" ), context ); QCOMPARE( spy3.count(), 1 ); - QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "bb" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "bb" ) ); - wrapper3.setWidgetValue( QStringLiteral( "aa" ), context ); - QCOMPARE( spy3.count(), 2 ); - QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "aa" ) ); - QCOMPARE( static_cast< QComboBox * >( wrapper3.wrappedWidget() )->currentText(), QStringLiteral( "aa" ) ); + QCOMPARE( wrapper3.widgetValue().toString(), QStringLiteral( "/bb" ) ); + QCOMPARE( static_cast< QgsProcessingLayerOutputDestinationWidget * >( wrapper3.wrappedWidget() )->value().toString(), QStringLiteral( "/bb" ) ); wrapper3.setWidgetValue( QVariant(), context ); - QCOMPARE( spy3.count(), 3 ); + QCOMPARE( spy3.count(), 2 ); QVERIFY( !wrapper3.widgetValue().isValid() ); delete w; + QLabel *l = wrapper.createWrappedLabel(); if ( wrapper.type() != QgsProcessingGui::Batch ) { QVERIFY( l ); - QCOMPARE( l->text(), QStringLiteral( "connection" ) ); + QCOMPARE( l->text(), QStringLiteral( "folder" ) ); QCOMPARE( l->toolTip(), param.toolTip() ); delete l; } @@ -4703,44 +8242,10 @@ void TestProcessingGui::testProviderConnectionWrapper() testWrapper( QgsProcessingGui::Standard ); // batch wrapper - testWrapper( QgsProcessingGui::Batch ); + // testWrapper( QgsProcessingGui::Batch ); // modeler wrapper testWrapper( QgsProcessingGui::Modeler ); - - // config widget - QgsProcessingParameterWidgetContext widgetContext; - QgsProcessingContext context; - std::unique_ptr< QgsProcessingParameterDefinitionWidget > widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext ); - std::unique_ptr< QgsProcessingParameterDefinition > def( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); // should default to mandatory - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QVERIFY( !static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().isValid() ); - - // using a parameter definition as initial values - QgsProcessingParameterProviderConnection connParam( QStringLiteral( "n" ), QStringLiteral( "test desc" ), QStringLiteral( "spatialite" ), QStringLiteral( "aaa" ), false ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext, &connParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagOptional ) ); - QVERIFY( !( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ) ); - QCOMPARE( static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().toString(), QStringLiteral( "aaa" ) ); - QCOMPARE( static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->providerId(), QStringLiteral( "spatialite" ) ); - connParam.setFlags( QgsProcessingParameterDefinition::FlagAdvanced | QgsProcessingParameterDefinition::FlagOptional ); - connParam.setDefaultValue( QStringLiteral( "xxx" ) ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext, &connParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QCOMPARE( def->name(), QStringLiteral( "param_name" ) ); - QCOMPARE( def->description(), QStringLiteral( "test desc" ) ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagOptional ); - QVERIFY( def->flags() & QgsProcessingParameterDefinition::FlagAdvanced ); - QCOMPARE( static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().toString(), QStringLiteral( "xxx" ) ); - connParam.setDefaultValue( QVariant() ); - widget = qgis::make_unique< QgsProcessingParameterDefinitionWidget >( QStringLiteral( "providerconnection" ), context, widgetContext, &connParam ); - def.reset( widget->createParameter( QStringLiteral( "param_name" ) ) ); - QVERIFY( !static_cast< QgsProcessingParameterProviderConnection * >( def.get() )->defaultValue().isValid() ); } void TestProcessingGui::cleanupTempDir() diff --git a/tests/src/gui/testqgsdatetimeedit.cpp b/tests/src/gui/testqgsdatetimeedit.cpp index 0e11eac4b082..104b1489bf2f 100644 --- a/tests/src/gui/testqgsdatetimeedit.cpp +++ b/tests/src/gui/testqgsdatetimeedit.cpp @@ -57,7 +57,7 @@ void TestQgsDateTimeEdit::init() vl = qgis::make_unique( QStringLiteral( "Point?crs=epsg:4326" ), QStringLiteral( "myvl" ), - QLatin1Literal( "memory" ) ); + QLatin1String( "memory" ) ); // add fields QList fields; diff --git a/tests/src/gui/testqgsfeaturelistcombobox.cpp b/tests/src/gui/testqgsfeaturelistcombobox.cpp index bcc28e1b4d86..cfb327321f19 100644 --- a/tests/src/gui/testqgsfeaturelistcombobox.cpp +++ b/tests/src/gui/testqgsfeaturelistcombobox.cpp @@ -46,10 +46,11 @@ class TestQgsFeatureListComboBox : public QObject void testMultipleForeignKeys(); void testAllowNull(); void testValuesAndSelection(); + void testValuesAndSelection_data(); void nullRepresentation(); + void testNotExistingYetFeature(); private: - void waitForLoaded( QgsFeatureListComboBox *cb ); std::unique_ptr mLayer; @@ -122,6 +123,10 @@ void TestQgsFeatureListComboBox::testSetGetForeignKey() { std::unique_ptr cb( new QgsFeatureListComboBox() ); + QgsFeatureFilterModel *model = qobject_cast( cb->model() ); + QEventLoop loop; + connect( model, &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); + Q_NOWARN_DEPRECATED_PUSH QVERIFY( cb->identifierValue().isNull() ); @@ -131,7 +136,7 @@ void TestQgsFeatureListComboBox::testSetGetForeignKey() emit cb->lineEdit()->textChanged( "ro" ); QVERIFY( cb->identifierValue().isNull() ); - waitForLoaded( cb.get() ); + loop.exec(); QVERIFY( cb->identifierValue().isNull() ); @@ -158,14 +163,14 @@ void TestQgsFeatureListComboBox::testMultipleForeignKeys() cb->setIdentifierValuesToNull(); QCOMPARE( cb->identifierValues().count(), 3 ); - QCOMPARE( cb->identifierValues(), QVariantList() << QVariant() << QVariant() << QVariant() ); + QCOMPARE( cb->identifierValues(), QVariantList() << QVariant( QVariant::Int ) << QVariant( QVariant::Int ) << QVariant( QVariant::Int ) ); cb->setIdentifierValues( QVariantList() << "silver" << 888 << "fish" ); QCOMPARE( cb->identifierValues(), QVariantList() << "silver" << 888 << "fish" ); cb->setIdentifierValuesToNull(); QCOMPARE( cb->identifierValues().count(), 3 ); - QCOMPARE( cb->identifierValues(), QVariantList() << QVariant() << QVariant() << QVariant() ); + QCOMPARE( cb->identifierValues(), QVariantList() << QVariant( QVariant::Int ) << QVariant( QVariant::Int ) << QVariant( QVariant::Int ) ); cb->setIdentifierFields( QStringList() << "material" << "raccord" ); cb->setDisplayExpression( "\"material\" || ' ' || \"raccord\"" ); @@ -177,7 +182,7 @@ void TestQgsFeatureListComboBox::testMultipleForeignKeys() cb->setIdentifierValuesToNull(); QCOMPARE( cb->identifierValues().count(), 2 ); - QCOMPARE( cb->identifierValues(), QVariantList() << QVariant() << QVariant() ); + QCOMPARE( cb->identifierValues(), QVariantList() << QVariant( QVariant::Int ) << QVariant( QVariant::Int ) ); } void TestQgsFeatureListComboBox::testAllowNull() @@ -186,65 +191,95 @@ void TestQgsFeatureListComboBox::testAllowNull() // Note to self: implement this! } +void TestQgsFeatureListComboBox::testValuesAndSelection_data() +{ + QTest::addColumn( "allowNull" ); + + QTest::newRow( "allowNull=true" ) << true; + QTest::newRow( "allowNull=false" ) << false; +} + void TestQgsFeatureListComboBox::testValuesAndSelection() { + QFETCH( bool, allowNull ); + + QgsApplication::setNullRepresentation( QStringLiteral( "nope" ) ); std::unique_ptr cb( new QgsFeatureListComboBox() ); + QgsFeatureFilterModel *model = qobject_cast( cb->model() ); + QEventLoop loop; + connect( model, &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); + cb->setSourceLayer( mLayer.get() ); cb->setDisplayExpression( QStringLiteral( "\"raccord\"" ) ); - cb->setAllowNull( true ); + cb->setAllowNull( allowNull ); //check if everything is fine: - waitForLoaded( cb.get() ); - QCOMPARE( cb->currentIndex(), cb->nullIndex() ); - QCOMPARE( cb->currentText(), QStringLiteral( "nope" ) ); + loop.exec(); + QCOMPARE( cb->currentIndex(), allowNull ? cb->nullIndex() : 0 ); + QCOMPARE( cb->currentText(), allowNull ? QStringLiteral( "nope" ) : QStringLiteral( "brides" ) ); //check if text correct, selected and if the clear button disappeared: cb->mLineEdit->clearValue(); - waitForLoaded( cb.get() ); - QCOMPARE( cb->currentIndex(), cb->nullIndex() ); - QCOMPARE( cb->currentText(), QStringLiteral( "nope" ) ); - QCOMPARE( cb->lineEdit()->selectedText(), QStringLiteral( "nope" ) ); + QCOMPARE( cb->currentIndex(), allowNull ? cb->nullIndex() : 0 ); + QCOMPARE( cb->currentText(), allowNull ? QStringLiteral( "nope" ) : QString() ); + QCOMPARE( cb->lineEdit()->selectedText(), allowNull ? QStringLiteral( "nope" ) : QString() ); QVERIFY( ! cb->mLineEdit->mClearAction ); //check if text is selected after receiving focus cb->setFocus(); - waitForLoaded( cb.get() ); - QCOMPARE( cb->currentIndex(), cb->nullIndex() ); - QCOMPARE( cb->currentText(), QStringLiteral( "nope" ) ); - QCOMPARE( cb->lineEdit()->selectedText(), QStringLiteral( "nope" ) ); + QCOMPARE( cb->currentIndex(), allowNull ? cb->nullIndex() : 0 ); + QCOMPARE( cb->currentText(), allowNull ? QStringLiteral( "nope" ) : QString() ); + QCOMPARE( cb->lineEdit()->selectedText(), allowNull ? QStringLiteral( "nope" ) : QString() ); QVERIFY( ! cb->mLineEdit->mClearAction ); //check with another entry, clear button needs to be there then: QTest::keyClicks( cb.get(), QStringLiteral( "sleeve" ) ); - //QTest::keyClick(cb.get(), Qt::Key_Enter ); - waitForLoaded( cb.get() ); + loop.exec(); QCOMPARE( cb->currentText(), QStringLiteral( "sleeve" ) ); QVERIFY( cb->mLineEdit->mClearAction ); - //QVERIFY( cb->currentIndex() != cb->nullIndex()); - //QCOMPARE( cb->model()->data( cb->currentModelIndex() ).toString(), QStringLiteral( "sleeve" ) ); } void TestQgsFeatureListComboBox::nullRepresentation() { - QgsApplication::setNullRepresentation( QStringLiteral( "nope" ) ); std::unique_ptr cb( new QgsFeatureListComboBox() ); + + QgsFeatureFilterModel *model = qobject_cast( cb->model() ); + QEventLoop loop; + connect( model, &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); + cb->setAllowNull( true ); + cb->setSourceLayer( mLayer.get() ); + loop.exec(); QCOMPARE( cb->lineEdit()->text(), QStringLiteral( "nope" ) ); QCOMPARE( cb->nullIndex(), 0 ); - } -void TestQgsFeatureListComboBox::waitForLoaded( QgsFeatureListComboBox *cb ) + +void TestQgsFeatureListComboBox::testNotExistingYetFeature() { + // test behavior when feature list combo box identifier values references a + // not existing yet feature (created but not saved for instance) + + std::unique_ptr cb( new QgsFeatureListComboBox() ); QgsFeatureFilterModel *model = qobject_cast( cb->model() ); + QEventLoop loop; + connect( model, &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); + + QgsApplication::setNullRepresentation( QStringLiteral( "nope" ) ); + + QVERIFY( cb->identifierValues().isEmpty() ); + + cb->setSourceLayer( mLayer.get() ); + cb->setAllowNull( true ); + + cb->setIdentifierValues( QVariantList() << 42 ); - // Wait - while ( model->isLoading() ) - {} + loop.exec(); + QCOMPARE( cb->currentText(), QStringLiteral( "(42)" ) ); } QGSTEST_MAIN( TestQgsFeatureListComboBox ) diff --git a/tests/src/gui/testqgsrangewidgetwrapper.cpp b/tests/src/gui/testqgsrangewidgetwrapper.cpp index a1c71ed5d0a2..c8e0c2144d1d 100644 --- a/tests/src/gui/testqgsrangewidgetwrapper.cpp +++ b/tests/src/gui/testqgsrangewidgetwrapper.cpp @@ -84,7 +84,7 @@ void TestQgsRangeWidgetWrapper::init() { vl = qgis::make_unique( QStringLiteral( "Point?crs=epsg:4326" ), QStringLiteral( "myvl" ), - QLatin1Literal( "memory" ) ); + QLatin1String( "memory" ) ); // add fields QList fields; diff --git a/tests/src/gui/testqgsrelationreferencewidget.cpp b/tests/src/gui/testqgsrelationreferencewidget.cpp index 8867dd024d7e..72f13680021f 100644 --- a/tests/src/gui/testqgsrelationreferencewidget.cpp +++ b/tests/src/gui/testqgsrelationreferencewidget.cpp @@ -33,6 +33,15 @@ #include "qgsadvanceddigitizingdockwidget.h" #include "qgsmaptooldigitizefeature.h" +QStringList getComboBoxItems( const QComboBox *cb ) +{ + QStringList items; + for ( int i = 0; i < cb->count(); i++ ) + items << cb->itemText( i ); + + return items; +} + class TestQgsRelationReferenceWidget : public QObject { Q_OBJECT @@ -47,6 +56,8 @@ class TestQgsRelationReferenceWidget : public QObject void testChainFilter(); void testChainFilter_data(); + void testChainFilterFirstInit_data(); + void testChainFilterFirstInit(); void testChainFilterRefreshed(); void testChainFilterDeleteForeignKey(); void testInvalidRelation(); @@ -165,6 +176,10 @@ void TestQgsRelationReferenceWidget::testChainFilter() QWidget parentWidget; QgsRelationReferenceWidget w( &parentWidget ); + + QEventLoop loop; + connect( qobject_cast( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); + w.setChainFilters( true ); w.setFilterFields( filterFields ); w.setRelation( *mRelation, allowNull ); @@ -183,9 +198,18 @@ void TestQgsRelationReferenceWidget::testChainFilter() QCOMPARE( cb->count(), 3 ); } + loop.exec(); + QStringList items = getComboBoxItems( w.mComboBox ); + QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) ); + // set first filter cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "iron" ) ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) ); + cbs[1]->setCurrentIndex( cbs[1]->findText( QStringLiteral( "120" ) ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) ); Q_FOREACH ( const QComboBox *cb, cbs ) { @@ -195,9 +219,7 @@ void TestQgsRelationReferenceWidget::testChainFilter() QCOMPARE( cb->count(), 2 ); else if ( cb->itemText( 0 ) == QLatin1String( "raccord" ) ) { - QStringList items; - for ( int i = 0; i < cb->count(); i++ ) - items << cb->itemText( i ); + QStringList items = getComboBoxItems( cb ); QCOMPARE( cb->count(), 3 ); QCOMPARE( items.contains( "collar" ), false ); @@ -207,38 +229,67 @@ void TestQgsRelationReferenceWidget::testChainFilter() } } + // set the filter for "raccord" and then reset filter for "diameter". As // chain filter is activated, the filter on "raccord" field should be reset - QEventLoop loop; - connect( qobject_cast( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); cbs[0]->setCurrentIndex( 0 ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" ); + + if ( allowNull ) + { + w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "10" ) ) ); + QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" ); + } cbs[0]->setCurrentIndex( cbs[0]->findText( "iron" ) ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); + // prefer 12 over NULL cbs[0]->setCurrentIndex( cbs[0]->findText( "steel" ) ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), QString( "12" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" ); + + if ( allowNull ) + { + w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "12" ) ) ); + QCOMPARE( w.mComboBox->currentText(), QString( "12" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" ); + } + // reset IRON, prefer 10 over NULL cbs[0]->setCurrentIndex( cbs[0]->findText( "iron" ) ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); + + if ( allowNull ) + { + w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "10" ) ) ); + QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); + } cbs[1]->setCurrentIndex( cbs[1]->findText( "120" ) ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); cbs[2]->setCurrentIndex( cbs[2]->findText( QStringLiteral( "brides" ) ) ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" ); cbs[1]->setCurrentIndex( cbs[1]->findText( QStringLiteral( "diameter" ) ) ); loop.exec(); QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); // combobox should propose NULL (if allowNull is true), 10 and 11 because the filter is now: // "material" == 'iron' @@ -248,9 +299,106 @@ void TestQgsRelationReferenceWidget::testChainFilter() cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "material" ) ) ); loop.exec(); QCOMPARE( w.mComboBox->count(), allowNull ? 4 : 3 ); - QCOMPARE( w.mComboBox->currentText(), allowNull ? QString( "NULL" ) : QString( "10" ) ); + QCOMPARE( w.mComboBox->currentText(), QString( "10" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" ); + + // change item to check that currently selected item remains + w.mComboBox->setCurrentIndex( w.mComboBox->findText( QStringLiteral( "11" ) ) ); + cbs[0]->setCurrentIndex( cbs[0]->findText( "iron" ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), QString( "11" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); + + // reset all filter + cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "material" ) ) ); + loop.exec(); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" << "12" ); + + // set value with foreign key -> all the comboboxes matches feature values + w.setForeignKeys( QVariantList() << "11" ); + loop.exec(); + QCOMPARE( cbs[0]->currentText(), QString( "iron" ) ); + QCOMPARE( cbs[1]->currentText(), QString( "120" ) ); + QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) ); + QCOMPARE( w.mComboBox->currentText(), QString( "11" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "11" ); + + // remove filter on raccord + cbs[2]->setCurrentIndex( cbs[2]->findText( "raccord" ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), QString( "11" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); + + // change material, prever 12 over NULL + cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "steel" ) ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), QString( "12" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" ); } +void TestQgsRelationReferenceWidget::testChainFilterFirstInit_data() +{ + QTest::addColumn( "allowNull" ); + + QTest::newRow( "allowNull=true" ) << true; + QTest::newRow( "allowNull=false" ) << false; +} + +void TestQgsRelationReferenceWidget::testChainFilterFirstInit() +{ + QFETCH( bool, allowNull ); + + // init a relation reference widget + QStringList filterFields = { "material", "diameter", "raccord" }; + + QWidget parentWidget; + QgsRelationReferenceWidget w( &parentWidget ); + w.setChainFilters( true ); + w.setFilterFields( filterFields ); + w.setRelation( *mRelation, allowNull ); + w.init(); + + // check default status for comboboxes + QList cbs = w.mFilterComboBoxes; + QCOMPARE( cbs.count(), 3 ); + Q_FOREACH ( const QComboBox *cb, cbs ) + { + if ( cb->currentText() == QLatin1String( "raccord" ) ) + QCOMPARE( cb->count(), 5 ); + else if ( cb->currentText() == QLatin1String( "material" ) ) + QCOMPARE( cb->count(), 4 ); + else if ( cb->currentText() == QLatin1String( "diameter" ) ) + QCOMPARE( cb->count(), 3 ); + } + + // set the filter for "raccord" and then reset filter for "diameter". As + // chain filter is activated, the filter on "raccord" field should be reset + QEventLoop loop; + connect( qobject_cast( w.mComboBox->model() ), &QgsFeatureFilterModel::filterJobCompleted, &loop, &QEventLoop::quit ); + + // set value with foreign key -> all the comboboxes matches feature values + w.setForeignKeys( QVariantList() << "11" ); + loop.exec(); + QCOMPARE( cbs[0]->currentText(), QString( "iron" ) ); + QCOMPARE( cbs[1]->currentText(), QString( "120" ) ); + QCOMPARE( cbs[2]->currentText(), QString( "sleeve" ) ); + QCOMPARE( w.mComboBox->currentText(), QString( "11" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "11" ); + + // remove filter on raccord + cbs[2]->setCurrentIndex( cbs[2]->findText( "raccord" ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), QString( "11" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "10" << "11" ); + + // change material prever 12 over NULL + cbs[0]->setCurrentIndex( cbs[0]->findText( QStringLiteral( "steel" ) ) ); + loop.exec(); + QCOMPARE( w.mComboBox->currentText(), QString( "12" ) ); + QCOMPARE( getComboBoxItems( w.mComboBox ), ( allowNull ? QStringList() << "NULL" : QStringList() ) << "12" ); +} + + void TestQgsRelationReferenceWidget::testChainFilterRefreshed() { // init a relation reference widget @@ -380,8 +528,9 @@ void TestQgsRelationReferenceWidget::testSetGetForeignKey() QCOMPARE( w.mComboBox->currentText(), QStringLiteral( "(12)" ) ); QCOMPARE( spy.count(), 2 ); - w.setForeignKeys( QVariantList() << QVariant( QVariant::Int ) ); - Q_ASSERT( w.foreignKeys().at( 0 ).isNull() ); + w.setForeignKeys( QVariantList() << QVariant() ); + QVERIFY( w.foreignKeys().at( 0 ).isNull() ); + QVERIFY( w.foreignKeys().at( 0 ).isValid() ); QCOMPARE( spy.count(), 3 ); } diff --git a/tests/src/gui/testqgstemporalvcrdockwidget.cpp b/tests/src/gui/testqgstemporalvcrdockwidget.cpp deleted file mode 100644 index a30b7efbada5..000000000000 --- a/tests/src/gui/testqgstemporalvcrdockwidget.cpp +++ /dev/null @@ -1,95 +0,0 @@ -/*************************************************************************** - testqgstemporalvcrdockwidget.cpp - --------------- - begin : February 2020 - copyright : (C) 2020 by Samweli Mwakisambwe - email : samweli at kartoza 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 "qgstest.h" -#include -#include "qgsapplication.h" -#include "qgsproject.h" -#include "qgsrasterlayer.h" -#include "qgsprojecttimesettings.h" -#include "qgsgui.h" - -#include "qgstemporalvcrdockwidget.h" - -/** - * \ingroup UnitTests - * This is a unit test for the QgsRasterLayerTemporalProperties class. - */ -class TestQgsTemporalVcrDockWidget : public QObject -{ - Q_OBJECT - - public: - TestQgsTemporalVcrDockWidget() = default; - - private slots: - void initTestCase();// will be called before the first testfunction is executed. - void cleanupTestCase();// will be called after the last testfunction was executed. - void init(); // will be called before each testfunction is executed. - void cleanup(); // will be called after every testfunction. - - void testSettingDateTimes(); - - private: - // QgisApp *mQgisApp = nullptr; - QgsTemporalVcrDockWidget *temporalVcrWidget = nullptr; - QgsRasterLayer *layer = nullptr; -}; - -void TestQgsTemporalVcrDockWidget::initTestCase() -{ - // - // Runs once before any tests are run - // - // init QGIS's paths - true means that all path will be inited from prefix - QgsApplication::init(); - QgsApplication::initQgis(); - // mQgisApp = new QgisApp(); - QgsApplication::showSettings(); - -} - -void TestQgsTemporalVcrDockWidget::init() -{ -// temporalVcrWidget = new QgsTemporalVcrDockWidget(); -// mQgisApp->addDockWidget( Qt::BottomDockWidgetArea, temporalVcrWidget ); - -// layer = new QgsRasterLayer( "", "test", "wms" ); -// QgsProject::instance()->addMapLayer( layer ); -} - -void TestQgsTemporalVcrDockWidget::cleanup() -{ -} - -void TestQgsTemporalVcrDockWidget::cleanupTestCase() -{ - QgsApplication::exitQgis(); -} - -void TestQgsTemporalVcrDockWidget::testSettingDateTimes() -{ - QgsDateTimeRange range = QgsDateTimeRange( QDateTime( QDate( 2020, 1, 1 ) ), - QDateTime( QDate( 2020, 1, 31 ) ) ); - if ( QgsProject::instance()->timeSettings() ) - QgsProject::instance()->timeSettings()->setTemporalRange( range ); - - // QCOMPARE( temporalVcrWidget->dateTimes().size(), 31 ); -} - -QGSTEST_MAIN( TestQgsTemporalVcrDockWidget ) -#include "testqgstemporalvcrdockwidget.moc" diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 1d9c37bbedb5..4d6abb77fb00 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -40,6 +40,7 @@ ADD_PYTHON_TEST(PyQgsCoordinateFormatter test_qgscoordinateformatter.py) ADD_PYTHON_TEST(PyQgsCoordinateOperationWidget test_qgscoordinateoperationwidget.py) ADD_PYTHON_TEST(PyQgsConditionalFormatWidgets test_qgsconditionalformatwidgets.py) ADD_PYTHON_TEST(PyQgsConditionalStyle test_qgsconditionalstyle.py) +ADD_PYTHON_TEST(PyQgsConnectionRegistry test_qgsconnectionregistry.py) ADD_PYTHON_TEST(PyQgsCoordinateTransformContext test_qgscoordinatetransformcontext.py) ADD_PYTHON_TEST(PyQgsDefaultValue test_qgsdefaultvalue.py) ADD_PYTHON_TEST(PyQgsXmlUtils test_qgsxmlutils.py) @@ -61,6 +62,7 @@ ADD_PYTHON_TEST(PyQgsExpression test_qgsexpression.py) ADD_PYTHON_TEST(PyQgsExpressionBuilderWidget test_qgsexpressionbuilderwidget.py) ADD_PYTHON_TEST(PyQgsExpressionLineEdit test_qgsexpressionlineedit.py) ADD_PYTHON_TEST(PyQgsExtentGroupBox test_qgsextentgroupbox.py) +ADD_PYTHON_TEST(PyQgsExtentWidget test_qgsextentwidget.py) ADD_PYTHON_TEST(PyQgsFeature test_qgsfeature.py) ADD_PYTHON_TEST(PyQgsFeatureSink test_qgsfeaturesink.py) ADD_PYTHON_TEST(PyQgsFeatureSource test_qgsfeaturesource.py) @@ -76,6 +78,7 @@ ADD_PYTHON_TEST(PyQgsFeatureIterator test_qgsfeatureiterator.py) ADD_PYTHON_TEST(PyQgsFeedback test_qgsfeedback.py) ADD_PYTHON_TEST(PyQgsFields test_qgsfields.py) ADD_PYTHON_TEST(PyQgsFieldModel test_qgsfieldmodel.py) +ADD_PYTHON_TEST(PyQgsFieldMappingWidget test_qgsfieldmappingwidget.py) ADD_PYTHON_TEST(PyQgsFileUtils test_qgsfileutils.py) ADD_PYTHON_TEST(PyQgsFilterLineEdit test_qgsfilterlineedit.py) ADD_PYTHON_TEST(PyQgsFloatingWidget test_qgsfloatingwidget.py) @@ -122,6 +125,8 @@ ADD_PYTHON_TEST(PyQgsLayoutLegend test_qgslayoutlegend.py) ADD_PYTHON_TEST(PyQgsLayoutMap test_qgslayoutmap.py) ADD_PYTHON_TEST(PyQgsLayoutMapGrid test_qgslayoutmapgrid.py) ADD_PYTHON_TEST(PyQgsLayoutMapOverview test_qgslayoutmapoverview.py) +ADD_PYTHON_TEST(PyQgsLayoutMarker test_qgslayoutmarker.py) +ADD_PYTHON_TEST(PyQgsLayoutNorthArrowHandler test_qgslayoutnortharrowhandler.py) ADD_PYTHON_TEST(PyQgsLayoutPage test_qgslayoutpage.py) ADD_PYTHON_TEST(PyQgsLayoutPicture test_qgslayoutpicture.py) ADD_PYTHON_TEST(PyQgsLayoutPolygon test_qgslayoutpolygon.py) @@ -130,6 +135,7 @@ ADD_PYTHON_TEST(PyQgsLayoutScaleBar test_qgslayoutscalebar.py) ADD_PYTHON_TEST(PyQgsLayoutShape test_qgslayoutshape.py) ADD_PYTHON_TEST(PyQgsLayoutSnapper test_qgslayoutsnapper.py) ADD_PYTHON_TEST(PyQgsLayoutUnitsComboBox test_qgslayoutunitscombobox.py) +ADD_PYTHON_TEST(PyQgsLegendPatchShape test_qgslegendpatchshape.py) ADD_PYTHON_TEST(PyQgsLineSegment test_qgslinesegment.py) ADD_PYTHON_TEST(PyQgsLineSymbolLayers test_qgslinesymbollayers.py) ADD_PYTHON_TEST(PyQgsLocalDefaultSettings test_qgslocaldefaultsettings.py) @@ -188,6 +194,8 @@ ADD_PYTHON_TEST(PyQgsPropertyOverrideButton test_qgspropertyoverridebutton.py) ADD_PYTHON_TEST(PyQgsProviderConnectionComboBox test_qgsproviderconnectioncombobox.py) ADD_PYTHON_TEST(PyQgsProviderConnectionModel test_qgsproviderconnectionmodel.py) ADD_PYTHON_TEST(PyQgsProviderConnectionGpkg test_qgsproviderconnection_ogr_gpkg.py) +ADD_PYTHON_TEST(PyQgsProviderConnectionSpatialite test_qgsproviderconnection_spatialite.py) +ADD_PYTHON_TEST(PyQgsProviderRegistry test_qgsproviderregistry.py) ADD_PYTHON_TEST(TestQgsRandomMarkerSymbolLayer test_qgsrandommarkersymbollayer.py) ADD_PYTHON_TEST(PyQgsRange test_qgsrange.py) ADD_PYTHON_TEST(PyQgsRangeWidgets test_qgsrangewidgets.py) @@ -198,6 +206,7 @@ ADD_PYTHON_TEST(PyQgsRasterLayer test_qgsrasterlayer.py) ADD_PYTHON_TEST(PyQgsRasterColorRampShader test_qgsrastercolorrampshader.py) ADD_PYTHON_TEST(PyQgsRasterRange test_qgsrasterrange.py) ADD_PYTHON_TEST(PyQgsRasterResampler test_qgsrasterresampler.py) +ADD_PYTHON_TEST(PyQgsRasterTransparencyWidget test_qgsrastertransparencywidget.py) ADD_PYTHON_TEST(PyQgsRatioLockButton test_qgsratiolockbutton.py) ADD_PYTHON_TEST(PyQgsRectangle test_qgsrectangle.py) ADD_PYTHON_TEST(PyQgsReferencedGeometry test_qgsreferencedgeometry.py) @@ -208,6 +217,7 @@ ADD_PYTHON_TEST(PyQgsRenderContext test_qgsrendercontext.py) ADD_PYTHON_TEST(PyQgsRenderer test_qgsrenderer.py) ADD_PYTHON_TEST(PyQgsReport test_qgsreport.py) ADD_PYTHON_TEST(PyQgsRulebasedRenderer test_qgsrulebasedrenderer.py) +ADD_PYTHON_TEST(PyQgsScaleBarRendererRegistry test_qgsscalebarrendererregistry.py) ADD_PYTHON_TEST(PyQgsScaleWidget test_qgsscalewidget.py) ADD_PYTHON_TEST(PyQgsSingleSymbolRenderer test_qgssinglesymbolrenderer.py) ADD_PYTHON_TEST(PyQgsShapefileProvider test_provider_shapefile.py) @@ -240,6 +250,7 @@ ADD_PYTHON_TEST(PyQgsSvgSourceLineEdit test_qgssvgsourcelineedit.py) ADD_PYTHON_TEST(PyQgsSymbol test_qgssymbol.py) ADD_PYTHON_TEST(PyQgsSymbolLayerUtils test_qgssymbollayerutils.py) ADD_PYTHON_TEST(PyQgsTaskManager test_qgstaskmanager.py) +ADD_PYTHON_TEST(PyQgsTemporalUtils test_qgstemporalutils.py) ADD_PYTHON_TEST(PyQgsTextFormatWidget test_qgstextformatwidget.py) ADD_PYTHON_TEST(PyQgsTreeWidgetItem test_qgstreewidgetitem.py) ADD_PYTHON_TEST(PyQgsUnitTypes test_qgsunittypes.py) @@ -313,6 +324,7 @@ ENDIF (ENABLE_PGTEST) IF (ENABLE_MSSQLTEST) ADD_PYTHON_TEST(PyQgsMssqlProvider test_provider_mssql.py) + ADD_PYTHON_TEST(PyQgsProviderConnectionMssql test_qgsproviderconnection_mssql.py) ENDIF (ENABLE_MSSQLTEST) IF (ENABLE_ORACLETEST) diff --git a/tests/src/python/provider_python.py b/tests/src/python/provider_python.py index 804fe32167ce..e92a5cb393ab 100644 --- a/tests/src/python/provider_python.py +++ b/tests/src/python/provider_python.py @@ -78,6 +78,10 @@ def __init__(self, source, request): if self._filter_rect is not None and self._source._provider._spatialindex is not None: self._feature_id_list = self._source._provider._spatialindex.intersects(self._filter_rect) + if self._request.filterType() == QgsFeatureRequest.FilterFid or self._request.filterType() == QgsFeatureRequest.FilterFids: + fids = [self._request.filterFid()] if self._request.filterType() == QgsFeatureRequest.FilterFid else self._request.filterFids() + self._feature_id_list = list(set(self._feature_id_list).intersection(set(fids))) if self._feature_id_list else fids + def fetchFeature(self, f): """fetch next feature, return true on success""" #virtual bool nextFeature( QgsFeature &f ); diff --git a/tests/src/python/providertestbase.py b/tests/src/python/providertestbase.py index 79fc655b4d93..a3e66781463f 100644 --- a/tests/src/python/providertestbase.py +++ b/tests/src/python/providertestbase.py @@ -210,6 +210,29 @@ def testSubsetString(self): result, subset) self.assertTrue(all_valid) + # Subset string AND filter fid + ids = {f['pk']: f.id() for f in self.source.getFeatures()} + self.source.setSubsetString(subset) + request = QgsFeatureRequest().setFilterFid(4) + result = set([f.id() for f in self.source.getFeatures(request)]) + all_valid = (all(f.isValid() for f in self.source.getFeatures(request))) + self.source.setSubsetString(None) + expected = set([4]) + assert set(expected) == result, 'Expected {} and got {} when testing subset string {}'.format(set(expected), + result, subset) + self.assertTrue(all_valid) + + # Subset string AND filter fids + self.source.setSubsetString(subset) + request = QgsFeatureRequest().setFilterFids([ids[2], ids[4]]) + result = set([f.id() for f in self.source.getFeatures(request)]) + all_valid = (all(f.isValid() for f in self.source.getFeatures(request))) + self.source.setSubsetString(None) + expected = set([ids[2], ids[4]]) + assert set(expected) == result, 'Expected {} and got {} when testing subset string {}'.format(set(expected), + result, subset) + self.assertTrue(all_valid) + def getSubsetString(self): """Individual providers may need to override this depending on their subset string formats""" return '"cnt" > 100 and "cnt" < 410' diff --git a/tests/src/python/test_layer_dependencies.py b/tests/src/python/test_layer_dependencies.py index 9b9479300ac4..2e4495fededb 100644 --- a/tests/src/python/test_layer_dependencies.py +++ b/tests/src/python/test_layer_dependencies.py @@ -113,7 +113,7 @@ def test_resetSnappingIndex(self): cfg.setMode(QgsSnappingConfig.AdvancedConfiguration) cfg.setIndividualLayerSettings(self.pointsLayer, QgsSnappingConfig.IndividualLayerSettings(True, - QgsSnappingConfig.Vertex, 20, QgsTolerance.Pixels)) + QgsSnappingConfig.VertexFlag, 20, QgsTolerance.Pixels, 0.0, 0.0)) u.setConfig(cfg) m = u.snapToMap(QPoint(95, 100)) @@ -156,7 +156,7 @@ def test_resetSnappingIndex(self): # test chained layer dependencies A -> B -> C cfg.setIndividualLayerSettings(self.pointsLayer2, QgsSnappingConfig.IndividualLayerSettings(True, - QgsSnappingConfig.Vertex, 20, QgsTolerance.Pixels)) + QgsSnappingConfig.VertexFlag, 20, QgsTolerance.Pixels, 0.0, 0.0)) u.setConfig(cfg) self.pointsLayer.setDependencies([QgsMapLayerDependency(self.linesLayer.id())]) self.pointsLayer2.setDependencies([QgsMapLayerDependency(self.pointsLayer.id())]) @@ -304,10 +304,10 @@ def test_signalConnection(self): cfg.setMode(QgsSnappingConfig.AdvancedConfiguration) cfg.setIndividualLayerSettings(self.pointsLayer, QgsSnappingConfig.IndividualLayerSettings(True, - QgsSnappingConfig.Vertex, 20, QgsTolerance.Pixels)) + QgsSnappingConfig.VertexFlag, 20, QgsTolerance.Pixels, 0.0, 0.0)) cfg.setIndividualLayerSettings(self.pointsLayer2, QgsSnappingConfig.IndividualLayerSettings(True, - QgsSnappingConfig.Vertex, 20, QgsTolerance.Pixels)) + QgsSnappingConfig.VertexFlag, 20, QgsTolerance.Pixels, 0.0, 0.0)) u.setConfig(cfg) # add another line f = QgsFeature(self.linesLayer.fields()) diff --git a/tests/src/python/test_provider_postgresraster.py b/tests/src/python/test_provider_postgresraster.py index cb8beb9fc415..f8fbd3fd058e 100644 --- a/tests/src/python/test_provider_postgresraster.py +++ b/tests/src/python/test_provider_postgresraster.py @@ -58,7 +58,7 @@ def _load_test_table(cls, schemaname, tablename, basename=None): with open(os.path.join(TEST_DATA_DIR, 'provider', 'postgresraster', basename + '.sql'), 'r') as f: sql = f.read() conn.executeSql(sql) - assert (tablename in [n.tableName() for n in conn.tables(schemaname)]) + assert (tablename in [n.tableName() for n in conn.tables(schemaname)]), tablename + ' not found!' @classmethod def setUpClass(cls): @@ -316,6 +316,55 @@ def testSetSubsetString(self): data.append(int(block.value(i, j))) self.assertEqual(data, [136, 142, 145, 153]) + def testTime(self): + """Test time series and time subset string when default value is set""" + + def _test_block(rl, expected_block, expected_single): + + self.assertTrue(rl.isValid()) + block = rl.dataProvider().block(1, rl.extent(), 2, 2) + data = [] + for i in range(2): + for j in range(2): + data.append(int(block.value(i, j))) + self.assertEqual(data, expected_block) + + block = rl.dataProvider().block(1, rl.extent(), 1, 1) + self.assertEqual(int(block.value(0, 0)), expected_single) + + # First check that setting different temporal default values we get different results + rl = QgsRasterLayer(self.dbconn + " sslmode=disable table={table} schema={schema} temporalDefaultTime='2020-04-01T00:00:00' temporalFieldIndex='1'".format(table='raster_3035_untiled_multiple_rows', schema='public'), 'pg_layer', 'postgresraster') + self.assertEqual(rl.subsetString(), "") + + _test_block(rl, [136, 142, 145, 153], 153) + + rl = QgsRasterLayer(self.dbconn + " sslmode=disable table={table} schema={schema} temporalDefaultTime='2020-04-05T00:00:00' temporalFieldIndex='1'".format(table='raster_3035_untiled_multiple_rows', schema='public'), 'pg_layer', 'postgresraster') + self.assertEqual(rl.subsetString(), "") + + _test_block(rl, [136, 142, 161, 169], 169) + + # Check that manually setting a subsetString we get the same results + rl = QgsRasterLayer(self.dbconn + " sslmode=disable table={table} schema={schema} sql=\"data\" = '2020-04-01'".format(table='raster_3035_untiled_multiple_rows', schema='public'), 'pg_layer', 'postgresraster') + self.assertEqual(rl.subsetString(), '"data" = \'2020-04-01\'') + + _test_block(rl, [136, 142, 145, 153], 153) + + rl = QgsRasterLayer(self.dbconn + " sslmode=disable table={table} schema={schema} sql=\"data\" = '2020-04-05'".format(table='raster_3035_untiled_multiple_rows', schema='public'), 'pg_layer', 'postgresraster') + self.assertEqual(rl.subsetString(), '"data" = \'2020-04-05\'') + + _test_block(rl, [136, 142, 161, 169], 169) + + # Now check if the varchar temporal field works the same + rl = QgsRasterLayer(self.dbconn + " sslmode=disable table={table} schema={schema} temporalDefaultTime='2020-04-01T00:00:00' temporalFieldIndex='2'".format(table='raster_3035_untiled_multiple_rows', schema='public'), 'pg_layer', 'postgresraster') + self.assertEqual(rl.subsetString(), '') + + _test_block(rl, [136, 142, 145, 153], 153) + + rl = QgsRasterLayer(self.dbconn + " sslmode=disable table={table} schema={schema} temporalDefaultTime='2020-04-05T00:00:00' temporalFieldIndex='2'".format(table='raster_3035_untiled_multiple_rows', schema='public'), 'pg_layer', 'postgresraster') + self.assertEqual(rl.subsetString(), '') + + _test_block(rl, [136, 142, 161, 169], 169) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_provider_spatialite.py b/tests/src/python/test_provider_spatialite.py index 85dd24a378b6..d1f9e56bb596 100644 --- a/tests/src/python/test_provider_spatialite.py +++ b/tests/src/python/test_provider_spatialite.py @@ -17,9 +17,11 @@ import sys import shutil import tempfile +from osgeo import ogr from datetime import datetime from qgis.core import (QgsProviderRegistry, + QgsDataSourceUri, QgsVectorLayer, QgsVectorDataProvider, QgsPointXY, @@ -770,7 +772,7 @@ def testSubsetStringRegexp(self): testPath = "dbname=%s table='test_filter' (geometry) key='id'" % self.dbname vl = QgsVectorLayer(testPath, 'test', 'spatialite') self.assertTrue(vl.isValid()) - vl.setSubsetString('"name" REGEXP \'[txe]\'') + vl.setSubsetString('"name" REGEXP \'[txe]{3}\'') self.assertEqual(vl.featureCount(), 4) del(vl) @@ -837,7 +839,7 @@ def testEncodeUri(self): parts = {'path': filename, 'layerName': 'test'} uri = registry.encodeUri('spatialite', parts) - self.assertEqual(uri, 'dbname=\'{}\' table="test" (geometry) sql='.format(filename)) + self.assertEqual(uri, 'dbname=\'{}\' table="test"'.format(filename)) def testPKNotInt(self): """ Check when primary key is not an integer """ @@ -1295,6 +1297,85 @@ def testAddFeatureNoFields(self): self.assertEqual(vl.featureCount(), 1) self.assertEqual(vl.getFeature(1).geometry().asWkt().upper(), 'POINT (9 45)') + def testTransaction(self): + """Test spatialite transactions""" + + tmpfile = tempfile.mktemp('.db') + ds = ogr.GetDriverByName('SQLite').CreateDataSource(tmpfile, options=['SPATIALITE=YES']) + lyr = ds.CreateLayer('lyr1', geom_type=ogr.wkbPoint) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(0 1)')) + lyr.CreateFeature(f) + lyr = ds.CreateLayer('lyr2', geom_type=ogr.wkbPoint) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(2 3)')) + lyr.CreateFeature(f) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(4 5)')) + lyr.CreateFeature(f) + ds = None + + uri1 = QgsDataSourceUri() + uri1.setDatabase(tmpfile) + uri1.setTable('lyr1') + uri2 = QgsDataSourceUri() + uri2.setDatabase(tmpfile) + uri2.setTable('lyr2') + + vl1 = QgsVectorLayer(uri1.uri(), 'test', 'spatialite') + self.assertTrue(vl1.isValid()) + vl2 = QgsVectorLayer(uri2.uri(), 'test', 'spatialite') + self.assertTrue(vl2.isValid()) + + # prepare a project with transactions enabled + p = QgsProject() + p.setAutoTransaction(True) + p.addMapLayers([vl1, vl2]) + + self.assertTrue(vl1.startEditing()) + self.assertIsNotNone(vl1.dataProvider().transaction()) + + self.assertTrue(vl1.deleteFeature(1)) + + # An iterator opened on the layer should see the feature deleted + self.assertEqual(len([f for f in vl1.getFeatures(QgsFeatureRequest())]), 0) + + # But not if opened from another connection + vl1_external = QgsVectorLayer(uri1.uri(), 'test', 'spatialite') + self.assertTrue(vl1_external.isValid()) + self.assertEqual(len([f for f in vl1_external.getFeatures(QgsFeatureRequest())]), 1) + del vl1_external + + self.assertTrue(vl1.commitChanges()) + + # Should still get zero features on vl1 + self.assertEqual(len([f for f in vl1.getFeatures(QgsFeatureRequest())]), 0) + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 2) + + # Test undo/redo + self.assertTrue(vl2.startEditing()) + self.assertIsNotNone(vl2.dataProvider().transaction()) + self.assertTrue(vl2.editBuffer().deleteFeature(1)) + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + self.assertTrue(vl2.editBuffer().deleteFeature(2)) + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 0) + vl2.undoStack().undo() + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + vl2.undoStack().undo() + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 2) + vl2.undoStack().redo() + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + self.assertTrue(vl2.commitChanges()) + + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + del vl1 + del vl2 + + vl2_external = QgsVectorLayer(uri2.uri(), 'test', 'spatialite') + self.assertTrue(vl2_external.isValid()) + self.assertEqual(len([f for f in vl2_external.getFeatures(QgsFeatureRequest())]), 1) + del vl2_external + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_python_repr.py b/tests/src/python/test_python_repr.py index 65292696a35d..29b3eca85870 100644 --- a/tests/src/python/test_python_repr.py +++ b/tests/src/python/test_python_repr.py @@ -18,7 +18,7 @@ QgsCurvePolygon, QgsEllipse, QgsLineString, QgsMultiCurve, QgsRectangle, QgsExpression, QgsField, QgsError,\ QgsMimeDataUtils, QgsVector, QgsVector3D, QgsVectorLayer, QgsReferencedPointXY, QgsReferencedRectangle,\ QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject, QgsClassificationRange, QgsBookmark, \ - QgsLayoutMeasurement, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes, QgsConditionalStyle, QgsTableCell + QgsLayoutMeasurement, QgsLayoutPoint, QgsLayoutSize, QgsUnitTypes, QgsConditionalStyle, QgsTableCell, QgsProperty start_app() @@ -225,6 +225,22 @@ def testQgsTableCell(self): b.setContent(5) self.assertEqual(b.__repr__(), "") + def testQgsProperty(self): + p = QgsProperty.fromValue(5) + self.assertEqual(p.__repr__(), '') + p = QgsProperty.fromField('my_field') + self.assertEqual(p.__repr__(), '') + p = QgsProperty.fromExpression('5*5 || \'a\'') + self.assertEqual(p.__repr__(), '') + p = QgsProperty.fromValue(5, False) + self.assertEqual(p.__repr__(), '') + p = QgsProperty.fromField('my_field', False) + self.assertEqual(p.__repr__(), '') + p = QgsProperty.fromExpression('5*5 || \'a\'', False) + self.assertEqual(p.__repr__(), '') + p = QgsProperty() + self.assertEqual(p.__repr__(), '') + if __name__ == "__main__": unittest.main() diff --git a/tests/src/python/test_qgsaggregatecalculator.py b/tests/src/python/test_qgsaggregatecalculator.py index b3b2f028f801..a89797c86bee 100644 --- a/tests/src/python/test_qgsaggregatecalculator.py +++ b/tests/src/python/test_qgsaggregatecalculator.py @@ -212,8 +212,6 @@ def testString(self): QgsAggregateCalculator.StDev, QgsAggregateCalculator.StDevSample, QgsAggregateCalculator.Range, - QgsAggregateCalculator.Minority, - QgsAggregateCalculator.Majority, QgsAggregateCalculator.FirstQuartile, QgsAggregateCalculator.ThirdQuartile, QgsAggregateCalculator.InterQuartileRange @@ -232,6 +230,10 @@ def testString(self): self.assertEqual(val, ['', '', 'aaaa', 'aaaa', 'bbbbbbbb', 'cc', 'dddd', 'eeee', 'eeee']) val, ok = agg.calculate(QgsAggregateCalculator.StringConcatenate, 'fldstring') self.assertEqual(val, 'aaaaaaaabbbbbbbbccddddeeeeeeee') + val, ok = agg.calculate(QgsAggregateCalculator.Minority, 'fldstring') + self.assertEqual(val, 'bbbbbbbb') + val, ok = agg.calculate(QgsAggregateCalculator.Majority, 'fldstring') + self.assertEqual(val, '') def testDateTime(self): """ Test calculation of aggregates on date/datetime fields""" diff --git a/tests/src/python/test_qgsconnectionregistry.py b/tests/src/python/test_qgsconnectionregistry.py new file mode 100644 index 000000000000..027da4d1c289 --- /dev/null +++ b/tests/src/python/test_qgsconnectionregistry.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsConnectionRegistry. + +.. note:: 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__ = 'Nyall Dawson' +__date__ = '16/03/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +import shutil +import os +import tempfile +from qgis.core import ( + QgsApplication, + QgsSettings, + QgsProviderConnectionException, + QgsVectorLayer, + QgsProviderRegistry +) +from qgis.PyQt.QtCore import QCoreApplication +from qgis.testing import start_app, unittest +from utilities import unitTestDataPath + +# Convenience instances in case you may need them +# to find the srs.db +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsConnectionRegistry(unittest.TestCase): + + @classmethod + def setUpClass(cls): + """Run before all tests""" + QCoreApplication.setOrganizationName("QGIS_Test") + QCoreApplication.setOrganizationDomain(cls.__name__) + QCoreApplication.setApplicationName(cls.__name__) + start_app() + QgsSettings().clear() + + gpkg_original_path = '{}/qgis_server/test_project_wms_grouped_layers.gpkg'.format(TEST_DATA_DIR) + cls.basetestpath = tempfile.mkdtemp() + cls.gpkg_path = '{}/test_gpkg.gpkg'.format(cls.basetestpath) + shutil.copy(gpkg_original_path, cls.gpkg_path) + vl = QgsVectorLayer('{}|layername=cdb_lines'.format(cls.gpkg_path), 'test', 'ogr') + assert vl.isValid() + + @classmethod + def tearDownClass(cls): + """Run after all tests""" + os.unlink(cls.gpkg_path) + + def testCreateConnectionBad(self): + """ + Test creating connection with bad parameters + """ + with self.assertRaises(QgsProviderConnectionException): + QgsApplication.connectionRegistry().createConnection('invalid') + + with self.assertRaises(QgsProviderConnectionException): + QgsApplication.connectionRegistry().createConnection('invalid://') + + with self.assertRaises(QgsProviderConnectionException): + QgsApplication.connectionRegistry().createConnection('invalid://aa') + + def testCreateConnectionGood(self): + # make a valid connection + md = QgsProviderRegistry.instance().providerMetadata('ogr') + conn = md.createConnection(self.gpkg_path, {}) + md.saveConnection(conn, 'qgis_test1') + + conn = QgsApplication.connectionRegistry().createConnection('ogr://adasdas') + self.assertFalse(conn.uri()) + + conn = QgsApplication.connectionRegistry().createConnection('ogr://qgis_test1') + self.assertEqual(conn.uri(), self.gpkg_path) + + # case insensitive provider name + conn = QgsApplication.connectionRegistry().createConnection('OGR://qgis_test1') + self.assertEqual(conn.uri(), self.gpkg_path) + + # connection name with spaces + md.saveConnection(conn, 'qgis Test 2') + conn = QgsApplication.connectionRegistry().createConnection('OGR://qgis Test 2') + self.assertEqual(conn.uri(), self.gpkg_path) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgscoordinatetransformcontext.py b/tests/src/python/test_qgscoordinatetransformcontext.py index 90c9783d5323..0d7b3a0a5e08 100644 --- a/tests/src/python/test_qgscoordinatetransformcontext.py +++ b/tests/src/python/test_qgscoordinatetransformcontext.py @@ -54,12 +54,12 @@ def testSourceDestinationDatumTransforms(self): self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2)}) self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:28356'), - QgsCoordinateReferenceSystem(4283), 3, 4)) + QgsCoordinateReferenceSystem('EPSG:4283'), 3, 4)) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2), ('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4)}) self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:28356'), - QgsCoordinateReferenceSystem(28357), 7, 8)) + QgsCoordinateReferenceSystem('EPSG:28357'), 7, 8)) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2), ('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), @@ -86,23 +86,23 @@ def testSourceDestinationDatumTransforms(self): ('EPSG:28356', 'EPSG:28357'): QgsDatumTransform.TransformPair(9, 11)}) # indicate no transform required - self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem(28357), - QgsCoordinateReferenceSystem(28356), -1, -1)) + self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:28357'), + QgsCoordinateReferenceSystem('EPSG:28356'), -1, -1)) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2), ('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), ('EPSG:28356', 'EPSG:28357'): QgsDatumTransform.TransformPair(9, 11), ('EPSG:28357', 'EPSG:28356'): QgsDatumTransform.TransformPair(-1, -1)}) - self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem(3111), - QgsCoordinateReferenceSystem(28356), 17, -1)) + self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:3111'), + QgsCoordinateReferenceSystem('EPSG:28356'), 17, -1)) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2), ('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), ('EPSG:28356', 'EPSG:28357'): QgsDatumTransform.TransformPair(9, 11), ('EPSG:28357', 'EPSG:28356'): QgsDatumTransform.TransformPair(-1, -1), ('EPSG:3111', 'EPSG:28356'): QgsDatumTransform.TransformPair(17, -1)}) - self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem(3113), - QgsCoordinateReferenceSystem(28356), -1, 18)) + self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:3113'), + QgsCoordinateReferenceSystem('EPSG:28356'), -1, 18)) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2), ('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), @@ -111,7 +111,7 @@ def testSourceDestinationDatumTransforms(self): ('EPSG:3111', 'EPSG:28356'): QgsDatumTransform.TransformPair(17, -1), ('EPSG:3113', 'EPSG:28356'): QgsDatumTransform.TransformPair(-1, 18)}) # remove non-existing - context.removeCoordinateOperation(QgsCoordinateReferenceSystem(3113), QgsCoordinateReferenceSystem(3111)) + context.removeCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:3113'), QgsCoordinateReferenceSystem('EPSG:3111')) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:3111', 'EPSG:4283'): QgsDatumTransform.TransformPair(1, 2), ('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), @@ -121,16 +121,16 @@ def testSourceDestinationDatumTransforms(self): ('EPSG:3113', 'EPSG:28356'): QgsDatumTransform.TransformPair(-1, 18)}) # remove existing - context.removeCoordinateOperation(QgsCoordinateReferenceSystem(3111), - QgsCoordinateReferenceSystem(4283)) + context.removeCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:3111'), + QgsCoordinateReferenceSystem('EPSG:4283')) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), ('EPSG:28356', 'EPSG:28357'): QgsDatumTransform.TransformPair(9, 11), ('EPSG:28357', 'EPSG:28356'): QgsDatumTransform.TransformPair(-1, -1), ('EPSG:3111', 'EPSG:28356'): QgsDatumTransform.TransformPair(17, -1), ('EPSG:3113', 'EPSG:28356'): QgsDatumTransform.TransformPair(-1, 18)}) - context.removeCoordinateOperation(QgsCoordinateReferenceSystem(3111), - QgsCoordinateReferenceSystem(28356)) + context.removeCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:3111'), + QgsCoordinateReferenceSystem('EPSG:28356')) self.assertEqual(context.sourceDestinationDatumTransforms(), {('EPSG:28356', 'EPSG:4283'): QgsDatumTransform.TransformPair(3, 4), ('EPSG:28356', 'EPSG:28357'): QgsDatumTransform.TransformPair(9, 11), @@ -143,7 +143,7 @@ def testSourceDestinationDatumTransforms(self): @unittest.skipIf(QgsProjUtils.projVersionMajor() < 6, 'Skipped on non proj6 builds') def testSourceDestinationDatumTransformsProj6(self): context = QgsCoordinateTransformContext() - self.assertEqual(context.sourceDestinationDatumTransforms(), {}) + self.assertEqual(context.coordinateOperations(), {}) proj_string = '+proj=pipeline +step +inv +proj=lcc +lat_0=-37 +lon_0=145 +lat_1=-36 +lat_2=-38 +x_0=2500000 +y_0=2500000 +ellps=GRS80 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1' self.assertFalse( context.hasTransform(QgsCoordinateReferenceSystem('EPSG:3111'), QgsCoordinateReferenceSystem('EPSG:4283'))) @@ -203,12 +203,12 @@ def testSourceDestinationDatumTransformsProj6(self): proj_string_2 = '+proj=pipeline +step +inv +proj=utm +zone=56 +south +ellps=GRS80 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1' self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:28356'), - QgsCoordinateReferenceSystem(4283), proj_string_2)) + QgsCoordinateReferenceSystem('EPSG:4283'), proj_string_2)) self.assertEqual(context.coordinateOperations(), {('EPSG:3111', 'EPSG:4283'): proj_string, ('EPSG:28356', 'EPSG:4283'): proj_string_2}) proj_string_3 = '+proj=pipeline +step +inv +proj=utm +zone=56 +south +ellps=GRS80 +step +proj=utm +zone=57 +south +ellps=GRS80' self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:28356'), - QgsCoordinateReferenceSystem(28357), proj_string_3)) + QgsCoordinateReferenceSystem('EPSG:28357'), proj_string_3)) self.assertEqual(context.coordinateOperations(), {('EPSG:3111', 'EPSG:4283'): proj_string, ('EPSG:28356', 'EPSG:4283'): proj_string_2, ('EPSG:28356', 'EPSG:28357'): proj_string_3}) @@ -232,28 +232,28 @@ def testSourceDestinationDatumTransformsProj6(self): ('EPSG:28356', 'EPSG:28357'): 'some other proj string'}) # indicate no transform required - self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem(28357), - QgsCoordinateReferenceSystem(28356), '')) + self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:28357'), + QgsCoordinateReferenceSystem('EPSG:28356'), '')) self.assertEqual(context.coordinateOperations(), {('EPSG:3111', 'EPSG:4283'): proj_string, ('EPSG:28356', 'EPSG:4283'): proj_string_2, ('EPSG:28356', 'EPSG:28357'): 'some other proj string', ('EPSG:28357', 'EPSG:28356'): ''}) # remove non-existing - context.removeCoordinateOperation(QgsCoordinateReferenceSystem(3113), QgsCoordinateReferenceSystem(3111)) + context.removeCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:3113'), QgsCoordinateReferenceSystem('EPSG:3111')) self.assertEqual(context.coordinateOperations(), {('EPSG:3111', 'EPSG:4283'): proj_string, ('EPSG:28356', 'EPSG:4283'): proj_string_2, ('EPSG:28356', 'EPSG:28357'): 'some other proj string', ('EPSG:28357', 'EPSG:28356'): ''}) # remove existing - context.removeCoordinateOperation(QgsCoordinateReferenceSystem(3111), - QgsCoordinateReferenceSystem(4283)) + context.removeCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:3111'), + QgsCoordinateReferenceSystem('EPSG:4283')) self.assertEqual(context.coordinateOperations(), {('EPSG:28356', 'EPSG:4283'): proj_string_2, ('EPSG:28356', 'EPSG:28357'): 'some other proj string', ('EPSG:28357', 'EPSG:28356'): ''}) - context.removeCoordinateOperation(QgsCoordinateReferenceSystem(28356), - QgsCoordinateReferenceSystem(28357)) + context.removeCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:28356'), + QgsCoordinateReferenceSystem('EPSG:28357')) self.assertEqual(context.coordinateOperations(), {('EPSG:28356', 'EPSG:4283'): proj_string_2, ('EPSG:28357', 'EPSG:28356'): ''}) @@ -319,21 +319,21 @@ def testWriteReadXml(self): # setup a context context = QgsCoordinateTransformContext() - source_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))[0].sourceTransformId - dest_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))[0].destinationTransformId + source_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].sourceTransformId + dest_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].destinationTransformId - source_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))[0].sourceTransformId - dest_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))[0].destinationTransformId + source_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].sourceTransformId + dest_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].destinationTransformId - self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326), source_id_1, + self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'), source_id_1, dest_id_1)) - self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326), source_id_2, + self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'), source_id_2, dest_id_2)) self.assertEqual(context.sourceDestinationDatumTransforms(), @@ -361,15 +361,21 @@ def testWriteReadXmlProj6(self): proj_1 = '+proj=pipeline +step +proj=axisswap +order=2,1 +step +proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 +step +proj=cart +ellps=intl +step +proj=helmert +x=-18.944 +y=-379.364 +z=-24.063 +rx=-0.04 +ry=0.764 +rz=-6.431 +s=3.657 +convention=coordinate_frame +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1' proj_2 = '+proj=pipeline +step +proj=axisswap +order=2,1 +step +proj=unitconvert +xy_in=deg +xy_out=rad +step +proj=push +v_3 +step +proj=cart +ellps=intl +step +proj=helmert +x=-150 +y=-250 +z=-1 +step +inv +proj=cart +ellps=WGS84 +step +proj=pop +v_3 +step +proj=unitconvert +xy_in=rad +xy_out=deg +step +proj=axisswap +order=2,1' + proj_3 = '+proj=pipeline +step +proj=axisswap +order=2,1' + + self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'), proj_1, True)) + self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'), proj_2, False)) - self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326), proj_1, True)) - self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326), proj_2, False)) + # also insert a crs with no authid available + self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem.fromProj("+proj=longlat +a=6378137 +rf=298.25722356300003 +no_defs"), + QgsCoordinateReferenceSystem('EPSG:4326'), proj_3, False)) self.assertEqual(context.coordinateOperations(), {('EPSG:4204', 'EPSG:4326'): proj_1, - ('EPSG:4205', 'EPSG:4326'): proj_2}) + ('EPSG:4205', 'EPSG:4326'): proj_2, + ('', 'EPSG:4326'): proj_3}) # save to xml doc = QDomDocument("testdoc") @@ -383,11 +389,21 @@ def testWriteReadXmlProj6(self): # check result self.assertEqual(context2.coordinateOperations(), {('EPSG:4204', 'EPSG:4326'): proj_1, - ('EPSG:4205', 'EPSG:4326'): proj_2}) - self.assertTrue(context2.allowFallbackTransform(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))) - self.assertFalse(context2.allowFallbackTransform(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))) + ('EPSG:4205', 'EPSG:4326'): proj_2, + ('', 'EPSG:4326'): proj_3}) + self.assertEqual(context2.calculateCoordinateOperation(QgsCoordinateReferenceSystem.fromProj("+proj=longlat +a=6378137 +rf=298.25722356300003 +no_defs"), + QgsCoordinateReferenceSystem('EPSG:4326')), '+proj=pipeline +step +proj=axisswap +order=2,1') + self.assertFalse(context2.mustReverseCoordinateOperation(QgsCoordinateReferenceSystem.fromProj("+proj=longlat +a=6378137 +rf=298.25722356300003 +no_defs"), + QgsCoordinateReferenceSystem('EPSG:4326'))) + self.assertEqual(context2.calculateCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:4326'), + QgsCoordinateReferenceSystem.fromProj("+proj=longlat +a=6378137 +rf=298.25722356300003 +no_defs")), + '+proj=pipeline +step +proj=axisswap +order=2,1') + self.assertTrue(context2.mustReverseCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:4326'), + QgsCoordinateReferenceSystem.fromProj("+proj=longlat +a=6378137 +rf=298.25722356300003 +no_defs"))) + self.assertTrue(context2.allowFallbackTransform(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))) + self.assertFalse(context2.allowFallbackTransform(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))) @unittest.skipIf(QgsProjUtils.projVersionMajor() >= 6, 'Skipped on proj6 builds') def testMissingTransforms(self): @@ -482,15 +498,15 @@ def testReadWriteSettings(self): context = QgsCoordinateTransformContext() context.readSettings() - source_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))[0].sourceTransformId - dest_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))[0].destinationTransformId + source_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].sourceTransformId + dest_id_1 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].destinationTransformId - source_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))[0].sourceTransformId - dest_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))[0].destinationTransformId + source_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].sourceTransformId + dest_id_2 = QgsDatumTransform.datumTransformations(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))[0].destinationTransformId # should be empty self.assertEqual(context.sourceDestinationDatumTransforms(), {}) @@ -499,7 +515,7 @@ def testReadWriteSettings(self): QgsCoordinateReferenceSystem('EPSG:4326'), source_id_1, dest_id_1)) self.assertTrue(context.addSourceDestinationDatumTransform(QgsCoordinateReferenceSystem('EPSG:4205'), - QgsCoordinateReferenceSystem(4326), source_id_2, + QgsCoordinateReferenceSystem('EPSG:4326'), source_id_2, dest_id_2)) self.assertEqual(context.sourceDestinationDatumTransforms(), @@ -530,18 +546,18 @@ def testReadWriteSettingsProj6(self): # should be empty self.assertEqual(context.coordinateOperations(), {}) - self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326), proj_1, True)) - self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326), proj_2, False)) + self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'), proj_1, True)) + self.assertTrue(context.addCoordinateOperation(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'), proj_2, False)) self.assertEqual(context.coordinateOperations(), {('EPSG:4204', 'EPSG:4326'): proj_1, ('EPSG:4205', 'EPSG:4326'): proj_2}) - self.assertTrue(context.allowFallbackTransform(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))) - self.assertFalse(context.allowFallbackTransform(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))) + self.assertTrue(context.allowFallbackTransform(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))) + self.assertFalse(context.allowFallbackTransform(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))) # save to settings context.writeSettings() @@ -556,10 +572,10 @@ def testReadWriteSettingsProj6(self): {('EPSG:4204', 'EPSG:4326'): proj_1, ('EPSG:4205', 'EPSG:4326'): proj_2}) - self.assertTrue(context2.allowFallbackTransform(QgsCoordinateReferenceSystem(4204), - QgsCoordinateReferenceSystem(4326))) - self.assertFalse(context2.allowFallbackTransform(QgsCoordinateReferenceSystem(4205), - QgsCoordinateReferenceSystem(4326))) + self.assertTrue(context2.allowFallbackTransform(QgsCoordinateReferenceSystem('EPSG:4204'), + QgsCoordinateReferenceSystem('EPSG:4326'))) + self.assertFalse(context2.allowFallbackTransform(QgsCoordinateReferenceSystem('EPSG:4205'), + QgsCoordinateReferenceSystem('EPSG:4326'))) @unittest.skipIf(QgsProjUtils.projVersionMajor() >= 6, 'Skipped on proj6 builds') def testEqualOperator(self): diff --git a/tests/src/python/test_qgsdatabaseschemacombobox.py b/tests/src/python/test_qgsdatabaseschemacombobox.py index 19242ae4bd07..56c578c16c04 100644 --- a/tests/src/python/test_qgsdatabaseschemacombobox.py +++ b/tests/src/python/test_qgsdatabaseschemacombobox.py @@ -117,6 +117,83 @@ def testCombo(self): self.assertFalse(m.currentSchema()) self.assertFalse(spy[-1][0]) + def testComboWithEmpty(self): + """ test combobox functionality with the empty row""" + conn = QgsProviderRegistry.instance().providerMetadata('postgres').createConnection(self.uri, {}) + self.assertTrue(conn) + + m = QgsDatabaseSchemaComboBox(conn) + spy = QSignalSpy(m.schemaChanged) + old_count = m.comboBox().count() + self.assertGreaterEqual(m.comboBox().count(), 3) + m.setAllowEmptySchema(True) + self.assertEqual(m.comboBox().count(), old_count + 1) + + text = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertFalse(text[0]) + self.assertIn('CamelCaseSchema', text) + self.assertIn('qgis_test', text) + self.assertLess(text.index('CamelCaseSchema'), text.index('qgis_test')) + self.assertEqual(m.currentSchema(), 'CamelCaseSchema') + + m.setSchema('qgis_test') + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], 'qgis_test') + + m.setSchema('') + self.assertEqual(m.comboBox().currentIndex(), 0) + self.assertFalse(m.currentSchema()) + self.assertEqual(len(spy), 2) + self.assertFalse(spy[-1][0]) + m.setSchema('') + self.assertEqual(len(spy), 2) + self.assertFalse(m.currentSchema()) + + m.setSchema('qgis_test') + self.assertEqual(len(spy), 3) + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(spy[-1][0], 'qgis_test') + + conn.createSchema('myNewSchema') + text2 = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + # schemas are not automatically refreshed + self.assertEqual(text2, text) + + # but setting a new connection should fix this! + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn2 = md.createConnection(self.uri, {}) + md.saveConnection(conn2, 'another') + m.setConnectionName('another', 'postgres') + # ideally there'd be no extra signal here, but it's a minor issue... + self.assertEqual(len(spy), 4) + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(spy[-1][0], 'qgis_test') + + text2 = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertNotEqual(text2, text) + self.assertIn('myNewSchema', text2) + self.assertFalse(text2[0]) + + m.setSchema('myNewSchema') + self.assertEqual(len(spy), 5) + self.assertEqual(m.currentSchema(), 'myNewSchema') + self.assertEqual(spy[-1][0], 'myNewSchema') + + # no auto drop + conn.dropSchema('myNewSchema') + self.assertEqual(len(spy), 5) + self.assertEqual(m.currentSchema(), 'myNewSchema') + self.assertEqual(spy[-1][0], 'myNewSchema') + + m.refreshSchemas() + text2 = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertNotIn('myNewSchema', text2) + self.assertEqual(len(spy), 6) + self.assertFalse(m.currentSchema()) + self.assertFalse(spy[-1][0]) + self.assertFalse(text2[0]) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsdatabaseschemamodel.py b/tests/src/python/test_qgsdatabaseschemamodel.py index daf72b51c8b5..5156315a99f4 100644 --- a/tests/src/python/test_qgsdatabaseschemamodel.py +++ b/tests/src/python/test_qgsdatabaseschemamodel.py @@ -110,6 +110,102 @@ def testModel(self): self.assertNotIn('myNewSchema3', schemas) self.assertNotIn('myNewSchema4', schemas) + def test_model_allow_empty(self): + """Test model with empty entry""" + conn = QgsProviderRegistry.instance().providerMetadata('postgres').createConnection(self.uri, {}) + self.assertTrue(conn) + model = QgsDatabaseSchemaModel(conn) + self.assertGreaterEqual(model.rowCount(), 3) + old_count = model.rowCount() + model.setAllowEmptySchema(True) + self.assertEqual(model.rowCount(), old_count + 1) + + schemas = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('public', schemas) + self.assertIn('CamelCaseSchema', schemas) + self.assertIn('qgis_test', schemas) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertFalse(model.data(model.index(schemas.index('qgis_test'), 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertIsNone(model.data(model.index(model.rowCount(), 0, QModelIndex()), Qt.DisplayRole)) + + model.refresh() + self.assertEqual(model.rowCount(), old_count + 1) + + conn.createSchema('myNewSchema') + self.assertEqual(model.rowCount(), old_count + 1) + model.refresh() + self.assertEqual(model.rowCount(), old_count + 2) + schemas = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('public', schemas) + self.assertIn('CamelCaseSchema', schemas) + self.assertIn('qgis_test', schemas) + self.assertIn('myNewSchema', schemas) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertFalse(model.data(model.index(schemas.index('qgis_test'), 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + + model.setAllowEmptySchema(False) + self.assertEqual(model.rowCount(), old_count + 1) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + model.setAllowEmptySchema(True) + self.assertEqual(model.rowCount(), old_count + 2) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertFalse(model.data(model.index(schemas.index('qgis_test'), 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + + conn.createSchema('myNewSchema2') + conn.createSchema('myNewSchema3') + model.refresh() + self.assertEqual(model.rowCount(), old_count + 4) + schemas = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('public', schemas) + self.assertIn('CamelCaseSchema', schemas) + self.assertIn('qgis_test', schemas) + self.assertIn('myNewSchema', schemas) + self.assertIn('myNewSchema2', schemas) + self.assertIn('myNewSchema3', schemas) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertFalse(model.data(model.index(schemas.index('qgis_test'), 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + + conn.createSchema('myNewSchema4') + conn.dropSchema('myNewSchema2') + conn.dropSchema('myNewSchema') + model.refresh() + self.assertEqual(model.rowCount(), old_count + 3) + schemas = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('public', schemas) + self.assertIn('CamelCaseSchema', schemas) + self.assertIn('qgis_test', schemas) + self.assertNotIn('myNewSchema', schemas) + self.assertNotIn('myNewSchema2', schemas) + self.assertIn('myNewSchema3', schemas) + self.assertIn('myNewSchema4', schemas) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertFalse(model.data(model.index(schemas.index('qgis_test'), 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + + conn.dropSchema('myNewSchema3') + conn.dropSchema('myNewSchema4') + model.refresh() + self.assertEqual(model.rowCount(), old_count + 1) + schemas = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('public', schemas) + self.assertIn('CamelCaseSchema', schemas) + self.assertIn('qgis_test', schemas) + self.assertNotIn('myNewSchema3', schemas) + self.assertNotIn('myNewSchema4', schemas) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + self.assertFalse(model.data(model.index(schemas.index('qgis_test'), 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + + model.setAllowEmptySchema(False) + self.assertEqual(model.rowCount(), old_count) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseSchemaModel.RoleEmpty)) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsdatabasetablecombobox.py b/tests/src/python/test_qgsdatabasetablecombobox.py index 68f15244b7a9..3c2523ebd43e 100644 --- a/tests/src/python/test_qgsdatabasetablecombobox.py +++ b/tests/src/python/test_qgsdatabasetablecombobox.py @@ -213,6 +213,100 @@ def testComboAllSchemas(self): self.assertFalse(m.currentSchema()) self.assertFalse(spy[-1][0]) + def testComboWithEmpty(self): + """ test combobox functionality with empty choice """ + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn = md.createConnection(self.uri, {}) + md.saveConnection(conn, 'mycon') + + m = QgsDatabaseTableComboBox('postgres', 'mycon') + old_count = m.comboBox().count() + self.assertGreaterEqual(old_count, 3) + + m.setAllowEmptyTable(True) + self.assertEqual(m.comboBox().count(), old_count + 1) + + text = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertFalse(text[0]) + self.assertIn('information_schema.attributes', text) + self.assertIn('qgis_test.some_poly_data', text) + self.assertLess(text.index('information_schema.attributes'), text.index('qgis_test.some_poly_data')) + self.assertTrue(m.currentSchema()) + self.assertTrue(m.currentTable()) + + m.setSchema('information_schema') + m.setTable('attributes') + spy = QSignalSpy(m.tableChanged) + + m.setSchema('qgis_test') + text = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertNotIn('information_schema.attributes', text) + self.assertNotIn('attributes', text) + self.assertIn('some_poly_data', text) + + self.assertEqual(m.currentTable(), '') + self.assertEqual(m.currentSchema(), '') + self.assertEqual(len(spy), 1) + self.assertFalse(spy[-1][0]) + + m.setTable('') + self.assertEqual(m.currentTable(), '') + self.assertEqual(m.currentSchema(), '') + self.assertEqual(len(spy), 1) + self.assertFalse(spy[-1][0]) + m.setTable('someData') + self.assertEqual(len(spy), 2) + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(m.currentTable(), 'someData') + self.assertEqual(spy[-1][0], 'someData') + self.assertEqual(spy[-1][1], 'qgis_test') + + fields = QgsFields() + fields.append(QgsField('test', QVariant.String)) + conn.createVectorTable('qgis_test', 'myNewTable', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem('EPSG:3857'), False, {}) + + text2 = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + # tables are not automatically refreshed + self.assertEqual(text2, text) + + # but setting a new connection should fix this! + md = QgsProviderRegistry.instance().providerMetadata('postgres') + conn2 = md.createConnection(self.uri, {}) + md.saveConnection(conn2, 'another') + m.setConnectionName('another', 'postgres') + # ideally there'd be no extra signal here, but it's a minor issue... + self.assertEqual(len(spy), 3) + self.assertEqual(m.currentTable(), 'someData') + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(spy[-1][0], 'someData') + self.assertEqual(spy[-1][1], 'qgis_test') + + text2 = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertNotEqual(text2, text) + self.assertIn('myNewTable', text2) + + m.setTable('myNewTable') + self.assertEqual(len(spy), 4) + self.assertEqual(m.currentTable(), 'myNewTable') + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(spy[-1][0], 'myNewTable') + self.assertEqual(spy[-1][1], 'qgis_test') + + # no auto drop + conn.dropVectorTable('qgis_test', 'myNewTable') + self.assertEqual(len(spy), 4) + self.assertEqual(m.currentTable(), 'myNewTable') + self.assertEqual(m.currentSchema(), 'qgis_test') + self.assertEqual(spy[-1][0], 'myNewTable') + self.assertEqual(spy[-1][1], 'qgis_test') + + m.refreshTables() + text2 = [m.comboBox().itemText(i) for i in range(m.comboBox().count())] + self.assertNotIn('myNewTable', text2) + self.assertEqual(len(spy), 5) + self.assertFalse(m.currentSchema()) + self.assertFalse(spy[-1][0]) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsdatabasetablemodel.py b/tests/src/python/test_qgsdatabasetablemodel.py index 8f8d71e18ccb..27b56c16cc4e 100644 --- a/tests/src/python/test_qgsdatabasetablemodel.py +++ b/tests/src/python/test_qgsdatabasetablemodel.py @@ -162,6 +162,128 @@ def testModelSpecificSchema(self): QgsDatabaseTableModel.RoleWkbType), QgsWkbTypes.Polygon) self.assertIsNone(model.data(model.index(model.rowCount(), 0, QModelIndex()), Qt.DisplayRole)) + def test_model_allow_empty(self): + """Test model with empty entry""" + conn = QgsProviderRegistry.instance().providerMetadata('postgres').createConnection(self.uri, {}) + self.assertTrue(conn) + model = QgsDatabaseTableModel(conn) + self.assertGreaterEqual(model.rowCount(), 3) + old_count = model.rowCount() + + model.setAllowEmptyTable(True) + self.assertTrue(model.allowEmptyTable()) + self.assertEqual(model.rowCount(), old_count + 1) + self.assertEqual(model.columnCount(), 1) + tables = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('qgis_test.someData', tables) + self.assertIn('qgis_test.some_poly_data', tables) + self.assertIn('information_schema.attributes', tables) + + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + self.assertFalse(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleTableName), 'someData') + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleSchema), 'qgis_test') + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleComment), 'QGIS Test Table') + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleCrs), QgsCoordinateReferenceSystem('EPSG:4326')) + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleCustomInfo), {}) + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleTableFlags), 4) + self.assertEqual(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleWkbType), QgsWkbTypes.Point) + self.assertEqual(model.data(model.index(tables.index('qgis_test.some_poly_data'), 0, QModelIndex()), + QgsDatabaseTableModel.RoleWkbType), QgsWkbTypes.Polygon) + self.assertIsNone(model.data(model.index(model.rowCount(), 0, QModelIndex()), Qt.DisplayRole)) + + model.refresh() + self.assertEqual(model.rowCount(), old_count + 1) + + fields = QgsFields() + fields.append(QgsField('test', QVariant.String)) + conn.createVectorTable('qgis_test', 'myNewTable', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem('EPSG:3857'), False, {}) + self.assertEqual(model.rowCount(), old_count + 1) + + model.refresh() + self.assertEqual(model.rowCount(), old_count + 2) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + self.assertFalse(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + tables = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('qgis_test.someData', tables) + self.assertIn('qgis_test.some_poly_data', tables) + self.assertIn('information_schema.attributes', tables) + self.assertIn('qgis_test.myNewTable', tables) + + model.setAllowEmptyTable(False) + self.assertEqual(model.rowCount(), old_count + 1) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + model.setAllowEmptyTable(True) + self.assertEqual(model.rowCount(), old_count + 2) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + conn.createVectorTable('qgis_test', 'myNewTable2', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem('EPSG:3857'), False, {}) + conn.createVectorTable('qgis_test', 'myNewTable3', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem('EPSG:3857'), False, {}) + model.refresh() + self.assertEqual(model.rowCount(), old_count + 4) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + tables = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('qgis_test.someData', tables) + self.assertIn('qgis_test.some_poly_data', tables) + self.assertIn('information_schema.attributes', tables) + self.assertIn('qgis_test.myNewTable', tables) + self.assertIn('qgis_test.myNewTable2', tables) + self.assertIn('qgis_test.myNewTable3', tables) + self.assertFalse(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + conn.createVectorTable('qgis_test', 'myNewTable4', fields, QgsWkbTypes.Point, QgsCoordinateReferenceSystem('EPSG:3857'), False, {}) + conn.dropVectorTable('qgis_test', 'myNewTable2') + conn.dropVectorTable('qgis_test', 'myNewTable') + model.refresh() + self.assertEqual(model.rowCount(), old_count + 3) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + tables = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('qgis_test.someData', tables) + self.assertIn('qgis_test.some_poly_data', tables) + self.assertIn('information_schema.attributes', tables) + self.assertNotIn('qgis_test.myNewTable', tables) + self.assertNotIn('qgis_test.myNewTable2', tables) + self.assertIn('qgis_test.myNewTable3', tables) + self.assertIn('qgis_test.myNewTable4', tables) + self.assertFalse(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + conn.dropVectorTable('qgis_test', 'myNewTable3') + conn.dropVectorTable('qgis_test', 'myNewTable4') + model.refresh() + self.assertEqual(model.rowCount(), old_count + 1) + tables = [model.data(model.index(r, 0, QModelIndex()), Qt.DisplayRole) for r in range(model.rowCount())] + self.assertIn('qgis_test.someData', tables) + self.assertIn('qgis_test.some_poly_data', tables) + self.assertIn('information_schema.attributes', tables) + self.assertNotIn('qgis_test.myNewTable', tables) + self.assertNotIn('qgis_test.myNewTable2', tables) + self.assertNotIn('qgis_test.myNewTable3', tables) + self.assertNotIn('qgis_test.myNewTable4', tables) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + self.assertFalse(model.data(model.index(tables.index('qgis_test.someData'), 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + + model.setAllowEmptyTable(False) + self.assertEqual(model.rowCount(), old_count) + self.assertTrue(model.data(model.index(0, 0, QModelIndex()), Qt.DisplayRole)) + self.assertFalse(model.data(model.index(0, 0, QModelIndex()), QgsDatabaseTableModel.RoleEmpty)) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsextentgroupbox.py b/tests/src/python/test_qgsextentgroupbox.py index 4506414a71b9..6f6d164739ac 100644 --- a/tests/src/python/test_qgsextentgroupbox.py +++ b/tests/src/python/test_qgsextentgroupbox.py @@ -41,6 +41,38 @@ def testGettersSetters(self): w.setTitleBase('abc') self.assertEqual(w.titleBase(), 'abc') + def test_checkstate(self): + w = QgsExtentGroupBox() + spy = QSignalSpy(w.extentChanged) + self.assertFalse(w.isCheckable()) + self.assertFalse(w.isChecked()) + self.assertTrue(w.outputExtent().isNull()) + + w.setCheckable(True) + self.assertTrue(w.isCheckable()) + self.assertTrue(w.isChecked()) + self.assertTrue(w.outputExtent().isNull()) + self.assertEqual(len(spy), 0) + + w.setCurrentExtent(QgsRectangle(11, 12, 13, 14), QgsCoordinateReferenceSystem('epsg:3113')) + w.setOutputExtentFromCurrent() + self.assertTrue(w.isCheckable()) + self.assertTrue(w.isChecked()) + self.assertEqual(w.outputExtent(), QgsRectangle(11, 12, 13, 14)) + self.assertEqual(len(spy), 1) + + w.setChecked(False) + self.assertTrue(w.isCheckable()) + self.assertFalse(w.isChecked()) + self.assertTrue(w.outputExtent().isNull()) + self.assertEqual(len(spy), 2) + + w.setChecked(True) + self.assertTrue(w.isCheckable()) + self.assertTrue(w.isChecked()) + self.assertEqual(w.outputExtent(), QgsRectangle(11, 12, 13, 14)) + self.assertEqual(len(spy), 3) + def test_SettingExtent(self): w = qgis.gui.QgsExtentGroupBox() diff --git a/tests/src/python/test_qgsextentwidget.py b/tests/src/python/test_qgsextentwidget.py new file mode 100644 index 000000000000..ca29245fa2fb --- /dev/null +++ b/tests/src/python/test_qgsextentwidget.py @@ -0,0 +1,191 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsExtentWidget + +.. note:: 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__ = 'Nyall Dawson' +__date__ = '25/03/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA +import os + +from qgis.core import QgsRectangle, QgsCoordinateReferenceSystem, QgsVectorLayer, QgsProject, QgsFeature, QgsGeometry +from qgis.gui import QgsExtentWidget + +from qgis.PyQt.QtTest import QSignalSpy +from qgis.testing import start_app, unittest +from utilities import unitTestDataPath + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsExtentWidget(unittest.TestCase): + + def testGettersSetters(self): + """ test widget getters/setters """ + w = QgsExtentWidget() + + w.setOriginalExtent(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:3111')) + self.assertEqual(w.originalExtent(), QgsRectangle(1, 2, 3, 4)) + self.assertEqual(w.originalCrs().authid(), 'EPSG:3111') + + w.setCurrentExtent(QgsRectangle(11, 12, 13, 14), QgsCoordinateReferenceSystem('epsg:3113')) + self.assertEqual(w.currentExtent(), QgsRectangle(11, 12, 13, 14)) + self.assertEqual(w.currentCrs().authid(), 'EPSG:3113') + + def testValid(self): + w = QgsExtentWidget() + spy = QSignalSpy(w.validationChanged) + self.assertFalse(w.isValid()) + w.setOriginalExtent(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:3111')) + w.setCurrentExtent(QgsRectangle(11, 12, 13, 14), QgsCoordinateReferenceSystem('epsg:3113')) + w.setOutputExtentFromOriginal() + self.assertEqual(len(spy), 1) + self.assertTrue(w.isValid()) + + def test_SettingExtent(self): + w = QgsExtentWidget() + + spy = QSignalSpy(w.extentChanged) + + w.setOriginalExtent(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:3111')) + w.setCurrentExtent(QgsRectangle(11, 12, 13, 14), QgsCoordinateReferenceSystem('epsg:3113')) + + w.setOutputExtentFromOriginal() + self.assertEqual(w.outputExtent(), QgsRectangle(1, 2, 3, 4)) + self.assertEqual(w.extentState(), QgsExtentWidget.OriginalExtent) + self.assertEqual(len(spy), 1) + + w.setOutputExtentFromCurrent() + self.assertEqual(w.outputExtent(), QgsRectangle(11, 12, 13, 14)) + self.assertEqual(w.extentState(), QgsExtentWidget.CurrentExtent) + self.assertEqual(len(spy), 2) + + w.setOutputExtentFromUser(QgsRectangle(21, 22, 23, 24), QgsCoordinateReferenceSystem('epsg:3111')) + self.assertEqual(w.outputExtent(), QgsRectangle(21, 22, 23, 24)) + self.assertEqual(w.extentState(), QgsExtentWidget.UserExtent) + self.assertEqual(len(spy), 3) + + shapefile = os.path.join(TEST_DATA_DIR, 'polys.shp') + layer = QgsVectorLayer(shapefile, 'Polys', 'ogr') + QgsProject.instance().addMapLayer(layer) + + w.setOutputExtentFromLayer(None) + # no layer - should be unchanged + self.assertEqual(len(spy), 3) + self.assertEqual(w.outputExtent(), QgsRectangle(21, 22, 23, 24)) + self.assertEqual(w.extentState(), QgsExtentWidget.UserExtent) + self.assertEqual(len(spy), 3) + + w.setOutputExtentFromLayer(layer) + self.assertEqual(w.outputExtent().toString(4), QgsRectangle(-118.9229, 24.5079, -83.7900, 46.7262).toString(4)) + self.assertEqual(w.extentState(), QgsExtentWidget.ProjectLayerExtent) + self.assertEqual(len(spy), 4) + + QgsProject.instance().removeAllMapLayers() + + def testSetOutputCrs(self): + w = QgsExtentWidget() + + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + w.setCurrentExtent(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:4326')) + w.setOutputExtentFromCurrent() + self.assertEqual(w.outputExtent(), QgsRectangle(1, 2, 3, 4)) + + # with reprojection + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:3785')) + self.assertEqual(w.outputExtent().toString(4), QgsRectangle(111319.4908, 222684.2085, 333958.4724, 445640.1097).toString(4)) + # change CRS back + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + # extent should be back to current - not a reprojection of the reprojected bounds + self.assertEqual(w.outputExtent().toString(20), QgsRectangle(1, 2, 3, 4).toString(20)) + + # repeat, this time using original extents + w = qgis.gui.QgsExtentGroupBox() + + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + w.setOriginalExtent(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:4326')) + w.setOutputExtentFromOriginal() + self.assertEqual(w.outputExtent(), QgsRectangle(1, 2, 3, 4)) + + # with reprojection + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:3785')) + self.assertEqual(w.outputExtent().toString(4), + QgsRectangle(111319.4908, 222684.2085, 333958.4724, 445640.1097).toString(4)) + # change CRS back + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + # extent should be back to original - not a reprojection of the reprojected bounds + self.assertEqual(w.outputExtent().toString(20), QgsRectangle(1, 2, 3, 4).toString(20)) + + # repeat, this time using layer extent + layer = QgsVectorLayer("Polygon?crs=epsg:4326", 'memory', 'memory') + self.assertTrue(layer.isValid()) + f = QgsFeature() + f.setGeometry(QgsGeometry.fromWkt('Polygon((1 2, 3 2, 3 4, 1 4, 1 2))')) + layer.dataProvider().addFeatures([f]) + QgsProject.instance().addMapLayer(layer) + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + w.setOutputExtentFromLayer(layer) + self.assertEqual(w.outputExtent(), QgsRectangle(1, 2, 3, 4)) + + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:3785')) + self.assertEqual(w.outputExtent().toString(4), + QgsRectangle(111319.4908, 222684.2085, 333958.4724, 445640.1097).toString(4)) + # change CRS back + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + # extent should be back to original - not a reprojection of the reprojected bounds + self.assertEqual(w.outputExtent().toString(20), QgsRectangle(1, 2, 3, 4).toString(20)) + + # custom extent + w = qgis.gui.QgsExtentGroupBox() + + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + w.setOutputExtentFromUser(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:4326')) + self.assertEqual(w.outputExtent(), QgsRectangle(1, 2, 3, 4)) + + # with reprojection + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:3785')) + self.assertEqual(w.outputExtent().toString(4), + QgsRectangle(111319.4908, 222684.2085, 333958.4724, 445640.1097).toString(4)) + # change CRS back + w.setOutputCrs(QgsCoordinateReferenceSystem('epsg:4326')) + # in this case we can't retrieve the original user extent in 4326, so we have a reprojection of the reprojected bounds + # just test this by restricting the test to 4 decimals + self.assertEqual(w.outputExtent().toString(4), QgsRectangle(1, 2, 3, 4).toString(4)) + + def testClear(self): + w = QgsExtentWidget() + w.setNullValueAllowed(True, 'test') + valid_spy = QSignalSpy(w.validationChanged) + changed_spy = QSignalSpy(w.extentChanged) + self.assertFalse(w.isValid()) + w.setOriginalExtent(QgsRectangle(1, 2, 3, 4), QgsCoordinateReferenceSystem('epsg:3111')) + w.setCurrentExtent(QgsRectangle(11, 12, 13, 14), QgsCoordinateReferenceSystem('epsg:3113')) + w.setOutputExtentFromOriginal() + self.assertEqual(len(valid_spy), 1) + self.assertEqual(len(changed_spy), 1) + self.assertTrue(w.isValid()) + + w.clear() + self.assertEqual(len(valid_spy), 2) + self.assertEqual(len(changed_spy), 2) + self.assertFalse(w.isValid()) + self.assertTrue(w.outputExtent().isNull()) + w.clear() + self.assertEqual(len(valid_spy), 2) + self.assertEqual(len(changed_spy), 2) + self.assertTrue(w.outputExtent().isNull()) + w.setOutputExtentFromOriginal() + self.assertEqual(len(valid_spy), 3) + self.assertEqual(len(changed_spy), 3) + self.assertTrue(w.isValid()) + self.assertEqual(w.outputExtent(), QgsRectangle(1, 2, 3, 4)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsfeaturesink.py b/tests/src/python/test_qgsfeaturesink.py index 924aec6e9b33..95958142f79c 100644 --- a/tests/src/python/test_qgsfeaturesink.py +++ b/tests/src/python/test_qgsfeaturesink.py @@ -21,7 +21,16 @@ QgsField, QgsFields, QgsCoordinateReferenceSystem, - QgsProxyFeatureSink) + QgsProxyFeatureSink, + QgsRemappingProxyFeatureSink, + QgsRemappingSinkDefinition, + QgsWkbTypes, + QgsCoordinateTransform, + QgsProject, + QgsProperty, + QgsExpressionContext, + QgsExpressionContextScope + ) from qgis.PyQt.QtCore import QVariant from qgis.testing import start_app, unittest start_app() @@ -94,6 +103,129 @@ def testProxyFeatureSink(self): self.assertEqual(store.features()[1]['fldtxt'], 'test2') self.assertEqual(store.features()[2]['fldtxt'], 'test3') + def testRemappingSinkDefinition(self): + """ + Test remapping sink definitions + """ + fields = QgsFields() + fields.append(QgsField('fldtxt', QVariant.String)) + fields.append(QgsField('fldint', QVariant.Int)) + fields.append(QgsField('fldtxt2', QVariant.String)) + + mapping_def = QgsRemappingSinkDefinition() + mapping_def.setDestinationWkbType(QgsWkbTypes.Point) + self.assertEqual(mapping_def.destinationWkbType(), QgsWkbTypes.Point) + mapping_def.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + mapping_def.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + self.assertEqual(mapping_def.sourceCrs().authid(), 'EPSG:4326') + self.assertEqual(mapping_def.destinationCrs().authid(), 'EPSG:3857') + mapping_def.setDestinationFields(fields) + self.assertEqual(mapping_def.destinationFields(), fields) + mapping_def.addMappedField('fldtxt2', QgsProperty.fromField('fld1')) + mapping_def.addMappedField('fldint', QgsProperty.fromExpression('@myval * fldint')) + + self.assertEqual(mapping_def.fieldMap()['fldtxt2'].field(), 'fld1') + self.assertEqual(mapping_def.fieldMap()['fldint'].expressionString(), '@myval * fldint') + + mapping_def2 = QgsRemappingSinkDefinition(mapping_def) + self.assertTrue(mapping_def == mapping_def2) + self.assertFalse(mapping_def != mapping_def2) + mapping_def2.setDestinationWkbType(QgsWkbTypes.Polygon) + self.assertFalse(mapping_def == mapping_def2) + self.assertTrue(mapping_def != mapping_def2) + mapping_def2.setDestinationWkbType(QgsWkbTypes.Point) + mapping_def2.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:3111')) + self.assertFalse(mapping_def == mapping_def2) + self.assertTrue(mapping_def != mapping_def2) + mapping_def2.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + mapping_def2.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3111')) + self.assertFalse(mapping_def == mapping_def2) + self.assertTrue(mapping_def != mapping_def2) + mapping_def2.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + mapping_def2.setDestinationFields(QgsFields()) + self.assertFalse(mapping_def == mapping_def2) + self.assertTrue(mapping_def != mapping_def2) + mapping_def2.setDestinationFields(fields) + mapping_def2.addMappedField('fldint3', QgsProperty.fromExpression('@myval * fldint')) + self.assertFalse(mapping_def == mapping_def2) + self.assertTrue(mapping_def != mapping_def2) + mapping_def2.setFieldMap(mapping_def.fieldMap()) + self.assertTrue(mapping_def == mapping_def2) + self.assertFalse(mapping_def != mapping_def2) + + # to variant + var = mapping_def.toVariant() + + def2 = QgsRemappingSinkDefinition() + def2.loadVariant(var) + self.assertEqual(def2.destinationWkbType(), QgsWkbTypes.Point) + self.assertEqual(def2.sourceCrs().authid(), 'EPSG:4326') + self.assertEqual(def2.destinationCrs().authid(), 'EPSG:3857') + self.assertEqual(def2.destinationFields()[0].name(), 'fldtxt') + self.assertEqual(def2.destinationFields()[1].name(), 'fldint') + self.assertEqual(def2.fieldMap()['fldtxt2'].field(), 'fld1') + self.assertEqual(def2.fieldMap()['fldint'].expressionString(), '@myval * fldint') + + def testRemappingSink(self): + """ + Test remapping features + """ + fields = QgsFields() + fields.append(QgsField('fldtxt', QVariant.String)) + fields.append(QgsField('fldint', QVariant.Int)) + fields.append(QgsField('fldtxt2', QVariant.String)) + + store = QgsFeatureStore(fields, QgsCoordinateReferenceSystem('EPSG:3857')) + + mapping_def = QgsRemappingSinkDefinition() + mapping_def.setDestinationWkbType(QgsWkbTypes.Point) + mapping_def.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + mapping_def.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + mapping_def.setDestinationFields(fields) + mapping_def.addMappedField('fldtxt2', QgsProperty.fromField('fld1')) + mapping_def.addMappedField('fldint', QgsProperty.fromExpression('@myval * fldint')) + + proxy = QgsRemappingProxyFeatureSink(mapping_def, store) + self.assertEqual(proxy.destinationSink(), store) + + self.assertEqual(len(store), 0) + + incoming_fields = QgsFields() + incoming_fields.append(QgsField('fld1', QVariant.String)) + incoming_fields.append(QgsField('fldint', QVariant.Int)) + + context = QgsExpressionContext() + scope = QgsExpressionContextScope() + scope.setVariable('myval', 2) + context.appendScope(scope) + context.setFields(incoming_fields) + proxy.setExpressionContext(context) + proxy.setTransformContext(QgsProject.instance().transformContext()) + + f = QgsFeature() + f.setFields(incoming_fields) + f.setAttributes(["test", 123]) + f.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(1, 2))) + self.assertTrue(proxy.addFeature(f)) + self.assertEqual(len(store), 1) + self.assertEqual(store.features()[0].geometry().asWkt(1), 'Point (111319.5 222684.2)') + self.assertEqual(store.features()[0].attributes(), [None, 246, 'test']) + + f2 = QgsFeature() + f2.setAttributes(["test2", 457]) + f2.setGeometry(QgsGeometry.fromWkt('LineString( 1 1, 2 2)')) + f3 = QgsFeature() + f3.setAttributes(["test3", 888]) + f3.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(3, 4))) + self.assertTrue(proxy.addFeatures([f2, f3])) + self.assertEqual(len(store), 4) + self.assertEqual(store.features()[1].attributes(), [None, 914, 'test2']) + self.assertEqual(store.features()[2].attributes(), [None, 914, 'test2']) + self.assertEqual(store.features()[3].attributes(), [None, 1776, 'test3']) + self.assertEqual(store.features()[1].geometry().asWkt(1), 'Point (111319.5 111325.1)') + self.assertEqual(store.features()[2].geometry().asWkt(1), 'Point (222639 222684.2)') + self.assertEqual(store.features()[3].geometry().asWkt(1), 'Point (333958.5 445640.1)') + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgsfieldmappingwidget.py b/tests/src/python/test_qgsfieldmappingwidget.py new file mode 100644 index 000000000000..a4d57d7485f5 --- /dev/null +++ b/tests/src/python/test_qgsfieldmappingwidget.py @@ -0,0 +1,280 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsFieldMapping widget and model. + +.. note:: 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__ = 'Alessandro Pasotti' +__date__ = '16/03/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +from qgis.core import ( + QgsFields, + QgsField, + QgsFieldConstraints, + QgsProperty +) +from qgis.gui import ( + QgsFieldMappingWidget, + QgsFieldMappingModel, +) +from qgis.PyQt.Qt import Qt +from qgis.PyQt.QtCore import ( + QCoreApplication, + QVariant, + QModelIndex, + QItemSelectionModel, +) +from qgis.PyQt.QtGui import ( + QColor +) +from qgis.testing import start_app, unittest + + +class TestPyQgsFieldMappingModel(unittest.TestCase): + + @classmethod + def setUpClass(cls): + """Run before all tests""" + + QCoreApplication.setOrganizationName("QGIS_Test") + QCoreApplication.setOrganizationDomain(cls.__name__) + QCoreApplication.setApplicationName(cls.__name__) + start_app() + + def setUp(self): + """Run before each test""" + + source_fields = QgsFields() + f = QgsField('source_field1', QVariant.String) + self.assertTrue(source_fields.append(f)) + f = QgsField('source_field2', QVariant.Int, 'integer', 10, 8) + self.assertTrue(source_fields.append(f)) + + destination_fields = QgsFields() + f = QgsField('destination_field1', QVariant.Int, 'integer', 10, 8) + self.assertTrue(destination_fields.append(f)) + f = QgsField('destination_field2', QVariant.String) + self.assertTrue(destination_fields.append(f)) + f = QgsField('destination_field3', QVariant.String) + self.assertTrue(destination_fields.append(f)) + + self.source_fields = source_fields + self.destination_fields = destination_fields + + def _showDialog(self, widget): + """Used during development""" + + from qgis.PyQt.QtWidgets import QDialog, QVBoxLayout + d = QDialog() + l = QVBoxLayout() + l.addWidget(widget) + d.setLayout(l) + d.exec() + + def testModel(self): + """Test the mapping model""" + + model = QgsFieldMappingModel(self.source_fields, self.destination_fields) + self.assertEqual(model.rowCount(QModelIndex()), 3) + self.assertIsNone(model.data(model.index(9999, 0), Qt.DisplayRole)) + # We now have this default mapping: + # source exp | destination fld + # ------------------------------------------- + # source_field2 | destination_field1 + # source_field1 | destination_field2 + # NOT SET (NULL) | destination_field3 + self.assertEqual(model.data(model.index(0, 0), Qt.DisplayRole), '"source_field2"') + self.assertEqual(model.data(model.index(0, 1), Qt.DisplayRole), 'destination_field1') + self.assertEqual(model.data(model.index(0, 3), Qt.DisplayRole), 10) + self.assertEqual(model.data(model.index(0, 4), Qt.DisplayRole), 8) + + self.assertEqual(model.data(model.index(1, 0), Qt.DisplayRole), '"source_field1"') + self.assertEqual(model.data(model.index(1, 1), Qt.DisplayRole), 'destination_field2') + + self.assertEqual(model.data(model.index(2, 0), Qt.DisplayRole), QVariant()) + self.assertEqual(model.data(model.index(2, 1), Qt.DisplayRole), 'destination_field3') + + # Test expression scope + ctx = model.contextGenerator().createExpressionContext() + self.assertTrue('source_field1' in ctx.fields().names()) + + # Test add fields + model.appendField(QgsField('destination_field4', QVariant.String)) + self.assertEqual(model.rowCount(QModelIndex()), 4) + self.assertEqual(model.data(model.index(3, 1), Qt.DisplayRole), 'destination_field4') + + # Test remove field + model.removeField(model.index(3, 0)) + self.assertEqual(model.rowCount(QModelIndex()), 3) + self.assertEqual(model.data(model.index(2, 1), Qt.DisplayRole), 'destination_field3') + + # Test edit fields + mapping = model.mapping() + self.assertEqual(mapping[0].field.name(), 'destination_field1') + self.assertEqual(mapping[1].field.name(), 'destination_field2') + self.assertEqual(mapping[2].field.name(), 'destination_field3') + self.assertEqual(mapping[0].originalName, 'destination_field1') + self.assertEqual(mapping[1].originalName, 'destination_field2') + self.assertEqual(mapping[2].originalName, 'destination_field3') + + # Test move up or down + self.assertFalse(model.moveUp(model.index(0, 0))) + self.assertFalse(model.moveUp(model.index(100, 0))) + self.assertFalse(model.moveDown(model.index(2, 0))) + self.assertFalse(model.moveDown(model.index(100, 0))) + + self.assertTrue(model.moveDown(model.index(0, 0))) + mapping = model.mapping() + self.assertEqual(mapping[1].field.name(), 'destination_field1') + self.assertEqual(mapping[0].field.name(), 'destination_field2') + self.assertEqual(mapping[2].field.name(), 'destination_field3') + self.assertEqual(mapping[1].originalName, 'destination_field1') + self.assertEqual(mapping[0].originalName, 'destination_field2') + self.assertEqual(mapping[2].originalName, 'destination_field3') + + self.assertTrue(model.moveUp(model.index(1, 0))) + mapping = model.mapping() + self.assertEqual(mapping[0].field.name(), 'destination_field1') + self.assertEqual(mapping[1].field.name(), 'destination_field2') + self.assertEqual(mapping[2].field.name(), 'destination_field3') + self.assertEqual(mapping[0].originalName, 'destination_field1') + self.assertEqual(mapping[1].originalName, 'destination_field2') + self.assertEqual(mapping[2].originalName, 'destination_field3') + + self.assertTrue(model.moveUp(model.index(2, 0))) + mapping = model.mapping() + self.assertEqual(mapping[0].field.name(), 'destination_field1') + self.assertEqual(mapping[2].field.name(), 'destination_field2') + self.assertEqual(mapping[1].field.name(), 'destination_field3') + self.assertEqual(mapping[0].originalName, 'destination_field1') + self.assertEqual(mapping[2].originalName, 'destination_field2') + self.assertEqual(mapping[1].originalName, 'destination_field3') + + def testSetSourceFields(self): + """Test that changing source fields also empty expressions are updated""" + + model = QgsFieldMappingModel(self.source_fields, self.destination_fields) + self.assertEqual(model.data(model.index(2, 0), Qt.DisplayRole), QVariant()) + self.assertEqual(model.data(model.index(2, 1), Qt.DisplayRole), 'destination_field3') + + f = QgsField('source_field3', QVariant.String) + fields = self.source_fields + fields.append(f) + model.setSourceFields(fields) + self.assertEqual(model.data(model.index(0, 0), Qt.DisplayRole), '"source_field2"') + self.assertEqual(model.data(model.index(0, 1), Qt.DisplayRole), 'destination_field1') + self.assertEqual(model.data(model.index(1, 0), Qt.DisplayRole), '"source_field1"') + self.assertEqual(model.data(model.index(1, 1), Qt.DisplayRole), 'destination_field2') + self.assertEqual(model.data(model.index(2, 0), Qt.DisplayRole), '"source_field3"') + self.assertEqual(model.data(model.index(2, 1), Qt.DisplayRole), 'destination_field3') + + def testProperties(self): + model = QgsFieldMappingModel(self.source_fields, self.destination_fields) + model.setDestinationFields(self.destination_fields, {'destination_field1': '5', + 'destination_field2': 'source_field2', + 'destination_field3': 'source_field2 * @myvar'}) + + mapping = model.mapping() + self.assertEqual(mapping[0].field.name(), 'destination_field1') + self.assertEqual(mapping[1].field.name(), 'destination_field2') + self.assertEqual(mapping[2].field.name(), 'destination_field3') + self.assertEqual(mapping[0].expression, '5') + self.assertEqual(mapping[1].expression, 'source_field2') + self.assertEqual(mapping[2].expression, 'source_field2 * @myvar') + + self.assertEqual(model.fieldPropertyMap(), {'destination_field1': QgsProperty.fromExpression('5'), + 'destination_field2': QgsProperty.fromField('source_field2'), + 'destination_field3': QgsProperty.fromExpression('source_field2 * @myvar'), + }) + + model = QgsFieldMappingModel(self.source_fields, self.destination_fields) + self.assertEqual(model.fieldPropertyMap(), {'destination_field1': QgsProperty.fromField('source_field2'), + 'destination_field2': QgsProperty.fromField('source_field1'), + 'destination_field3': QgsProperty.fromExpression(''), + }) + + model.setFieldPropertyMap({ + 'destination_field1': QgsProperty.fromField('source_field1'), + 'destination_field2': QgsProperty.fromExpression('55*6'), + 'destination_field3': QgsProperty.fromValue(6), + }) + self.assertEqual(model.fieldPropertyMap(), { + 'destination_field1': QgsProperty.fromField('source_field1'), + 'destination_field2': QgsProperty.fromExpression('55*6'), + 'destination_field3': QgsProperty.fromExpression('6'), + }) + + def testWidget(self): + """Test widget operations""" + + widget = QgsFieldMappingWidget() + for i in range(10): + widget.appendField(QgsField(str(i))) + self.assertTrue(widget.model().rowCount(QModelIndex()), 10) + + def _compare(widget, expected): + actual = [] + for field in widget.mapping(): + actual.append(int(field.originalName)) + self.assertEqual(actual, expected) + + _compare(widget, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + + selection_model = widget.selectionModel() + selection_model.clear() + for i in range(0, 10, 2): + selection_model.select(widget.model().index(i, 0), QItemSelectionModel.Select) + + self.assertTrue(widget.moveSelectedFieldsDown()) + _compare(widget, [1, 0, 3, 2, 5, 4, 7, 6, 9, 8]) + + selection_model.clear() + for i in range(1, 10, 2): + selection_model.select(widget.model().index(i, 0), QItemSelectionModel.Select) + + self.assertTrue(widget.moveSelectedFieldsUp()) + _compare(widget, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + + selection_model.clear() + for i in range(0, 10, 2): + selection_model.select(widget.model().index(i, 0), QItemSelectionModel.Select) + + self.assertTrue(widget.removeSelectedFields()) + _compare(widget, [1, 3, 5, 7, 9]) + + # Test set destination fields + widget.setSourceFields(self.source_fields) + widget.setDestinationFields(self.destination_fields) + mapping = widget.mapping() + self.assertEqual(mapping[0].field.name(), 'destination_field1') + self.assertEqual(mapping[1].field.name(), 'destination_field2') + self.assertEqual(mapping[2].field.name(), 'destination_field3') + self.assertEqual(mapping[0].originalName, 'destination_field1') + self.assertEqual(mapping[1].originalName, 'destination_field2') + self.assertEqual(mapping[2].originalName, 'destination_field3') + + # Test constraints + f = QgsField('constraint_field', QVariant.Int) + constraints = QgsFieldConstraints() + constraints.setConstraint(QgsFieldConstraints.ConstraintNotNull, QgsFieldConstraints.ConstraintOriginProvider) + constraints.setConstraint(QgsFieldConstraints.ConstraintExpression, QgsFieldConstraints.ConstraintOriginProvider) + constraints.setConstraint(QgsFieldConstraints.ConstraintUnique, QgsFieldConstraints.ConstraintOriginProvider) + f.setConstraints(constraints) + fields = QgsFields() + fields.append(f) + widget.setDestinationFields(fields) + self.assertEqual(widget.model().data(widget.model().index(0, 5, QModelIndex()), Qt.DisplayRole), "Constraints active") + self.assertEqual(widget.model().data(widget.model().index(0, 5, QModelIndex()), Qt.ToolTipRole), "Unique
    Not null
    Expression") + self.assertEqual(widget.model().data(widget.model().index(0, 5, QModelIndex()), Qt.BackgroundColorRole), QColor(255, 224, 178)) + + #self._showDialog(widget) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsfieldmodel.py b/tests/src/python/test_qgsfieldmodel.py index ea708568d2e1..0548c45cc5e2 100644 --- a/tests/src/python/test_qgsfieldmodel.py +++ b/tests/src/python/test_qgsfieldmodel.py @@ -19,7 +19,8 @@ QgsFieldProxyModel, QgsEditorWidgetSetup, QgsProject, - QgsVectorLayerJoinInfo) + QgsVectorLayerJoinInfo, + QgsFieldConstraints) from qgis.PyQt.QtCore import QVariant, Qt, QModelIndex from qgis.testing import start_app, unittest @@ -348,15 +349,50 @@ def testJoinedFieldIsEditableRole(self): self.assertEqual(proxy_m.rowCount(), 1) self.assertEqual(proxy_m.data(proxy_m.index(0, 0)), 'id_a') + def testFieldIsWidgetEditableRole(self): + l, m = create_model() + self.assertTrue(m.data(m.indexFromName('fldtxt'), QgsFieldModel.FieldIsWidgetEditable)) + self.assertTrue(m.data(m.indexFromName('fldint'), QgsFieldModel.FieldIsWidgetEditable)) + self.assertFalse(m.data(m.indexFromName('an expression'), QgsFieldModel.FieldIsWidgetEditable)) + self.assertFalse(m.data(m.indexFromName(None), QgsFieldModel.FieldIsWidgetEditable)) + m.setAllowExpression(True) + m.setExpression('an expression') + self.assertTrue(m.data(m.indexFromName('an expression'), QgsFieldModel.FieldIsWidgetEditable)) + m.setAllowEmptyFieldName(True) + self.assertTrue(m.data(m.indexFromName(None), QgsFieldModel.FieldIsWidgetEditable)) + + editFormConfig = l.editFormConfig() + idx = l.fields().indexOf('fldtxt') + # Make fldtxt readOnly + editFormConfig.setReadOnly(idx, True) + l.setEditFormConfig(editFormConfig) + # It's read only, so the widget is NOT editable + self.assertFalse(m.data(m.indexFromName('fldtxt'), QgsFieldModel.FieldIsWidgetEditable)) + def testFieldTooltip(self): f = QgsField('my_string', QVariant.String, 'string') - self.assertEqual(QgsFieldModel.fieldToolTip(f), 'my_string

    string

    ') + self.assertEqual(QgsFieldModel.fieldToolTip(f), "my_string
    string NULL") f.setAlias('my alias') - self.assertEqual(QgsFieldModel.fieldToolTip(f), 'my alias (my_string)

    string

    ') + self.assertEqual(QgsFieldModel.fieldToolTip(f), "my alias (my_string)
    string NULL") f.setLength(20) - self.assertEqual(QgsFieldModel.fieldToolTip(f), 'my alias (my_string)

    string (20)

    ') + self.assertEqual(QgsFieldModel.fieldToolTip(f), "my alias (my_string)
    string(20) NULL") f = QgsField('my_real', QVariant.Double, 'real', 8, 3) - self.assertEqual(QgsFieldModel.fieldToolTip(f), 'my_real

    real (8, 3)

    ') + self.assertEqual(QgsFieldModel.fieldToolTip(f), "my_real
    real(8, 3) NULL") + f.setComment('Comment text') + self.assertEqual(QgsFieldModel.fieldToolTip(f), "my_real
    real(8, 3) NULL
    Comment text") + + def testFieldTooltipExtended(self): + layer = QgsVectorLayer("Point?", "tooltip", "memory") + f = QgsField('my_real', QVariant.Double, 'real', 8, 3, 'Comment text') + layer.addExpressionField('1+1', f) + layer.updateFields() + self.assertEqual(QgsFieldModel.fieldToolTipExtended(QgsField('my_string', QVariant.String, 'string'), layer), '') + self.assertEqual(QgsFieldModel.fieldToolTipExtended(f, layer), "my_real
    real(8, 3) NULL
    Comment text
    1+1") + f.setAlias('my alias') + constraints = f.constraints() + constraints.setConstraint(QgsFieldConstraints.ConstraintUnique) + f.setConstraints(constraints) + self.assertEqual(QgsFieldModel.fieldToolTipExtended(f, layer), "my alias (my_real)
    real(8, 3) NULL UNIQUE
    Comment text
    1+1") if __name__ == '__main__': diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index 6cea93258b2f..2553a66ba8e5 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -5304,6 +5304,113 @@ def testLineStringFromQPolygonF(self): line = QgsLineString.fromQPolygonF(QPolygonF([QPointF(1.5, 2.5), QPointF(3, 4), QPointF(3, 6.5), QPointF(1.5, 2.5)])) self.assertEqual(line.asWkt(1), 'LineString (1.5 2.5, 3 4, 3 6.5, 1.5 2.5)') + def testCoerce(self): + """Test coerce function""" + + def coerce_to_wkt(wkt, type): + geom = QgsGeometry.fromWkt(wkt) + return [g.asWkt(2) for g in geom.coerceToType(type)] + + self.assertEqual(coerce_to_wkt('Point (1 1)', QgsWkbTypes.Point), ['Point (1 1)']) + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3)', QgsWkbTypes.LineString), ['LineString (1 1, 2 2, 3 3)']) + self.assertEqual(coerce_to_wkt('Polygon((1 1, 2 2, 1 2, 1 1))', QgsWkbTypes.Polygon), ['Polygon ((1 1, 2 2, 1 2, 1 1))']) + + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3)', QgsWkbTypes.Point), ['Point (1 1)', 'Point (2 2)', 'Point (3 3)']) + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3)', QgsWkbTypes.Polygon), ['Polygon ((1 1, 2 2, 3 3, 1 1))']) + self.assertEqual(coerce_to_wkt('Polygon((1 1, 2 2, 1 2, 1 1))', QgsWkbTypes.Point), ['Point (1 1)', 'Point (2 2)', 'Point (1 2)']) + self.assertEqual(coerce_to_wkt('Polygon((1 1, 2 2, 1 2, 1 1))', QgsWkbTypes.LineString), ['LineString (1 1, 2 2, 1 2, 1 1)']) + + self.assertEqual(coerce_to_wkt('Point z (1 1 3)', QgsWkbTypes.Point), ['Point (1 1)']) + self.assertEqual(coerce_to_wkt('Point z (1 1 3)', QgsWkbTypes.PointZ), ['PointZ (1 1 3)']) + + # Adding Z back + self.assertEqual(coerce_to_wkt('Point (1 1)', QgsWkbTypes.PointZ), ['PointZ (1 1 0)']) + + # Adding M back + self.assertEqual(coerce_to_wkt('Point (1 1)', QgsWkbTypes.PointM), ['PointM (1 1 0)']) + self.assertEqual(coerce_to_wkt('Point m (1 1 3)', QgsWkbTypes.Point), ['Point (1 1)']) + self.assertEqual(coerce_to_wkt('Point(1 3)', QgsWkbTypes.MultiPoint), ['MultiPoint ((1 3))']) + self.assertEqual(coerce_to_wkt('MultiPoint((1 3), (2 2))', QgsWkbTypes.MultiPoint), ['MultiPoint ((1 3),(2 2))']) + + self.assertEqual(coerce_to_wkt('Polygon((1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.Polygon), ['Polygon ((1 1, 2 2, 3 3, 1 1))']) + self.assertEqual(coerce_to_wkt('Polygon z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.Polygon), ['Polygon ((1 1, 2 2, 3 3, 1 1))']) + self.assertEqual(coerce_to_wkt('Polygon z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.PolygonZ), ['PolygonZ ((1 1 1, 2 2 2, 3 3 3, 1 1 1))']) + + # Adding Z back + self.assertEqual(coerce_to_wkt('Polygon ((1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.PolygonZ), ['PolygonZ ((1 1 0, 2 2 0, 3 3 0, 1 1 0))']) + + # Adding M back + self.assertEqual(coerce_to_wkt('Polygon ((1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.PolygonM), ['PolygonM ((1 1 0, 2 2 0, 3 3 0, 1 1 0))']) + + self.assertEqual(coerce_to_wkt('Polygon m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.Polygon), ['Polygon ((1 1, 2 2, 3 3, 1 1))']) + self.assertEqual(coerce_to_wkt('Polygon m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.PolygonM), ['PolygonM ((1 1 1, 2 2 2, 3 3 3, 1 1 1))']) + self.assertEqual(coerce_to_wkt('Polygon((1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.MultiPolygon), ['MultiPolygon (((1 1, 2 2, 3 3, 1 1)))']) + self.assertEqual(coerce_to_wkt('MultiPolygon(((1 1, 2 2, 3 3, 1 1)), ((1 1, 2 2, 3 3, 1 1)))', QgsWkbTypes.MultiPolygon), ['MultiPolygon (((1 1, 2 2, 3 3, 1 1)),((1 1, 2 2, 3 3, 1 1)))']) + + self.assertEqual(coerce_to_wkt('LineString((1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.LineString), ['LineString (0 1, 2 2, 3 3, 1 0)']) + self.assertEqual(coerce_to_wkt('LineString z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.LineString), ['LineString (0 1, 2 2, 3 3, 1 1)']) + self.assertEqual(coerce_to_wkt('LineString z ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.LineStringZ), ['LineStringZ (0 1 1, 2 2 2, 3 3 3, 1 1 0)']) + self.assertEqual(coerce_to_wkt('LineString m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.LineString), ['LineString (0 1, 2 2, 3 3, 1 1)']) + self.assertEqual(coerce_to_wkt('LineString m ((1 1 1, 2 2 2, 3 3 3, 1 1 1))', QgsWkbTypes.LineStringM), ['LineStringM (0 1 1, 2 2 2, 3 3 3, 1 1 0)']) + + # Adding Z back + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.LineStringZ), ['LineStringZ (1 1 0, 2 2 0, 3 3 0, 1 0 0)']) + + # Adding M back + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3, 1 1))', QgsWkbTypes.LineStringM), ['LineStringM (1 1 0, 2 2 0, 3 3 0, 1 0 0)']) + + self.assertEqual(coerce_to_wkt('LineString(1 1, 2 2, 3 3, 1 1)', QgsWkbTypes.MultiLineString), ['MultiLineString ((1 1, 2 2, 3 3, 1 1))']) + self.assertEqual(coerce_to_wkt('MultiLineString((1 1, 2 2, 3 3, 1 1), (1 1, 2 2, 3 3, 1 1))', + QgsWkbTypes.MultiLineString), ['MultiLineString ((1 1, 2 2, 3 3, 1 1),(1 1, 2 2, 3 3, 1 1))']) + + # Test Multi -> Single + self.assertEqual(coerce_to_wkt('MultiLineString((1 1, 2 2, 3 3, 1 1), (10 1, 20 2, 30 3, 10 1))', + QgsWkbTypes.LineString), ['LineString (1 1, 2 2, 3 3, 1 1)', 'LineString (10 1, 20 2, 30 3, 10 1)']) + + # line -> points + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3)', QgsWkbTypes.Point), ['Point (1 1)', 'Point (2 2)', 'Point (3 3)']) + + self.assertEqual(coerce_to_wkt('LineString (1 1, 2 2, 3 3)', QgsWkbTypes.MultiPoint), ['MultiPoint ((1 1),(2 2),(3 3))']) + + self.assertEqual(coerce_to_wkt('MultiLineString ((1 1, 2 2),(4 4, 3 3))', QgsWkbTypes.Point), ['Point (1 1)', 'Point (2 2)', 'Point (4 4)', 'Point (3 3)']) + + self.assertEqual(coerce_to_wkt('MultiLineString ((1 1, 2 2),(4 4, 3 3))', QgsWkbTypes.MultiPoint), ['MultiPoint ((1 1),(2 2),(4 4),(3 3))']) + + # line -> polygon + self.assertEqual(coerce_to_wkt('LineString (1 1, 1 2, 2 2)', QgsWkbTypes.Polygon), ['Polygon ((1 1, 1 2, 2 2, 1 1))']) + + self.assertEqual(coerce_to_wkt('LineString (1 1, 1 2, 2 2)', QgsWkbTypes.MultiPolygon), ['MultiPolygon (((1 1, 1 2, 2 2, 1 1)))']) + + self.assertEqual(coerce_to_wkt('MultiLineString ((1 1, 1 2, 2 2, 1 1),(3 3, 4 3, 4 4))', QgsWkbTypes.Polygon), ['Polygon ((1 1, 1 2, 2 2, 1 1))', 'Polygon ((3 3, 4 3, 4 4, 3 3))']) + + self.assertEqual(coerce_to_wkt('MultiLineString ((1 1, 1 2, 2 2, 1 1),(3 3, 4 3, 4 4))', + QgsWkbTypes.MultiPolygon), ['MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))']) + + self.assertEqual(coerce_to_wkt('CircularString (1 1, 1 2, 2 2, 2 0, 1 1)', QgsWkbTypes.CurvePolygon), ['CurvePolygon (CircularString (1 1, 1 2, 2 2, 2 0, 1 1))']) + self.assertEqual(coerce_to_wkt('CircularString (1 1, 1 2, 2 2, 2 0, 1 1)', QgsWkbTypes.LineString)[0][:100], 'LineString (1 1, 0.99 1.01, 0.98 1.02, 0.97 1.03, 0.97 1.04, 0.96 1.05, 0.95 1.06, 0.94 1.06, 0.94 1') + self.assertEqual(coerce_to_wkt('CircularString (1 1, 1 2, 2 2, 2 0, 1 1)', QgsWkbTypes.Polygon)[0][:100], 'Polygon ((1 1, 0.99 1.01, 0.98 1.02, 0.97 1.03, 0.97 1.04, 0.96 1.05, 0.95 1.06, 0.94 1.06, 0.94 1.0') + + # polygon -> points + self.assertEqual(coerce_to_wkt('Polygon ((1 1, 1 2, 2 2, 1 1))', QgsWkbTypes.Point), ['Point (1 1)', 'Point (1 2)', 'Point (2 2)']) + + self.assertEqual(coerce_to_wkt('Polygon ((1 1, 1 2, 2 2, 1 1))', QgsWkbTypes.MultiPoint), ['MultiPoint ((1 1),(1 2),(2 2))']) + + self.assertEqual(coerce_to_wkt('MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))', QgsWkbTypes.Point), ['Point (1 1)', 'Point (1 2)', 'Point (2 2)', 'Point (3 3)', 'Point (4 3)', 'Point (4 4)']) + + self.assertEqual(coerce_to_wkt('MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))', + QgsWkbTypes.MultiPoint), ['MultiPoint ((1 1),(1 2),(2 2),(3 3),(4 3),(4 4))']) + + # polygon -> lines + self.assertEqual(coerce_to_wkt('Polygon ((1 1, 1 2, 2 2, 1 1))', QgsWkbTypes.LineString), ['LineString (1 1, 1 2, 2 2, 1 1)']) + + self.assertEqual(coerce_to_wkt('Polygon ((1 1, 1 2, 2 2, 1 1))', QgsWkbTypes.MultiLineString), ['MultiLineString ((1 1, 1 2, 2 2, 1 1))']) + + self.assertEqual(coerce_to_wkt('MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))', + QgsWkbTypes.LineString), ['LineString (1 1, 1 2, 2 2, 1 1)', 'LineString (3 3, 4 3, 4 4, 3 3)']) + + self.assertEqual(coerce_to_wkt('MultiPolygon (((1 1, 1 2, 2 2, 1 1)),((3 3, 4 3, 4 4, 3 3)))', + QgsWkbTypes.MultiLineString), ['MultiLineString ((1 1, 1 2, 2 2, 1 1),(3 3, 4 3, 4 4, 3 3))']) + def renderGeometry(self, geom, use_pen, as_polygon=False, as_painter_path=False): image = QImage(200, 200, QImage.Format_RGB32) image.fill(QColor(0, 0, 0)) diff --git a/tests/src/python/test_qgslayertreeview.py b/tests/src/python/test_qgslayertreeview.py index 207434efe48e..630fc69ebf99 100644 --- a/tests/src/python/test_qgslayertreeview.py +++ b/tests/src/python/test_qgslayertreeview.py @@ -211,6 +211,148 @@ def testMoveToTopActionEmbeddedGroup(self): groupname + '-' + self.layer4.name(), ]) + def testMoveToTopActionLayerAndGroup(self): + """Test move to top action for a group and it's layer simultaneously""" + view = QgsLayerTreeView() + group = self.project.layerTreeRoot().addGroup("embeddedgroup") + group.addLayer(self.layer4) + group.addLayer(self.layer5) + groupname = group.name() + view.setModel(self.model) + actions = QgsLayerTreeViewDefaultActions(view) + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + groupname, + groupname + '-' + self.layer4.name(), + groupname + '-' + self.layer5.name(), + ]) + + selectionMode = view.selectionMode() + view.setSelectionMode(QgsLayerTreeView.MultiSelection) + nodeLayerIndex = self.model.node2index(group) + view.setCurrentIndex(nodeLayerIndex) + view.setCurrentLayer(self.layer5) + view.setSelectionMode(selectionMode) + movetotop = actions.actionMoveToTop() + movetotop.trigger() + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + groupname, + groupname + '-' + self.layer5.name(), + groupname + '-' + self.layer4.name(), + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + ]) + + def testMoveToBottomActionLayer(self): + """Test move to bottom action on layer""" + view = QgsLayerTreeView() + view.setModel(self.model) + actions = QgsLayerTreeViewDefaultActions(view) + self.assertEqual(self.project.layerTreeRoot().layerOrder(), [self.layer, self.layer2, self.layer3]) + view.setCurrentLayer(self.layer) + movetobottom = actions.actionMoveToBottom() + movetobottom.trigger() + self.assertEqual(self.project.layerTreeRoot().layerOrder(), [self.layer2, self.layer3, self.layer]) + + def testMoveToBottomActionGroup(self): + """Test move to bottom action on group""" + view = QgsLayerTreeView() + group = self.project.layerTreeRoot().insertGroup(0, "embeddedgroup") + group.addLayer(self.layer4) + group.addLayer(self.layer5) + groupname = group.name() + view.setModel(self.model) + actions = QgsLayerTreeViewDefaultActions(view) + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + groupname, + groupname + '-' + self.layer4.name(), + groupname + '-' + self.layer5.name(), + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + ]) + + nodeLayerIndex = self.model.node2index(group) + view.setCurrentIndex(nodeLayerIndex) + movetobottom = actions.actionMoveToBottom() + movetobottom.trigger() + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + groupname, + groupname + '-' + self.layer4.name(), + groupname + '-' + self.layer5.name(), + ]) + + def testMoveToBottomActionEmbeddedGroup(self): + """Test move to bottom action on embeddedgroup layer""" + view = QgsLayerTreeView() + group = self.project.layerTreeRoot().addGroup("embeddedgroup") + group.addLayer(self.layer4) + group.addLayer(self.layer5) + groupname = group.name() + view.setModel(self.model) + actions = QgsLayerTreeViewDefaultActions(view) + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + groupname, + groupname + '-' + self.layer4.name(), + groupname + '-' + self.layer5.name(), + ]) + + view.setCurrentLayer(self.layer4) + movetobottom = actions.actionMoveToBottom() + movetobottom.trigger() + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + groupname, + groupname + '-' + self.layer5.name(), + groupname + '-' + self.layer4.name(), + ]) + + def testMoveToBottomActionLayerAndGroup(self): + """Test move to top action for a group and it's layer simultaneously""" + view = QgsLayerTreeView() + group = self.project.layerTreeRoot().insertGroup(0, "embeddedgroup") + group.addLayer(self.layer4) + group.addLayer(self.layer5) + groupname = group.name() + view.setModel(self.model) + actions = QgsLayerTreeViewDefaultActions(view) + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + groupname, + groupname + '-' + self.layer4.name(), + groupname + '-' + self.layer5.name(), + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + ]) + + selectionMode = view.selectionMode() + view.setSelectionMode(QgsLayerTreeView.MultiSelection) + nodeLayerIndex = self.model.node2index(group) + view.setCurrentIndex(nodeLayerIndex) + view.setCurrentLayer(self.layer4) + view.setSelectionMode(selectionMode) + movetobottom = actions.actionMoveToBottom() + movetobottom.trigger() + self.assertEqual(self.nodeOrder(self.project.layerTreeRoot().children()), [ + self.layer.name(), + self.layer2.name(), + self.layer3.name(), + groupname, + groupname + '-' + self.layer5.name(), + groupname + '-' + self.layer4.name(), + ]) + def testSetLayerVisible(self): view = QgsLayerTreeView() view.setModel(self.model) diff --git a/tests/src/python/test_qgslayoutlegend.py b/tests/src/python/test_qgslayoutlegend.py index 94fb971cc9c3..0f95626aa138 100644 --- a/tests/src/python/test_qgslayoutlegend.py +++ b/tests/src/python/test_qgslayoutlegend.py @@ -33,6 +33,10 @@ QgsMapLayerLegendUtils, QgsLegendStyle, QgsFontUtils, + QgsLineSymbol, + QgsMapThemeCollection, + QgsCategorizedSymbolRenderer, + QgsRendererCategory, QgsApplication) from qgis.testing import (start_app, unittest @@ -480,6 +484,145 @@ def testSymbolExpressionRender(self): QgsProject.instance().removeMapLayers([point_layer.id()]) + def testThemes(self): + layout = QgsPrintLayout(QgsProject.instance()) + layout.setName('LAYOUT') + + map = QgsLayoutItemMap(layout) + layout.addLayoutItem(map) + legend = QgsLayoutItemLegend(layout) + + self.assertFalse(legend.themeName()) + legend.setLinkedMap(map) + self.assertFalse(legend.themeName()) + + map.setFollowVisibilityPresetName('theme1') + map.setFollowVisibilityPreset(True) + self.assertEqual(legend.themeName(), 'theme1') + map.setFollowVisibilityPresetName('theme2') + self.assertEqual(legend.themeName(), 'theme2') + map.setFollowVisibilityPreset(False) + self.assertFalse(legend.themeName()) + + # with theme set before linking map + map2 = QgsLayoutItemMap(layout) + map2.setFollowVisibilityPresetName('theme3') + map2.setFollowVisibilityPreset(True) + legend.setLinkedMap(map2) + self.assertEqual(legend.themeName(), 'theme3') + map2.setFollowVisibilityPresetName('theme2') + self.assertEqual(legend.themeName(), 'theme2') + + # replace with map with no theme + map3 = QgsLayoutItemMap(layout) + legend.setLinkedMap(map3) + self.assertFalse(legend.themeName()) + + def testLegendRenderWithMapTheme(self): + """Test rendering legends linked to map themes""" + QgsProject.instance().removeAllMapLayers() + + point_path = os.path.join(TEST_DATA_DIR, 'points.shp') + point_layer = QgsVectorLayer(point_path, 'points', 'ogr') + line_path = os.path.join(TEST_DATA_DIR, 'lines.shp') + line_layer = QgsVectorLayer(line_path, 'lines', 'ogr') + QgsProject.instance().clear() + QgsProject.instance().addMapLayers([point_layer, line_layer]) + + marker_symbol = QgsMarkerSymbol.createSimple({'color': '#ff0000', 'outline_style': 'no', 'size': '5'}) + point_layer.setRenderer(QgsSingleSymbolRenderer(marker_symbol)) + point_layer.styleManager().addStyleFromLayer("red") + + line_symbol = QgsLineSymbol.createSimple({'color': '#ff0000', 'line_width': '2'}) + line_layer.setRenderer(QgsSingleSymbolRenderer(line_symbol)) + line_layer.styleManager().addStyleFromLayer("red") + + red_record = QgsMapThemeCollection.MapThemeRecord() + point_red_record = QgsMapThemeCollection.MapThemeLayerRecord(point_layer) + point_red_record.usingCurrentStyle = True + point_red_record.currentStyle = 'red' + red_record.addLayerRecord(point_red_record) + line_red_record = QgsMapThemeCollection.MapThemeLayerRecord(line_layer) + line_red_record.usingCurrentStyle = True + line_red_record.currentStyle = 'red' + red_record.addLayerRecord(line_red_record) + QgsProject.instance().mapThemeCollection().insert('red', red_record) + + marker_symbol1 = QgsMarkerSymbol.createSimple({'color': '#0000ff', 'outline_style': 'no', 'size': '5'}) + marker_symbol2 = QgsMarkerSymbol.createSimple({'color': '#0000ff', 'name': 'diamond', 'outline_style': 'no', 'size': '5'}) + marker_symbol3 = QgsMarkerSymbol.createSimple({'color': '#0000ff', 'name': 'rectangle', 'outline_style': 'no', 'size': '5'}) + + point_layer.setRenderer(QgsCategorizedSymbolRenderer('Class', [QgsRendererCategory('B52', marker_symbol1, ''), + QgsRendererCategory('Biplane', marker_symbol2, ''), + QgsRendererCategory('Jet', marker_symbol3, ''), + ])) + point_layer.styleManager().addStyleFromLayer("blue") + + line_symbol = QgsLineSymbol.createSimple({'color': '#0000ff', 'line_width': '2'}) + line_layer.setRenderer(QgsSingleSymbolRenderer(line_symbol)) + line_layer.styleManager().addStyleFromLayer("blue") + + blue_record = QgsMapThemeCollection.MapThemeRecord() + point_blue_record = QgsMapThemeCollection.MapThemeLayerRecord(point_layer) + point_blue_record.usingCurrentStyle = True + point_blue_record.currentStyle = 'blue' + blue_record.addLayerRecord(point_blue_record) + line_blue_record = QgsMapThemeCollection.MapThemeLayerRecord(line_layer) + line_blue_record.usingCurrentStyle = True + line_blue_record.currentStyle = 'blue' + blue_record.addLayerRecord(line_blue_record) + QgsProject.instance().mapThemeCollection().insert('blue', blue_record) + + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + + map1 = QgsLayoutItemMap(layout) + map1.attemptSetSceneRect(QRectF(20, 20, 80, 80)) + map1.setFrameEnabled(True) + map1.setLayers([point_layer, line_layer]) + layout.addLayoutItem(map1) + map1.setExtent(point_layer.extent()) + map1.setFollowVisibilityPreset(True) + map1.setFollowVisibilityPresetName('red') + + map2 = QgsLayoutItemMap(layout) + map2.attemptSetSceneRect(QRectF(20, 120, 80, 80)) + map2.setFrameEnabled(True) + map2.setLayers([point_layer, line_layer]) + layout.addLayoutItem(map2) + map2.setExtent(point_layer.extent()) + map2.setFollowVisibilityPreset(True) + map2.setFollowVisibilityPresetName('blue') + + legend = QgsLayoutItemLegend(layout) + legend.setTitle("Legend") + legend.attemptSetSceneRect(QRectF(120, 20, 80, 80)) + legend.setFrameEnabled(True) + legend.setFrameStrokeWidth(QgsLayoutMeasurement(2)) + legend.setBackgroundColor(QColor(200, 200, 200)) + legend.setTitle('') + layout.addLayoutItem(legend) + legend.setLinkedMap(map1) + + legend2 = QgsLayoutItemLegend(layout) + legend2.setTitle("Legend") + legend2.attemptSetSceneRect(QRectF(120, 120, 80, 80)) + legend2.setFrameEnabled(True) + legend2.setFrameStrokeWidth(QgsLayoutMeasurement(2)) + legend2.setBackgroundColor(QColor(200, 200, 200)) + legend2.setTitle('') + layout.addLayoutItem(legend2) + legend2.setLinkedMap(map2) + + checker = QgsLayoutChecker( + 'composer_legend_theme', layout) + checker.setControlPathPrefix("composer_legend") + result, message = checker.testLayout() + self.report += checker.report() + self.assertTrue(result, message) + + QgsProject.instance().clear() + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutmap.py b/tests/src/python/test_qgslayoutmap.py index 7959d6e8f717..26375755b29d 100644 --- a/tests/src/python/test_qgslayoutmap.py +++ b/tests/src/python/test_qgslayoutmap.py @@ -17,6 +17,7 @@ from qgis.PyQt.QtCore import QFileInfo, QRectF, QDir from qgis.PyQt.QtXml import QDomDocument from qgis.PyQt.QtGui import QPainter, QColor +from qgis.PyQt.QtTest import QSignalSpy from qgis.core import (QgsLayoutItemMap, QgsRectangle, @@ -398,6 +399,63 @@ def testBlockingItems(self): self.assertTrue(map_restore.isLabelBlockingItem(map2_restore)) self.assertTrue(map_restore.isLabelBlockingItem(map3_restore)) + def testTheme(self): + layout = QgsLayout(QgsProject.instance()) + map = QgsLayoutItemMap(layout) + self.assertFalse(map.followVisibilityPreset()) + self.assertFalse(map.followVisibilityPresetName()) + + spy = QSignalSpy(map.themeChanged) + + map.setFollowVisibilityPresetName('theme') + self.assertFalse(map.followVisibilityPreset()) + self.assertEqual(map.followVisibilityPresetName(), 'theme') + # should not be emitted - followVisibilityPreset is False + self.assertEqual(len(spy), 0) + map.setFollowVisibilityPresetName('theme2') + self.assertEqual(map.followVisibilityPresetName(), 'theme2') + self.assertEqual(len(spy), 0) + + map.setFollowVisibilityPresetName('') + map.setFollowVisibilityPreset(True) + # should not be emitted - followVisibilityPresetName is empty + self.assertEqual(len(spy), 0) + self.assertFalse(map.followVisibilityPresetName()) + self.assertTrue(map.followVisibilityPreset()) + + map.setFollowVisibilityPresetName('theme') + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], 'theme') + map.setFollowVisibilityPresetName('theme') + self.assertEqual(len(spy), 1) + map.setFollowVisibilityPresetName('theme2') + self.assertEqual(len(spy), 2) + self.assertEqual(spy[-1][0], 'theme2') + map.setFollowVisibilityPreset(False) + self.assertEqual(len(spy), 3) + self.assertFalse(spy[-1][0]) + map.setFollowVisibilityPreset(False) + self.assertEqual(len(spy), 3) + map.setFollowVisibilityPresetName('theme3') + self.assertEqual(len(spy), 3) + map.setFollowVisibilityPreset(True) + self.assertEqual(len(spy), 4) + self.assertEqual(spy[-1][0], 'theme3') + map.setFollowVisibilityPreset(True) + self.assertEqual(len(spy), 4) + + # data defined theme + map.dataDefinedProperties().setProperty(QgsLayoutObject.MapStylePreset, QgsProperty.fromValue('theme4')) + map.refresh() + self.assertEqual(len(spy), 5) + self.assertEqual(spy[-1][0], 'theme4') + map.refresh() + self.assertEqual(len(spy), 5) + map.dataDefinedProperties().setProperty(QgsLayoutObject.MapStylePreset, QgsProperty.fromValue('theme6')) + map.refresh() + self.assertEqual(len(spy), 6) + self.assertEqual(spy[-1][0], 'theme6') + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgslayoutmarker.py b/tests/src/python/test_qgslayoutmarker.py new file mode 100644 index 000000000000..1f6f316f74a6 --- /dev/null +++ b/tests/src/python/test_qgslayoutmarker.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutItemMarker. + +.. note:: 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__ = '(C) 2020 by Nyall Dawson' +__date__ = '05/04/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.PyQt.QtCore import Qt, QRectF +from qgis.PyQt.QtXml import QDomDocument + +from qgis.core import (QgsLayoutItemMarker, + QgsLayoutItemRegistry, + QgsLayout, + QgsMarkerSymbol, + QgsProject, + QgsReadWriteContext, + QgsLayoutPoint, + QgsUnitTypes, + QgsLayoutItemMap, + QgsRectangle, + QgsLayoutNorthArrowHandler, + QgsCoordinateReferenceSystem) +from qgis.testing import (start_app, + unittest + ) +from utilities import unitTestDataPath +from qgslayoutchecker import QgsLayoutChecker +from test_qgslayoutitem import LayoutItemTestCase + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsLayoutMarker(unittest.TestCase, LayoutItemTestCase): + + @classmethod + def setUpClass(cls): + cls.item_class = QgsLayoutItemMarker + + def __init__(self, methodName): + """Run once on class initialization.""" + unittest.TestCase.__init__(self, methodName) + + # style + props = {} + props["color"] = "green" + props["style"] = "solid" + props["style_border"] = "solid" + props["color_border"] = "black" + props["width_border"] = "10.0" + props["joinstyle"] = "miter" + + style = QgsMarkerSymbol.createSimple(props) + + def testDisplayName(self): + """Test if displayName is valid""" + + layout = QgsLayout(QgsProject.instance()) + marker = QgsLayoutItemMarker(layout) + self.assertEqual(marker.displayName(), "") + marker.setId('id') + self.assertEqual(marker.displayName(), "id") + + def testType(self): + """Test if type is valid""" + layout = QgsLayout(QgsProject.instance()) + marker = QgsLayoutItemMarker(layout) + + self.assertEqual( + marker.type(), QgsLayoutItemRegistry.LayoutMarker) + + def testRender(self): + """Test marker rendering.""" + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + marker = QgsLayoutItemMarker(layout) + marker.attemptMove(QgsLayoutPoint(100, 50, QgsUnitTypes.LayoutMillimeters)) + props = {} + props["color"] = "0,255,255" + props["outline_width"] = "4" + props["outline_color"] = "0,0,0" + props["size"] = "14.4" + + style = QgsMarkerSymbol.createSimple(props) + marker.setSymbol(style) + layout.addLayoutItem(marker) + checker = QgsLayoutChecker( + 'layout_marker_render', layout) + checker.setControlPathPrefix("layout_marker") + myTestResult, myMessage = checker.testLayout() + assert myTestResult, myMessage + + def testReadWriteXml(self): + pr = QgsProject() + l = QgsLayout(pr) + marker = QgsLayoutItemMarker(l) + l.addLayoutItem(marker) + + map = QgsLayoutItemMap(l) + l.addLayoutItem(map) + + props = {} + props["color"] = "green" + props["outline_style"] = "no" + props["size"] = "4.4" + + style = QgsMarkerSymbol.createSimple(props) + marker.setSymbol(style) + + marker.setLinkedMap(map) + marker.setNorthMode(QgsLayoutNorthArrowHandler.TrueNorth) + marker.setNorthOffset(15) + + #save original item to xml + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + self.assertTrue(marker.writeXml(elem, doc, QgsReadWriteContext())) + + marker2 = QgsLayoutItemMarker(l) + self.assertTrue(marker2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext())) + marker2.finalizeRestoreFromXml() + + self.assertEqual(marker2.symbol().symbolLayer(0).color().name(), '#008000') + self.assertEqual(marker2.symbol().symbolLayer(0).strokeStyle(), Qt.NoPen) + self.assertEqual(marker2.symbol().symbolLayer(0).size(), 4.4) + + self.assertEqual(marker2.linkedMap(), map) + self.assertEqual(marker2.northMode(), QgsLayoutNorthArrowHandler.TrueNorth) + self.assertEqual(marker2.northOffset(), 15.0) + + def testBounds(self): + pr = QgsProject() + l = QgsLayout(pr) + + shape = QgsLayoutItemMarker(l) + shape.attemptMove(QgsLayoutPoint(10, 20, QgsUnitTypes.LayoutMillimeters)) + props = {} + props["shape"] = "square" + props["size"] = "6" + props["outline_width"] = "2" + style = QgsMarkerSymbol.createSimple(props) + shape.setSymbol(style) + + # these must match symbol size + size = shape.sizeWithUnits().toQSizeF() + self.assertAlmostEqual(size.width(), 8.0846, 1) + self.assertAlmostEqual(size.height(), 8.08, 1) + pos = shape.positionWithUnits().toQPointF() + self.assertAlmostEqual(pos.x(), 10.0, 1) + self.assertAlmostEqual(pos.y(), 20.0, 1) + + # these are just rough! + bounds = shape.sceneBoundingRect() + self.assertAlmostEqual(bounds.left(), 0.957, 1) + self.assertAlmostEqual(bounds.right(), 19.04, 1) + self.assertAlmostEqual(bounds.top(), 10.95, 1) + self.assertAlmostEqual(bounds.bottom(), 29.04, 1) + + def testNorthArrowWithMapItemRotation(self): + """Test picture rotation when map item is also rotated""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.setExtent(QgsRectangle(0, -256, 256, 0)) + layout.addLayoutItem(map) + + marker = QgsLayoutItemMarker(layout) + layout.addLayoutItem(marker) + + marker.setLinkedMap(map) + self.assertEqual(marker.linkedMap(), map) + + marker.setNorthMode(QgsLayoutNorthArrowHandler.GridNorth) + map.setItemRotation(45) + self.assertEqual(marker.northArrowRotation(), 45) + map.setMapRotation(-34) + self.assertEqual(marker.northArrowRotation(), 11) + + # add an offset + marker.setNorthOffset(-10) + self.assertEqual(marker.northArrowRotation(), 1) + + map.setItemRotation(55) + self.assertEqual(marker.northArrowRotation(), 11) + + def testGridNorth(self): + """Test syncing picture to grid north""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.setExtent(QgsRectangle(0, -256, 256, 0)) + layout.addLayoutItem(map) + + marker = QgsLayoutItemMarker(layout) + layout.addLayoutItem(marker) + + marker.setLinkedMap(map) + self.assertEqual(marker.linkedMap(), map) + + marker.setNorthMode(QgsLayoutNorthArrowHandler.GridNorth) + map.setMapRotation(45) + self.assertEqual(marker.northArrowRotation(), 45) + + # add an offset + marker.setNorthOffset(-10) + self.assertEqual(marker.northArrowRotation(), 35) + + def testTrueNorth(self): + """Test syncing picture to true north""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.setCrs(QgsCoordinateReferenceSystem.fromEpsgId(3575)) + map.setExtent(QgsRectangle(-2126029.962, -2200807.749, -119078.102, -757031.156)) + layout.addLayoutItem(map) + + marker = QgsLayoutItemMarker(layout) + layout.addLayoutItem(marker) + + marker.setLinkedMap(map) + self.assertEqual(marker.linkedMap(), map) + + marker.setNorthMode(QgsLayoutNorthArrowHandler.TrueNorth) + self.assertAlmostEqual(marker.northArrowRotation(), 37.20, 1) + + # shift map + map.setExtent(QgsRectangle(2120672.293, -3056394.691, 2481640.226, -2796718.780)) + self.assertAlmostEqual(marker.northArrowRotation(), -38.18, 1) + + # rotate map + map.setMapRotation(45) + self.assertAlmostEqual(marker.northArrowRotation(), -38.18 + 45, 1) + + # add an offset + marker.setNorthOffset(-10) + self.assertAlmostEqual(marker.northArrowRotation(), -38.18 + 35, 1) + + def testRenderWithNorthRotation(self): + layout = QgsLayout(QgsProject.instance()) + layout.initializeDefaults() + + map = QgsLayoutItemMap(layout) + map.setExtent(QgsRectangle(0, -256, 256, 0)) + + marker = QgsLayoutItemMarker(layout) + marker.attemptMove(QgsLayoutPoint(100, 50, QgsUnitTypes.LayoutMillimeters)) + props = {} + props["color"] = "0,255,255" + props["outline_style"] = "no" + props["size"] = "14.4" + props["name"] = "arrow" + props["angle"] = "10" + + marker.setLinkedMap(map) + self.assertEqual(marker.linkedMap(), map) + + marker.setNorthMode(QgsLayoutNorthArrowHandler.GridNorth) + map.setMapRotation(35) + self.assertEqual(marker.northArrowRotation(), 35) + + # when rendering, north arrow rotation must be ADDED to symbol rotation! ie. + # it does not replace it + + style = QgsMarkerSymbol.createSimple(props) + marker.setSymbol(style) + layout.addLayoutItem(marker) + checker = QgsLayoutChecker( + 'layout_marker_render_north', layout) + checker.setControlPathPrefix("layout_marker") + myTestResult, myMessage = checker.testLayout() + assert myTestResult, myMessage + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayoutnortharrowhandler.py b/tests/src/python/test_qgslayoutnortharrowhandler.py new file mode 100644 index 000000000000..8cc711137b77 --- /dev/null +++ b/tests/src/python/test_qgslayoutnortharrowhandler.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutNorthArrowHandler. + +.. note:: 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__ = '(C) 2020 by Nyall Dawson' +__date__ = '05/04/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.PyQt.QtCore import QRectF +from qgis.PyQt.QtTest import QSignalSpy +from qgis.core import (QgsLayoutNorthArrowHandler, + QgsLayout, + QgsLayoutItemMap, + QgsRectangle, + QgsCoordinateReferenceSystem, + QgsProject + ) +from qgis.testing import start_app, unittest +from utilities import unitTestDataPath + + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsLayoutNorthArrowHandler(unittest.TestCase): + + def testNorthArrowWithMapItemRotation(self): + """Test arrow rotation when map item is also rotated""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.setExtent(QgsRectangle(0, -256, 256, 0)) + layout.addLayoutItem(map) + + handler = QgsLayoutNorthArrowHandler(layout) + spy = QSignalSpy(handler.arrowRotationChanged) + + handler.setLinkedMap(map) + self.assertEqual(handler.linkedMap(), map) + self.assertEqual(len(spy), 0) + + handler.setNorthMode(QgsLayoutNorthArrowHandler.GridNorth) + map.setItemRotation(45) + self.assertEqual(handler.arrowRotation(), 45) + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], 45) + map.setMapRotation(-34) + self.assertEqual(handler.arrowRotation(), 11) + self.assertEqual(len(spy), 2) + self.assertEqual(spy[-1][0], 11) + + # add an offset + handler.setNorthOffset(-10) + self.assertEqual(handler.arrowRotation(), 1) + self.assertEqual(len(spy), 3) + self.assertEqual(spy[-1][0], 1) + + map.setItemRotation(55) + self.assertEqual(handler.arrowRotation(), 11) + self.assertEqual(len(spy), 4) + self.assertEqual(spy[-1][0], 11) + + def testMapWithInitialRotation(self): + """Test arrow rotation when map item is initially rotated""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.setExtent(QgsRectangle(0, -256, 256, 0)) + map.setRotation(45) + layout.addLayoutItem(map) + + handler = QgsLayoutNorthArrowHandler(layout) + spy = QSignalSpy(handler.arrowRotationChanged) + + handler.setLinkedMap(map) + self.assertEqual(handler.linkedMap(), map) + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], 45) + + handler.setLinkedMap(None) + self.assertEqual(len(spy), 2) + self.assertEqual(spy[-1][0], 0) + + def testGridNorth(self): + """Test syncing arrow to grid north""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.setExtent(QgsRectangle(0, -256, 256, 0)) + layout.addLayoutItem(map) + + handler = QgsLayoutNorthArrowHandler(layout) + spy = QSignalSpy(handler.arrowRotationChanged) + + handler.setLinkedMap(map) + self.assertEqual(handler.linkedMap(), map) + self.assertEqual(len(spy), 0) + + handler.setNorthMode(QgsLayoutNorthArrowHandler.GridNorth) + map.setMapRotation(45) + self.assertEqual(handler.arrowRotation(), 45) + self.assertEqual(len(spy), 1) + self.assertEqual(spy[-1][0], 45) + + # add an offset + handler.setNorthOffset(-10) + self.assertEqual(handler.arrowRotation(), 35) + self.assertEqual(len(spy), 2) + self.assertEqual(spy[-1][0], 35) + + def testTrueNorth(self): + """Test syncing arrow to true north""" + + layout = QgsLayout(QgsProject.instance()) + + map = QgsLayoutItemMap(layout) + map.attemptSetSceneRect(QRectF(0, 0, 10, 10)) + map.setCrs(QgsCoordinateReferenceSystem.fromEpsgId(3575)) + map.setExtent(QgsRectangle(-2126029.962, -2200807.749, -119078.102, -757031.156)) + layout.addLayoutItem(map) + + handler = QgsLayoutNorthArrowHandler(layout) + spy = QSignalSpy(handler.arrowRotationChanged) + + handler.setLinkedMap(map) + self.assertEqual(handler.linkedMap(), map) + self.assertEqual(len(spy), 0) + + handler.setNorthMode(QgsLayoutNorthArrowHandler.TrueNorth) + self.assertAlmostEqual(handler.arrowRotation(), 37.20, 1) + self.assertEqual(len(spy), 1) + self.assertAlmostEqual(spy[-1][0], 37.20, 1) + + # shift map + map.setExtent(QgsRectangle(2120672.293, -3056394.691, 2481640.226, -2796718.780)) + self.assertAlmostEqual(handler.arrowRotation(), -38.18, 1) + self.assertEqual(len(spy), 2) + self.assertAlmostEqual(spy[-1][0], -38.18, 1) + + # rotate map + map.setMapRotation(45) + self.assertAlmostEqual(handler.arrowRotation(), -38.18 + 45, 1) + self.assertEqual(len(spy), 3) + self.assertAlmostEqual(spy[-1][0], -38.18 + 45, 1) + + # add an offset + handler.setNorthOffset(-10) + self.assertAlmostEqual(handler.arrowRotation(), -38.18 + 35, 1) + self.assertEqual(len(spy), 4) + self.assertAlmostEqual(spy[-1][0], -38.18 + 35, 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgslayoutpicture.py b/tests/src/python/test_qgslayoutpicture.py index deedcfcbdffe..a4593718d776 100644 --- a/tests/src/python/test_qgslayoutpicture.py +++ b/tests/src/python/test_qgslayoutpicture.py @@ -17,13 +17,15 @@ import threading import http.server from qgis.PyQt.QtCore import QRectF, QDir - +from qgis.PyQt.QtTest import QSignalSpy +from qgis.PyQt.QtXml import QDomDocument from qgis.core import (QgsLayoutItemPicture, QgsLayout, QgsLayoutItemMap, QgsRectangle, QgsCoordinateReferenceSystem, - QgsProject + QgsProject, + QgsReadWriteContext ) from qgis.testing import start_app, unittest from utilities import unitTestDataPath @@ -57,6 +59,7 @@ def __init__(self, methodName): TEST_DATA_DIR = unitTestDataPath() self.pngImage = TEST_DATA_DIR + "/sample_image.png" + self.svgImage = TEST_DATA_DIR + "/sample_svg.svg" # create composition self.layout = QgsLayout(QgsProject.instance()) @@ -76,6 +79,59 @@ def tearDown(self): with open(report_file_path, 'a') as report_file: report_file.write(self.report) + def testMode(self): + pic = QgsLayoutItemPicture(self.layout) + # should default to unknown + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatUnknown) + spy = QSignalSpy(pic.changed) + pic.setMode(QgsLayoutItemPicture.FormatRaster) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatRaster) + self.assertEqual(len(spy), 1) + pic.setMode(QgsLayoutItemPicture.FormatRaster) + self.assertEqual(len(spy), 1) + pic.setMode(QgsLayoutItemPicture.FormatSVG) + self.assertEqual(len(spy), 3) # ideally only 2! + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatSVG) + + # set picture path without explicit format + pic.setPicturePath(self.pngImage) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatRaster) + pic.setPicturePath(self.svgImage) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatSVG) + # forced format + pic.setPicturePath(self.pngImage, QgsLayoutItemPicture.FormatSVG) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatSVG) + pic.setPicturePath(self.pngImage, QgsLayoutItemPicture.FormatRaster) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatRaster) + pic.setPicturePath(self.svgImage, QgsLayoutItemPicture.FormatSVG) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatSVG) + pic.setPicturePath(self.svgImage, QgsLayoutItemPicture.FormatRaster) + self.assertEquals(pic.mode(), QgsLayoutItemPicture.FormatRaster) + + def testReadWriteXml(self): + pr = QgsProject() + l = QgsLayout(pr) + + pic = QgsLayoutItemPicture(l) + # mode should be saved/restored + pic.setMode(QgsLayoutItemPicture.FormatRaster) + + #save original item to xml + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + self.assertTrue(pic.writeXml(elem, doc, QgsReadWriteContext())) + + pic2 = QgsLayoutItemPicture(l) + self.assertTrue(pic2.readXml(elem.firstChildElement(), doc, QgsReadWriteContext())) + self.assertEqual(pic2.mode(), QgsLayoutItemPicture.FormatRaster) + + pic.setMode(QgsLayoutItemPicture.FormatSVG) + elem = doc.createElement("test2") + self.assertTrue(pic.writeXml(elem, doc, QgsReadWriteContext())) + pic3 = QgsLayoutItemPicture(l) + self.assertTrue(pic3.readXml(elem.firstChildElement(), doc, QgsReadWriteContext())) + self.assertEqual(pic3.mode(), QgsLayoutItemPicture.FormatSVG) + def testResizeZoom(self): """Test picture resize zoom mode.""" self.picture.setResizeMode(QgsLayoutItemPicture.Zoom) diff --git a/tests/src/python/test_qgslegendpatchshape.py b/tests/src/python/test_qgslegendpatchshape.py new file mode 100644 index 000000000000..bc423fd37974 --- /dev/null +++ b/tests/src/python/test_qgslegendpatchshape.py @@ -0,0 +1,275 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLegendPatchShape. + +.. note:: 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__ = '(C) 2020 by Nyall Dawson' +__date__ = '05/04/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.PyQt.QtCore import (QSize, + QSizeF, + QPointF, + QDir) +from qgis.PyQt.QtGui import (QPolygonF, + QImage, + QPainter, + QColor) +from qgis.core import (QgsLegendPatchShape, + QgsGeometry, + QgsSymbol, + QgsFillSymbol, + QgsLineSymbol, + QgsMarkerSymbol, + QgsRenderChecker, + QgsReadWriteContext, + QgsRenderContext + ) +from qgis.PyQt.QtXml import QDomDocument, QDomElement + +from qgis.testing import start_app, unittest +from utilities import unitTestDataPath + + +start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsLegendPatchShape(unittest.TestCase): + + def setUp(self): + # Create some simple symbols + self.fill_symbol = QgsFillSymbol.createSimple({'color': '#ffffff', 'outline_color': 'black'}) + self.line_symbol = QgsLineSymbol.createSimple({'color': '#ffffff', 'line_width': '3'}) + self.marker_symbol = QgsMarkerSymbol.createSimple({'color': '#ffffff', 'size': '3', 'outline_color': 'black'}) + self.report = "

    Python QgsLegendPatchShape Tests

    \n" + + def tearDown(self): + report_file_path = "%s/qgistest.html" % QDir.tempPath() + with open(report_file_path, 'a') as report_file: + report_file.write(self.report) + + def testBasic(self): + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('LineString( 0 0, 1 1)'), False) + self.assertFalse(shape.isNull()) + self.assertEqual(shape.symbolType(), QgsSymbol.Line) + self.assertEqual(shape.geometry().asWkt(), 'LineString (0 0, 1 1)') + self.assertFalse(shape.preserveAspectRatio()) + + shape.setSymbolType(QgsSymbol.Marker) + self.assertEqual(shape.symbolType(), QgsSymbol.Marker) + + shape.setGeometry(QgsGeometry.fromWkt('Multipoint( 1 1, 2 2)')) + self.assertEqual(shape.geometry().asWkt(), 'MultiPoint ((1 1),(2 2))') + + shape.setPreserveAspectRatio(True) + self.assertTrue(shape.preserveAspectRatio()) + + @staticmethod + def polys_to_list(polys): + return [[[[round(p.x(), 3), round(p.y(), 3)] for p in ring] for ring in poly] for poly in polys] + + def testNull(self): + shape = QgsLegendPatchShape() + self.assertTrue(shape.isNull()) + shape.setGeometry(QgsGeometry.fromWkt('Multipoint( 1 1, 2 2)')) + self.assertFalse(shape.isNull()) + shape.setGeometry(QgsGeometry()) + self.assertTrue(shape.isNull()) + + def testDefault(self): + self.assertEqual(QgsLegendPatchShape.defaultPatch(QgsSymbol.Hybrid, QSizeF(1, 1)), []) + self.assertEqual(QgsLegendPatchShape.defaultPatch(QgsSymbol.Hybrid, QSizeF(10, 10)), []) + + # markers + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Marker, QSizeF(1, 1))), [[[[0.0, 0.0]]]]) + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Marker, QSizeF(2, 2))), + [[[[1.0, 1.0]]]]) + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Marker, QSizeF(10, 2))), [[[[5.0, 1.0]]]]) + + # lines + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Line, QSizeF(1, 1))), [[[[0.0, 0.5], [1.0, 0.5]]]]) + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Line, QSizeF(10, 2))), [[[[0.0, 1.5], [10.0, 1.5]]]]) + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Line, QSizeF(9, 3))), [[[[0.0, 1.5], [9.0, 1.5]]]]) + + # fills + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Fill, QSizeF(1, 1))), [[[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]]) + self.assertEqual(self.polys_to_list(QgsLegendPatchShape.defaultPatch(QgsSymbol.Fill, QSizeF(10, 2))), [[[[0.0, 0.0], [10.0, 0.0], [10.0, 2.0], [0.0, 2.0], [0.0, 0.0]]]]) + + def testMarkers(self): + # shouldn't matter what a point geometry is, it will always be rendered in center of symbol patch + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('Point( 5 5 )'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(1, 1))), [[[[0.5, 0.5]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(10, 2))), [[[[5.0, 1.0]]]]) + + # requesting different symbol type, should return default + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(1, 1))), [[[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]]) + + # ... but a multipoint WILL change the result! + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('MultiPoint((5 5), (1 2))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(1, 1))), [[[[1.0, 0.0], [0.0, 1.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(10, 2))), [[[[10.0, 0.0], [0.0, 2.0]]]]) + + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('MultiPoint((5 5), (1 2), (4 3))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(1, 1))), [[[[1.0, 0.0], [0.0, 1.0], [0.75, 0.667]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(10, 2))), [[[[10.0, 0.0], [0.0, 2.0], [7.5, 1.333]]]]) + + def testPreserveAspect(self): + # wider + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('MultiPoint((5 5), (1 2))')) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(1, 1))), [[[[1.0, 0.125], [0.0, 0.875]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(10, 2))), [[[[6.333, 0.0], [3.667, 2.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(2, 10))), [[[[2.0, 4.25], [0.0, 5.75]]]]) + + # higher + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('MultiPoint((5 5), (2 1))')) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(1, 1))), [[[[0.875, 0.0], [0.125, 1.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(10, 2))), [[[[5.75, 0.0], [4.25, 2.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Marker, QSizeF(2, 10))), [[[[2.0, 3.667], [0.0, 6.333]]]]) + + def testLines(self): + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('LineString(5 5, 1 2)'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(1, 1))), [[[[1.0, 0.0], [0.0, 1.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(10, 2))), [[[[10.0, 0.0], [0.0, 2.0]]]]) + + # requesting different symbol type, should return default + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(1, 1))), [[[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0]]]]) + + # circularstring + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('CircularString(5 5, 1 2, 3 4)'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(1, 1)))[0][0][:5], + [[0.342, 0.026], [0.35, 0.023], [0.359, 0.02], [0.367, 0.018], [0.375, 0.016]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(10, 2)))[0][0][:5], + [[3.419, 0.051], [3.647, 0.042], [3.875, 0.036], [4.104, 0.034], [4.332, 0.036]]) + + # multilinestring + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('MultiLineString((5 5, 1 2),(3 6, 4 2))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(1, 1))), [[[[1.0, 0.25], [0.0, 1.0]]], [[[0.5, 0.0], [0.75, 1.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(10, 2))), [[[[10.0, 0.5], [0.0, 2.0]]], [[[5.0, 0.0], [7.5, 2.0]]]]) + + def testFills(self): + shape = QgsLegendPatchShape(QgsSymbol.Fill, QgsGeometry.fromWkt('Polygon((5 5, 1 2, 3 4, 5 5))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(1, 1))), [[[[1.0, 0.0], [0.0, 1.0], [0.5, 0.333], [1.0, 0.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(10, 2))), [[[[10.0, 0.0], [0.0, 2.0], [5.0, 0.667], [10.0, 0.0]]]]) + + # requesting different symbol type, should return default + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Line, QSizeF(1, 1))), [[[[0.0, 0.5], [1.0, 0.5]]]]) + + # rings + shape = QgsLegendPatchShape(QgsSymbol.Fill, QgsGeometry.fromWkt('Polygon((5 5, 1 2, 3 4, 5 5), (4.5 4.5, 4.4 4.4, 4.5 4.4, 4.5 4.5))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(1, 1))), + [[[[1.0, 0.0], [0.0, 1.0], [0.5, 0.333], [1.0, 0.0]], [[0.875, 0.167], [0.85, 0.2], [0.875, 0.2], [0.875, 0.167]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(10, 2))), + [[[[10.0, 0.0], [0.0, 2.0], [5.0, 0.667], [10.0, 0.0]], [[8.75, 0.333], [8.5, 0.4], [8.75, 0.4], [8.75, 0.333]]]]) + + # circular + shape = QgsLegendPatchShape(QgsSymbol.Fill, QgsGeometry.fromWkt('CurvePolygon(CircularString(5 5, 3 4, 1 2, 3 0, 5 5))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(1, 1)))[0][0][:5], + [[0.746, -0.0], [0.722, 0.009], [0.698, 0.018], [0.675, 0.028], [0.651, 0.038]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(10, 2)))[0][0][:5], + [[7.459, -0.0], [6.83, 0.04], [6.201, 0.09], [5.574, 0.151], [4.947, 0.223]]) + + # multipolygon + shape = QgsLegendPatchShape(QgsSymbol.Fill, QgsGeometry.fromWkt('MultiPolygon(((5 5, 1 2, 3 4, 5 5), (4.5 4.5, 4.4 4.4, 4.5 4.4, 4.5 4.5)),((10 11, 11 11, 11 10, 10 11)))'), False) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(1, 1))), [[[[0.4, 0.667], [0.0, 1.0], [0.2, 0.778], [0.4, 0.667]], [[0.35, 0.722], [0.34, 0.733], [0.35, 0.733], [0.35, 0.722]]], [[[0.9, 0.0], [1.0, 0.0], [1.0, 0.111], [0.9, 0.0]]]]) + self.assertEqual(self.polys_to_list(shape.toQPolygonF(QgsSymbol.Fill, QSizeF(10, 2))), [[[[4.0, 1.333], [0.0, 2.0], [2.0, 1.556], [4.0, 1.333]], [[3.5, 1.444], [3.4, 1.467], [3.5, 1.467], [3.5, 1.444]]], [[[9.0, 0.0], [10.0, 0.0], [10.0, 0.222], [9.0, 0.0]]]]) + + def testRenderMarker(self): + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('MultiPoint((5 5), (3 4), (1 2))'), False) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('Marker', 'marker_multipoint', rendered_image)) + + def testRenderMarkerPreserve(self): + shape = QgsLegendPatchShape(QgsSymbol.Marker, QgsGeometry.fromWkt('MultiPoint((5 5), (3 4), (1 2))'), True) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('Marker Preserve', 'marker_multipoint_preserve', rendered_image)) + + def testRenderLine(self): + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('LineString(5 5, 3 4, 1 2)'), False) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('Line', 'line', rendered_image)) + + def testRenderLinePreserve(self): + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('LineString(5 5, 3 4, 1 2)'), True) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('Line Preserve', 'line_preserve', rendered_image)) + + def testRenderMultiLine(self): + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('MultiLineString((5 5, 3 4, 1 2), ( 6 6, 6 0))'), True) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('Multiline', 'multiline', rendered_image)) + + def testRenderPolygon(self): + shape = QgsLegendPatchShape(QgsSymbol.Fill, QgsGeometry.fromWkt('Polygon((1 1 , 6 1, 6 6, 1 1),(4 2, 5 3, 4 3, 4 2))'), False) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('Polygon', 'polygon', rendered_image)) + + def testRenderMultiPolygon(self): + shape = QgsLegendPatchShape(QgsSymbol.Fill, QgsGeometry.fromWkt('MultiPolygon(((1 1 , 6 1, 6 6, 1 1),(4 2, 5 3, 4 3, 4 2)),((1 5, 2 5, 1 6, 1 5)))'), False) + rendered_image = self.renderPatch(shape) + self.assertTrue(self.imageCheck('MultiPolygon', 'multipolygon', rendered_image)) + + def testReadWriteXml(self): + doc = QDomDocument("testdoc") + elem = doc.createElement('test') + shape = QgsLegendPatchShape(QgsSymbol.Line, QgsGeometry.fromWkt('MultiLineString((5 5, 3 4, 1 2), ( 6 6, 6 0))'), False) + + shape.writeXml(elem, doc, QgsReadWriteContext()) + + s2 = QgsLegendPatchShape() + s2.readXml(elem, QgsReadWriteContext()) + + self.assertFalse(s2.isNull()) + self.assertEqual(s2.geometry().asWkt(), 'MultiLineString ((5 5, 3 4, 1 2),(6 6, 6 0))') + self.assertFalse(s2.preserveAspectRatio()) + self.assertEqual(s2.symbolType(), QgsSymbol.Line) + + def renderPatch(self, patch): + image = QImage(200, 200, QImage.Format_RGB32) + image.setDotsPerMeterX(96 / 25.4 * 1000) + image.setDotsPerMeterY(96 / 25.4 * 1000) + + painter = QPainter() + painter.begin(image) + + context = QgsRenderContext.fromQPainter(painter) + context.setPainter(painter) + context.setScaleFactor(96 / 25.4) # 96 DPI + + try: + image.fill(QColor(0, 0, 0)) + + if patch.symbolType() == QgsSymbol.Fill: + self.fill_symbol.drawPreviewIcon(painter, QSize(200, 200), None, False, None, patch) + elif patch.symbolType() == QgsSymbol.Line: + self.line_symbol.drawPreviewIcon(painter, QSize(200, 200), None, False, None, patch) + elif patch.symbolType() == QgsSymbol.Marker: + self.marker_symbol.drawPreviewIcon(painter, QSize(200, 200), None, False, None, patch) + finally: + painter.end() + + return image + + def imageCheck(self, name, reference_image, image): + self.report += "

    Render {}

    \n".format(name) + temp_dir = QDir.tempPath() + '/' + file_name = temp_dir + 'patch_' + name + ".png" + image.save(file_name, "PNG") + checker = QgsRenderChecker() + checker.setControlPathPrefix("legend_patch") + checker.setControlName("expected_" + reference_image) + checker.setRenderedImage(file_name) + checker.setColorTolerance(2) + result = checker.compareImages(name, 20) + self.report += checker.report() + print((self.report)) + return result + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsmaplayercombobox.py b/tests/src/python/test_qgsmaplayercombobox.py index d3e7b4b639b8..341da2b2e5aa 100644 --- a/tests/src/python/test_qgsmaplayercombobox.py +++ b/tests/src/python/test_qgsmaplayercombobox.py @@ -174,10 +174,15 @@ def testSignals(self): m.setLayer(None) self.assertEqual(len(spy), 3) self.assertIsNone(m.currentLayer()) + self.assertFalse(m.currentText()) m.setLayer(None) self.assertEqual(len(spy), 3) self.assertIsNone(m.currentLayer()) + m.setEditable(True) + m.setCurrentText('aaa') + self.assertIsNone(m.currentLayer()) + m.setLayer(l1) self.assertEqual(len(spy), 4) self.assertEqual(m.currentLayer(), l1) diff --git a/tests/src/python/test_qgsnumericformat.py b/tests/src/python/test_qgsnumericformat.py index beac20359e2b..30ef58d96a71 100644 --- a/tests/src/python/test_qgsnumericformat.py +++ b/tests/src/python/test_qgsnumericformat.py @@ -21,6 +21,7 @@ QgsCurrencyNumericFormat, QgsNumericFormatRegistry, QgsNumericFormat, + QgsFractionNumericFormat, QgsReadWriteContext) from qgis.testing import start_app, unittest from qgis.PyQt.QtXml import QDomDocument @@ -619,6 +620,150 @@ def testScientificFormat(self): self.assertEqual(f3.numberDecimalPlaces(), f.numberDecimalPlaces()) self.assertEqual(f3.showThousandsSeparator(), f.showThousandsSeparator()) + def testDoubleToFraction(self): + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(1), (True, 1, 1, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(2), (True, 2, 1, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-1), (True, 1, 1, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-2), (True, 2, 1, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0), (True, 0, 1, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(1000000), (True, 1000000, 1, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-1000000), (True, 1000000, 1, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.5), (True, 1, 2, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.25), (True, 1, 4, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.75), (True, 3, 4, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.5), (True, 1, 2, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.25), (True, 1, 4, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.75), (True, 3, 4, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(1.5), (True, 3, 2, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(1.25), (True, 5, 4, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(1.75), (True, 7, 4, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.5), (True, 1, 2, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.25), (True, 1, 4, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.1), (True, 1, 10, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-1.5), (True, 3, 2, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-1.25), (True, 5, 4, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-1.75), (True, 7, 4, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.3333333333333333333333), (True, 1, 3, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.333333333), (True, 333333355, 1000000066, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.333333333, 0.0000000001), (True, 333333355, 1000000066, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.333333333, 0.000000001), (True, 1, 3, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.333333333, 0.1), (True, 1, 3, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.3333333333333333333333), (True, 1, 3, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.333333333), (True, 333333355, 1000000066, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.333333333, 0.0000000001), (True, 333333355, 1000000066, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.333333333, 0.000000001), (True, 1, 3, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(-0.333333333, 0.1), (True, 1, 3, -1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(0.000000123123), (True, 1, 8121959, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(3.14159265358979), (True, 312689, 99532, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(3.14159265358979, 0.0000001), (True, 103993, 33102, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(3.14159265358979, 0.00001), (True, 355, 113, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(3.14159265358979, 0.001), (True, 333, 106, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(3.14159265358979, 0.1), (True, 22, 7, 1)) + self.assertEqual(QgsFractionNumericFormat.doubleToVulgarFraction(3.14159265358979, 1), (True, 3, 1, 1)) + + def testToUnicodeSuperscript(self): + self.assertEqual(QgsFractionNumericFormat.toUnicodeSuperscript(''), '') + self.assertEqual(QgsFractionNumericFormat.toUnicodeSuperscript('asd'), 'asd') + self.assertEqual(QgsFractionNumericFormat.toUnicodeSuperscript('1234567890'), '¹²³⁴⁵⁶⁷⁸⁹⁰') + self.assertEqual(QgsFractionNumericFormat.toUnicodeSuperscript('aa112233bbcc'), 'aa¹¹²²³³bbcc') + + def testToUnicodeSubcript(self): + self.assertEqual(QgsFractionNumericFormat.toUnicodeSubscript(''), '') + self.assertEqual(QgsFractionNumericFormat.toUnicodeSubscript('asd'), 'asd') + self.assertEqual(QgsFractionNumericFormat.toUnicodeSubscript('1234567890'), '₁₂₃₄₅₆₇₈₉₀') + self.assertEqual(QgsFractionNumericFormat.toUnicodeSubscript('aa112233bbcc'), 'aa₁₁₂₂₃₃bbcc') + + def testFractionFormat(self): + """ test fraction formatter """ + f = QgsFractionNumericFormat() + f.setUseUnicodeSuperSubscript(False) + context = QgsNumericFormatContext() + self.assertEqual(f.formatDouble(0, context), '0') + self.assertEqual(f.formatDouble(5, context), '5') + self.assertEqual(f.formatDouble(5.5, context), '5 1/2') + self.assertEqual(f.formatDouble(-5, context), '-5') + self.assertEqual(f.formatDouble(-5.5, context), '-5 1/2') + self.assertEqual(f.formatDouble(-55555555.5, context), '-55,555,555 1/2') + context.setThousandsSeparator('⚡') + self.assertEqual(f.formatDouble(-55555555.5, context), '-55⚡555⚡555 1/2') + f.setShowThousandsSeparator(False) + self.assertEqual(f.formatDouble(-55555555.5, context), '-55555555 1/2') + f.setShowPlusSign(True) + self.assertEqual(f.formatDouble(0, context), '0') + self.assertEqual(f.formatDouble(5, context), '+5') + self.assertEqual(f.formatDouble(-5, context), '-5') + self.assertEqual(f.formatDouble(5.5, context), '+5 1/2') + self.assertEqual(f.formatDouble(-5.5, context), '-5 1/2') + self.assertEqual(f.formatDouble(55555555.5, context), '+55555555 1/2') + self.assertEqual(f.formatDouble(55555555.123456, context), '+55555555 5797/46956') + self.assertEqual(f.formatDouble(-5.5, context), '-5 1/2') + self.assertEqual(f.formatDouble(-55555555.5, context), '-55555555 1/2') + context.setPositiveSign('w') + self.assertEqual(f.formatDouble(5, context), 'w5') + self.assertEqual(f.formatDouble(-5, context), '-5') + self.assertEqual(f.formatDouble(5.5, context), 'w5 1/2') + + f.setShowPlusSign(False) + f.setUseDedicatedUnicodeCharacters(True) + self.assertEqual(f.formatDouble(0, context), '0') + self.assertEqual(f.formatDouble(5, context), '5') + self.assertEqual(f.formatDouble(5.5, context), '5 ½') + self.assertEqual(f.formatDouble(-5, context), '-5') + self.assertEqual(f.formatDouble(-5.5, context), '-5 ½') + self.assertEqual(f.formatDouble(5.333333333333333333333333333, context), '5 ⅓') + self.assertEqual(f.formatDouble(5.666666666666666666666666666, context), '5 ⅔') + self.assertEqual(f.formatDouble(5.25, context), '5 ¼') + self.assertEqual(f.formatDouble(5.75, context), '5 ¾') + self.assertEqual(f.formatDouble(5.2, context), '5 ⅕') + self.assertEqual(f.formatDouble(5.4, context), '5 ⅖') + self.assertEqual(f.formatDouble(5.6, context), '5 ⅗') + self.assertEqual(f.formatDouble(5.8, context), '5 ⅘') + self.assertEqual(f.formatDouble(5.1666666666666666666666666666666666, context), '5 ⅙') + self.assertEqual(f.formatDouble(5.8333333333333333333333333333333333, context), '5 ⅚') + self.assertEqual(f.formatDouble(5.14285714285714285, context), '5 ⅐') + self.assertEqual(f.formatDouble(5.125, context), '5 ⅛') + self.assertEqual(f.formatDouble(5.375, context), '5 ⅜') + self.assertEqual(f.formatDouble(5.625, context), '5 ⅝') + self.assertEqual(f.formatDouble(5.875, context), '5 ⅞') + self.assertEqual(f.formatDouble(5.1111111111111111, context), '5 ⅑') + self.assertEqual(f.formatDouble(5.1, context), '5 ⅒') + self.assertEqual(f.formatDouble(5.13131313133, context), '5 13/99') + + f.setUseUnicodeSuperSubscript(True) + self.assertEqual(f.formatDouble(0, context), '0') + self.assertEqual(f.formatDouble(5, context), '5') + self.assertEqual(f.formatDouble(5.5, context), '5 ½') + self.assertEqual(f.formatDouble(-5, context), '-5') + self.assertEqual(f.formatDouble(-5.5, context), '-5 ½') + self.assertEqual(f.formatDouble(5.55555555, context), '5 ¹¹¹¹¹¹¹¹/₂₀₀₀₀₀₀₀') + self.assertEqual(f.formatDouble(-5.55555555, context), '-5 ¹¹¹¹¹¹¹¹/₂₀₀₀₀₀₀₀') + self.assertEqual(f.formatDouble(0.555, context), '¹¹¹/₂₀₀') + + f.setShowPlusSign(True) + f.setUseUnicodeSuperSubscript(False) + + f2 = f.clone() + self.assertIsInstance(f2, QgsFractionNumericFormat) + + self.assertEqual(f2.showPlusSign(), f.showPlusSign()) + self.assertEqual(f2.showThousandsSeparator(), f.showThousandsSeparator()) + self.assertEqual(f2.thousandsSeparator(), f.thousandsSeparator()) + self.assertEqual(f2.useDedicatedUnicodeCharacters(), f.useDedicatedUnicodeCharacters()) + self.assertEqual(f2.useUnicodeSuperSubscript(), f.useUnicodeSuperSubscript()) + + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + f2.writeXml(elem, doc, QgsReadWriteContext()) + + f3 = QgsNumericFormatRegistry().createFromXml(elem, QgsReadWriteContext()) + self.assertIsInstance(f3, QgsFractionNumericFormat) + + self.assertEqual(f3.showPlusSign(), f.showPlusSign()) + self.assertEqual(f3.showThousandsSeparator(), f.showThousandsSeparator()) + self.assertEqual(f3.thousandsSeparator(), f.thousandsSeparator()) + self.assertEqual(f3.useDedicatedUnicodeCharacters(), f.useDedicatedUnicodeCharacters()) + self.assertEqual(f3.useUnicodeSuperSubscript(), f.useUnicodeSuperSubscript()) + def testRegistry(self): registry = QgsNumericFormatRegistry() self.assertTrue(registry.formats()) diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py index 356c8b38da6c..d2f375730e78 100644 --- a/tests/src/python/test_qgsproject.py +++ b/tests/src/python/test_qgsproject.py @@ -1018,6 +1018,13 @@ def testHomePath(self): scope = QgsExpressionContextUtils.projectScope(p) self.assertEqual(scope.variable('project_home'), '../home') + p = QgsProject() + path_changed_spy = QSignalSpy(p.homePathChanged) + p.setFileName('/tmp/not/existing/here/path.qgz') + self.assertFalse(p.presetHomePath()) + self.assertEqual(p.homePath(), '/tmp/not/existing/here') + self.assertEqual(len(path_changed_spy), 1) + def testDirtyBlocker(self): # first test manual QgsProjectDirtyBlocker construction p = QgsProject() diff --git a/tests/src/python/test_qgsproviderconnection_base.py b/tests/src/python/test_qgsproviderconnection_base.py index 233915ea4503..0925dc77c996 100644 --- a/tests/src/python/test_qgsproviderconnection_base.py +++ b/tests/src/python/test_qgsproviderconnection_base.py @@ -29,6 +29,8 @@ QgsField, QgsAbstractDatabaseProviderConnection, QgsProviderConnectionException, + QgsFeature, + QgsGeometry, ) from qgis.PyQt import QtCore from qgis.PyQt.QtTest import QSignalSpy @@ -59,20 +61,13 @@ def setUp(self): def _test_save_load(self, md, uri): """Common tests on connection save and load""" - conn = md.createConnection(self.uri, {}) - created_spy = QSignalSpy(md.connectionCreated) - changed_spy = QSignalSpy(md.connectionChanged) + + conn = md.createConnection(uri, {}) + md.saveConnection(conn, 'qgis_test1') # Check that we retrieve the new connection self.assertTrue('qgis_test1' in md.connections().keys()) self.assertTrue('qgis_test1' in md.dbConnections().keys()) - self.assertEqual(len(created_spy), 1) - self.assertEqual(len(changed_spy), 0) - - # if we try to save again, the connectionChanged signal should be emitted instead of connectionCreated - md.saveConnection(conn, 'qgis_test1') - self.assertEqual(len(created_spy), 1) - self.assertEqual(len(changed_spy), 1) return md.connections()['qgis_test1'] @@ -95,59 +90,94 @@ def _test_operations(self, md, conn): # Schema operations if (capabilities & QgsAbstractDatabaseProviderConnection.CreateSchema and capabilities & QgsAbstractDatabaseProviderConnection.Schemas and - capabilities & QgsAbstractDatabaseProviderConnection.RenameSchema and capabilities & QgsAbstractDatabaseProviderConnection.DropSchema): - if capabilities & QgsAbstractDatabaseProviderConnection.DropSchema and 'myNewSchema' in conn.schemas(): + + # Start clean + if 'myNewSchema' in conn.schemas(): conn.dropSchema('myNewSchema', True) + # Create conn.createSchema('myNewSchema') schemas = conn.schemas() self.assertTrue('myNewSchema' in schemas) + # Create again with self.assertRaises(QgsProviderConnectionException) as ex: conn.createSchema('myNewSchema') - # Rename - conn.renameSchema('myNewSchema', 'myVeryNewSchema') + + # Test rename + if capabilities & QgsAbstractDatabaseProviderConnection.RenameSchema: + # Rename + conn.renameSchema('myNewSchema', 'myVeryNewSchema') + schemas = conn.schemas() + self.assertTrue('myVeryNewSchema' in schemas) + self.assertFalse('myNewSchema' in schemas) + conn.renameSchema('myVeryNewSchema', 'myNewSchema') + schemas = conn.schemas() + self.assertFalse('myVeryNewSchema' in schemas) + self.assertTrue('myNewSchema' in schemas) + + # Drop + conn.dropSchema('myNewSchema') schemas = conn.schemas() - self.assertTrue('myVeryNewSchema' in schemas) self.assertFalse('myNewSchema' in schemas) - # Drop - conn.dropSchema('myVeryNewSchema') + + #UTF8 schema + conn.createSchema('myUtf8\U0001f604NewSchema') + schemas = conn.schemas() + conn.dropSchema('myUtf8\U0001f604NewSchema') schemas = conn.schemas() - self.assertFalse('myVeryNewSchema' in schemas) + self.assertFalse('myUtf8\U0001f604NewSchema' in schemas) # Table operations if (capabilities & QgsAbstractDatabaseProviderConnection.CreateVectorTable and capabilities & QgsAbstractDatabaseProviderConnection.Tables and - capabilities & QgsAbstractDatabaseProviderConnection.RenameVectorTable and capabilities & QgsAbstractDatabaseProviderConnection.DropVectorTable): - if capabilities & QgsAbstractDatabaseProviderConnection.DropSchema and 'myNewSchema' in conn.schemas(): - conn.dropSchema('myNewSchema', True) if capabilities & QgsAbstractDatabaseProviderConnection.CreateSchema: schema = 'myNewSchema' conn.createSchema('myNewSchema') else: schema = 'public' + # Start clean if 'myNewTable' in self._table_names(conn.tables(schema)): conn.dropVectorTable(schema, 'myNewTable') + fields = QgsFields() - fields.append(QgsField("string", QVariant.String)) - fields.append(QgsField("long", QVariant.LongLong)) - fields.append(QgsField("double", QVariant.Double)) - fields.append(QgsField("integer", QVariant.Int)) - fields.append(QgsField("date", QVariant.Date)) - fields.append(QgsField("datetime", QVariant.DateTime)) - fields.append(QgsField("time", QVariant.Time)) + fields.append(QgsField("string_t", QVariant.String)) + fields.append(QgsField("long_t", QVariant.LongLong)) + fields.append(QgsField("double_t", QVariant.Double)) + fields.append(QgsField("integer_t", QVariant.Int)) + fields.append(QgsField("date_t", QVariant.Date)) + fields.append(QgsField("datetime_t", QVariant.DateTime)) + fields.append(QgsField("time_t", QVariant.Time)) options = {} crs = QgsCoordinateReferenceSystem.fromEpsgId(3857) typ = QgsWkbTypes.LineString + # Create conn.createVectorTable(schema, 'myNewTable', fields, typ, crs, True, options) table_names = self._table_names(conn.tables(schema)) self.assertTrue('myNewTable' in table_names) + # Create UTF8 table + conn.createVectorTable(schema, 'myUtf8\U0001f604Table', fields, typ, crs, True, options) + table_names = self._table_names(conn.tables(schema)) + self.assertTrue('myNewTable' in table_names) + self.assertTrue('myUtf8\U0001f604Table' in table_names) + conn.dropVectorTable(schema, 'myUtf8\U0001f604Table') + table_names = self._table_names(conn.tables(schema)) + self.assertFalse('myUtf8\U0001f604Table' in table_names) + self.assertTrue('myNewTable' in table_names) + + # insert something, because otherwise MSSQL cannot guess + if self.providerKey == 'mssql': + f = QgsFeature(fields) + f.setGeometry(QgsGeometry.fromWkt('LineString (-72.345 71.987, -80 80)')) + vl = QgsVectorLayer(conn.tableUri('myNewSchema', 'myNewTable'), 'vl', 'mssql') + vl.dataProvider().addFeatures([f]) + # Check table information table_properties = conn.tables(schema) table_property = self._table_by_name(table_properties, 'myNewTable') @@ -171,6 +201,7 @@ def _test_operations(self, md, conn): self.assertEqual(table_property.geometryColumn(), '') self.assertEqual(table_property.defaultName(), 'myNewAspatialTable') cols = table_property.geometryColumnTypes() + # We always return geom col types, even when there is no geometry self.assertEqual(cols[0].wkbType, QgsWkbTypes.NoGeometry) self.assertFalse(cols[0].crs.isValid()) self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Raster) @@ -184,23 +215,30 @@ def _test_operations(self, md, conn): table = "\"%s\".\"myNewAspatialTable\"" % schema else: table = 'myNewAspatialTable' - sql = "INSERT INTO %s (string, long, double, integer, date, datetime, time) VALUES ('QGIS Rocks - \U0001f604', 666, 1.234, 1234, '2019-07-08', '2019-07-08T12:00:12', '12:00:13.00')" % table + + # MSSQL literal syntax for UTF8 requires 'N' prefix + sql = "INSERT INTO %s (string_t, long_t, double_t, integer_t, date_t, datetime_t, time_t) VALUES (%s'QGIS Rocks - \U0001f604', 666, 1.234, 1234, '2019-07-08', '2019-07-08T12:00:12', '12:00:13.00')" % (table, 'N' if self.providerKey == 'mssql' else '') res = conn.executeSql(sql) self.assertEqual(res, []) - sql = "SELECT string, long, double, integer, date, datetime FROM %s" % table + sql = "SELECT string_t, long_t, double_t, integer_t, date_t, datetime_t FROM %s" % table res = conn.executeSql(sql) # GPKG has no type for time and spatialite has no support for dates and time ... if self.providerKey == 'spatialite': self.assertEqual(res, [['QGIS Rocks - \U0001f604', 666, 1.234, 1234, '2019-07-08', '2019-07-08T12:00:12']]) else: self.assertEqual(res, [['QGIS Rocks - \U0001f604', 666, 1.234, 1234, QtCore.QDate(2019, 7, 8), QtCore.QDateTime(2019, 7, 8, 12, 0, 12)]]) - sql = "SELECT time FROM %s" % table + sql = "SELECT time_t FROM %s" % table res = conn.executeSql(sql) - self.assertIn(res, ([[QtCore.QTime(12, 0, 13)]], [['12:00:13.00']])) - sql = "DELETE FROM %s WHERE string = 'QGIS Rocks - \U0001f604'" % table + + # This does not work in MSSQL and returns a QByteArray, we have no way to know that it is a time + # value and there is no way we can convert it. + if self.providerKey != 'mssql': + self.assertIn(res, ([[QtCore.QTime(12, 0, 13)]], [['12:00:13.00']])) + + sql = "DELETE FROM %s WHERE string_t = %s'QGIS Rocks - \U0001f604'" % (table, 'N' if self.providerKey == 'mssql' else '') res = conn.executeSql(sql) self.assertEqual(res, []) - sql = "SELECT string, integer FROM %s" % table + sql = "SELECT string_t, integer_t FROM %s" % table res = conn.executeSql(sql) self.assertEqual(res, []) @@ -210,7 +248,7 @@ def _test_operations(self, md, conn): self.assertFalse('myNewAspatialTable' in table_names) # Query for rasters (in qgis_test schema or no schema for GPKG, spatialite has no support) - if self.providerKey != 'spatialite': + if self.providerKey not in ('spatialite', 'mssql'): table_properties = conn.tables('qgis_test', QgsAbstractDatabaseProviderConnection.Raster) # At least one raster should be there (except for spatialite) self.assertTrue(len(table_properties) >= 1) @@ -219,14 +257,46 @@ def _test_operations(self, md, conn): self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Vector) self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Aspatial) - # Rename - conn.renameVectorTable(schema, 'myNewTable', 'myVeryNewTable') - tables = self._table_names(conn.tables(schema)) - self.assertFalse('myNewTable' in tables) - self.assertTrue('myVeryNewTable' in tables) + if capabilities & QgsAbstractDatabaseProviderConnection.RenameVectorTable: + # Rename + conn.renameVectorTable(schema, 'myNewTable', 'myVeryNewTable') + tables = self._table_names(conn.tables(schema)) + self.assertFalse('myNewTable' in tables) + self.assertTrue('myVeryNewTable' in tables) + # Rename it back + conn.renameVectorTable(schema, 'myVeryNewTable', 'myNewTable') + tables = self._table_names(conn.tables(schema)) + self.assertTrue('myNewTable' in tables) + self.assertFalse('myVeryNewTable' in tables) + # Vacuum if capabilities & QgsAbstractDatabaseProviderConnection.Vacuum: - conn.vacuum('myNewSchema', 'myVeryNewTable') + conn.vacuum('myNewSchema', 'myNewTable') + + # Spatial index + spatial_index_exists = False + # we don't initially know if a spatial index exists -- some formats may create them by default, others not + if capabilities & QgsAbstractDatabaseProviderConnection.SpatialIndexExists: + spatial_index_exists = conn.spatialIndexExists('myNewSchema', 'myNewTable', 'geom') + if capabilities & QgsAbstractDatabaseProviderConnection.DeleteSpatialIndex: + if spatial_index_exists: + conn.deleteSpatialIndex('myNewSchema', 'myNewTable', 'geom') + if capabilities & QgsAbstractDatabaseProviderConnection.SpatialIndexExists: + self.assertFalse(conn.spatialIndexExists('myNewSchema', 'myNewTable', 'geom')) + + if capabilities & QgsAbstractDatabaseProviderConnection.CreateSpatialIndex: + options = QgsAbstractDatabaseProviderConnection.SpatialIndexOptions() + options.geometryColumnName = 'geom' + conn.createSpatialIndex('myNewSchema', 'myNewTable', options) + + if capabilities & QgsAbstractDatabaseProviderConnection.SpatialIndexExists: + self.assertTrue(conn.spatialIndexExists('myNewSchema', 'myNewTable', 'geom')) + + # now we know for certain a spatial index exists, let's retry dropping it + if capabilities & QgsAbstractDatabaseProviderConnection.DeleteSpatialIndex: + conn.deleteSpatialIndex('myNewSchema', 'myNewTable', 'geom') + if capabilities & QgsAbstractDatabaseProviderConnection.SpatialIndexExists: + self.assertFalse(conn.spatialIndexExists('myNewSchema', 'myNewTable', 'geom')) if capabilities & QgsAbstractDatabaseProviderConnection.DropSchema: # Drop schema (should fail) @@ -234,7 +304,7 @@ def _test_operations(self, md, conn): conn.dropSchema('myNewSchema') # Check some column types operations - table = self._table_by_name(conn.tables(schema), 'myVeryNewTable') + table = self._table_by_name(conn.tables(schema), 'myNewTable') self.assertEqual(len(table.geometryColumnTypes()), 1) ct = table.geometryColumnTypes()[0] self.assertEqual(ct.crs, QgsCoordinateReferenceSystem.fromEpsgId(3857)) @@ -256,10 +326,10 @@ def _test_operations(self, md, conn): self.assertEqual(ct.wkbType, QgsWkbTypes.LineString) # Drop table - conn.dropVectorTable(schema, 'myVeryNewTable') + conn.dropVectorTable(schema, 'myNewTable') conn.dropVectorTable(schema, 'myNewAspatialTable') table_names = self._table_names(conn.tables(schema)) - self.assertFalse('myVeryNewTable' in table_names) + self.assertFalse('myNewTable' in table_names) if capabilities & QgsAbstractDatabaseProviderConnection.DropSchema: # Drop schema @@ -295,8 +365,21 @@ def test_errors(self): def test_connections(self): """Main test""" + md = QgsProviderRegistry.instance().providerMetadata(self.providerKey) # Run common tests + created_spy = QSignalSpy(md.connectionCreated) + changed_spy = QSignalSpy(md.connectionChanged) + conn = self._test_save_load(md, self.uri) + + self.assertEqual(len(created_spy), 1) + self.assertEqual(len(changed_spy), 0) + + # if we try to save again, the connectionChanged signal should be emitted instead of connectionCreated + md.saveConnection(conn, 'qgis_test1') + self.assertEqual(len(created_spy), 1) + self.assertEqual(len(changed_spy), 1) + self._test_operations(md, conn) diff --git a/tests/src/python/test_qgsproviderconnection_mssql.py b/tests/src/python/test_qgsproviderconnection_mssql.py new file mode 100644 index 000000000000..65b01d3d1f62 --- /dev/null +++ b/tests/src/python/test_qgsproviderconnection_mssql.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for MSSQL QgsAbastractProviderConnection API. + +.. note:: 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__ = 'Alessandro Pasotti' +__date__ = '12/03/2020' +__copyright__ = 'Copyright 2019, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import os +from test_qgsproviderconnection_base import TestPyQgsProviderConnectionBase +from qgis.core import ( + QgsWkbTypes, + QgsAbstractDatabaseProviderConnection, + QgsProviderConnectionException, + QgsVectorLayer, + QgsProviderRegistry, + QgsCoordinateReferenceSystem, + QgsRasterLayer, + QgsDataSourceUri, +) +from qgis.testing import unittest +from osgeo import gdal +from qgis.PyQt.QtCore import QTemporaryDir + + +class TestPyQgsProviderConnectionMssql(unittest.TestCase, TestPyQgsProviderConnectionBase): + + # Provider test cases must define the string URI for the test + uri = '' + # Provider test cases must define the provider name (e.g. "postgres" or "ogr") + providerKey = 'mssql' + + @classmethod + def setUpClass(cls): + """Run before all tests""" + + TestPyQgsProviderConnectionBase.setUpClass() + + # These are the connection details for the SQL Server instance running on Travis + cls.dbconn = "service='testsqlserver' user=sa password='' " + if 'QGIS_MSSQLTEST_DB' in os.environ: + cls.dbconn = os.environ['QGIS_MSSQLTEST_DB'] + + cls.uri = cls.dbconn + + try: + md = QgsProviderRegistry.instance().providerMetadata('mssql') + conn = md.createConnection(cls.uri, {}) + conn.executeSql('drop schema [myNewSchema]') + except: + pass + + def test_confguration(self): + """Test storage and retrieval for configuration parameters""" + + uri = 'dbname=\'qgis_test\' service=\'driver={SQL Server};server=localhost;port=1433;database=qgis_test\' user=\'sa\' password=\'\' srid=4326 type=Point estimatedMetadata=\'true\' disableInvalidGeometryHandling=\'1\' table="qgis_test"."someData" (geom)' + md = QgsProviderRegistry.instance().providerMetadata('mssql') + conn = md.createConnection(uri, {}) + ds_uri = QgsDataSourceUri(conn.uri()) + self.assertEqual(ds_uri.username(), 'sa') + self.assertEqual(ds_uri.database(), 'qgis_test') + self.assertEqual(ds_uri.table(), '') + self.assertEqual(ds_uri.schema(), '') + self.assertEqual(ds_uri.geometryColumn(), '') + self.assertTrue(ds_uri.useEstimatedMetadata()) + self.assertEqual(ds_uri.srid(), '') + self.assertEqual(ds_uri.password(), '') + self.assertEqual(ds_uri.param('disableInvalidGeometryHandling'), '1') + + conn.store('coronavirus') + conn = md.findConnection('coronavirus', False) + ds_uri = QgsDataSourceUri(conn.uri()) + self.assertEqual(ds_uri.username(), 'sa') + self.assertEqual(ds_uri.database(), 'qgis_test') + self.assertEqual(ds_uri.table(), '') + self.assertEqual(ds_uri.schema(), '') + self.assertTrue(ds_uri.useEstimatedMetadata()) + self.assertEqual(ds_uri.geometryColumn(), '') + self.assertEqual(ds_uri.srid(), '') + self.assertEqual(ds_uri.password(), '') + self.assertEqual(ds_uri.param('disableInvalidGeometryHandling'), 'true') + conn.remove('coronavirus') + + def test_mssql_connections_from_uri(self): + """Create a connection from a layer uri and retrieve it""" + + md = QgsProviderRegistry.instance().providerMetadata('mssql') + + def test_table_uri(self): + """Create a connection from a layer uri and create a table URI""" + + md = QgsProviderRegistry.instance().providerMetadata('mssql') + conn = md.createConnection(self.uri, {}) + vl = QgsVectorLayer(conn.tableUri('qgis_test', 'someData'), 'my', 'mssql') + self.assertTrue(vl.isValid()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsproviderconnection_postgres.py b/tests/src/python/test_qgsproviderconnection_postgres.py index 4e37b2cb8891..c817bf735bf3 100644 --- a/tests/src/python/test_qgsproviderconnection_postgres.py +++ b/tests/src/python/test_qgsproviderconnection_postgres.py @@ -69,6 +69,13 @@ def test_postgis_connections_from_uri(self): rl = QgsRasterLayer(conn.tableUri('qgis_test', 'Raster1'), 'r1', 'postgresraster') self.assertTrue(rl.isValid()) + def test_postgis_geometry_filter(self): + """Make sure the postgres provider only returns one matching geometry record and no polygons etc.""" + vl = QgsVectorLayer(self.postgres_conn + ' srid=4326 type=POINT table="qgis_test"."geometries_table" (geom) sql=', 'test', 'postgres') + + ids = [f.id() for f in vl.getFeatures()] + self.assertEqual(ids, [2]) + def test_postgis_table_uri(self): """Create a connection from a layer uri and create a table URI""" diff --git a/tests/src/python/test_qgsproviderconnection_spatialite.py b/tests/src/python/test_qgsproviderconnection_spatialite.py index ccc5405c5b3e..fdc7fed52f7c 100644 --- a/tests/src/python/test_qgsproviderconnection_spatialite.py +++ b/tests/src/python/test_qgsproviderconnection_spatialite.py @@ -15,6 +15,7 @@ import os import shutil +import tempfile from test_qgsproviderconnection_base import TestPyQgsProviderConnectionBase from qgis.core import ( QgsWkbTypes, @@ -43,8 +44,9 @@ class TestPyQgsProviderConnectionSpatialite(unittest.TestCase, TestPyQgsProvider def setUpClass(cls): """Run before all tests""" TestPyQgsProviderConnectionBase.setUpClass() + cls.basetestpath = tempfile.mkdtemp() spatialite_original_path = '{}/qgis_server/test_project_wms_grouped_layers.sqlite'.format(TEST_DATA_DIR) - cls.spatialite_path = '{}/qgis_server/test_project_wms_grouped_layers_test.sqlite'.format(TEST_DATA_DIR) + cls.spatialite_path = os.path.join(cls.basetestpath, 'test.sqlite') shutil.copy(spatialite_original_path, cls.spatialite_path) cls.uri = "dbname=\'%s\'" % cls.spatialite_path vl = QgsVectorLayer('{} table=\'cdb_lines\''.format(cls.uri), 'test', 'spatialite') diff --git a/tests/src/python/test_qgsproviderconnectioncombobox.py b/tests/src/python/test_qgsproviderconnectioncombobox.py index d9a01191c518..a5a100e5fb56 100644 --- a/tests/src/python/test_qgsproviderconnectioncombobox.py +++ b/tests/src/python/test_qgsproviderconnectioncombobox.py @@ -116,6 +116,20 @@ def testCombo(self): md.deleteConnection('aaa_qgis_test2') + def testComboSetProvider(self): + """ test combobox functionality with empty entry """ + m = QgsProviderConnectionComboBox('ogr') + + md = QgsProviderRegistry.instance().providerMetadata('ogr') + conn = md.createConnection(self.gpkg_path, {}) + md.saveConnection(conn, 'qgis_test_zzz') + + self.assertEqual(m.count(), 1) + m.setProvider('ogr') + self.assertEqual(m.count(), 1) + + md.deleteConnection('qgis_test_zzz') + def testComboWithEmpty(self): """ test combobox functionality with empty entry """ m = QgsProviderConnectionComboBox('ogr') diff --git a/tests/src/python/test_qgsproviderregistry.py b/tests/src/python/test_qgsproviderregistry.py new file mode 100644 index 000000000000..b024d9123a8e --- /dev/null +++ b/tests/src/python/test_qgsproviderregistry.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsProviderRegistry. + +.. note:: 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__ = 'Nyall Dawson' +__date__ = '16/03/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.core import QgsProviderRegistry +from qgis.testing import start_app, unittest + +# Convenience instances in case you may need them +# to find the srs.db +start_app() + + +class TestQgsProviderRegistry(unittest.TestCase): + + def testProviderList(self): + """ + Test provider list + """ + + providers = QgsProviderRegistry.instance().providerList() + self.assertIn('ogr', providers) + self.assertIn('gdal', providers) + + def testProviderMetadata(self): + """ + Test retrieving provider metadata + """ + + providers = QgsProviderRegistry.instance().providerList() + for p in providers: + self.assertTrue(QgsProviderRegistry.instance().providerMetadata(p)) + # should be case-insensitive + self.assertTrue(QgsProviderRegistry.instance().providerMetadata(p.lower())) + self.assertTrue(QgsProviderRegistry.instance().providerMetadata(p.upper())) + + self.assertIsNone(QgsProviderRegistry.instance().providerMetadata('asdasdasdasdasd')) + + def testCreateProvider(self): + """ + Test creating provider instance + """ + providers = QgsProviderRegistry.instance().providerList() + for p in providers: + if p == 'geonode' or p == 'vectortile': + continue + + self.assertTrue(QgsProviderRegistry.instance().createProvider(p, '')) + # should be case-insensitive + self.assertTrue(QgsProviderRegistry.instance().createProvider(p.lower(), '')) + self.assertTrue(QgsProviderRegistry.instance().createProvider(p.upper(), '')) + + self.assertIsNone(QgsProviderRegistry.instance().createProvider('asdasdasdasdasd', '')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrastertransparencywidget.py b/tests/src/python/test_qgsrastertransparencywidget.py new file mode 100644 index 000000000000..33aad119269c --- /dev/null +++ b/tests/src/python/test_qgsrastertransparencywidget.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsRasterRange. + +.. note:: 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__ = 'Nyall Dawson' +__date__ = '07/06/2018' +__copyright__ = 'Copyright 2018, The QGIS Project' + +import qgis # NOQA switch sip api + +import pathlib +from qgis.gui import QgsRasterTransparencyWidget, QgsMapCanvas +from qgis.core import QgsRasterLayer, QgsRasterRange + +from qgis.testing import TestCase, unittest +from qgis.testing.mocked import get_iface + +from utilities import unitTestDataPath + + +class TestQgsRasterTransparencyWidget(TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.iface = get_iface() + + @staticmethod + def no_data_values(layer: QgsRasterLayer): + return [n.min() for n in layer.dataProvider().userNoDataValues(1)] + + def test_transparency_widget(self): + path = pathlib.Path(unitTestDataPath()) / 'landsat_4326.tif' + self.assertTrue(path.is_file()) + layer = QgsRasterLayer(path.as_posix()) + self.assertTrue(layer.isValid()) + canvas = QgsMapCanvas() + canvas.setLayers([layer]) + + no_data_value = -99 + nd_ref = [no_data_value] + layer.dataProvider().setUserNoDataValue(1, [QgsRasterRange(no_data_value, no_data_value)]) + nd0 = self.no_data_values(layer) + self.assertListEqual(nd0, nd_ref) + + w = QgsRasterTransparencyWidget(layer, canvas) + self.assertIsInstance(w, QgsRasterTransparencyWidget) + nd1 = self.no_data_values(layer) + self.assertListEqual(nd1, nd_ref, msg='Widget initialization should not change the "no data value"') + + w.syncToLayer() + nd2 = self.no_data_values(layer) + self.assertListEqual(nd2, nd_ref, msg='syncToLayer changed the "no data value"') + + w.syncToLayer() + nd3 = self.no_data_values(layer) + self.assertListEqual(nd3, nd_ref, msg='repeated syncToLayer changed the "no data value"') + + w.apply() + nd4 = self.no_data_values(layer) + self.assertListEqual(nd4, nd_ref, msg='apply changed the "no data value" but should not') + + w.apply() + nd5 = self.no_data_values(layer) + self.assertListEqual(nd5, nd_ref, msg='repeated apply changed the "no data value" but should not') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsrendererrasterpropertieswidget.py b/tests/src/python/test_qgsrendererrasterpropertieswidget.py new file mode 100644 index 000000000000..142f8dd0885e --- /dev/null +++ b/tests/src/python/test_qgsrendererrasterpropertieswidget.py @@ -0,0 +1,69 @@ +import pathlib +from qgis.testing import start_app, unittest, TestCase +from qgis.testing.mocked import get_iface +from qgis.core import QgsRasterLayer, QgsProject, QgsMultiBandColorRenderer, QgsRasterRenderer, QgsSingleBandGrayRenderer +from qgis.gui import QgsRendererRasterPropertiesWidget, QgsMapCanvas, QgsMultiBandColorRendererWidget, QgsRasterRendererWidget + + +class QgsRendererRasterPropertiesTestCases(TestCase): + + def setUp(self): + self.iface = get_iface() + + def multibandRasterLayer(self) -> QgsRasterLayer: + + try: + from utilities import unitTestDataPath + path = pathlib.Path(unitTestDataPath()) / 'landsat_4326.tif' + except ModuleNotFoundError: + path = pathlib.Path(__file__).parent / 'landsat_4326.tif' + + assert isinstance(path, pathlib.Path) and path.is_file() + lyr = QgsRasterLayer(path.as_posix()) + lyr.setName(path.name) + self.assertIsInstance(lyr, QgsRasterLayer) + self.assertTrue(lyr.isValid()) + self.assertTrue(lyr.bandCount() > 1) + + return lyr + + def test_syncToLayer_SingleBandGray(self): + + lyr = self.multibandRasterLayer() + lyr.setRenderer(QgsSingleBandGrayRenderer(lyr.dataProvider(), 1)) + c = QgsMapCanvas() + w = QgsRendererRasterPropertiesWidget(lyr, c) + assert isinstance(w.currentRenderWidget().renderer(), QgsSingleBandGrayRenderer) + assert w.currentRenderWidget().renderer().grayBand() == 1 + lyr.renderer().setGrayBand(2) + w.syncToLayer(lyr) + assert w.currentRenderWidget().renderer().grayBand() == 2 + + def test_syncToLayer_MultiBand(self): + + lyr = self.multibandRasterLayer() + assert isinstance(lyr.renderer(), QgsMultiBandColorRenderer) + lyr.renderer().setRedBand(1) + lyr.renderer().setGreenBand(2) + lyr.renderer().setBlueBand(3) + + c = QgsMapCanvas() + w = QgsRendererRasterPropertiesWidget(lyr, c) + assert isinstance(w.currentRenderWidget().renderer(), QgsMultiBandColorRenderer) + r = w.currentRenderWidget().renderer() + assert isinstance(r, QgsMultiBandColorRenderer) + assert r.usesBands() == [1, 2, 3] + + lyr.renderer().setRedBand(3) + lyr.renderer().setGreenBand(1) + lyr.renderer().setBlueBand(2) + + w.syncToLayer(lyr) + + r = w.currentRenderWidget().renderer() + assert isinstance(r, QgsMultiBandColorRenderer) + assert r.usesBands() == [3, 1, 2] + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsscalebarrendererregistry.py b/tests/src/python/test_qgsscalebarrendererregistry.py new file mode 100644 index 000000000000..2aae870cb9cc --- /dev/null +++ b/tests/src/python/test_qgsscalebarrendererregistry.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsScaleBarRendererRegistry + +.. note:: 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__ = 'Nyall Dawson' +__date__ = '20/03/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.core import (QgsScaleBarRendererRegistry, + QgsScaleBarRenderer) +from qgis.testing import start_app, unittest + +start_app() + + +class TestRenderer(QgsScaleBarRenderer): + + def id(self): + return 'test' + + def sortKey(self): + return 45 + + def visibleName(self): + return 'TesT' + + def clone(self): + return TestRenderer() + + +class TestQgsScaleBarRendererRegistry(unittest.TestCase): + + def testRegistry(self): + registry = QgsScaleBarRendererRegistry() + self.assertTrue(registry.renderers()) + for f in registry.renderers(): + self.assertEqual(registry.renderer(f).id(), f) + self.assertEqual(registry.visibleName(f), registry.renderer(f).visibleName()) + self.assertEqual(registry.sortKey(f), registry.renderer(f).sortKey()) + + self.assertIsNone(registry.renderer('bad')) + self.assertFalse(registry.visibleName('bad')) + self.assertFalse(registry.sortKey('bad')) + + self.assertIn('Double Box', registry.renderers()) + + registry.addRenderer(TestRenderer()) + self.assertIn('test', registry.renderers()) + self.assertTrue(isinstance(registry.renderer('test'), TestRenderer)) + self.assertEqual(registry.visibleName('test'), 'TesT') + self.assertEqual(registry.sortKey('test'), 45) + + registry.removeRenderer('test') + + self.assertNotIn('test', registry.renderers()) + self.assertIsNone(registry.renderer('test')) + self.assertFalse(registry.visibleName('test')) + + registry.removeRenderer('test') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsstringstatisticalsummary.py b/tests/src/python/test_qgsstringstatisticalsummary.py index 103da85808e1..3007e07b5648 100644 --- a/tests/src/python/test_qgsstringstatisticalsummary.py +++ b/tests/src/python/test_qgsstringstatisticalsummary.py @@ -24,14 +24,14 @@ def testStats(self): # added one-at-a-time s = QgsStringStatisticalSummary() self.assertEqual(s.statistics(), QgsStringStatisticalSummary.All) - strings = ['cc', 'aaaa', 'bbbbbbbb', 'aaaa', 'eeee', '', 'eeee', '', 'dddd'] + strings = ['cc', 'aaaa', 'bbbbbbbb', 'aaaa', 'eeee', '', 'eeee', 'aaaa', '', 'dddd'] s.calculate(strings) s2 = QgsStringStatisticalSummary() for string in strings: s2.addString(string) s2.finalize() - self.assertEqual(s.count(), 9) - self.assertEqual(s2.count(), 9) + self.assertEqual(s.count(), 10) + self.assertEqual(s2.count(), 10) self.assertEqual(s.countDistinct(), 6) self.assertEqual(s2.countDistinct(), 6) self.assertEqual(set(s.distinctValues()), set(['cc', 'aaaa', 'bbbbbbbb', 'eeee', 'dddd', ''])) @@ -46,8 +46,12 @@ def testStats(self): self.assertEqual(s2.minLength(), 0) self.assertEqual(s.maxLength(), 8) self.assertEqual(s2.maxLength(), 8) - self.assertEqual(s.meanLength(), 3.33333333333333333333333) - self.assertEqual(s2.meanLength(), 3.33333333333333333333333) + self.assertEqual(s.meanLength(), 3.4) + self.assertEqual(s2.meanLength(), 3.4) + self.assertEqual(s.minority(), 'bbbbbbbb') + self.assertEqual(s2.minority(), 'bbbbbbbb') + self.assertEqual(s.majority(), 'aaaa') + self.assertEqual(s2.majority(), 'aaaa') #extra check for minLength without empty strings s.calculate(['1111111', '111', '11111']) @@ -56,14 +60,16 @@ def testStats(self): def testIndividualStats(self): # tests calculation of statistics one at a time, to make sure statistic calculations are not # dependent on each other - tests = [{'stat': QgsStringStatisticalSummary.Count, 'expected': 9}, + tests = [{'stat': QgsStringStatisticalSummary.Count, 'expected': 10}, {'stat': QgsStringStatisticalSummary.CountDistinct, 'expected': 6}, {'stat': QgsStringStatisticalSummary.CountMissing, 'expected': 2}, {'stat': QgsStringStatisticalSummary.Min, 'expected': 'aaaa'}, {'stat': QgsStringStatisticalSummary.Max, 'expected': 'eeee'}, {'stat': QgsStringStatisticalSummary.MinimumLength, 'expected': 0}, {'stat': QgsStringStatisticalSummary.MaximumLength, 'expected': 8}, - {'stat': QgsStringStatisticalSummary.MeanLength, 'expected': 3.3333333333333335}, + {'stat': QgsStringStatisticalSummary.MeanLength, 'expected': 3.4}, + {'stat': QgsStringStatisticalSummary.Minority, 'expected': 'bbbbbbbb'}, + {'stat': QgsStringStatisticalSummary.Majority, 'expected': 'aaaa'}, ] s = QgsStringStatisticalSummary() @@ -77,7 +83,7 @@ def testIndividualStats(self): s3.setStatistics(t['stat']) self.assertEqual(s.statistics(), t['stat']) - strings = ['cc', 'aaaa', 'bbbbbbbb', 'aaaa', 'eeee', '', 'eeee', '', 'dddd'] + strings = ['cc', 'aaaa', 'bbbbbbbb', 'aaaa', 'eeee', '', 'eeee', 'aaaa', '', 'dddd'] s.calculate(strings) s3.reset() for string in strings: diff --git a/tests/src/python/test_qgsstringutils.py b/tests/src/python/test_qgsstringutils.py index d78bc8771f69..d5bbbf8add55 100644 --- a/tests/src/python/test_qgsstringutils.py +++ b/tests/src/python/test_qgsstringutils.py @@ -175,9 +175,50 @@ def testCapitalizeFirst(self): self.assertEqual(QgsStringUtils.capitalize(' testing abc', QgsStringUtils.ForceFirstLetterToCapital), ' Testing Abc') - def testSubstituteVerticalCharacters(self): - """ test substitute vertical characters """ - self.assertEqual(QgsStringUtils.substituteVerticalCharacters('123{[(45654)]}321'), '123︷﹇︵45654︶﹈︸321') + def testfuzzyScore(self): + self.assertEqual(QgsStringUtils.fuzzyScore('', ''), 0) + self.assertEqual(QgsStringUtils.fuzzyScore('foo', ''), 0) + self.assertEqual(QgsStringUtils.fuzzyScore('', 'foo'), 0) + self.assertEqual(QgsStringUtils.fuzzyScore('foo', 'foo'), 1) + self.assertEqual(QgsStringUtils.fuzzyScore('bar', 'foo'), 0) + self.assertEqual(QgsStringUtils.fuzzyScore('FOO', 'foo'), 1) + self.assertEqual(QgsStringUtils.fuzzyScore('foo', 'FOO'), 1) + self.assertEqual(QgsStringUtils.fuzzyScore(' foo ', 'foo'), 1) + self.assertEqual(QgsStringUtils.fuzzyScore('foo', ' foo '), 1) + self.assertEqual(QgsStringUtils.fuzzyScore('foo', ' foo '), 1) + self.assertEqual(QgsStringUtils.fuzzyScore('foo_bar', 'foo bar'), 1) + self.assertGreater(QgsStringUtils.fuzzyScore('foo bar', 'foo'), 0) + self.assertGreater(QgsStringUtils.fuzzyScore('foo bar', 'fooba'), 0) + self.assertGreater(QgsStringUtils.fuzzyScore('foo_bar', 'ob'), 0) + self.assertGreater(QgsStringUtils.fuzzyScore('foo bar', 'foobar'), 0) + self.assertGreater(QgsStringUtils.fuzzyScore('foo bar', 'foo_bar'), 0) + self.assertGreater(QgsStringUtils.fuzzyScore('foo_bar', 'foo bar'), 0) + self.assertEqual( + QgsStringUtils.fuzzyScore('foo bar', 'foobar'), + QgsStringUtils.fuzzyScore('foo_bar', 'foobar') + ) + self.assertEqual( + QgsStringUtils.fuzzyScore('foo bar', 'foo_bar'), + QgsStringUtils.fuzzyScore('foo_bar', 'foo_bar') + ) + self.assertEqual( + QgsStringUtils.fuzzyScore('foo bar', 'foo bar'), + QgsStringUtils.fuzzyScore('foo_bar', 'foo bar') + ) + # note the accent + self.assertEqual( + QgsStringUtils.fuzzyScore('foo_bér', 'foober'), + QgsStringUtils.fuzzyScore('foo_ber', 'foobér') + ) + self.assertGreater( + QgsStringUtils.fuzzyScore('abcd efg hig', 'abcd hig'), + QgsStringUtils.fuzzyScore('abcd efg hig', 'abcd e h') + ) + # full words are preferred, even though the same number of characters used + self.assertGreater( + QgsStringUtils.fuzzyScore('abcd efg hig', 'abcd hig'), + QgsStringUtils.fuzzyScore('abcd efg hig', 'abcd e hi') + ) if __name__ == '__main__': diff --git a/tests/src/python/test_qgstemporalutils.py b/tests/src/python/test_qgstemporalutils.py new file mode 100644 index 000000000000..9d39adb96c92 --- /dev/null +++ b/tests/src/python/test_qgstemporalutils.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsTemporalUtils. + +.. note:: 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__ = 'Nyall Dawson' +__date__ = '13/3/2020' +__copyright__ = 'Copyright 2020, The QGIS Project' + +import qgis # NOQA + +from qgis.core import (QgsProject, + QgsTemporalUtils, + QgsRasterLayer, + QgsDateTimeRange, + QgsDateTimeRange) + +from qgis.PyQt.QtCore import (QDate, + QTime, + QDateTime, + Qt) + +from qgis.testing import start_app, unittest +from utilities import (unitTestDataPath) + +app = start_app() +TEST_DATA_DIR = unitTestDataPath() + + +class TestQgsTemporalUtils(unittest.TestCase): + + def testTemporalRangeForProject(self): + p = QgsProject() + r1 = QgsRasterLayer('', '', 'wms') + r2 = QgsRasterLayer('', '', 'wms') + r3 = QgsRasterLayer('', '', 'wms') + r1.temporalProperties().setIsActive(True) + r1.temporalProperties().setFixedTemporalRange(QgsDateTimeRange(QDateTime(QDate(2020, 1, 1), QTime(), Qt.UTC), + QDateTime(QDate(2020, 3, 31), QTime(), Qt.UTC))) + r2.temporalProperties().setIsActive(True) + r2.temporalProperties().setFixedTemporalRange(QgsDateTimeRange(QDateTime(QDate(2020, 4, 1), QTime(), Qt.UTC), + QDateTime(QDate(2020, 7, 31), QTime(), Qt.UTC))) + r3.temporalProperties().setIsActive(True) + r3.temporalProperties().setFixedTemporalRange(QgsDateTimeRange(QDateTime(QDate(2019, 1, 1), QTime(), Qt.UTC), + QDateTime(QDate(2020, 2, 28), QTime(), Qt.UTC))) + + p.addMapLayers([r1, r2, r3]) + + range = QgsTemporalUtils.calculateTemporalRangeForProject(p) + self.assertEqual(range.begin(), QDateTime(QDate(2019, 1, 1), QTime(), Qt.UTC)) + self.assertEqual(range.end(), QDateTime(QDate(2020, 7, 31), QTime(), Qt.UTC)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/src/python/test_qgsxmlutils.py b/tests/src/python/test_qgsxmlutils.py index de285e91fa15..6f5ed939bcf1 100644 --- a/tests/src/python/test_qgsxmlutils.py +++ b/tests/src/python/test_qgsxmlutils.py @@ -14,11 +14,19 @@ from qgis.core import (QgsXmlUtils, QgsProperty, QgsGeometry, + QgsFeatureRequest, QgsCoordinateReferenceSystem, QgsProcessingOutputLayerDefinition, + QgsProcessingFeatureSourceDefinition, + QgsRemappingSinkDefinition, + QgsWkbTypes, + QgsCoordinateTransform, + QgsFields, + QgsField, + QgsProject, NULL) -from qgis.PyQt.QtCore import QDateTime, QDate, QTime +from qgis.PyQt.QtCore import QDateTime, QDate, QTime, QVariant from qgis.PyQt.QtXml import QDomDocument from qgis.PyQt.QtGui import QColor @@ -242,6 +250,26 @@ def test_time(self): c = QgsXmlUtils.readVariant(elem) self.assertEqual(c, NULL) + def test_feature_source_definition(self): + """ + Test that QgsProcessingFeatureSourceDefinition values are correctly loaded and written + """ + doc = QDomDocument("properties") + + definition = QgsProcessingFeatureSourceDefinition(QgsProperty.fromValue('my source')) + definition.selectedFeaturesOnly = True + definition.featureLimit = 27 + definition.flags = QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature + definition.geometryCheck = QgsFeatureRequest.GeometrySkipInvalid + + elem = QgsXmlUtils.writeVariant(definition, doc) + c = QgsXmlUtils.readVariant(elem) + self.assertEqual(c.source.staticValue(), 'my source') + self.assertTrue(c.selectedFeaturesOnly) + self.assertEqual(c.featureLimit, 27) + self.assertEqual(c.flags, QgsProcessingFeatureSourceDefinition.FlagCreateIndividualOutputPerInputFeature) + self.assertEqual(c.geometryCheck, QgsFeatureRequest.GeometrySkipInvalid) + def test_output_layer_definition(self): """ Test that QgsProcessingOutputLayerDefinition values are correctly loaded and written @@ -256,6 +284,32 @@ def test_output_layer_definition(self): self.assertEqual(c.sink.staticValue(), 'my sink') self.assertEqual(c.createOptions, {'opt': 1, 'opt2': 2}) + def testRemappingDefinition(self): + fields = QgsFields() + fields.append(QgsField('fldtxt', QVariant.String)) + fields.append(QgsField('fldint', QVariant.Int)) + fields.append(QgsField('fldtxt2', QVariant.String)) + + mapping_def = QgsRemappingSinkDefinition() + mapping_def.setDestinationWkbType(QgsWkbTypes.Point) + mapping_def.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:4326')) + mapping_def.setDestinationCrs(QgsCoordinateReferenceSystem('EPSG:3857')) + mapping_def.setDestinationFields(fields) + mapping_def.addMappedField('fldtxt2', QgsProperty.fromField('fld1')) + mapping_def.addMappedField('fldint', QgsProperty.fromExpression('@myval * fldint')) + + doc = QDomDocument("properties") + elem = QgsXmlUtils.writeVariant(mapping_def, doc) + c = QgsXmlUtils.readVariant(elem) + + self.assertEqual(c.destinationWkbType(), QgsWkbTypes.Point) + self.assertEqual(c.sourceCrs().authid(), 'EPSG:4326') + self.assertEqual(c.destinationCrs().authid(), 'EPSG:3857') + self.assertEqual(c.destinationFields()[0].name(), 'fldtxt') + self.assertEqual(c.destinationFields()[1].name(), 'fldint') + self.assertEqual(c.fieldMap()['fldtxt2'].field(), 'fld1') + self.assertEqual(c.fieldMap()['fldint'].expressionString(), '@myval * fldint') + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/utilities.py b/tests/src/python/utilities.py index a57935f53b74..dcf9639e7f85 100644 --- a/tests/src/python/utilities.py +++ b/tests/src/python/utilities.py @@ -140,22 +140,22 @@ def compareWkt(a, b, tol=0.000001): b0 = b.lower() # remove optional spaces before z/m - r = re.compile("\s+([zm])") + r = re.compile(r"\s+([zm])") a0 = r.sub(r'\1', a0) b0 = r.sub(r'\1', b0) # spaces before brackets are optional - r = re.compile("\s*\(\s*") + r = re.compile(r"\s*\(\s*") a0 = r.sub('(', a0) b0 = r.sub('(', b0) # spaces after brackets are optional - r = re.compile("\s*\)\s*") + r = re.compile(r"\s*\)\s*") a0 = r.sub(')', a0) b0 = r.sub(')', b0) # compare the structure - r0 = re.compile("-?\d+(?:\.\d+)?(?:[eE]\d+)?") - r1 = re.compile("\s*,\s*") + r0 = re.compile(r"-?\d+(?:\.\d+)?(?:[eE]\d+)?") + r1 = re.compile(r"\s*,\s*") a0 = r1.sub(",", r0.sub("#", a0)) b0 = r1.sub(",", r0.sub("#", b0)) if a0 != b0: diff --git a/tests/src/server/testqgsserverquerystringparameter.cpp b/tests/src/server/testqgsserverquerystringparameter.cpp index 0c7619303d35..0daa6a4cce5a 100644 --- a/tests/src/server/testqgsserverquerystringparameter.cpp +++ b/tests/src/server/testqgsserverquerystringparameter.cpp @@ -118,6 +118,7 @@ void TestQgsServerQueryStringParameter::testArguments() QCOMPARE( p.value( ctx ).type(), QVariant::Double ); request.setUrl( QStringLiteral( "http://www.qgis.org/api/?parameter1=a%20string" ) ); QVERIFY_EXCEPTION_THROWN( p.value( ctx ), QgsServerApiBadRequestException ); + QCOMPARE( QString::fromStdString( p.data()["schema"]["type"] ), QString( "number" ) ); // Test list p.mType = QgsServerQueryStringParameter::Type::List; diff --git a/tests/testdata/control_images/atlas/expected_atlas_autoscale1/expected_atlas_autoscale1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_autoscale1/expected_atlas_autoscale1_mask.png index 920776f72f6c..543a81cc105e 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_autoscale1/expected_atlas_autoscale1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_autoscale1/expected_atlas_autoscale1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_autoscale2/expected_atlas_autoscale2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_autoscale2/expected_atlas_autoscale2_mask.png index b8e968eb553b..1bfffa3717f9 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_autoscale2/expected_atlas_autoscale2_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_autoscale2/expected_atlas_autoscale2_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_filtering1/expected_atlas_filtering1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_filtering1/expected_atlas_filtering1_mask.png index cdadd635a627..90c894786afd 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_filtering1/expected_atlas_filtering1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_filtering1/expected_atlas_filtering1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_fixedscale1/expected_atlas_fixedscale1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_fixedscale1/expected_atlas_fixedscale1_mask.png index c9617a146e61..2403b9f1667e 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_fixedscale1/expected_atlas_fixedscale1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_fixedscale1/expected_atlas_fixedscale1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_fixedscale2/expected_atlas_fixedscale2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_fixedscale2/expected_atlas_fixedscale2_mask.png index 285b1ace2081..eef447fd3e32 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_fixedscale2/expected_atlas_fixedscale2_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_fixedscale2/expected_atlas_fixedscale2_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_hiding1/expected_atlas_hiding1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_hiding1/expected_atlas_hiding1_mask.png index d49f55a2246e..7fc56f7039c2 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_hiding1/expected_atlas_hiding1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_hiding1/expected_atlas_hiding1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_hiding2/expected_atlas_hiding2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_hiding2/expected_atlas_hiding2_mask.png index 76dbd6a350ff..34564c15285d 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_hiding2/expected_atlas_hiding2_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_hiding2/expected_atlas_hiding2_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_predefinedscales1/expected_atlas_predefinedscales1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_predefinedscales1/expected_atlas_predefinedscales1_mask.png index 1cffbb42e6f1..2d1d5b8f9134 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_predefinedscales1/expected_atlas_predefinedscales1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_predefinedscales1/expected_atlas_predefinedscales1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_predefinedscales2/expected_atlas_predefinedscales2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_predefinedscales2/expected_atlas_predefinedscales2_mask.png index 7b3023412ed8..58529a853840 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_predefinedscales2/expected_atlas_predefinedscales2_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_predefinedscales2/expected_atlas_predefinedscales2_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_sorting1/expected_atlas_sorting1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_sorting1/expected_atlas_sorting1_mask.png index 364d2b12fb95..9eff3ba18482 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_sorting1/expected_atlas_sorting1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_sorting1/expected_atlas_sorting1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_sorting2/expected_atlas_sorting2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_sorting2/expected_atlas_sorting2_mask.png index 750a3c2deaa7..3bd0a9dbcb82 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_sorting2/expected_atlas_sorting2_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_sorting2/expected_atlas_sorting2_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_two_maps1/expected_atlas_two_maps1_mask.png b/tests/testdata/control_images/atlas/expected_atlas_two_maps1/expected_atlas_two_maps1_mask.png index 56f51f7408e1..23e16947fff8 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_two_maps1/expected_atlas_two_maps1_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_two_maps1/expected_atlas_two_maps1_mask.png differ diff --git a/tests/testdata/control_images/atlas/expected_atlas_two_maps2/expected_atlas_two_maps2_mask.png b/tests/testdata/control_images/atlas/expected_atlas_two_maps2/expected_atlas_two_maps2_mask.png index b2b910d2d1b9..c9c4032fbecd 100644 Binary files a/tests/testdata/control_images/atlas/expected_atlas_two_maps2/expected_atlas_two_maps2_mask.png and b/tests/testdata/control_images/atlas/expected_atlas_two_maps2/expected_atlas_two_maps2_mask.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize/expected_composer_legend_noresize_mask.png b/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize/expected_composer_legend_noresize_mask.png index 91b7251a82c5..20d9d08caa1e 100644 Binary files a/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize/expected_composer_legend_noresize_mask.png and b/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize/expected_composer_legend_noresize_mask.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize_crop/expected_composer_legend_noresize_crop_mask.png b/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize_crop/expected_composer_legend_noresize_crop_mask.png index 721ee5130c41..45ac87ad319f 100644 Binary files a/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize_crop/expected_composer_legend_noresize_crop_mask.png and b/tests/testdata/control_images/composer_legend/expected_composer_legend_noresize_crop/expected_composer_legend_noresize_crop_mask.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composer_legend_size_content/expected_composer_legend_size_content_mask.png b/tests/testdata/control_images/composer_legend/expected_composer_legend_size_content/expected_composer_legend_size_content_mask.png index 0fac4b471cee..3cfdecb354af 100644 Binary files a/tests/testdata/control_images/composer_legend/expected_composer_legend_size_content/expected_composer_legend_size_content_mask.png and b/tests/testdata/control_images/composer_legend/expected_composer_legend_size_content/expected_composer_legend_size_content_mask.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composer_legend_theme/expected_composer_legend_theme.png b/tests/testdata/control_images/composer_legend/expected_composer_legend_theme/expected_composer_legend_theme.png new file mode 100644 index 000000000000..603ed6ef4025 Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composer_legend_theme/expected_composer_legend_theme.png differ diff --git a/tests/testdata/control_images/composer_legend/expected_composer_legend_theme/expected_composer_legend_theme_mask.png b/tests/testdata/control_images/composer_legend/expected_composer_legend_theme/expected_composer_legend_theme_mask.png new file mode 100644 index 000000000000..faeef30d769c Binary files /dev/null and b/tests/testdata/control_images/composer_legend/expected_composer_legend_theme/expected_composer_legend_theme_mask.png differ diff --git a/tests/testdata/control_images/composer_table/expected_composerattributetable_columnwidth/expected_composerattributetable_columnwidth.png b/tests/testdata/control_images/composer_table/expected_composerattributetable_columnwidth/expected_composerattributetable_columnwidth.png index f7a6724b5235..b1bc38a299f5 100644 Binary files a/tests/testdata/control_images/composer_table/expected_composerattributetable_columnwidth/expected_composerattributetable_columnwidth.png and b/tests/testdata/control_images/composer_table/expected_composerattributetable_columnwidth/expected_composerattributetable_columnwidth.png differ diff --git a/tests/testdata/control_images/compositionconverter/expected_importComposerTemplateScaleBar_0/expected_importComposerTemplateScaleBar_0_mask.png b/tests/testdata/control_images/compositionconverter/expected_importComposerTemplateScaleBar_0/expected_importComposerTemplateScaleBar_0_mask.png index 5559a2912ce0..c1d62e4cd170 100644 Binary files a/tests/testdata/control_images/compositionconverter/expected_importComposerTemplateScaleBar_0/expected_importComposerTemplateScaleBar_0_mask.png and b/tests/testdata/control_images/compositionconverter/expected_importComposerTemplateScaleBar_0/expected_importComposerTemplateScaleBar_0_mask.png differ diff --git a/tests/testdata/control_images/compositionconverter/expected_importComposerTemplate_0/expected_importComposerTemplate_0_mask.png b/tests/testdata/control_images/compositionconverter/expected_importComposerTemplate_0/expected_importComposerTemplate_0_mask.png index 5aa00180067b..5d06ed79246b 100644 Binary files a/tests/testdata/control_images/compositionconverter/expected_importComposerTemplate_0/expected_importComposerTemplate_0_mask.png and b/tests/testdata/control_images/compositionconverter/expected_importComposerTemplate_0/expected_importComposerTemplate_0_mask.png differ diff --git a/tests/testdata/control_images/expected_raster_contours/expected_raster_contours.png b/tests/testdata/control_images/expected_raster_contours/expected_raster_contours.png new file mode 100644 index 000000000000..d0f05174e62e Binary files /dev/null and b/tests/testdata/control_images/expected_raster_contours/expected_raster_contours.png differ diff --git a/tests/testdata/control_images/layout_manual_table/expected_manualtable_columnwidth/expected_manualtable_columnwidth_mask.png b/tests/testdata/control_images/layout_manual_table/expected_manualtable_columnwidth/expected_manualtable_columnwidth_mask.png index a3c534ad4141..e9cf1764109b 100644 Binary files a/tests/testdata/control_images/layout_manual_table/expected_manualtable_columnwidth/expected_manualtable_columnwidth_mask.png and b/tests/testdata/control_images/layout_manual_table/expected_manualtable_columnwidth/expected_manualtable_columnwidth_mask.png differ diff --git a/tests/testdata/control_images/layout_manual_table/expected_manualtable_headers/expected_manualtable_headers_mask.png b/tests/testdata/control_images/layout_manual_table/expected_manualtable_headers/expected_manualtable_headers_mask.png index 6ab0c2ce4740..fc9d27f7c966 100644 Binary files a/tests/testdata/control_images/layout_manual_table/expected_manualtable_headers/expected_manualtable_headers_mask.png and b/tests/testdata/control_images/layout_manual_table/expected_manualtable_headers/expected_manualtable_headers_mask.png differ diff --git a/tests/testdata/control_images/layout_manual_table/expected_manualtable_rowheight/expected_manualtable_rowheight_mask.png b/tests/testdata/control_images/layout_manual_table/expected_manualtable_rowheight/expected_manualtable_rowheight_mask.png index fc4e25601e98..8f9fe0b84554 100644 Binary files a/tests/testdata/control_images/layout_manual_table/expected_manualtable_rowheight/expected_manualtable_rowheight_mask.png and b/tests/testdata/control_images/layout_manual_table/expected_manualtable_rowheight/expected_manualtable_rowheight_mask.png differ diff --git a/tests/testdata/control_images/layout_marker/expected_layout_marker_render/expected_layout_marker_render.png b/tests/testdata/control_images/layout_marker/expected_layout_marker_render/expected_layout_marker_render.png new file mode 100644 index 000000000000..cb18c4d635b8 Binary files /dev/null and b/tests/testdata/control_images/layout_marker/expected_layout_marker_render/expected_layout_marker_render.png differ diff --git a/tests/testdata/control_images/layout_marker/expected_layout_marker_render_north/expected_layout_marker_render_north.png b/tests/testdata/control_images/layout_marker/expected_layout_marker_render_north/expected_layout_marker_render_north.png new file mode 100644 index 000000000000..ec88c4cf3cae Binary files /dev/null and b/tests/testdata/control_images/layout_marker/expected_layout_marker_render_north/expected_layout_marker_render_north.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_datadefined/expected_layoutscalebar_datadefined.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_datadefined/expected_layoutscalebar_datadefined.png new file mode 100644 index 000000000000..c60db88bd6cb Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_datadefined/expected_layoutscalebar_datadefined.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png index b7bd14a22275..2df42b3099e4 100644 Binary files a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox/expected_layoutscalebar_doublebox_mask.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_fillsymbol/expected_layoutscalebar_doublebox_fillsymbol.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_fillsymbol/expected_layoutscalebar_doublebox_fillsymbol.png new file mode 100644 index 000000000000..f218c09e8dca Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_fillsymbol/expected_layoutscalebar_doublebox_fillsymbol.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_labelcenteredsegment/expected_layoutscalebar_doublebox_labelcenteredsegment_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_labelcenteredsegment/expected_layoutscalebar_doublebox_labelcenteredsegment_mask.png new file mode 100644 index 000000000000..9c1066240044 Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_labelcenteredsegment/expected_layoutscalebar_doublebox_labelcenteredsegment_mask.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_linesymbol/expected_layoutscalebar_doublebox_linesymbol.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_linesymbol/expected_layoutscalebar_doublebox_linesymbol.png new file mode 100644 index 000000000000..e649e3c851fc Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_doublebox_linesymbol/expected_layoutscalebar_doublebox_linesymbol.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_hollow/expected_layoutscalebar_hollow.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_hollow/expected_layoutscalebar_hollow.png new file mode 100644 index 000000000000..f071643a297c Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_hollow/expected_layoutscalebar_hollow.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numericformat/expected_layoutscalebar_numericformat_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numericformat/expected_layoutscalebar_numericformat_mask.png new file mode 100644 index 000000000000..a52bab6d446b Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_numericformat/expected_layoutscalebar_numericformat_mask.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png index abbcf28048b7..93b44e9bc9e9 100644 Binary files a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox/expected_layoutscalebar_singlebox_mask.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_alpha/expected_layoutscalebar_singlebox_alpha_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_alpha/expected_layoutscalebar_singlebox_alpha_mask.png index cd39e415a7dc..4af03d107b38 100644 Binary files a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_alpha/expected_layoutscalebar_singlebox_alpha_mask.png and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_alpha/expected_layoutscalebar_singlebox_alpha_mask.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_fillsymbol/expected_layoutscalebar_singlebox_fillsymbol.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_fillsymbol/expected_layoutscalebar_singlebox_fillsymbol.png new file mode 100644 index 000000000000..31e881fb2834 Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_fillsymbol/expected_layoutscalebar_singlebox_fillsymbol.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_linesymbol/expected_layoutscalebar_singlebox_linesymbol.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_linesymbol/expected_layoutscalebar_singlebox_linesymbol.png new file mode 100644 index 000000000000..d8f14b0aa558 Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_singlebox_linesymbol/expected_layoutscalebar_singlebox_linesymbol.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_stepped/expected_layoutscalebar_stepped.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_stepped/expected_layoutscalebar_stepped.png new file mode 100644 index 000000000000..6b294616ed9e Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_stepped/expected_layoutscalebar_stepped.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_textformat/expected_layoutscalebar_textformat_mask.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_textformat/expected_layoutscalebar_textformat_mask.png index 1726776ddd02..82866cb4c3a9 100644 Binary files a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_textformat/expected_layoutscalebar_textformat_mask.png and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_textformat/expected_layoutscalebar_textformat_mask.png differ diff --git a/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_tick_linesymbol/expected_layoutscalebar_tick_linesymbol.png b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_tick_linesymbol/expected_layoutscalebar_tick_linesymbol.png new file mode 100644 index 000000000000..c85d638fad2f Binary files /dev/null and b/tests/testdata/control_images/layout_scalebar/expected_layoutscalebar_tick_linesymbol/expected_layoutscalebar_tick_linesymbol.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_line/expected_line.png b/tests/testdata/control_images/legend_patch/expected_line/expected_line.png new file mode 100644 index 000000000000..129d8655d24f Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_line/expected_line.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_line_preserve/expected_line_preserve.png b/tests/testdata/control_images/legend_patch/expected_line_preserve/expected_line_preserve.png new file mode 100644 index 000000000000..0f0a26541ff5 Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_line_preserve/expected_line_preserve.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_marker_multipoint/expected_marker_multipoint.png b/tests/testdata/control_images/legend_patch/expected_marker_multipoint/expected_marker_multipoint.png new file mode 100644 index 000000000000..16cc5a4d026e Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_marker_multipoint/expected_marker_multipoint.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_marker_multipoint_preserve/expected_marker_multipoint_preserve.png b/tests/testdata/control_images/legend_patch/expected_marker_multipoint_preserve/expected_marker_multipoint_preserve.png new file mode 100644 index 000000000000..7030d273d030 Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_marker_multipoint_preserve/expected_marker_multipoint_preserve.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_multiline/expected_multiline.png b/tests/testdata/control_images/legend_patch/expected_multiline/expected_multiline.png new file mode 100644 index 000000000000..18a57f42d222 Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_multiline/expected_multiline.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_multipolygon/expected_multipolygon.png b/tests/testdata/control_images/legend_patch/expected_multipolygon/expected_multipolygon.png new file mode 100644 index 000000000000..73d316b865dc Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_multipolygon/expected_multipolygon.png differ diff --git a/tests/testdata/control_images/legend_patch/expected_polygon/expected_polygon.png b/tests/testdata/control_images/legend_patch/expected_polygon/expected_polygon.png new file mode 100644 index 000000000000..ef3602d4f8b1 Binary files /dev/null and b/tests/testdata/control_images/legend_patch/expected_polygon/expected_polygon.png differ diff --git a/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_dataset_colorRamp/expected_quad_and_triangle_vertex_vector_dataset_colorRamp.png b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_dataset_colorRamp/expected_quad_and_triangle_vertex_vector_dataset_colorRamp.png new file mode 100644 index 000000000000..a7a63e4bb1b3 Binary files /dev/null and b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_dataset_colorRamp/expected_quad_and_triangle_vertex_vector_dataset_colorRamp.png differ diff --git a/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_traces_colorRamp/expected_quad_and_triangle_vertex_vector_traces_colorRamp.png b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_traces_colorRamp/expected_quad_and_triangle_vertex_vector_traces_colorRamp.png new file mode 100644 index 000000000000..d484c0c47fad Binary files /dev/null and b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_traces_colorRamp/expected_quad_and_triangle_vertex_vector_traces_colorRamp.png differ diff --git a/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp/expected_quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp.png b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp/expected_quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp.png new file mode 100644 index 000000000000..3e96f28b6b1d Binary files /dev/null and b/tests/testdata/control_images/mesh/expected_quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp/expected_quad_and_triangle_vertex_vector_user_grid_dataset_streamlines_colorRamp.png differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase1.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase1.tif new file mode 100644 index 000000000000..f63eb789b662 Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase1.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase1.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase1.tif.aux.xml new file mode 100644 index 000000000000..a0b9a48728b9 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase1.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 243 + 147.17196622062 + 85 + 43.961649616584 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase2.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase2.tif new file mode 100644 index 000000000000..aa76e3cab086 Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase2.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase2.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase2.tif.aux.xml new file mode 100644 index 000000000000..18fb9a154cd9 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase2.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 243 + 147.17682604699 + 85 + 43.961614964497 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase3.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase3.tif new file mode 100644 index 000000000000..2ac92894cc6c Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase3.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase3.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase3.tif.aux.xml new file mode 100644 index 000000000000..842654e07bdd --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase3.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 243 + 147.12357797723 + 85 + 43.961975275193 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase4.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase4.tif new file mode 100644 index 000000000000..cbab287f98ae Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase4.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase4.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase4.tif.aux.xml new file mode 100644 index 000000000000..984b3ed63172 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase4.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 240 + 147.14883186518 + 90 + 44.101684164291 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase5.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase5.tif new file mode 100644 index 000000000000..547ddbcf4d3a Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase5.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase5.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase5.tif.aux.xml new file mode 100644 index 000000000000..3dc606a346d7 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase5.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 250 + 152.12271160475 + 90 + 44.024396474557 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase6.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase6.tif new file mode 100644 index 000000000000..8bbbd350403c Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase6.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase6.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase6.tif.aux.xml new file mode 100644 index 000000000000..09427ecb7ee6 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase6.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 240 + 142.26158559939 + 80 + 44.065492747785 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase7.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase7.tif new file mode 100644 index 000000000000..6576c28fde93 Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase7.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase7.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase7.tif.aux.xml new file mode 100644 index 000000000000..0d576a5e6ce8 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase7.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 32110 + 1520.9135802469 + -32766 + 19163.971897002 + + + diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase8.tif b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase8.tif new file mode 100644 index 000000000000..6576c28fde93 Binary files /dev/null and b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase8.tif differ diff --git a/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase8.tif.aux.xml b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase8.tif.aux.xml new file mode 100644 index 000000000000..0d576a5e6ce8 --- /dev/null +++ b/tests/testdata/control_images/roundRasterValues/roundRasterValues_testcase8.tif.aux.xml @@ -0,0 +1,10 @@ + + + + 32110 + 1520.9135802469 + -32766 + 19163.971897002 + + + diff --git a/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds.png b/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds.png index d5beea1ad1b6..f2fd4f10648a 100644 Binary files a/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds.png and b/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds.png differ diff --git a/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds_mask.png b/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds_mask.png deleted file mode 100644 index 000dc25a4f50..000000000000 Binary files a/tests/testdata/control_images/symbol_ellipsemarker/expected_ellipsemarker_bounds/expected_ellipsemarker_bounds_mask.png and /dev/null differ diff --git a/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_datadefinedproperties/expected_fontmarker_datadefinedproperties.png b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_datadefinedproperties/expected_fontmarker_datadefinedproperties.png new file mode 100644 index 000000000000..40a97a15c838 Binary files /dev/null and b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_datadefinedproperties/expected_fontmarker_datadefinedproperties.png differ diff --git a/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_datadefinedproperties/expected_fontmarker_datadefinedproperties_mask.png b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_datadefinedproperties/expected_fontmarker_datadefinedproperties_mask.png new file mode 100644 index 000000000000..edcf8a471aa0 Binary files /dev/null and b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_datadefinedproperties/expected_fontmarker_datadefinedproperties_mask.png differ diff --git a/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_style/expected_fontmarker_style.png b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_style/expected_fontmarker_style.png new file mode 100644 index 000000000000..40a97a15c838 Binary files /dev/null and b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_style/expected_fontmarker_style.png differ diff --git a/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_style/expected_fontmarker_style_mask.png b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_style/expected_fontmarker_style_mask.png new file mode 100644 index 000000000000..edcf8a471aa0 Binary files /dev/null and b/tests/testdata/control_images/symbol_fontmarker/expected_fontmarker_style/expected_fontmarker_style_mask.png differ diff --git a/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_alpha_percentage/default/expected_rasterfill_alpha_percentage.png b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_alpha_percentage/default/expected_rasterfill_alpha_percentage.png new file mode 100644 index 000000000000..06107ca1973d Binary files /dev/null and b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_alpha_percentage/default/expected_rasterfill_alpha_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_offset_percentage/default/expected_rasterfill_offset_percentage.png b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_offset_percentage/default/expected_rasterfill_offset_percentage.png new file mode 100644 index 000000000000..83d7fbce4b2e Binary files /dev/null and b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_offset_percentage/default/expected_rasterfill_offset_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_percentage/default/expected_rasterfill_percentage.png b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_percentage/default/expected_rasterfill_percentage.png new file mode 100644 index 000000000000..a6b570b231a6 Binary files /dev/null and b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_percentage/default/expected_rasterfill_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_viewport_percentage/default/expected_rasterfill_viewport_percentage.png b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_viewport_percentage/default/expected_rasterfill_viewport_percentage.png new file mode 100644 index 000000000000..87008d8da061 Binary files /dev/null and b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_viewport_percentage/default/expected_rasterfill_viewport_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_width_percentage/default/expected_rasterfill_width_percentage.png b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_width_percentage/default/expected_rasterfill_width_percentage.png new file mode 100644 index 000000000000..c9f595e85b2a Binary files /dev/null and b/tests/testdata/control_images/symbol_rasterfill/expected_rasterfill_width_percentage/default/expected_rasterfill_width_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_alpha_percentage/default/expected_rastermarker_alpha_percentage.png b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_alpha_percentage/default/expected_rastermarker_alpha_percentage.png new file mode 100644 index 000000000000..971f9ff75c5f Binary files /dev/null and b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_alpha_percentage/default/expected_rastermarker_alpha_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_anchor_percentage/default/expected_rastermarker_anchor_percentage.png b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_anchor_percentage/default/expected_rastermarker_anchor_percentage.png new file mode 100644 index 000000000000..0d97c923b30d Binary files /dev/null and b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_anchor_percentage/default/expected_rastermarker_anchor_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_fixedaspectratio_percentage/default/expected_rastermarker_fixedaspectratio_percentage.png b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_fixedaspectratio_percentage/default/expected_rastermarker_fixedaspectratio_percentage.png new file mode 100644 index 000000000000..b5a5a1fcbfca Binary files /dev/null and b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_fixedaspectratio_percentage/default/expected_rastermarker_fixedaspectratio_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_offset_percentage/default/expected_rastermarker_offset_percentage.png b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_offset_percentage/default/expected_rastermarker_offset_percentage.png new file mode 100644 index 000000000000..b9a324305ac1 Binary files /dev/null and b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_offset_percentage/default/expected_rastermarker_offset_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_percentage/default/expected_rastermarker_percentage.png b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_percentage/default/expected_rastermarker_percentage.png new file mode 100644 index 000000000000..c372a2fc1fc2 Binary files /dev/null and b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_percentage/default/expected_rastermarker_percentage.png differ diff --git a/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_rotation_percentage/default/expected_rastermarker_rotation_percentage.png b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_rotation_percentage/default/expected_rastermarker_rotation_percentage.png new file mode 100644 index 000000000000..62809d33541b Binary files /dev/null and b/tests/testdata/control_images/symbol_rastermarker/expected_rastermarker_rotation_percentage/default/expected_rastermarker_rotation_percentage.png differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds.png index 0fa0ea3b8965..66930f5e1fab 100644 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds.png and b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds.png differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds_mask.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds_mask.png deleted file mode 100644 index 968e67d98039..000000000000 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_bounds/expected_simplemarker_bounds_mask.png and /dev/null differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset.png index 980b80f02fda..63ccc7311354 100644 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset.png and b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset.png differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset_mask.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset_mask.png deleted file mode 100644 index 75b1e9ad79aa..000000000000 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsoffset/expected_simplemarker_boundsoffset_mask.png and /dev/null differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation.png index a5e38c208a39..43355b5187d2 100644 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation.png and b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation.png differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation_mask.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation_mask.png deleted file mode 100644 index 29d3d936b480..000000000000 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotation/expected_simplemarker_boundsrotation_mask.png and /dev/null differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset.png index ebd50f779a25..1f1307e90cb9 100644 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset.png and b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset.png differ diff --git a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset_mask.png b/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset_mask.png deleted file mode 100644 index 989897157a4a..000000000000 Binary files a/tests/testdata/control_images/symbol_simplemarker/expected_simplemarker_boundsrotationoffset/expected_simplemarker_boundsrotationoffset_mask.png and /dev/null differ diff --git a/tests/testdata/control_images/vector_tile/expected_render_test_basic/expected_render_test_basic.png b/tests/testdata/control_images/vector_tile/expected_render_test_basic/expected_render_test_basic.png new file mode 100644 index 000000000000..13e0421ed2b0 Binary files /dev/null and b/tests/testdata/control_images/vector_tile/expected_render_test_basic/expected_render_test_basic.png differ diff --git a/tests/testdata/invalidgeometries.gfs b/tests/testdata/invalidgeometries.gfs new file mode 100644 index 000000000000..cb490f0326f7 --- /dev/null +++ b/tests/testdata/invalidgeometries.gfs @@ -0,0 +1,15 @@ + + + invalidgeometries + invalidgeometries + 3 + EPSG:4326 + + 1 + 122.17632 + 122.19038 + -8.60636 + -8.59579 + + + diff --git a/tests/testdata/invalidgeometries.gml b/tests/testdata/invalidgeometries.gml new file mode 100644 index 000000000000..b8b426b28656 --- /dev/null +++ b/tests/testdata/invalidgeometries.gml @@ -0,0 +1,19 @@ + + + + + 122.1763203897661-8.606355050987483 + 122.1903768142314-8.595792419886385 + + + + + + 122.176320389766076,-8.601236237453875 122.179976685147238,-8.597742444089665 122.184526741621553,-8.598392452157425 122.188508041036584,-8.602129998547044 122.186395514816354,-8.606355050987483 122.183795482545321,-8.602617504597864 122.188508041036584,-8.598311201148954 122.190376814231385,-8.595792419886385 122.190376814231385,-8.595792419886385 122.176320389766076,-8.601236237453875 + + + diff --git a/tests/testdata/layouts/scalebar_old_datadefined.qgs b/tests/testdata/layouts/scalebar_old_datadefined.qgs new file mode 100644 index 000000000000..79ab251188bf --- /dev/null +++ b/tests/testdata/layouts/scalebar_old_datadefined.qgs @@ -0,0 +1,288 @@ + + + + + + + + + + GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + + + + + + + + + + degrees + + -1 + -1 + 1 + 1 + + 0 + + + GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]],AUTHORITY["EPSG","4326"]] + +proj=longlat +datum=WGS84 +no_defs + 3452 + 4326 + EPSG:4326 + WGS 84 + longlat + WGS84 + true + + + 0 + + + + + + + + + + + 255 + 255 + 255 + 255 + 0 + 255 + 255 + + + false + + + WGS84 + + + m2 + meters + + + 5 + 2.5 + false + false + 1 + 0 + false + false + true + 0 + 255,0,0,255 + + + false + + + true + 2 + + + 1 + + + + + + + + + + + + + Nyall Dawson + 2020-03-21T15:09:09 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/testdata/provider/postgresraster/bug_34823_pg_raster.sql b/tests/testdata/provider/postgresraster/bug_34823_pg_raster.sql index 25dc8ffbd08c..fb34858e395b 100644 --- a/tests/testdata/provider/postgresraster/bug_34823_pg_raster.sql +++ b/tests/testdata/provider/postgresraster/bug_34823_pg_raster.sql @@ -1,6 +1,8 @@ CREATE SCHEMA IF NOT EXISTS idro; +DROP TABLE IF EXISTS idro.cosmo_i5_snow CASCADE; + -- -- Name: cosmo_i5_snow; Type: TABLE; Schema: idro; Owner: - -- diff --git a/tests/testdata/provider/postgresraster/raster_3035_no_constraints.sql b/tests/testdata/provider/postgresraster/raster_3035_no_constraints.sql index b7f7c8ada04b..d8cfbe2b16b8 100644 --- a/tests/testdata/provider/postgresraster/raster_3035_no_constraints.sql +++ b/tests/testdata/provider/postgresraster/raster_3035_no_constraints.sql @@ -2,6 +2,9 @@ -- -- in-db float 32 raster with no constraints -- + +DROP TABLE IF EXISTS "public"."raster_3035_no_constraints" CASCADE; + CREATE TABLE "raster_3035_no_constraints" ("rid" serial PRIMARY KEY,"rast" raster); INSERT INTO "raster_3035_no_constraints" ("rast") VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000060005004A003C1CC66A610843880B0E431CC2194306342543B7633C43861858436E0A1143BBAD194359612743A12B334317BE4343DECE59432B621B43F0E42843132B3843AC824043E6CF48436E465A435C4D2D430FA63D43F87A4843B5494A4349454E4374F35B43906E41433AB54C43B056504358575243B1EC574322615F43'::raster); diff --git a/tests/testdata/provider/postgresraster/raster_3035_tiled_composite_pk.sql b/tests/testdata/provider/postgresraster/raster_3035_tiled_composite_pk.sql index ad60fcf398ec..6b11cdd4e31f 100644 --- a/tests/testdata/provider/postgresraster/raster_3035_tiled_composite_pk.sql +++ b/tests/testdata/provider/postgresraster/raster_3035_tiled_composite_pk.sql @@ -2,6 +2,7 @@ -- -- in-db float32 tiled raster with composite PK -- +DROP TABLE IF EXISTS "public"."raster_3035_tiled_composite_pk" CASCADE; CREATE TABLE "public"."raster_3035_tiled_composite_pk" ("pk1" INT NOT NULL, "pk2" INT NOT NULL, "rast" raster,"category" text, CONSTRAINT unique_pk UNIQUE ("pk1", "pk2") diff --git a/tests/testdata/provider/postgresraster/raster_3035_tiled_no_overviews.sql b/tests/testdata/provider/postgresraster/raster_3035_tiled_no_overviews.sql index 4b4ce432a63d..7ff72612351c 100644 --- a/tests/testdata/provider/postgresraster/raster_3035_tiled_no_overviews.sql +++ b/tests/testdata/provider/postgresraster/raster_3035_tiled_no_overviews.sql @@ -2,6 +2,10 @@ -- -- in-db float32 tiled raster with no overviews (for WHERE testing) -- + +DROP TABLE IF EXISTS "public"."raster_3035_tiled_no_overviews" CASCADE; + + CREATE TABLE "public"."raster_3035_tiled_no_overviews" ("rid" serial PRIMARY KEY,"rast" raster,"category" text); INSERT INTO "public"."raster_3035_tiled_no_overviews" ("rast","category") VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A1143BBAD1943'::raster,'cat1'); INSERT INTO "public"."raster_3035_tiled_no_overviews" ("rast","category") VALUES ('0100000100000000000000394000000000000039C000000000F2204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC61CC219430634254359612743A12B3343'::raster,'cat1'); diff --git a/tests/testdata/provider/postgresraster/raster_3035_tiled_no_pk.sql b/tests/testdata/provider/postgresraster/raster_3035_tiled_no_pk.sql index 04b028ddb16c..5666c502693e 100644 --- a/tests/testdata/provider/postgresraster/raster_3035_tiled_no_pk.sql +++ b/tests/testdata/provider/postgresraster/raster_3035_tiled_no_pk.sql @@ -3,6 +3,9 @@ -- in-db float32 tiled raster with no overviews and no PK -- +DROP TABLE IF EXISTS "public"."raster_3035_tiled_no_pk" CASCADE; + + CREATE TABLE "public"."raster_3035_tiled_no_pk" ("rast" raster,"category" text); INSERT INTO "public"."raster_3035_tiled_no_pk" ("rast","category") VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A1143BBAD1943'::raster,'cat1'); INSERT INTO "public"."raster_3035_tiled_no_pk" ("rast","category") VALUES ('0100000100000000000000394000000000000039C000000000F2204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC61CC219430634254359612743A12B3343'::raster,'cat1'); diff --git a/tests/testdata/provider/postgresraster/raster_3035_untiled_multiple_rows.sql b/tests/testdata/provider/postgresraster/raster_3035_untiled_multiple_rows.sql index 9bc9d9312ae6..a0b9d661e313 100644 --- a/tests/testdata/provider/postgresraster/raster_3035_untiled_multiple_rows.sql +++ b/tests/testdata/provider/postgresraster/raster_3035_untiled_multiple_rows.sql @@ -1,19 +1,25 @@ -- -- in-db float32 untiled raster with multiple rows +-- also used to test temporal capabilities -- -CREATE TABLE "public"."raster_3035_untiled_multiple_rows" ("pk" SERIAL PRIMARY KEY, "rast" raster +DROP TABLE IF EXISTS "public"."raster_3035_untiled_multiple_rows"; + +CREATE TABLE "public"."raster_3035_untiled_multiple_rows" ( + "pk" SERIAL PRIMARY KEY, + "rast" raster, + "data" timestamp, + "data_text" varchar ); -INSERT INTO "public"."raster_3035_untiled_multiple_rows" ("rast", "pk") -VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A1143BBAD1943'::raster, 1); -INSERT INTO "public"."raster_3035_untiled_multiple_rows" ("rast", "pk") -VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A2143BBAD2943'::raster, 2); +INSERT INTO "public"."raster_3035_untiled_multiple_rows" ("rast", "pk", "data", "data_text") +VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A1143BBAD1943'::raster, 1, '2020-04-01', '2020-04-01'); +INSERT INTO "public"."raster_3035_untiled_multiple_rows" ("rast", "pk", "data", "data_text") +VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A2143BBAD2943'::raster, 2, '2020-04-05', '2020-04-05'); -- offset row -INSERT INTO "public"."raster_3035_untiled_multiple_rows" ("rast", "pk") -VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A2143BBAD2943'::raster, 3); +INSERT INTO "public"."raster_3035_untiled_multiple_rows" ("rast", "pk", "data", "data_text") +VALUES ('0100000100000000000000394000000000000039C000000000D9204F41000000008F8B424100000000000000000000000000000000DB0B0000020002004A003C1CC66A610843880B0E436E0A2143BBAD0942'::raster, 3, '2020-04-06', '2020-04-06'); CREATE INDEX ON "public"."raster_3035_untiled_multiple_rows" USING gist (st_convexhull("rast")); ANALYZE "public"."raster_3035_untiled_multiple_rows"; SELECT AddRasterConstraints('public','raster_3035_untiled_multiple_rows','rast',TRUE,TRUE,TRUE,TRUE,TRUE,TRUE,FALSE,TRUE,TRUE,TRUE,TRUE,TRUE); - diff --git a/tests/testdata/provider/postgresraster/raster_tiled_3035.sql b/tests/testdata/provider/postgresraster/raster_tiled_3035.sql index 89d79d14a273..a513ce4eadc8 100644 --- a/tests/testdata/provider/postgresraster/raster_tiled_3035.sql +++ b/tests/testdata/provider/postgresraster/raster_tiled_3035.sql @@ -1,6 +1,11 @@ -- -- in-db 1 band float 32 raster -- + +DROP TABLE IF EXISTS "public"."raster_tiled_3035" CASCADE; +DROP TABLE IF EXISTS "o_2_raster_tiled_3035" CASCADE; +DROP TABLE IF EXISTS "o_4_raster_tiled_3035" CASCADE; + CREATE TABLE "raster_tiled_3035" ("rid" serial PRIMARY KEY,"rast" raster,"filename" text); CREATE TABLE "o_2_raster_tiled_3035" ("rid" serial PRIMARY KEY,"rast" raster,"filename" text); CREATE TABLE "o_4_raster_tiled_3035" ("rid" serial PRIMARY KEY,"rast" raster,"filename" text); diff --git a/tests/testdata/qgis_server/api/test_wfs3_api_project.json b/tests/testdata/qgis_server/api/test_wfs3_api_project.json index 0c93a31929a9..f9911832a59b 100644 --- a/tests/testdata/qgis_server/api/test_wfs3_api_project.json +++ b/tests/testdata/qgis_server/api/test_wfs3_api_project.json @@ -782,40 +782,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "post": { "description": "Adds a new feature to the collection {collectionId}", "operationId": "getFeatures_testlayer_èé_2_a5f61891_b949_43e3_ad30_84013fc922de_POST", - "responses": [ - [ - "201", - { - "description": "A new feature was successfully added to the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "A new feature was successfully added to the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Adds a new feature to the collection {collectionId}", "tags": [ "edit", @@ -827,40 +819,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "delete": { "description": "Deletes the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeatureDELETE", - "responses": [ - [ - "201", - { - "description": "The feature was successfully deleted from the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "The feature was successfully deleted from the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Deletes the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -915,40 +899,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "patch": { "description": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit" @@ -957,40 +933,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "put": { "description": "Replaces the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Replaces the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -1151,40 +1119,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "post": { "description": "Adds a new feature to the collection {collectionId}", "operationId": "getFeatures_testlayer_èé_cf86cf11_222f_4b62_929c_12cfc82b9774_POST", - "responses": [ - [ - "201", - { - "description": "A new feature was successfully added to the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "A new feature was successfully added to the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Adds a new feature to the collection {collectionId}", "tags": [ "edit", @@ -1196,40 +1156,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "delete": { "description": "Deletes the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeatureDELETE", - "responses": [ - [ - "201", - { - "description": "The feature was successfully deleted from the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "The feature was successfully deleted from the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Deletes the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -1284,40 +1236,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "patch": { "description": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit" @@ -1326,40 +1270,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "put": { "description": "Replaces the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Replaces the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -1520,40 +1456,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "post": { "description": "Adds a new feature to the collection {collectionId}", "operationId": "getFeatures_testlayer_c0988fd7_97ca_451d_adbc_37ad6d10583a_POST", - "responses": [ - [ - "201", - { - "description": "A new feature was successfully added to the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "A new feature was successfully added to the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Adds a new feature to the collection {collectionId}", "tags": [ "edit", @@ -1565,40 +1493,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "delete": { "description": "Deletes the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeatureDELETE", - "responses": [ - [ - "201", - { - "description": "The feature was successfully deleted from the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "The feature was successfully deleted from the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Deletes the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -1653,40 +1573,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "patch": { "description": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit" @@ -1695,40 +1607,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "put": { "description": "Replaces the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Replaces the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -1889,40 +1793,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "post": { "description": "Adds a new feature to the collection {collectionId}", "operationId": "getFeatures_testlayer20150528120452665_POST", - "responses": [ - [ - "201", - { - "description": "A new feature was successfully added to the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "A new feature was successfully added to the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Adds a new feature to the collection {collectionId}", "tags": [ "edit", @@ -1934,40 +1830,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "delete": { "description": "Deletes the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeatureDELETE", - "responses": [ - [ - "201", - { - "description": "The feature was successfully deleted from the collection" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "201": { + "description": "The feature was successfully deleted from the collection" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Deletes the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -2022,40 +1910,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "patch": { "description": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Changes attributes of feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit" @@ -2064,40 +1944,32 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 "put": { "description": "Replaces the feature with ID {featureId} in the collection {collectionId}", "operationId": "getFeaturePUT", - "responses": [ - [ - "200", - { - "description": "The feature was successfully updated" - }, - "403", - { - "description": "Forbidden: the operation requested was not authorized" - }, - "500", - { - "description": "Posted data could not be parsed correctly or another error occurred" - } - ], - [ - "default", - { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/exception" - } - }, - "text/html": { - "schema": { - "type": "string" - } + "responses": { + "200": { + "description": "The feature was successfully updated" + }, + "403": { + "description": "Forbidden: the operation requested was not authorized" + }, + "500": { + "description": "Posted data could not be parsed correctly or another error occurred" + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/exception" } }, - "description": "An error occurred." - } - ] - ], + "text/html": { + "schema": { + "type": "string" + } + } + }, + "description": "An error occurred." + } + }, "summary": "Replaces the feature with ID {featureId} in the collection {collectionId}", "tags": [ "edit", @@ -2164,4 +2036,4 @@ Content-Type: application/vnd.oai.openapi+json;version=3.0 } ], "timeStamp": "2019-07-05T12:27:07Z" -} +} \ No newline at end of file diff --git a/tests/testdata/qgis_server/getprojectsettings.txt b/tests/testdata/qgis_server/getprojectsettings.txt index dcfb766613af..955834a47061 100644 --- a/tests/testdata/qgis_server/getprojectsettings.txt +++ b/tests/testdata/qgis_server/getprojectsettings.txt @@ -141,7 +141,7 @@ Content-Type: text/xml; charset=utf-8 infoMapAccessService QGIS Test Project - + layer_with_short_name A Layer with a short name A Layer with an abstract @@ -171,7 +171,7 @@ Content-Type: text/xml; charset=utf-8 - + landsat landsat CRS:84 @@ -195,7 +195,7 @@ Content-Type: text/xml; charset=utf-8 landsat - + testlayer èé A test vector layer A test vector layer with unicode òà @@ -225,7 +225,7 @@ Content-Type: text/xml; charset=utf-8 - + fields_alias A test vector layer A test vector layer with unicode òà @@ -255,7 +255,7 @@ Content-Type: text/xml; charset=utf-8 - + exclude_attribute A test vector layer A test vector layer with unicode òà @@ -284,7 +284,7 @@ Content-Type: text/xml; charset=utf-8 - + group_name Group title Group abstract @@ -300,7 +300,7 @@ Content-Type: text/xml; charset=utf-8 groupwithshortname - + testlayer2 testlayer2 CRS:84 @@ -330,7 +330,7 @@ Content-Type: text/xml; charset=utf-8 - + groupwithoutshortname groupwithoutshortname CRS:84 @@ -345,7 +345,7 @@ Content-Type: text/xml; charset=utf-8 groupwithoutshortname - + testlayer3 testlayer3 CRS:84 diff --git a/tests/testdata/qgis_server/getprojectsettings_opacity.txt b/tests/testdata/qgis_server/getprojectsettings_opacity.txt index 1d3bfa6b2a93..6d7b19183c6d 100644 --- a/tests/testdata/qgis_server/getprojectsettings_opacity.txt +++ b/tests/testdata/qgis_server/getprojectsettings_opacity.txt @@ -141,7 +141,7 @@ Content-Type: text/xml; charset=utf-8 infoMapAccessService QGIS Test Project - + layer_with_short_name A Layer with a short name A Layer with an abstract @@ -171,7 +171,7 @@ Content-Type: text/xml; charset=utf-8 - + landsat landsat CRS:84 @@ -195,7 +195,7 @@ Content-Type: text/xml; charset=utf-8 landsat - + testlayer èé A test vector layer A test vector layer with unicode òà @@ -225,7 +225,7 @@ Content-Type: text/xml; charset=utf-8 - + fields_alias A test vector layer A test vector layer with unicode òà @@ -255,7 +255,7 @@ Content-Type: text/xml; charset=utf-8 - + exclude_attribute A test vector layer A test vector layer with unicode òà @@ -284,7 +284,7 @@ Content-Type: text/xml; charset=utf-8 - + group_name Group title Group abstract @@ -300,7 +300,7 @@ Content-Type: text/xml; charset=utf-8 groupwithshortname - + testlayer2 testlayer2 CRS:84 @@ -330,7 +330,7 @@ Content-Type: text/xml; charset=utf-8 - + groupwithoutshortname groupwithoutshortname CRS:84 @@ -345,7 +345,7 @@ Content-Type: text/xml; charset=utf-8 groupwithoutshortname - + testlayer3 testlayer3 CRS:84 diff --git a/tests/testdata/qgis_server/test_project_wms_grouped_layers_test.gpkg b/tests/testdata/qgis_server/test_project_wms_grouped_layers_test.gpkg deleted file mode 100644 index dca210ebfe44..000000000000 Binary files a/tests/testdata/qgis_server/test_project_wms_grouped_layers_test.gpkg and /dev/null differ diff --git a/tests/testdata/raster/dem.tif b/tests/testdata/raster/dem.tif new file mode 100644 index 000000000000..efa17dd827e1 Binary files /dev/null and b/tests/testdata/raster/dem.tif differ diff --git a/tests/testdata/vector_tile/0-0-0.pbf b/tests/testdata/vector_tile/0-0-0.pbf new file mode 100644 index 000000000000..3a2e0450500e Binary files /dev/null and b/tests/testdata/vector_tile/0-0-0.pbf differ diff --git a/tests/testdata/vector_tile/1-0-0.pbf b/tests/testdata/vector_tile/1-0-0.pbf new file mode 100644 index 000000000000..876a0b7ddf59 Binary files /dev/null and b/tests/testdata/vector_tile/1-0-0.pbf differ diff --git a/tests/testdata/vector_tile/1-0-1.pbf b/tests/testdata/vector_tile/1-0-1.pbf new file mode 100644 index 000000000000..3c4f544d9874 Binary files /dev/null and b/tests/testdata/vector_tile/1-0-1.pbf differ diff --git a/tests/testdata/vector_tile/1-1-0.pbf b/tests/testdata/vector_tile/1-1-0.pbf new file mode 100644 index 000000000000..cfcc8aedc45a Binary files /dev/null and b/tests/testdata/vector_tile/1-1-0.pbf differ diff --git a/tests/testdata/vector_tile/1-1-1.pbf b/tests/testdata/vector_tile/1-1-1.pbf new file mode 100644 index 000000000000..896ef86722a6 Binary files /dev/null and b/tests/testdata/vector_tile/1-1-1.pbf differ diff --git a/tests/testdata/vector_tile/README.md b/tests/testdata/vector_tile/README.md new file mode 100644 index 000000000000..81c38e58dcbe --- /dev/null +++ b/tests/testdata/vector_tile/README.md @@ -0,0 +1,15 @@ + +# Vector Tiles Test Data + +Downloaded from https://api.maptiler.com/tiles/v3/{z}/{x}/{y}.pbf?key=XXX +(where XXX stands for a key that can be obtained when registered at MapTiler: https://www.maptiler.com/) + +## License + +These tiles are coming from OpenMapTiles project, under the "Free data" terms of use: https://openmaptiles.com/terms/ + +The FREE tiles are legally usable for: + +- open-source and open-data community project websites +- non-commercial personal projects +- evaluation and education purposes